From 0eeed99508b0c44ae8bfc5f496cecf8a6c73d733 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:22:54 +0100 Subject: [PATCH] Add summer animation effects with CSS and JavaScript implementation --- Jellyfin.Plugin.Seasonals/Web/summer.css | 80 +++++++++++++ Jellyfin.Plugin.Seasonals/Web/summer.js | 142 +++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 Jellyfin.Plugin.Seasonals/Web/summer.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/summer.js diff --git a/Jellyfin.Plugin.Seasonals/Web/summer.css b/Jellyfin.Plugin.Seasonals/Web/summer.css new file mode 100644 index 0000000..5ff53ed --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/summer.css @@ -0,0 +1,80 @@ +.summer-container { + display: block; + position: fixed; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; +} + +.summer-bubble { + position: fixed; + bottom: -50px; + z-index: 15; + background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1) 60%, rgba(255, 255, 255, 0.05)); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); + + will-change: transform, bottom; + animation-name: summer-rise, summer-wobble; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-duration: 10s, 3s; +} + +.summer-dust { + position: fixed; + bottom: -10px; + z-index: 12; + background-color: rgba(255, 223, 186, 0.6); + border-radius: 50%; + box-shadow: 0 0 5px rgba(255, 223, 186, 0.4); + + will-change: transform, bottom; + animation-name: summer-rise, summer-drift; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-duration: 20s, 5s; +} + +@keyframes summer-rise { + 0% { + bottom: -10%; + } + 100% { + bottom: 110%; + } +} + +@keyframes summer-wobble { + 0% { + transform: translateX(0); + } + 33% { + transform: translateX(15px); + } + 66% { + transform: translateX(-15px); + } + 100% { + transform: translateX(0); + } +} + +@keyframes summer-drift { + 0% { + transform: translateX(0) translateY(0); + } + 50% { + transform: translateX(30px) translateY(-20px); + } + 100% { + transform: translateX(0) translateY(0); + } +} + + diff --git a/Jellyfin.Plugin.Seasonals/Web/summer.js b/Jellyfin.Plugin.Seasonals/Web/summer.js new file mode 100644 index 0000000..13bf9a0 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/summer.js @@ -0,0 +1,142 @@ +const config = window.SeasonalsPluginConfig?.Summer || {}; + +const summer = config.EnableSummer !== undefined ? config.EnableSummer : true; // enable/disable summer +const summerCount = config.ObjectCount || 30; // default count +const randomSummer = config.EnableRandomSummer !== undefined ? config.EnableRandomSummer : true; // enable random objects +const randomSummerMobile = config.EnableRandomSummerMobile !== undefined ? config.EnableRandomSummerMobile : false; // enable random objects on mobile +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration + +let msgPrinted = false; + +function toggleSummer() { + const summerContainer = document.querySelector('.summer-container'); + if (!summerContainer) 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) { + summerContainer.style.display = 'none'; + if (!msgPrinted) { + console.log('Summer hidden'); + msgPrinted = true; + } + } else { + summerContainer.style.display = 'block'; + if (msgPrinted) { + console.log('Summer visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleSummer); +observer.observe(document.body, { childList: true, subtree: true, attributes: true }); + +function createBubble(container, isDust = false) { + const bubble = document.createElement('div'); + + if (isDust) { + bubble.classList.add('summer-dust'); + } else { + bubble.classList.add('summer-bubble'); + } + + // Random horizontal position + const randomLeft = Math.random() * 100; + bubble.style.left = `${randomLeft}%`; + + // Random size + if (!isDust) { + const size = Math.random() * 20 + 10; // 10-30px bubbles + bubble.style.width = `${size}px`; + bubble.style.height = `${size}px`; + } else { + const size = Math.random() * 3 + 1; // 1-4px dust + bubble.style.width = `${size}px`; + bubble.style.height = `${size}px`; + } + + // Animation properties + const duration = isDust ? (Math.random() * 20 + 10) : (Math.random() * 10 + 5); // Dust is slower + const delay = Math.random() * 10; + + if (enableDifferentDuration) { + bubble.style.animationDuration = `${duration}s`; + } + bubble.style.animationDelay = `${delay}s`; + + container.appendChild(bubble); +} + +function addRandomSummerObjects(count) { + const container = document.querySelector('.summer-container'); + if (!container) return; + + // Add bubbles + for (let i = 0; i < count; i++) { + createBubble(container, false); + } + + // Add some dust particles (more of them, they are subtle) + for (let i = 0; i < count * 2; i++) { + createBubble(container, true); + } +} + +function initSummerObjects() { + let container = document.querySelector('.summer-container'); + if (!container) { + container = document.createElement("div"); + container.className = "summer-container"; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + // Initial bubbles/dust + for (let i = 0; i < 10; i++) { + const bubble = document.createElement('div'); + const isDust = Math.random() > 0.5; + if (isDust) { + bubble.classList.add('summer-dust'); + } else { + bubble.classList.add('summer-bubble'); + } + + const randomLeft = Math.random() * 100; + bubble.style.left = `${randomLeft}%`; + + if (!isDust) { + const size = Math.random() * 20 + 10; + bubble.style.width = `${size}px`; + bubble.style.height = `${size}px`; + } else { + const size = Math.random() * 3 + 1; + bubble.style.width = `${size}px`; + bubble.style.height = `${size}px`; + } + + const duration = isDust ? (Math.random() * 20 + 10) : (Math.random() * 10 + 5); + if (enableDifferentDuration) { + bubble.style.animationDuration = `${duration}s`; + } + + bubble.style.animationDelay = `-${Math.random() * 10}s`; + container.appendChild(bubble); + } +} + +function initializeSummer() { + if (!summer) return; + initSummerObjects(); + toggleSummer(); + + const screenWidth = window.innerWidth; + if (randomSummer && (screenWidth > 768 || randomSummerMobile)) { + addRandomSummerObjects(summerCount); + } +} + +initializeSummer();