mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Fix cases where Home Screen wouldn't correctly render
This commit is contained in:
@@ -77,6 +77,10 @@
|
||||
[data-modal-id="kefin-homescreen-config-main"] div#section-content {
|
||||
max-height: calc(90vh - 19em);
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
[data-modal-id="kefin-homescreen-config-main"] #section-content > div > div:last-child {
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
|
||||
div#order-sections-container .editable-group-name {
|
||||
@@ -342,3 +346,21 @@ span.listItemBodyText.secondary.seasonal-tag {
|
||||
[data-modal-id="kefin-homescreen-config-main"] .hsc-settings-grid .hsc-settings-span-2 .kefin-toggle-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] .kefin-settings-input-card {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] .kefin-settings-input-card-control {
|
||||
flex-shrink: 0;
|
||||
width: 7.5em;
|
||||
}
|
||||
|
||||
[data-modal-id="kefin-homescreen-config-main"] .kefin-settings-input-card-control .emby-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-row-actions .btnSectionConfigure {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
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.',
|
||||
on: 'Adds a 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. The button is hidden on mobile.',
|
||||
off: 'You must use the KefinTweaks Configuration > Home Screen > New Section flow to create new Home Screen sections.'
|
||||
},
|
||||
'home-showCategoryFilters': {
|
||||
@@ -2247,7 +2247,7 @@
|
||||
const D = GENERAL_TOGGLE_DESCRIPTIONS;
|
||||
const maxPeopleCount = homeSettings.maxPeopleCount ?? 100;
|
||||
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;';
|
||||
const settingsGroupStyle = 'margin-top: 0.75em; 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)}
|
||||
@@ -2263,12 +2263,19 @@
|
||||
${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)}
|
||||
${hscToggleCard('home-showCreateSectionButtonOnHome', homeSettings.showCreateSectionButtonOnHome === true, 'Quick Create Button', 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;">
|
||||
When a section loads with no items: <code>0</code> removes it immediately. Any other value shows “No items found” for that many milliseconds, then fades the section out.
|
||||
<div class="kefin-toggle-card kefin-settings-input-card">
|
||||
<div class="kefin-toggle-card-text">
|
||||
<label class="listItemBodyText kefin-toggle-card-label" for="home-dismissEmptySectionTimer">Empty Section Dismiss Timer (ms)</label>
|
||||
<div class="listItemBodyText secondary kefin-toggle-card-desc">
|
||||
When a section loads with no items: 0 removes it immediately. Any other value shows “No items found” for that many milliseconds, then fades the section out.
|
||||
</div>
|
||||
</div>
|
||||
<div class="kefin-settings-input-card-control">
|
||||
<input type="number" id="home-dismissEmptySectionTimer" class="fld emby-input" value="${homeSettings.dismissEmptySectionTimer ?? 0}" min="0" step="1" aria-label="Empty Section Dismiss Timer (ms)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2622,6 +2629,22 @@
|
||||
const root = dialog?.querySelector('#global-settings-content') || dialog;
|
||||
if (!root) return;
|
||||
|
||||
if (root.querySelector('#home-showCategoryFilters')
|
||||
|| root.querySelector('#home-renderCategoriesSeparately')
|
||||
|| root.querySelector('#kefin-home-category-list')) {
|
||||
currentConfig.HOME_SETTINGS = {
|
||||
...(currentConfig.HOME_SETTINGS || {}),
|
||||
showCategoryFilters: root.querySelector('#home-showCategoryFilters')?.checked === true,
|
||||
renderCategoriesSeparately: root.querySelector('#home-renderCategoriesSeparately')?.checked === true,
|
||||
...(root.querySelector('#kefin-home-category-list')
|
||||
? { categories: normalizeHomeCategories(collectCategoriesFromForm(root)) }
|
||||
: {})
|
||||
};
|
||||
try {
|
||||
window.homeScreen3?.refreshHomeCategoryChrome?.();
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (root.querySelector('#discovery-enabled')) {
|
||||
currentConfig.DISCOVERY_SETTINGS = {
|
||||
...(currentConfig.DISCOVERY_SETTINGS || {}),
|
||||
@@ -5100,12 +5123,92 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovery templates need a source pick before queries are concrete.
|
||||
*/
|
||||
function sectionNeedsDiscoveryResolve(section) {
|
||||
if (!section) return false;
|
||||
if (Array.isArray(section.items) && section.items.length > 0) return false;
|
||||
if (Array.isArray(section.externalListUrls) && section.externalListUrls.length > 0) return false;
|
||||
if (section.discoveryType) return true;
|
||||
if (section.discoveryEnabled === true) return true;
|
||||
const type = String(section.type || '').toLowerCase();
|
||||
return ['genre', 'studio', 'person', 'collection', 'similar', 'watchlist', 'tag'].includes(type);
|
||||
}
|
||||
|
||||
function snapshotDiscoveryDedupeState(discoveryState) {
|
||||
if (!discoveryState?.renderedDiscoveryIds) return null;
|
||||
const renderedDiscoveryIds = {};
|
||||
Object.keys(discoveryState.renderedDiscoveryIds).forEach((key) => {
|
||||
const set = discoveryState.renderedDiscoveryIds[key];
|
||||
renderedDiscoveryIds[key] = set instanceof Set ? new Set(set) : new Set();
|
||||
});
|
||||
return {
|
||||
renderedDiscoveryIds,
|
||||
currentDiscoveryGenre: discoveryState.currentDiscoveryGenre ?? null,
|
||||
currentDiscoveryStudio: discoveryState.currentDiscoveryStudio ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function restoreDiscoveryDedupeState(discoveryState, snapshot) {
|
||||
if (!discoveryState?.renderedDiscoveryIds || !snapshot?.renderedDiscoveryIds) return;
|
||||
Object.keys(discoveryState.renderedDiscoveryIds).forEach((key) => {
|
||||
const live = discoveryState.renderedDiscoveryIds[key];
|
||||
if (!(live instanceof Set)) return;
|
||||
live.clear();
|
||||
const saved = snapshot.renderedDiscoveryIds[key];
|
||||
if (saved instanceof Set) saved.forEach((id) => live.add(id));
|
||||
});
|
||||
discoveryState.currentDiscoveryGenre = snapshot.currentDiscoveryGenre ?? null;
|
||||
discoveryState.currentDiscoveryStudio = snapshot.currentDiscoveryStudio ?? null;
|
||||
}
|
||||
|
||||
async function resolveDiscoverySectionForPreview(section) {
|
||||
const helper = window.sectionHelper;
|
||||
if (!helper?.buildDiscoverySectionInstance) {
|
||||
throw new Error('Discovery resolver unavailable for preview');
|
||||
}
|
||||
const discoveryState = helper.getDiscoveryState?.();
|
||||
const snapshot = snapshotDiscoveryDedupeState(discoveryState);
|
||||
try {
|
||||
const resolved = await helper.buildDiscoverySectionInstance(section, {
|
||||
pairSpotlight: false,
|
||||
resolveViewMore: false
|
||||
});
|
||||
if (!resolved?.queries?.length && !resolved?.items?.length) {
|
||||
throw new Error('Could not resolve discovery source for preview');
|
||||
}
|
||||
return resolved;
|
||||
} finally {
|
||||
restoreDiscoveryDedupeState(discoveryState, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay resolved discovery display fields onto a live draft so appearance
|
||||
* edits stay on the draft while titles/captions come from resolve.
|
||||
* @param {Object} draft
|
||||
* @param {Object|null|undefined} resolved
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mergeDraftWithResolvedPreview(draft, resolved) {
|
||||
if (!draft) return draft;
|
||||
if (!resolved) return draft;
|
||||
const merged = { ...draft };
|
||||
if (resolved.name != null) merged.name = resolved.name;
|
||||
if (resolved.caption != null) merged.caption = resolved.caption;
|
||||
if (resolved.captionUrl != null) merged.captionUrl = resolved.captionUrl;
|
||||
if (resolved.viewMoreUrl != null) merged.viewMoreUrl = resolved.viewMoreUrl;
|
||||
if (resolved.metadata != null) merged.metadata = resolved.metadata;
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch items for a section preview (shared by modal preview and wizard inline preview).
|
||||
* @param {Object} section
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.useSpotlightFields] - Request spotlight-grade Fields regardless of renderMode
|
||||
* @returns {Promise<{ items: Array, limit: number }>}
|
||||
* @returns {Promise<{ items: Array, limit: number, section: Object }>}
|
||||
*/
|
||||
async function fetchSectionPreviewItems(section, options = {}) {
|
||||
if (!window.ApiClient || !window.apiHelper || !window.cardBuilder) {
|
||||
@@ -5119,27 +5222,32 @@
|
||||
throw new Error('Unable to get user ID or server URL');
|
||||
}
|
||||
|
||||
const queries = section.queries || [];
|
||||
if (Array.isArray(section.externalListUrls) && section.externalListUrls.length > 0) {
|
||||
let previewSection = section;
|
||||
if (sectionNeedsDiscoveryResolve(section)) {
|
||||
previewSection = await resolveDiscoverySectionForPreview(section);
|
||||
}
|
||||
|
||||
const queries = previewSection.queries || [];
|
||||
if (Array.isArray(previewSection.externalListUrls) && previewSection.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 listResult = await window.KefinExternalList.resolveExternalListItems(previewSection);
|
||||
const items = listResult?.Items || [];
|
||||
const processed = window.cardBuilder.postProcessItems
|
||||
? window.cardBuilder.postProcessItems(section, items)
|
||||
? window.cardBuilder.postProcessItems(previewSection, items)
|
||||
: items;
|
||||
return { items: processed, limit: processed.length };
|
||||
return { items: processed, limit: processed.length, section: previewSection };
|
||||
}
|
||||
|
||||
if (section.items?.length) {
|
||||
if (previewSection.items?.length) {
|
||||
const kefinTweaksRoot = window.KefinTweaksConfig?.kefinTweaksRoot || '';
|
||||
const serverId = ApiClient.serverId();
|
||||
const normalizeTemplate = (value) => (value || '')
|
||||
.replace(/\$\{kefinTweaksRoot\}/g, kefinTweaksRoot)
|
||||
.replace(/\$\{serverId\}/g, serverId);
|
||||
|
||||
const items = section.items.map((item, index) => {
|
||||
const items = previewSection.items.map((item, index) => {
|
||||
const posterUrl = normalizeTemplate(item.posterUrl);
|
||||
const thumbUrl = normalizeTemplate(item.thumbUrl);
|
||||
const squareUrl = normalizeTemplate(item.squareUrl);
|
||||
@@ -5151,7 +5259,7 @@
|
||||
|
||||
return {
|
||||
Name: item.Name,
|
||||
Id: item.Id || 'static-preview-' + (section.id || 'draft') + '-' + index,
|
||||
Id: item.Id || 'static-preview-' + (previewSection.id || 'draft') + '-' + index,
|
||||
Type: item.Type || 'Folder',
|
||||
posterUrl,
|
||||
thumbUrl,
|
||||
@@ -5166,9 +5274,9 @@
|
||||
});
|
||||
|
||||
const processed = window.cardBuilder.postProcessItems
|
||||
? window.cardBuilder.postProcessItems(section, items)
|
||||
? window.cardBuilder.postProcessItems(previewSection, items)
|
||||
: items;
|
||||
return { items: processed, limit: processed.length };
|
||||
return { items: processed, limit: processed.length, section: previewSection };
|
||||
}
|
||||
|
||||
if (queries.length === 0) {
|
||||
@@ -5176,7 +5284,7 @@
|
||||
}
|
||||
|
||||
const useSpotlightFields = options.useSpotlightFields === true
|
||||
|| section.renderMode === 'Spotlight';
|
||||
|| previewSection.renderMode === 'Spotlight';
|
||||
|
||||
let allItems = [];
|
||||
for (const query of queries) {
|
||||
@@ -5186,7 +5294,7 @@
|
||||
if (previewQuery.dataSource) {
|
||||
queryUrl = null;
|
||||
} else {
|
||||
queryUrl = window.apiHelper.buildQueryFromSection(previewQuery, userId, serverUrl, useSpotlightFields, { sectionType: section.type });
|
||||
queryUrl = window.apiHelper.buildQueryFromSection(previewQuery, userId, serverUrl, useSpotlightFields, { sectionType: previewSection.type });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -5206,16 +5314,16 @@
|
||||
}
|
||||
|
||||
if (allItems.length === 0) {
|
||||
return { items: [], limit: 0 };
|
||||
return { items: [], limit: 0, section: previewSection };
|
||||
}
|
||||
|
||||
const limit = withPreviewQueryOptions(queries[0]).queryOptions.Limit
|
||||
|| section.itemLimit
|
||||
|| previewSection.itemLimit
|
||||
|| PREVIEW_DEFAULT_LIMIT;
|
||||
allItems = window.cardBuilder.postProcessItems(section, allItems);
|
||||
allItems = window.cardBuilder.postProcessItems(previewSection, allItems);
|
||||
const limitedItems = allItems.slice(0, parseInt(limit, 10));
|
||||
|
||||
return { items: limitedItems, limit: parseInt(limit, 10) };
|
||||
return { items: limitedItems, limit: parseInt(limit, 10), section: previewSection };
|
||||
}
|
||||
|
||||
const EDITOR_PREVIEW_SECTION_ID = 'hsae-editor-preview';
|
||||
@@ -5280,8 +5388,9 @@
|
||||
|
||||
try {
|
||||
const draft = getDraftSection();
|
||||
const { items } = await fetchSectionPreviewItems(draft);
|
||||
await renderSectionPreviewInto(draft, items, containerEl, { getDraftSection });
|
||||
const { items, section: resolvedSection } = await fetchSectionPreviewItems(draft);
|
||||
const previewSection = mergeDraftWithResolvedPreview(draft, resolvedSection);
|
||||
await renderSectionPreviewInto(previewSection, items, containerEl, { getDraftSection });
|
||||
} catch (err) {
|
||||
console.error('[Preview] Refresh failed:', err);
|
||||
containerEl.innerHTML = `<p class="listItemBodyText secondary" style="text-align:center;padding:1.5em 0;">${err.message || 'Preview refresh failed'}</p>`;
|
||||
@@ -5363,7 +5472,8 @@
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
try {
|
||||
const { items } = await fetchSectionPreviewItems(section);
|
||||
const { items, section: resolvedSection } = await fetchSectionPreviewItems(section);
|
||||
const previewSection = mergeDraftWithResolvedPreview(section, resolvedSection);
|
||||
|
||||
if (items.length === 0) {
|
||||
showToast('No items found for preview');
|
||||
@@ -5388,7 +5498,7 @@
|
||||
content.style.cssText = 'padding: 1em; max-width: 1400px; width: 100%;';
|
||||
content.innerHTML = `
|
||||
<div class="listItemBodyText" style="margin-bottom: 1em;">
|
||||
Preview: ${section.name || 'Unnamed Section'} (${items.length} item${items.length !== 1 ? 's' : ''})
|
||||
Preview: ${previewSection.name || 'Unnamed Section'} (${items.length} item${items.length !== 1 ? 's' : ''})
|
||||
</div>
|
||||
<div id="preview-container" style="min-height: 400px;"></div>
|
||||
`;
|
||||
@@ -5407,7 +5517,7 @@
|
||||
onOpen: async (modalInstance) => {
|
||||
const container = modalInstance.dialogContent.querySelector('#preview-container');
|
||||
modalInstance.dialog.style.width = '1400px';
|
||||
await renderSectionPreviewInto(section, items, container);
|
||||
await renderSectionPreviewInto(previewSection, items, container);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5427,6 +5537,7 @@
|
||||
window.KefinHomeScreen.getConfig = getConfig;
|
||||
window.KefinHomeScreen.getSections = getSections;
|
||||
window.KefinHomeScreen.fetchSectionPreviewItems = fetchSectionPreviewItems;
|
||||
window.KefinHomeScreen.mergeDraftWithResolvedPreview = mergeDraftWithResolvedPreview;
|
||||
window.KefinHomeScreen.renderSectionPreviewInto = renderSectionPreviewInto;
|
||||
window.KefinHomeScreen.buildEditorPreviewProgressiveSection = buildEditorPreviewProgressiveSection;
|
||||
window.KefinHomeScreen.canOpenSectionEditor = canOpenSectionEditor;
|
||||
|
||||
@@ -945,9 +945,11 @@
|
||||
function getServerSectionDefaults(section) {
|
||||
if (!section) return {};
|
||||
const spotlight = section.spotlightConfig || {};
|
||||
const id = String(section.id || '');
|
||||
const isPinned = id.startsWith(PINNED_LIST_ID_PREFIX) || id.startsWith(PINNED_PARENT_ID_PREFIX);
|
||||
return {
|
||||
renderMode: resolveRenderMode(section),
|
||||
order: section.order ?? 0,
|
||||
order: isPinned ? PINNED_DEFAULT_ORDER : (section.order ?? 0),
|
||||
ttl: section.ttl,
|
||||
cardFormat: section.cardFormat,
|
||||
animationEnabled: spotlight.panAnimation !== false,
|
||||
@@ -1512,11 +1514,13 @@
|
||||
return pref?.id && getPrefDedupeKey(pref) === storedId;
|
||||
});
|
||||
const existing = parseSectionPrefString(existingRaw) || {};
|
||||
const serverDefaults = section._userPrefDefaults
|
||||
|| getServerSectionDefaults(serverSection);
|
||||
return serializeSectionPref(
|
||||
storedId,
|
||||
section.enabled !== false,
|
||||
{ ...existing, order: section.order },
|
||||
getServerSectionDefaults(serverSection)
|
||||
serverDefaults
|
||||
);
|
||||
});
|
||||
return sanitizeHomeScreenSections({
|
||||
@@ -1855,7 +1859,7 @@
|
||||
const modalId = 'kefin-user-homescreen-restore-defaults';
|
||||
const content = '<p class="listItemBodyText">Restoring the default Home Screen Settings will override your existing settings. Are you sure you want to proceed?</p>';
|
||||
const footer = `
|
||||
<button type="button" class="emby-button" onclick="window.ModalSystem.close('${modalId}')">Cancel</button>
|
||||
<button type="button" class="emby-button button-cancel button-flat" onclick="window.ModalSystem.close('${modalId}')">Cancel</button>
|
||||
<button type="button" class="emby-button raised button-submit" id="kefin-restore-defaults-confirm">Proceed</button>
|
||||
`;
|
||||
window.ModalSystem.create({
|
||||
|
||||
@@ -1596,7 +1596,7 @@
|
||||
.hsae-root[data-editor-profile="normal"] .hsae-toggle-card-grid > [data-hsae-profiles~="normal"],
|
||||
.hsae-root[data-editor-profile="dateCutoffs"] .hsae-toggle-card-grid > [data-hsae-profiles~="dateCutoffs"],
|
||||
.hsae-root[data-editor-profile="postProcessing"] .hsae-toggle-card-grid > [data-hsae-profiles~="postProcessing"] {
|
||||
display: block !important;
|
||||
display: grid !important;
|
||||
}
|
||||
|
||||
/* Item source — show only the panel matching the active source (profile rules still gate custom items on non-full profiles) */
|
||||
@@ -1624,7 +1624,7 @@
|
||||
.hsae-root[data-editor-profile] .hsae-query-row-sort .hsae-query-sort-by,
|
||||
.hsae-root[data-editor-profile] .hsae-query-row-sort .hsae-query-sort-order,
|
||||
.hsae-root[data-editor-profile] .hsae-query-row-sort .hsae-query-limit {
|
||||
display: block !important;
|
||||
display: grid !important;
|
||||
}
|
||||
|
||||
.hsae-root[data-editor-profile="minimal"] .hsae-query-row-sort .hsae-query-sort-by,
|
||||
|
||||
@@ -391,8 +391,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.',
|
||||
'section-flattenSeries': {
|
||||
on: 'Multiple episodes from the same series will be grouped into one item in the section.',
|
||||
off: 'Episodes from the same series appear as separate items in the section.'
|
||||
},
|
||||
'hsae-limit-before-sort': {
|
||||
on: 'When enabled, the item limit is applied before section-level sort.',
|
||||
off: 'The item limit is applied after section-level sort.'
|
||||
},
|
||||
'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.'
|
||||
@@ -1915,7 +1921,7 @@
|
||||
|
||||
let appearancePreviewDebounceTimer = null;
|
||||
let appearancePreviewRequestId = 0;
|
||||
let appearancePreviewCache = { fetchKey: null, renderKey: null, items: null };
|
||||
let appearancePreviewCache = { fetchKey: null, renderKey: null, items: null, resolvedSection: null };
|
||||
|
||||
function clearAppearancePreviewScheduling() {
|
||||
if (appearancePreviewDebounceTimer) {
|
||||
@@ -1935,6 +1941,7 @@
|
||||
|
||||
const fetchPreview = window.KefinHomeScreen?.fetchSectionPreviewItems;
|
||||
const renderPreview = window.KefinHomeScreen?.renderSectionPreviewInto;
|
||||
const mergeResolved = window.KefinHomeScreen?.mergeDraftWithResolvedPreview;
|
||||
if (typeof fetchPreview !== 'function' || typeof renderPreview !== 'function') {
|
||||
container.innerHTML = '<p class="listItemBodyText secondary hsae-preview-placeholder">Preview unavailable.</p>';
|
||||
return;
|
||||
@@ -1967,9 +1974,9 @@
|
||||
|
||||
try {
|
||||
if (!cacheHit) {
|
||||
const { items } = await fetchPreview(draft);
|
||||
const { items, section: resolvedSection } = await fetchPreview(draft);
|
||||
if (requestId !== appearancePreviewRequestId) return;
|
||||
appearancePreviewCache = { fetchKey, renderKey, items };
|
||||
appearancePreviewCache = { fetchKey, renderKey, items, resolvedSection: resolvedSection || null };
|
||||
} else {
|
||||
appearancePreviewCache.renderKey = renderKey;
|
||||
}
|
||||
@@ -1982,10 +1989,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await renderPreview(getDraftSection(), previewItems, container, { getDraftSection });
|
||||
const liveDraft = getDraftSection();
|
||||
const previewSection = typeof mergeResolved === 'function'
|
||||
? mergeResolved(liveDraft, appearancePreviewCache.resolvedSection)
|
||||
: liveDraft;
|
||||
await renderPreview(previewSection, previewItems, container, { getDraftSection });
|
||||
} catch (err) {
|
||||
if (requestId !== appearancePreviewRequestId) return;
|
||||
appearancePreviewCache = { fetchKey: null, renderKey: null, items: null };
|
||||
appearancePreviewCache = { fetchKey: null, renderKey: null, items: null, resolvedSection: null };
|
||||
container.innerHTML = `<p class="listItemBodyText secondary hsae-preview-placeholder">${escapeHtml(err.message || 'Preview failed')}</p>`;
|
||||
}
|
||||
}
|
||||
@@ -2066,13 +2077,6 @@
|
||||
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' : ''};">
|
||||
<div class="hsae-field-grid hsae-field-grid-2">
|
||||
@@ -2204,13 +2208,25 @@
|
||||
</div>
|
||||
|
||||
<div class="hsae-advanced-section-sort hsae-field-grid hsae-field-grid-2 hsae-advanced-sort-row" data-hsae-profiles="full postProcessing normal dateCutoffs">
|
||||
${buildSelect('hsae-advanced-sortBy', toSortBySelectOptions(SORT_ORDERS), state.sortBy || '', 'Sort By')}
|
||||
${buildSelect('hsae-advanced-sortOrder', getSectionSortOrderDirectionOptions(SORT_ORDER_DIRECTIONS), state.sortOrder || '', 'Sort Order')}
|
||||
<div class="hsae-field-with-hint">
|
||||
${buildSelect('hsae-advanced-sortBy', toSortBySelectOptions(SORT_ORDERS), state.sortBy || '', 'Sort By')}
|
||||
<div class="listItemBodyText secondary hsae-field-hint">Items will be sorted in this order after they are returned from Jellyfin.</div>
|
||||
</div>
|
||||
<div class="hsae-field-with-hint">
|
||||
${buildSelect('hsae-advanced-sortOrder', getSectionSortOrderDirectionOptions(SORT_ORDER_DIRECTIONS), state.sortOrder || '', 'Sort Order')}
|
||||
<div class="listItemBodyText secondary hsae-field-hint">Items will be sorted in this direction after they are returned from Jellyfin.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hsae-advanced-min-max-age hsae-field-grid hsae-field-grid-2 hsae-advanced-age-row" data-hsae-profiles="full dateCutoffs">
|
||||
${buildTextInput('hsae-query-minAge', premiereAge.minAge, 'Minimum Age (Days)', 'number')}
|
||||
${buildTextInput('hsae-query-maxAge', premiereAge.maxAge, 'Maximum Age (Days)', 'number')}
|
||||
<div class="hsae-field-with-hint">
|
||||
${buildTextInput('hsae-query-minAge', premiereAge.minAge, 'Minimum Age (Days)', 'number')}
|
||||
<div class="listItemBodyText secondary hsae-field-hint">Only show items that have a Premiere Date older than this.</div>
|
||||
</div>
|
||||
<div class="hsae-field-with-hint">
|
||||
${buildTextInput('hsae-query-maxAge', premiereAge.maxAge, 'Maximum Age (Days)', 'number')}
|
||||
<div class="listItemBodyText secondary hsae-field-hint">Only show items that have a Premiere Date newer than this.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hsae-advanced-limits hsae-field-grid hsae-field-grid-2 hsae-advanced-limits-row" data-hsae-profiles="full postProcessing normal dateCutoffs">
|
||||
@@ -2230,7 +2246,8 @@
|
||||
'section-flattenSeries',
|
||||
section.flattenSeries === true,
|
||||
'Flatten Series',
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['section-flattenSeries']
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['section-flattenSeries'][section.flattenSeries === true ? 'on' : 'off'],
|
||||
{ hintKey: 'section-flattenSeries' }
|
||||
)}
|
||||
</div>
|
||||
<div class="hsae-advanced-limit-before-sort" data-hsae-profiles="full postProcessing normal dateCutoffs">
|
||||
@@ -2238,10 +2255,20 @@
|
||||
'hsae-limit-before-sort',
|
||||
section.limitBeforeSort === true,
|
||||
'Limit Before Sort',
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['hsae-limit-before-sort']
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['hsae-limit-before-sort'][section.limitBeforeSort === true ? 'on' : 'off'],
|
||||
{ hintKey: 'hsae-limit-before-sort' }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hsae-toggle-card-grid" data-hsae-profiles="full minimal">
|
||||
${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>
|
||||
</details>
|
||||
`;
|
||||
@@ -2951,7 +2978,9 @@
|
||||
{ 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: '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'] }
|
||||
{ key: 'hsae-use-parent-card', checkboxId: 'hsae-use-parent-card', tooltips: ADVANCED_TOGGLE_DESCRIPTIONS['hsae-use-parent-card'] },
|
||||
{ key: 'section-flattenSeries', checkboxId: 'section-flattenSeries', tooltips: ADVANCED_TOGGLE_DESCRIPTIONS['section-flattenSeries'] },
|
||||
{ key: 'hsae-limit-before-sort', checkboxId: 'hsae-limit-before-sort', tooltips: ADVANCED_TOGGLE_DESCRIPTIONS['hsae-limit-before-sort'] }
|
||||
];
|
||||
|
||||
toggles.forEach(({ key, checkboxId, tooltips }) => {
|
||||
@@ -3302,7 +3331,7 @@
|
||||
state.renderMode = renderMode;
|
||||
const renderModeInput = root.querySelector('#hsae-render-mode-value');
|
||||
if (renderModeInput) renderModeInput.value = renderMode;
|
||||
appearancePreviewCache = { fetchKey: null, renderKey: null, items: null };
|
||||
appearancePreviewCache = { fetchKey: null, renderKey: null, items: null, resolvedSection: null };
|
||||
refreshEditorBody(modalInstance);
|
||||
return;
|
||||
}
|
||||
@@ -3519,7 +3548,7 @@
|
||||
|
||||
function attachListeners(modalInstance, context, callbacks = {}) {
|
||||
clearAppearancePreviewScheduling();
|
||||
appearancePreviewCache = { fetchKey: null, renderKey: null, items: null };
|
||||
appearancePreviewCache = { fetchKey: null, renderKey: null, items: null, resolvedSection: null };
|
||||
modalInstance._hsaePreviewInitialized = false;
|
||||
|
||||
const section = context.section || {};
|
||||
@@ -3584,7 +3613,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (target.id === 'hsae-hide-section-name' || target.id === 'hsae-hide-watched' || target.id === 'hsae-user-enabled-by-default' || target.id === 'hsae-user-configurable') {
|
||||
if (target.matches?.('input[type="checkbox"]') && target.closest('.kefin-toggle-card')) {
|
||||
updateToggleCardHints(root);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
minimal: {
|
||||
queryFields: ['limit'],
|
||||
advancedFields: ['cache'],
|
||||
advancedFields: ['cache', 'useParentCard'],
|
||||
lockVisibility: true,
|
||||
allowGroupChange: false,
|
||||
allowItemSourceChange: false,
|
||||
|
||||
@@ -1381,43 +1381,6 @@
|
||||
`;
|
||||
}
|
||||
|
||||
function hsseBuildToggleSlider(id, checked, label, labelTooltip) {
|
||||
const isEnabled = checked === true;
|
||||
const checkboxId = id ? `${id}-checkbox` : `toggle-${Math.random().toString(36).slice(2)}-checkbox`;
|
||||
const labelHtml = labelTooltip
|
||||
? `<div class="listItemBodyText hsse-label-with-info">
|
||||
<label for="${escapeHtml(checkboxId)}" style="cursor: pointer;">${escapeHtml(label)}</label>
|
||||
<span class="material-icons hsse-info-icon" title="${escapeHtml(labelTooltip)}" aria-label="${escapeHtml(labelTooltip)}">info</span>
|
||||
</div>`
|
||||
: `<label class="listItemBodyText" for="${escapeHtml(checkboxId)}" style="cursor: pointer;">${escapeHtml(label)}</label>`;
|
||||
|
||||
return `
|
||||
<div class="hsse-section-block" style="display: flex; align-items: center; justify-content: space-between; padding: 0.75em 1em; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px;">
|
||||
${labelHtml}
|
||||
<input type="checkbox" id="${escapeHtml(checkboxId)}" class="hide" ${isEnabled ? 'checked' : ''} data-hsse-toggle="${escapeHtml(id || checkboxId)}">
|
||||
<button type="button" class="toggle-slider" data-checkbox-id="${escapeHtml(checkboxId)}" data-enabled="${isEnabled}" aria-pressed="${isEnabled}" style="
|
||||
position: relative; width: 52px; height: 28px; border-radius: 14px; border: none; cursor: pointer;
|
||||
background: ${isEnabled ? 'rgba(0, 164, 220, 0.8)' : 'rgba(158, 158, 158, 0.5)'};
|
||||
">
|
||||
<span style="position:absolute; top:50%; left:${isEnabled ? '8px' : '6px'}; transform:translateY(-50%); font-size:9px; font-weight:700; color:white; opacity:${isEnabled ? '1' : '0'};">ON</span>
|
||||
<span style="position:absolute; top:50%; right:${isEnabled ? '6px' : '8px'}; transform:translateY(-50%); font-size:9px; font-weight:700; color:white; opacity:${isEnabled ? '0' : '1'};">OFF</span>
|
||||
<span style="position:absolute; top:3px; left:${isEnabled ? 'calc(100% - 26px)' : '3px'}; width:22px; height:22px; border-radius:50%; background:white; transition:left 0.2s;"></span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateToggleSliderUI(btn, checked) {
|
||||
const isOn = checked === true;
|
||||
btn.dataset.enabled = isOn;
|
||||
btn.setAttribute('aria-pressed', String(isOn));
|
||||
btn.style.background = isOn ? 'rgba(0, 164, 220, 0.8)' : 'rgba(158, 158, 158, 0.5)';
|
||||
const spans = btn.querySelectorAll('span');
|
||||
if (spans[0]) spans[0].style.opacity = isOn ? '1' : '0';
|
||||
if (spans[1]) spans[1].style.opacity = isOn ? '0' : '1';
|
||||
if (spans[2]) spans[2].style.left = isOn ? 'calc(100% - 26px)' : '3px';
|
||||
}
|
||||
|
||||
function getExistingGroupNames(config) {
|
||||
const groups = config?.CUSTOM_SECTION_GROUPS || [];
|
||||
return [...new Set(groups.map(g => g.name).filter(Boolean))];
|
||||
@@ -1544,7 +1507,7 @@
|
||||
function collectQueryStepFromForm(root, wizardState) {
|
||||
const sortByEl = root.querySelector('#hsse-wizard-sortBy');
|
||||
const sortOrderEl = root.querySelector('#hsse-wizard-sortOrder');
|
||||
const unwatchedEl = root.querySelector('#hsse-wizard-unwatched-checkbox');
|
||||
const unwatchedEl = root.querySelector('#hsse-wizard-unwatched');
|
||||
if (sortByEl) wizardState.sortBy = sortByEl.value;
|
||||
if (sortOrderEl) wizardState.sortOrder = sortOrderEl.value;
|
||||
if (unwatchedEl) wizardState.unwatchedOnly = unwatchedEl.checked;
|
||||
@@ -1555,7 +1518,7 @@
|
||||
const layoutEl = root.querySelector('#hsse-wizard-spotlightLayout');
|
||||
const sizeEl = root.querySelector('#hsse-wizard-spotlightSize');
|
||||
const tileEl = root.querySelector('#hsse-wizard-tileCount');
|
||||
const animatedEl = root.querySelector('#hsse-wizard-animated-checkbox');
|
||||
const animatedEl = root.querySelector('#hsse-wizard-animated');
|
||||
if (layoutEl) wizardState.spotlightLayout = layoutEl.value;
|
||||
if (sizeEl) wizardState.spotlightSize = sizeEl.value;
|
||||
if (tileEl) wizardState.tileCount = tileEl.value;
|
||||
@@ -2106,7 +2069,7 @@
|
||||
${hsseBuildSelect('hsse-wizard-sortOrder', getSortOrderDirections(), wizardState.sortOrder, 'Sort order')}
|
||||
</div>
|
||||
</div>
|
||||
${hsseBuildToggleSlider('hsse-wizard-unwatched', wizardState.unwatchedOnly, 'Hide Watched Items')}
|
||||
${buildToggleCard('hsse-wizard-unwatched', wizardState.unwatchedOnly === true, 'Hide Watched Items', '')}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2146,7 +2109,7 @@
|
||||
${hsseBuildSelect('hsse-wizard-tileCount', getSpotlightTileCountOptions(), wizardState.tileCount, 'Tiled Backdrop Count', 'Set the number of backdrops that will be tiled next to each other in a single slide of the Spotlight.')}
|
||||
</div>
|
||||
</div>
|
||||
${hsseBuildToggleSlider('hsse-wizard-animated', wizardState.panAnimation, 'Animated Slides')}
|
||||
${buildToggleCard('hsse-wizard-animated', wizardState.panAnimation !== false, 'Animated Slides', '')}
|
||||
` : ''}
|
||||
${renderAppearancePreviewHTML()}
|
||||
`;
|
||||
@@ -2255,10 +2218,22 @@
|
||||
}
|
||||
|
||||
function attachToggleHandlers(root) {
|
||||
if (root.dataset.hsseToggleHandlersAttached === 'true') return;
|
||||
if (root.dataset.hsseToggleHandlersAttached === 'true') {
|
||||
if (typeof window.KefinTweaksUI?.bindToggleCards === 'function') {
|
||||
window.KefinTweaksUI.bindToggleCards(root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
root.dataset.hsseToggleHandlersAttached = 'true';
|
||||
|
||||
if (typeof window.KefinTweaksUI?.bindToggleCards === 'function') {
|
||||
window.KefinTweaksUI.bindToggleCards(root);
|
||||
}
|
||||
|
||||
root.addEventListener('click', (e) => {
|
||||
const switchBtn = e.target.closest('.kefin-toggle-switch');
|
||||
if (switchBtn && root.contains(switchBtn)) return;
|
||||
|
||||
const btn = e.target.closest('.toggle-slider');
|
||||
if (!btn || !root.contains(btn)) return;
|
||||
const checkboxId = btn.dataset.checkboxId;
|
||||
@@ -2275,7 +2250,12 @@
|
||||
|
||||
root.addEventListener('change', (e) => {
|
||||
const checkbox = e.target;
|
||||
if (!checkbox.matches('input[type="checkbox"][data-hsse-toggle]') || !root.contains(checkbox)) return;
|
||||
if (!checkbox.matches?.('input[type="checkbox"]') || !root.contains(checkbox)) return;
|
||||
const switchBtn = root.querySelector(`.kefin-toggle-switch[data-checkbox-id="${checkbox.id}"]`);
|
||||
if (switchBtn && typeof window.KefinTweaksUI?.updateToggleSwitchUI === 'function') {
|
||||
window.KefinTweaksUI.updateToggleSwitchUI(switchBtn, checkbox.checked);
|
||||
return;
|
||||
}
|
||||
const btn = root.querySelector(`.toggle-slider[data-checkbox-id="${checkbox.id}"]`);
|
||||
if (btn) updateToggleSliderUI(btn, checkbox.checked);
|
||||
});
|
||||
@@ -4000,7 +3980,7 @@
|
||||
let appearancePreviewDebounceTimer = null;
|
||||
let appearancePreviewRequestId = 0;
|
||||
let parentLoadRequestId = 0;
|
||||
let appearancePreviewCache = { queryKey: null, items: null };
|
||||
let appearancePreviewCache = { queryKey: null, items: null, resolvedSection: null };
|
||||
|
||||
async function loadParentItemsForType(wizardState, root) {
|
||||
if (!wizardState.parentType) return;
|
||||
@@ -4074,6 +4054,7 @@
|
||||
|
||||
const fetchPreview = window.KefinHomeScreen?.fetchSectionPreviewItems;
|
||||
const renderPreview = window.KefinHomeScreen?.renderSectionPreviewInto;
|
||||
const mergeResolved = window.KefinHomeScreen?.mergeDraftWithResolvedPreview;
|
||||
if (typeof fetchPreview !== 'function' || typeof renderPreview !== 'function') {
|
||||
container.innerHTML = '<p class="listItemBodyText secondary" style="text-align:center;padding:1.5em 0;">Preview unavailable.</p>';
|
||||
return;
|
||||
@@ -4092,16 +4073,19 @@
|
||||
|
||||
try {
|
||||
if (!cacheHit) {
|
||||
const { items } = await fetchPreview(draft, { useSpotlightFields: true });
|
||||
const { items, section: resolvedSection } = await fetchPreview(draft, { useSpotlightFields: true });
|
||||
if (requestId !== appearancePreviewRequestId) return;
|
||||
appearancePreviewCache = { queryKey, items };
|
||||
appearancePreviewCache = { queryKey, items, resolvedSection: resolvedSection || null };
|
||||
}
|
||||
|
||||
if (requestId !== appearancePreviewRequestId) return;
|
||||
renderPreview(draft, appearancePreviewCache.items, container);
|
||||
const previewSection = typeof mergeResolved === 'function'
|
||||
? mergeResolved(draft, appearancePreviewCache.resolvedSection)
|
||||
: draft;
|
||||
renderPreview(previewSection, appearancePreviewCache.items, container);
|
||||
} catch (err) {
|
||||
if (requestId !== appearancePreviewRequestId) return;
|
||||
appearancePreviewCache = { queryKey: null, items: null };
|
||||
appearancePreviewCache = { queryKey: null, items: null, resolvedSection: null };
|
||||
container.innerHTML = `<p class="listItemBodyText secondary" style="text-align:center;padding:1.5em 0;">${escapeHtml(err.message || 'Preview failed')}</p>`;
|
||||
}
|
||||
}, WIZARD_PARENT_DATA.APPEARANCE_PREVIEW_DEBOUNCE_MS);
|
||||
@@ -4659,7 +4643,7 @@
|
||||
e.target.id === 'hsse-wizard-spotlightLayout' ||
|
||||
e.target.id === 'hsse-wizard-spotlightSize' ||
|
||||
e.target.id === 'hsse-wizard-tileCount' ||
|
||||
e.target.id === 'hsse-wizard-animated-checkbox'
|
||||
e.target.id === 'hsse-wizard-animated'
|
||||
)) {
|
||||
collectAppearanceStepFromForm(root, wizardState);
|
||||
scheduleAppearancePreview(root);
|
||||
|
||||
@@ -369,7 +369,6 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 0.4em;
|
||||
max-height: 460px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5em;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
|
||||
+2
-2
@@ -306,7 +306,7 @@
|
||||
name: 'homeScreen-configuration',
|
||||
script: '../configuration/homeScreen-configuration.js',
|
||||
css: '../configuration/homeScreen-configuration.css',
|
||||
dependencies: ['modal', 'toaster', 'utils', 'cardBuilder', 'ui', 'homeScreenCustomItemsEditor', 'homeScreenSectionEditor', 'homescreenBenchmark'],
|
||||
dependencies: ['modal', 'toaster', 'utils', 'cardBuilder', 'ui', 'homeScreenCustomItemsEditor', 'homeScreenSectionEditor', 'homescreenBenchmark', 'sectionHelper'],
|
||||
description: 'Home screen configuration for KefinTweaks'
|
||||
},
|
||||
{
|
||||
@@ -1157,7 +1157,7 @@
|
||||
}
|
||||
|
||||
async function loadConfigurationJS() {
|
||||
const configDependencyNames = ['modal', 'toaster', 'utils', 'homeScreenConfig2', 'ui', 'homeScreen-migration', 'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration', 'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration', 'customMenuLinks-configuration', 'thumbnailScrubber-configuration', 'watchTogether-configuration', 'userManager-configuration', 'apiHelper'];
|
||||
const configDependencyNames = ['modal', 'toaster', 'utils', 'homeScreenConfig2', 'ui', 'homeScreen-migration', 'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration', 'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration', 'customMenuLinks-configuration', 'thumbnailScrubber-configuration', 'watchTogether-configuration', 'userManager-configuration', 'apiHelper', 'sectionHelper'];
|
||||
for (const depName of configDependencyNames) {
|
||||
const depScript = SCRIPT_DEFINITIONS.find(script => script.name === depName);
|
||||
if (depScript) {
|
||||
|
||||
@@ -1719,7 +1719,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
#reactRoot:has(.MuiBox-root) .libraryPage:not(.noSecondaryNavPage).customPage {
|
||||
#reactRoot:has(main.MuiBox-root) .libraryPage:not(.noSecondaryNavPage).customPage {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
+93
-23
@@ -734,7 +734,7 @@
|
||||
font-style: normal;
|
||||
opacity: 0.9;
|
||||
line-height: 1.45;
|
||||
max-width: 75%;
|
||||
max-width: min(700px, 75%);
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
@@ -853,10 +853,8 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spotlight-nav-container {
|
||||
.spotlight-section:is([data-section-id]):not(.hide) .spotlight-nav-container {
|
||||
position: absolute !important;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 2;
|
||||
@@ -1184,8 +1182,17 @@
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.section-controls-more-popover .section-controls-more-item {
|
||||
align-items: start;
|
||||
/* Modal-hosted more menu: normal flow so the dialog body does not collapse */
|
||||
.section-controls-more-menu {
|
||||
position: static;
|
||||
display: block;
|
||||
min-width: 12rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.section-controls-more-popover .section-controls-more-item,
|
||||
.section-controls-more-menu .section-controls-more-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-controls-more-item {
|
||||
@@ -1209,12 +1216,56 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
:not(.layout-mobile) .section-controls-more {
|
||||
display: inline-flex !important;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:not(.layout-mobile) .section-controls-inline .show-all-button,
|
||||
:not(.layout-mobile) .section-controls-inline .section-refresh-button,
|
||||
:not(.layout-mobile) .section-controls-inline .section-configure-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.spotlight-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spotlight-section .section-controls {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
z-index: 5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section .section-controls {
|
||||
position: absolute;
|
||||
right: -0.5rem;
|
||||
bottom: unset;
|
||||
top: -4px;
|
||||
z-index: 5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.layout-mobile .section-controls-inline .show-all-button,
|
||||
.layout-mobile .section-controls-inline .section-refresh-button,
|
||||
.layout-mobile .section-controls-inline .section-configure-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section .section-controls-more {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.layout-mobile .section-controls-more {
|
||||
display: inline-flex !important;
|
||||
opacity: 1;
|
||||
@@ -1222,6 +1273,18 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section:hover .section-controls-more {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover.section-controls-more-menu > .kefinTweaks-popover-item {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 !important;
|
||||
padding: 0.5em 0.9em;
|
||||
max-width: unset !important;
|
||||
}
|
||||
|
||||
/* Grid (non-scrolling) item layouts */
|
||||
.itemsContainer[data-layout="grid"] {
|
||||
display: flex !important;
|
||||
@@ -1416,7 +1479,7 @@
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section[data-layout="Borderless"] {
|
||||
margin-top: -6rem;
|
||||
margin-top: -4rem;
|
||||
margin-bottom: -6rem;
|
||||
}
|
||||
|
||||
@@ -1453,7 +1516,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 22%;
|
||||
height: 15%;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -1660,7 +1723,7 @@
|
||||
.layout-mobile .spotlight-overlay {
|
||||
width: calc(100% - 2rem) !important;
|
||||
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.85) 100%) !important;
|
||||
padding: 4rem 1rem 2rem 1rem !important;
|
||||
padding: 4rem 1rem 1rem 1rem !important;
|
||||
align-items: start;
|
||||
align-content: start;
|
||||
grid-template-rows: auto auto 1fr minmax(48px, auto);
|
||||
@@ -1677,7 +1740,7 @@
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section[data-layout="Borderless"] .spotlight-section-title-container {
|
||||
top: 6rem;
|
||||
top: 4rem;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section .spotlight-section-title-container {
|
||||
@@ -1686,6 +1749,7 @@
|
||||
left: unset !important;
|
||||
justify-content: center;
|
||||
margin: 0 auto !important;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.layout-mobile .metadata-container {
|
||||
@@ -1716,9 +1780,20 @@
|
||||
gap: 0px !important;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-buttons-container .starRatingContainer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section .spotlight-buttons-container.mainDetailButtons .btnPlay {
|
||||
margin: 0.3em !important;
|
||||
}
|
||||
|
||||
.spotlight-section .spotlight-buttons-container.mainDetailButtons .btnPlay {
|
||||
right: 0px !important;
|
||||
margin: 0.3em !important;
|
||||
margin-top: 0 !important;
|
||||
min-width: unset;
|
||||
flex: unset !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.spotlight-section .spotlight-buttons-container.mainDetailButtons {
|
||||
@@ -1736,8 +1811,6 @@
|
||||
margin-right: 0px !important;
|
||||
width: auto !important;
|
||||
position: relative !important;
|
||||
top: initial !important;
|
||||
left: initial !important;
|
||||
transform: unset !important;
|
||||
}
|
||||
|
||||
@@ -1788,7 +1861,7 @@
|
||||
}
|
||||
|
||||
.spotlight-section[data-layout="Borderless"][data-size="normal"] {
|
||||
margin-top: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 2));
|
||||
margin-top: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 1.5));
|
||||
margin-bottom: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 2));
|
||||
}
|
||||
.spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-banner-container {
|
||||
@@ -1859,7 +1932,7 @@
|
||||
display: none;
|
||||
}
|
||||
.spotlight-section[data-size="large"] .spotlight-overlay {
|
||||
grid-template-rows: 2fr auto minmax(auto, 6rem) 1fr;
|
||||
grid-template-rows: 2fr auto minmax(auto, 6rem) auto;
|
||||
}
|
||||
.spotlight-section[data-size="large"] .spotlight-overlay .spotlight-item-logo {
|
||||
background-position: left bottom;
|
||||
@@ -1894,8 +1967,8 @@
|
||||
}
|
||||
.spotlight-section[data-layout="Border"][data-size="large"] .spotlight-nav-container,
|
||||
.spotlight-section[data-layout="Border"][data-size="full"] .spotlight-nav-container {
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
top: 2rem !important;
|
||||
right: 2rem !important;
|
||||
}
|
||||
.spotlight-section[data-layout="Border"][data-size="normal"] .spotlight-section-title a {
|
||||
font-size: 1rem !important;
|
||||
@@ -1929,8 +2002,7 @@
|
||||
}
|
||||
|
||||
.layout-mobile .section-controls-inline .show-all-button,
|
||||
.layout-mobile .section-controls-inline .section-refresh-button,
|
||||
.layout-mobile .section-controls-inline .section-configure-button {
|
||||
.layout-mobile .section-controls-inline .section-refresh-button {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -1963,6 +2035,7 @@
|
||||
|
||||
.layout-mobile .spotlight-dots {
|
||||
bottom: 1rem !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section[data-layout="Borderless"] .spotlight-dots {
|
||||
@@ -2220,10 +2293,6 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) .show-all-button {
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
:has(.spotlight-section[data-layout="Borderless"]) .homeSectionsContainer > *:not(.spotlight-section) {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -2264,6 +2333,7 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"
|
||||
|
||||
.layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) > * {
|
||||
display: block !important;
|
||||
padding: 0.25em 0.5em !important;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section[data-hide-section-name="true"] .spotlight-section-title,
|
||||
|
||||
+165
-30
@@ -14,7 +14,8 @@
|
||||
const SPOTLIGHT_WINDOW_NEXT = 4;
|
||||
|
||||
const state = {
|
||||
defaultCardBackgroundNumber: 1
|
||||
defaultCardBackgroundNumber: 1,
|
||||
useBlurhash: false
|
||||
}
|
||||
|
||||
function getSpotlightWindowIndices(currentIndex, totalLength) {
|
||||
@@ -2223,13 +2224,14 @@
|
||||
|
||||
// Append one section per frame so scroll/compositor get frames between paints.
|
||||
// Build already happened in the fragment; only the container append is paced.
|
||||
const sectionNodes = Array.from(fragment.childNodes);
|
||||
/* const sectionNodes = Array.from(fragment.childNodes);
|
||||
for (let i = 0; i < sectionNodes.length; i++) {
|
||||
container.appendChild(sectionNodes[i]);
|
||||
if (i + 1 < sectionNodes.length) {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
}
|
||||
} */
|
||||
container.appendChild(fragment);
|
||||
syncAttachedGridTilesLayouts(container);
|
||||
|
||||
// Update the scroll buttons for all scrollable containers
|
||||
@@ -4454,7 +4456,7 @@
|
||||
|
||||
// Watchlist button (material icon span button with bookmark icon)
|
||||
const watchlistButton = document.createElement('button');
|
||||
watchlistButton.className = 'emby-button button-flat';
|
||||
watchlistButton.className = 'emby-button button-flat btnWatchlist';
|
||||
|
||||
// Check watchlist status - try to get from cache or API
|
||||
let isInWatchlist = false;
|
||||
@@ -5152,8 +5154,8 @@
|
||||
grid: 'grid_view'
|
||||
};
|
||||
const ITEMS_LAYOUT_NEXT_TITLE = {
|
||||
row: 'Show as grid',
|
||||
grid: 'Show as scrollable row'
|
||||
row: 'Grid layout',
|
||||
grid: 'Row layout'
|
||||
};
|
||||
|
||||
function resolveItemsLayout(sectionConfig) {
|
||||
@@ -5526,7 +5528,7 @@
|
||||
definitions.push({
|
||||
id: 'refresh',
|
||||
icon: 'refresh',
|
||||
label: 'Refresh Section',
|
||||
label: 'Refresh',
|
||||
className: 'section-refresh-button',
|
||||
isVisible: () => true,
|
||||
onClick: async (e) => {
|
||||
@@ -5541,7 +5543,7 @@
|
||||
id: 'showAll',
|
||||
icon: 'grid_view',
|
||||
getLabel: () => getShowAllControlLabel(sectionElement),
|
||||
label: 'Show as grid',
|
||||
label: 'Grid layout',
|
||||
className: 'show-all-button',
|
||||
isVisible: () => true,
|
||||
onClick: (e) => {
|
||||
@@ -5591,26 +5593,23 @@
|
||||
const newMoreButton = moreButton.cloneNode(true);
|
||||
moreButton.replaceWith(newMoreButton);
|
||||
|
||||
const MORE_CONTROLS_MODAL_ID = 'kefin-section-controls-more';
|
||||
let activeMorePopover = null;
|
||||
const closeMorePopover = () => {
|
||||
if (window.ModalSystem?.isOpen?.(MORE_CONTROLS_MODAL_ID)) {
|
||||
window.ModalSystem.close(MORE_CONTROLS_MODAL_ID);
|
||||
}
|
||||
if (activeMorePopover) {
|
||||
activeMorePopover.remove();
|
||||
activeMorePopover = null;
|
||||
}
|
||||
};
|
||||
|
||||
newMoreButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (activeMorePopover) {
|
||||
closeMorePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
const buildMorePopoverContent = (onActivate, { forModal = false } = {}) => {
|
||||
const popover = document.createElement('div');
|
||||
popover.className = 'kefinTweaks-popover section-controls-more-popover itemDetailsGroup';
|
||||
|
||||
popover.className = forModal
|
||||
? 'kefinTweaks-popover section-controls-more-menu'
|
||||
: 'kefinTweaks-popover section-controls-more-popover';
|
||||
visibleDefinitions.forEach((def) => {
|
||||
const label = typeof def.getLabel === 'function' ? def.getLabel() : def.label;
|
||||
const item = document.createElement('div');
|
||||
@@ -5632,7 +5631,7 @@
|
||||
const activate = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
closeMorePopover();
|
||||
onActivate();
|
||||
def.onClick(ev, newMoreButton);
|
||||
};
|
||||
item.addEventListener('click', activate);
|
||||
@@ -5643,7 +5642,82 @@
|
||||
});
|
||||
popover.appendChild(item);
|
||||
});
|
||||
return popover;
|
||||
};
|
||||
|
||||
const positionMoreModal = (modal, anchorBtn) => {
|
||||
if (!modal?.dialog || !anchorBtn) return;
|
||||
modal.dialog.classList.remove('centeredDialog', 'formDialog', 'smoothScrollY', 'dialog-fixedSize');
|
||||
modal.dialog.style.position = 'fixed';
|
||||
modal.dialog.style.margin = '0';
|
||||
modal.dialog.style.maxHeight = 'none';
|
||||
modal.dialog.style.height = 'auto';
|
||||
modal.dialog.style.minHeight = 'auto';
|
||||
modal.dialog.style.minWidth = '12rem';
|
||||
modal.dialog.style.width = 'auto';
|
||||
modal.dialog.style.padding = '0.25em 0';
|
||||
modal.dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup';
|
||||
if (modal.dialogContent) {
|
||||
modal.dialogContent.style.padding = '0';
|
||||
modal.dialogContent.style.overflow = 'visible';
|
||||
modal.dialogContent.style.minHeight = 'auto';
|
||||
modal.dialogContent.style.height = 'auto';
|
||||
modal.dialogContent.style.flex = '0 0 auto';
|
||||
}
|
||||
if (modal.backdrop) {
|
||||
modal.backdrop.style.background = 'transparent';
|
||||
}
|
||||
if (modal.dialogContainer) {
|
||||
modal.dialogContainer.style.pointerEvents = 'none';
|
||||
}
|
||||
modal.dialog.style.pointerEvents = 'auto';
|
||||
|
||||
const btnRect = anchorBtn.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const gap = 4;
|
||||
requestAnimationFrame(() => {
|
||||
const menuHeight = modal.dialog.offsetHeight || 0;
|
||||
const menuWidth = Math.max(modal.dialog.offsetWidth || 0, 180);
|
||||
let top = btnRect.bottom + gap;
|
||||
if (top + menuHeight > window.innerHeight - margin) {
|
||||
top = Math.max(margin, btnRect.top - menuHeight - gap);
|
||||
}
|
||||
let left = btnRect.right - menuWidth;
|
||||
left = Math.min(Math.max(left, margin), window.innerWidth - menuWidth - margin);
|
||||
modal.dialog.style.top = `${top}px`;
|
||||
modal.dialog.style.left = `${left}px`;
|
||||
});
|
||||
};
|
||||
|
||||
newMoreButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (activeMorePopover || window.ModalSystem?.isOpen?.(MORE_CONTROLS_MODAL_ID)) {
|
||||
closeMorePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMobileLayout() && window.ModalSystem?.create) {
|
||||
const content = buildMorePopoverContent(() => closeMorePopover(), { forModal: true });
|
||||
window.ModalSystem.create({
|
||||
id: MORE_CONTROLS_MODAL_ID,
|
||||
title: '',
|
||||
content,
|
||||
showCloseButton: false,
|
||||
closeOnBackdrop: true,
|
||||
closeOnEscape: true,
|
||||
fixedSize: false,
|
||||
onOpen: (modal) => {
|
||||
if (modal?.dialogHeader) modal.dialogHeader.style.display = 'none';
|
||||
if (modal?.dialogFooter) modal.dialogFooter.style.display = 'none';
|
||||
positionMoreModal(modal, newMoreButton);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const popover = buildMorePopoverContent(() => closeMorePopover());
|
||||
titleContainer.style.position = 'relative';
|
||||
newMoreButton.parentNode.insertBefore(popover, newMoreButton.nextSibling);
|
||||
|
||||
@@ -5816,13 +5890,68 @@
|
||||
}
|
||||
}
|
||||
|
||||
leftButton.addEventListener('click', () => {
|
||||
function getScrollCards() {
|
||||
const itemsContainer = scroller.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
const scrollAmount = itemsContainer.clientWidth - itemsContainer.offsetLeft;
|
||||
if (!itemsContainer) return [];
|
||||
return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy):not(.skeleton-card)'));
|
||||
}
|
||||
|
||||
const scrollerPadding = (() => {
|
||||
const style = window.getComputedStyle(scroller);
|
||||
return {
|
||||
left: parseFloat(style.paddingLeft) || 0,
|
||||
right: parseFloat(style.paddingRight) || 0
|
||||
};
|
||||
})();
|
||||
|
||||
function getCardScrollLeft(card) {
|
||||
// Scroller + cards share the same translateX, so this delta is layout offset
|
||||
// within the border box. Subtract paddingLeft so snap aligns to the padded
|
||||
// content gutter (same inset as card 0 at scroll 0), not the page edge.
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
return cardRect.left - scrollerRect.left - scrollerPadding.left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cards fully inside the padded content clip (not the browser viewport / border box).
|
||||
* Undo translateX, then inset by scroller padding so visibility matches snap targets.
|
||||
*/
|
||||
function getFullyVisibleCardIndices(cards) {
|
||||
const fullyVisible = [];
|
||||
const epsilon = 1;
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
const currentPosition = getCurrentPosition();
|
||||
const newPosition = scrollAmount > currentPosition ? 0 : currentPosition - scrollAmount;
|
||||
setPosition(newPosition, { animate: true });
|
||||
const contentWidth = Math.max(
|
||||
0,
|
||||
scroller.clientWidth - scrollerPadding.left - scrollerPadding.right
|
||||
);
|
||||
const visibleLeft = scrollerRect.left + currentPosition + scrollerPadding.left;
|
||||
const visibleRight = visibleLeft + contentWidth;
|
||||
cards.forEach((card, index) => {
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
if (cardRect.width <= 0) return;
|
||||
if (cardRect.left >= visibleLeft - epsilon && cardRect.right <= visibleRight + epsilon) {
|
||||
fullyVisible.push(index);
|
||||
}
|
||||
});
|
||||
return fullyVisible;
|
||||
}
|
||||
|
||||
function snapScrollToCardIndex(cards, targetIndex) {
|
||||
if (!cards.length) return;
|
||||
const clampedIndex = Math.max(0, Math.min(targetIndex, cards.length - 1));
|
||||
const targetLeft = getCardScrollLeft(cards[clampedIndex]);
|
||||
setPosition(targetLeft, { animate: true });
|
||||
}
|
||||
|
||||
leftButton.addEventListener('click', () => {
|
||||
const cards = getScrollCards();
|
||||
if (!cards.length) return;
|
||||
const fullyVisible = getFullyVisibleCardIndices(cards);
|
||||
const firstVisible = fullyVisible.length ? fullyVisible[0] : 0;
|
||||
const step = Math.max(1, fullyVisible.length);
|
||||
snapScrollToCardIndex(cards, firstVisible - step);
|
||||
});
|
||||
|
||||
scrollButtons.appendChild(leftButton);
|
||||
@@ -5839,11 +5968,16 @@
|
||||
rightButton.appendChild(rightSpan);
|
||||
|
||||
rightButton.addEventListener('click', () => {
|
||||
const itemsContainer = scroller.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
const scrollAmount = itemsContainer.clientWidth - itemsContainer.offsetLeft;
|
||||
const currentPosition = getCurrentPosition();
|
||||
setPosition(currentPosition + scrollAmount, { animate: true });
|
||||
const cards = getScrollCards();
|
||||
if (!cards.length) return;
|
||||
const fullyVisible = getFullyVisibleCardIndices(cards);
|
||||
const lastVisible = fullyVisible.length ? fullyVisible[fullyVisible.length - 1] : -1;
|
||||
const nextIndex = lastVisible + 1;
|
||||
if (nextIndex >= cards.length) {
|
||||
setPosition(getMaxPosition(), { animate: true });
|
||||
return;
|
||||
}
|
||||
snapScrollToCardIndex(cards, nextIndex);
|
||||
});
|
||||
|
||||
scrollButtons.appendChild(rightButton);
|
||||
@@ -7631,6 +7765,7 @@
|
||||
await window.userHelper.waitForLogin();
|
||||
}
|
||||
startCardUserDataListener();
|
||||
state.useBlurhash = localStorage.getItem(`${ApiClient.getCurrentUserId()}-blurhash`) === 'true' || false;
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
||||
+66
-38
@@ -164,22 +164,6 @@ div#discovery-loading-indicator:not(.show)::after {
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide next up section if next up if KefinTweaks next up section is present */
|
||||
/* .homeSectionsContainer:has([data-section-id="nextUp"]) > div:nth-child(-n+10):has(a[href*="nextup"]) {
|
||||
display: none;
|
||||
} */
|
||||
/* Hide continue watching section if continue watching if KefinTweaks continue watching section is present */
|
||||
/* .homeSectionsContainer:has([data-section-id="continueWatching"]) > div:nth-child(-n+10):has([data-positionticks][data-mediatype="Video"]) {
|
||||
display: none;
|
||||
} */
|
||||
/* Hide recently added section if recently added if KefinTweaks recently added section is present */
|
||||
/* .homeSectionsContainer:has([data-section-id*="recently-added"]) > div:nth-child(-n+10):has(a[href*="movies&tab=1"]),
|
||||
.homeSectionsContainer:has([data-section-id*="recently-added"]) > div:nth-child(-n+10):has(a[href*="tv&tab=1"]),
|
||||
.homeSectionsContainer:has([data-section-id*="recently-added"]) > div:nth-child(-n+10):has(a[href*="music&tab=1"])
|
||||
{
|
||||
display: none;
|
||||
} */
|
||||
|
||||
/* Hide scroll buttons on mobile using Jellyfin's layout classes */
|
||||
.layout-mobile .emby-scrollbuttons,
|
||||
.layout-tv .emby-scrollbuttons {
|
||||
@@ -427,8 +411,11 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
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));
|
||||
left: 0;
|
||||
bottom: 0.25em;
|
||||
}
|
||||
.kefin-home-create-section-btn {
|
||||
padding: 0.25em;
|
||||
}
|
||||
.kefin-home-create-section-btn .material-icons {
|
||||
font-size: 1.75rem;
|
||||
@@ -441,27 +428,22 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
}
|
||||
|
||||
/* 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;
|
||||
.kefin-home-category-rail {
|
||||
--AppBar-background: var(--jf-palette-AppBar-defaultBg);
|
||||
--AppBar-color: var(--jf-palette-text-primary);
|
||||
background-color: var(--jf-palette-AppBar-darkBg, var(--AppBar-background));
|
||||
color: var(--jf-palette-AppBar-darkColor, var(--AppBar-color));
|
||||
z-index: 10;
|
||||
gap: 0.25rem;
|
||||
border-radius: 999px;
|
||||
box-shadow: 1px 1px 2px 0 #fff !important;
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
}
|
||||
#kefin-home-category-rail .kefin-home-category-rail:hover,
|
||||
#kefin-home-category-rail .kefin-home-category-rail:focus-within {
|
||||
opacity: 1;
|
||||
background: #0f0f0f !important;
|
||||
border-radius: 0 25px 25px 0;
|
||||
box-shadow: 1px 0px 5px -1px black;
|
||||
}
|
||||
.kefin-home-category-btn {
|
||||
opacity: 0.75;
|
||||
@@ -473,6 +455,7 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
}
|
||||
.kefin-home-category-btn .material-icons {
|
||||
font-size: 1.35rem;
|
||||
top: unset !important;
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
@@ -486,15 +469,18 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
padding: 0.4rem 0.5rem calc(0.4rem + env(safe-area-inset-bottom, 0px)) !important;
|
||||
box-shadow: 1px 1px 1px 1px #fff !important;
|
||||
border-radius: 0;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
margin: 0 !important;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
border: none !important;
|
||||
}
|
||||
.kefin-home-create-section {
|
||||
bottom: calc(3.25rem + env(safe-area-inset-bottom, 0px));
|
||||
.kefin-home-create-section,
|
||||
#kefin-home-create-section-btn {
|
||||
display: none !important;
|
||||
}
|
||||
.homePage:not(.hide) {
|
||||
padding-bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px));
|
||||
@@ -502,9 +488,51 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
}
|
||||
|
||||
.kefin-section-more-toggle {
|
||||
margin-left: auto;
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
body:not(:has(.homePage:not(.hide))) div#kefin-home-category-rail {
|
||||
body:not(:has(.homePage:not(.hide) #homeTab.is-active)) div#kefin-home-category-rail,
|
||||
body:not(:has(.homePage:not(.hide) #homeTab.is-active)) div#kefin-home-create-section-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
#kefin-home-category-rail .kefin-home-category-rail,
|
||||
#kefin-home-category-rail .kefin-home-category-rail::before {
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
#kefin-home-category-rail .kefin-home-category-rail {
|
||||
position: fixed !important;
|
||||
z-index: 10;
|
||||
top: 50vh !important;
|
||||
left: 0 !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: none !important;
|
||||
flex-direction: column !important;
|
||||
transform: translateY(-50%);
|
||||
overflow: visible !important;
|
||||
padding: 5px 0 !important;
|
||||
margin: 0 !important;
|
||||
backdrop-filter: none;
|
||||
border: none !important;
|
||||
display: grid !important;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
body:not(:has(#slides-container)):has(.MuiBox-root) .homeSectionsContainer:has([data-section-id]) {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
#kefin-home-category-rail .kefin-home-category-rail {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
body:has(.kefin-home-category-btn.is-active[data-category-id="discovery"]) .sections.homeSectionsContainer::before {
|
||||
content: "Discover Something To Watch";
|
||||
padding-left: max(env(safe-area-inset-left), 3.3%);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
+118
-84
@@ -113,20 +113,26 @@
|
||||
recoveryRenderEnd: null
|
||||
};
|
||||
|
||||
// Create loading indicator element
|
||||
// Create loading indicator element (always last child of the sections container)
|
||||
function createDiscoveryLoadingIndicator(container) {
|
||||
let loadingDiv = document.querySelector('.homePage:not(.hide) #discovery-loading-indicator')
|
||||
|| document.querySelector('.libraryPage:not(.hide) #discovery-loading-indicator');
|
||||
if (loadingDiv) {
|
||||
return loadingDiv;
|
||||
}
|
||||
|
||||
// Prefer the provided container, but fall back to the standard sections container
|
||||
if (!container) {
|
||||
container = document.querySelector('.homePage:not(.hide) #homeTab .sections')
|
||||
|| document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer');
|
||||
}
|
||||
|
||||
let loadingDiv = document.querySelector('.homePage:not(.hide) #discovery-loading-indicator')
|
||||
|| document.querySelector('.libraryPage:not(.hide) #discovery-loading-indicator');
|
||||
|
||||
if (loadingDiv) {
|
||||
if (container && loadingDiv.parentElement !== container) {
|
||||
container.appendChild(loadingDiv);
|
||||
} else if (container && container.lastElementChild !== loadingDiv) {
|
||||
container.appendChild(loadingDiv);
|
||||
}
|
||||
return loadingDiv;
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
WARN('Discovery loading indicator: sections container not found');
|
||||
return null;
|
||||
@@ -146,6 +152,10 @@
|
||||
return loadingDiv;
|
||||
}
|
||||
|
||||
function removeDiscoveryLoadingIndicator() {
|
||||
document.querySelectorAll('#discovery-loading-indicator').forEach((el) => el.remove());
|
||||
}
|
||||
|
||||
function getDiscoveryHomePageEl() {
|
||||
return document.querySelector('#indexPage')
|
||||
|| document.querySelector('.homePage:not(.hide)')
|
||||
@@ -260,7 +270,7 @@
|
||||
return homeSectionsContainer;
|
||||
}
|
||||
|
||||
const HOME_SECTIONS_SELECTOR = '.homePage:not(.hide) #homeTab .sections';
|
||||
const HOME_SECTIONS_SELECTOR = '.homePage:not(.hide) #homeTab.is-active .sections';
|
||||
let homeSectionsReadyObserver = null;
|
||||
|
||||
function disconnectHomeSectionsReadyObserver() {
|
||||
@@ -433,7 +443,7 @@
|
||||
performanceMetrics.initialRenderStart = performance.now();
|
||||
|
||||
mountCreateSectionButton();
|
||||
mountCategoryRail();
|
||||
// Defer mountCategoryRail until pinned availability is known (avoids first-load tab jump)
|
||||
syncHomeScreenChromeVisibility();
|
||||
|
||||
// Reset cache/metrics for a new initialization cycle
|
||||
@@ -476,20 +486,18 @@
|
||||
const kefinTweaksHomeSections = document.querySelectorAll('.homePage:not(.hide) #homeTab .homeSectionsContainer [data-section-id]');
|
||||
if (kefinTweaksHomeSections.length > 0) {
|
||||
LOG('Home screen already initialized and Jellyfin has rendered');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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"]');
|
||||
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]');
|
||||
const homeScreenFragment = document.createDocumentFragment();
|
||||
otherPageSections.forEach(section => {
|
||||
homeScreenFragment.appendChild(section.cloneNode(true));
|
||||
});
|
||||
container.appendChild(homeScreenFragment);
|
||||
LOG('Cloned existing home sections from a previous container');
|
||||
try {
|
||||
updatePinnedCategoryAvailability(
|
||||
Array.from(kefinTweaksHomeSections).map((el) => ({
|
||||
id: el.dataset.sectionId,
|
||||
category: el.dataset.category
|
||||
})),
|
||||
null
|
||||
);
|
||||
refreshHomeCategoryChrome();
|
||||
} catch (err) {
|
||||
ERR('Failed to sync home category chrome for existing home:', err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -535,6 +543,8 @@
|
||||
}
|
||||
|
||||
updatePinnedCategoryAvailability(filteredHomeSections, homeScreen);
|
||||
mountCategoryRail();
|
||||
syncHomeScreenChromeVisibility();
|
||||
|
||||
performanceEndTime = performance.now();
|
||||
performanceDuration = performanceEndTime - performanceStartTime;
|
||||
@@ -1485,12 +1495,18 @@
|
||||
|
||||
const config = await window.KefinHomeScreen.getConfig();
|
||||
const revealSectionsSequentially = config.DISCOVERY_SETTINGS?.fadeInSections === true;
|
||||
|
||||
// Park the indicator so newly appended sections are not inserted after it in the DOM
|
||||
removeDiscoveryLoadingIndicator();
|
||||
|
||||
await window.cardBuilder.renderProgressiveSections(container, bufferedSections, {
|
||||
revealSectionsSequentially,
|
||||
enhanceOnVisible: true,
|
||||
showStaleDataBeforeRefresh: config.HOME_SETTINGS?.showStaleDataBeforeRefresh === true
|
||||
});
|
||||
|
||||
createDiscoveryLoadingIndicator(container);
|
||||
|
||||
container.dataset.loadingDiscovery = 'false';
|
||||
container.classList.remove('loading-discovery');
|
||||
|
||||
@@ -1504,6 +1520,7 @@
|
||||
} catch (e) {
|
||||
ERR('Error rendering discovery group:', e);
|
||||
state.discoveryBuffer = null;
|
||||
createDiscoveryLoadingIndicator(container);
|
||||
} finally {
|
||||
state.isRenderingDiscovery = false;
|
||||
if (state.discoverySectionsRemain !== false) {
|
||||
@@ -1844,13 +1861,14 @@
|
||||
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';
|
||||
const HOME_CHROME_CLASSIC_RAIL_CLASSES = 'skinHeader skinHeader-withBackground emby-tabs-slider emby-tab-button';
|
||||
const HOME_CHROME_MODERN_RAIL_CLASSES = 'MuiPaper-root MuiPaper-elevation MuiPaper-elevation1 MuiAppBar-root MuiAppBar-colorDefault MuiAppBar-positionFixed mui-fixed';
|
||||
let homeChromeActiveCategory = 'none';
|
||||
let homeChromeCreateMounted = false;
|
||||
let homeChromeCreateMounting = false;
|
||||
let homeChromeCategoryMounted = false;
|
||||
let homeChromeHasPinnedContent = false;
|
||||
let homeChromeVisibilityObserver = null;
|
||||
let homeChromeMuiAppBarObserver = null;
|
||||
|
||||
function getHomeSettings() {
|
||||
return window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS
|
||||
@@ -1873,10 +1891,8 @@
|
||||
}
|
||||
|
||||
function updatePinnedCategoryAvailability(sections, homeScreen) {
|
||||
const userHomeConfig = window.KefinUserHomeScreenConfig;
|
||||
const fromUserPins = (userHomeConfig?.buildPinnedSectionConfigs?.(homeScreen) || []).length > 0;
|
||||
const fromSections = (sections || []).some(sectionIsPinnedCategory);
|
||||
homeChromeHasPinnedContent = fromUserPins || fromSections;
|
||||
const fromSections = (sections || []).some(s => sectionIsPinnedCategory(s) && s.enabled !== false);
|
||||
homeChromeHasPinnedContent = fromSections;
|
||||
}
|
||||
|
||||
function getRailCategories() {
|
||||
@@ -1887,34 +1903,17 @@
|
||||
}
|
||||
|
||||
function isModernUIChrome() {
|
||||
return document.querySelector('.MuiBox-root') !== null || localStorage.getItem('layout')?.length === 0;
|
||||
return !!document.querySelector('main.MuiBox-root') || 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_MODERN_RAIL_CLASSES}`;
|
||||
}
|
||||
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)');
|
||||
}
|
||||
@@ -1950,38 +1949,57 @@
|
||||
}
|
||||
|
||||
async function mountCreateSectionButton() {
|
||||
if (homeChromeCreateMounted || document.getElementById(HOME_CHROME_CREATE_ID)) {
|
||||
homeChromeCreateMounted = true;
|
||||
const existingNodes = document.querySelectorAll(`#${HOME_CHROME_CREATE_ID}`);
|
||||
if (existingNodes.length > 1) {
|
||||
existingNodes.forEach((el, idx) => {
|
||||
if (idx > 0) el.remove();
|
||||
});
|
||||
}
|
||||
if (homeChromeCreateMounted || homeChromeCreateMounting || document.getElementById(HOME_CHROME_CREATE_ID)) {
|
||||
homeChromeCreateMounted = !!document.getElementById(HOME_CHROME_CREATE_ID);
|
||||
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();
|
||||
homeChromeCreateMounting = true;
|
||||
try {
|
||||
let isAdmin = false;
|
||||
try {
|
||||
window.KefinHomeScreen?.openNewCustomSection?.({ refreshHomeOnSave: true });
|
||||
} catch (err) {
|
||||
ERR('Failed to open create section from home:', err);
|
||||
isAdmin = !!(await window.apiHelper?.isAdmin?.());
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.body.appendChild(wrap);
|
||||
homeChromeCreateMounted = true;
|
||||
if (!isAdmin) return;
|
||||
|
||||
// Re-check after await — another caller may have mounted meanwhile
|
||||
if (document.getElementById(HOME_CHROME_CREATE_ID)) {
|
||||
homeChromeCreateMounted = true;
|
||||
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;
|
||||
} finally {
|
||||
homeChromeCreateMounting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildCategoryFilterStyles(categories) {
|
||||
@@ -2017,7 +2035,6 @@
|
||||
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;
|
||||
|
||||
@@ -2031,11 +2048,12 @@
|
||||
rail.querySelectorAll('.kefin-home-category-btn').forEach((btn) => {
|
||||
const active = btn.dataset.categoryId === next;
|
||||
btn.classList.toggle('is-active', active);
|
||||
btn.classList.toggle('emby-tab-button-active', active);
|
||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
if (next === 'discovery' && prev !== 'discovery' && isDiscoveryAllowedForActiveCategory()) {
|
||||
if (next === 'discovery' && isDiscoveryAllowedForActiveCategory()) {
|
||||
const container = document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer, .homePage:not(.hide) .homeSectionsContainer');
|
||||
if (container) {
|
||||
const hasDiscovery = !!(
|
||||
@@ -2089,7 +2107,7 @@
|
||||
outer.hidden = !isHomePageVisible();
|
||||
outer.setAttribute('role', 'toolbar');
|
||||
outer.setAttribute('aria-label', 'Home categories');
|
||||
outer.setAttribute('aria-hidden', outer.hidden ? 'true' : 'false');
|
||||
//outer.setAttribute('aria-hidden', outer.hidden ? 'true' : 'false');
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.className = getCategoryRailNativeClasses();
|
||||
@@ -2100,12 +2118,12 @@
|
||||
const active = desiredCategory === cat.id;
|
||||
return `
|
||||
<button type="button"
|
||||
class="paper-icon-button-light emby-button kefin-home-category-btn${active ? ' is-active' : ''}"
|
||||
class="paper-icon-button-light emby-button kefin-home-category-btn${active ? ' is-active emby-tab-button-active' : ''}"
|
||||
data-category-id="${id}"
|
||||
title="${name}"
|
||||
aria-label="${name}"
|
||||
aria-pressed="${active ? 'true' : 'false'}">
|
||||
<span class="material-icons" aria-hidden="true">${icon}</span>
|
||||
<span class="material-icons emby-button-foreground ${icon}" aria-hidden="true"></span>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -2121,16 +2139,32 @@
|
||||
|
||||
document.body.appendChild(outer);
|
||||
homeChromeCategoryMounted = true;
|
||||
ensureMuiAppBarClassObserver();
|
||||
applyActiveHomeCategory(desiredCategory);
|
||||
}
|
||||
|
||||
function getMountedRailCategoryIds() {
|
||||
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (!rail) return null;
|
||||
return Array.from(rail.querySelectorAll('.kefin-home-category-btn'))
|
||||
.map((btn) => btn.dataset.categoryId);
|
||||
}
|
||||
|
||||
function refreshHomeCategoryChrome() {
|
||||
ensureCategoryFilterStyles();
|
||||
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (rail) rail.remove();
|
||||
homeChromeCategoryMounted = false;
|
||||
mountCategoryRail();
|
||||
const desired = getRailCategories().map((c) => String(c?.id || ''));
|
||||
const mounted = getMountedRailCategoryIds();
|
||||
const same = Array.isArray(mounted)
|
||||
&& mounted.length === desired.length
|
||||
&& mounted.every((id, i) => id === desired[i]);
|
||||
|
||||
if (!same) {
|
||||
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (rail) rail.remove();
|
||||
homeChromeCategoryMounted = false;
|
||||
mountCategoryRail();
|
||||
} else {
|
||||
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
|
||||
}
|
||||
syncHomeScreenChromeVisibility();
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@
|
||||
order: cfg.order,
|
||||
ttl: cfg.ttl,
|
||||
cardFormat: cfg.cardFormat,
|
||||
animationEnabled: spotlight.panAnimation,
|
||||
animationEnabled: spotlight.panAnimation !== false,
|
||||
hideName: cfg.hideName === true,
|
||||
hideCardTitles: cfg.cardTitleVisibility === 'hidden' || cfg.hideCardTitles === true,
|
||||
cardTitlePosition: cfg.cardTitlePosition,
|
||||
@@ -450,6 +450,7 @@
|
||||
|
||||
function syncRestoreDefaultsButton(btn, dirty) {
|
||||
if (!btn) return;
|
||||
btn.hidden = !dirty;
|
||||
btn.disabled = !dirty;
|
||||
btn.classList.toggle('is-disabled', !dirty);
|
||||
btn.setAttribute('aria-disabled', String(!dirty));
|
||||
@@ -2309,7 +2310,7 @@
|
||||
<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'}">
|
||||
<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 ? '' : 'hidden disabled'} aria-disabled="${sectionDirty ? 'false' : 'true'}">
|
||||
<span class="material-icons" aria-hidden="true">settings_backup_restore</span>
|
||||
</button>
|
||||
${isPinnedParent ? `
|
||||
@@ -2714,8 +2715,8 @@
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'section-configure-button material-icons settings';
|
||||
button.title = 'Configure Section';
|
||||
button.setAttribute('aria-label', 'Configure Section');
|
||||
button.title = 'Configure';
|
||||
button.setAttribute('aria-label', 'Configure');
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -2743,7 +2744,7 @@
|
||||
return {
|
||||
id: 'configure',
|
||||
icon: 'settings',
|
||||
label: 'Configure Section',
|
||||
label: 'Configure',
|
||||
className: 'section-configure-button',
|
||||
isVisible: () => true,
|
||||
onClick: (e, anchorButton) => {
|
||||
@@ -2892,8 +2893,7 @@
|
||||
}
|
||||
.kefin-section-restore-defaults-btn.is-disabled,
|
||||
.kefin-section-restore-defaults-btn:disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
display: none !important;
|
||||
}
|
||||
.kefin-section-more-toggle[aria-expanded="true"] {
|
||||
opacity: 1;
|
||||
@@ -3175,6 +3175,7 @@
|
||||
attachSectionControls,
|
||||
createConfigureButton,
|
||||
getConfigureSectionControl,
|
||||
openConfigurePopover,
|
||||
updateRuntimeItemsLayout,
|
||||
updateRuntimeGridExpanded
|
||||
};
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
name: 'homeScreen-configuration',
|
||||
script: '../configuration/homeScreen-configuration.js',
|
||||
css: '../configuration/homeScreen-configuration.css',
|
||||
dependencies: ['modal', 'toaster', 'utils', 'cardBuilder', 'ui', 'homeScreenCustomItemsEditor', 'homeScreenSectionEditor', 'homescreenBenchmark'],
|
||||
dependencies: ['modal', 'toaster', 'utils', 'cardBuilder', 'ui', 'homeScreenCustomItemsEditor', 'homeScreenSectionEditor', 'homescreenBenchmark', 'sectionHelper'],
|
||||
description: 'Home screen configuration for KefinTweaks'
|
||||
},
|
||||
{
|
||||
@@ -377,11 +377,18 @@
|
||||
dependencies: [],
|
||||
description: 'Watchlist statistics charts and activity analytics'
|
||||
},
|
||||
{
|
||||
name: 'watchlist-configuration',
|
||||
script: '../configuration/watchlist-configuration.js',
|
||||
css: null,
|
||||
dependencies: ['modal', 'ui'],
|
||||
description: 'Configuration UI for Watchlist menu placements'
|
||||
},
|
||||
{
|
||||
name: 'watchlist',
|
||||
script: 'watchlist.js',
|
||||
css: 'watchlist.css',
|
||||
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics'],
|
||||
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics', 'watchlist-configuration'],
|
||||
thirdParty: ['chartjs'],
|
||||
description: 'Adds watchlist functionality throughout Jellyfin interface'
|
||||
},
|
||||
@@ -544,7 +551,8 @@
|
||||
'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration',
|
||||
'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration',
|
||||
'customMenuLinks-configuration', 'thumbnailScrubber-configuration',
|
||||
'watchTogether-configuration', 'userManager-configuration', 'apiHelper'
|
||||
'watchTogether-configuration', 'watchlist-configuration', 'userManager-configuration', 'apiHelper',
|
||||
'sectionHelper'
|
||||
];
|
||||
|
||||
const CONFIGURATION_FILES = { script: 'configuration.js', css: 'configuration.css' };
|
||||
|
||||
Reference in New Issue
Block a user