Improve support for Seasonal sections. Add Valentines animated overlay and background

This commit is contained in:
HighImKevin committed 2026-02-05 19:57:21 -05:00
1 parent c91281e31e
commit d1d979ed13
6 files changed
+403 -90

No files matched your search

+5 -19
View File
@@ -58,28 +58,14 @@
order: 11;
}
/* Halloween theme */
.home-screen.halloween-theme.with-background #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: -25%;
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(https://ranaldsgift.github.io/KefinTweaks/pages/images/halloween.jpg);
background-position: 0px -250px;
background-size: auto 102%;
}
/* Christmas theme */
.home-screen.christmas-theme.with-background #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after {
body[data-seasonal-background="true"]:has(.homePage:not(.hide)) #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0%;
background: linear-gradient(rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0.85)), url('../pages/images/christmas.jpg');
background: linear-gradient(rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0.85)), var(--seasonal-background-image);
background-position: center;
background-size: cover;
}
@@ -88,12 +74,12 @@
display: none;
}
.home-screen.christmas-theme .snowverlay {
body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
display: block;
}
.home-screen [data-section-id] h2.sectionTitle,
.home-screen [data-section-id] .sectionTitleTextButton>.material-icons {
body:has(.homePage:not(.hide)) [data-section-id] h2.sectionTitle,
body:has(.homePage:not(.hide)) [data-section-id] .sectionTitleTextButton>.material-icons {
margin-bottom: 0;
margin-top: 0;
}
+89 -46
View File
@@ -244,51 +244,77 @@
observer.observe(sectionEl, { childList: true, subtree: true });
}
function manageBodyClasses() {
async function manageBodyClasses() {
const homeScreenConfig = await window.KefinHomeScreen.getConfig();
const seasonalSectionGroups = homeScreenConfig.SEASONAL_SECTION_GROUPS || [];
const seasonalConfig = homeScreenConfig.SEASONAL_THEME_SETTINGS || {};
const enableSeasonalAnimations = seasonalConfig.enableSeasonalAnimations !== false;
const enableSeasonalBackground = seasonalConfig.enableSeasonalBackground !== false;
const seasonalThemes = seasonalConfig.seasonalThemes || {};
if (!enableSeasonalBackground && !enableSeasonalAnimations) {
LOG('Seasonal background and animations are disabled');
return;
}
const body = document.body;
const currentView = window.KefinTweaksUtils?.getCurrentView ? window.KefinTweaksUtils.getCurrentView() : null;
const isHomePage = currentView === 'home' || currentView === 'home.html' || window.location.href.includes('home.html');
// Get the active Seasonal groups by checking if any section in the group is enabled and where the current date falls within the StartDate and EndDate of the section
const activeSeasonalGroups = seasonalSectionGroups.filter(sectionGroup => {
return sectionGroup.sections.some(s => s.enabled && isInSeasonalPeriod(s.startDate, s.endDate));
});
const homeScreenConfig = window.KefinTweaksConfig?.homeScreen || {};
const seasonalConfig = homeScreenConfig.seasonal || {};
const enableSeasonalAnimations = seasonalConfig.enableSeasonalAnimations !== false;
const enableSeasonalBackground = seasonalConfig.enableSeasonalBackground !== false;
if (isHomePage && body) {
body.classList.add('home-screen');
} else if (body) {
body.classList.remove('home-screen');
if (activeSeasonalGroups.length === 0) {
LOG('No active seasonal groups found');
return;
}
let activeSeasonalThemeGroup;
if (enableSeasonalBackground && enableSeasonalAnimations) {
activeSeasonalThemeGroup = activeSeasonalGroups.find(sectionGroup => {
const theme = seasonalThemes[sectionGroup.id] || {};
return theme.backgroundImage && theme.animation;
});
} else {
activeSeasonalThemeGroup = activeSeasonalGroups.find(sectionGroup => {
const theme = seasonalThemes[sectionGroup.id] || {};
return (enableSeasonalBackground && theme.backgroundImage)
|| (enableSeasonalAnimations && theme.animation);
});
}
if (!activeSeasonalThemeGroup) {
LOG('No seasonal theme found');
return;
}
if (body) {
const themeClasses = ['halloween-theme', 'christmas-theme', 'valentines-theme', 'newyear-theme', 'with-background'];
themeClasses.forEach(themeClass => body.classList.remove(themeClass));
if (isHomePage) {
if (isHalloweenPeriod()) {
body.classList.add('halloween-theme');
} else if (isChristmasPeriod()) {
body.classList.add('christmas-theme');
document.body.dataset.seasonalTheme = activeSeasonalThemeGroup.id;
if (enableSeasonalBackground) {
body.classList.add('with-background');
}
if (enableSeasonalBackground && !document.body.dataset.seasonalBackground) {
document.body.dataset.seasonalBackground = 'true';
if (enableSeasonalAnimations) {
const snowverlayScript = document.querySelector('script[src*="snowverlay.js"]');
if (!snowverlayScript) {
const snowverlayScriptSrc = (window.KefinTweaksConfig.kefinTweaksRoot || '') + 'scripts/snowverlay.js';
const script = document.createElement('script');
script.src = snowverlayScriptSrc;
document.head.appendChild(script);
}
}
// Add the background image as a css variable to the body
const backgroundImage = seasonalThemes[activeSeasonalThemeGroup.id]?.backgroundImage;
if (backgroundImage) {
document.body.style.setProperty('--seasonal-background-image', backgroundImage);
}
}
} else if (isValentinesPeriod()) {
body.classList.add('valentines-theme');
} else if (isNewYearsPeriod()) {
body.classList.add('newyear-theme');
if (enableSeasonalAnimations && !document.body.dataset.seasonalAnimations) {
document.body.dataset.seasonalAnimations = 'true';
// Add the animation script to the head
const animation = seasonalThemes[activeSeasonalThemeGroup.id]?.animation;
if (animation) {
const animationScript = document.querySelector(`script[src*="${animation}.js"]`);
if (!animationScript) {
const animationScriptSrc = (window.KefinTweaksConfig.kefinTweaksRoot || '') + `scripts/seasonal/${animation}.js`;
const script = document.createElement('script');
script.src = animationScriptSrc;
document.head.appendChild(script);
}
}
}
@@ -1735,6 +1761,14 @@
return;
}
// Ensure at least 1 discovery section is enabled
const discoverySections = getDiscoverySections();
if (!discoverySections || discoverySections.length === 0 || discoverySections.every(section => section.enabled === false)) {
LOG('No discovery sections enabled in config.');
state.discoveryEnabled = false;
return;
}
if (!container) {
WARN('setupDiscoveryInteraction called without a valid container');
return;
@@ -2035,11 +2069,7 @@
const activeTab = document.querySelector('.headerTabs .emby-tab-button-active').getAttribute('data-index');
if (activeTab !== '0') return;
const activeContainer = document.querySelector('.libraryPage:not(.hide) .kefinHomeSectionsContainer');
if (!activeContainer) return;
if (!activeContainer.dataset.sectionsRendered) return;
if (!container.dataset.sectionsRendered) return;
if (state.isRenderingDiscovery) return;
@@ -2050,7 +2080,7 @@
const threshold = windowHeight * 1.5;
if (scrollTop + windowHeight >= documentHeight - threshold) {
renderNextDiscoveryGroup(activeContainer);
renderNextDiscoveryGroup(container);
}
}, { passive: true });
LOG('Infinite scroll enabled for discovery.');
@@ -2102,8 +2132,20 @@
return isInSeasonalPeriod('01-01', '01-05');
}
function isValentinesPeriod() {
return isInSeasonalPeriod('02-01', '02-15');
async function isValentinesPeriod() {
const homeScreenConfig = await window.KefinHomeScreen.getConfig();
const valentinesConfig = homeScreenConfig.SEASONAL_SECTION_GROUPS.find(s => s.id === 'seasonal-valentines');
if (!valentinesConfig) {
return false;
}
const activeSection = valentinesConfig.sections.find(s => s.enabled);
if (!activeSection) {
return false;
}
return isInSeasonalPeriod(activeSection.startDate, activeSection.endDate);
}
const localCache = new window.LocalStorageCache();
@@ -2309,14 +2351,15 @@
}
try {
manageBodyClasses();
enhanceHomeScreen();
} catch (err) { ERR('Home screen page change handler failed:', err); }
}, { pages: ['home', 'home.html'] });
}
if (window.KefinTweaksUtils && typeof window.KefinTweaksUtils.onViewPage === 'function') {
/* if (window.KefinTweaksUtils && typeof window.KefinTweaksUtils.onViewPage === 'function') {
window.KefinTweaksUtils.onViewPage(manageBodyClasses, { pages: [] });
}
} */
})();
+21 -25
View File
@@ -480,8 +480,8 @@
id: 'seasonal.valentines.valentines-movies',
name: 'Valentine\'s Movies',
enabled: true,
startDate: '02-01',
endDate: '02-15',
startDate: '02-07',
endDate: '02-14',
order: 50,
cardFormat: 'Poster',
queries: [
@@ -499,8 +499,8 @@
id: 'seasonal.valentines.valentines-episodes',
name: 'Valentine\'s Episodes',
enabled: true,
startDate: '02-01',
endDate: '02-15',
startDate: '02-07',
endDate: '02-14',
order: 51,
cardFormat: 'Poster',
queries: [
@@ -819,31 +819,27 @@
}
const SEASONAL_THEME_SETTINGS = {
enabled: true,
enableSeasonalAnimations: true,
enableSeasonalBackground: true,
seasonToggles: [
{
id: 'halloween',
name: 'Halloween',
enabled: false
enabled: false,
enableSeasonalAnimations: false,
enableSeasonalBackground: false,
seasonalThemes: {
'seasonal-halloween': {
animation: 'snowverlay',
backgroundImage: 'url(../pages/images/halloween.jpg)'
},
{
id: 'christmas',
name: 'Christmas',
enabled: false
'seasonal-christmas': {
animation: 'snowverlay',
backgroundImage: 'url(../pages/images/christmas.jpg)'
},
{
id: 'new-years',
name: 'New Year\'s',
enabled: false
'seasonal-new-years': {
animation: 'snowverlay',
backgroundImage: 'url(../pages/images/new-years.jpg)'
},
{
id: 'valentines',
name: 'Valentine\'s Day',
enabled: false
'seasonal-valentines': {
animation: 'valentinesOverlay',
backgroundImage: 'url(../pages/images/valentines.jpg)'
}
]
}
}
const HOME_SETTINGS = {
minimumSeriesForPopularTVNetworks: 10,
+144
View File
@@ -0,0 +1,144 @@
function addSnowverlay() {
// Check if snowverlay canvas is already added
const snowverlayCanvas = document.querySelector('.snowverlay');
if (snowverlayCanvas) {
return;
}
// Configuration constants
const CANVAS_HEIGHT_PX = 160;
const SNOWFLAKE_AMOUNT = 15;
const SNOWFLAKE_SIZE = { min: 10, max: 22 };
const SNOWFLAKE_SPEED = { min: 0.2, max: 0.7 };
// Dynamically inject styles
const style = document.createElement('style');
style.textContent = `
body {
height: 100svh;
margin: 0;
}
.snowverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: ${CANVAS_HEIGHT_PX}px;
pointer-events: none;
z-index: 1000;
}
`;
document.head.appendChild(style);
// Create and inject canvas
const canvas = document.createElement('canvas');
canvas.className = 'snowverlay';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// Resize handler
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = CANVAS_HEIGHT_PX;
};
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Snowflake factory
const createSnowflake = (isAnimated = true, index = 0) => ({
x: Math.random() * canvas.width,
y: isAnimated
? -20 - (index * canvas.height) / SNOWFLAKE_AMOUNT
: Math.random() * canvas.height,
size: Math.random() * (SNOWFLAKE_SIZE.max - SNOWFLAKE_SIZE.min) + SNOWFLAKE_SIZE.min,
speed: Math.random() * (SNOWFLAKE_SPEED.max - SNOWFLAKE_SPEED.min) + SNOWFLAKE_SPEED.min,
opacity: isAnimated ? null : Math.random() * 0.5 + 0.2,
drift: Math.random() * 0.4 - 0.2, // Random horizontal drift (wind)
rotation: Math.random() * 360,
rotationSpeed: Math.random() * 2 - 1 // Degrees per frame
});
// Draw single snowflake
const drawSnowflake = (flake) => {
ctx.save();
ctx.translate(flake.x, flake.y);
ctx.rotate((flake.rotation * Math.PI) / 180);
ctx.font = `${flake.size}px sans-serif`;
ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity ?? (1 - flake.y / canvas.height)})`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('❄', 0, 0);
ctx.restore();
};
// Animation control
let animationFrame;
const stopAnimation = () => {
if (animationFrame) cancelAnimationFrame(animationFrame);
};
// Static snow renderer
const renderStaticSnow = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
Array.from({ length: SNOWFLAKE_AMOUNT }, () => createSnowflake(false))
.forEach(drawSnowflake);
};
// Animated snow renderer
const startSnowAnimation = () => {
const snowflakes = Array.from(
{ length: SNOWFLAKE_AMOUNT },
(_, i) => createSnowflake(true, i)
);
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
snowflakes.forEach(flake => {
flake.y += flake.speed;
flake.x += flake.drift;
flake.rotation += flake.rotationSpeed;
if (flake.y > canvas.height) {
flake.y = -10;
flake.x = Math.random() * canvas.width;
}
drawSnowflake(flake);
});
animationFrame = requestAnimationFrame(animate);
};
animate();
return stopAnimation;
};
// Motion preference handler
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
let currentAnimation = null;
const handleMotionChange = (e) => {
stopAnimation();
if (currentAnimation) currentAnimation();
if (e.matches) {
renderStaticSnow();
currentAnimation = null;
} else {
currentAnimation = startSnowAnimation();
}
};
mediaQuery.addEventListener('change', handleMotionChange);
handleMotionChange(mediaQuery);
// Cleanup on unload
window.addEventListener('beforeunload', () => {
mediaQuery.removeEventListener('change', handleMotionChange);
stopAnimation();
});
}
addSnowverlay();
+144
View File
@@ -0,0 +1,144 @@
function addSnowverlay() {
// Check if snowverlay canvas is already added
const snowverlayCanvas = document.querySelector('.snowverlay');
if (snowverlayCanvas) {
return;
}
// Configuration constants
const CANVAS_HEIGHT_PX = 160;
const SNOWFLAKE_AMOUNT = 15;
const SNOWFLAKE_SIZE = { min: 10, max: 22 };
const SNOWFLAKE_SPEED = { min: 0.2, max: 0.7 };
// Dynamically inject styles
const style = document.createElement('style');
style.textContent = `
body {
height: 100svh;
margin: 0;
}
.snowverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: ${CANVAS_HEIGHT_PX}px;
pointer-events: none;
z-index: 1000;
}
`;
document.head.appendChild(style);
// Create and inject canvas
const canvas = document.createElement('canvas');
canvas.className = 'snowverlay';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// Resize handler
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = CANVAS_HEIGHT_PX;
};
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Snowflake factory
const createSnowflake = (isAnimated = true, index = 0) => ({
x: Math.random() * canvas.width,
y: isAnimated
? -20 - (index * canvas.height) / SNOWFLAKE_AMOUNT
: Math.random() * canvas.height,
size: Math.random() * (SNOWFLAKE_SIZE.max - SNOWFLAKE_SIZE.min) + SNOWFLAKE_SIZE.min,
speed: Math.random() * (SNOWFLAKE_SPEED.max - SNOWFLAKE_SPEED.min) + SNOWFLAKE_SPEED.min,
opacity: isAnimated ? null : Math.random() * 0.5 + 0.2,
drift: Math.random() * 0.4 - 0.2, // Random horizontal drift (wind)
rotation: Math.random() * 360,
rotationSpeed: Math.random() * 2 - 1 // Degrees per frame
});
// Draw single snowflake
const drawSnowflake = (flake) => {
ctx.save();
ctx.translate(flake.x, flake.y);
ctx.rotate((flake.rotation * Math.PI) / 180);
ctx.font = `${flake.size}px sans-serif`;
ctx.fillStyle = `rgba(255, 0, 0, ${flake.opacity ?? (1 - flake.y / canvas.height)})`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('❤', 0, 0);
ctx.restore();
};
// Animation control
let animationFrame;
const stopAnimation = () => {
if (animationFrame) cancelAnimationFrame(animationFrame);
};
// Static snow renderer
const renderStaticSnow = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
Array.from({ length: SNOWFLAKE_AMOUNT }, () => createSnowflake(false))
.forEach(drawSnowflake);
};
// Animated snow renderer
const startSnowAnimation = () => {
const snowflakes = Array.from(
{ length: SNOWFLAKE_AMOUNT },
(_, i) => createSnowflake(true, i)
);
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
snowflakes.forEach(flake => {
flake.y += flake.speed;
flake.x += flake.drift;
flake.rotation += flake.rotationSpeed;
if (flake.y > canvas.height) {
flake.y = -10;
flake.x = Math.random() * canvas.width;
}
drawSnowflake(flake);
});
animationFrame = requestAnimationFrame(animate);
};
animate();
return stopAnimation;
};
// Motion preference handler
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
let currentAnimation = null;
const handleMotionChange = (e) => {
stopAnimation();
if (currentAnimation) currentAnimation();
if (e.matches) {
renderStaticSnow();
currentAnimation = null;
} else {
currentAnimation = startSnowAnimation();
}
};
mediaQuery.addEventListener('change', handleMotionChange);
handleMotionChange(mediaQuery);
// Cleanup on unload
window.addEventListener('beforeunload', () => {
mediaQuery.removeEventListener('change', handleMotionChange);
stopAnimation();
});
}
addSnowverlay();