diff --git a/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs index 9c3993b..690e04d 100644 --- a/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.Seasonals/Configuration/PluginConfiguration.cs @@ -32,6 +32,12 @@ public class PluginConfiguration : BasePluginConfiguration Summer = new SummerOptions(); CherryBlossom = new CherryBlossomOptions(); Carnival = new CarnivalOptions(); + PiDay = new PiDayOptions(); + Eurovision = new EurovisionOptions(); + Storm = new StormOptions(); + Pride = new PrideOptions(); + EarthDay = new EarthDayOptions(); + Rain = new RainOptions(); } /// @@ -57,7 +63,7 @@ public class PluginConfiguration : BasePluginConfiguration /// /// Gets or sets the seasonal rules configuration as JSON. /// - public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"}]"; + public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"},{\"Name\":\"Cherry Blossom\",\"StartDay\":1,\"StartMonth\":4,\"EndDay\":30,\"EndMonth\":4,\"Theme\":\"cherryblossom\"}]"; /// /// Gets or sets the Seasonals options. @@ -77,6 +83,12 @@ public class PluginConfiguration : BasePluginConfiguration public SummerOptions Summer { get; set; } public CherryBlossomOptions CherryBlossom { get; set; } public CarnivalOptions Carnival { get; set; } + public PiDayOptions PiDay { get; set; } + public EurovisionOptions Eurovision { get; set; } + public StormOptions Storm { get; set; } + public PrideOptions Pride { get; set; } + public EarthDayOptions EarthDay { get; set; } + public RainOptions Rain { get; set; } } public class AutumnOptions @@ -234,3 +246,55 @@ public class CherryBlossomOptions public bool EnableRandomCherryBlossomMobile { get; set; } = false; public bool EnableDifferentDuration { get; set; } = true; } + +public class PiDayOptions +{ + public int SymbolCount { get; set; } = 50; + public bool EnablePiDay { get; set; } = true; + public bool EnableRandomPiDay { get; set; } = true; + public bool EnableRandomPiDayMobile { get; set; } = false; + public bool EnableDifferentDuration { get; set; } = true; +} + +public class EurovisionOptions +{ + public int SymbolCount { get; set; } = 25; + public bool EnableEurovision { get; set; } = true; + public bool EnableRandomEurovision { get; set; } = true; + public bool EnableRandomEurovisionMobile { get; set; } = false; + public bool EnableDifferentDuration { get; set; } = true; + public bool EnableColorfulNotes { get; set; } = true; + public string EurovisionColors { get; set; } = "#ff0026ff,#17a6ffff,#32d432ff,#FFD700,#f0821bff,#f826f8ff"; + public int EurovisionGlowSize { get; set; } = 8; +} + +public class StormOptions +{ + public int RaindropCount { get; set; } = 300; + public int RaindropCountMobile { get; set; } = 150; + public bool EnableStorm { get; set; } = true; + public bool EnableLightning { get; set; } = true; + public double RainSpeed { get; set; } = 1; +} + +public class PrideOptions +{ + public bool EnablePride { get; set; } = true; + public int HeartCount { get; set; } = 20; + public int HeartSize { get; set; } = 2; + public bool ColorHeader { get; set; } = true; +} + +public class EarthDayOptions +{ + public bool EnableEarthDay { get; set; } = true; + public int VineCount { get; set; } = 4; +} + +public class RainOptions +{ + public bool EnableRain { get; set; } = true; + public int RaindropCount { get; set; } = 300; + public int RaindropCountMobile { get; set; } = 150; + public double RainSpeed { get; set; } = 1; +} diff --git a/Jellyfin.Plugin.Seasonals/Configuration/configPage.html b/Jellyfin.Plugin.Seasonals/Configuration/configPage.html index 14b4d3f..da24584 100644 --- a/Jellyfin.Plugin.Seasonals/Configuration/configPage.html +++ b/Jellyfin.Plugin.Seasonals/Configuration/configPage.html @@ -73,10 +73,14 @@ - - - - + + + + + + + +
The season to display if automation is disabled or no "Auto Selection" rule matches the current date.
@@ -775,6 +779,211 @@
Randomize the falling speed of cherry blossoms.
+
+ +
+ Earth Day +
+ +
Enable the Earth Day theme in general (e.g. for automation).
+
+
+ + +
Number of animated vines (if enabled).
+
+
+
+ +
+ Eurovision / Musik +
+ +
Enable the Eurovision/Music theme in general (e.g. for automation).
+
+
+ +
Displays dancing music notes.
+
+
+ +
Displays dancing music notes on mobile devices. Warning: High values may affect performance.
+
+
+ + +
Number of additional dancing music notes (if enabled).
+
+
+ +
Randomize the movement speed of music notes.
+
+
+ +
If checked, notes will pick colors from the array below. If unchecked, notes will be white.
+
+
+ + +
Example: #FFB6C1,#87CEFA,#98FB98 (Hex or CSS colors separated by commas).
+
+
+ + +
Set the text-shadow size of the notes. Set this to 0 to remove the shadow/glow completely.
+
+
+
+ +
+ Pi-Day +
+ +
Enable the Pi-Day theme in general (e.g. for automation).
+
+
+ +
Displays additional digital rain elements.
+
+
+ +
Displays additional digital rain elements on mobile devices. Warning: High values may affect performance.
+
+
+ + +
Number of additional digital rain columns (if enabled).
+
+
+ +
Randomize the digital rain falling speed.
+
+
+
+ +
+ Pride +
+ +
Enable the Pride theme in general (e.g. for automation).
+
+
+ + +
Number of rising rainbow hearts.
+
+
+ + +
Base size of the Pride hearts (default 2).
+
+
+ + +
Number of falling rainbow confetti pieces.
+
+
+ +
Color the top navigation bar with a rainbow gradient.
+
+
+
+ +
+ Rain (Pure) +
+ +
Enable the pure Rain theme.
+
+
+ + +
Total number of raindrops.
+
+
+ + +
Total number of raindrops on mobile devices.
+
+
+ + +
The speed of the falling rain.
+
+
+
+ +
+ Storm +
+ +
Enable the Storm theme in general (e.g. for automation).
+
+
+ + +
Total number of raindrops in the storm.
+
+
+ + +
Total number of raindrops on mobile devices. Warning: High values may affect performance.
+
+
+ + +
The speed of the falling rain.
+
+
+ +
Periodically flash the screen white to simulate lightning.
+
+
@@ -957,6 +1166,12 @@ ' ' + ' ' + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + ' ' + ' ' + '
' + @@ -1166,6 +1381,46 @@ document.querySelector('#EnableRandomCherryBlossomMobile').checked = config.CherryBlossom.EnableRandomCherryBlossomMobile; document.querySelector('#EnableDifferentDurationCherryBlossom').checked = config.CherryBlossom.EnableDifferentDuration; + // Earth Day + document.querySelector('#EnableEarthDay').checked = config.EarthDay.EnableEarthDay; + document.querySelector('#EarthDayVineCount').value = config.EarthDay.VineCount; + + // Eurovision + document.querySelector('#EnableEurovision').checked = config.Eurovision.EnableEurovision; + document.querySelector('#EurovisionSymbolCount').value = config.Eurovision.SymbolCount; + document.querySelector('#EnableRandomEurovision').checked = config.Eurovision.EnableRandomEurovision; + document.querySelector('#EnableRandomEurovisionMobile').checked = config.Eurovision.EnableRandomEurovisionMobile; + document.querySelector('#EnableDifferentDurationEurovision').checked = config.Eurovision.EnableDifferentDuration; + document.querySelector('#EnableColorfulNotes').checked = config.Eurovision.EnableColorfulNotes; + document.querySelector('#EurovisionColors').value = config.Eurovision.EurovisionColors; + document.querySelector('#EurovisionGlowSize').value = config.Eurovision.EurovisionGlowSize; + + // Pi-Day + document.querySelector('#EnablePiDay').checked = config.PiDay.EnablePiDay; + document.querySelector('#PiDaySymbolCount').value = config.PiDay.SymbolCount; + document.querySelector('#EnableRandomPiDay').checked = config.PiDay.EnableRandomPiDay; + document.querySelector('#EnableRandomPiDayMobile').checked = config.PiDay.EnableRandomPiDayMobile; + document.querySelector('#EnableDifferentDurationPiDay').checked = config.PiDay.EnableDifferentDuration; + + // Pride + document.querySelector('#EnablePride').checked = config.Pride.EnablePride; + document.querySelector('#PrideHeartCount').value = config.Pride.HeartCount; + document.querySelector('#PrideHeartSize').value = config.Pride.HeartSize; + document.querySelector('#PrideColorHeader').checked = config.Pride.ColorHeader; + + // Rain + document.querySelector('#EnableRain').checked = config.Rain.EnableRain; + document.querySelector('#RaindropCount').value = config.Rain.RaindropCount; + document.querySelector('#RaindropCountMobile').value = config.Rain.RaindropCountMobile; + document.querySelector('#RainSpeed').value = config.Rain.RainSpeed; + + // Storm + document.querySelector('#EnableStorm').checked = config.Storm.EnableStorm; + document.querySelector('#StormRaindropCount').value = config.Storm.RaindropCount; + document.querySelector('#StormRaindropCountMobile').value = config.Storm.RaindropCountMobile; + document.querySelector('#StormRainSpeed').value = config.Storm.RainSpeed; + document.querySelector('#StormEnableLightning').checked = config.Storm.EnableLightning; + Dashboard.hideLoadingMsg(); }); }); @@ -1309,6 +1564,46 @@ config.CherryBlossom.EnableRandomCherryBlossomMobile = document.querySelector('#EnableRandomCherryBlossomMobile').checked; config.CherryBlossom.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationCherryBlossom').checked; + // Earth Day + config.EarthDay.EnableEarthDay = document.querySelector('#EnableEarthDay').checked; + config.EarthDay.VineCount = parseInt(document.querySelector('#EarthDayVineCount').value); + + // Eurovision + config.Eurovision.EnableEurovision = document.querySelector('#EnableEurovision').checked; + config.Eurovision.SymbolCount = parseInt(document.querySelector('#EurovisionSymbolCount').value); + config.Eurovision.EnableRandomEurovision = document.querySelector('#EnableRandomEurovision').checked; + config.Eurovision.EnableRandomEurovisionMobile = document.querySelector('#EnableRandomEurovisionMobile').checked; + config.Eurovision.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEurovision').checked; + config.Eurovision.EnableColorfulNotes = document.querySelector('#EnableColorfulNotes').checked; + config.Eurovision.EurovisionColors = document.querySelector('#EurovisionColors').value; + config.Eurovision.EurovisionGlowSize = parseInt(document.querySelector('#EurovisionGlowSize').value); + + // Pi-Day + config.PiDay.EnablePiDay = document.querySelector('#EnablePiDay').checked; + config.PiDay.SymbolCount = parseInt(document.querySelector('#PiDaySymbolCount').value); + config.PiDay.EnableRandomPiDay = document.querySelector('#EnableRandomPiDay').checked; + config.PiDay.EnableRandomPiDayMobile = document.querySelector('#EnableRandomPiDayMobile').checked; + config.PiDay.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationPiDay').checked; + + // Pride + config.Pride.EnablePride = document.querySelector('#EnablePride').checked; + config.Pride.HeartCount = parseInt(document.querySelector('#PrideHeartCount').value); + config.Pride.HeartSize = parseFloat(document.querySelector('#PrideHeartSize').value); + config.Pride.ColorHeader = document.querySelector('#PrideColorHeader').checked; + + // Rain + config.Rain.EnableRain = document.querySelector('#EnableRain').checked; + config.Rain.RaindropCount = parseInt(document.querySelector('#RaindropCount').value); + config.Rain.RaindropCountMobile = parseInt(document.querySelector('#RaindropCountMobile').value); + config.Rain.RainSpeed = parseFloat(document.querySelector('#RainSpeed').value); + + // Storm + config.Storm.EnableStorm = document.querySelector('#EnableStorm').checked; + config.Storm.RaindropCount = parseInt(document.querySelector('#StormRaindropCount').value); + config.Storm.RaindropCountMobile = parseInt(document.querySelector('#StormRaindropCountMobile').value); + config.Storm.RainSpeed = parseFloat(document.querySelector('#StormRainSpeed').value); + config.Storm.EnableLightning = document.querySelector('#StormEnableLightning').checked; + ApiClient.updatePluginConfiguration(SeasonalsConfigPage.pluginUniqueId, config).then(function (result) { Dashboard.processPluginConfigurationUpdateResult(result); }); diff --git a/Jellyfin.Plugin.Seasonals/Web/autumn.css b/Jellyfin.Plugin.Seasonals/Web/autumn.css index e4d8bdc..4626341 100644 --- a/Jellyfin.Plugin.Seasonals/Web/autumn.css +++ b/Jellyfin.Plugin.Seasonals/Web/autumn.css @@ -8,6 +8,7 @@ height: 100%; pointer-events: none; z-index: 10; + contain: layout paint; } .leaf { @@ -44,7 +45,7 @@ } 100% { - top: 100%; + top: 110%; } } @@ -54,7 +55,7 @@ } 100% { - top: 100%; + top: 110%; } } diff --git a/Jellyfin.Plugin.Seasonals/Web/carnival.css b/Jellyfin.Plugin.Seasonals/Web/carnival.css index 7abc34f..41e62ed 100644 --- a/Jellyfin.Plugin.Seasonals/Web/carnival.css +++ b/Jellyfin.Plugin.Seasonals/Web/carnival.css @@ -9,13 +9,14 @@ pointer-events: none; z-index: 10; perspective: 600px; + contain: layout paint; } .carnival-wrapper { position: fixed; z-index: 15; top: -20px; - will-change: top; + will-change: transform; animation-name: carnival-fall; animation-timing-function: linear; animation-iteration-count: 1; @@ -59,10 +60,10 @@ @keyframes carnival-fall { 0% { - top: -10%; + transform: translateY(0); } 100% { - top: 110%; + transform: translateY(120vh); } } diff --git a/Jellyfin.Plugin.Seasonals/Web/cherryblossom.css b/Jellyfin.Plugin.Seasonals/Web/cherryblossom.css index 1b2fe2f..8bea8e5 100644 --- a/Jellyfin.Plugin.Seasonals/Web/cherryblossom.css +++ b/Jellyfin.Plugin.Seasonals/Web/cherryblossom.css @@ -8,6 +8,7 @@ height: 100%; pointer-events: none; z-index: 1000; + contain: layout paint; } /* Petals */ @@ -45,7 +46,7 @@ @keyframes cherryblossom-fall { 0% { top: -10%; } - 100% { top: 100%; } + 100% { top: 110%; } } @keyframes cherryblossom-sway { diff --git a/Jellyfin.Plugin.Seasonals/Web/christmas.css b/Jellyfin.Plugin.Seasonals/Web/christmas.css index c54838c..747939a 100644 --- a/Jellyfin.Plugin.Seasonals/Web/christmas.css +++ b/Jellyfin.Plugin.Seasonals/Web/christmas.css @@ -8,6 +8,7 @@ height: 100%; pointer-events: none; z-index: 10; + contain: layout paint; } .christmas { @@ -37,7 +38,7 @@ } 100% { - top: 100%; + top: 110%; } } @@ -61,7 +62,7 @@ } 100% { - top: 100%; + top: 110%; } } diff --git a/Jellyfin.Plugin.Seasonals/Web/earthday.css b/Jellyfin.Plugin.Seasonals/Web/earthday.css new file mode 100644 index 0000000..6457d8a --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/earthday.css @@ -0,0 +1,35 @@ +.earthday-container { + position: fixed; + bottom: 0; + left: 0; + width: 100vw; + height: 15vh; + pointer-events: none; + z-index: 1000; + overflow: hidden; +} + +.earthday-meadow { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + transform-origin: bottom; + animation: grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards; +} + +@keyframes grow-meadow { + 0% { transform: translateY(100%); opacity: 0; } + 100% { transform: translateY(0); opacity: 0.95; } +} + +.earthday-sway { + transform-origin: bottom center; + animation: sway-grass 4s ease-in-out infinite alternate; +} + +@keyframes sway-grass { + 0% { transform: skewX(-2deg); } + 100% { transform: skewX(2deg); } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/earthday.js b/Jellyfin.Plugin.Seasonals/Web/earthday.js new file mode 100644 index 0000000..51bd0c9 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/earthday.js @@ -0,0 +1,126 @@ +// 1. Read Configuration +const config = window.SeasonalsPluginConfig?.EarthDay || {}; + +const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true; +const vineCount = config.VineCount || 4; + +let msgPrinted = false; + +// 2. Toggle Function +function toggleEarthDay() { + const container = document.querySelector('.earthday-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('EarthDay hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('EarthDay visible'); + msgPrinted = false; + } + } +} + +// 3. MutationObserver +const observer = new MutationObserver(toggleEarthDay); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +// 4. Element Creation +function createElements() { + const container = document.querySelector('.earthday-container') || document.createElement('div'); + + if (!document.querySelector('.earthday-container')) { + container.className = 'earthday-container'; + container.setAttribute('aria-hidden', 'true'); + document.body.appendChild(container); + } + + const w = window.innerWidth; + const hSVG = Math.floor(window.innerHeight * 0.15) || 100; // 15vh roughly + let paths = ''; + + // Generate Grass + for (let i = 0; i < 400; i++) { + const x = Math.random() * w; + const h = hSVG * 0.2 + Math.random() * (hSVG * 0.8); + const cY = hSVG - h; + const bend = x + (Math.random() * 40 - 20); + const color = Math.random() > 0.5 ? '#2E8B57' : '#3CB371'; + const width = 1 + Math.random() * 2; + paths += ``; + } + + // Generate Flowers + const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493']; + const flowerCount = Math.max(10, vineCount * 15); + for (let i = 0; i < flowerCount; i++) { + const x = 10 + Math.random() * (w - 20); + const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5); + const col = colors[Math.floor(Math.random() * colors.length)]; + + paths += ``; + + const r = 2 + Math.random() * 1.5; + paths += ``; + paths += ``; + paths += ``; + paths += ``; + paths += ``; + } + + const svgContent = ` + + + ${paths} + + + `; + + container.innerHTML = svgContent; +} + +// 5. Responsive Resize +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +const handleResize = debounce(() => { + const container = document.querySelector('.earthday-container'); + if (container) { + container.innerHTML = ''; + createElements(); + } +}, 250); + +window.addEventListener('resize', handleResize); + +// 6. Initialization +function initializeEarthDay() { + if (!enabled) return; + createElements(); + toggleEarthDay(); +} + +initializeEarthDay(); diff --git a/Jellyfin.Plugin.Seasonals/Web/easter.css b/Jellyfin.Plugin.Seasonals/Web/easter.css index cae9c15..f29ccf7 100644 --- a/Jellyfin.Plugin.Seasonals/Web/easter.css +++ b/Jellyfin.Plugin.Seasonals/Web/easter.css @@ -8,6 +8,7 @@ height: 100%; pointer-events: none; z-index: 10; + contain: layout paint; } .hopping-rabbit { @@ -58,7 +59,7 @@ } 100% { - top: 100%; + top: 110%; } } @@ -82,7 +83,7 @@ } 100% { - top: 100%; + top: 110%; } } diff --git a/Jellyfin.Plugin.Seasonals/Web/eurovision.css b/Jellyfin.Plugin.Seasonals/Web/eurovision.css new file mode 100644 index 0000000..54c795d --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/eurovision.css @@ -0,0 +1,43 @@ +.eurovision-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 1000; + overflow: hidden; + contain: layout paint; +} + +.music-note-wrapper { + position: absolute; + left: 0; + /* initial top will be set via JS */ + opacity: 0; + animation: move-right linear infinite; + will-change: transform, opacity; +} + +.music-note { + display: block; + font-size: 2rem; + color: rgba(255, 255, 255, 0.9); + text-shadow: 0 0 8px rgba(255, 255, 255, 0.6); + animation: sway ease-in-out infinite alternate; + will-change: transform; +} + +/* Horizontal scroll from left to right */ +@keyframes move-right { + 0% { transform: translateX(-10vw); opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { transform: translateX(110vw); opacity: 0; } +} + +/* Sine-wave style vertical bouncing for the note itself */ +@keyframes sway { + 0% { transform: translateY(-30px); } + 100% { transform: translateY(30px); } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/eurovision.js b/Jellyfin.Plugin.Seasonals/Web/eurovision.js new file mode 100644 index 0000000..2f258d0 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/eurovision.js @@ -0,0 +1,105 @@ +// 1. Read Configuration +const config = window.SeasonalsPluginConfig?.Eurovision || {}; + +const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true; +const elementCount = config.SymbolCount || 25; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; +const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true; +const eurovisionColorsStr = config.EurovisionColors || '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff'; +const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2; + +let msgPrinted = false; + +// 2. Toggle Function +function toggleEurovision() { + const container = document.querySelector('.eurovision-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('Eurovision hidden'); + msgPrinted = true; + } + } else { + container.style.display = 'block'; + if (msgPrinted) { + console.log('Eurovision visible'); + msgPrinted = false; + } + } +} + +// 3. MutationObserver +const observer = new MutationObserver(toggleEurovision); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +// 4. Element Creation +function createElements() { + const container = document.querySelector('.eurovision-container') || document.createElement('div'); + + if (!document.querySelector('.eurovision-container')) { + container.className = 'eurovision-container'; + container.setAttribute('aria-hidden', 'true'); + document.body.appendChild(container); + } + + const notesSymbols = ['♪', '♫', '♬', '♭', '♮', '♯', '𝄞', '𝄢']; + const pColors = eurovisionColorsStr.split(',').map(s => s.trim()).filter(s => s); + + for (let i = 0; i < elementCount; i++) { + const wrapper = document.createElement('div'); + wrapper.className = 'music-note-wrapper'; + + const note = document.createElement('span'); + note.className = 'music-note'; + note.textContent = notesSymbols[Math.floor(Math.random() * notesSymbols.length)]; + wrapper.appendChild(note); + + wrapper.style.top = `${Math.random() * 90}vh`; + + const minMoveDur = 10; + const maxMoveDur = 25; + const moveDur = enableDifferentDuration + ? minMoveDur + Math.random() * (maxMoveDur - minMoveDur) + : (minMoveDur + maxMoveDur) / 2; + wrapper.style.animationDuration = `${moveDur}s`; + wrapper.style.animationDelay = `${Math.random() * 15}s`; + + const minSwayDur = 1; + const maxSwayDur = 3; + const swayDur = minSwayDur + Math.random() * (maxSwayDur - minSwayDur); + note.style.animationDuration = `${swayDur}s`; + note.style.animationDelay = `${Math.random() * 2}s`; + + note.style.fontSize = `${Math.random() * 1.5 + 1.5}rem`; + + if (enableColorfulNotes && pColors.length > 0) { + note.style.color = pColors[Math.floor(Math.random() * pColors.length)]; + note.style.textShadow = `0 0 ${glowSize}px ${note.style.color}`; + } else { + note.style.color = `rgba(255, 255, 255, 0.9)`; + note.style.textShadow = `0 0 ${glowSize}px rgba(255, 255, 255, 0.6)`; + } + + container.appendChild(wrapper); + } +} + +// 5. Initialization +function initializeEurovision() { + if (!enabled) return; + createElements(); + toggleEurovision(); +} + +initializeEurovision(); diff --git a/Jellyfin.Plugin.Seasonals/Web/fireworks.css b/Jellyfin.Plugin.Seasonals/Web/fireworks.css index f84feab..1dc299b 100644 --- a/Jellyfin.Plugin.Seasonals/Web/fireworks.css +++ b/Jellyfin.Plugin.Seasonals/Web/fireworks.css @@ -7,6 +7,7 @@ height: 100%; pointer-events: none; z-index: 10; + contain: layout paint; } .rocket-trail { diff --git a/Jellyfin.Plugin.Seasonals/Web/halloween.css b/Jellyfin.Plugin.Seasonals/Web/halloween.css index 61736ab..a0f6ea7 100644 --- a/Jellyfin.Plugin.Seasonals/Web/halloween.css +++ b/Jellyfin.Plugin.Seasonals/Web/halloween.css @@ -8,6 +8,7 @@ height: 100%; pointer-events: none; z-index: 10; + contain: layout paint; } .halloween { @@ -34,11 +35,11 @@ @-webkit-keyframes halloween-fall { 0% { - bottom: -10% + bottom: -10%; } 100% { - bottom: 100% + bottom: 110%; } } @@ -58,11 +59,11 @@ @keyframes halloween-fall { 0% { - bottom: -10% + bottom: -10%; } 100% { - bottom: 100% + bottom: 110%; } } diff --git a/Jellyfin.Plugin.Seasonals/Web/hearts.css b/Jellyfin.Plugin.Seasonals/Web/hearts.css index 76062e5..5f61fda 100644 --- a/Jellyfin.Plugin.Seasonals/Web/hearts.css +++ b/Jellyfin.Plugin.Seasonals/Web/hearts.css @@ -8,6 +8,7 @@ height: 100%; pointer-events: none; z-index: 10; + contain: layout paint; } .heart { @@ -32,11 +33,11 @@ @-webkit-keyframes heart-fall { 0% { - bottom: -10% + bottom: -10%; } 100% { - bottom: 100% + bottom: 110%; } } @@ -56,11 +57,11 @@ @keyframes heart-fall { 0% { - bottom: -10% + bottom: -10%; } 100% { - bottom: 100% + bottom: 110%; } } diff --git a/Jellyfin.Plugin.Seasonals/Web/piday.css b/Jellyfin.Plugin.Seasonals/Web/piday.css new file mode 100644 index 0000000..3810f31 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/piday.css @@ -0,0 +1,11 @@ +.piday-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 1000; + overflow: hidden; + contain: layout paint; +} diff --git a/Jellyfin.Plugin.Seasonals/Web/piday.js b/Jellyfin.Plugin.Seasonals/Web/piday.js new file mode 100644 index 0000000..843703f --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/piday.js @@ -0,0 +1,165 @@ +// 1. Read Configuration +const config = window.SeasonalsPluginConfig?.PiDay || {}; + +const enabled = config.EnablePiDay !== undefined ? config.EnablePiDay : true; +const maxTrails = config.SymbolCount || 25; // Directly mapped, smaller default + +let msgPrinted = false; +let isHidden = false; + +// 2. Toggle Function +function togglePiDay() { + const container = document.querySelector('.piday-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) { + if (!isHidden) { + container.style.display = 'none'; + isHidden = true; + if (!msgPrinted) { + console.log('PiDay hidden'); + msgPrinted = true; + } + } + } else { + if (isHidden) { + container.style.display = 'block'; + isHidden = false; + if (msgPrinted) { + console.log('PiDay visible'); + msgPrinted = false; + } + } + } +} + +// 3. MutationObserver +const observer = new MutationObserver(togglePiDay); +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true +}); + +// 4. Element Creation +function createElements() { + const container = document.querySelector('.piday-container') || document.createElement('div'); + + if (!document.querySelector('.piday-container')) { + container.className = 'piday-container'; + container.setAttribute('aria-hidden', 'true'); + document.body.appendChild(container); + } + + const canvas = document.createElement('canvas'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + canvas.style.display = 'block'; + container.appendChild(canvas); + + const ctx = canvas.getContext('2d'); +// const chars = '0123456789πABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + const chars = '0123456789'.split(''); + const fontSize = 18; + + class Trail { + constructor() { + this.reset(); + this.y = Math.random() * -100; // Allow initial staggered start + } + reset() { + const cols = Math.floor(canvas.width / fontSize); + this.x = Math.floor(Math.random() * cols); + this.y = -Math.round(Math.random() * 20); + this.speed = 0.5 + Math.random() * 0.5; + this.len = 10 + Math.floor(Math.random() * 20); + this.chars = []; + for(let i=0; i oldY) { + this.chars.unshift(chars[Math.floor(Math.random() * chars.length)]); + this.chars.pop(); + } + + // Randomly mutate some characters (heads mutate faster) + for (let i = 0; i < this.len; i++) { + const chance = i < 3 ? 0.90 : 0.98; + if (Math.random() > chance) { + this.chars[i] = chars[Math.floor(Math.random() * chars.length)]; + } + } + if (this.y - this.len > Math.ceil(canvas.height / fontSize)) { + this.reset(); + } + } + draw(ctx) { + const headY = Math.floor(this.y); + for (let i = 0; i < this.len; i++) { + const charY = headY - i; + if (charY < 0 || charY * fontSize > canvas.height + fontSize) continue; + + const ratio = i / this.len; + const alpha = 1 - ratio; + + if (i === 0) { + ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; + ctx.shadowBlur = 8; + ctx.shadowColor = '#0F0'; + } else if (i === 1) { + ctx.fillStyle = `rgba(150, 255, 150, ${alpha})`; + ctx.shadowBlur = 4; + ctx.shadowColor = '#0F0'; + } else { + ctx.fillStyle = `rgba(0, 255, 0, ${alpha * 0.8})`; + ctx.shadowBlur = 0; + } + + ctx.fillText(this.chars[i], this.x * fontSize + fontSize/2, charY * fontSize); + } + } + } + + const trails = []; + for(let i=0; i