From 861f431e50d066ea51e827aa90db69000c7a6c97 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:40:21 +0100 Subject: [PATCH] Add seasonal themes for Birthday, Olympia, Space, Sports, and Underwater: implement CSS and JavaScript for visual effects and configurations --- Jellyfin.Plugin.Seasonals/Web/birthday.css | 90 ++++++++++ Jellyfin.Plugin.Seasonals/Web/birthday.js | 145 +++++++++++++++ Jellyfin.Plugin.Seasonals/Web/olympia.css | 71 ++++++++ Jellyfin.Plugin.Seasonals/Web/olympia.js | 135 ++++++++++++++ Jellyfin.Plugin.Seasonals/Web/space.css | 58 ++++++ Jellyfin.Plugin.Seasonals/Web/space.js | 124 +++++++++++++ Jellyfin.Plugin.Seasonals/Web/sports.css | 81 +++++++++ Jellyfin.Plugin.Seasonals/Web/sports.js | 146 +++++++++++++++ Jellyfin.Plugin.Seasonals/Web/underwater.css | 88 +++++++++ Jellyfin.Plugin.Seasonals/Web/underwater.js | 180 +++++++++++++++++++ 10 files changed, 1118 insertions(+) create mode 100644 Jellyfin.Plugin.Seasonals/Web/birthday.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/birthday.js create mode 100644 Jellyfin.Plugin.Seasonals/Web/olympia.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/olympia.js create mode 100644 Jellyfin.Plugin.Seasonals/Web/space.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/space.js create mode 100644 Jellyfin.Plugin.Seasonals/Web/sports.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/sports.js create mode 100644 Jellyfin.Plugin.Seasonals/Web/underwater.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/underwater.js diff --git a/Jellyfin.Plugin.Seasonals/Web/birthday.css b/Jellyfin.Plugin.Seasonals/Web/birthday.css new file mode 100644 index 0000000..b4a4882 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/birthday.css @@ -0,0 +1,90 @@ +.birthday-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; + contain: strict; +} + +.birthday-cake { + position: absolute; + bottom: 2vh; + left: 50vw; + transform: translateX(-50%); + font-size: 8rem; + z-index: 50; + filter: drop-shadow(0 0 10px rgba(255,255,255,0.4)); +} + +.birthday-cake img { + height: 15vh; + width: auto; + object-fit: contain; + max-height: 150px; +} + +.birthday-symbol { + position: absolute; + bottom: -10vh; /* balloons rise from bottom */ + animation: birthday-rise linear infinite; + font-size: 3rem; + opacity: 0.95; + z-index: 40; +} + +.birthday-symbol img { + width: 6vh; + height: auto; + max-width: 60px; + object-fit: contain; +} + +.birthday-confetti { + position: absolute; + top: -5vh; + width: 10px; + height: 10px; + opacity: 0.9; + animation: birthday-confetti-fall linear infinite; + z-index: 30; + /* Mix of circles and squares by using CSS variables or random in JS. For simplicity, we make all slightly rounded rectangles */ + border-radius: 2px; +} + +@keyframes birthday-rise { + 0% { + transform: translateY(10vh) rotate(var(--start-rot, 0deg)); + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(-110vh) rotate(calc(var(--start-rot, 0deg) * -1)); + opacity: 0; + } +} + +@keyframes birthday-confetti-fall { + 0% { + transform: translateY(-5vh) rotateX(0deg) rotateY(0deg) rotateZ(0deg); + opacity: 0; + } + 5% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(105vh) rotateX(720deg) rotateY(360deg) rotateZ(180deg); + opacity: 0; + } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/birthday.js b/Jellyfin.Plugin.Seasonals/Web/birthday.js new file mode 100644 index 0000000..741f200 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/birthday.js @@ -0,0 +1,145 @@ +const config = window.SeasonalsPluginConfig?.Birthday || {}; + +const birthday = config.EnableBirthday !== undefined ? config.EnableBirthday : true; +const symbolCount = config.SymbolCount || 25; +const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; +const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; + +let msgPrinted = false; + +function toggleBirthday() { + const container = document.querySelector('.birthday-container'); + if (!container) return; + + const videoPlayer = document.querySelector('.videoPlayerContainer'); + const trailerPlayer = document.querySelector('.youtubePlayerContainer'); + const isDashboard = document.body.classList.contains('dashboardDocument'); + const hasUserMenu = document.querySelector('#app-user-menu'); + + if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { + container.style.display = 'none'; + if (!msgPrinted) { + console.log('Birthday hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Birthday visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleBirthday); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +function createBirthday() { + const container = document.querySelector('.birthday-container') || document.createElement('div'); + + if (!document.querySelector('.birthday-container')) { + container.className = 'birthday-container'; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + // Spawn Birthday Cake at the bottom + const cake = document.createElement('div'); + cake.className = 'birthday-cake'; + let cakeImg = document.createElement('img'); + cakeImg.src = `../Seasonals/Resources/birthday_images/cake.png`; + cakeImg.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = '🎂'; + }; + cake.appendChild(cakeImg); + container.appendChild(cake); + + const standardCount = 15; + const totalSymbols = symbolCount + standardCount; + + let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; + let finalCount = totalSymbols; + + if (isMobile) { + finalCount = enableRandomMobile ? totalSymbols : standardCount; + } + + const useRandomDuration = enableDifferentDuration !== false; + + // We'll treat balloons and gifts as rising symbols + const activeItems = ['balloon_red', 'balloon_blue', 'balloon_yellow', 'gift']; + + for (let i = 0; i < finalCount; i++) { + let symbol = document.createElement('div'); + + const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; + symbol.className = `birthday-symbol birthday-${randomItem}`; + + let img = document.createElement('img'); + img.src = `../Seasonals/Resources/birthday_images/${randomItem}.png`; + img.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = getBirthdayEmojiFallback(randomItem); + }; + symbol.appendChild(img); + + const leftPos = Math.random() * 95; + const delaySeconds = Math.random() * 10; + + let durationSeconds = 9; + if (useRandomDuration) { + durationSeconds = Math.random() * 5 + 7; // 7 to 12 seconds + } + + const startRot = (Math.random() * 20) - 10; // -10 to +10 spread + symbol.style.setProperty('--start-rot', `${startRot}deg`); + + symbol.style.left = `${leftPos}vw`; + symbol.style.animationDuration = `${durationSeconds}s`; + symbol.style.animationDelay = `${delaySeconds}s`; + + container.appendChild(symbol); + } + + // Party Confetti + const confettiColors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000']; + const confettiCount = isMobile ? 40 : 80; + + for (let i = 0; i < confettiCount; i++) { + let confetti = document.createElement('div'); + confetti.className = 'birthday-confetti'; + + const color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.style.backgroundColor = color; + + const leftPos = Math.random() * 100; + const delaySeconds = Math.random() * 8; + const duration = Math.random() * 3 + 4; + + confetti.style.left = `${leftPos}vw`; + confetti.style.animationDuration = `${duration}s`; + confetti.style.animationDelay = `${delaySeconds}s`; + + container.appendChild(confetti); + } +} + +function getBirthdayEmojiFallback(type) { + if (type.startsWith('balloon')) return '🎈'; + if (type === 'gift') return '🎁'; + return ''; +} + +function initializeBirthday() { + if (!birthday) return; + createBirthday(); + toggleBirthday(); +} + +initializeBirthday(); diff --git a/Jellyfin.Plugin.Seasonals/Web/olympia.css b/Jellyfin.Plugin.Seasonals/Web/olympia.css new file mode 100644 index 0000000..cae4425 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/olympia.css @@ -0,0 +1,71 @@ +.olympia-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; + contain: strict; +} + +.olympia-symbol { + position: absolute; + top: -10vh; + animation: olympia-fall linear infinite; + font-size: 3rem; + opacity: 0.95; + text-shadow: 0 0 10px rgba(255,255,255,0.2); +} + +.olympia-symbol img { + width: 6vh; + height: auto; + max-width: 60px; + object-fit: contain; +} + +.olympia-confetti { + position: absolute; + top: -5vh; + width: 8px; + height: 16px; + opacity: 0.85; + animation: olympia-confetti-fall linear infinite; + border-radius: 4px; /* slightly rounder confetti */ +} + +@keyframes olympia-fall { + 0% { + transform: translateY(-10vh) rotate(var(--start-rot, 0deg)); + opacity: 0; + } + 10% { + opacity: 1; + } + 85% { + opacity: 1; + } + 100% { + transform: translateY(110vh) rotate(var(--end-rot, 360deg)); + opacity: 0; + } +} + +@keyframes olympia-confetti-fall { + 0% { + transform: translateY(-5vh) rotateX(0deg) rotateY(0deg); + opacity: 0; + } + 5% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(105vh) rotateX(720deg) rotateY(360deg); + opacity: 0; + } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/olympia.js b/Jellyfin.Plugin.Seasonals/Web/olympia.js new file mode 100644 index 0000000..0cccff6 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/olympia.js @@ -0,0 +1,135 @@ +const config = window.SeasonalsPluginConfig?.Olympia || {}; + +const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true; +const symbolCount = config.SymbolCount || 25; +const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; +const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; + +let msgPrinted = false; + +function toggleOlympia() { + const container = document.querySelector('.olympia-container'); + if (!container) return; + + const videoPlayer = document.querySelector('.videoPlayerContainer'); + const trailerPlayer = document.querySelector('.youtubePlayerContainer'); + const isDashboard = document.body.classList.contains('dashboardDocument'); + const hasUserMenu = document.querySelector('#app-user-menu'); + + if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { + container.style.display = 'none'; + if (!msgPrinted) { + console.log('Olympia hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Olympia visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleOlympia); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +function createOlympia() { + const container = document.querySelector('.olympia-container') || document.createElement('div'); + + if (!document.querySelector('.olympia-container')) { + container.className = 'olympia-container'; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + const standardCount = 15; + const totalSymbols = symbolCount + standardCount; + + let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; + let finalCount = totalSymbols; + + if (isMobile) { + finalCount = enableRandomMobile ? totalSymbols : standardCount; + } + + const useRandomDuration = enableDifferentDuration !== false; + + const activeItems = ['gold', 'silver', 'bronze', 'torch']; + + for (let i = 0; i < finalCount; i++) { + let symbol = document.createElement('div'); + + const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; + symbol.className = `olympia-symbol olympia-${randomItem}`; + + let img = document.createElement('img'); + img.src = `../Seasonals/Resources/olympia_images/${randomItem}.png`; + img.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = getOlympiaEmojiFallback(randomItem); + }; + symbol.appendChild(img); + + const leftPos = Math.random() * 100; + const delaySeconds = Math.random() * 10; + + let durationSeconds = 8; + if (useRandomDuration) { + durationSeconds = Math.random() * 5 + 6; // 6 to 11 seconds + } + + const startRot = Math.random() * 360; + symbol.style.setProperty('--start-rot', `${startRot}deg`); + symbol.style.setProperty('--end-rot', `${startRot + (Math.random() > 0.5 ? 360 : -360)}deg`); + + symbol.style.left = `${leftPos}vw`; + symbol.style.animationDuration = `${durationSeconds}s`; + symbol.style.animationDelay = `${delaySeconds}s`; + + container.appendChild(symbol); + } + + // Olympic Ring Colors + const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E']; + const confettiCount = isMobile ? 30 : 60; + + for (let i = 0; i < confettiCount; i++) { + let confetti = document.createElement('div'); + confetti.className = 'olympia-confetti'; + + const color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.style.backgroundColor = color; + + const leftPos = Math.random() * 100; + const delaySeconds = Math.random() * 8; + const duration = Math.random() * 3 + 5; + + confetti.style.left = `${leftPos}vw`; + confetti.style.animationDuration = `${duration}s`; + confetti.style.animationDelay = `${delaySeconds}s`; + + container.appendChild(confetti); + } +} + +function getOlympiaEmojiFallback(type) { + if (type === 'gold') return '🥇'; + if (type === 'silver') return '🥈'; + if (type === 'bronze') return '🥉'; + if (type === 'torch') return '🔥'; + return ''; +} + +function initializeOlympia() { + if (!olympia) return; + createOlympia(); + toggleOlympia(); +} + +initializeOlympia(); diff --git a/Jellyfin.Plugin.Seasonals/Web/space.css b/Jellyfin.Plugin.Seasonals/Web/space.css new file mode 100644 index 0000000..3c8b31b --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/space.css @@ -0,0 +1,58 @@ +.space-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; + contain: strict; +} + +.space-symbol { + position: absolute; + animation-timing-function: linear; + animation-iteration-count: infinite; + font-size: 3rem; + opacity: 0.85; + z-index: 9999; +} + +.space-symbol img { + width: 6vh; + height: auto; + max-width: 60px; + object-fit: contain; + /* Add a slow spin to images */ + animation: space-slow-spin var(--rot-dur, 20s) linear infinite; +} + +/* Specific elements scaling */ +.space-planet1, .space-planet2 { font-size: 4rem; } +.space-planet1 img, .space-planet2 img { width: 8vh; max-width: 80px; } +.space-star { font-size: 2rem; opacity: 0.6; } +.space-star img { width: 3vh; max-width: 30px; } + +@keyframes space-drift-right { + 0% { + transform: translateX(0) scaleX(-1); + } + 100% { + transform: translateX(120vw) scaleX(-1); + } +} + +@keyframes space-drift-left { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-120vw); + } +} + +@keyframes space-slow-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/space.js b/Jellyfin.Plugin.Seasonals/Web/space.js new file mode 100644 index 0000000..a986cb2 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/space.js @@ -0,0 +1,124 @@ +const config = window.SeasonalsPluginConfig?.Space || {}; + +const space = config.EnableSpace !== undefined ? config.EnableSpace : true; +const symbolCount = config.SymbolCount || 25; +const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; +const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; + +let msgPrinted = false; + +function toggleSpace() { + const container = document.querySelector('.space-container'); + if (!container) return; + + const videoPlayer = document.querySelector('.videoPlayerContainer'); + const trailerPlayer = document.querySelector('.youtubePlayerContainer'); + const isDashboard = document.body.classList.contains('dashboardDocument'); + const hasUserMenu = document.querySelector('#app-user-menu'); + + if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { + container.style.display = 'none'; + if (!msgPrinted) { + console.log('Space hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Space visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleSpace); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +function createSpace() { + const container = document.querySelector('.space-container') || document.createElement('div'); + + if (!document.querySelector('.space-container')) { + container.className = 'space-container'; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + const standardCount = 15; + const totalSymbols = symbolCount + standardCount; + + let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; + let finalCount = totalSymbols; + + if (isMobile) { + finalCount = enableRandomMobile ? totalSymbols : standardCount; + } + + const useRandomDuration = enableDifferentDuration !== false; + + const activeItems = ['planet1', 'planet2', 'star', 'astronaut', 'rocket']; + + for (let i = 0; i < finalCount; i++) { + let symbol = document.createElement('div'); + + const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; + symbol.className = `space-symbol space-${randomItem}`; + + let img = document.createElement('img'); + img.src = `../Seasonals/Resources/space_images/${randomItem}.png`; + img.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = getSpaceEmojiFallback(randomItem); + }; + symbol.appendChild(img); + + const topPos = Math.random() * 90; // 0 to 90vh + const delaySeconds = Math.random() * 10; + + let durationSeconds = 15; + if (useRandomDuration) { + durationSeconds = Math.random() * 15 + 15; // 15 to 30 seconds for slow drift + } + + // Randomly pick direction: left-to-right OR right-to-left + const goRight = Math.random() > 0.5; + if (goRight) { + symbol.style.animationName = 'space-drift-right'; + symbol.style.left = '-10vw'; + symbol.style.transform = 'scaleX(-1)'; // flip some items horizontally if moving right + } else { + symbol.style.animationName = 'space-drift-left'; + symbol.style.right = '-10vw'; + } + + symbol.style.top = `${topPos}vh`; + symbol.style.animationDuration = `${durationSeconds}s`; + symbol.style.animationDelay = `${delaySeconds}s`; + + // Add a slow rotation + symbol.style.setProperty('--rot-dur', `${durationSeconds}s`); + + container.appendChild(symbol); + } +} + +function getSpaceEmojiFallback(type) { + if (type === 'planet1') return '🪐'; + if (type === 'planet2') return '🌍'; + if (type === 'star') return '⭐'; + if (type === 'astronaut') return '👨‍🚀'; + if (type === 'rocket') return '🚀'; + return '✨'; +} + +function initializeSpace() { + if (!space) return; + createSpace(); + toggleSpace(); +} + +initializeSpace(); diff --git a/Jellyfin.Plugin.Seasonals/Web/sports.css b/Jellyfin.Plugin.Seasonals/Web/sports.css new file mode 100644 index 0000000..b78e6e0 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/sports.css @@ -0,0 +1,81 @@ +.sports-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; + contain: strict; +} + +.sports-symbol { + position: absolute; + top: -10vh; + animation: sports-fall linear infinite; + font-size: 3rem; /* Fallback emoji size */ + opacity: 0.9; +} + +.sports-symbol img { + width: 6vh; + height: auto; + max-width: 60px; + object-fit: contain; +} + +.sports-confetti { + position: absolute; + top: -5vh; + width: 10px; + height: 15px; + opacity: 0.8; + animation: sports-confetti-fall linear infinite; + border-radius: 2px; +} + +.sports-turf { + position: absolute; + bottom: 0; + left: 0; + width: 100vw; + height: 12vh; + background: linear-gradient(180deg, transparent 0%, rgba(34, 139, 34, 0.4) 30%, rgba(0, 100, 0, 0.8) 100%); + pointer-events: none; + z-index: 10; +} + +@keyframes sports-fall { + 0% { + transform: translateY(-10vh) rotate(var(--start-rot, 0deg)); + opacity: 0; + } + 10% { + opacity: 1; + } + 85% { + opacity: 1; + } + 100% { + transform: translateY(110vh) rotate(var(--end-rot, 360deg)); + opacity: 0; + } +} + +@keyframes sports-confetti-fall { + 0% { + transform: translateY(-5vh) rotateX(0deg) rotateY(0deg); + opacity: 0; + } + 5% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(105vh) rotateX(720deg) rotateY(360deg); + opacity: 0; + } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/sports.js b/Jellyfin.Plugin.Seasonals/Web/sports.js new file mode 100644 index 0000000..d7c4713 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/sports.js @@ -0,0 +1,146 @@ +const config = window.SeasonalsPluginConfig?.Sports || {}; + +const sports = config.EnableSports !== undefined ? config.EnableSports : true; +const symbolCount = config.SymbolCount || 25; +const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; +const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; + +let msgPrinted = false; + +function toggleSports() { + const container = document.querySelector('.sports-container'); + if (!container) return; + + const videoPlayer = document.querySelector('.videoPlayerContainer'); + const trailerPlayer = document.querySelector('.youtubePlayerContainer'); + const isDashboard = document.body.classList.contains('dashboardDocument'); + const hasUserMenu = document.querySelector('#app-user-menu'); + + if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { + container.style.display = 'none'; + if (!msgPrinted) { + console.log('Sports hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Sports visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleSports); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +function createSports() { + const container = document.querySelector('.sports-container') || document.createElement('div'); + + if (!document.querySelector('.sports-container')) { + container.className = 'sports-container'; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + // Create a turf/grass overlay at the bottom + const turf = document.createElement('div'); + turf.className = 'sports-turf'; + container.appendChild(turf); + + const standardCount = 15; + const totalSymbols = symbolCount + standardCount; + + let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; + let finalCount = totalSymbols; + + if (isMobile) { + finalCount = enableRandomMobile ? totalSymbols : standardCount; + } + + const useRandomDuration = enableDifferentDuration !== false; + + // Standard sports items to spawn + const activeItems = ['soccer', 'football', 'yellow_card', 'red_card', 'trophy']; + + // Create falling sports items + for (let i = 0; i < finalCount; i++) { + let symbol = document.createElement('div'); + + // Randomly pick an item + const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; + symbol.className = `sports-symbol sports-${randomItem}`; + + // Try load image + let img = document.createElement('img'); + img.src = `../Seasonals/Resources/sports_images/${randomItem}.png`; + img.onerror = function() { + this.style.display = 'none'; // hide broken image icon + this.parentElement.innerHTML = getEmojiFallback(randomItem); // inject emoji fallback + }; + symbol.appendChild(img); + + const leftPos = Math.random() * 100; + const delaySeconds = Math.random() * 10; + + let durationSeconds = 8; + if (useRandomDuration) { + durationSeconds = Math.random() * 4 + 6; // 6 to 10 seconds + } + + // Add a random slight rotation difference + const startRot = Math.random() * 360; + symbol.style.setProperty('--start-rot', `${startRot}deg`); + symbol.style.setProperty('--end-rot', `${startRot + (Math.random() > 0.5 ? 360 : -360)}deg`); + + symbol.style.left = `${leftPos}vw`; + symbol.style.animationDuration = `${durationSeconds}s`; + symbol.style.animationDelay = `${delaySeconds}s`; + + container.appendChild(symbol); + } + + // Add Germany Colored confetti (Black, Red, Gold) + const confettiColors = ['#000000', '#FF0000', '#FFCC00']; + const confettiCount = isMobile ? 30 : 60; + + for (let i = 0; i < confettiCount; i++) { + let confetti = document.createElement('div'); + confetti.className = 'sports-confetti'; + + const color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.style.backgroundColor = color; + + const leftPos = Math.random() * 100; + const delaySeconds = Math.random() * 8; + const duration = Math.random() * 3 + 4; // 4 to 7 seconds + + confetti.style.left = `${leftPos}vw`; + confetti.style.animationDuration = `${duration}s`; + confetti.style.animationDelay = `${delaySeconds}s`; + + container.appendChild(confetti); + } +} + +function getEmojiFallback(type) { + if (type === 'soccer') return '⚽'; + if (type === 'football') return '🏈'; + if (type === 'yellow_card') return '🟨'; + if (type === 'red_card') return '🟥'; + if (type === 'trophy') return '🏆'; + return ''; +} + +function initializeSports() { + if (!sports) return; + createSports(); + toggleSports(); +} + +initializeSports(); diff --git a/Jellyfin.Plugin.Seasonals/Web/underwater.css b/Jellyfin.Plugin.Seasonals/Web/underwater.css new file mode 100644 index 0000000..709d7ef --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/underwater.css @@ -0,0 +1,88 @@ +.underwater-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; + contain: strict; +} + +.underwater-bg { + position: absolute; + top: 0; left: 0; width: 100vw; height: 100vh; + background: linear-gradient(180deg, rgba(0, 105, 148, 0.1) 0%, rgba(0, 48, 143, 0.4) 100%); + pointer-events: none; + z-index: 10; +} + +.underwater-symbol { + position: absolute; + animation-timing-function: linear; + animation-iteration-count: infinite; + font-size: 3rem; + opacity: 0.9; + z-index: 20; +} + +.underwater-symbol img { + width: 6vh; + height: auto; + max-width: 60px; + object-fit: contain; +} + +.underwater-seaweed { + position: absolute; + bottom: -1vh; + font-size: 4rem; + transform-origin: bottom center; + animation: underwater-sway 4s ease-in-out infinite alternate; + z-index: 30; + opacity: 0.8; +} + +.underwater-seaweed img { + height: 12vh; + width: auto; + object-fit: contain; + max-height: 120px; +} + +.underwater-bubble { + position: absolute; + bottom: -5vh; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.4); + background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1)); + box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.5); + animation: underwater-bubble-rise linear infinite; + z-index: 40; +} + +@keyframes underwater-swim-right { + 0% { transform: translateX(0) translateY(0) scaleX(-1); } + 50% { transform: translateX(65vw) translateY(-5vh) scaleX(-1); } + 100% { transform: translateX(130vw) translateY(0) scaleX(-1); } +} + +@keyframes underwater-swim-left { + 0% { transform: translateX(0) translateY(0); } + 50% { transform: translateX(-65vw) translateY(5vh); } + 100% { transform: translateX(-130vw) translateY(0); } +} + +@keyframes underwater-sway { + 0% { transform: rotate(-10deg); } + 100% { transform: rotate(10deg); } +} + +@keyframes underwater-bubble-rise { + 0% { transform: translateY(0) translateX(0); opacity: 0; } + 10% { opacity: 0.7; } + 50% { transform: translateY(-55vh) translateX(-20px); } + 90% { opacity: 0; } + 100% { transform: translateY(-110vh) translateX(10px); opacity: 0; } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/underwater.js b/Jellyfin.Plugin.Seasonals/Web/underwater.js new file mode 100644 index 0000000..7cd68db --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/underwater.js @@ -0,0 +1,180 @@ +const config = window.SeasonalsPluginConfig?.Underwater || {}; + +const underwater = config.EnableUnderwater !== undefined ? config.EnableUnderwater : true; +const symbolCount = config.SymbolCount || 15; +const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; +const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; + +let msgPrinted = false; + +function toggleUnderwater() { + const container = document.querySelector('.underwater-container'); + if (!container) return; + + const videoPlayer = document.querySelector('.videoPlayerContainer'); + const trailerPlayer = document.querySelector('.youtubePlayerContainer'); + const isDashboard = document.body.classList.contains('dashboardDocument'); + const hasUserMenu = document.querySelector('#app-user-menu'); + + if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { + container.style.display = 'none'; + if (!msgPrinted) { + console.log('Underwater hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Underwater visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleUnderwater); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +function createUnderwater() { + const container = document.querySelector('.underwater-container') || document.createElement('div'); + + if (!document.querySelector('.underwater-container')) { + container.className = 'underwater-container'; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + // Deep blue overlay + const bg = document.createElement('div'); + bg.className = 'underwater-bg'; + container.appendChild(bg); + + const standardCount = 8; + const totalSymbols = symbolCount + standardCount; + + let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; + let finalCount = totalSymbols; + + if (isMobile) { + finalCount = enableRandomMobile ? totalSymbols : standardCount; + } + + const useRandomDuration = enableDifferentDuration !== false; + + // Seaweed swaying at the bottom + for (let i = 0; i < 4; i++) { + let seaweed = document.createElement('div'); + seaweed.className = 'underwater-seaweed'; + seaweed.style.left = `${10 + (i * 25)}vw`; + seaweed.style.animationDelay = `-${Math.random() * 5}s`; + + // Randomly flip + if (Math.random() > 0.5) { + seaweed.style.transform = 'scaleX(-1)'; + } + + let img = document.createElement('img'); + img.src = '../Seasonals/Resources/underwater_images/seaweed.png'; + img.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = '🌿'; + this.parentElement.style.fontSize = '3rem'; + this.parentElement.style.bottom = '0'; + this.parentElement.style.transformOrigin = 'bottom center'; + }; + seaweed.appendChild(img); + container.appendChild(seaweed); + } + + const activeItems = ['fish_orange', 'fish_blue', 'jellyfish', 'turtle']; + + for (let i = 0; i < finalCount; i++) { + let symbol = document.createElement('div'); + + const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; + symbol.className = `underwater-symbol underwater-${randomItem}`; + + let img = document.createElement('img'); + img.src = `../Seasonals/Resources/underwater_images/${randomItem}.png`; + img.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = getUnderwaterEmojiFallback(randomItem); + }; + symbol.appendChild(img); + + const topPos = 10 + Math.random() * 80; // 10 to 90vh + const delaySeconds = Math.random() * 10; + + let durationSeconds = 15; + if (useRandomDuration) { + durationSeconds = Math.random() * 10 + 15; // 15 to 25 seconds slow swimming + } + + // Randomly pick direction: left-to-right OR right-to-left + const goRight = Math.random() > 0.5; + if (goRight) { + symbol.style.animationName = 'underwater-swim-right'; + symbol.style.left = '-10vw'; + } else { + symbol.style.animationName = 'underwater-swim-left'; + symbol.style.right = '-10vw'; + symbol.style.transform = 'scaleX(-1)'; // flip fish to face left + } + + symbol.style.top = `${topPos}vh`; + symbol.style.animationDuration = `${durationSeconds}s`; + symbol.style.animationDelay = `${delaySeconds}s`; + + // Small vertical sway + const swimSway = document.createElement('div'); + swimSway.style.animation = `underwater-sway ${Math.random() * 2 + 3}s ease-in-out infinite`; + swimSway.appendChild(symbol.cloneNode(true)); + symbol.innerHTML = ''; + symbol.appendChild(swimSway); + + container.appendChild(symbol); + } + + // Bubbles + const bubbleCount = isMobile ? 15 : 30; + + for (let i = 0; i < bubbleCount; i++) { + let bubble = document.createElement('div'); + bubble.className = 'underwater-bubble'; + + const leftPos = Math.random() * 100; + const delaySeconds = Math.random() * 8; + const duration = Math.random() * 4 + 4; // 4 to 8s rising + + bubble.style.left = `${leftPos}vw`; + bubble.style.animationDuration = `${duration}s`; + bubble.style.animationDelay = `${delaySeconds}s`; + + // randomize bubble size + const size = Math.random() * 15 + 5; + bubble.style.width = `${size}px`; + bubble.style.height = `${size}px`; + + container.appendChild(bubble); + } +} + +function getUnderwaterEmojiFallback(type) { + if (type === 'fish_orange') return '🐠'; + if (type === 'fish_blue') return '🐟'; + if (type === 'jellyfish') return '🪼'; + if (type === 'turtle') return '🐢'; + return '🫧'; +} + +function initializeUnderwater() { + if (!underwater) return; + createUnderwater(); + toggleUnderwater(); +} + +initializeUnderwater();