From 2bbf13c04427ac1f55f468a6e4ae1498ec8e8993 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:22:24 +0100 Subject: [PATCH] Add Spooky theme: replace Legacy Halloween with Spooky options and implement related CSS and JavaScript for visual effects --- .../Configuration/PluginConfiguration.cs | 11 +- .../Configuration/configPage.html | 52 +++++-- Jellyfin.Plugin.Seasonals/Web/seasonals.js | 8 +- Jellyfin.Plugin.Seasonals/Web/spooky.css | 126 +++++++++++++++ Jellyfin.Plugin.Seasonals/Web/spooky.js | 144 ++++++++++++++++++ Jellyfin.Plugin.Seasonals/Web/test-site.html | 6 +- 6 files changed, 321 insertions(+), 26 deletions(-) create mode 100644 Jellyfin.Plugin.Seasonals/Web/spooky.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/spooky.js diff --git a/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs index 30a61a0..8a17024 100644 --- a/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs @@ -46,7 +46,7 @@ public class PluginConfiguration : BasePluginConfiguration Oktoberfest = new OktoberfestOptions(); Friday13 = new Friday13Options(); Eid = new EidOptions(); - LegacyHalloween = new LegacyHalloweenOptions(); + Spooky = new SpookyOptions(); Sports = new SportsOptions(); Olympia = new OlympiaOptions(); Space = new SpaceOptions(); @@ -111,7 +111,7 @@ public class PluginConfiguration : BasePluginConfiguration public OktoberfestOptions Oktoberfest { get; set; } public Friday13Options Friday13 { get; set; } public EidOptions Eid { get; set; } - public LegacyHalloweenOptions LegacyHalloween { get; set; } + public SpookyOptions Spooky { get; set; } public SportsOptions Sports { get; set; } public OlympiaOptions Olympia { get; set; } public SpaceOptions Space { get; set; } @@ -370,10 +370,13 @@ public class EidOptions public bool EnableEid { get; set; } = true; } -public class LegacyHalloweenOptions +public class SpookyOptions { - public bool EnableLegacyHalloween { get; set; } = true; + public bool EnableSpooky { get; set; } = true; public int SymbolCount { get; set; } = 25; + public bool EnableSpookySway { get; set; } = true; + public int SpookySize { get; set; } = 20; + public int SpookyGlowSize { get; set; } = 2; } public class SportsOptions diff --git a/Jellyfin.Plugin.Seasonals/Configuration/configPage.html b/Jellyfin.Plugin.Seasonals/Configuration/configPage.html index f16e85d..7bae820 100644 --- a/Jellyfin.Plugin.Seasonals/Configuration/configPage.html +++ b/Jellyfin.Plugin.Seasonals/Configuration/configPage.html @@ -87,7 +87,7 @@ - +
The season to display if automation is disabled or no "Auto Selection" rule matches the current date.
@@ -371,19 +371,35 @@
- Legacy Halloween + Spooky Theme
-
Enable the classic Halloween theme (only floating symbols, no fog/spiders).
+
Enable the Spooky Halloween theme (floating, swaying symbols).
- - + +
Number of floating symbols.
+
+ + +
Size of the floating symbols in pixels (default 30).
+
+
+ +
+
+ + +
Size of the glow effect in pixels (0 to disable, default 5).
+

@@ -1623,8 +1639,8 @@ // Eid document.querySelector('#EidSymbolCount').value = config.Eid.SymbolCount || 25; - // Legacy Halloween - document.querySelector('#LegacyHalloweenSymbolCount').value = config.LegacyHalloween.SymbolCount || 25; + // Spooky Theme + document.querySelector('#SpookyCount').value = config.Spooky.SymbolCount || 25; // Sports document.querySelector('#EnableSports').checked = config.Sports.EnableSports || false; @@ -1779,9 +1795,12 @@ document.querySelector('#PrideHeartSize').value = config.Pride.HeartSize; document.querySelector('#PrideColorHeader').checked = config.Pride.ColorHeader; - // Legacy Halloween - document.querySelector('#EnableLegacyHalloween').checked = config.LegacyHalloween.EnableLegacyHalloween !== undefined ? config.LegacyHalloween.EnableLegacyHalloween : true; - document.querySelector('#LegacyHalloweenCount').value = config.LegacyHalloween.SymbolCount !== undefined ? config.LegacyHalloween.SymbolCount : 25; + // Spooky Theme + document.querySelector('#EnableSpooky').checked = config.Spooky.EnableSpooky !== undefined ? config.Spooky.EnableSpooky : true; + document.querySelector('#SpookyCount').value = config.Spooky.SymbolCount !== undefined ? config.Spooky.SymbolCount : 25; + document.querySelector('#SpookySize').value = config.Spooky.SpookySize !== undefined ? config.Spooky.SpookySize : 30; + document.querySelector('#EnableSpookySway').checked = config.Spooky.EnableSpookySway !== undefined ? config.Spooky.EnableSpookySway : true; + document.querySelector('#SpookyGlowSize').value = config.Spooky.SpookyGlowSize !== undefined ? config.Spooky.SpookyGlowSize : 5; // Rain document.querySelector('#EnableRain').checked = config.Rain.EnableRain; @@ -1965,9 +1984,12 @@ config.Resurrection.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomResurrectionMobile').checked; config.Resurrection.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationResurrection').checked; - // Legacy Halloween - config.LegacyHalloween.EnableLegacyHalloween = document.querySelector('#EnableLegacyHalloween').checked; - config.LegacyHalloween.SymbolCount = parseInt(document.querySelector('#LegacyHalloweenCount').value); + // Spooky Theme + config.Spooky.EnableSpooky = document.querySelector('#EnableSpooky').checked; + config.Spooky.SymbolCount = parseInt(document.querySelector('#SpookyCount').value); + config.Spooky.SpookySize = parseInt(document.querySelector('#SpookySize').value); + config.Spooky.EnableSpookySway = document.querySelector('#EnableSpookySway').checked; + config.Spooky.SpookyGlowSize = parseInt(document.querySelector('#SpookyGlowSize').value); // Spring config.Spring.EnableSpring = document.querySelector('#EnableSpring').checked; diff --git a/Jellyfin.Plugin.Seasonals/Web/seasonals.js b/Jellyfin.Plugin.Seasonals/Web/seasonals.js index 347a0e8..b390481 100644 --- a/Jellyfin.Plugin.Seasonals/Web/seasonals.js +++ b/Jellyfin.Plugin.Seasonals/Web/seasonals.js @@ -148,10 +148,10 @@ const ThemeConfigs = { js: '../Seasonals/Resources/eid.js', containerClass: 'eid-container' }, - legacyhalloween: { - css: '../Seasonals/Resources/legacyhalloween.css', - js: '../Seasonals/Resources/legacyhalloween.js', - containerClass: 'legacyhalloween-container' + spooky: { + css: '../Seasonals/Resources/spooky.css', + js: '../Seasonals/Resources/spooky.js', + containerClass: 'spooky-container' }, sports: { css: '../Seasonals/Resources/sports.css', diff --git a/Jellyfin.Plugin.Seasonals/Web/spooky.css b/Jellyfin.Plugin.Seasonals/Web/spooky.css new file mode 100644 index 0000000..de9a5a0 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/spooky.css @@ -0,0 +1,126 @@ +.spooky-container { + display: block; + position: fixed; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; +} + +.spooky { + position: fixed; + top: 0; + will-change: transform; + translate: 0 120vh; + z-index: 15; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + cursor: default; + + -webkit-animation-name: spooky-float; + -webkit-animation-duration: 10s; + -webkit-animation-timing-function: linear; + -webkit-animation-iteration-count: infinite; + -webkit-animation-play-state: running; + + animation-name: spooky-float; + animation-duration: 10s; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-play-state: running; +} + +.spooky-inner { + width: 30px; + height: auto; + will-change: transform; + + -webkit-animation-name: spooky-shake; + -webkit-animation-duration: 3s; + -webkit-animation-timing-function: ease-in-out; + -webkit-animation-iteration-count: infinite; + -webkit-animation-play-state: running; + + animation-name: spooky-shake; + animation-duration: 3s; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + animation-play-state: running; +} + +.spooky-inner img { + height: auto; + width: 100%; +} + +@-webkit-keyframes spooky-float { + 0% { + translate: 0 120vh; + opacity: 0; + } + 10% { + opacity: 0.8; + } + 90% { + opacity: 0.8; + } + 100% { + translate: 0 -150px; + opacity: 0; + } +} + +@keyframes spooky-float { + 0% { + translate: 0 120vh; + opacity: 0; + } + 10% { + opacity: 0.8; + } + 90% { + opacity: 0.8; + } + 100% { + translate: 0 -150px; + opacity: 0; + } +} + +@-webkit-keyframes spooky-shake { + 0%, 100% { + transform: translateX(0) scale(1) rotate(15deg); + } + 50% { + transform: translateX(80px) scale(1.2) rotate(-15deg); + } +} + +@keyframes spooky-shake { + 0%, 100% { + transform: translateX(0) scale(1) rotate(15deg); + } + 50% { + transform: translateX(80px) scale(1.2) rotate(-15deg); + } +} + +/* Base predefined starting offsets (if not overridden by js) */ +.spooky:nth-of-type(0) { left: 1%; } +.spooky:nth-of-type(1) { left: 10%; } +.spooky:nth-of-type(2) { left: 20%; } +.spooky:nth-of-type(3) { left: 30%; } +.spooky:nth-of-type(4) { left: 40%; } +.spooky:nth-of-type(5) { left: 50%; } +.spooky:nth-of-type(6) { left: 60%; } +.spooky:nth-of-type(7) { left: 70%; } +.spooky:nth-of-type(8) { left: 80%; } +.spooky:nth-of-type(9) { left: 90%; } +.spooky:nth-of-type(10) { left: 25%; } +.spooky:nth-of-type(11) { left: 65%; } diff --git a/Jellyfin.Plugin.Seasonals/Web/spooky.js b/Jellyfin.Plugin.Seasonals/Web/spooky.js new file mode 100644 index 0000000..47f3cae --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/spooky.js @@ -0,0 +1,144 @@ +const config = window.SeasonalsPluginConfig?.Spooky || {}; + +const spooky = config.EnableSpooky !== undefined ? config.EnableSpooky : true; // enable/disable +const spookyCount = config.SymbolCount || 25; // count of random extra symbols +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; +const enableSpookySway = config.EnableSpookySway !== undefined ? config.EnableSpookySway : true; +const spookySize = config.SpookySize || 20; +const spookyGlowSize = config.SpookyGlowSize !== undefined ? config.SpookyGlowSize : 2; + +let msgPrinted = false; + +// function to check and control the spooky theme +function toggleSpooky() { + const spookyContainer = document.querySelector('.spooky-container'); + if (!spookyContainer) 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) { + spookyContainer.style.display = 'none'; // hide spooky + if (!msgPrinted) { + console.log('Spooky Theme hidden'); + msgPrinted = true; + } + } else { + spookyContainer.style.display = 'block'; // show spooky + if (msgPrinted) { + console.log('Spooky Theme visible'); + msgPrinted = false; + } + } +} + +// observe changes in the DOM +const observer = new MutationObserver(toggleSpooky); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +const spookyImages = [ + "../Seasonals/Resources/halloween_images/ghost_20x20.png", + "../Seasonals/Resources/halloween_images/bat_20x20.png", + "../Seasonals/Resources/halloween_images/pumpkin_20x20.png", +]; + +// create spooky objects +function createSpooky() { + const container = document.querySelector('.spooky-container') || document.createElement("div"); + + if (!document.querySelector('.spooky-container')) { + container.className = "spooky-container"; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + // Base items per image + for (let i = 0; i < 4; i++) { + spookyImages.forEach(imageSrc => { + const spookyOuter = document.createElement("div"); + spookyOuter.className = "spooky"; + + const spookyInner = document.createElement("div"); + spookyInner.className = "spooky-inner"; + spookyInner.style.width = `${spookySize}px`; + if (!enableSpookySway) spookyInner.style.animationName = 'none'; + + const img = document.createElement("img"); + img.src = imageSrc; + img.style.filter = spookyGlowSize > 0 ? `drop-shadow(0 0 ${spookyGlowSize}px rgba(255, 120, 0, 0.4))` : 'none'; + + // randomize fall and sway (shake) speeds like halloween.js + if (enableDifferentDuration) { + const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 10s) + const randomAnimationDuration2 = Math.random() * 5 + 2; // shake duration (2s to 5s) + spookyOuter.style.animationDuration = `${randomAnimationDuration}s`; + spookyInner.style.animationDuration = `${randomAnimationDuration2}s`; + } + + const randomLeft = Math.random() * 100; + const randomAnimationDelay = Math.random() * 10; + const randomAnimationDelay2 = Math.random() * 3; + + spookyOuter.style.left = `${randomLeft}%`; + spookyOuter.style.animationDelay = `${randomAnimationDelay}s`; + spookyInner.style.animationDelay = `${randomAnimationDelay2}s`; + + spookyInner.appendChild(img); + spookyOuter.appendChild(spookyInner); + container.appendChild(spookyOuter); + }); + } + + // Add configured extra symbols + for (let i = 0; i < spookyCount; i++) { + const spookyOuter = document.createElement("div"); + spookyOuter.className = "spooky"; + + const spookyInner = document.createElement("div"); + spookyInner.className = "spooky-inner"; + spookyInner.style.width = `${spookySize}px`; + if (!enableSpookySway) spookyInner.style.animationName = 'none'; + + const imageSrc = spookyImages[Math.floor(Math.random() * spookyImages.length)]; + const img = document.createElement("img"); + img.src = imageSrc; + img.style.filter = spookyGlowSize > 0 ? `drop-shadow(0 0 ${spookyGlowSize}px rgba(255, 120, 0, 0.4))` : 'none'; + + const randomLeft = Math.random() * 100; + const randomAnimationDelay = Math.random() * 10; + const randomAnimationDelay2 = Math.random() * 3; + + spookyOuter.style.left = `${randomLeft}%`; + spookyOuter.style.animationDelay = `${randomAnimationDelay}s`; + spookyInner.style.animationDelay = `${randomAnimationDelay2}s`; + + if (enableDifferentDuration) { + const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s) + const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s) + spookyOuter.style.animationDuration = `${randomAnimationDuration}s`; + spookyInner.style.animationDuration = `${randomAnimationDuration2}s`; + } + + spookyInner.appendChild(img); + spookyOuter.appendChild(spookyInner); + container.appendChild(spookyOuter); + } + + console.log('Spooky symbols added'); +} + +// initialize spooky +function initializeSpooky() { + if (!spooky) return; + createSpooky(); + toggleSpooky(); +} + +// initialize script +initializeSpooky(); diff --git a/Jellyfin.Plugin.Seasonals/Web/test-site.html b/Jellyfin.Plugin.Seasonals/Web/test-site.html index a1177b8..9e070d5 100644 --- a/Jellyfin.Plugin.Seasonals/Web/test-site.html +++ b/Jellyfin.Plugin.Seasonals/Web/test-site.html @@ -240,7 +240,7 @@ - + @@ -346,7 +346,7 @@ snowstorm: { css: 'snowstorm.css', js: 'snowstorm.js', container: 'snowstorm-container' }, fireworks: { css: 'fireworks.css', js: 'fireworks.js', container: 'fireworks' }, halloween: { css: 'halloween.css', js: 'halloween.js', container: 'halloween-container' }, - legacyhalloween: { css: 'legacyhalloween.css', js: 'legacyhalloween.js', container: 'legacyhalloween-container' }, + spooky: { css: 'spooky.css', js: 'spooky.js', container: 'spooky-container' }, hearts: { css: 'hearts.css', js: 'hearts.js', container: 'hearts-container' }, christmas: { css: 'christmas.css', js: 'christmas.js', container: 'christmas-container' }, santa: { css: 'santa.css', js: 'santa.js', container: 'santa-container' }, @@ -399,7 +399,7 @@ // Remove any theme-created containers on body const knownContainers = [ '.snowfall-container', '.snowflakes', '.snowstorm-container', - '.fireworks', '.halloween-container', '.legacyhalloween-container', '.hearts-container', + '.fireworks', '.halloween-container', '.spooky-container', '.hearts-container', '.christmas-container', '.santa-container', '.autumn-container', '.easter-container', '.resurrection-container', '.spring-container', '.summer-container', '.carnival-container', '.cherryblossom-container',