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
+
+
+
+
+
@@ -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',