Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c10583601 | ||
|
|
20dcf08bda | ||
|
|
e4b3a132b1 | ||
|
|
63ec6d5e52 | ||
|
|
ec89f2d48d | ||
|
|
61b21de566 | ||
|
|
590f2c3606 | ||
|
|
fdadc00a0c | ||
|
|
2ab88fd5ac | ||
|
|
9a41c0a2ce | ||
|
|
816f58cf02 | ||
|
|
5be9a60eed | ||
|
|
133808105e | ||
|
|
c631aca44f | ||
|
|
241450d132 | ||
|
|
d50d71bde1 | ||
|
|
262dd98519 | ||
|
|
b45ec73a67 | ||
|
|
4e8a37540f | ||
|
|
cde5201991 | ||
|
|
b2420b8eb4 | ||
|
|
dacec7d03c | ||
|
|
65f8261fb7 | ||
|
|
78872e7f96 | ||
|
|
45c9a199c2 | ||
|
|
1df6fb37b1 | ||
|
|
82a1e8a178 | ||
|
|
22bf887d10 | ||
|
|
07600766cf |
207
Injector_new.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Jellyfin.Plugin.Seasonals.Helpers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the injection of the Seasonals script into the Jellyfin web interface.
|
||||
/// </summary>
|
||||
public class ScriptInjector
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<ScriptInjector> _logger;
|
||||
public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
|
||||
public const string Marker = "</body>";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScriptInjector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects the script tag into index.html if it's not already present.
|
||||
/// </summary>
|
||||
public void Inject()
|
||||
{
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath))
|
||||
{
|
||||
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
|
||||
RegisterFileTransformation();
|
||||
return;
|
||||
}
|
||||
|
||||
var indexPath = Path.Combine(webPath, "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
_logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
|
||||
RegisterFileTransformation();
|
||||
return;
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (!content.Contains(ScriptTag))
|
||||
{
|
||||
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
content = content.Insert(index, ScriptTag + Environment.NewLine);
|
||||
File.WriteAllText(indexPath, content);
|
||||
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Script already present in index.html. Or could not be injected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
|
||||
RegisterFileTransformation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
|
||||
RegisterFileTransformation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the script tag from index.html.
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
UnregisterFileTransformation();
|
||||
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var indexPath = Path.Combine(webPath, "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (content.Contains(ScriptTag))
|
||||
{
|
||||
content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
|
||||
File.WriteAllText(indexPath, content);
|
||||
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
|
||||
} else {
|
||||
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error removing Seasonals script.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the path to the Jellyfin web interface directory.
|
||||
/// </summary>
|
||||
/// <returns>The path to the web directory, or null if not found.</returns>
|
||||
private string? GetWebPath()
|
||||
{
|
||||
// Use reflection to access WebPath property to ensure compatibility across different Jellyfin versions
|
||||
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
|
||||
return prop?.GetValue(_appPaths) as string;
|
||||
}
|
||||
|
||||
private void RegisterFileTransformation()
|
||||
{
|
||||
_logger.LogInformation("Seasonals Fallback. Registering file transformations.");
|
||||
|
||||
List<JObject> payloads = new List<JObject>();
|
||||
|
||||
{
|
||||
JObject payload = new JObject();
|
||||
payload.Add("id", "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
payload.Add("fileNamePattern", "index.html");
|
||||
payload.Add("callbackAssembly", GetType().Assembly.FullName);
|
||||
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
|
||||
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
|
||||
|
||||
payloads.Add(payload);
|
||||
}
|
||||
|
||||
Assembly? fileTransformationAssembly =
|
||||
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
|
||||
x.FullName?.Contains(".FileTransformation") ?? false);
|
||||
|
||||
if (fileTransformationAssembly != null)
|
||||
{
|
||||
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||
|
||||
if (pluginInterfaceType != null)
|
||||
{
|
||||
foreach (JObject payload in payloads)
|
||||
{
|
||||
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
|
||||
}
|
||||
_logger.LogInformation("File transformations registered successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback injection skipped.");
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterFileTransformation()
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly? fileTransformationAssembly =
|
||||
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
|
||||
x.FullName?.Contains(".FileTransformation") ?? false);
|
||||
|
||||
if (fileTransformationAssembly != null)
|
||||
{
|
||||
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||
|
||||
if (pluginInterfaceType != null)
|
||||
{
|
||||
Guid id = Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
|
||||
_logger.LogInformation("File transformation unregistered successfully.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,14 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
Resurrection = new ResurrectionOptions();
|
||||
Spring = new SpringOptions();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,7 +63,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// <summary>
|
||||
/// Gets or sets the seasonal rules configuration as JSON.
|
||||
/// </summary>
|
||||
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\"}]";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Seasonals options.
|
||||
@@ -74,7 +81,14 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
public ResurrectionOptions Resurrection { get; set; }
|
||||
public SpringOptions Spring { get; set; }
|
||||
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
|
||||
@@ -191,11 +205,14 @@ public class ResurrectionOptions
|
||||
|
||||
public class SpringOptions
|
||||
{
|
||||
public int PetalCount { get; set; } = 25;
|
||||
public int PollenCount { get; set; } = 15;
|
||||
public int LadybugCount { get; set; } = 5;
|
||||
public int PollenCount { get; set; } = 30;
|
||||
public int SunbeamCount { get; set; } = 5;
|
||||
public int BirdCount { get; set; } = 4;
|
||||
public int ButterflyCount { get; set; } = 4;
|
||||
public int BeeCount { get; set; } = 2;
|
||||
public int LadybugCount { get; set; } = 2;
|
||||
public bool EnableSpring { get; set; } = true;
|
||||
public bool EnableSpringSunbeams { get; set; } = true;
|
||||
public bool EnableRandomSpring { get; set; } = true;
|
||||
public bool EnableRandomSpringMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
@@ -218,4 +235,66 @@ public class CarnivalOptions
|
||||
public bool EnableRandomCarnival { get; set; } = true;
|
||||
public bool EnableRandomCarnivalMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableCarnivalSway { get; set; } = true;
|
||||
}
|
||||
|
||||
public class CherryBlossomOptions
|
||||
{
|
||||
public int PetalCount { get; set; } = 25;
|
||||
public bool EnableCherryBlossom { get; set; } = true;
|
||||
public bool EnableRandomCherryBlossom { get; set; } = true;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -65,14 +65,22 @@
|
||||
<option value="santa">Santa (flying santa & snowfall)</option>
|
||||
<option value="autumn">Autumn (falling leaves)</option>
|
||||
<option value="easter">Easter</option>
|
||||
<option value="resurrection">Resurrection</option>
|
||||
<option value="summer">Summer (Bubbles)</option>
|
||||
<option value="spring">Spring</option>
|
||||
<option value="carnival">Carnival (Confetti)</option>
|
||||
<option value="championships" disabled>European/World Championships (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="patrick" disabled>St. Patrick's Day (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="pride" disabled>Pride (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="cherryblossom">Cherry Blossom</option>
|
||||
<option value="resurrection">Resurrection by Bioflash257</option>
|
||||
<option value="championships" disabled>European/World Championships (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
|
||||
<option value="patrick" disabled>St. Patrick's Day (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
|
||||
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
|
||||
<option value="earthday">Earth Day (Growing Vines)</option>
|
||||
<option value="eurovision">Eurovision (Dancing Notes)</option>
|
||||
<option value="oscar" disabled>Oscar Awards (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
|
||||
<option value="piday">Pi-Day (Matrix Rain)</option>
|
||||
<option value="pride">Pride (Rainbow Border)</option>
|
||||
<option value="rain">Rain (Pure Rain)</option>
|
||||
<option value="storm">Storm (Heavy Rain & Lightning (Epilepsy Warning!))</option>
|
||||
<option value="sugarfeast" disabled>Sugar Feast (Eid al-Fitr, Ramadan) (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
|
||||
</select>
|
||||
<div class="fieldDescription">The season to display if automation is disabled or no "Auto Selection" rule matches the current date.</div>
|
||||
</div>
|
||||
@@ -582,39 +590,29 @@
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableSpring" name="EnableSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Spring Seasonal</span>
|
||||
<span class="checkboxLabel">Enable Spring</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Spring theme in general (e.g. for automation).</div>
|
||||
<div class="fieldDescription">Enables the Spring theme (grass, pollen).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomSpring" name="EnableRandomSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Sakura Petals</span>
|
||||
<span>Enable Animation Assets</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional Sakura petals falling across the screen.</div>
|
||||
<div class="fieldDescription">Enables animated spring assets (birds, butterflies, bees, etc.).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomSpringMobile" name="EnableRandomSpringMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Sakura Petals on Mobile</span>
|
||||
<span>Enable Animation Assets on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional Sakura petals falling across the screen on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringPetalCount">Sakura Petal Count</label>
|
||||
<input is="emby-input" type="number" id="SpringPetalCount" name="SpringPetalCount" />
|
||||
<div class="fieldDescription">Number of additional Sakura petals (if enabled).</div>
|
||||
<div class="fieldDescription">Displays animated assets on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringPollenCount">Pollen Count</label>
|
||||
<input is="emby-input" type="number" id="SpringPollenCount" name="SpringPollenCount" />
|
||||
<div class="fieldDescription">Number of pollen particles (if enabled).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringLadybugCount">Ladybug Count</label>
|
||||
<input is="emby-input" type="number" id="SpringLadybugCount" name="SpringLadybugCount" />
|
||||
<div class="fieldDescription">Number of ladybugs.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringSunbeamCount">Sunbeam Count</label>
|
||||
<input is="emby-input" type="number" id="SpringSunbeamCount" name="SpringSunbeamCount" />
|
||||
@@ -622,10 +620,37 @@
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationSpring" name="EnableDifferentDurationSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Falling Speed</span>
|
||||
<input id="EnableSpringSunbeams" name="EnableSpringSunbeams" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Sunbeams</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the falling speed of petals.</div>
|
||||
<div class="fieldDescription">Display sunbeams at the top of the screen.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringBirdCount">Bird Count</label>
|
||||
<input is="emby-input" type="number" id="SpringBirdCount" name="SpringBirdCount" />
|
||||
<div class="fieldDescription">Number of birds flying across the screen.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringButterflyCount">Butterfly Count</label>
|
||||
<input is="emby-input" type="number" id="SpringButterflyCount" name="SpringButterflyCount" />
|
||||
<div class="fieldDescription">Number of butterflies.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringBeeCount">Bee Count</label>
|
||||
<input is="emby-input" type="number" id="SpringBeeCount" name="SpringBeeCount" />
|
||||
<div class="fieldDescription">Number of bees.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringLadybugCount">Ladybug Count</label>
|
||||
<input is="emby-input" type="number" id="SpringLadybugCount" name="SpringLadybugCount" />
|
||||
<div class="fieldDescription">Number of ladybugs walking along the bottom.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationSpring" name="EnableDifferentDurationSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Duration</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the animations duration.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
@@ -708,6 +733,256 @@
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the falling speed of confetti.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableCarnivalSway" name="EnableCarnivalSway" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Sway</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable sway animation for carnival confetti.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Cherry Blossom</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableCherryBlossom" name="EnableCherryBlossom" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Cherry Blossom Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Cherry Blossom theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomCherryBlossom" name="EnableRandomCherryBlossom" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Cherry Blossoms</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional cherry blossoms falling and fluttering across the screen.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomCherryBlossomMobile" name="EnableRandomCherryBlossomMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Cherry Blossoms on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional cherry blossoms falling and fluttering across the screen on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="CherryBlossomPetalCount">Petal Count</label>
|
||||
<input is="emby-input" type="number" id="CherryBlossomPetalCount" name="CherryBlossomPetalCount" />
|
||||
<div class="fieldDescription">Number of additional cherry blossoms (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationCherryBlossom" name="EnableDifferentDurationCherryBlossom" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Falling Speed</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the falling speed of cherry blossoms.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Earth Day</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableEarthDay" name="EnableEarthDay" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Earth Day Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Earth Day theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="EarthDayVineCount">Vine Count</label>
|
||||
<input is="emby-input" type="number" id="EarthDayVineCount" name="EarthDayVineCount" />
|
||||
<div class="fieldDescription">Number of animated vines (if enabled).</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Eurovision / Musik</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableEurovision" name="EnableEurovision" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Eurovision Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Eurovision/Music theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomEurovision" name="EnableRandomEurovision" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Music Notes</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays dancing music notes.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomEurovisionMobile" name="EnableRandomEurovisionMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Music Notes on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays dancing music notes on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="EurovisionSymbolCount">Symbol Count</label>
|
||||
<input is="emby-input" type="number" id="EurovisionSymbolCount" name="EurovisionSymbolCount" />
|
||||
<div class="fieldDescription">Number of additional dancing music notes (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationEurovision" name="EnableDifferentDurationEurovision" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Falling Speed</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the movement speed of music notes.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableColorfulNotes" name="EnableColorfulNotes" type="checkbox" is="emby-checkbox" />
|
||||
<span>Colorful Notes Mode</span>
|
||||
</label>
|
||||
<div class="fieldDescription">If checked, notes will pick colors from the array below. If unchecked, notes will be white.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="EurovisionColors">Color Array (Comma-separated)</label>
|
||||
<input is="emby-input" type="text" id="EurovisionColors" name="EurovisionColors" />
|
||||
<div class="fieldDescription">Example: #FFB6C1,#87CEFA,#98FB98 (Hex or CSS colors separated by commas).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="EurovisionGlowSize">Note Glow/Shadow Size (px)</label>
|
||||
<input is="emby-input" type="number" id="EurovisionGlowSize" name="EurovisionGlowSize" />
|
||||
<div class="fieldDescription">Set the text-shadow size of the notes. Set this to 0 to remove the shadow/glow completely.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Pi-Day</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnablePiDay" name="EnablePiDay" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Pi-Day Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Pi-Day theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomPiDay" name="EnableRandomPiDay" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Pi Symbols</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional digital rain elements.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomPiDayMobile" name="EnableRandomPiDayMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Pi Symbols on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional digital rain elements on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="PiDaySymbolCount">Symbol Count</label>
|
||||
<input is="emby-input" type="number" id="PiDaySymbolCount" name="PiDaySymbolCount" />
|
||||
<div class="fieldDescription">Number of additional digital rain columns (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationPiDay" name="EnableDifferentDurationPiDay" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Falling Speed</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the digital rain falling speed.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Pride</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnablePride" name="EnablePride" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Pride Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Pride theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="PrideHeartCount">Heart Count</label>
|
||||
<input is="emby-input" type="number" id="PrideHeartCount" name="PrideHeartCount" />
|
||||
<div class="fieldDescription">Number of rising rainbow hearts.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="PrideHeartSize">Heart Size (rem)</label>
|
||||
<input is="emby-input" type="number" id="PrideHeartSize" name="PrideHeartSize" step="0.1" />
|
||||
<div class="fieldDescription">Base size of the Pride hearts (default 2).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="PrideConfettiCount">Confetti Count</label>
|
||||
<input is="emby-input" type="number" id="PrideConfettiCount" name="PrideConfettiCount" />
|
||||
<div class="fieldDescription">Number of falling rainbow confetti pieces.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="PrideColorHeader" name="PrideColorHeader" type="checkbox" is="emby-checkbox" />
|
||||
<span>Rainbow Header</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Color the top navigation bar with a rainbow gradient.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Rain (Pure)</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRain" name="EnableRain" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Rain Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the pure Rain theme.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="RaindropCount">Raindrop Count</label>
|
||||
<input is="emby-input" type="number" id="RaindropCount" name="RaindropCount" />
|
||||
<div class="fieldDescription">Total number of raindrops.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="RaindropCountMobile">Raindrop Count (Mobile)</label>
|
||||
<input is="emby-input" type="number" id="RaindropCountMobile" name="RaindropCountMobile" />
|
||||
<div class="fieldDescription">Total number of raindrops on mobile devices.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="RainSpeed">Rain Speed</label>
|
||||
<input is="emby-input" type="number" id="RainSpeed" name="RainSpeed" step="0.1" />
|
||||
<div class="fieldDescription">The speed of the falling rain.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Storm</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableStorm" name="EnableStorm" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Storm Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Storm theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="StormRaindropCount">Raindrop Count</label>
|
||||
<input is="emby-input" type="number" id="StormRaindropCount" name="StormRaindropCount" />
|
||||
<div class="fieldDescription">Total number of raindrops in the storm.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="StormRaindropCountMobile">Raindrop Count (Mobile)</label>
|
||||
<input is="emby-input" type="number" id="StormRaindropCountMobile" name="StormRaindropCountMobile" />
|
||||
<div class="fieldDescription">Total number of raindrops on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="StormRainSpeed">Rain Speed</label>
|
||||
<input is="emby-input" type="number" id="StormRainSpeed" name="StormRainSpeed" step="0.1" />
|
||||
<div class="fieldDescription">The speed of the falling rain.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="StormEnableLightning" name="StormEnableLightning" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Lightning Flashes</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Periodically flash the screen white to simulate lightning.</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -884,13 +1159,20 @@
|
||||
' <option value="halloween">Halloween</option>' +
|
||||
' <option value="hearts">Hearts</option>' +
|
||||
' <option value="christmas">Christmas</option>' +
|
||||
' <option value="santa">Santa</option>' +
|
||||
' <option value="autumn">Autumn</option>' +
|
||||
' <option value="santa">Santa (flying santa & snowfall)</option>' +
|
||||
' <option value="autumn">Autumn (falling leaves)</option>' +
|
||||
' <option value="easter">Easter</option>' +
|
||||
' <option value="resurrection">Resurrection</option>' +
|
||||
' <option value="summer">Summer (Bubbles)</option>' +
|
||||
' <option value="spring">Spring</option>' +
|
||||
' <option value="summer">Summer</option>' +
|
||||
' <option value="carnival">Carnival</option>' +
|
||||
' <option value="carnival">Carnival (Confetti)</option>' +
|
||||
' <option value="cherryblossom">Cherry Blossom</option>' +
|
||||
' <option value="earthday">Earth Day</option>' +
|
||||
' <option value="eurovision">Eurovision</option>' +
|
||||
' <option value="piday">Pi-Day</option>' +
|
||||
' <option value="pride">Pride</option>' +
|
||||
' <option value="rain">Rain</option>' +
|
||||
' <option value="storm">Storm (Epilepsy Warning!)</option>' +
|
||||
' <option value="resurrection">Resurrection by Bioflash257</option>' +
|
||||
' </select>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
@@ -1065,10 +1347,13 @@
|
||||
|
||||
// Spring
|
||||
document.querySelector('#EnableSpring').checked = config.Spring.EnableSpring;
|
||||
document.querySelector('#SpringPetalCount').value = config.Spring.PetalCount;
|
||||
document.querySelector('#EnableSpringSunbeams').checked = config.Spring.EnableSpringSunbeams !== undefined ? config.Spring.EnableSpringSunbeams : true;
|
||||
document.querySelector('#SpringPollenCount').value = config.Spring.PollenCount;
|
||||
document.querySelector('#SpringLadybugCount').value = config.Spring.LadybugCount;
|
||||
document.querySelector('#SpringSunbeamCount').value = config.Spring.SunbeamCount;
|
||||
document.querySelector('#SpringBirdCount').value = config.Spring.BirdCount !== undefined ? config.Spring.BirdCount : 3;
|
||||
document.querySelector('#SpringButterflyCount').value = config.Spring.ButterflyCount !== undefined ? config.Spring.ButterflyCount : 2;
|
||||
document.querySelector('#SpringBeeCount').value = config.Spring.BeeCount !== undefined ? config.Spring.BeeCount : 1;
|
||||
document.querySelector('#SpringLadybugCount').value = config.Spring.LadybugCount !== undefined ? config.Spring.LadybugCount : 1;
|
||||
document.querySelector('#EnableRandomSpring').checked = config.Spring.EnableRandomSpring;
|
||||
document.querySelector('#EnableRandomSpringMobile').checked = config.Spring.EnableRandomSpringMobile;
|
||||
document.querySelector('#EnableDifferentDurationSpring').checked = config.Spring.EnableDifferentDuration;
|
||||
@@ -1083,11 +1368,59 @@
|
||||
|
||||
// Carnival
|
||||
document.querySelector('#EnableCarnival').checked = config.Carnival.EnableCarnival;
|
||||
document.querySelector('#EnableCarnivalSway').checked = config.Carnival.EnableCarnivalSway !== undefined ? config.Carnival.EnableCarnivalSway : true;
|
||||
document.querySelector('#CarnivalObjectCount').value = config.Carnival.ObjectCount;
|
||||
document.querySelector('#EnableRandomCarnival').checked = config.Carnival.EnableRandomCarnival;
|
||||
document.querySelector('#EnableRandomCarnivalMobile').checked = config.Carnival.EnableRandomCarnivalMobile;
|
||||
document.querySelector('#EnableDifferentDurationCarnival').checked = config.Carnival.EnableDifferentDuration;
|
||||
|
||||
// Cherry Blossom
|
||||
document.querySelector('#EnableCherryBlossom').checked = config.CherryBlossom.EnableCherryBlossom;
|
||||
document.querySelector('#CherryBlossomPetalCount').value = config.CherryBlossom.PetalCount;
|
||||
document.querySelector('#EnableRandomCherryBlossom').checked = config.CherryBlossom.EnableRandomCherryBlossom;
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1197,10 +1530,13 @@
|
||||
|
||||
// Spring
|
||||
config.Spring.EnableSpring = document.querySelector('#EnableSpring').checked;
|
||||
config.Spring.PetalCount = parseInt(document.querySelector('#SpringPetalCount').value);
|
||||
config.Spring.EnableSpringSunbeams = document.querySelector('#EnableSpringSunbeams').checked;
|
||||
config.Spring.PollenCount = parseInt(document.querySelector('#SpringPollenCount').value);
|
||||
config.Spring.LadybugCount = parseInt(document.querySelector('#SpringLadybugCount').value);
|
||||
config.Spring.SunbeamCount = parseInt(document.querySelector('#SpringSunbeamCount').value);
|
||||
config.Spring.BirdCount = parseInt(document.querySelector('#SpringBirdCount').value);
|
||||
config.Spring.ButterflyCount = parseInt(document.querySelector('#SpringButterflyCount').value);
|
||||
config.Spring.BeeCount = parseInt(document.querySelector('#SpringBeeCount').value);
|
||||
config.Spring.LadybugCount = parseInt(document.querySelector('#SpringLadybugCount').value);
|
||||
config.Spring.EnableRandomSpring = document.querySelector('#EnableRandomSpring').checked;
|
||||
config.Spring.EnableRandomSpringMobile = document.querySelector('#EnableRandomSpringMobile').checked;
|
||||
config.Spring.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSpring').checked;
|
||||
@@ -1215,11 +1551,59 @@
|
||||
|
||||
// Carnival
|
||||
config.Carnival.EnableCarnival = document.querySelector('#EnableCarnival').checked;
|
||||
config.Carnival.EnableCarnivalSway = document.querySelector('#EnableCarnivalSway').checked;
|
||||
config.Carnival.ObjectCount = parseInt(document.querySelector('#CarnivalObjectCount').value);
|
||||
config.Carnival.EnableRandomCarnival = document.querySelector('#EnableRandomCarnival').checked;
|
||||
config.Carnival.EnableRandomCarnivalMobile = document.querySelector('#EnableRandomCarnivalMobile').checked;
|
||||
config.Carnival.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationCarnival').checked;
|
||||
|
||||
// Cherry Blossom
|
||||
config.CherryBlossom.EnableCherryBlossom = document.querySelector('#EnableCherryBlossom').checked;
|
||||
config.CherryBlossom.PetalCount = parseInt(document.querySelector('#CherryBlossomPetalCount').value);
|
||||
config.CherryBlossom.EnableRandomCherryBlossom = document.querySelector('#EnableRandomCherryBlossom').checked;
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||
<Title>Jellyfin Seasonals Plugin</Title>
|
||||
<Authors>CodeDevMLH</Authors>
|
||||
<Version>1.7.1.0</Version>
|
||||
<Version>1.7.2.0</Version>
|
||||
<RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -56,6 +56,18 @@ public class ScriptInjector
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
|
||||
|
||||
// MARK: Legacy Tags, remove in future versions
|
||||
bool modified = false;
|
||||
// Cleanup legacy tags first to avoid duplicates or conflicts
|
||||
content = RemoveLegacyTags(content, ref modified);
|
||||
if (modified)
|
||||
{
|
||||
_logger.LogInformation("Removed legacy tags from index.html.");
|
||||
}
|
||||
|
||||
|
||||
if (!content.Contains(ScriptTag))
|
||||
{
|
||||
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -113,6 +125,17 @@ public class ScriptInjector
|
||||
} else {
|
||||
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
||||
}
|
||||
|
||||
// MARK: Legacy Tags, remove in future versions
|
||||
// Remove legacy tags
|
||||
bool modified = false;
|
||||
content = RemoveLegacyTags(content, ref modified);
|
||||
if (modified)
|
||||
{
|
||||
_logger.LogInformation("Removed legacy tags from index.html.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
@@ -204,4 +227,21 @@ public class ScriptInjector
|
||||
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Legacy Tags, remove in future versions
|
||||
/// <summary>
|
||||
/// Removes legacy script tags from the content.
|
||||
/// </summary>
|
||||
private string RemoveLegacyTags(string content, ref bool modified)
|
||||
{
|
||||
// Legacy tags (used in versions prior to 1.6.3.0 where paths started with / instead of ../)
|
||||
const string LegacyScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
|
||||
|
||||
if (content.Contains(LegacyScriptTag))
|
||||
{
|
||||
content = content.Replace(LegacyScriptTag + Environment.NewLine, "").Replace(LegacyScriptTag, "");
|
||||
modified = true;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,18 @@
|
||||
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: infinite;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.carnival-sway-wrapper {
|
||||
@@ -35,9 +37,8 @@
|
||||
background-color: #f0f;
|
||||
will-change: transform;
|
||||
animation-name: carnival-flutter;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: 2s;
|
||||
}
|
||||
|
||||
.carnival-confetti.circle {
|
||||
@@ -52,24 +53,17 @@
|
||||
}
|
||||
|
||||
.carnival-confetti.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent !important;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 10px solid;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: inherit;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes carnival-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
top: 110%;
|
||||
transform: translateY(120vh);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,18 +78,9 @@
|
||||
|
||||
@keyframes carnival-flutter {
|
||||
0% {
|
||||
transform: rotate3d(1, 1, 1, 0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate3d(1, 0.5, 0, 90deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate3d(0.5, 1, 0.5, 180deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate3d(0, 0.5, 1, 270deg);
|
||||
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(1, 1, 1, 360deg);
|
||||
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const config = window.SeasonalsPluginConfig?.Carnival || {};
|
||||
|
||||
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true;
|
||||
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true;
|
||||
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true;
|
||||
const carnivalCount = config.ObjectCount || 80;
|
||||
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // Enable/disable carnival
|
||||
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true; // Enable random carnival objects
|
||||
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false; // Enable random carnival objects on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // Randomize falling and flutter speeds
|
||||
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // Enable side-to-side sway animation
|
||||
const carnivalCount = config.ObjectCount || 120; // Number of confetti pieces to spawn
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
@@ -86,14 +86,21 @@ function createConfettiPiece(container, isInitial = false) {
|
||||
wrapper.style.left = `${Math.random() * 100}%`;
|
||||
|
||||
// Random dimensions
|
||||
// MARK: CONFETTI SIZE (RECTANGLES)
|
||||
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
|
||||
const width = Math.random() * 5 + 4;
|
||||
const height = Math.random() * 6 + 8;
|
||||
const width = Math.random() * 3 + 4; // 4-7px
|
||||
const height = Math.random() * 5 + 8; // 8-13px
|
||||
confetti.style.width = `${width}px`;
|
||||
confetti.style.height = `${height}px`;
|
||||
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
|
||||
// MARK: CONFETTI SIZE (CIRCLES/SQUARES)
|
||||
const size = Math.random() * 5 + 5; // 5-10px
|
||||
confetti.style.width = `${size}px`;
|
||||
confetti.style.height = `${size}px`;
|
||||
}
|
||||
|
||||
// Animation settings
|
||||
// MARK: CONFETTI FALLING SPEED (in seconds)
|
||||
const duration = Math.random() * 5 + 5;
|
||||
|
||||
let delay = 0;
|
||||
@@ -113,14 +120,22 @@ function createConfettiPiece(container, isInitial = false) {
|
||||
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
|
||||
// Random sway amplitude (using CSS variable for dynamic keyframe)
|
||||
// Sway between 30px and 100px
|
||||
const swayAmount = Math.random() * 70 + 30;
|
||||
// MARK: SWAY DISTANCE RANGE (in px)
|
||||
const swayAmount = Math.random() * 70 + 30; // 30-100px
|
||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||
}
|
||||
|
||||
// Flutter speed variation
|
||||
// Flutter speed and random 3D rotation axis
|
||||
// MARK: CONFETTI FLUTTER ROTATION SPEED
|
||||
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
|
||||
confetti.style.setProperty('--rx', Math.random().toFixed(2));
|
||||
confetti.style.setProperty('--ry', Math.random().toFixed(2));
|
||||
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||
|
||||
// Random direction for 3D rotation
|
||||
const rotDir = Math.random() > 0.5 ? 1 : -1;
|
||||
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
|
||||
|
||||
if (enableSway) {
|
||||
swayWrapper.appendChild(confetti);
|
||||
@@ -129,6 +144,14 @@ function createConfettiPiece(container, isInitial = false) {
|
||||
wrapper.appendChild(confetti);
|
||||
}
|
||||
|
||||
// Respawn confetti when it hits the bottom
|
||||
wrapper.addEventListener('animationend', (e) => {
|
||||
if (e.animationName === 'carnival-fall') {
|
||||
wrapper.remove();
|
||||
createConfettiPiece(container, false); // respawn without initial huge delay
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
@@ -154,7 +177,7 @@ function initCarnivalObjects() {
|
||||
}
|
||||
|
||||
// Initial confetti
|
||||
for (let i = 0; i < 30; i++) {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
createConfettiPiece(container, true);
|
||||
}
|
||||
}
|
||||
|
||||
59
Jellyfin.Plugin.Seasonals/Web/cherryblossom.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.cherryblossom-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
/* Petals */
|
||||
.cherryblossom-petal {
|
||||
position: fixed;
|
||||
top: -20px;
|
||||
z-index: 1005;
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
background-color: #ffc0cb;
|
||||
border-radius: 15px 0px 15px 0px;
|
||||
|
||||
will-change: transform, top;
|
||||
animation-name: cherryblossom-fall, cherryblossom-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-duration: 10s, 3s;
|
||||
}
|
||||
|
||||
.cherryblossom-petal.lighter {
|
||||
background-color: #ffd1dc;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cherryblossom-petal.darker {
|
||||
background-color: #ffb7c5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cherryblossom-petal.type2 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 10px 0px 10px 5px;
|
||||
}
|
||||
|
||||
@keyframes cherryblossom-fall {
|
||||
0% { top: -10%; }
|
||||
100% { top: 110%; }
|
||||
}
|
||||
|
||||
@keyframes cherryblossom-sway {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(30px) rotate(45deg);
|
||||
}
|
||||
}
|
||||
101
Jellyfin.Plugin.Seasonals/Web/cherryblossom.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
|
||||
|
||||
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true;
|
||||
const petalCount = config.PetalCount || 25;
|
||||
const randomCherryBlossom = config.EnableRandomCherryBlossom !== undefined ? config.EnableRandomCherryBlossom : true;
|
||||
const randomCherryBlossomMobile = config.EnableRandomCherryBlossomMobile !== undefined ? config.EnableRandomCherryBlossomMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleCherryBlossom() {
|
||||
const container = document.querySelector('.cherryblossom-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('CherryBlossom hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('CherryBlossom visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleCherryBlossom);
|
||||
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
||||
|
||||
function createPetal(container) {
|
||||
const petal = document.createElement('div');
|
||||
petal.classList.add('cherryblossom-petal');
|
||||
|
||||
const type = Math.random() > 0.5 ? 'type1' : 'type2';
|
||||
petal.classList.add(type);
|
||||
|
||||
const color = Math.random() > 0.7 ? 'darker' : 'lighter';
|
||||
petal.classList.add(color);
|
||||
|
||||
const randomLeft = Math.random() * 100;
|
||||
petal.style.left = `${randomLeft}%`;
|
||||
|
||||
const size = Math.random() * 0.5 + 0.5;
|
||||
petal.style.transform = `scale(${size})`;
|
||||
|
||||
const duration = Math.random() * 5 + 8;
|
||||
const delay = Math.random() * 10;
|
||||
const swayDuration = Math.random() * 2 + 2;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
|
||||
}
|
||||
petal.style.animationDelay = `${delay}s, ${Math.random() * 3}s`;
|
||||
|
||||
container.appendChild(petal);
|
||||
}
|
||||
|
||||
function addRandomObjects() {
|
||||
const container = document.querySelector('.cherryblossom-container');
|
||||
if (!container) return;
|
||||
|
||||
for (let i = 0; i < petalCount; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initObjects() {
|
||||
let container = document.querySelector('.cherryblossom-container');
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "cherryblossom-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Initial batch
|
||||
for (let i = 0; i < 15; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCherryBlossom() {
|
||||
if (!cherryBlossom) return;
|
||||
initObjects();
|
||||
toggleCherryBlossom();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomCherryBlossom && (screenWidth > 768 || randomCherryBlossomMobile)) {
|
||||
addRandomObjects();
|
||||
}
|
||||
}
|
||||
|
||||
initializeCherryBlossom();
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
Jellyfin.Plugin.Seasonals/Web/earthday.css
Normal file
@@ -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); }
|
||||
}
|
||||
126
Jellyfin.Plugin.Seasonals/Web/earthday.js
Normal file
@@ -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 += `<path d="M ${x} ${hSVG} Q ${bend} ${cY+hSVG*0.2} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
|
||||
}
|
||||
|
||||
// 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 += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#006400" stroke-width="1.5" fill="none"/>`;
|
||||
|
||||
const r = 2 + Math.random() * 1.5;
|
||||
paths += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||
paths += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||
paths += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||
paths += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||
paths += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
|
||||
}
|
||||
|
||||
const svgContent = `
|
||||
<svg class="earthday-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="earthday-sway">
|
||||
${paths}
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
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();
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
Jellyfin.Plugin.Seasonals/Web/eurovision.css
Normal file
@@ -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); }
|
||||
}
|
||||
105
Jellyfin.Plugin.Seasonals/Web/eurovision.js
Normal file
@@ -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();
|
||||
@@ -7,6 +7,7 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.rocket-trail {
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
Jellyfin.Plugin.Seasonals/Web/piday.css
Normal file
@@ -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;
|
||||
}
|
||||
165
Jellyfin.Plugin.Seasonals/Web/piday.js
Normal file
@@ -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<this.len; i++) {
|
||||
this.chars.push(chars[Math.floor(Math.random() * chars.length)]);
|
||||
}
|
||||
}
|
||||
update() {
|
||||
const oldY = Math.floor(this.y);
|
||||
this.y += this.speed;
|
||||
const newY = Math.floor(this.y);
|
||||
|
||||
// If crossed a full vertical unit, push a new character and pop the old one to preserve screen positions
|
||||
if (newY > 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<maxTrails; i++) trails.push(new Trail());
|
||||
|
||||
function loop() {
|
||||
if (isHidden) return; // Pause drawing when hidden
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = 'bold ' + fontSize + 'px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
for (const t of trails) {
|
||||
t.update();
|
||||
t.draw(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.pidayInterval) clearInterval(window.pidayInterval);
|
||||
window.pidayInterval = setInterval(loop, 50);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Initialization
|
||||
function initializePiDay() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
togglePiDay();
|
||||
}
|
||||
|
||||
initializePiDay();
|
||||
32
Jellyfin.Plugin.Seasonals/Web/pride.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.pride-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
|
||||
.pride-heart {
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
animation: pride-rise ease-in infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
@keyframes pride-rise {
|
||||
0% { transform: translateY(0) scale(0.8); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(-115vh) scale(1.2); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Coloring the Jellyfin Header */
|
||||
.skinHeader.pride-header {
|
||||
background: linear-gradient(90deg, #E40303, #FF8C00, #FFED00, #008026, #24408E, #732982) !important;
|
||||
}
|
||||
88
Jellyfin.Plugin.Seasonals/Web/pride.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// 1. Read Configuration
|
||||
const config = window.SeasonalsPluginConfig?.Pride || {};
|
||||
|
||||
const enabled = config.EnablePride !== undefined ? config.EnablePride : true;
|
||||
const elementCount = config.HeartCount || 20;
|
||||
const heartSize = config.HeartSize || 1.5;
|
||||
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// 2. Toggle Function
|
||||
// Hides the effect when a video player, trailer (in full width mode), dashboard, or user menu is active.
|
||||
function togglePride() {
|
||||
const container = document.querySelector('.pride-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('Pride hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Pride visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. MutationObserver
|
||||
// Watches the DOM for changes so the effect can auto-hide/show.
|
||||
const observer = new MutationObserver(togglePride);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// 4. Element Creation
|
||||
// Create and append your animated elements to the container.
|
||||
function createElements() {
|
||||
const container = document.querySelector('.pride-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.pride-container')) {
|
||||
container.className = 'pride-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
if (colorHeader) {
|
||||
const header = document.querySelector('.skinHeader');
|
||||
if (header) {
|
||||
header.classList.add('pride-header');
|
||||
}
|
||||
}
|
||||
|
||||
const heartEmojis = ['❤️', '🧡', '💛', '💚', '💙', '💜'];
|
||||
|
||||
for (let i = 0; i < elementCount; i++) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'pride-heart';
|
||||
|
||||
el.innerText = heartEmojis[Math.floor(Math.random() * heartEmojis.length)];
|
||||
el.style.fontSize = `${heartSize}rem`;
|
||||
el.style.left = `${Math.random() * 100}vw`;
|
||||
el.style.animationDuration = `${5 + Math.random() * 5}s`;
|
||||
el.style.animationDelay = `${Math.random() * 5}s`;
|
||||
el.style.marginLeft = `${(Math.random() - 0.5) * 100}px`;
|
||||
|
||||
container.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Initialization
|
||||
function initializePride() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
togglePride();
|
||||
}
|
||||
|
||||
initializePride();
|
||||
26
Jellyfin.Plugin.Seasonals/Web/rain.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.rain-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.raindrop-pure {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
@keyframes pure-rain {
|
||||
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
|
||||
5% { opacity: 1; }
|
||||
95% { opacity: 1; }
|
||||
100% { transform: translateY(110vh) translateX(-40vh) rotate(20deg); opacity: 0; }
|
||||
}
|
||||
77
Jellyfin.Plugin.Seasonals/Web/rain.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// 1. Read Configuration
|
||||
const config = window.SeasonalsPluginConfig?.Rain || {};
|
||||
|
||||
const enabled = config.EnableRain !== undefined ? config.EnableRain : true;
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
|
||||
const rainSpeed = config.RainSpeed || 1.0;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// 2. Toggle Function
|
||||
function toggleRain() {
|
||||
const container = document.querySelector('.rain-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('Rain hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Rain visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. MutationObserver
|
||||
const observer = new MutationObserver(toggleRain);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// 4. Element Creation
|
||||
function createElements() {
|
||||
const container = document.querySelector('.rain-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.rain-container')) {
|
||||
container.className = 'rain-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < elementCount; i++) {
|
||||
const drop = document.createElement('div');
|
||||
drop.className = 'raindrop-pure';
|
||||
|
||||
drop.style.left = `${Math.random() * 140}vw`;
|
||||
drop.style.top = `${-20 - Math.random() * 50}vh`;
|
||||
|
||||
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
|
||||
drop.style.animation = `pure-rain ${duration}s linear infinite`;
|
||||
drop.style.animationDelay = `${Math.random() * 2}s`;
|
||||
drop.style.opacity = Math.random() * 0.5 + 0.3;
|
||||
|
||||
container.appendChild(drop);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Initialization
|
||||
function initializeRain() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleRain();
|
||||
}
|
||||
|
||||
initializeRain();
|
||||
@@ -8,6 +8,7 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.resurrection-symbol {
|
||||
@@ -43,7 +44,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 105%;
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,41 @@ const ThemeConfigs = {
|
||||
js: '../Seasonals/Resources/carnival.js',
|
||||
containerClass: 'carnival-container'
|
||||
},
|
||||
cherryblossom: {
|
||||
css: '../Seasonals/Resources/cherryblossom.css',
|
||||
js: '../Seasonals/Resources/cherryblossom.js',
|
||||
containerClass: 'cherryblossom-container'
|
||||
},
|
||||
piday: {
|
||||
css: '../Seasonals/Resources/piday.css',
|
||||
js: '../Seasonals/Resources/piday.js',
|
||||
containerClass: 'piday-container'
|
||||
},
|
||||
eurovision: {
|
||||
css: '../Seasonals/Resources/eurovision.css',
|
||||
js: '../Seasonals/Resources/eurovision.js',
|
||||
containerClass: 'eurovision-container'
|
||||
},
|
||||
storm: {
|
||||
css: '../Seasonals/Resources/storm.css',
|
||||
js: '../Seasonals/Resources/storm.js',
|
||||
containerClass: 'storm-container'
|
||||
},
|
||||
pride: {
|
||||
css: '../Seasonals/Resources/pride.css',
|
||||
js: '../Seasonals/Resources/pride.js',
|
||||
containerClass: 'pride-container'
|
||||
},
|
||||
rain: {
|
||||
css: '../Seasonals/Resources/rain.css',
|
||||
js: '../Seasonals/Resources/rain.js',
|
||||
containerClass: 'rain-container'
|
||||
},
|
||||
earthday: {
|
||||
css: '../Seasonals/Resources/earthday.css',
|
||||
js: '../Seasonals/Resources/earthday.js',
|
||||
containerClass: 'earthday-container'
|
||||
},
|
||||
none: {
|
||||
containerClass: 'none'
|
||||
},
|
||||
@@ -246,6 +281,12 @@ const SeasonalsManager = {
|
||||
if (response.ok) {
|
||||
this.config = await response.json();
|
||||
window.SeasonalsPluginConfig = this.config;
|
||||
|
||||
if (this.config.IsEnabled === false) {
|
||||
console.log('Seasonals: Plugin is disabled globally.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Seasonals: Seasonals Config loaded:', this.config);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
@@ -37,7 +38,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
|
||||
@@ -5,42 +5,10 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Petals */
|
||||
.spring-petal {
|
||||
position: fixed;
|
||||
top: -20px;
|
||||
z-index: 1005;
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
background-color: #ffc0cb;
|
||||
border-radius: 15px 0px 15px 0px;
|
||||
|
||||
will-change: transform, top;
|
||||
animation-name: spring-fall, spring-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-duration: 10s, 3s;
|
||||
}
|
||||
|
||||
.spring-petal.lighter {
|
||||
background-color: #ffd1dc;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.spring-petal.darker {
|
||||
background-color: #ffb7c5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.spring-petal.type2 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 10px 0px 10px 5px;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
/* Pollen */
|
||||
@@ -67,24 +35,21 @@
|
||||
z-index: 5;
|
||||
transform-origin: top center;
|
||||
pointer-events: none;
|
||||
|
||||
animation-name: spring-beam-pulse;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Grass */
|
||||
/* Grass Container (Wrapper) */
|
||||
.spring-grass-container {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
z-index: 1002;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
/* HTML Grass Overlayer */
|
||||
.spring-grass {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -99,55 +64,119 @@
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/* Ladybugs */
|
||||
.spring-ladybug {
|
||||
@keyframes spring-grass-sway {
|
||||
0% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(8deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
/* SVG Meadow Layer */
|
||||
.spring-meadow-layer {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 4px;
|
||||
background-color: #e74c3c; /* Red */
|
||||
border-radius: 3px 3px 0 0;
|
||||
z-index: 1003;
|
||||
|
||||
will-change: left, transform;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: spring-grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.spring-meadow {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes spring-grow-meadow {
|
||||
0% { transform: translateY(100%); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 0.95; }
|
||||
}
|
||||
|
||||
.spring-sway {
|
||||
transform-origin: bottom center;
|
||||
animation: spring-meadow-sway 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes spring-meadow-sway {
|
||||
0% { transform: skewX(-2deg); }
|
||||
100% { transform: skewX(2deg); }
|
||||
}
|
||||
|
||||
/* Birds */
|
||||
.spring-bird {
|
||||
position: static !important;
|
||||
display: block;
|
||||
z-index: 1001;
|
||||
/* MARK: BIRD SIZE */
|
||||
width: 80px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.spring-ladybug.right {
|
||||
animation-name: spring-bug-crawl-right;
|
||||
transform: scaleX(1);
|
||||
/* Butterflies */
|
||||
.spring-butterfly {
|
||||
position: static !important;
|
||||
display: block;
|
||||
z-index: 1001;
|
||||
/* MARK: BUTTERFLY SIZE */
|
||||
width: 40px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.spring-ladybug.left {
|
||||
animation-name: spring-bug-crawl-left;
|
||||
transform: scaleX(-1);
|
||||
/* Bee */
|
||||
.spring-bee {
|
||||
position: static !important;
|
||||
display: block;
|
||||
z-index: 1001;
|
||||
/* MARK: BEE SIZE */
|
||||
width: 30px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.spring-ladybug::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 1px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
/* Ladybug */
|
||||
.spring-ladybug-gif {
|
||||
position: static !important;
|
||||
display: block;
|
||||
/* MARK: LADYBUG SIZE */
|
||||
width: 30px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
--bug-rotation: -55deg;
|
||||
}
|
||||
|
||||
@keyframes spring-fall {
|
||||
0% { top: -10%; }
|
||||
100% { top: 100%; }
|
||||
.spring-ladybug-wrapper {
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
@keyframes spring-sway {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(30px) rotate(45deg);
|
||||
}
|
||||
/* Generic Wrappers */
|
||||
.spring-anim-wrapper {
|
||||
position: fixed;
|
||||
z-index: 1001;
|
||||
will-change: transform;
|
||||
top: 0; left: 0;
|
||||
}
|
||||
|
||||
.spring-align-y {
|
||||
width: 100%; height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.spring-mirror-wrapper {
|
||||
width: 100%; height: 100%;
|
||||
will-change: transform;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spring-float {
|
||||
0% { transform: translateX(0) translateY(0); }
|
||||
25% { transform: translateX(20px) translateY(-10px); }
|
||||
@@ -157,23 +186,70 @@
|
||||
}
|
||||
|
||||
@keyframes spring-beam-pulse {
|
||||
0% { opacity: 0.3; transform: rotate(45deg) scaleX(1); }
|
||||
50% { opacity: 0.6; transform: rotate(45deg) scaleX(1.1); }
|
||||
100% { opacity: 0.3; transform: rotate(45deg) scaleX(1); }
|
||||
0% { opacity: 0; transform: rotate(var(--beam-rotation, 45deg)) scaleX(0.8); }
|
||||
50% { opacity: 0.6; transform: rotate(var(--beam-rotation, 45deg)) scaleX(1.2); }
|
||||
100% { opacity: 0; transform: rotate(var(--beam-rotation, 45deg)) scaleX(0.8); }
|
||||
}
|
||||
|
||||
@keyframes spring-grass-sway {
|
||||
0% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(8deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
|
||||
|
||||
/* Wrapper animations (Flight across screen) */
|
||||
@keyframes spring-fly-right-wrapper {
|
||||
0% { transform: translateX(-10vw); }
|
||||
100% { transform: translateX(110vw); }
|
||||
}
|
||||
|
||||
@keyframes spring-bug-crawl-right {
|
||||
0% { left: -5%; }
|
||||
100% { left: 105%; }
|
||||
@keyframes spring-fly-left-wrapper {
|
||||
0% { transform: translateX(110vw); }
|
||||
100% { transform: translateX(-10vw); }
|
||||
}
|
||||
|
||||
@keyframes spring-bug-crawl-left {
|
||||
0% { left: 105%; }
|
||||
100% { left: -5%; }
|
||||
/* Vertical Drift for Sloped Flight */
|
||||
@keyframes spring-vertical-drift {
|
||||
0% { transform: translateY(var(--start-y, 10vh)); }
|
||||
100% { transform: translateY(var(--end-y, 10vh)); }
|
||||
}
|
||||
|
||||
/* Inner animations (Bobbing/Fluttering) */
|
||||
@keyframes spring-bob {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-20px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes spring-flutter {
|
||||
0% { transform: translateY(0) rotate(0deg); }
|
||||
25% { transform: translateY(-5px) rotate(5deg); }
|
||||
50% { transform: translateY(0) rotate(0deg); }
|
||||
75% { transform: translateY(5px) rotate(-5deg); }
|
||||
100% { transform: translateY(0) rotate(0deg); }
|
||||
}
|
||||
|
||||
/* Bee Buzz - Reduced Intensity */
|
||||
@keyframes spring-buzz {
|
||||
0% { transform: translate(0, 0); }
|
||||
25% { transform: translate(2px, -2px); }
|
||||
50% { transform: translate(0, 2px); }
|
||||
75% { transform: translate(-2px, -2px); }
|
||||
100% { transform: translate(0, 0); }
|
||||
}
|
||||
|
||||
/* Ladybug Walk (Wrapper handles X) */
|
||||
@keyframes spring-walk-right {
|
||||
0% { transform: translateX(-10vw); }
|
||||
100% { transform: translateX(110vw); }
|
||||
}
|
||||
|
||||
@keyframes spring-walk-left {
|
||||
0% { transform: translateX(110vw); }
|
||||
100% { transform: translateX(-10vw); }
|
||||
}
|
||||
|
||||
/* Ladybug Crawl (Inner Wobble) */
|
||||
@keyframes spring-crawl {
|
||||
0% { transform: translateY(0) rotate(var(--bug-rotation, 0deg)); }
|
||||
25% { transform: translateY(-3px) rotate(calc(var(--bug-rotation, 0deg) + 8deg)); }
|
||||
50% { transform: translateY(0) rotate(var(--bug-rotation, 0deg)); }
|
||||
75% { transform: translateY(-3px) rotate(calc(var(--bug-rotation, 0deg) - 8deg)); }
|
||||
100% { transform: translateY(0) rotate(var(--bug-rotation, 0deg)); }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
const config = window.SeasonalsPluginConfig?.Spring || {};
|
||||
|
||||
const spring = config.EnableSpring !== undefined ? config.EnableSpring : true;
|
||||
const petalCount = config.PetalCount || 25;
|
||||
const pollenCount = config.PollenCount || 15;
|
||||
const ladybugCountConfig = config.LadybugCount || 5;
|
||||
const sunbeamCount = config.SunbeamCount || 5;
|
||||
const spring = config.EnableSpring !== undefined ? config.EnableSpring : true; // Enable/disable spring
|
||||
const pollenCount = config.PollenCount || 30; // Number of pollen particles
|
||||
const sunbeamCount = config.SunbeamCount || 5; // Number of sunbeams
|
||||
const enableSunbeams = config.EnableSpringSunbeams !== undefined ? config.EnableSpringSunbeams : true; // Enable/disable sunbeams
|
||||
const birdCount = config.BirdCount !== undefined ? config.BirdCount : 3; // Number of birds
|
||||
const butterflyCount = config.ButterflyCount !== undefined ? config.ButterflyCount : 4; // Number of butterflies
|
||||
const beeCount = config.BeeCount !== undefined ? config.BeeCount : 2; // Number of bees
|
||||
const ladybugCount = config.LadybugCount !== undefined ? config.LadybugCount : 2; // Number of ladybugs
|
||||
const randomSpring = config.EnableRandomSpring !== undefined ? config.EnableRandomSpring : true; // Enable random spring objects
|
||||
|
||||
const randomSpring = config.EnableRandomSpring !== undefined ? config.EnableRandomSpring : true;
|
||||
const randomSpringMobile = config.EnableRandomSpringMobile !== undefined ? config.EnableRandomSpringMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const enablePetals = config.EnableSpringPetals !== undefined ? config.EnableSpringPetals : true;
|
||||
const enableSunbeams = config.EnableSpringSunbeams !== undefined ? config.EnableSpringSunbeams : true;
|
||||
const birdImages = [
|
||||
'../Seasonals/Resources/spring_assets/Bird_1.gif',
|
||||
'../Seasonals/Resources/spring_assets/Bird_2.gif',
|
||||
'../Seasonals/Resources/spring_assets/Bird_3.gif'
|
||||
];
|
||||
|
||||
const butterflyImages = [
|
||||
'../Seasonals/Resources/spring_assets/Butterfly_1.gif',
|
||||
'../Seasonals/Resources/spring_assets/Butterfly_2.gif'
|
||||
];
|
||||
|
||||
const beeImage = '../Seasonals/Resources/spring_assets/Bee.gif';
|
||||
const ladybugImage = '../Seasonals/Resources/spring_assets/ladybug.gif';
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
@@ -41,45 +53,18 @@ function toggleSpring() {
|
||||
const observer = new MutationObserver(toggleSpring);
|
||||
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
||||
|
||||
function createPetal(container) {
|
||||
if (!enablePetals) return;
|
||||
|
||||
const petal = document.createElement('div');
|
||||
petal.classList.add('spring-petal');
|
||||
|
||||
const type = Math.random() > 0.5 ? 'type1' : 'type2';
|
||||
petal.classList.add(type);
|
||||
|
||||
const color = Math.random() > 0.7 ? 'darker' : 'lighter';
|
||||
petal.classList.add(color);
|
||||
|
||||
const randomLeft = Math.random() * 100;
|
||||
petal.style.left = `${randomLeft}%`;
|
||||
|
||||
const size = Math.random() * 0.5 + 0.5;
|
||||
petal.style.transform = `scale(${size})`;
|
||||
|
||||
const duration = Math.random() * 5 + 8;
|
||||
const delay = Math.random() * 10;
|
||||
const swayDuration = Math.random() * 2 + 2;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
|
||||
}
|
||||
petal.style.animationDelay = `${delay}s, ${Math.random() * 3}s`;
|
||||
|
||||
container.appendChild(petal);
|
||||
}
|
||||
|
||||
function createPollen(container) {
|
||||
const pollen = document.createElement('div');
|
||||
pollen.classList.add('spring-pollen');
|
||||
|
||||
// MARK: POLLEN START VERTICAL POSITION (in %)
|
||||
const startY = Math.random() * 60 + 20;
|
||||
pollen.style.top = `${startY}%`;
|
||||
pollen.style.left = `${Math.random() * 100}%`;
|
||||
|
||||
const size = Math.random() * 3 + 1;
|
||||
// MARK: POLLEN SIZE
|
||||
const size = Math.random() * 3 + 1; // 1-4px
|
||||
pollen.style.width = `${size}px`;
|
||||
pollen.style.height = `${size}px`;
|
||||
|
||||
@@ -90,27 +75,40 @@ function createPollen(container) {
|
||||
container.appendChild(pollen);
|
||||
}
|
||||
|
||||
function createSunbeam(container) {
|
||||
function spawnSunbeamGroup(container, count) {
|
||||
if (!enableSunbeams) return;
|
||||
|
||||
const beam = document.createElement('div');
|
||||
beam.classList.add('spring-sunbeam');
|
||||
|
||||
const left = Math.random() * 100; // Spread across full width
|
||||
beam.style.left = `${left}%`;
|
||||
|
||||
// Thinner beams as requested
|
||||
const width = Math.random() * 20 + 10; // 10-30px wide
|
||||
beam.style.width = `${width}px`;
|
||||
|
||||
const rotate = Math.random() * 20 - 10 + 45;
|
||||
beam.style.transform = `rotate(${rotate}deg)`;
|
||||
|
||||
const duration = Math.random() * 10 + 10;
|
||||
beam.style.animationDuration = `${duration}s`;
|
||||
beam.style.animationDelay = `-${Math.random() * 10}s`;
|
||||
|
||||
container.appendChild(beam);
|
||||
const rotate = Math.random() * 30 - 15 + 45;
|
||||
let beamsActive = count;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const beam = document.createElement('div');
|
||||
beam.classList.add('spring-sunbeam');
|
||||
|
||||
const left = Math.random() * 100;
|
||||
beam.style.left = `${left}%`;
|
||||
|
||||
// MARK: SUNBEAM WIDTH (in px)
|
||||
const width = Math.random() * 12 + 8; // 8-20px wide
|
||||
beam.style.width = `${width}px`;
|
||||
|
||||
beam.style.setProperty('--beam-rotation', `${rotate}deg`);
|
||||
|
||||
const duration = Math.random() * 7 + 8; // 8-15s
|
||||
beam.style.animation = `spring-beam-pulse ${duration}s ease-in-out forwards`;
|
||||
|
||||
beam.style.animationDelay = `${Math.random() * 3}s`;
|
||||
|
||||
beam.addEventListener('animationend', () => {
|
||||
beam.remove();
|
||||
beamsActive--;
|
||||
if (beamsActive === 0) {
|
||||
spawnSunbeamGroup(container, count);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(beam);
|
||||
}
|
||||
}
|
||||
|
||||
function createGrass(container) {
|
||||
@@ -121,71 +119,82 @@ function createGrass(container) {
|
||||
container.appendChild(grassContainer);
|
||||
}
|
||||
|
||||
// More grass: 1 blade every 3px (was 15px)
|
||||
const bladeCount = window.innerWidth / 3;
|
||||
grassContainer.innerHTML = '';
|
||||
|
||||
let pathsBg = '';
|
||||
let pathsFg = '';
|
||||
const w = window.innerWidth;
|
||||
const hSVG = 80;
|
||||
|
||||
// 1. Generate Straight Line HTML-Style Grass (converted to SVG Paths)
|
||||
const bladeCount = w / 5; // Reduced from w/3
|
||||
for (let i = 0; i < bladeCount; i++) {
|
||||
const blade = document.createElement('div');
|
||||
blade.classList.add('spring-grass');
|
||||
|
||||
const height = Math.random() * 40 + 20; // 20-60px height
|
||||
blade.style.height = `${height}px`;
|
||||
blade.style.left = `${i * 3 + Math.random() * 2}px`;
|
||||
|
||||
const duration = Math.random() * 2 + 3;
|
||||
blade.style.animationDuration = `${duration}s`;
|
||||
blade.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
const x = i * 5 + Math.random() * 3;
|
||||
|
||||
const hue = 100 + Math.random() * 40;
|
||||
blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`;
|
||||
const color = `hsl(${hue}, 60%, 40%)`;
|
||||
|
||||
grassContainer.appendChild(blade);
|
||||
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
|
||||
// ~66% chance to be in background (1001), 33% foreground (1003)
|
||||
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
|
||||
}
|
||||
|
||||
// Add Ladybugs
|
||||
const bugs = ladybugCountConfig;
|
||||
for (let i = 0; i < bugs; i++) {
|
||||
createLadybug(grassContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function createLadybug(container) {
|
||||
const bug = document.createElement('div');
|
||||
bug.classList.add('spring-ladybug');
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
bug.classList.add(direction);
|
||||
|
||||
// Position lower (bottom of grass), but ensure visibility
|
||||
const bottomOffset = direction === 'right' ? Math.random() * 5 + 6 : Math.random() * 5 + 2;
|
||||
bug.style.bottom = `${bottomOffset}px`;
|
||||
|
||||
// Start position depends on direction
|
||||
if (direction === 'right') {
|
||||
bug.style.left = '-5%'; // Start off-screen left
|
||||
} else {
|
||||
bug.style.left = '105%'; // Start off-screen right
|
||||
// 2. Generate Curved Earth-Day Style Grass
|
||||
for (let i = 0; i < 200; i++) { // Reduced from 400
|
||||
const x = Math.random() * w;
|
||||
const h = 20 + Math.random() * 50;
|
||||
const cY = hSVG - h;
|
||||
const bend = x + (Math.random() * 40 - 20);
|
||||
const color = Math.random() > 0.5 ? '#4caf50' : '#45a049';
|
||||
const width = 1 + Math.random() * 2;
|
||||
const path = `<path d="M ${x} ${hSVG} Q ${bend} ${cY+20} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
|
||||
// ~66% chance to be in background (1001), 33% foreground (1003)
|
||||
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
|
||||
}
|
||||
|
||||
const duration = Math.random() * 20 + 20; // Slow crawl
|
||||
bug.style.animationDuration = `${duration}s`;
|
||||
bug.style.animationDelay = `-${Math.random() * 20}s`;
|
||||
|
||||
container.appendChild(bug);
|
||||
}
|
||||
// 3. Generate SVG Flowers
|
||||
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493', '#FFFFFF'];
|
||||
const flowerCount = Math.floor(w / 40); // Reduced from w/30
|
||||
for (let i = 0; i < flowerCount; i++) {
|
||||
const x = 10 + Math.random() * (w - 20);
|
||||
const y = 10 + Math.random() * 40; // 10-50px from top of SVG
|
||||
const col = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
let flower = '';
|
||||
// Stem
|
||||
flower += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#2e7d32" stroke-width="1.5" fill="none"/>`;
|
||||
|
||||
// Petals
|
||||
const r = 2 + Math.random() * 1.5;
|
||||
flower += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||
flower += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||
flower += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||
flower += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||
// Center
|
||||
flower += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
|
||||
|
||||
function addRandomSpringObjects() {
|
||||
const container = document.querySelector('.spring-container');
|
||||
if (!container) return;
|
||||
// ~66% chance to be in background (1001), 33% foreground (1003)
|
||||
if (Math.random() > 0.33) pathsBg += flower; else pathsFg += flower;
|
||||
}
|
||||
|
||||
if (enablePetals) {
|
||||
for (let i = 0; i < petalCount; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < pollenCount; i++) {
|
||||
createPollen(container);
|
||||
}
|
||||
// Inject purely SVG based grass container
|
||||
grassContainer.innerHTML = `
|
||||
<div class="spring-meadow-layer" style="z-index: 1001;">
|
||||
<svg class="spring-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="spring-sway">
|
||||
${pathsBg}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="spring-meadow-layer" style="z-index: 1003;">
|
||||
<svg class="spring-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="spring-sway" style="animation-delay: -2s;">
|
||||
${pathsFg}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function initSpringObjects() {
|
||||
@@ -199,47 +208,254 @@ function initSpringObjects() {
|
||||
|
||||
createGrass(container);
|
||||
|
||||
if (enablePetals) {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const petal = document.createElement('div');
|
||||
petal.classList.add('spring-petal');
|
||||
const type = Math.random() > 0.5 ? 'type1' : 'type2';
|
||||
petal.classList.add(type);
|
||||
const color = Math.random() > 0.7 ? 'darker' : 'lighter';
|
||||
petal.classList.add(color);
|
||||
const randomLeft = Math.random() * 100;
|
||||
petal.style.left = `${randomLeft}%`;
|
||||
const size = Math.random() * 0.5 + 0.5;
|
||||
petal.style.transform = `scale(${size})`;
|
||||
|
||||
const duration = Math.random() * 5 + 8;
|
||||
const swayDuration = Math.random() * 2 + 2;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
|
||||
}
|
||||
petal.style.animationDelay = `-${Math.random() * 10}s, -${Math.random() * 3}s`;
|
||||
container.appendChild(petal);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableSunbeams) {
|
||||
// Initial sunbeams
|
||||
for (let i = 0; i < sunbeamCount; i++) {
|
||||
createSunbeam(container);
|
||||
}
|
||||
spawnSunbeamGroup(container, sunbeamCount);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeSpring() {
|
||||
if (!spring) return;
|
||||
if (!spring) {
|
||||
console.warn('Spring is disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
initSpringObjects();
|
||||
toggleSpring();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomSpring && (screenWidth > 768 || randomSpringMobile)) {
|
||||
addRandomSpringObjects();
|
||||
const container = document.querySelector('.spring-container');
|
||||
if (container) {
|
||||
if (randomSpring) {
|
||||
// Add Pollen
|
||||
for (let i = 0; i < pollenCount; i++) {
|
||||
createPollen(container);
|
||||
}
|
||||
|
||||
// Add Birds
|
||||
for (let i = 0; i < birdCount; i++) {
|
||||
setTimeout(() => createBird(container), Math.random() * 1000); // 0-1s desync
|
||||
}
|
||||
// Add Butterflies
|
||||
for (let i = 0; i < butterflyCount; i++) {
|
||||
setTimeout(() => createButterfly(container), Math.random() * 1000); // 0-1s desync
|
||||
}
|
||||
// Add Bees
|
||||
for (let i = 0; i < beeCount; i++) {
|
||||
setTimeout(() => createBee(container), Math.random() * 1000); // 0-1s desync
|
||||
}
|
||||
// Add Ladybugs
|
||||
for (let i = 0; i < ladybugCount; i++) {
|
||||
setTimeout(() => createLadybugGif(container), Math.random() * 1000); // 0-1s desync
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createBird(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('spring-anim-wrapper');
|
||||
wrapper.classList.add('spring-bird-wrapper');
|
||||
|
||||
const alignY = document.createElement('div');
|
||||
alignY.classList.add('spring-align-y');
|
||||
|
||||
const mirror = document.createElement('div');
|
||||
mirror.classList.add('spring-mirror-wrapper');
|
||||
|
||||
const bird = document.createElement('img');
|
||||
bird.classList.add('spring-bird');
|
||||
|
||||
const randomSrc = birdImages[Math.floor(Math.random() * birdImages.length)];
|
||||
bird.src = randomSrc;
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
// MARK: BIRD SPEED (10-15s)
|
||||
const duration = Math.random() * 5 + 10;
|
||||
|
||||
// MARK: BIRD HEIGHT RANGE (in vh)
|
||||
const startY = Math.random() * 55 + 5; // Start 5-60vh
|
||||
const endY = Math.random() * 55 + 5; // End 5-60vh
|
||||
alignY.style.setProperty('--start-y', `${startY}vh`);
|
||||
alignY.style.setProperty('--end-y', `${endY}vh`);
|
||||
|
||||
if (direction === 'right') {
|
||||
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(-1)';
|
||||
} else {
|
||||
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(1)';
|
||||
}
|
||||
alignY.style.animation = `spring-vertical-drift ${duration}s linear forwards`;
|
||||
|
||||
wrapper.addEventListener('animationend', (e) => {
|
||||
if (e.animationName.includes('fly-')) {
|
||||
wrapper.remove();
|
||||
createBird(container);
|
||||
}
|
||||
});
|
||||
|
||||
bird.style.animation = `spring-bob 2s ease-in-out infinite`;
|
||||
|
||||
mirror.appendChild(bird);
|
||||
alignY.appendChild(mirror);
|
||||
wrapper.appendChild(alignY);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
function createButterfly(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('spring-anim-wrapper');
|
||||
wrapper.classList.add('spring-butterfly-wrapper');
|
||||
|
||||
const alignY = document.createElement('div');
|
||||
alignY.classList.add('spring-align-y');
|
||||
|
||||
const mirror = document.createElement('div');
|
||||
mirror.classList.add('spring-mirror-wrapper');
|
||||
|
||||
const butterfly = document.createElement('img');
|
||||
butterfly.classList.add('spring-butterfly');
|
||||
|
||||
const randomSrc = butterflyImages[Math.floor(Math.random() * butterflyImages.length)];
|
||||
butterfly.src = randomSrc;
|
||||
|
||||
const duration = Math.random() * 15 + 25;
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
|
||||
if (direction === 'right') {
|
||||
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(1)';
|
||||
} else {
|
||||
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
wrapper.addEventListener('animationend', (e) => {
|
||||
if (e.animationName.includes('fly-')) {
|
||||
wrapper.remove();
|
||||
createButterfly(container);
|
||||
}
|
||||
});
|
||||
|
||||
// MARK: BUTTERFLY FLUTTER RHYTHM
|
||||
butterfly.style.animation = `spring-flutter 3s ease-in-out infinite`;
|
||||
butterfly.style.animationDelay = `-${Math.random() * 3}s`;
|
||||
|
||||
// MARK: BUTTERFLY HEIGHT (in vh)
|
||||
const top = Math.random() * 35 + 30; // 30-65vh
|
||||
alignY.style.transform = `translateY(${top}vh)`;
|
||||
|
||||
mirror.appendChild(butterfly);
|
||||
alignY.appendChild(mirror);
|
||||
wrapper.appendChild(alignY);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
function createBee(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('spring-anim-wrapper');
|
||||
wrapper.classList.add('spring-bee-wrapper');
|
||||
|
||||
const alignY = document.createElement('div');
|
||||
alignY.classList.add('spring-align-y');
|
||||
|
||||
const mirror = document.createElement('div');
|
||||
mirror.classList.add('spring-mirror-wrapper');
|
||||
|
||||
const bee = document.createElement('img');
|
||||
bee.classList.add('spring-bee');
|
||||
bee.src = beeImage;
|
||||
|
||||
const duration = Math.random() * 10 + 15;
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
|
||||
if (direction === 'right') {
|
||||
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(1)';
|
||||
} else {
|
||||
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
wrapper.addEventListener('animationend', (e) => {
|
||||
if (e.animationName.includes('fly-')) {
|
||||
wrapper.remove();
|
||||
createBee(container);
|
||||
}
|
||||
});
|
||||
|
||||
// MARK: BEE HEIGHT (in vh)
|
||||
const top = Math.random() * 60 + 20; // 20-80vh
|
||||
alignY.style.transform = `translateY(${top}vh)`;
|
||||
|
||||
mirror.appendChild(bee);
|
||||
alignY.appendChild(mirror);
|
||||
wrapper.appendChild(alignY);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
function createLadybugGif(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('spring-anim-wrapper');
|
||||
wrapper.classList.add('spring-ladybug-wrapper');
|
||||
|
||||
const alignY = document.createElement('div');
|
||||
alignY.classList.add('spring-align-y');
|
||||
|
||||
const mirror = document.createElement('div');
|
||||
mirror.classList.add('spring-mirror-wrapper');
|
||||
|
||||
const bug = document.createElement('img');
|
||||
bug.classList.add('spring-ladybug-gif');
|
||||
bug.src = ladybugImage;
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
const duration = Math.random() * 20 + 30;
|
||||
|
||||
if (direction === 'right') {
|
||||
wrapper.style.animation = `spring-walk-right ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(1)';
|
||||
} else {
|
||||
wrapper.style.animation = `spring-walk-left ${duration}s linear forwards`;
|
||||
mirror.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
wrapper.addEventListener('animationend', (e) => {
|
||||
if (e.animationName.includes('walk-')) {
|
||||
wrapper.remove();
|
||||
createLadybugGif(container);
|
||||
}
|
||||
});
|
||||
|
||||
bug.style.animation = `spring-crawl 2s ease-in-out infinite`;
|
||||
|
||||
// Target the Ladybug to walk on the ground visually (aligning properly with the CSS/SVG grass size)
|
||||
alignY.style.transform = `translateY(calc(100vh - 5px - 30px))`;
|
||||
|
||||
mirror.appendChild(bug);
|
||||
alignY.appendChild(mirror);
|
||||
wrapper.appendChild(alignY);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
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('.spring-container');
|
||||
if (container) {
|
||||
createGrass(container);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
initializeSpring();
|
||||
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Bee.gif
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Bird_1.gif
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Bird_2.gif
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Bird_3.gif
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Butterfly_1.gif
Normal file
|
After Width: | Height: | Size: 502 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Butterfly_2.gif
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/Rotkehlchen.gif
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/ladybug.gif
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/spring_assets/wasp.gif
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
39
Jellyfin.Plugin.Seasonals/Web/storm.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.storm-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.raindrop {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
@keyframes stormy-rain {
|
||||
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
|
||||
5% { opacity: 1; }
|
||||
95% { opacity: 1; }
|
||||
100% { transform: translateY(110vh) translateX(-40vh) rotate(20deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.lightning-flash {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
will-change: opacity;
|
||||
}
|
||||
99
Jellyfin.Plugin.Seasonals/Web/storm.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// 1. Read Configuration
|
||||
const config = window.SeasonalsPluginConfig?.Storm || {};
|
||||
|
||||
const enabled = config.EnableStorm !== undefined ? config.EnableStorm : true;
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
|
||||
const enableLightning = config.EnableLightning !== undefined ? config.EnableLightning : true;
|
||||
const rainSpeed = config.RainSpeed || 1.0;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// 2. Toggle Function
|
||||
function toggleStorm() {
|
||||
const container = document.querySelector('.storm-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('Storm hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Storm visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. MutationObserver
|
||||
const observer = new MutationObserver(toggleStorm);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// 4. Element Creation
|
||||
function createElements() {
|
||||
const container = document.querySelector('.storm-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.storm-container')) {
|
||||
container.className = 'storm-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < elementCount; i++) {
|
||||
const drop = document.createElement('div');
|
||||
drop.className = 'raindrop';
|
||||
|
||||
drop.style.left = `${Math.random() * 140}vw`;
|
||||
drop.style.top = `${-20 - Math.random() * 50}vh`;
|
||||
|
||||
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
|
||||
drop.style.animation = `stormy-rain ${duration}s linear infinite`;
|
||||
drop.style.animationDelay = `${Math.random() * 2}s`;
|
||||
drop.style.opacity = Math.random() * 0.5 + 0.3;
|
||||
|
||||
container.appendChild(drop);
|
||||
}
|
||||
|
||||
if (enableLightning) {
|
||||
const flash = document.createElement('div');
|
||||
flash.className = 'lightning-flash';
|
||||
container.appendChild(flash);
|
||||
|
||||
function triggerFlash() {
|
||||
const nextFlashDelay = 5000 + Math.random() * 10000;
|
||||
|
||||
setTimeout(() => {
|
||||
flash.style.opacity = '0.8';
|
||||
setTimeout(() => { flash.style.opacity = '0'; }, 50);
|
||||
setTimeout(() => { flash.style.opacity = '0.5'; }, 100);
|
||||
setTimeout(() => { flash.style.opacity = '0'; }, 150);
|
||||
|
||||
triggerFlash();
|
||||
}, nextFlashDelay);
|
||||
}
|
||||
|
||||
triggerFlash();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Initialization
|
||||
function initializeStorm() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleStorm();
|
||||
}
|
||||
|
||||
initializeStorm();
|
||||
@@ -8,6 +8,7 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.summer-bubble {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const config = window.SeasonalsPluginConfig?.Summer || {};
|
||||
|
||||
const summer = config.EnableSummer !== undefined ? config.EnableSummer : true; // enable/disable summer
|
||||
const bubbleCount = config.BubbleCount || 20;
|
||||
const dustCount = config.DustCount || 50;
|
||||
const randomSummer = config.EnableRandomSummer !== undefined ? config.EnableRandomSummer : true; // enable random objects
|
||||
const randomSummerMobile = config.EnableRandomSummerMobile !== undefined ? config.EnableRandomSummerMobile : false; // enable random objects on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
||||
const summer = config.EnableSummer !== undefined ? config.EnableSummer : true; // Enable/disable summer theme
|
||||
const bubbleCount = config.BubbleCount || 30; // Number of bubbles
|
||||
const dustCount = config.DustCount || 50; // Number of dust particles
|
||||
const randomSummer = config.EnableRandomSummer !== undefined ? config.EnableRandomSummer : true; // Enable random generating objects
|
||||
const randomSummerMobile = config.EnableRandomSummerMobile !== undefined ? config.EnableRandomSummerMobile : false; // Enable random generating objects on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // Randomize animation duration of bubbles and dust
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
@@ -51,10 +51,12 @@ function createBubble(container, isDust = false) {
|
||||
|
||||
// Random size
|
||||
if (!isDust) {
|
||||
// MARK: BUBBLE SIZE
|
||||
const size = Math.random() * 20 + 10; // 10-30px bubbles
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
} else {
|
||||
// MARK: DUST SIZE
|
||||
const size = Math.random() * 3 + 1; // 1-4px dust
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
@@ -81,7 +83,7 @@ function addRandomSummerObjects() {
|
||||
createBubble(container, false);
|
||||
}
|
||||
|
||||
// Add some dust particles (more of them, they are subtle)
|
||||
// Add some dust particles
|
||||
for (let i = 0; i < dustCount; i++) {
|
||||
createBubble(container, true);
|
||||
}
|
||||
@@ -110,10 +112,12 @@ function initSummerObjects() {
|
||||
bubble.style.left = `${randomLeft}%`;
|
||||
|
||||
if (!isDust) {
|
||||
// MARK: BUBBLE SIZE
|
||||
const size = Math.random() * 20 + 10;
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
} else {
|
||||
// MARK: DUST SIZE
|
||||
const size = Math.random() * 3 + 1;
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
|
||||
@@ -248,6 +248,13 @@
|
||||
<option value="spring">Spring</option>
|
||||
<option value="summer">Summer (Bubbles)</option>
|
||||
<option value="carnival">Carnival (Confetti)</option>
|
||||
<option value="cherryblossom">Cherryblossom</option>
|
||||
<option value="earthday">Earth Day</option>
|
||||
<option value="eurovision">Eurovision</option>
|
||||
<option value="piday">Pi-Day</option>
|
||||
<option value="pride">Pride</option>
|
||||
<option value="rain">Rain</option>
|
||||
<option value="storm">Storm (Epilepsy Warning!)</option>
|
||||
<option value="custom">⚙ Custom (Local Files)</option>
|
||||
</select>
|
||||
|
||||
@@ -333,6 +340,13 @@
|
||||
spring: { css: 'spring.css', js: 'spring.js', container: 'spring-container' },
|
||||
summer: { css: 'summer.css', js: 'summer.js', container: 'summer-container' },
|
||||
carnival: { css: 'carnival.css', js: 'carnival.js', container: 'carnival-container' },
|
||||
cherryblossom: { css: 'cherryblossom.css', js: 'cherryblossom.js', container: 'cherryblossom-container' },
|
||||
earthday: { css: 'earthday.css', js: 'earthday.js', container: 'earthday-container' },
|
||||
eurovision: { css: 'eurovision.css', js: 'eurovision.js', container: 'eurovision-container' },
|
||||
piday: { css: 'piday.css', js: 'piday.js', container: 'piday-container' },
|
||||
pride: { css: 'pride.css', js: 'pride.js', container: 'pride-container' },
|
||||
rain: { css: 'rain.css', js: 'rain.js', container: 'rain-container' },
|
||||
storm: { css: 'storm.css', js: 'storm.js', container: 'storm-container' },
|
||||
};
|
||||
|
||||
const select = document.getElementById('theme-select');
|
||||
@@ -360,7 +374,9 @@
|
||||
'.christmas-container', '.santa-container', '.autumn-container',
|
||||
'.christmas-container', '.santa-container', '.autumn-container',
|
||||
'.easter-container', '.resurrection-container', '.spring-container',
|
||||
'.summer-container', '.carnival-container'
|
||||
'.summer-container', '.carnival-container', '.cherryblossom-container',
|
||||
'.earthday-container', '.eurovision-container', '.piday-container',
|
||||
'.pride-container', '.rain-container', '.storm-container'
|
||||
];
|
||||
knownContainers.forEach(sel => {
|
||||
document.querySelectorAll(sel).forEach(el => {
|
||||
|
||||
@@ -9,12 +9,20 @@
|
||||
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.7.1.0",
|
||||
"version": "1.7.2.0",
|
||||
"changelog": "- feat: add Pi Day, Pride, Rain, and Storm themes\n- fix: improve performance",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.2.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "34c8426c48bd7d470c3e8dc7f02f86da",
|
||||
"timestamp": "2026-02-23T00:34:13Z"
|
||||
},
|
||||
{
|
||||
"version": "1.7.1.5",
|
||||
"changelog": "- feat: add summer, spring and carnival themes",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "5f288972124771d1223484f75138f566",
|
||||
"timestamp": "2026-02-19T02:20:23Z"
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.5/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "f6447d476189e69fb96fe1675c55a1a0",
|
||||
"timestamp": "2026-02-21T14:28:30Z"
|
||||
},
|
||||
{
|
||||
"version": "1.7.0.15",
|
||||
|
||||