diff --git a/pages/images/valentines.jpg b/pages/images/valentines.jpg new file mode 100644 index 0000000..d1ffa02 Binary files /dev/null and b/pages/images/valentines.jpg differ diff --git a/scripts/homeScreen.css b/scripts/homeScreen.css index 0413317..cf70ec9 100644 --- a/scripts/homeScreen.css +++ b/scripts/homeScreen.css @@ -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; } diff --git a/scripts/homeScreen3.js b/scripts/homeScreen3.js index e99336c..dcfbecf 100644 --- a/scripts/homeScreen3.js +++ b/scripts/homeScreen3.js @@ -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: [] }); - } + } */ })(); diff --git a/scripts/homeScreenConfig2.js b/scripts/homeScreenConfig2.js index 4f7e153..2c4e17e 100644 --- a/scripts/homeScreenConfig2.js +++ b/scripts/homeScreenConfig2.js @@ -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, diff --git a/scripts/seasonal/snowverlay.js b/scripts/seasonal/snowverlay.js new file mode 100644 index 0000000..5acc2dd --- /dev/null +++ b/scripts/seasonal/snowverlay.js @@ -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(); \ No newline at end of file diff --git a/scripts/seasonal/valentinesOverlay.js b/scripts/seasonal/valentinesOverlay.js new file mode 100644 index 0000000..10e8edf --- /dev/null +++ b/scripts/seasonal/valentinesOverlay.js @@ -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(); \ No newline at end of file