Add Spooky theme: replace Legacy Halloween with Spooky options and implement related CSS and JavaScript for visual effects

This commit is contained in:
CodeDevMLH
2026-02-25 00:22:24 +01:00
parent 082120b70b
commit 2bbf13c044
6 changed files with 321 additions and 26 deletions

View File

@@ -46,7 +46,7 @@ public class PluginConfiguration : BasePluginConfiguration
Oktoberfest = new OktoberfestOptions(); Oktoberfest = new OktoberfestOptions();
Friday13 = new Friday13Options(); Friday13 = new Friday13Options();
Eid = new EidOptions(); Eid = new EidOptions();
LegacyHalloween = new LegacyHalloweenOptions(); Spooky = new SpookyOptions();
Sports = new SportsOptions(); Sports = new SportsOptions();
Olympia = new OlympiaOptions(); Olympia = new OlympiaOptions();
Space = new SpaceOptions(); Space = new SpaceOptions();
@@ -111,7 +111,7 @@ public class PluginConfiguration : BasePluginConfiguration
public OktoberfestOptions Oktoberfest { get; set; } public OktoberfestOptions Oktoberfest { get; set; }
public Friday13Options Friday13 { get; set; } public Friday13Options Friday13 { get; set; }
public EidOptions Eid { get; set; } public EidOptions Eid { get; set; }
public LegacyHalloweenOptions LegacyHalloween { get; set; } public SpookyOptions Spooky { get; set; }
public SportsOptions Sports { get; set; } public SportsOptions Sports { get; set; }
public OlympiaOptions Olympia { get; set; } public OlympiaOptions Olympia { get; set; }
public SpaceOptions Space { get; set; } public SpaceOptions Space { get; set; }
@@ -370,10 +370,13 @@ public class EidOptions
public bool EnableEid { get; set; } = true; 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 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 public class SportsOptions

View File

@@ -87,7 +87,7 @@
<option value="oktoberfest">Oktoberfest</option> <option value="oktoberfest">Oktoberfest</option>
<option value="friday13">Friday the 13th</option> <option value="friday13">Friday the 13th</option>
<option value="eid">Eid al-Fitr</option> <option value="eid">Eid al-Fitr</option>
<option value="legacyhalloween">Legacy Halloween</option> <option value="spooky">Spooky</option>
</select> </select>
<div class="fieldDescription">The season to display if automation is disabled or no "Auto Selection" rule matches the current date.</div> <div class="fieldDescription">The season to display if automation is disabled or no "Auto Selection" rule matches the current date.</div>
</div> </div>
@@ -371,19 +371,35 @@
<hr style="max-width: 800px; margin: 1em 0;"> <hr style="max-width: 800px; margin: 1em 0;">
<details> <details>
<summary>Legacy Halloween</summary> <summary>Spooky Theme</summary>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label"> <label class="emby-checkbox-label">
<input id="EnableLegacyHalloween" name="EnableLegacyHalloween" type="checkbox" is="emby-checkbox" /> <input id="EnableSpooky" name="EnableSpooky" type="checkbox" is="emby-checkbox" />
<span>Enable Legacy Halloween Seasonal</span> <span>Enable Spooky Seasonal</span>
</label> </label>
<div class="fieldDescription">Enable the classic Halloween theme (only floating symbols, no fog/spiders).</div> <div class="fieldDescription">Enable the Spooky Halloween theme (floating, swaying symbols).</div>
</div> </div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel" for="LegacyHalloweenCount">Symbol Count</label> <label class="inputLabel" for="SpookyCount">Symbol Count</label>
<input is="emby-input" type="number" id="LegacyHalloweenCount" name="LegacyHalloweenCount" /> <input is="emby-input" type="number" id="SpookyCount" name="SpookyCount" />
<div class="fieldDescription">Number of floating symbols.</div> <div class="fieldDescription">Number of floating symbols.</div>
</div> </div>
<div class="inputContainer">
<label class="inputLabel" for="SpookySize">Symbol Size</label>
<input is="emby-input" type="number" id="SpookySize" name="SpookySize" />
<div class="fieldDescription">Size of the floating symbols in pixels (default 30).</div>
</div>
<div class="checkboxContainer">
<label class="emby-checkbox-label">
<input id="EnableSpookySway" name="EnableSpookySway" type="checkbox" is="emby-checkbox" />
<span>Enable Swaying Motion</span>
</label>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpookyGlowSize">Glow Size</label>
<input is="emby-input" type="number" id="SpookyGlowSize" name="SpookyGlowSize" />
<div class="fieldDescription">Size of the glow effect in pixels (0 to disable, default 5).</div>
</div>
</details> </details>
<hr style="max-width: 800px; margin: 1em 0;"> <hr style="max-width: 800px; margin: 1em 0;">
@@ -1623,8 +1639,8 @@
// Eid // Eid
document.querySelector('#EidSymbolCount').value = config.Eid.SymbolCount || 25; document.querySelector('#EidSymbolCount').value = config.Eid.SymbolCount || 25;
// Legacy Halloween // Spooky Theme
document.querySelector('#LegacyHalloweenSymbolCount').value = config.LegacyHalloween.SymbolCount || 25; document.querySelector('#SpookyCount').value = config.Spooky.SymbolCount || 25;
// Sports // Sports
document.querySelector('#EnableSports').checked = config.Sports.EnableSports || false; document.querySelector('#EnableSports').checked = config.Sports.EnableSports || false;
@@ -1779,9 +1795,12 @@
document.querySelector('#PrideHeartSize').value = config.Pride.HeartSize; document.querySelector('#PrideHeartSize').value = config.Pride.HeartSize;
document.querySelector('#PrideColorHeader').checked = config.Pride.ColorHeader; document.querySelector('#PrideColorHeader').checked = config.Pride.ColorHeader;
// Legacy Halloween // Spooky Theme
document.querySelector('#EnableLegacyHalloween').checked = config.LegacyHalloween.EnableLegacyHalloween !== undefined ? config.LegacyHalloween.EnableLegacyHalloween : true; document.querySelector('#EnableSpooky').checked = config.Spooky.EnableSpooky !== undefined ? config.Spooky.EnableSpooky : true;
document.querySelector('#LegacyHalloweenCount').value = config.LegacyHalloween.SymbolCount !== undefined ? config.LegacyHalloween.SymbolCount : 25; 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 // Rain
document.querySelector('#EnableRain').checked = config.Rain.EnableRain; document.querySelector('#EnableRain').checked = config.Rain.EnableRain;
@@ -1965,9 +1984,12 @@
config.Resurrection.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomResurrectionMobile').checked; config.Resurrection.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomResurrectionMobile').checked;
config.Resurrection.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationResurrection').checked; config.Resurrection.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationResurrection').checked;
// Legacy Halloween // Spooky Theme
config.LegacyHalloween.EnableLegacyHalloween = document.querySelector('#EnableLegacyHalloween').checked; config.Spooky.EnableSpooky = document.querySelector('#EnableSpooky').checked;
config.LegacyHalloween.SymbolCount = parseInt(document.querySelector('#LegacyHalloweenCount').value); 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 // Spring
config.Spring.EnableSpring = document.querySelector('#EnableSpring').checked; config.Spring.EnableSpring = document.querySelector('#EnableSpring').checked;

View File

@@ -148,10 +148,10 @@ const ThemeConfigs = {
js: '../Seasonals/Resources/eid.js', js: '../Seasonals/Resources/eid.js',
containerClass: 'eid-container' containerClass: 'eid-container'
}, },
legacyhalloween: { spooky: {
css: '../Seasonals/Resources/legacyhalloween.css', css: '../Seasonals/Resources/spooky.css',
js: '../Seasonals/Resources/legacyhalloween.js', js: '../Seasonals/Resources/spooky.js',
containerClass: 'legacyhalloween-container' containerClass: 'spooky-container'
}, },
sports: { sports: {
css: '../Seasonals/Resources/sports.css', css: '../Seasonals/Resources/sports.css',

View File

@@ -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%; }

View File

@@ -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();

View File

@@ -240,7 +240,7 @@
<option value="snowstorm">Snowstorm</option> <option value="snowstorm">Snowstorm</option>
<option value="fireworks">Fireworks</option> <option value="fireworks">Fireworks</option>
<option value="halloween">Halloween</option> <option value="halloween">Halloween</option>
<option value="legacyhalloween">Legacy Halloween</option> <option value="spooky">Spooky</option>
<option value="hearts">Hearts</option> <option value="hearts">Hearts</option>
<option value="christmas">Christmas</option> <option value="christmas">Christmas</option>
<option value="santa">Santa</option> <option value="santa">Santa</option>
@@ -346,7 +346,7 @@
snowstorm: { css: 'snowstorm.css', js: 'snowstorm.js', container: 'snowstorm-container' }, snowstorm: { css: 'snowstorm.css', js: 'snowstorm.js', container: 'snowstorm-container' },
fireworks: { css: 'fireworks.css', js: 'fireworks.js', container: 'fireworks' }, fireworks: { css: 'fireworks.css', js: 'fireworks.js', container: 'fireworks' },
halloween: { css: 'halloween.css', js: 'halloween.js', container: 'halloween-container' }, 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' }, hearts: { css: 'hearts.css', js: 'hearts.js', container: 'hearts-container' },
christmas: { css: 'christmas.css', js: 'christmas.js', container: 'christmas-container' }, christmas: { css: 'christmas.css', js: 'christmas.js', container: 'christmas-container' },
santa: { css: 'santa.css', js: 'santa.js', container: 'santa-container' }, santa: { css: 'santa.css', js: 'santa.js', container: 'santa-container' },
@@ -399,7 +399,7 @@
// Remove any theme-created containers on body // Remove any theme-created containers on body
const knownContainers = [ const knownContainers = [
'.snowfall-container', '.snowflakes', '.snowstorm-container', '.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', '.christmas-container', '.santa-container', '.autumn-container',
'.easter-container', '.resurrection-container', '.spring-container', '.easter-container', '.resurrection-container', '.spring-container',
'.summer-container', '.carnival-container', '.cherryblossom-container', '.summer-container', '.carnival-container', '.cherryblossom-container',