mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Improve resolution of Spotlight styles and ensure Seasonal sections load correctly
This commit is contained in:
@@ -238,6 +238,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether today falls in an MM-DD (or M-D) seasonal window.
|
||||
* Supports year-wrap when end < start (e.g. 12-20 … 01-05).
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInSeasonalPeriod(startDate, endDate) {
|
||||
if (startDate == null || endDate == null || startDate === '' || endDate === '') {
|
||||
return false;
|
||||
}
|
||||
const parseMonthDay = (value) => {
|
||||
const parts = String(value).trim().split(/[-/.]/).map((p) => parseInt(p, 10));
|
||||
if (parts.length < 2 || parts.some((n) => !Number.isFinite(n))) return null;
|
||||
let month;
|
||||
let day;
|
||||
// YYYY-MM-DD → take month/day
|
||||
if (parts.length >= 3 && parts[0] > 31) {
|
||||
month = parts[1];
|
||||
day = parts[2];
|
||||
} else {
|
||||
month = parts[0];
|
||||
day = parts[1];
|
||||
}
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
||||
return { month, day };
|
||||
};
|
||||
const startMd = parseMonthDay(startDate);
|
||||
const endMd = parseMonthDay(endDate);
|
||||
if (!startMd || !endMd) return false;
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const start = new Date(year, startMd.month - 1, startMd.day, 0, 0, 0, 0);
|
||||
let end = new Date(year, endMd.month - 1, endMd.day, 23, 59, 59, 999);
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return false;
|
||||
|
||||
if (end < start) {
|
||||
if (now >= start) {
|
||||
end.setFullYear(year + 1);
|
||||
} else {
|
||||
start.setFullYear(year - 1);
|
||||
}
|
||||
}
|
||||
return now >= start && now <= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect discovery-enabled sections from custom groups, stamping pageNumber
|
||||
* per group (1..N among discovery sections only, in group order).
|
||||
@@ -1435,18 +1482,6 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`;
|
||||
// ENABLED_DISCOVERY_SECTIONS is all enabled sections from DISCOVERY_SECTION_GROUPS and CUSTOM_SECTION_GROUPS that are discovery sections
|
||||
// If they have a start/end date the current date must fall within that date range
|
||||
|
||||
const isInSeasonalPeriod = (startDate, endDate) => {
|
||||
const currentDate = new Date();
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
// Ensure the years are the same as the current date year
|
||||
start.setFullYear(currentDate.getFullYear());
|
||||
end.setFullYear(currentDate.getFullYear());
|
||||
|
||||
return currentDate >= start && currentDate <= end;
|
||||
};
|
||||
|
||||
const enabledHomeSections = flattenSectionGroups(mergedConfig.HOME_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
|
||||
const enabledSeasonalSections = flattenSectionGroups(mergedConfig.SEASONAL_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && isInSeasonalPeriod(s.startDate, s.endDate));
|
||||
const enabledCustomSections = flattenSectionGroups(mergedConfig.CUSTOM_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
|
||||
@@ -1490,18 +1525,6 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`;
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const isInSeasonalPeriod = (startDate, endDate) => {
|
||||
const currentDate = new Date();
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
// Ensure the years are the same as the current date year
|
||||
start.setFullYear(currentDate.getFullYear());
|
||||
end.setFullYear(currentDate.getFullYear());
|
||||
|
||||
return currentDate >= start && currentDate <= end;
|
||||
};
|
||||
|
||||
const enabledHomeSections = flattenSectionGroups(config.HOME_SECTION_GROUPS).filter(s => (s.enabled || s.userConfigurable || s.userConfigurable === undefined) && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
|
||||
const enabledSeasonalSections = flattenSectionGroups(config.SEASONAL_SECTION_GROUPS).filter(s => (s.enabled || s.userConfigurable || s.userConfigurable === undefined) && s.discoveryEnabled !== true && isInSeasonalPeriod(s.startDate, s.endDate));
|
||||
const enabledCustomSections = flattenSectionGroups(config.CUSTOM_SECTION_GROUPS).filter(s => (s.enabled || s.userConfigurable || s.userConfigurable === undefined) && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true));
|
||||
@@ -5751,6 +5774,7 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`;
|
||||
|
||||
// Export getConfig for use by other scripts
|
||||
window.KefinHomeScreen = window.KefinHomeScreen || {};
|
||||
window.KefinHomeScreen.isInSeasonalPeriod = isInSeasonalPeriod;
|
||||
window.KefinHomeScreen.getConfig = getConfig;
|
||||
window.KefinHomeScreen.getSections = getSections;
|
||||
window.KefinHomeScreen.fetchSectionPreviewItems = fetchSectionPreviewItems;
|
||||
|
||||
@@ -201,19 +201,21 @@
|
||||
}
|
||||
|
||||
function appendBoolPrefField(fields, value, serverValue) {
|
||||
// Persist defined bools even when equal to server default so user choice survives reload.
|
||||
if (value === undefined) {
|
||||
fields.push('');
|
||||
return;
|
||||
}
|
||||
const normalized = value === true;
|
||||
const serverNormalized = serverValue === true;
|
||||
fields.push(normalized === serverNormalized ? '' : (normalized ? 'true' : 'false'));
|
||||
fields.push(value === true ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function appendPrefField(fields, value, serverValue) {
|
||||
const normalized = value == null ? '' : String(value);
|
||||
const serverNormalized = serverValue == null ? '' : String(serverValue);
|
||||
fields.push(normalized !== '' && normalized !== serverNormalized ? normalized : '');
|
||||
// Persist defined overrides even when equal to server default so user choice survives reload.
|
||||
if (value == null || value === '') {
|
||||
fields.push('');
|
||||
return;
|
||||
}
|
||||
fields.push(String(value));
|
||||
}
|
||||
|
||||
function isCustomSectionForPref(section) {
|
||||
@@ -946,9 +948,28 @@
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSpotlightPresentationLocal(options = {}) {
|
||||
if (typeof window !== 'undefined' && window.cardBuilder?.resolveSpotlightPresentation) {
|
||||
return window.cardBuilder.resolveSpotlightPresentation({
|
||||
...options,
|
||||
capForViewport: false
|
||||
});
|
||||
}
|
||||
const fullScreen = options.fullScreen === true;
|
||||
const layout = options.spotlightLayout ?? (fullScreen ? 'Borderless' : 'Border');
|
||||
const size = options.spotlightSize ?? (fullScreen ? 'full' : 'normal');
|
||||
let tileCount = options.tileCount;
|
||||
if (tileCount == null || tileCount < 1 || tileCount > 3) {
|
||||
tileCount = size === 'full' ? 1 : size === 'large' ? 2 : 3;
|
||||
}
|
||||
tileCount = Math.max(1, Math.min(3, Math.floor(tileCount)));
|
||||
return { layout, size, tileCount };
|
||||
}
|
||||
|
||||
function getServerSectionDefaults(section) {
|
||||
if (!section) return {};
|
||||
const spotlight = section.spotlightConfig || {};
|
||||
const resolved = resolveSpotlightPresentationLocal(spotlight);
|
||||
const id = String(section.id || '');
|
||||
const isPinned = id.startsWith(PINNED_LIST_ID_PREFIX) || id.startsWith(PINNED_PARENT_ID_PREFIX);
|
||||
return {
|
||||
@@ -963,9 +984,9 @@
|
||||
cardTitlePosition: section.cardTitlePosition || '',
|
||||
borderStyle: section.borderStyle || '',
|
||||
borderColor: section.borderColor || '',
|
||||
spotlightLayout: spotlight.spotlightLayout ?? 'Border',
|
||||
spotlightSize: spotlight.spotlightSize ?? 'normal',
|
||||
spotlightTileCount: parseInt(spotlight.tileCount, 10) || 1,
|
||||
spotlightLayout: resolved.layout,
|
||||
spotlightSize: resolved.size,
|
||||
spotlightTileCount: resolved.tileCount,
|
||||
itemsLayout: 'row',
|
||||
useGaplessCards: false,
|
||||
cardTitleCapitalization: section.cardTitleCapitalization || 'normal',
|
||||
|
||||
+36
-12
@@ -134,6 +134,22 @@
|
||||
mask-position: 0 0;
|
||||
}
|
||||
|
||||
.spotlight-section[data-size="large"] .spotlight-background-single,
|
||||
.spotlight-section[data-size="large"] .spotlight-background-dual {
|
||||
height: 140%;
|
||||
transform: translateY(-10%);
|
||||
-webkit-mask-image: linear-gradient( to bottom, transparent 0%, black calc(var(--spotlight-fullscreen-vertical) * 4), black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 4)), transparent 100% );
|
||||
mask-image: linear-gradient( to bottom, transparent 0%, black calc(var(--spotlight-fullscreen-vertical) * 4), black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 4)), transparent 100% );
|
||||
}
|
||||
|
||||
.spotlight-section[data-size="full"] .spotlight-background-single,
|
||||
.spotlight-section[data-size="full"] .spotlight-background-dual {
|
||||
height: 125%;
|
||||
transform: translateY(-5%);
|
||||
-webkit-mask-image: linear-gradient( to bottom, transparent 0%, black calc(var(--spotlight-fullscreen-vertical) * 2), black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 4)), transparent 100% );
|
||||
mask-image: linear-gradient( to bottom, transparent 0%, black calc(var(--spotlight-fullscreen-vertical) * 2), black calc(100% - calc(var(--spotlight-fullscreen-vertical) * 4)), transparent 100% );
|
||||
}
|
||||
|
||||
.spotlight-background-single img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -844,17 +860,21 @@
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
|
||||
transition-delay: 0s;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.spotlight-info-button:hover .spotlight-overview-tooltip {
|
||||
/* Show only while hovering the info icon glyph — not the button/tooltip footprint */
|
||||
.spotlight-info-button:has(.material-icons:hover) .spotlight-overview-tooltip {
|
||||
opacity: 1;
|
||||
transition-delay: 0.5s;
|
||||
visibility: visible;
|
||||
pointer-events: none;
|
||||
transition-delay: 0.5s, 0.5s;
|
||||
}
|
||||
|
||||
/* Hover overview only on Normal desktop spotlights */
|
||||
@@ -864,7 +884,7 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.spotlight-section:not(.hide)[data-section-id] .spotlight-nav-container {
|
||||
.spotlight-section:not(.hide)[data-section-id] > .spotlight-banner-container > .spotlight-nav-container {
|
||||
position: absolute !important;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -1677,17 +1697,17 @@
|
||||
}
|
||||
|
||||
/* Tagline at bottom center above navigation dots (all spotlights) - always 1 line max */
|
||||
.spotlight-tagline {
|
||||
.spotlight-section:not(.hide) > .spotlight-banner-container .spotlight-tagline {
|
||||
position: absolute !important;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: fit-content;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
width: fit-content !important;
|
||||
max-width: min(85%, 700px);
|
||||
text-align: center;
|
||||
text-align: center !important;
|
||||
font-style: italic;
|
||||
font-size: 1.05em;
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
@@ -1695,7 +1715,7 @@
|
||||
border-radius: 6px;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
line-height: 1.4;
|
||||
height: unset !important;
|
||||
}
|
||||
.spotlight-section:not(.hide) .spotlight-tagline p {
|
||||
font-weight: initial !important;
|
||||
@@ -1913,6 +1933,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spotlight-metadata-row .emby-tab-button {
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
:where(.spotlight-section-title.headerTabs.sectionTabs),
|
||||
:where(.spotlight-section .headerTabs.sectionTabs),
|
||||
:where(.spotlight-nav-container.headerTabs.sectionTabs) {
|
||||
|
||||
+63
-25
@@ -703,7 +703,7 @@
|
||||
|
||||
const pos = getScrollerPosition(scroller);
|
||||
const maxPosition = getMaxPositionFromPadding(scroller, readScrollerPadding(scroller), readSampleCardPadding(scroller));
|
||||
leftButton.disabled = pos === 0;
|
||||
leftButton.disabled = pos <= 20;
|
||||
rightButton.disabled = pos >= maxPosition;
|
||||
}
|
||||
|
||||
@@ -1562,6 +1562,7 @@
|
||||
...sectionConfig,
|
||||
cardFormat: finalCardFormat,
|
||||
});
|
||||
unobserveSkeletonShimmer(sectionElement);
|
||||
sectionElement.dataset.refreshing = 'false';
|
||||
ensureSectionRevealObservation(sectionElement);
|
||||
return sectionElement;
|
||||
@@ -1919,6 +1920,7 @@
|
||||
...sectionConfig,
|
||||
cardFormat: finalCardFormat,
|
||||
});
|
||||
unobserveSkeletonShimmer(sectionElement);
|
||||
sectionElement.dataset.refreshing = 'false';
|
||||
ensureSectionRevealObservation(sectionElement);
|
||||
const newButton = sectionElement.querySelector('.section-refresh-button');
|
||||
@@ -2122,6 +2124,7 @@
|
||||
ensureCardBorders: ensureCardBorders,
|
||||
applyItemsLayoutState: applyItemsLayoutState,
|
||||
resolveItemsLayout: resolveItemsLayout,
|
||||
resolveSpotlightPresentation: resolveSpotlightPresentation,
|
||||
resolveUseGaplessCards: resolveUseGaplessCards,
|
||||
invalidateLastRowPadding: invalidateLastRowPadding,
|
||||
invalidateCardRadiusCache: invalidateCardRadiusCache,
|
||||
@@ -3931,7 +3934,8 @@
|
||||
|
||||
buttonContainer.appendChild(moreButton);
|
||||
|
||||
if (item.Type !== 'CollectionFolder' && item.Type !== 'Folder' && item.Type !== 'UserView' && item.LocationType !== 'Virtual') {
|
||||
const NON_PLAYABLE_TYPES = ['CollectionFolder', 'Folder', 'UserView', 'Virtual', 'Studio'];
|
||||
if (!NON_PLAYABLE_TYPES.includes(item.Type) && item.LocationType !== 'Virtual') {
|
||||
cardOverlayContainer.appendChild(playButton);
|
||||
}
|
||||
cardOverlayContainer.appendChild(buttonContainer);
|
||||
@@ -4189,6 +4193,37 @@
|
||||
* @param {boolean} options.pauseOnHover - Pause auto-cycle when cursor is over spotlight (default: true, false when fullScreen)
|
||||
* @returns {HTMLElement} - The constructed spotlight container
|
||||
*/
|
||||
/**
|
||||
* Resolve spotlight layout/size/tileCount from options (incl. legacy fullScreen).
|
||||
* Shared by live create, skeleton create, and configure/defaults callers.
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.capForViewport=true] - Cap tileCount by viewport width
|
||||
* @returns {{ layout: string, size: string, tileCount: number }}
|
||||
*/
|
||||
function resolveSpotlightPresentation(options = {}) {
|
||||
const fullScreen = options.fullScreen === true;
|
||||
const layout = options.spotlightLayout ?? (fullScreen ? 'Borderless' : 'Border');
|
||||
const size = options.spotlightSize ?? (fullScreen ? 'full' : 'normal');
|
||||
let tileCount = options.tileCount;
|
||||
if (tileCount == null || tileCount < 1 || tileCount > 3) {
|
||||
tileCount = size === 'full' ? 1 : size === 'large' ? 2 : 3;
|
||||
}
|
||||
tileCount = Math.max(1, Math.min(3, Math.floor(tileCount)));
|
||||
if (options.capForViewport !== false) {
|
||||
const vw = window.innerWidth || 0;
|
||||
if (vw < 900) tileCount = Math.min(tileCount, 1);
|
||||
else if (vw < 1600) tileCount = Math.min(tileCount, 2);
|
||||
}
|
||||
return { layout, size, tileCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Netflix-style slim banner carousel (spotlight)
|
||||
* @param {Array} itemsInput - Array of Jellyfin item objects
|
||||
* @param {string} title - Title for the spotlight section
|
||||
* @param {Object} options - Options for the spotlight carousel
|
||||
* @returns {HTMLElement} - The constructed spotlight container
|
||||
*/
|
||||
function createSpotlightSection(itemsInput, title, options = {}) {
|
||||
let items = Array.isArray(itemsInput) ? itemsInput.slice() : [];
|
||||
if (!items.length) {
|
||||
@@ -4203,8 +4238,6 @@
|
||||
showNavButtons = true,
|
||||
showClearArt = false,
|
||||
panAnimation = true,
|
||||
spotlightLayout,
|
||||
spotlightSize,
|
||||
tileCount: tileCountOpt,
|
||||
cycleBackdrops = false,
|
||||
cycleBackdropsTime,
|
||||
@@ -4220,19 +4253,10 @@
|
||||
/** Build DOM only — no timers/observers (for harvesting slides during in-place refresh). */
|
||||
inert = false,
|
||||
} = options;
|
||||
// Backward compat: derive layout/size from fullScreen if new keys missing
|
||||
const fullScreen = options.fullScreen === true;
|
||||
const layout = spotlightLayout ?? (fullScreen ? 'Borderless' : 'Border');
|
||||
const size = spotlightSize ?? (fullScreen ? 'full' : 'normal');
|
||||
let tileCount = tileCountOpt;
|
||||
if (tileCount == null || tileCount < 1 || tileCount > 3) {
|
||||
tileCount = size === 'full' ? 1 : size === 'large' ? 2 : 3;
|
||||
}
|
||||
tileCount = Math.max(1, Math.min(3, Math.floor(tileCount)));
|
||||
// Cap multi-tile layouts on narrower viewports
|
||||
const vw = window.innerWidth || 0;
|
||||
if (vw < 900) tileCount = Math.min(tileCount, 1);
|
||||
else if (vw < 1600) tileCount = Math.min(tileCount, 2);
|
||||
const { layout, size, tileCount } = resolveSpotlightPresentation({
|
||||
...options,
|
||||
tileCount: tileCountOpt,
|
||||
});
|
||||
let backdropsCount = backdropsCountOpt;
|
||||
if (backdropsCount == null && cycleBackdropsTime > 0 && interval > 0) {
|
||||
backdropsCount = Math.max(1, Math.round(interval / cycleBackdropsTime));
|
||||
@@ -7180,9 +7204,8 @@
|
||||
* @returns {HTMLElement} - Skeleton spotlight container
|
||||
*/
|
||||
function createSkeletonSpotlightSection(title, options = {}) {
|
||||
const { viewMoreUrl = null, spotlightLayout, spotlightSize, fullScreen, discoveryPending = false, caption = null } = options;
|
||||
const layout = spotlightLayout ?? (fullScreen === true ? 'Borderless' : 'Border');
|
||||
const size = spotlightSize ?? (fullScreen === true ? 'full' : 'normal');
|
||||
const { viewMoreUrl = null, discoveryPending = false, caption = null } = options;
|
||||
const { layout, size } = resolveSpotlightPresentation(options);
|
||||
const captionText = caption && String(caption).trim();
|
||||
const useSkeletonTitle = discoveryPending === true;
|
||||
|
||||
@@ -7790,7 +7813,7 @@
|
||||
} else {
|
||||
maxScroll = getMaxPositionFromPadding(scroller, readScrollerPadding(scroller), readSampleCardPadding(scroller));
|
||||
}
|
||||
leftButton.disabled = scrollX <= 0;
|
||||
leftButton.disabled = scrollX <= 20;
|
||||
rightButton.disabled = scrollX >= maxScroll - 1;
|
||||
}
|
||||
|
||||
@@ -8245,11 +8268,26 @@
|
||||
if (scroller.classList.contains('scrollX')) {
|
||||
scroller.scrollTo({ left: clamped, behavior: animate ? 'smooth' : 'auto' });
|
||||
setStoredScrollPosition(scroller, clamped);
|
||||
return;
|
||||
} else {
|
||||
scroller.style.transition = animate ? 'transform 400ms ease-out' : 'none';
|
||||
scroller.style.transform = `translateX(-${clamped}px)`;
|
||||
setStoredScrollPosition(scroller, clamped);
|
||||
}
|
||||
|
||||
const section = scroller.closest('.emby-scroller-container');
|
||||
if (section) {
|
||||
if (animate) {
|
||||
const onTransitionEnd = () => {
|
||||
scroller.removeEventListener('transitionend', onTransitionEnd);
|
||||
applyScrollButtonState(section);
|
||||
};
|
||||
scroller.addEventListener('transitionend', onTransitionEnd);
|
||||
// Native scrollX has no transitionend — refresh after a frame too
|
||||
requestAnimationFrame(() => applyScrollButtonState(section));
|
||||
} else {
|
||||
applyScrollButtonState(section);
|
||||
}
|
||||
}
|
||||
scroller.style.transition = animate ? 'transform 400ms ease-out' : 'none';
|
||||
scroller.style.transform = `translateX(-${clamped}px)`;
|
||||
setStoredScrollPosition(scroller, clamped);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+19
-2
@@ -533,14 +533,31 @@ html:not([data-active-page="home"]) #kefin-home-create-section-btn {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer[data-category="discovery"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer[data-category="discovery"]::before {
|
||||
content: "\E87A\00a0 Discover Something To Watch";
|
||||
font-family: "Material Icons", inherit;
|
||||
content: "\E87A";
|
||||
font-family: "Material Icons";
|
||||
padding-left: max(env(safe-area-inset-left), 3.3%);
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
padding-bottom: .2em;
|
||||
padding-top: 1em;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer[data-category="discovery"]::after {
|
||||
content: "Discover Something To Watch";
|
||||
font-family: inherit;
|
||||
font-size: 1.5rem;
|
||||
position: absolute;
|
||||
left: calc(max(env(safe-area-inset-left), 3.3%) + 1.7em);
|
||||
top: 1em;
|
||||
line-height: 1.2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html:not([data-active-page="home"]) .snowverlay {
|
||||
|
||||
+38
-15
@@ -875,14 +875,7 @@
|
||||
return section;
|
||||
});
|
||||
|
||||
const isInSeasonalPeriod = (startDate, endDate) => {
|
||||
const currentDate = new Date();
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
start.setFullYear(currentDate.getFullYear());
|
||||
end.setFullYear(currentDate.getFullYear());
|
||||
return currentDate >= start && currentDate <= end;
|
||||
};
|
||||
const isInSeasonalPeriodLocal = (startDate, endDate) => isInSeasonalPeriod(startDate, endDate);
|
||||
|
||||
const customGroups = homeScreenConfig?.CUSTOM_SECTION_GROUPS
|
||||
|| window.KefinTweaksConfig?.homeScreenConfig?.CUSTOM_SECTION_GROUPS
|
||||
@@ -891,7 +884,7 @@
|
||||
const customDiscoverySections = typeof collect === 'function'
|
||||
? collect(customGroups, {
|
||||
isSectionActive: (s) => s.enabled === true,
|
||||
isInSeasonalPeriod
|
||||
isInSeasonalPeriod: isInSeasonalPeriodLocal
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -1521,6 +1514,7 @@
|
||||
// Collect enabled templates unresolved — progressive resolve happens in ensureDiscoveryBuffer
|
||||
for (const template of discoveryTemplates) {
|
||||
if (template.enabled === false) continue;
|
||||
if (window.sectionHelper?.isEmptyDiscoverySourceTemplate?.(template)) continue;
|
||||
selectedConfigs.push(template);
|
||||
}
|
||||
|
||||
@@ -1746,14 +1740,43 @@
|
||||
}
|
||||
|
||||
function isInSeasonalPeriod(start, end) {
|
||||
if (typeof window.KefinHomeScreen?.isInSeasonalPeriod === 'function'
|
||||
&& window.KefinHomeScreen.isInSeasonalPeriod !== isInSeasonalPeriod) {
|
||||
return window.KefinHomeScreen.isInSeasonalPeriod(start, end);
|
||||
}
|
||||
if (start == null || end == null || start === '' || end === '') return false;
|
||||
const parseMonthDay = (value) => {
|
||||
const parts = String(value).trim().split(/[-/.]/).map((p) => parseInt(p, 10));
|
||||
if (parts.length < 2 || parts.some((n) => !Number.isFinite(n))) return null;
|
||||
let month;
|
||||
let day;
|
||||
if (parts.length >= 3 && parts[0] > 31) {
|
||||
month = parts[1];
|
||||
day = parts[2];
|
||||
} else {
|
||||
month = parts[0];
|
||||
day = parts[1];
|
||||
}
|
||||
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
||||
return { month, day };
|
||||
};
|
||||
const startMd = parseMonthDay(start);
|
||||
const endMd = parseMonthDay(end);
|
||||
if (!startMd || !endMd) return false;
|
||||
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const startDate = new Date(`${start}-${currentYear}`);
|
||||
|
||||
let endDate = new Date(`${end}-${currentYear}`);
|
||||
if (endDate < startDate) endDate.setFullYear(currentYear + 1);
|
||||
else if (endDate > startDate) endDate.setFullYear(currentYear);
|
||||
const year = now.getFullYear();
|
||||
const startDate = new Date(year, startMd.month - 1, startMd.day, 0, 0, 0, 0);
|
||||
let endDate = new Date(year, endMd.month - 1, endMd.day, 23, 59, 59, 999);
|
||||
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return false;
|
||||
|
||||
if (endDate < startDate) {
|
||||
if (now >= startDate) {
|
||||
endDate.setFullYear(year + 1);
|
||||
} else {
|
||||
startDate.setFullYear(year - 1);
|
||||
}
|
||||
}
|
||||
return now >= startDate && now <= endDate;
|
||||
}
|
||||
|
||||
|
||||
@@ -378,19 +378,47 @@
|
||||
}
|
||||
|
||||
function sectionHasUserOverrides(entry, serverDefaults) {
|
||||
const api = getConfigApi();
|
||||
const cfg = entry?.effectiveConfig;
|
||||
const defaults = serverDefaults || entry?.serverDefaults || {};
|
||||
if (!api?.serializeSectionPref || !cfg) return false;
|
||||
const prefStr = api.serializeSectionPref(
|
||||
resolveStoredSectionPrefId(entry.sectionId, cfg),
|
||||
cfg.enabled !== false,
|
||||
buildPrefOverrides(cfg),
|
||||
defaults
|
||||
);
|
||||
const parts = String(prefStr || '').split(';');
|
||||
if (parts[1] === 'false') return true;
|
||||
return parts.slice(2).some((part) => part !== '');
|
||||
if (!cfg) return false;
|
||||
if (cfg.enabled === false) return true;
|
||||
|
||||
const overrides = buildPrefOverrides(cfg);
|
||||
const norm = (v) => (v == null || v === '' ? '' : String(v));
|
||||
const boolEq = (a, b) => (a === true) === (b === true);
|
||||
|
||||
const layout = overrides.itemsLayout === 'grid' || overrides.itemsLayout === 'row'
|
||||
? overrides.itemsLayout
|
||||
: (overrides.gridExpanded === true ? 'grid' : 'row');
|
||||
const serverLayout = defaults.itemsLayout === 'grid' || defaults.itemsLayout === 'row'
|
||||
? defaults.itemsLayout
|
||||
: (defaults.gridExpanded === true ? 'grid' : 'row');
|
||||
|
||||
if (norm(overrides.renderMode) !== norm(defaults.renderMode || 'Normal')
|
||||
&& norm(overrides.renderMode) !== '') return true;
|
||||
if (norm(overrides.order) !== norm(defaults.order)) return true;
|
||||
if (norm(overrides.ttl) !== norm(defaults.ttl)) return true;
|
||||
if (norm(overrides.cardFormat) !== norm(defaults.cardFormat)) return true;
|
||||
{
|
||||
const anim = overrides.animationEnabled !== false;
|
||||
const serverAnim = (defaults.animationEnabled ?? defaults.panAnimation) !== false;
|
||||
if (anim !== serverAnim) return true;
|
||||
}
|
||||
if (!boolEq(overrides.hideName === true, defaults.hideName === true)) return true;
|
||||
if (!boolEq(overrides.hideCardTitles === true, defaults.hideCardTitles === true)) return true;
|
||||
if (norm(overrides.cardTitlePosition) !== norm(defaults.cardTitlePosition)) return true;
|
||||
if (norm(overrides.borderStyle) !== norm(defaults.borderStyle)) return true;
|
||||
if (norm(overrides.borderColor) !== norm(defaults.borderColor)) return true;
|
||||
if (norm(overrides.spotlightLayout) !== norm(defaults.spotlightLayout)) return true;
|
||||
if (norm(overrides.spotlightSize) !== norm(defaults.spotlightSize)) return true;
|
||||
if (norm(overrides.spotlightTileCount) !== norm(defaults.spotlightTileCount)) return true;
|
||||
if (layout !== serverLayout) return true;
|
||||
if (!boolEq(overrides.useGaplessCards === true, defaults.useGaplessCards === true)) return true;
|
||||
if (norm(overrides.cardTitleCapitalization || 'normal') !== norm(defaults.cardTitleCapitalization || 'normal')) return true;
|
||||
if (norm(overrides.cardTitleFontFamily || 'default') !== norm(defaults.cardTitleFontFamily || 'default')) return true;
|
||||
if (norm(overrides.cardTitleFontSize || 'normal') !== norm(defaults.cardTitleFontSize || 'normal')) return true;
|
||||
if (norm(overrides.cardTitleColor) !== norm(defaults.cardTitleColor)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function applyServerDefaultsToConfig(cfg, defaults) {
|
||||
@@ -776,17 +804,41 @@
|
||||
return isSpotlightSection(entry?.effectiveConfig);
|
||||
}
|
||||
|
||||
function resolveSpotlightPresentationLocal(options = {}) {
|
||||
if (window.cardBuilder?.resolveSpotlightPresentation) {
|
||||
return window.cardBuilder.resolveSpotlightPresentation({
|
||||
...options,
|
||||
capForViewport: false
|
||||
});
|
||||
}
|
||||
const fullScreen = options.fullScreen === true;
|
||||
const layout = options.spotlightLayout ?? (fullScreen ? 'Borderless' : 'Border');
|
||||
const size = options.spotlightSize ?? (fullScreen ? 'full' : 'normal');
|
||||
let tileCount = options.tileCount;
|
||||
if (tileCount == null || tileCount < 1 || tileCount > 3) {
|
||||
tileCount = size === 'full' ? 1 : size === 'large' ? 2 : 3;
|
||||
}
|
||||
tileCount = Math.max(1, Math.min(3, Math.floor(tileCount)));
|
||||
return { layout, size, tileCount };
|
||||
}
|
||||
|
||||
// A section that was never spotlight has no spotlightConfig, so the popover controls and
|
||||
// the renderer would otherwise fall back to different defaults.
|
||||
function buildSpotlightConfigSeed(existingConfig, serverDefaults = {}) {
|
||||
const existing = existingConfig || {};
|
||||
const admin = window.KefinHomeScreen?.getConfig?.()?.SPOTLIGHT_SETTINGS || {};
|
||||
const merged = { ...admin, ...existing };
|
||||
const resolved = resolveSpotlightPresentationLocal({
|
||||
...merged,
|
||||
spotlightLayout: existing.spotlightLayout ?? admin.spotlightLayout ?? serverDefaults.spotlightLayout,
|
||||
spotlightSize: existing.spotlightSize ?? admin.spotlightSize ?? serverDefaults.spotlightSize,
|
||||
tileCount: existing.tileCount ?? admin.tileCount ?? serverDefaults.spotlightTileCount
|
||||
});
|
||||
return {
|
||||
...admin,
|
||||
...existing,
|
||||
spotlightLayout: existing.spotlightLayout ?? admin.spotlightLayout ?? serverDefaults.spotlightLayout ?? 'Border',
|
||||
spotlightSize: existing.spotlightSize ?? admin.spotlightSize ?? serverDefaults.spotlightSize ?? 'normal',
|
||||
tileCount: existing.tileCount ?? admin.tileCount ?? serverDefaults.spotlightTileCount ?? 1
|
||||
...merged,
|
||||
spotlightLayout: resolved.layout,
|
||||
spotlightSize: resolved.size,
|
||||
tileCount: resolved.tileCount
|
||||
};
|
||||
}
|
||||
|
||||
@@ -864,15 +916,10 @@
|
||||
|
||||
if (shouldRenderAsSpotlight(entry)) {
|
||||
const spotlight = cfg.spotlightConfig || {};
|
||||
if (spotlight.spotlightLayout) {
|
||||
el.dataset.layout = spotlight.spotlightLayout;
|
||||
}
|
||||
if (spotlight.spotlightSize) {
|
||||
el.dataset.size = spotlight.spotlightSize;
|
||||
}
|
||||
if (spotlight.tileCount != null) {
|
||||
el.dataset.tileCount = String(spotlight.tileCount);
|
||||
}
|
||||
const resolved = resolveSpotlightPresentationLocal(spotlight);
|
||||
el.dataset.layout = resolved.layout;
|
||||
el.dataset.size = resolved.size;
|
||||
el.dataset.tileCount = String(resolved.tileCount);
|
||||
el.querySelectorAll('.spotlight-overlay').forEach(overlay => {
|
||||
const hasVisibleTitle = cfg.hideName !== true
|
||||
&& !!el.querySelector('.spotlight-section-title');
|
||||
@@ -2313,8 +2360,9 @@
|
||||
: ''}
|
||||
`;
|
||||
|
||||
const spotlightLayout = spotlight.spotlightLayout || 'Border';
|
||||
const spotlightTileCount = normalizeSpotlightTileCount(spotlight.tileCount);
|
||||
const resolvedSpotlight = resolveSpotlightPresentationLocal(spotlight);
|
||||
const spotlightLayout = resolvedSpotlight.layout;
|
||||
const spotlightTileCount = normalizeSpotlightTileCount(resolvedSpotlight.tileCount);
|
||||
const spotlightNextTileCount = nextSpotlightTileCount(spotlightTileCount);
|
||||
const spotlightTilesIcon = spotlightNextTileCount === 1
|
||||
? 'looks_one'
|
||||
@@ -2575,13 +2623,11 @@
|
||||
|
||||
const spotlightLayoutBtn = popover.querySelector('.kefin-section-spotlight-layout-btn');
|
||||
if (spotlightLayoutBtn) {
|
||||
syncSpotlightLayoutButton(
|
||||
spotlightLayoutBtn,
|
||||
entry.effectiveConfig.spotlightConfig?.spotlightLayout || 'Border'
|
||||
);
|
||||
const seedLayout = resolveSpotlightPresentationLocal(entry.effectiveConfig.spotlightConfig || {}).layout;
|
||||
syncSpotlightLayoutButton(spotlightLayoutBtn, seedLayout);
|
||||
spotlightLayoutBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const current = entry.effectiveConfig.spotlightConfig?.spotlightLayout || 'Border';
|
||||
const current = resolveSpotlightPresentationLocal(entry.effectiveConfig.spotlightConfig || {}).layout;
|
||||
const next = current === 'Borderless' ? 'Border' : 'Borderless';
|
||||
entry.effectiveConfig.spotlightConfig = {
|
||||
...(entry.effectiveConfig.spotlightConfig || {}),
|
||||
@@ -2595,8 +2641,9 @@
|
||||
const spotlightSizeBtn = popover.querySelector('.kefin-section-spotlight-size-btn');
|
||||
spotlightSizeBtn?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const seedSize = resolveSpotlightPresentationLocal(entry.effectiveConfig.spotlightConfig || {}).size;
|
||||
openOptionMenu(spotlightSizeBtn, getSpotlightSizeOptions(), {
|
||||
activeValue: entry.effectiveConfig.spotlightConfig?.spotlightSize || 'normal',
|
||||
activeValue: seedSize,
|
||||
onSelect: (value) => {
|
||||
entry.effectiveConfig.spotlightConfig = {
|
||||
...(entry.effectiveConfig.spotlightConfig || {}),
|
||||
@@ -2609,14 +2656,12 @@
|
||||
|
||||
const spotlightTilesBtn = popover.querySelector('.kefin-section-spotlight-tiles-btn');
|
||||
if (spotlightTilesBtn) {
|
||||
syncSpotlightTilesButton(
|
||||
spotlightTilesBtn,
|
||||
entry.effectiveConfig.spotlightConfig?.tileCount || 1
|
||||
);
|
||||
const seedTileCount = resolveSpotlightPresentationLocal(entry.effectiveConfig.spotlightConfig || {}).tileCount;
|
||||
syncSpotlightTilesButton(spotlightTilesBtn, seedTileCount);
|
||||
spotlightTilesBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const current = normalizeSpotlightTileCount(
|
||||
entry.effectiveConfig.spotlightConfig?.tileCount
|
||||
resolveSpotlightPresentationLocal(entry.effectiveConfig.spotlightConfig || {}).tileCount
|
||||
);
|
||||
const next = nextSpotlightTileCount(current);
|
||||
entry.effectiveConfig.spotlightConfig = {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
watchlist: new Set(),
|
||||
customDiscoverySections: new Set()
|
||||
},
|
||||
/** Session-scoped: discovery template ids whose source pool resolved empty (not incomplete cache). */
|
||||
emptyDiscoverySourceTemplates: new Set(),
|
||||
currentDiscoveryGenre: null,
|
||||
currentDiscoveryStudio: null,
|
||||
// Map of pairKey -> Promise resolving to dynamicResult for spotlight pairing
|
||||
@@ -51,11 +53,35 @@
|
||||
Object.keys(ids).forEach((key) => {
|
||||
if (ids[key] instanceof Set) ids[key].clear();
|
||||
});
|
||||
discoveryState.emptyDiscoverySourceTemplates.clear();
|
||||
discoveryState.currentDiscoveryGenre = null;
|
||||
discoveryState.currentDiscoveryStudio = null;
|
||||
discoveryState.pairResolvePromises.clear();
|
||||
}
|
||||
|
||||
function getDiscoveryTemplateExclusionKey(configOrId) {
|
||||
if (configOrId == null) return '';
|
||||
if (typeof configOrId === 'string') {
|
||||
const id = configOrId;
|
||||
const stamped = id.replace(/-\d{10,}-[a-z0-9]+$/i, '');
|
||||
return stamped || id.split('-')[0] || id;
|
||||
}
|
||||
if (configOrId.discoveryTemplateId) return String(configOrId.discoveryTemplateId);
|
||||
return getDiscoveryTemplateExclusionKey(String(configOrId.id || ''));
|
||||
}
|
||||
|
||||
function markEmptyDiscoverySourceTemplate(configOrId) {
|
||||
const key = getDiscoveryTemplateExclusionKey(configOrId);
|
||||
if (!key) return;
|
||||
discoveryState.emptyDiscoverySourceTemplates.add(key);
|
||||
LOG(`Excluding empty discovery source template for session: ${key}`);
|
||||
}
|
||||
|
||||
function isEmptyDiscoverySourceTemplate(configOrId) {
|
||||
const key = getDiscoveryTemplateExclusionKey(configOrId);
|
||||
return !!key && discoveryState.emptyDiscoverySourceTemplates.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* One-shot: whether this section's dependent Movies/Series/People cache is already complete.
|
||||
* Incomplete → skip (do not poll/wait). Missing LibraryCache → treat as ready.
|
||||
@@ -672,7 +698,8 @@
|
||||
queries: []
|
||||
})) === true) {
|
||||
LOG(`Discovery section ${config.id}: skipping pool fetch — incomplete library cache`);
|
||||
return [];
|
||||
// null = incomplete cache (do not permanently exclude template)
|
||||
return null;
|
||||
}
|
||||
|
||||
const cfg = Config();
|
||||
@@ -803,7 +830,12 @@
|
||||
}
|
||||
|
||||
const pool = await fetchDiscoveryPool(config);
|
||||
if (!pool.length) return null;
|
||||
// Incomplete library cache — skip this attempt without excluding the template
|
||||
if (pool == null) return null;
|
||||
if (!pool.length) {
|
||||
markEmptyDiscoverySourceTemplate(config);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (discoveryType === 'Genre') {
|
||||
const settings = getDiscoverySettings();
|
||||
@@ -915,6 +947,7 @@
|
||||
const instanceConfig = {
|
||||
...template,
|
||||
id: options.stubId || `${template.id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
discoveryTemplateId: template.id,
|
||||
type: 'discovery',
|
||||
};
|
||||
if (options.stubOrder != null) {
|
||||
@@ -1134,6 +1167,7 @@
|
||||
const config = {
|
||||
...template,
|
||||
id: uniqueId,
|
||||
discoveryTemplateId: template.id,
|
||||
type: 'discovery',
|
||||
discoveryPending: true,
|
||||
order: order != null ? order : template.order,
|
||||
@@ -1207,7 +1241,10 @@
|
||||
resolveViewMoreUrl,
|
||||
getDiscoverySettings,
|
||||
getDiscoveryState: () => discoveryState,
|
||||
resetDiscoveryDedupe
|
||||
resetDiscoveryDedupe,
|
||||
getDiscoveryTemplateExclusionKey,
|
||||
markEmptyDiscoverySourceTemplate,
|
||||
isEmptyDiscoverySourceTemplate
|
||||
};
|
||||
|
||||
LOG('sectionHelper ready');
|
||||
|
||||
Reference in New Issue
Block a user