From 042d89f5b8867bf9bf21419726093d0086a193ce Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:23:15 +0100 Subject: [PATCH] Add Oscar feature: implement CSS and JS for Oscar animations, visibility control, and dynamic spotlight effects --- Jellyfin.Plugin.Seasonals/Web/oscar.css | 67 ++++++++++++++++++ Jellyfin.Plugin.Seasonals/Web/oscar.js | 94 +++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 Jellyfin.Plugin.Seasonals/Web/oscar.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/oscar.js diff --git a/Jellyfin.Plugin.Seasonals/Web/oscar.css b/Jellyfin.Plugin.Seasonals/Web/oscar.css new file mode 100644 index 0000000..f674cb9 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/oscar.css @@ -0,0 +1,67 @@ +.oscar-container { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; /* Behind popups but over background */ + contain: strict; + overflow: hidden; +} + +.oscar-carpet { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 15vh; + background: linear-gradient(to top, rgba(139, 0, 0, 0.8) 0%, transparent 100%); + opacity: 0.9; +} + +.oscar-spotlights { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.oscar-spotlight { + position: absolute; + top: -10vh; + /* MARK: SPOTLIGHT WIDTH CONFIGURATION */ + /* To adjust bottom width (spread), change 'width' property (e.g., 20vw for narrow, 40vw for wide). */ + /* To adjust top width (origin), modify first two percentages in 'clip-path' (e.g., 48% 0, 52% 0 for a very thin start). */ + width: 30vw; + height: 120vh; + background: linear-gradient(to bottom, rgba(255, 215, 0, 0.4) 0%, transparent 80%); + clip-path: polygon(45% 0, 55% 0, 100% 100%, 0 100%); + transform-origin: top center; + animation: spotlight-sweep 12s infinite alternate ease-in-out; + mix-blend-mode: screen; +} + +.oscar-flash { + position: absolute; + width: 10px; + height: 10px; + background: white; + border-radius: 50%; + box-shadow: 0 0 50px 30px rgba(255, 255, 255, 0.8), 0 0 100px 50px rgba(255, 255, 255, 0.5); + animation: flash-pop 0.2s cubic-bezier(0.1, 0.8, 0.1, 1); + mix-blend-mode: screen; +} + +@keyframes spotlight-sweep { + 0% { transform: rotate(-30deg); } + 100% { transform: rotate(30deg); } +} + +@keyframes flash-pop { + 0% { transform: scale(0.5); opacity: 1; } + 50% { transform: scale(2); opacity: 1; } + 100% { transform: scale(3); opacity: 0; } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/oscar.js b/Jellyfin.Plugin.Seasonals/Web/oscar.js new file mode 100644 index 0000000..1e9254b --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/oscar.js @@ -0,0 +1,94 @@ +const config = window.SeasonalsPluginConfig?.Oscar || {}; +const oscar = config.EnableOscar !== undefined ? config.EnableOscar : true; + +let msgPrinted = false; + +function toggleOscar() { + const container = document.querySelector('.oscar-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('Oscar hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Oscar visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleOscar); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + + +function createOscar(container) { + // Red carpet floor + const carpet = document.createElement('div'); + carpet.className = 'oscar-carpet'; + + // Spotlights + const spotlights = document.createElement('div'); + spotlights.className = 'oscar-spotlights'; + + for (let i = 0; i < 3; i++) { + const spot = document.createElement('div'); + spot.className = 'oscar-spotlight'; + spot.style.animationDelay = `-${Math.random() * 8}s`; + spot.style.left = `${20 + (i * 30)}%`; + spot.style.top = `${-5 - Math.random() * 15}vh`; // randomize top origin + spotlights.appendChild(spot); + } + + container.appendChild(carpet); + container.appendChild(spotlights); + + // Paparazzi flashes with randomized intervals + function flashLoop() { + if (!document.querySelector('.oscar-container')) { + setTimeout(flashLoop, 1000); // Check again later if hidden + return; + } + const flash = document.createElement('div'); + flash.className = 'oscar-flash'; + flash.style.left = `${Math.random() * 100}%`; + flash.style.top = `${Math.random() * 100}%`; + container.appendChild(flash); + setTimeout(() => flash.remove(), 200); + + // Randomize next flash between 200ms and 1500ms + const nextDelay = Math.random() * 1300 + 200; + setTimeout(flashLoop, nextDelay); + } + flashLoop(); +} + +function initializeOscar() { + if (!oscar) return; + + const container = document.querySelector('.oscar-container') || document.createElement("div"); + + if (!document.querySelector('.oscar-container')) { + container.className = "oscar-container"; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + createOscar(container); + toggleOscar(); +} + +initializeOscar();