mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Anotha one!
This commit is contained in:
1 parent
dc0cda699d
commit
04d3ef0e92
35 files changed
+7577
-2002
No files matched your search
@@ -75,9 +75,6 @@
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] div#section-content {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 0.75em;
|
||||
max-height: calc(90vh - 19em);
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -245,6 +242,7 @@ span.listItemBodyText.secondary.seasonal-tag {
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] #global-settings-content .hsc-settings-grid-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] #global-settings-content .hsc-settings-grid-3 {
|
||||
@@ -336,3 +334,11 @@ span.listItemBodyText.secondary.seasonal-tag {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] .hsc-settings-span-2 {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] .hsc-settings-grid .hsc-settings-span-2 .kefin-toggle-card {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -43,6 +43,10 @@
|
||||
on: 'Allows users to create pinned sections on their Home Screen from library items by adding a "Pin To Home" button in the item context menu.',
|
||||
off: 'Users cannot pin items to their Home Screen.'
|
||||
},
|
||||
'userHome-pairNativeHomeSections': {
|
||||
on: 'Sections with a matching Native Jellyfin section are automatically kept in sync with your KefinTweaks Home Screen.',
|
||||
off: 'KefinTweaks will disable all native Jellyfin home screen sections in the native Jellyfin User Preferences. When pairing is disabled, the official Jellyfin Android TV and Roku clients will have no sections rendered on the home screen. You should only disable this when you do not use either of those clients. Disabling will have a performance benefit when loading the home screen.'
|
||||
},
|
||||
'home-fadeInSections': {
|
||||
on: 'Sections will be hidden when they are rendered and will fade up and in as they enter the window when you scroll.',
|
||||
off: 'Sections will be rendered normally without any fade in animation.'
|
||||
@@ -62,6 +66,10 @@
|
||||
'home-ensureThumbsForPopularTVNetworks': {
|
||||
on: 'Only Studios with a valid image will be rendered in Popular Studios sections.',
|
||||
off: 'Studios without any image may be rendered in Popular Studios sections. You can enable the Optional CSS Module "Studio Thumbnails" to overlay a background with the Studio Name on items without an image.'
|
||||
},
|
||||
'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.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,8 +79,8 @@
|
||||
off: 'No Discovery Sections will be generated.'
|
||||
},
|
||||
'discovery-infiniteScroll': {
|
||||
on: 'Discovery Sections will render automatically when the user scrolls near the bottom of the page.',
|
||||
off: 'Discovery Sections will only be rendered when the user presses the "Load More" button.'
|
||||
on: 'Discovery Sections will render automatically when the user scrolls near the bottom of the page (one group per scroll; scroll again for more).',
|
||||
off: 'Show “Discover More” at the bottom. Scroll again while at the bottom to load the next discovery group.'
|
||||
},
|
||||
'discovery-renderSpotlightAboveMatching': {
|
||||
on: 'Top Rated Genre/Studio and regular Genre/Studio sections will appear next to each other',
|
||||
@@ -113,7 +121,43 @@
|
||||
let currentGlobalSettingsSubTab = 'general'; // Track active sub-tab in Global Settings (general, spotlight, discovery, cache)
|
||||
let globalSettingsSaveTimer = null;
|
||||
const UPDATE_ALL_CONFIRM_MODAL_ID = 'kefin-homescreen-config-update-all-confirm';
|
||||
const RESTORE_DEFAULTS_MODAL_ID = 'kefin-homescreen-config-restore-defaults';
|
||||
const COMMUNITY_IMPORT_MODAL_ID = 'kefin-homescreen-community-import';
|
||||
const MAX_PEOPLE_COUNT_OPTIONS = [100, 250, 500, 1000, 2500, 5000];
|
||||
const RESTORE_RETAIN_TOGGLE_DESCRIPTIONS = {
|
||||
'restore-retain-status': {
|
||||
on: 'Current Default Status and User Configuration values will be unchanged.',
|
||||
off: 'Default Status and User Configuration will be restored to their defaults.'
|
||||
},
|
||||
'restore-retain-name-caption': {
|
||||
on: 'Current Section Name and Caption values will be unchanged.',
|
||||
off: 'Section Name and Caption will be restored to their defaults.'
|
||||
},
|
||||
'restore-retain-appearance': {
|
||||
on: 'Current Section Appearance settings will be unchanged.',
|
||||
off: 'Section Appearance settings will be restored to their defaults.'
|
||||
},
|
||||
'restore-retain-advanced': {
|
||||
on: 'Current Section Advanced Options will be unchanged.',
|
||||
off: 'Section Advanced Options will be restored to their defaults.'
|
||||
},
|
||||
'restore-retain-customs': {
|
||||
on: 'Current Custom Sections will be unchanged.',
|
||||
off: 'All custom sections will be removed.'
|
||||
}
|
||||
};
|
||||
const RESTORE_APPEARANCE_KEYS = [
|
||||
'renderMode', 'cardFormat', 'spotlight', 'spotlightConfig',
|
||||
'borderStyle', 'borderColor',
|
||||
'hideCardTitles', 'cardTitleVisibility',
|
||||
'cardTitlePosition', 'cardTitleCapitalization', 'cardTitleFontFamily',
|
||||
'cardTitleFontSize', 'cardTitleColor',
|
||||
'itemsLayout', 'useGaplessCards', 'sectionCssClass', 'hideName'
|
||||
];
|
||||
const RESTORE_ADVANCED_KEYS = [
|
||||
'ttl', 'sortBy', 'sortOrder', 'sortOrderDirection',
|
||||
'minimumItems', 'itemLimit', 'flattenSeries', 'limitBeforeSort', 'hideWatched'
|
||||
];
|
||||
const TTL_UNIT_OPTIONS = [
|
||||
{ value: 'seconds', ms: 1000 },
|
||||
{ value: 'minutes', ms: 60000 },
|
||||
@@ -155,28 +199,31 @@
|
||||
return flattened;
|
||||
}
|
||||
|
||||
const CONTINUE_WATCHING_SECTION_IDS = new Set([
|
||||
'continueWatching',
|
||||
'nextUp',
|
||||
'continueWatchingAndNextUp'
|
||||
]);
|
||||
const POPULAR_STUDIO_SECTION_IDS = new Set(['popularTVStudios', 'popularMovieStudios', 'popularStudios']);
|
||||
|
||||
/**
|
||||
* Clear MERGE_NEXT_UP-era hidden flags so CW / Next Up / combined are independent sections.
|
||||
* Sync HOME_SETTINGS.ensureThumbsForPopularTVNetworks onto popular studio section queryOptions.
|
||||
* When on: ImageTypes ['Thumb'] (API + postProcessItems filter). When off: remove ImageTypes.
|
||||
*/
|
||||
function normalizeContinueWatchingVisibility(config) {
|
||||
function syncEnsureThumbsOnPopularStudios(config) {
|
||||
if (!config) return;
|
||||
const groupKeys = ['HOME_SECTION_GROUPS', 'SEASONAL_SECTION_GROUPS', 'DISCOVERY_SECTION_GROUPS', 'CUSTOM_SECTION_GROUPS'];
|
||||
groupKeys.forEach((key) => {
|
||||
(config[key] || []).forEach((group) => {
|
||||
(group.sections || []).forEach((section) => {
|
||||
if (section?.id && CONTINUE_WATCHING_SECTION_IDS.has(section.id)) {
|
||||
section.hidden = false;
|
||||
const ensureThumbs = config.HOME_SETTINGS?.ensureThumbsForPopularTVNetworks === true;
|
||||
const groupKeys = ['HOME_SECTION_GROUPS', 'SEASONAL_SECTION_GROUPS', 'CUSTOM_SECTION_GROUPS', 'DISCOVERY_SECTION_GROUPS'];
|
||||
for (const key of groupKeys) {
|
||||
const sections = flattenSectionGroups(config[key] || []);
|
||||
for (const section of sections) {
|
||||
if (!section || !POPULAR_STUDIO_SECTION_IDS.has(section.id) || !Array.isArray(section.queries)) continue;
|
||||
for (const query of section.queries) {
|
||||
if (!query) continue;
|
||||
if (ensureThumbs) {
|
||||
query.queryOptions = { ...(query.queryOptions || {}), ImageTypes: ['Thumb'] };
|
||||
} else if (query.queryOptions && Object.prototype.hasOwnProperty.call(query.queryOptions, 'ImageTypes')) {
|
||||
const { ImageTypes, ...rest } = query.queryOptions;
|
||||
query.queryOptions = rest;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
delete config.MERGE_NEXT_UP;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -951,10 +998,19 @@
|
||||
CACHE: { ...defaults.CACHE, ...(existingConfig.CACHE || {}) },
|
||||
SPOTLIGHT_SETTINGS: { ...defaults.SPOTLIGHT_SETTINGS, ...(existingConfig.SPOTLIGHT_SETTINGS || {}) },
|
||||
HOME_SETTINGS: { ...defaults.HOME_SETTINGS, ...(existingConfig.HOME_SETTINGS || {}) },
|
||||
USER_HOME_SCREEN_SETTINGS: { ...defaults.USER_HOME_SCREEN_SETTINGS, ...(existingConfig.USER_HOME_SCREEN_SETTINGS || {}) }
|
||||
USER_HOME_SCREEN_SETTINGS: { ...defaults.USER_HOME_SCREEN_SETTINGS, ...(existingConfig.USER_HOME_SCREEN_SETTINGS || {}) },
|
||||
LIBRARY_CACHE: { ...defaults.LIBRARY_CACHE, ...(existingConfig.LIBRARY_CACHE || {}) }
|
||||
};
|
||||
|
||||
normalizeContinueWatchingVisibility(mergedConfig);
|
||||
// Soft-migrate legacy DISCOVERY_SETTINGS.minPeopleAppearances → HOME_SETTINGS mins
|
||||
const existingHome = existingConfig.HOME_SETTINGS || {};
|
||||
const legacyPeopleMin = existingConfig.DISCOVERY_SETTINGS?.minPeopleAppearances;
|
||||
if (legacyPeopleMin != null && existingHome.minPeopleAppearancesTotal == null) {
|
||||
mergedConfig.HOME_SETTINGS.minPeopleAppearancesTotal = legacyPeopleMin;
|
||||
mergedConfig.HOME_SETTINGS.minPeopleAppearancesMovies = legacyPeopleMin;
|
||||
mergedConfig.HOME_SETTINGS.minPeopleAppearancesSeries = legacyPeopleMin;
|
||||
mergedConfig.HOME_SETTINGS.minPeopleAppearancesEpisodes = legacyPeopleMin;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1547,11 +1603,13 @@
|
||||
USER_HOME_SCREEN_SETTINGS: {
|
||||
...(savedHomeScreenConfig.USER_HOME_SCREEN_SETTINGS || {}),
|
||||
...(config.USER_HOME_SCREEN_SETTINGS || {})
|
||||
},
|
||||
LIBRARY_CACHE: {
|
||||
...(savedHomeScreenConfig.LIBRARY_CACHE || {}),
|
||||
...(config.LIBRARY_CACHE || {})
|
||||
}
|
||||
};
|
||||
|
||||
normalizeContinueWatchingVisibility(mergedHomeScreenConfig);
|
||||
|
||||
// Empty values (= blank CustomPrefs segments) must not persist to JS Injector
|
||||
omitEmptyPropsFromGroups(mergedHomeScreenConfig.HOME_SECTION_GROUPS);
|
||||
omitEmptyPropsFromGroups(mergedHomeScreenConfig.SEASONAL_SECTION_GROUPS);
|
||||
@@ -1761,7 +1819,7 @@
|
||||
const sectionTypes = [...new Set(sections.map(s => s.sectionType))];
|
||||
|
||||
return `
|
||||
<div class="section-group" data-group-id="${groupId}" data-group-name="${group.name || 'Unnamed Group'}" style="margin-bottom: 1.5em; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 1em;">
|
||||
<div class="section-group" data-group-id="${groupId}" data-group-name="${group.name || 'Unnamed Group'}" style="margin-bottom: 1.5em;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75em;">
|
||||
<div>
|
||||
<input type="text" class="listItemBodyText editable-group-name" data-group-id="${groupId}" value="${(group.name || 'Unnamed Group').replace(/"/g, '"')}" size="${Math.max((group.name || 'Unnamed Group').length, 15)}" style="font-weight: 500; font-size: 1.1em; cursor: text; padding: 0.25em 0.5em; border-radius: 3px; transition: background-color 0.2s; border: none; outline: none; background: transparent; font-family: inherit; color: inherit;" onmouseover="this.style.backgroundColor='rgba(255,255,255,0.05)'" onmouseout="this.style.backgroundColor='transparent'" onfocus="this.style.backgroundColor='rgba(255,255,255,0.1)'" onblur="this.style.backgroundColor='transparent'" oninput="this.size=Math.max(this.value.length,15)" />
|
||||
@@ -1871,6 +1929,10 @@
|
||||
<span class="material-icons" style="margin-right: 0.35em; font-size: 1.1em; vertical-align: middle;">sync</span>
|
||||
<span>Update</span>
|
||||
</button>
|
||||
<button type="button" class="emby-button raised hsc-toolbar-restore-defaults-btn" title="Restore default Home Screen sections">
|
||||
<span class="material-icons" style="margin-right: 0.35em; font-size: 1.1em; vertical-align: middle;">restart_alt</span>
|
||||
<span>Restore Defaults</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
@@ -1984,19 +2046,49 @@
|
||||
const seasonal = currentConfig.SEASONAL_THEME_SETTINGS || {};
|
||||
const homeSettings = currentConfig.HOME_SETTINGS || {};
|
||||
const userHomeScreen = currentConfig.USER_HOME_SCREEN_SETTINGS || {};
|
||||
const libraryCache = currentConfig.LIBRARY_CACHE || {};
|
||||
const D = GENERAL_TOGGLE_DESCRIPTIONS;
|
||||
const maxPeopleCount = homeSettings.maxPeopleCount ?? 500;
|
||||
const maxPeopleOptions = MAX_PEOPLE_COUNT_OPTIONS.map((n) => ({ value: String(n), label: String(n) }));
|
||||
const settingsGroupStyle = 'margin-bottom: 1.5em; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 1em;';
|
||||
return `
|
||||
<div class="hsc-settings-grid hsc-settings-grid-2">
|
||||
${hscToggleCard('userHome-inlineConfigure', userHomeScreen.inlineConfigure !== false, 'Inline Section Configuration', D)}
|
||||
${hscToggleCard('userHome-pinning', userHomeScreen.pinning !== false, 'Pin To Home', D)}
|
||||
<div class="hsc-settings-span-2">
|
||||
${hscToggleCard('userHome-pairNativeHomeSections', userHomeScreen.pairNativeHomeSections !== false, 'Pair Home Sections to Native Jellyfin User Preferences', D)}
|
||||
</div>
|
||||
${hscToggleCard('home-fadeInSections', homeSettings.fadeInSections === true, 'Fade In Sections', D)}
|
||||
${hscToggleCard('home-showStaleDataBeforeRefresh', homeSettings.SHOW_STALE_DATA_BEFORE_REFRESH === true, 'Show Stale Items', D)}
|
||||
${hscToggleCard('home-showStaleDataBeforeRefresh', homeSettings.showStaleDataBeforeRefresh === true, 'Show Stale Items', D)}
|
||||
${hscToggleCard('seasonal-enableSeasonalAnimations', seasonal.enableSeasonalAnimations !== false, 'Seasonal Animations', D)}
|
||||
${hscToggleCard('seasonal-enableSeasonalBackground', seasonal.enableSeasonalBackground !== false, 'Seasonal Backgrounds', D)}
|
||||
<div class="hsc-settings-span-2">
|
||||
${hscToggleCard('home-ensureThumbsForPopularTVNetworks', homeSettings.ensureThumbsForPopularTVNetworks === true, 'Require Studio Thumbs', D)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hsc-settings-group" style="${settingsGroupStyle}">
|
||||
<div class="listItemBodyText" style="font-weight: 500; font-size: 1.1em;">Top People</div>
|
||||
<div class="listItemBodyText secondary" style="font-size: 0.85em; margin-top: 0.25em; margin-bottom: 0.75em;">
|
||||
Sections related to Top People from your library will use these values to determine which People qualify as "Top People". If a Person meets any of the minimum criteria listed below, they will be considered a Top Person. Top People will be sorted based on their total number of appearances, caching only the Top X number of people based on the Maximum Person Count.
|
||||
</div>
|
||||
<div class="hsc-settings-span-2" style="margin-bottom: 0.75em;">
|
||||
${hscToggleCard('home-loadPeopleEpisodeData', homeSettings.loadPeopleEpisodeData === true, 'Load Episode Data for People', D)}
|
||||
</div>
|
||||
<div class="hsc-settings-grid hsc-settings-grid-2">
|
||||
${buildTextInput('home-minPeopleAppearancesTotal', homeSettings.minPeopleAppearancesTotal ?? 10, 'Minimum Total Appearances', 'number')}
|
||||
${buildTextInput('home-minPeopleAppearancesMovies', homeSettings.minPeopleAppearancesMovies ?? 10, 'Minimum Movies Appearances', 'number')}
|
||||
${buildTextInput('home-minPeopleAppearancesSeries', homeSettings.minPeopleAppearancesSeries ?? 10, 'Minimum Series Appearances', 'number')}
|
||||
${buildTextInput('home-minPeopleAppearancesEpisodes', homeSettings.minPeopleAppearancesEpisodes ?? 10, 'Minimum Episodes Appearances', 'number')}
|
||||
${buildSelect('home-maxPeopleCount', maxPeopleOptions, String(maxPeopleCount), 'Maximum Person Count')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hsc-settings-group" style="${settingsGroupStyle}">
|
||||
<div class="listItemBodyText" style="font-weight: 500; font-size: 1.1em;">External Integrations</div>
|
||||
<div class="listItemBodyText secondary" style="font-size: 0.85em; margin-top: 0.25em; margin-bottom: 0.75em;">
|
||||
Enter your personal API keys for the services below to enable support for home sections based on external lists.
|
||||
</div>
|
||||
${buildTextInput('libraryCache-mdblistApiKey', libraryCache.mdblistApiKey || '', 'MDBList API Key', 'password', 'Required for External List sections')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2021,8 +2113,8 @@
|
||||
${hscToggleCard('discovery-randomizeOrder', discovery.randomizeOrder === true, 'Randomize Order', D)}
|
||||
${hscToggleCard('discovery-fadeInSections', discovery.fadeInSections === true, 'Fade In Sections', D)}
|
||||
${buildTextInput('discovery-spotlightDiscoveryChance', discovery.spotlightDiscoveryChance ?? 0.5, 'Spotlight Discovery Chance (0-1)', 'number')}
|
||||
${buildTextInput('discovery-minPeopleAppearances', discovery.minPeopleAppearances || 10, 'Top Person Appearance Count', 'number')}
|
||||
${buildTextInput('discovery-minGenreMovieCount', discovery.minGenreMovieCount || 50, 'Genre Movie Count', 'number')}
|
||||
${buildTextInput('discovery-minGenreSeriesCount', discovery.minGenreSeriesCount || 10, 'Genre Series Count', 'number')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -2149,8 +2241,8 @@
|
||||
...(currentConfig.DISCOVERY_SETTINGS || {}),
|
||||
enabled: root.querySelector('#discovery-enabled')?.checked !== false,
|
||||
infiniteScroll: root.querySelector('#discovery-infiniteScroll')?.checked !== false,
|
||||
minPeopleAppearances: parseInt(root.querySelector('#discovery-minPeopleAppearances')?.value || '10', 10),
|
||||
minGenreMovieCount: parseInt(root.querySelector('#discovery-minGenreMovieCount')?.value || '50', 10),
|
||||
minGenreSeriesCount: parseInt(root.querySelector('#discovery-minGenreSeriesCount')?.value || '10', 10),
|
||||
spotlightDiscoveryChance: parseFloat(root.querySelector('#discovery-spotlightDiscoveryChance')?.value || '0.5'),
|
||||
renderSpotlightAboveMatching: root.querySelector('#discovery-renderSpotlightAboveMatching')?.checked === true,
|
||||
randomizeOrder: root.querySelector('#discovery-randomizeOrder')?.checked === true,
|
||||
@@ -2160,7 +2252,11 @@
|
||||
|
||||
if (root.querySelector('#seasonal-enableSeasonalAnimations')
|
||||
|| root.querySelector('#home-fadeInSections')
|
||||
|| root.querySelector('#userHome-inlineConfigure')) {
|
||||
|| root.querySelector('#userHome-inlineConfigure')
|
||||
|| root.querySelector('#libraryCache-mdblistApiKey')
|
||||
|| root.querySelector('#home-minPeopleAppearancesTotal')
|
||||
|| root.querySelector('#home-loadPeopleEpisodeData')) {
|
||||
const prevLoadPeopleEpisodeData = currentConfig.HOME_SETTINGS?.loadPeopleEpisodeData === true;
|
||||
currentConfig.SEASONAL_THEME_SETTINGS = {
|
||||
...(currentConfig.SEASONAL_THEME_SETTINGS || {}),
|
||||
enableSeasonalAnimations: root.querySelector('#seasonal-enableSeasonalAnimations')?.checked !== false,
|
||||
@@ -2176,13 +2272,32 @@
|
||||
...(currentConfig.HOME_SETTINGS || {}),
|
||||
fadeInSections: root.querySelector('#home-fadeInSections')?.checked === true,
|
||||
ensureThumbsForPopularTVNetworks: root.querySelector('#home-ensureThumbsForPopularTVNetworks')?.checked === true,
|
||||
SHOW_STALE_DATA_BEFORE_REFRESH: root.querySelector('#home-showStaleDataBeforeRefresh')?.checked === true
|
||||
showStaleDataBeforeRefresh: root.querySelector('#home-showStaleDataBeforeRefresh')?.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),
|
||||
minPeopleAppearancesEpisodes: parseInt(root.querySelector('#home-minPeopleAppearancesEpisodes')?.value || '10', 10),
|
||||
maxPeopleCount: parseInt(root.querySelector('#home-maxPeopleCount')?.value || '500', 10),
|
||||
loadPeopleEpisodeData: root.querySelector('#home-loadPeopleEpisodeData')?.checked === true
|
||||
};
|
||||
syncEnsureThumbsOnPopularStudios(currentConfig);
|
||||
currentConfig.USER_HOME_SCREEN_SETTINGS = {
|
||||
...(currentConfig.USER_HOME_SCREEN_SETTINGS || {}),
|
||||
inlineConfigure: root.querySelector('#userHome-inlineConfigure')?.checked !== false,
|
||||
pinning: root.querySelector('#userHome-pinning')?.checked !== false
|
||||
pinning: root.querySelector('#userHome-pinning')?.checked !== false,
|
||||
pairNativeHomeSections: root.querySelector('#userHome-pairNativeHomeSections')?.checked !== false
|
||||
};
|
||||
if (root.querySelector('#libraryCache-mdblistApiKey')) {
|
||||
currentConfig.LIBRARY_CACHE = {
|
||||
...(currentConfig.LIBRARY_CACHE || {}),
|
||||
mdblistApiKey: root.querySelector('#libraryCache-mdblistApiKey')?.value || ''
|
||||
};
|
||||
}
|
||||
|
||||
const nextLoadEpisodes = currentConfig.HOME_SETTINGS.loadPeopleEpisodeData === true;
|
||||
if (prevLoadPeopleEpisodeData !== nextLoadEpisodes && window.PeopleCache?.invalidate) {
|
||||
window.PeopleCache.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
if (root.querySelector('#cache-DEFAULT_TTL-preset') || root.querySelector('#cache-VERY_SHORT_TTL')) {
|
||||
@@ -2230,7 +2345,8 @@
|
||||
CACHE: currentConfig?.CACHE,
|
||||
SPOTLIGHT_SETTINGS: currentConfig?.SPOTLIGHT_SETTINGS,
|
||||
HOME_SETTINGS: currentConfig?.HOME_SETTINGS,
|
||||
USER_HOME_SCREEN_SETTINGS: currentConfig?.USER_HOME_SCREEN_SETTINGS
|
||||
USER_HOME_SCREEN_SETTINGS: currentConfig?.USER_HOME_SCREEN_SETTINGS,
|
||||
LIBRARY_CACHE: currentConfig?.LIBRARY_CACHE
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2718,6 +2834,200 @@
|
||||
await saveConfig(currentConfig);
|
||||
}
|
||||
|
||||
function buildSectionIdMapFromGroups(...groupLists) {
|
||||
const map = new Map();
|
||||
groupLists.forEach((groups) => {
|
||||
flattenSectionGroups(groups).forEach((section) => {
|
||||
if (section?.id) map.set(section.id, section);
|
||||
});
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function copyRetainKeys(target, source, keys) {
|
||||
if (!target || !source) return;
|
||||
keys.forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
target[key] = JSON.parse(JSON.stringify(source[key]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyQueryAgeFields(target, source) {
|
||||
if (!target || !source) return;
|
||||
const srcQuery = source.queries?.[0];
|
||||
if (!srcQuery || !Array.isArray(target.queries) || !target.queries[0]) return;
|
||||
if (Object.prototype.hasOwnProperty.call(srcQuery, 'minAge')) {
|
||||
target.queries[0].minAge = srcQuery.minAge;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(srcQuery, 'maxAge')) {
|
||||
target.queries[0].maxAge = srcQuery.maxAge;
|
||||
}
|
||||
}
|
||||
|
||||
function applyRetainOptionsToGroups(groups, priorById, options) {
|
||||
(groups || []).forEach((group) => {
|
||||
(group.sections || []).forEach((section) => {
|
||||
const prior = priorById.get(section.id);
|
||||
if (!prior) return;
|
||||
if (options.retainStatus) {
|
||||
if (Object.prototype.hasOwnProperty.call(prior, 'enabled')) {
|
||||
section.enabled = prior.enabled;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(prior, 'userConfigurable')) {
|
||||
section.userConfigurable = prior.userConfigurable;
|
||||
}
|
||||
}
|
||||
if (options.retainNameCaption) {
|
||||
copyRetainKeys(section, prior, ['name', 'caption']);
|
||||
}
|
||||
if (options.retainAppearance) {
|
||||
copyRetainKeys(section, prior, RESTORE_APPEARANCE_KEYS);
|
||||
}
|
||||
if (options.retainAdvanced) {
|
||||
copyRetainKeys(section, prior, RESTORE_ADVANCED_KEYS);
|
||||
copyQueryAgeFields(section, prior);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stampGroupSectionTypes(groups, sectionType) {
|
||||
(groups || []).forEach((group) => {
|
||||
(group.sections || []).forEach((section) => {
|
||||
section.type = sectionType;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function applyRestoreDefaults(options) {
|
||||
const defaults = window.KefinHomeConfig2;
|
||||
if (!defaults || !currentConfig) {
|
||||
throw new Error('Home Screen defaults are not available.');
|
||||
}
|
||||
|
||||
const priorById = buildSectionIdMapFromGroups(
|
||||
currentConfig.HOME_SECTION_GROUPS,
|
||||
currentConfig.SEASONAL_SECTION_GROUPS,
|
||||
currentConfig.DISCOVERY_SECTION_GROUPS
|
||||
);
|
||||
|
||||
const homeGroups = JSON.parse(JSON.stringify(defaults.HOME_SECTION_GROUPS || []));
|
||||
const seasonalGroups = JSON.parse(JSON.stringify(defaults.SEASONAL_SECTION_GROUPS || []));
|
||||
const discoveryGroups = JSON.parse(JSON.stringify(defaults.DISCOVERY_SECTION_GROUPS || []));
|
||||
|
||||
stampGroupSectionTypes(homeGroups, 'home');
|
||||
stampGroupSectionTypes(seasonalGroups, 'seasonal');
|
||||
stampGroupSectionTypes(discoveryGroups, 'discovery');
|
||||
|
||||
applyRetainOptionsToGroups(homeGroups, priorById, options);
|
||||
applyRetainOptionsToGroups(seasonalGroups, priorById, options);
|
||||
applyRetainOptionsToGroups(discoveryGroups, priorById, options);
|
||||
|
||||
currentConfig.HOME_SECTION_GROUPS = homeGroups;
|
||||
currentConfig.SEASONAL_SECTION_GROUPS = seasonalGroups;
|
||||
currentConfig.DISCOVERY_SECTION_GROUPS = discoveryGroups;
|
||||
if (!options.retainCustoms) {
|
||||
currentConfig.CUSTOM_SECTION_GROUPS = [];
|
||||
}
|
||||
|
||||
// Pre-sync so saveConfig merge does not reintroduce removed sections
|
||||
if (!window.KefinTweaksConfig) window.KefinTweaksConfig = {};
|
||||
if (!window.KefinTweaksConfig.homeScreenConfig) window.KefinTweaksConfig.homeScreenConfig = {};
|
||||
const saved = window.KefinTweaksConfig.homeScreenConfig;
|
||||
saved.HOME_SECTION_GROUPS = JSON.parse(JSON.stringify(homeGroups));
|
||||
saved.SEASONAL_SECTION_GROUPS = JSON.parse(JSON.stringify(seasonalGroups));
|
||||
saved.DISCOVERY_SECTION_GROUPS = JSON.parse(JSON.stringify(discoveryGroups));
|
||||
if (!options.retainCustoms) {
|
||||
saved.CUSTOM_SECTION_GROUPS = [];
|
||||
}
|
||||
|
||||
await saveConfig(currentConfig);
|
||||
refreshMainModal();
|
||||
showToast('Home Screen defaults restored');
|
||||
}
|
||||
|
||||
function confirmAndRestoreDefaults() {
|
||||
if (!window.ModalSystem) {
|
||||
ERR('ModalSystem not available for restore defaults');
|
||||
return;
|
||||
}
|
||||
if (window.ModalSystem.isOpen(RESTORE_DEFAULTS_MODAL_ID)) {
|
||||
window.ModalSystem.close(RESTORE_DEFAULTS_MODAL_ID);
|
||||
}
|
||||
|
||||
const D = RESTORE_RETAIN_TOGGLE_DESCRIPTIONS;
|
||||
const content = document.createElement('div');
|
||||
content.innerHTML = `
|
||||
<div class="listItemBodyText" style="line-height: 1.5; margin-bottom: 1em;">
|
||||
Restoring Defaults for KefinTweaks Home Screen will update the existing Home Screen configuration.
|
||||
</div>
|
||||
<div class="hsc-settings-grid hsc-settings-grid-2">
|
||||
${hscToggleCard('restore-retain-status', true, 'Retain Section Status', D)}
|
||||
${hscToggleCard('restore-retain-name-caption', true, 'Retain Section Name and Caption', D)}
|
||||
${hscToggleCard('restore-retain-appearance', true, 'Retain Section Appearance', D)}
|
||||
${hscToggleCard('restore-retain-advanced', true, 'Retain Section Advanced Options', D)}
|
||||
<div class="hsc-settings-span-2">
|
||||
${hscToggleCard('restore-retain-customs', true, 'Retain Custom Sections', D)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const footer = document.createElement('div');
|
||||
footer.style.display = 'flex';
|
||||
footer.style.gap = '0.5em';
|
||||
footer.style.justifyContent = 'flex-end';
|
||||
footer.innerHTML = `
|
||||
<button type="button" class="emby-button raised" id="hsc-restore-defaults-cancel">Cancel</button>
|
||||
<button type="button" class="emby-button raised block button-submit" id="hsc-restore-defaults-confirm">Restore Defaults</button>
|
||||
`;
|
||||
|
||||
window.ModalSystem.create({
|
||||
id: RESTORE_DEFAULTS_MODAL_ID,
|
||||
title: 'Restore Defaults',
|
||||
content,
|
||||
footer,
|
||||
closeOnBackdrop: true,
|
||||
closeOnEscape: true,
|
||||
showCloseButton: true,
|
||||
fixedSize: true,
|
||||
onOpen: (modal) => {
|
||||
const root = modal.dialogContent || content;
|
||||
window.KefinTweaksUI?.bindToggleCards?.(root);
|
||||
updateHscToggleCardHints(root, RESTORE_RETAIN_TOGGLE_DESCRIPTIONS);
|
||||
root.addEventListener('change', (e) => {
|
||||
if (e.target?.matches?.('input[type="checkbox"][id^="restore-retain-"]')) {
|
||||
updateHscToggleCardHints(root, RESTORE_RETAIN_TOGGLE_DESCRIPTIONS);
|
||||
}
|
||||
});
|
||||
|
||||
modal.dialogFooter?.querySelector('#hsc-restore-defaults-cancel')?.addEventListener('click', () => {
|
||||
window.ModalSystem.close(RESTORE_DEFAULTS_MODAL_ID);
|
||||
});
|
||||
modal.dialogFooter?.querySelector('#hsc-restore-defaults-confirm')?.addEventListener('click', async () => {
|
||||
const confirmBtn = modal.dialogFooter.querySelector('#hsc-restore-defaults-confirm');
|
||||
if (confirmBtn) confirmBtn.disabled = true;
|
||||
try {
|
||||
const options = {
|
||||
retainStatus: root.querySelector('#restore-retain-status')?.checked !== false,
|
||||
retainNameCaption: root.querySelector('#restore-retain-name-caption')?.checked !== false,
|
||||
retainAppearance: root.querySelector('#restore-retain-appearance')?.checked !== false,
|
||||
retainAdvanced: root.querySelector('#restore-retain-advanced')?.checked !== false,
|
||||
retainCustoms: root.querySelector('#restore-retain-customs')?.checked !== false
|
||||
};
|
||||
await applyRestoreDefaults(options);
|
||||
window.ModalSystem.close(RESTORE_DEFAULTS_MODAL_ID);
|
||||
} catch (e) {
|
||||
ERR('Error restoring Home Screen defaults:', e);
|
||||
showToast('Error restoring defaults');
|
||||
} finally {
|
||||
if (confirmBtn) confirmBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmAndUpdateAllUsers() {
|
||||
if (!window.ModalSystem) {
|
||||
ERR('ModalSystem not available for update confirmation');
|
||||
@@ -2965,6 +3275,10 @@
|
||||
}
|
||||
if (e.target.closest('.hsc-toolbar-update-btn')) {
|
||||
confirmAndUpdateAllUsers();
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.hsc-toolbar-restore-defaults-btn')) {
|
||||
confirmAndRestoreDefaults();
|
||||
}
|
||||
});
|
||||
/* dialog.querySelectorAll('.section-type-nav-btn').forEach(btn => {
|
||||
@@ -4412,6 +4726,18 @@
|
||||
}
|
||||
|
||||
const queries = section.queries || [];
|
||||
if (Array.isArray(section.externalListUrls) && section.externalListUrls.length > 0) {
|
||||
if (!window.KefinExternalList?.resolveExternalListItems) {
|
||||
throw new Error('External list module not available for preview');
|
||||
}
|
||||
const listResult = await window.KefinExternalList.resolveExternalListItems(section);
|
||||
const items = listResult?.Items || [];
|
||||
const processed = window.cardBuilder.postProcessItems
|
||||
? window.cardBuilder.postProcessItems(section, items)
|
||||
: items;
|
||||
return { items: processed, limit: processed.length };
|
||||
}
|
||||
|
||||
if (section.items?.length) {
|
||||
const kefinTweaksRoot = window.KefinTweaksConfig?.kefinTweaksRoot || '';
|
||||
const serverId = ApiClient.serverId();
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
SEASONAL_SECTION_GROUPS: JSON.parse(JSON.stringify(defaults.SEASONAL_SECTION_GROUPS || [])),
|
||||
DISCOVERY_SECTION_GROUPS: JSON.parse(JSON.stringify(defaults.DISCOVERY_SECTION_GROUPS || [])),
|
||||
CUSTOM_SECTION_GROUPS: [],
|
||||
REMOVE_CONFLICTING_SECTIONS: legacyHomeScreen.removeConflictingSections !== false,
|
||||
DISCOVERY_SETTINGS: JSON.parse(JSON.stringify(defaults.DISCOVERY_SETTINGS || {})),
|
||||
SEASONAL_THEME_SETTINGS: JSON.parse(JSON.stringify(defaults.SEASONAL_THEME_SETTINGS || {}))
|
||||
SEASONAL_THEME_SETTINGS: JSON.parse(JSON.stringify(defaults.SEASONAL_THEME_SETTINGS || {})),
|
||||
HOME_SETTINGS: JSON.parse(JSON.stringify(defaults.HOME_SETTINGS || {}))
|
||||
};
|
||||
|
||||
// Migrate discovery settings
|
||||
@@ -101,7 +101,6 @@
|
||||
|
||||
if (discovery.enabled !== undefined) settings.enabled = discovery.enabled;
|
||||
if (discovery.infiniteScroll !== undefined) settings.infiniteScroll = discovery.infiniteScroll;
|
||||
if (discovery.minPeopleAppearances !== undefined) settings.minPeopleAppearances = discovery.minPeopleAppearances;
|
||||
if (discovery.minGenreMovieCount !== undefined) settings.minGenreMovieCount = discovery.minGenreMovieCount;
|
||||
if (discovery.defaultItemLimit !== undefined) settings.defaultItemLimit = discovery.defaultItemLimit;
|
||||
if (discovery.defaultSortOrder !== undefined) settings.defaultSortOrder = discovery.defaultSortOrder;
|
||||
@@ -113,6 +112,17 @@
|
||||
settings.spotlight = {};
|
||||
Object.assign(settings.spotlight, discovery.spotlight);
|
||||
}
|
||||
|
||||
// Legacy discovery.minPeopleAppearances → HOME_SETTINGS mins
|
||||
if (discovery.minPeopleAppearances !== undefined) {
|
||||
if (!newConfig.HOME_SETTINGS) newConfig.HOME_SETTINGS = {};
|
||||
const min = discovery.minPeopleAppearances;
|
||||
const home = newConfig.HOME_SETTINGS;
|
||||
if (home.minPeopleAppearancesTotal == null) home.minPeopleAppearancesTotal = min;
|
||||
if (home.minPeopleAppearancesMovies == null) home.minPeopleAppearancesMovies = min;
|
||||
if (home.minPeopleAppearancesSeries == null) home.minPeopleAppearancesSeries = min;
|
||||
if (home.minPeopleAppearancesEpisodes == null) home.minPeopleAppearancesEpisodes = min;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,8 +226,8 @@
|
||||
function mergeSectionConfig(defaultSection, legacyConfig) {
|
||||
const merged = JSON.parse(JSON.stringify(defaultSection));
|
||||
|
||||
// Map root fields
|
||||
if (legacyConfig.enabled !== undefined) merged.enabled = legacyConfig.enabled;
|
||||
// Map root fields — missing legacy enabled means on (same as seasonal/custom)
|
||||
merged.enabled = legacyConfig.enabled !== false;
|
||||
if (legacyConfig.name !== undefined) merged.name = legacyConfig.name;
|
||||
if (legacyConfig.order !== undefined) merged.order = legacyConfig.order;
|
||||
if (legacyConfig.cardFormat !== undefined) merged.cardFormat = legacyConfig.cardFormat;
|
||||
@@ -427,6 +437,7 @@
|
||||
watchlist: 'watchlist',
|
||||
upcoming: 'upcoming',
|
||||
imdbTop250: 'imdbTop250',
|
||||
trending: 'trending',
|
||||
popularTVNetworks: 'popularTVNetworks'
|
||||
};
|
||||
|
||||
@@ -550,6 +561,50 @@
|
||||
LOG(` Added as custom section: ${customSection.id}`);
|
||||
}
|
||||
|
||||
const RECENTLY_WATCHED_PERSON_IDS = new Set([
|
||||
'starringActorRecentlyWatched',
|
||||
'directedByDirectorRecentlyWatched',
|
||||
'writtenByWriterRecentlyWatched'
|
||||
]);
|
||||
|
||||
const RECENTLY_WATCHED_CAPTION_DEFAULT = 'because you recently watched {Title}';
|
||||
|
||||
const RECENTLY_WATCHED_NAME_DEFAULTS = {
|
||||
starringActorRecentlyWatched: 'Starring {Person}',
|
||||
directedByDirectorRecentlyWatched: 'Directed by {Person}',
|
||||
writtenByWriterRecentlyWatched: 'Written by {Person}'
|
||||
};
|
||||
|
||||
function normalizeDiscoveryTemplatePlaceholders(text) {
|
||||
if (!text || typeof text !== 'string') return text;
|
||||
return text
|
||||
.replace(/\[Actor\]/gi, '{Person}')
|
||||
.replace(/\[Director\]/gi, '{Person}')
|
||||
.replace(/\[Writer\]/gi, '{Person}')
|
||||
.replace(/\[Person\]/gi, '{Person}')
|
||||
.replace(/\[Movie\]/gi, '{Title}')
|
||||
.replace(/\[Title\]/gi, '{Title}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Split legacy combined "Starring X because you recently watched Y" into name + caption.
|
||||
*/
|
||||
function splitLegacyRecentlyWatchedName(legacyName) {
|
||||
if (!legacyName || typeof legacyName !== 'string') return null;
|
||||
const match = legacyName.match(/^(.*?)\s+because you recently watched\s+(.*)$/i);
|
||||
if (!match) return null;
|
||||
let name = normalizeDiscoveryTemplatePlaceholders(match[1].trim());
|
||||
let captionRight = normalizeDiscoveryTemplatePlaceholders(match[2].trim());
|
||||
const caption = captionRight
|
||||
? `because you recently watched ${captionRight}`
|
||||
: RECENTLY_WATCHED_CAPTION_DEFAULT;
|
||||
// Prefer canonical caption placeholder when right side is Movie/Title token only
|
||||
if (/^\{Title\}$/i.test(captionRight) || /^\[Movie\]$/i.test(match[2].trim())) {
|
||||
return { name, caption: RECENTLY_WATCHED_CAPTION_DEFAULT };
|
||||
}
|
||||
return { name, caption };
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate DISCOVERY sections
|
||||
*/
|
||||
@@ -562,13 +617,10 @@
|
||||
|
||||
const discoverySectionTypes = legacyHomeScreen.discovery.sectionTypes;
|
||||
Object.entries(discoverySectionTypes).forEach(([sectionKey, legacySectionConfig]) => {
|
||||
// Use the key as the section ID
|
||||
const found = findDiscoverySectionById(sectionKey, newConfig.DISCOVERY_SECTION_GROUPS);
|
||||
|
||||
if (found) {
|
||||
// Discovery sections still use legacy format, so we map fields directly
|
||||
if (legacySectionConfig.enabled !== undefined) found.section.enabled = legacySectionConfig.enabled;
|
||||
if (legacySectionConfig.name !== undefined) found.section.name = legacySectionConfig.name;
|
||||
found.section.enabled = legacySectionConfig.enabled !== false;
|
||||
if (legacySectionConfig.itemLimit !== undefined) found.section.itemLimit = legacySectionConfig.itemLimit;
|
||||
if (legacySectionConfig.cardFormat !== undefined) found.section.cardFormat = legacySectionConfig.cardFormat;
|
||||
if (legacySectionConfig.isPlayed !== undefined && legacySectionConfig.isPlayed !== null) {
|
||||
@@ -578,6 +630,29 @@
|
||||
if (legacySectionConfig.sortOrder !== undefined) found.section.sortOrder = legacySectionConfig.sortOrder;
|
||||
if (legacySectionConfig.sortOrderDirection !== undefined) found.section.sortOrderDirection = legacySectionConfig.sortOrderDirection;
|
||||
|
||||
const isRecentlyWatchedPerson = RECENTLY_WATCHED_PERSON_IDS.has(sectionKey);
|
||||
if (legacySectionConfig.name !== undefined) {
|
||||
const split = splitLegacyRecentlyWatchedName(legacySectionConfig.name);
|
||||
if (split) {
|
||||
found.section.name = split.name;
|
||||
found.section.caption = split.caption;
|
||||
} else if (isRecentlyWatchedPerson) {
|
||||
found.section.name = RECENTLY_WATCHED_NAME_DEFAULTS[sectionKey]
|
||||
|| normalizeDiscoveryTemplatePlaceholders(legacySectionConfig.name);
|
||||
if (!found.section.caption) {
|
||||
found.section.caption = RECENTLY_WATCHED_CAPTION_DEFAULT;
|
||||
}
|
||||
} else {
|
||||
found.section.name = legacySectionConfig.name;
|
||||
}
|
||||
} else if (isRecentlyWatchedPerson && !found.section.caption) {
|
||||
found.section.caption = RECENTLY_WATCHED_CAPTION_DEFAULT;
|
||||
}
|
||||
|
||||
if (legacySectionConfig.caption !== undefined) {
|
||||
found.section.caption = normalizeDiscoveryTemplatePlaceholders(legacySectionConfig.caption);
|
||||
}
|
||||
|
||||
LOG(` Migrated discovery section: ${sectionKey}`);
|
||||
} else {
|
||||
WARN(` Discovery section not found in defaults: ${sectionKey}`);
|
||||
|
||||
@@ -57,9 +57,60 @@
|
||||
sections: [],
|
||||
pinnedLists: [],
|
||||
pinnedParents: []
|
||||
// pairNativeHomeSections omitted until user dirties the field
|
||||
};
|
||||
}
|
||||
|
||||
function getServerPairNativeHomeSectionsDefault() {
|
||||
const settings = window.KefinHomeScreen?.getConfig?.()?.USER_HOME_SCREEN_SETTINGS
|
||||
|| window.KefinHomeConfig2?.USER_HOME_SCREEN_SETTINGS
|
||||
|| {};
|
||||
return settings.pairNativeHomeSections !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective pair-native setting: user override if present, else server default.
|
||||
*/
|
||||
function resolvePairNativeHomeSections(homeScreen) {
|
||||
if (homeScreen && typeof homeScreen.pairNativeHomeSections === 'boolean') {
|
||||
return homeScreen.pairNativeHomeSections;
|
||||
}
|
||||
return getServerPairNativeHomeSectionsDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all native Jellyfin home section slots (homesection0–9 = none).
|
||||
*/
|
||||
function disableAllNativeHomeSections(customPrefs, { syncUi = false } = {}) {
|
||||
const prefs = customPrefs || {};
|
||||
let changed = false;
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
const key = `homesection${i}`;
|
||||
const prev = prefs[key];
|
||||
const prevNorm = (!prev || prev === '') ? 'none' : String(prev).toLowerCase();
|
||||
if (prevNorm !== 'none' || prefs[key] !== 'none') {
|
||||
prefs[key] = 'none';
|
||||
if (prevNorm !== 'none' || prev !== 'none') changed = true;
|
||||
}
|
||||
}
|
||||
if (syncUi) {
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
setNativeHomeSectionSelect(findNativeHomeSectionSelect(i + 1), 'none');
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
LOG('Disabled all Jellyfin homesectionN (pair native off)');
|
||||
}
|
||||
return { slots: Array(10).fill('none'), changed };
|
||||
}
|
||||
|
||||
function syncNativeHomeSectionsFromKefin(sections, customPrefs, homeScreen, { syncUi = false } = {}) {
|
||||
if (resolvePairNativeHomeSections(homeScreen)) {
|
||||
return enableJellyfinSectionsFromKefin(sections, customPrefs, { syncUi });
|
||||
}
|
||||
return disableAllNativeHomeSections(customPrefs, { syncUi });
|
||||
}
|
||||
|
||||
function slugifyPinnedName(name) {
|
||||
return String(name || '')
|
||||
.trim()
|
||||
@@ -524,11 +575,15 @@
|
||||
if (parsed) {
|
||||
const hs = parsed?.homeScreen || (Array.isArray(parsed?.sections) ? parsed : null);
|
||||
if (hs) {
|
||||
return {
|
||||
const result = {
|
||||
sections: Array.isArray(hs.sections) ? [...hs.sections] : [],
|
||||
pinnedLists: Array.isArray(hs.pinnedLists) ? [...hs.pinnedLists] : [],
|
||||
pinnedParents: Array.isArray(hs.pinnedParents) ? [...hs.pinnedParents] : []
|
||||
};
|
||||
if (typeof hs.pairNativeHomeSections === 'boolean') {
|
||||
result.pairNativeHomeSections = hs.pairNativeHomeSections;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return migrateLegacyKefinHomeScreen(customPrefs);
|
||||
@@ -839,7 +894,7 @@
|
||||
sections = sections.filter((section) => section.enabled || section.userConfigurable === true || section.userConfigurable === undefined);
|
||||
sections = deduplicateHomeScreenSections(sections);
|
||||
|
||||
const jellyfinSync = enableJellyfinSectionsFromKefin(sections, customPrefs, { syncUi: true });
|
||||
const jellyfinSync = syncNativeHomeSectionsFromKefin(sections, customPrefs, homeScreen, { syncUi: true });
|
||||
if (jellyfinSync.changed && displayPrefs && !sanitizePersistInFlight) {
|
||||
sanitizePersistInFlight = true;
|
||||
try {
|
||||
@@ -1544,8 +1599,12 @@
|
||||
|
||||
/**
|
||||
* Save user preferences to display preferences
|
||||
* @param {Array} sections
|
||||
* @param {{ pairNativeHomeSections?: boolean|null }} [options]
|
||||
* pairNativeHomeSections: true/false to set override; null/undefined to leave unchanged;
|
||||
* pass 'default' symbol via omitOverride to clear dirty override
|
||||
*/
|
||||
async function saveUserPreferences(sections) {
|
||||
async function saveUserPreferences(sections, options = {}) {
|
||||
try {
|
||||
if (!window.userHelper || !window.userHelper.getUserDisplayPreferences || !window.userHelper.updateDisplayPreferences) {
|
||||
ERR('userHelper not available');
|
||||
@@ -1574,6 +1633,21 @@
|
||||
mergeEditorSectionsIntoHomeScreen(existingHomeScreen, sections, serverSectionsById)
|
||||
).homeScreen;
|
||||
|
||||
// Preserve or update dirty-only pairNativeHomeSections
|
||||
if (Object.prototype.hasOwnProperty.call(options, 'pairNativeHomeSections')) {
|
||||
const serverDefault = getServerPairNativeHomeSectionsDefault();
|
||||
const next = options.pairNativeHomeSections;
|
||||
if (next === null || next === undefined) {
|
||||
delete homeScreen.pairNativeHomeSections;
|
||||
} else if (next === serverDefault) {
|
||||
delete homeScreen.pairNativeHomeSections;
|
||||
} else {
|
||||
homeScreen.pairNativeHomeSections = next === true;
|
||||
}
|
||||
} else if (typeof existingHomeScreen.pairNativeHomeSections === 'boolean') {
|
||||
homeScreen.pairNativeHomeSections = existingHomeScreen.pairNativeHomeSections;
|
||||
}
|
||||
|
||||
const kefin = typeof window.userHelper?.parseKefinTweaks === 'function'
|
||||
? window.userHelper.parseKefinTweaks(customPrefs)
|
||||
: {};
|
||||
@@ -1582,7 +1656,7 @@
|
||||
customPrefs.kefinHomeScreen = JSON.stringify(homeScreenToLegacyArray(homeScreen));
|
||||
|
||||
// Rewrite Jellyfin homesectionN (+ native selects) from enabled Kefin sections
|
||||
enableJellyfinSectionsFromKefin(sections, customPrefs, { syncUi: true });
|
||||
syncNativeHomeSectionsFromKefin(sections, customPrefs, homeScreen, { syncUi: true });
|
||||
|
||||
// Save
|
||||
const success = await window.userHelper.updateDisplayPreferences(displayPrefs);
|
||||
@@ -1679,6 +1753,20 @@
|
||||
showUpDownButtons: true
|
||||
});
|
||||
|
||||
const serverPairDefault = getServerPairNativeHomeSectionsDefault();
|
||||
const effectivePair = resolvePairNativeHomeSections(userConfig.homeScreen);
|
||||
const pairDescOn = 'Do NOT enable this if you use the official Jellyfin for Roku or Android TV clients. When this is enabled, your Home Screen on those clients will be empty and you will need to go to your User Settings to re-enable sections.';
|
||||
const pairDescOff = 'Do NOT enable this if you use the official Jellyfin for Roku or Android TV clients. When this is enabled, your Home Screen on those clients will be empty and you will need to go to your User Settings to re-enable sections.';
|
||||
const pairToggleHTML = typeof window.KefinTweaksUI?.buildToggleCard === 'function'
|
||||
? window.KefinTweaksUI.buildToggleCard(
|
||||
'kefin-user-pair-native-home-sections',
|
||||
!effectivePair,
|
||||
'Improve Home Screen Performance',
|
||||
!effectivePair ? pairDescOn : pairDescOff,
|
||||
{ hintKey: 'pairNativeHomeSections' }
|
||||
)
|
||||
: '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer" style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5em;">
|
||||
@@ -1688,9 +1776,51 @@
|
||||
</button>
|
||||
</div>
|
||||
${editorHTML}
|
||||
${pairToggleHTML ? `
|
||||
<div class="verticalSection" style="margin-top: 1.5em;" id="kefin-user-pair-native-wrap">
|
||||
${pairToggleHTML}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Pair-native toggle: dirty-only persist (INVERTED UI: on in UI means flag == false)
|
||||
const pairCheckbox = container.querySelector('#kefin-user-pair-native-home-sections');
|
||||
const pairCard = pairCheckbox?.closest('.kefin-toggle-card');
|
||||
if (pairCheckbox && pairCard) {
|
||||
const pairSwitch = pairCard.querySelector('.kefin-toggle-switch, .toggle-slider, button');
|
||||
// When the toggle appears "on" in the UI, the flag is actually false (inverse)
|
||||
const updatePairDesc = (checked) => {
|
||||
// checked (UI "on") means flag === false
|
||||
const desc = pairCard.querySelector('.kefin-toggle-card-desc');
|
||||
if (desc) desc.textContent = checked ? pairDescOn : pairDescOff;
|
||||
};
|
||||
const onPairToggle = async () => {
|
||||
// checked (UI 'on') means flag should be false
|
||||
const flagValue = !pairCheckbox.checked;
|
||||
updatePairDesc(pairCheckbox.checked);
|
||||
if (typeof window.KefinTweaksUI?.updateToggleSwitchUI === 'function' && pairSwitch) {
|
||||
window.KefinTweaksUI.updateToggleSwitchUI(pairSwitch, pairCheckbox.checked);
|
||||
}
|
||||
// For persistence: (flagValue === serverPairDefault) ? null : flagValue
|
||||
await saveUserPreferences(allSections, {
|
||||
pairNativeHomeSections: flagValue === serverPairDefault ? null : flagValue
|
||||
});
|
||||
};
|
||||
pairSwitch?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
pairCheckbox.checked = !pairCheckbox.checked;
|
||||
onPairToggle();
|
||||
});
|
||||
pairCheckbox.addEventListener('change', onPairToggle);
|
||||
|
||||
// On initial render, enforce the correct description and checked value
|
||||
// (in case render logic needs to set checked/unchecked based on flag)
|
||||
// (Optional: If not already being handled by upstream render logic)
|
||||
//pairCheckbox.checked = !effectivePair;
|
||||
updatePairDesc(pairCheckbox.checked);
|
||||
}
|
||||
|
||||
// Restore Defaults: confirm then clear kefinHomeScreen and re-render
|
||||
const restoreBtn = container.querySelector('.kefin-restore-defaults-btn');
|
||||
if (restoreBtn && window.ModalSystem) {
|
||||
@@ -1906,6 +2036,9 @@
|
||||
homeScreenToLegacyArray,
|
||||
getConfig,
|
||||
enableJellyfinSectionsFromKefin,
|
||||
syncNativeHomeSectionsFromKefin,
|
||||
resolvePairNativeHomeSections,
|
||||
getServerPairNativeHomeSectionsDefault,
|
||||
buildHomesectionSlotsFromKefin,
|
||||
resolveSectionJellyfinId,
|
||||
updateUserHomeScreenConfiguration,
|
||||
|
||||
@@ -129,6 +129,14 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
: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;
|
||||
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-field-grid-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
@@ -137,11 +145,6 @@
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"])
|
||||
.hsae-query-row-sort.hsae-field-grid-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsae-seasonal-dates {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -1589,11 +1592,18 @@
|
||||
}
|
||||
|
||||
/* Item source — show only the panel matching the active source (profile rules still gate custom items on non-full profiles) */
|
||||
.hsae-root[data-item-source="jellyfin"] .hsae-custom-items-group {
|
||||
.hsae-root[data-item-source="jellyfin"] .hsae-custom-items-group,
|
||||
.hsae-root[data-item-source="jellyfin"] .hsae-external-list-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hsae-root[data-item-source="custom"] .hsae-queries-group {
|
||||
.hsae-root[data-item-source="custom"] .hsae-queries-group,
|
||||
.hsae-root[data-item-source="custom"] .hsae-external-list-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hsae-root[data-item-source="external"] .hsae-queries-group,
|
||||
.hsae-root[data-item-source="external"] .hsae-custom-items-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -625,7 +625,7 @@
|
||||
function getCachePresets() {
|
||||
const cache = window.KefinHomeConfig2?.CACHE || window.KefinHomeConfig2?.CACHE_CONFIG || {};
|
||||
return [
|
||||
{ key: 'FORCE_REFRESH', label: 'Force Refresh', ms: cache.FORCE_REFRESH_TTL ?? 0 },
|
||||
{ key: 'FORCE_REFRESH', label: 'No Cache', ms: cache.FORCE_REFRESH_TTL ?? 0 },
|
||||
{ key: 'VERY_SHORT', label: 'Very Short (1 min)', ms: cache.VERY_SHORT_TTL ?? 60000 },
|
||||
{ key: 'SHORT', label: 'Short (5 min)', ms: cache.SHORT_TTL ?? 300000 },
|
||||
{ key: 'DEFAULT', label: 'Default (1 hour)', ms: cache.DEFAULT_TTL ?? 3600000 },
|
||||
@@ -637,6 +637,9 @@
|
||||
}
|
||||
|
||||
function inferItemSource(section) {
|
||||
if (Array.isArray(section?.externalListUrls) && section.externalListUrls.length > 0) {
|
||||
return 'external';
|
||||
}
|
||||
if (section?.items?.length > 0 && (!section.queries?.length || section.queries.length === 0)) {
|
||||
return 'custom';
|
||||
}
|
||||
@@ -951,6 +954,7 @@
|
||||
startDate: section.startDate || '',
|
||||
endDate: section.endDate || '',
|
||||
order: section.order != null ? String(section.order) : '',
|
||||
caption: section.caption || '',
|
||||
renderMode,
|
||||
cardFormat: section.cardFormat || 'Poster',
|
||||
useRandomQuery: section.useRandomQuery === true,
|
||||
@@ -970,6 +974,9 @@
|
||||
groupNew: groupSelection.groupNew,
|
||||
queries: (section.queries?.length ? section.queries : [createDefaultQuery()]).map(q => ({ ...q })),
|
||||
customItems: customEditor ? customEditor.staticItemsToDrafts(section.items) : [],
|
||||
externalListUrls: Array.isArray(section.externalListUrls) && section.externalListUrls.length
|
||||
? section.externalListUrls.slice()
|
||||
: [''],
|
||||
customItemEditorIndex: null,
|
||||
customItemAdditionalExpanded: {},
|
||||
ttlPreset: ttlMatch.preset,
|
||||
@@ -987,6 +994,7 @@
|
||||
if (!root || !state) return;
|
||||
|
||||
const nameEl = root.querySelector('#section-name');
|
||||
const captionEl = root.querySelector('#section-caption');
|
||||
const groupEl = root.querySelector('#section-group');
|
||||
const groupNewEl = root.querySelector('#section-group-new');
|
||||
const visibilityEl = root.querySelector('#hsae-section-visibility');
|
||||
@@ -1001,6 +1009,7 @@
|
||||
const sortOrderEl = root.querySelector('#section-sortOrder');
|
||||
|
||||
if (nameEl) state.sectionName = nameEl.value;
|
||||
if (captionEl) state.caption = captionEl.value;
|
||||
if (groupEl) state.groupSelect = groupEl.value;
|
||||
if (groupNewEl) state.groupNew = groupNewEl.value;
|
||||
if (visibilityEl) state.sectionVisibility = visibilityEl.value;
|
||||
@@ -1058,6 +1067,12 @@
|
||||
customEditor.collectCustomItemsFromForm(root, state, customEditor.getCustomItemsOptions());
|
||||
}
|
||||
|
||||
if (state.itemSource === 'external') {
|
||||
const urlInputs = root.querySelectorAll('[id^="hsae-external-list-url-"]');
|
||||
state.externalListUrls = Array.from(urlInputs).map((el) => (el.value || '').trim()).filter(Boolean);
|
||||
if (!state.externalListUrls.length) state.externalListUrls = [''];
|
||||
}
|
||||
|
||||
if (state.itemSource === 'jellyfin') {
|
||||
const hideWatched = root.querySelector('#hsae-hide-watched')?.checked === true;
|
||||
const editors = root.querySelectorAll('.query-editor');
|
||||
@@ -1354,6 +1369,7 @@
|
||||
const endpointDisplay = sourceType === 'jellyfin' || sourceType === 'static' ? 'block' : 'none';
|
||||
const jellyfinDisplay = sourceType === 'jellyfin' ? 'block' : 'none';
|
||||
const staticDisplay = sourceType === 'static' ? 'block' : 'none';
|
||||
const sortExtrasDisplay = sourceType === 'jellyfin' || sourceType === 'static' ? 'block' : 'none';
|
||||
const staticItems = getQueryStaticItems(query);
|
||||
|
||||
const parentSelection = getQueryParentSelection(query);
|
||||
@@ -1396,17 +1412,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hsae-field-grid hsae-query-row-sort${sourceType === 'static' ? ' hsae-field-grid-2' : ''}">
|
||||
<div class="hsae-field-grid hsae-query-row-sort">
|
||||
<div class="hsae-query-sort-by" data-hsae-profiles="full normal dateCutoffs">
|
||||
${buildSelect(`query-${index}-SortBy`, toSortBySelectOptions(SORT_ORDERS), queryOptions.SortBy || '', 'Sort By')}
|
||||
</div>
|
||||
<div class="hsae-query-sort-order" data-hsae-profiles="full normal dateCutoffs">
|
||||
${buildSelect(`query-${index}-SortOrder`, SORT_ORDER_DIRECTIONS, queryOptions.SortOrder || '', 'Sort Order')}
|
||||
</div>
|
||||
<div class="hsae-query-limit hsae-query-jellyfin-only" data-hsae-profiles="full minimal postProcessing normal dateCutoffs" style="display:${jellyfinDisplay};">
|
||||
<div class="hsae-query-limit hsae-query-sort-extras" data-hsae-profiles="full minimal postProcessing normal dateCutoffs" style="display:${sortExtrasDisplay};">
|
||||
${buildTextInput(`query-${index}-Limit`, queryOptions.Limit ?? '', 'Limit', 'number', '', { min: 0, step: 1 })}
|
||||
</div>
|
||||
<div class="hsae-query-search hsae-query-jellyfin-only" data-hsae-profiles="full" style="display:${jellyfinDisplay};">
|
||||
<div class="hsae-query-search hsae-query-sort-extras" data-hsae-profiles="full" style="display:${sortExtrasDisplay};">
|
||||
${buildTextInput(`query-${index}-SearchTerm`, queryOptions.SearchTerm || '', 'Search Term')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1507,13 +1523,15 @@
|
||||
<div class="listItemBodyText secondary hsae-group-desc">General configuration for this section</div>
|
||||
</div>
|
||||
<div class="hsae-card">
|
||||
<div class="hsae-inline-row">
|
||||
<div class="hsae-field-grid hsae-field-grid-2 hsae-basic-name-caption">
|
||||
${buildTextInput('section-name', section.name || state.sectionName || '', 'Section Name')}
|
||||
<div data-hsae-profiles="full">
|
||||
${buildSelect('section-group', groupOptions, selectedGroup, 'Section Group')}
|
||||
<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>
|
||||
${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">
|
||||
${buildSelect('section-group', groupOptions, selectedGroup, 'Section Group')}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -1606,11 +1624,49 @@
|
||||
<div class="listItemBodyText secondary hsae-source-desc">${escapeHtml(CUSTOM_SOURCE_DESC)}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" class="hsae-source-card${itemSource === 'external' ? ' hsae-active' : ''}" data-hsae-action="set-item-source" data-item-source="external">
|
||||
<div class="hsae-source-card-text">
|
||||
<div class="listItemBodyText hsae-source-title">
|
||||
<span class="material-icons hsae-source-icon">link</span>
|
||||
External List
|
||||
</div>
|
||||
<div class="listItemBodyText secondary hsae-source-desc">Match an MDBList (or compatible) URL against your Jellyfin library by provider IDs</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildExternalListHTML(state) {
|
||||
const { buildTextInput } = getApi();
|
||||
const urls = Array.isArray(state.externalListUrls) && state.externalListUrls.length
|
||||
? state.externalListUrls
|
||||
: [''];
|
||||
const apiKey = window.LibraryCacheUtils?.getLibraryCacheSettings?.()?.mdblistApiKey
|
||||
|| window.KefinHomeConfig2?.LIBRARY_CACHE?.mdblistApiKey
|
||||
|| '';
|
||||
const warn = !String(apiKey || '').trim()
|
||||
? `<div class="listItemBodyText secondary" style="color: var(--error-color, #e57373); margin-bottom: 0.75em;">MDBList API key is not configured. Add it under Home Screen → General settings before using External Lists.</div>`
|
||||
: '';
|
||||
const rows = urls.map((url, index) => `
|
||||
<div class="hsae-external-list-row" data-external-index="${index}" style="margin-bottom: 0.75em;">
|
||||
${buildTextInput(`hsae-external-list-url-${index}`, url || '', `List URL ${index + 1}`, 'url', 'https://mdblist.com/...')}
|
||||
</div>
|
||||
`).join('');
|
||||
return `
|
||||
<section class="hsae-group hsae-external-list-group" data-hsae-profiles="full">
|
||||
<div class="hsae-group-header">
|
||||
<div class="listItemBodyText hsae-group-title">External List URLs</div>
|
||||
<div class="listItemBodyText secondary hsae-group-desc">Items from these lists are matched to your Jellyfin library by IMDb/TMDB ids</div>
|
||||
</div>
|
||||
${warn}
|
||||
<div class="hsae-external-list-rows">${rows}</div>
|
||||
<button type="button" class="emby-button raised" data-hsae-action="add-external-list-url" style="margin-top: 0.75em;">Add URL</button>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildQueriesHTML(state) {
|
||||
const queries = state.queries?.length ? state.queries : [createDefaultQuery()];
|
||||
const expandedStates = state.queryExpandedStates;
|
||||
@@ -1790,6 +1846,7 @@
|
||||
itemSource: inferItemSource(draft),
|
||||
queries: draft?.queries,
|
||||
items: draft?.items,
|
||||
externalListUrls: draft?.externalListUrls,
|
||||
hideWatched: Array.isArray(draft?.queries)
|
||||
&& draft.queries.length > 0
|
||||
&& draft.queries.every(q => q?.queryOptions?.IsUnplayed === true)
|
||||
@@ -2157,6 +2214,7 @@
|
||||
${buildItemSourceHTML(state)}
|
||||
${buildQueriesHTML(state)}
|
||||
${buildCustomItemsHTML(state)}
|
||||
${buildExternalListHTML(state)}
|
||||
${buildAdvancedOptionsHTML(state)}
|
||||
`;
|
||||
}
|
||||
@@ -2349,6 +2407,9 @@
|
||||
const userConfigChecked = dialog.querySelector('#hsae-user-configurable')?.checked === true;
|
||||
|
||||
section.name = dialog.querySelector('#section-name')?.value || '';
|
||||
const captionVal = dialog.querySelector('#section-caption')?.value?.trim() || '';
|
||||
if (captionVal) section.caption = captionVal;
|
||||
else section.caption = '';
|
||||
section.enabled = userEnabledChecked;
|
||||
section.userConfigurable = userConfigChecked;
|
||||
|
||||
@@ -2402,8 +2463,16 @@
|
||||
section.items = [];
|
||||
}
|
||||
section.queries = [];
|
||||
delete section.externalListUrls;
|
||||
} else if (itemSource === 'external') {
|
||||
delete section.items;
|
||||
section.queries = [];
|
||||
const urlInputs = dialog.querySelectorAll('[id^="hsae-external-list-url-"]');
|
||||
const urls = Array.from(urlInputs).map((el) => (el.value || '').trim()).filter(Boolean);
|
||||
section.externalListUrls = urls;
|
||||
} else {
|
||||
delete section.items;
|
||||
delete section.externalListUrls;
|
||||
const queryEditors = dialog.querySelectorAll('.query-editor');
|
||||
if (queryEditors.length > 0) {
|
||||
section.queries = Array.from(queryEditors).map(editor => {
|
||||
@@ -2563,6 +2632,7 @@
|
||||
const cacheWrap = editor.querySelector('.hsae-cache-wrap');
|
||||
const jellyfinOnlyBlocks = editor.querySelectorAll('.hsae-query-jellyfin-only');
|
||||
const staticOnlyBlocks = editor.querySelectorAll('.hsae-query-static-only');
|
||||
const sortExtrasBlocks = editor.querySelectorAll('.hsae-query-sort-extras');
|
||||
|
||||
const updateSourceVisibility = () => {
|
||||
const sourceType = sourceSelect?.value || 'jellyfin';
|
||||
@@ -2578,6 +2648,9 @@
|
||||
staticOnlyBlocks.forEach(block => {
|
||||
block.style.display = sourceType === 'static' ? '' : 'none';
|
||||
});
|
||||
sortExtrasBlocks.forEach(block => {
|
||||
block.style.display = sourceType === 'jellyfin' || sourceType === 'static' ? '' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
const updateEndpointVisibility = () => {
|
||||
@@ -3193,13 +3266,17 @@
|
||||
const newSource = actionEl.dataset.itemSource;
|
||||
if (!newSource || state.itemSource === newSource) return;
|
||||
|
||||
// Keep both queries and customItems in memory while switching;
|
||||
// Keep sources in memory while switching;
|
||||
// collectData clears the inactive source only on save/preview.
|
||||
collectStateFromForm(root, state);
|
||||
state.itemSource = newSource;
|
||||
if (newSource === 'custom') {
|
||||
const customEditor = getCustomItemsEditor();
|
||||
if (customEditor) customEditor.ensureCustomItemsInitialized(state);
|
||||
} else if (newSource === 'external') {
|
||||
if (!Array.isArray(state.externalListUrls) || !state.externalListUrls.length) {
|
||||
state.externalListUrls = [''];
|
||||
}
|
||||
} else {
|
||||
state.queries = state.queries?.length ? state.queries : [createDefaultQuery()];
|
||||
}
|
||||
@@ -3207,6 +3284,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'add-external-list-url') {
|
||||
collectStateFromForm(root, state);
|
||||
if (!Array.isArray(state.externalListUrls)) state.externalListUrls = [''];
|
||||
state.externalListUrls.push('');
|
||||
refreshEditorBody(modalInstance, { skipCollect: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'delete-query') {
|
||||
e.stopPropagation();
|
||||
const index = parseInt(actionEl.dataset.queryIndex, 10);
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
|
||||
const QUERY_WIZARD_STEPS = ['type', 'parent', 'query', 'settings', 'appearance', 'confirm'];
|
||||
const CUSTOM_WIZARD_STEPS = ['type', 'customItems', 'settings', 'appearance', 'confirm'];
|
||||
const EXTERNAL_WIZARD_STEPS = ['type', 'externalList', 'settings', 'appearance', 'confirm'];
|
||||
const WIZARD_STEP_LABELS = {
|
||||
type: 'Type',
|
||||
parent: 'Selection',
|
||||
query: 'Query',
|
||||
customItems: 'Create Items',
|
||||
externalList: 'External List',
|
||||
appearance: 'Appearance',
|
||||
settings: 'Settings',
|
||||
confirm: 'Confirm'
|
||||
@@ -28,6 +30,13 @@
|
||||
icon: 'widgets'
|
||||
};
|
||||
|
||||
const EXTERNAL_TYPE = {
|
||||
type: 'External',
|
||||
label: 'External List Section',
|
||||
description: 'Match an MDBList (or compatible) URL against your Jellyfin library by IMDb/TMDB provider IDs. Requires an MDBList API key in Home Screen General settings.',
|
||||
icon: 'link'
|
||||
};
|
||||
|
||||
const CUSTOM_IMAGE_FIELDS = [
|
||||
{ key: 'posterUrl', label: 'Poster URL', required: true },
|
||||
{ key: 'thumbUrl', label: 'Thumb URL' },
|
||||
@@ -77,13 +86,19 @@
|
||||
// --- Wizard parent data (live Jellyfin API) ---
|
||||
|
||||
function getWizardSteps(state) {
|
||||
return state?.parentType === 'Custom' ? CUSTOM_WIZARD_STEPS : QUERY_WIZARD_STEPS;
|
||||
if (state?.parentType === 'Custom') return CUSTOM_WIZARD_STEPS;
|
||||
if (state?.parentType === 'External') return EXTERNAL_WIZARD_STEPS;
|
||||
return QUERY_WIZARD_STEPS;
|
||||
}
|
||||
|
||||
function isCustomType(state) {
|
||||
return state?.parentType === 'Custom';
|
||||
}
|
||||
|
||||
function isExternalType(state) {
|
||||
return state?.parentType === 'External';
|
||||
}
|
||||
|
||||
function getStepIndex(step, state) {
|
||||
return getWizardSteps(state).indexOf(step);
|
||||
}
|
||||
@@ -251,6 +266,9 @@
|
||||
case 'customItems':
|
||||
collectCustomItemsFromForm(root, wizardState);
|
||||
break;
|
||||
case 'externalList':
|
||||
collectExternalListFromForm(root, wizardState);
|
||||
break;
|
||||
case 'appearance':
|
||||
collectAppearanceStepFromForm(root, wizardState);
|
||||
break;
|
||||
@@ -1093,6 +1111,12 @@
|
||||
}))
|
||||
});
|
||||
}
|
||||
if (isExternalType(wizardState)) {
|
||||
return JSON.stringify({
|
||||
parentType: 'External',
|
||||
externalListUrls: wizardState.externalListUrls || []
|
||||
});
|
||||
}
|
||||
const parents = wizardState.parents || [];
|
||||
return JSON.stringify({
|
||||
parentType: wizardState.parentType,
|
||||
@@ -1372,6 +1396,12 @@
|
||||
`;
|
||||
}
|
||||
|
||||
function collectExternalListFromForm(root, wizardState) {
|
||||
const inputs = root.querySelectorAll('[id^="hsse-external-list-url-"]');
|
||||
wizardState.externalListUrls = Array.from(inputs).map((el) => (el.value || '').trim()).filter(Boolean);
|
||||
if (!wizardState.externalListUrls.length) wizardState.externalListUrls = [''];
|
||||
}
|
||||
|
||||
function collectSettingsFromForm(root, wizardState) {
|
||||
const nameEl = root.querySelector('#hsse-wizard-sectionName');
|
||||
const groupSelect = root.querySelector('#hsse-wizard-sectionGroup');
|
||||
@@ -1410,19 +1440,23 @@
|
||||
|
||||
function renderConfirmOverview(wizardState, config) {
|
||||
const isCustom = isCustomType(wizardState);
|
||||
const meta = isCustom ? CUSTOM_TYPE : PARENT_TYPES.find(t => t.type === wizardState.parentType);
|
||||
const isExternal = isExternalType(wizardState);
|
||||
const meta = isCustom ? CUSTOM_TYPE
|
||||
: (isExternal ? EXTERNAL_TYPE : PARENT_TYPES.find(t => t.type === wizardState.parentType));
|
||||
const parents = wizardState.parents || [];
|
||||
const selectionLabel = isCustom
|
||||
? ((wizardState.customItems || []).map(i => i.name?.trim()).filter(Boolean).join(', ') || '—')
|
||||
: (parents.length ? parents.map(p => p.name).join(', ') : '—');
|
||||
: (isExternal
|
||||
? ((wizardState.externalListUrls || []).filter(Boolean).join(', ') || '—')
|
||||
: (parents.length ? parents.map(p => p.name).join(', ') : '—'));
|
||||
const rows = [
|
||||
['Section Name', wizardState.sectionName || '—'],
|
||||
['Section Group', resolveGroupName(wizardState, config)],
|
||||
['Based On', meta?.label || wizardState.parentType || '—'],
|
||||
[isCustom ? 'Items' : 'Selection', selectionLabel]
|
||||
[isCustom ? 'Items' : (isExternal ? 'List URLs' : 'Selection'), selectionLabel]
|
||||
];
|
||||
|
||||
if (!isCustom) {
|
||||
if (!isCustom && !isExternal) {
|
||||
rows.push(
|
||||
['Item Types', wizardState.includeItemTypes.length ? formatItemTypesLabel(wizardState.includeItemTypes) : 'All types'],
|
||||
['Sort By', wizardState.sortBy === 'Default' ? 'Default' : `${humanizeLabel(wizardState.sortBy)} (${wizardState.sortOrder})`],
|
||||
@@ -1547,6 +1581,39 @@
|
||||
return section;
|
||||
}
|
||||
|
||||
if (parentType === 'External') {
|
||||
const urls = (wizardState.externalListUrls || []).map((u) => String(u || '').trim()).filter(Boolean);
|
||||
const section = {
|
||||
...baseSection,
|
||||
id: baseSection.id,
|
||||
name: sectionName || 'External List',
|
||||
enabled: true,
|
||||
isCustom: true,
|
||||
renderMode: renderMode || 'Normal',
|
||||
cardFormat: renderMode === 'Spotlight' ? undefined : (cardFormat || 'Poster'),
|
||||
externalListUrls: urls,
|
||||
queries: []
|
||||
};
|
||||
|
||||
applySectionVisibilityToSection(section, wizardState, baseSection);
|
||||
|
||||
const targetGroup = resolveGroupName(wizardState, wizardState._config);
|
||||
if (targetGroup) {
|
||||
section._targetGroupName = targetGroup;
|
||||
}
|
||||
|
||||
if (renderMode === 'Spotlight' || renderMode === 'Random') {
|
||||
section.spotlightConfig = {
|
||||
spotlightLayout: spotlightLayout || 'Border',
|
||||
spotlightSize: spotlightSize || 'normal',
|
||||
tileCount: Math.max(1, Math.min(3, parseInt(tileCount, 10) || 1)),
|
||||
panAnimation: panAnimation !== false
|
||||
};
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
const queryOptions = {
|
||||
Limit: DEFAULT_SECTION_QUERY_LIMIT
|
||||
};
|
||||
@@ -1820,6 +1887,27 @@
|
||||
`;
|
||||
}
|
||||
|
||||
function renderExternalListHTML(wizardState) {
|
||||
const urls = Array.isArray(wizardState.externalListUrls) && wizardState.externalListUrls.length
|
||||
? wizardState.externalListUrls
|
||||
: [''];
|
||||
const apiKey = window.LibraryCacheUtils?.getLibraryCacheSettings?.()?.mdblistApiKey
|
||||
|| window.KefinHomeConfig2?.LIBRARY_CACHE?.mdblistApiKey
|
||||
|| '';
|
||||
const warn = !String(apiKey || '').trim()
|
||||
? `<p class="listItemBodyText secondary" style="color: var(--error-color, #e57373);">MDBList API key is not configured. Add it under Home Screen → General settings before using External Lists.</p>`
|
||||
: '';
|
||||
return `
|
||||
<div class="hsse-section-block">
|
||||
<div class="listItemBodyText hsse-section-label">External List URLs</div>
|
||||
<p class="listItemBodyText secondary hsse-field-hint">Paste one or more MDBList URLs. Matched against your library by IMDb/TMDB ids.</p>
|
||||
${warn}
|
||||
${urls.map((url, index) => hsseBuildTextInput(`hsse-external-list-url-${index}`, url || '', `List URL ${index + 1}`, 'url', 'https://mdblist.com/...')).join('')}
|
||||
<button type="button" class="emby-button raised" data-hsse-action="wizard-add-external-list-url" style="margin-top: 0.75em;">Add URL</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWizardBodyHTML(wizardState, config) {
|
||||
const step = wizardState.step;
|
||||
|
||||
@@ -1842,6 +1930,13 @@
|
||||
<div class="listItemBodyText secondary hsse-type-desc">${escapeHtml(CUSTOM_TYPE.description)}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" class="hsse-type-card hsse-type-card-span${wizardState.parentType === 'External' ? ' hsse-active' : ''}" data-hsse-action="wizard-select-type" data-type="External">
|
||||
${renderMaterialIcon(EXTERNAL_TYPE.icon, 'hsse-type-icon')}
|
||||
<div>
|
||||
<div class="listItemBodyText hsse-type-label">${escapeHtml(EXTERNAL_TYPE.label)}</div>
|
||||
<div class="listItemBodyText secondary hsse-type-desc">${escapeHtml(EXTERNAL_TYPE.description)}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1850,6 +1945,10 @@
|
||||
return renderCustomItemsHTML(wizardState);
|
||||
}
|
||||
|
||||
if (step === 'externalList') {
|
||||
return renderExternalListHTML(wizardState);
|
||||
}
|
||||
|
||||
if (step === 'parent') {
|
||||
const meta = PARENT_TYPES.find(t => t.type === wizardState.parentType);
|
||||
|
||||
@@ -1960,6 +2059,7 @@
|
||||
return `Choose a ${meta?.label?.toLowerCase() || 'item'}`;
|
||||
}
|
||||
if (wizardState.step === 'customItems') return 'Create Items';
|
||||
if (wizardState.step === 'externalList') return 'External List';
|
||||
return 'Create a Home Screen Section';
|
||||
}
|
||||
return 'Create a Section';
|
||||
@@ -3748,6 +3848,7 @@
|
||||
parentListError: null,
|
||||
personSearchResults: [],
|
||||
customItems: [],
|
||||
externalListUrls: [''],
|
||||
customItemAdditionalExpanded: {},
|
||||
customItemEditorIndex: null,
|
||||
includeItemTypes: [],
|
||||
@@ -4116,20 +4217,38 @@
|
||||
if (selected === 'Custom') {
|
||||
wizardState.customItems = [createBlankCustomItem()];
|
||||
wizardState.customItemEditorIndex = null;
|
||||
wizardState.externalListUrls = [''];
|
||||
advanceToStep(root, 'customItems');
|
||||
} else if (selected === 'External') {
|
||||
wizardState.customItems = [];
|
||||
wizardState.externalListUrls = wizardState.externalListUrls?.length
|
||||
? wizardState.externalListUrls
|
||||
: [''];
|
||||
advanceToStep(root, 'externalList');
|
||||
} else {
|
||||
wizardState.customItems = [];
|
||||
wizardState.externalListUrls = [''];
|
||||
advanceToStep(root, 'parent');
|
||||
}
|
||||
} else if (selected === 'Custom') {
|
||||
wizardState.customItemEditorIndex = null;
|
||||
advanceToStep(root, 'customItems');
|
||||
} else if (selected === 'External') {
|
||||
advanceToStep(root, 'externalList');
|
||||
} else {
|
||||
advanceToStep(root, 'parent');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'wizard-add-external-list-url': {
|
||||
collectExternalListFromForm(root, wizardState);
|
||||
if (!Array.isArray(wizardState.externalListUrls)) wizardState.externalListUrls = [''];
|
||||
wizardState.externalListUrls.push('');
|
||||
refreshModal(root);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'wizard-open-custom-item-editor': {
|
||||
const openIndex = parseInt(el.dataset.itemIndex, 10);
|
||||
if (Number.isNaN(openIndex)) break;
|
||||
@@ -4297,6 +4416,14 @@
|
||||
wizardState.sectionName = 'Custom Section';
|
||||
}
|
||||
}
|
||||
if (wizardState.step === 'externalList') {
|
||||
const urls = (wizardState.externalListUrls || []).map((u) => String(u || '').trim()).filter(Boolean);
|
||||
if (!urls.length) break;
|
||||
wizardState.externalListUrls = urls;
|
||||
if (!wizardState.sectionName?.trim()) {
|
||||
wizardState.sectionName = 'External List';
|
||||
}
|
||||
}
|
||||
if (wizardState.step === 'settings' && wizardState.sectionVisibility === 'seasonal') {
|
||||
if (!hasValidSeasonalDates(wizardState)) break;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user