Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a301564ac | ||
|
|
85e69a0b34 | ||
|
|
5adaf202ae | ||
|
|
99ac46a384 | ||
|
|
3a2750388b | ||
|
|
33e89ec16b | ||
|
|
9adbe92e7c | ||
|
|
103d63f1b1 | ||
|
|
49bad2e880 | ||
|
|
04616c2ac4 | ||
|
|
3b73dd1728 | ||
|
|
d5df90a6ae | ||
|
|
494e475f42 | ||
|
|
4703ba48ed | ||
|
|
93d5686b77 | ||
|
|
71d07aa0f3 | ||
|
|
c43f031617 | ||
|
|
89ce903e8a | ||
|
|
cbf5d73629 | ||
|
|
8be17dae74 | ||
|
|
76006dc162 | ||
|
|
492acb4052 | ||
|
|
68dc9efa4d | ||
|
|
7b15ed46c1 | ||
|
|
8019ba760f | ||
|
|
9d1a268875 | ||
|
|
9e5feafd64 | ||
|
|
5283a69bb8 | ||
|
|
a4b2d2edd5 | ||
|
|
d0634e4487 | ||
|
|
79c4f988f2 | ||
|
|
cee1fa6736 | ||
|
|
8510674d58 | ||
|
|
5ee724201b | ||
|
|
b85c038df0 | ||
|
|
3d4e04ab0f | ||
|
|
b1d1ce79e6 | ||
|
|
0b7b506b8d | ||
|
|
f3ea84cc80 | ||
|
|
3d9a474aae | ||
|
|
db5baa1fd7 | ||
|
|
72ad4ee1a4 | ||
|
|
bb6c7796d5 | ||
|
|
bd8088c52b | ||
|
|
3c1bd01373 | ||
|
|
669ac6d3da | ||
|
|
73f9be91ef | ||
|
|
f14785c54a | ||
|
|
296873f89e | ||
|
|
d6a9ff7176 | ||
|
|
ef15857533 | ||
|
|
19b21ba94f | ||
|
|
8f322fd6cf | ||
|
|
bdc7d2e325 | ||
|
|
8afe397c23 | ||
|
|
30c29d440f | ||
|
|
69adc64a44 | ||
|
|
b0fae10aa1 | ||
|
|
cee4dae769 | ||
|
|
f9aeeadccf | ||
|
|
fc35fcd3c4 | ||
|
|
6a83981e1d | ||
|
|
540d7f9baa | ||
|
|
a162b30bcd | ||
|
|
c6d04b9b3b | ||
|
|
1ceb9cef7f | ||
|
|
eb06a979f6 | ||
|
|
9b6d48a5fe | ||
|
|
e3ea4fa599 | ||
|
|
c5093073d0 | ||
|
|
85cabf29bb | ||
|
|
b008221cf4 | ||
|
|
2bbf13c044 | ||
|
|
082120b70b | ||
|
|
c66ccf970e | ||
|
|
861f431e50 | ||
|
|
be4313d776 | ||
|
|
9b8a563e43 | ||
|
|
8255683714 | ||
|
|
c24abcbd59 | ||
|
|
b17c2a6efe | ||
|
|
ad4fb7964b | ||
|
|
306b0c5e6e | ||
|
|
6cc344e0db | ||
|
|
3ea0709c77 | ||
|
|
b74c8ad2a1 | ||
|
|
8f0c2ac7df | ||
|
|
fa658c0057 | ||
|
|
de7e04c926 | ||
|
|
892be062d3 | ||
|
|
042d89f5b8 | ||
|
|
22709c38d1 | ||
|
|
22d40fb248 | ||
|
|
97dbc09daa | ||
|
|
df29e12699 | ||
|
|
6632cc81de | ||
|
|
437569ec1d | ||
|
|
5c0d8af5d8 | ||
|
|
5b98b442e5 | ||
|
|
e81ce3cab1 | ||
|
|
066ad6fc84 | ||
|
|
8baaa936e1 | ||
|
|
f9b4b3c25d | ||
|
|
f4f472e6ec | ||
|
|
e8effa7dfe | ||
|
|
ff2df0196a | ||
|
|
3e5da3dda2 | ||
|
|
509d198cd0 | ||
|
|
26eb40e282 | ||
|
|
08b2ae987e | ||
|
|
599518d627 | ||
|
|
23c5ab7e9d | ||
|
|
589a360729 | ||
|
|
5c10583601 | ||
|
|
20dcf08bda | ||
|
|
e4b3a132b1 | ||
|
|
63ec6d5e52 | ||
|
|
ec89f2d48d | ||
|
|
61b21de566 | ||
|
|
590f2c3606 | ||
|
|
fdadc00a0c | ||
|
|
2ab88fd5ac | ||
|
|
9a41c0a2ce | ||
|
|
816f58cf02 | ||
|
|
5be9a60eed | ||
|
|
133808105e | ||
|
|
c631aca44f | ||
|
|
241450d132 | ||
|
|
d50d71bde1 | ||
|
|
262dd98519 | ||
|
|
b45ec73a67 | ||
|
|
4e8a37540f | ||
|
|
cde5201991 | ||
|
|
b2420b8eb4 | ||
|
|
dacec7d03c | ||
|
|
65f8261fb7 | ||
|
|
78872e7f96 |
@@ -18,20 +18,40 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
EnableClientSideToggle = true;
|
||||
|
||||
Autumn = new AutumnOptions();
|
||||
Snowflakes = new SnowflakesOptions();
|
||||
Snowfall = new SnowfallOptions();
|
||||
Snowstorm = new SnowstormOptions();
|
||||
Birthday = new BirthdayOptions();
|
||||
Carnival = new CarnivalOptions();
|
||||
CherryBlossom = new CherryBlossomOptions();
|
||||
Christmas = new ChristmasOptions();
|
||||
EarthDay = new EarthDayOptions();
|
||||
Easter = new EasterOptions();
|
||||
Eid = new EidOptions();
|
||||
Eurovision = new EurovisionOptions();
|
||||
FilmNoir = new FilmNoirOptions();
|
||||
Fireworks = new FireworksOptions();
|
||||
Friday13 = new Friday13Options();
|
||||
Frost = new FrostOptions();
|
||||
Halloween = new HalloweenOptions();
|
||||
Hearts = new HeartsOptions();
|
||||
Christmas = new ChristmasOptions();
|
||||
Santa = new SantaOptions();
|
||||
Easter = new EasterOptions();
|
||||
MarioDay = new MarioDayOptions();
|
||||
Matrix = new MatrixOptions();
|
||||
Oktoberfest = new OktoberfestOptions();
|
||||
Olympia = new OlympiaOptions();
|
||||
Oscar = new OscarOptions();
|
||||
Rain = new RainOptions();
|
||||
Pride = new PrideOptions();
|
||||
Resurrection = new ResurrectionOptions();
|
||||
Santa = new SantaOptions();
|
||||
Snowfall = new SnowfallOptions();
|
||||
Snowflakes = new SnowflakesOptions();
|
||||
Snowstorm = new SnowstormOptions();
|
||||
Space = new SpaceOptions();
|
||||
Spooky = new SpookyOptions();
|
||||
Sports = new SportsOptions();
|
||||
Spring = new SpringOptions();
|
||||
StarWars = new StarWarsOptions();
|
||||
Storm = new StormOptions();
|
||||
Summer = new SummerOptions();
|
||||
CherryBlossom = new CherryBlossomOptions();
|
||||
Carnival = new CarnivalOptions();
|
||||
Underwater = new UnderwaterOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,175 +77,312 @@ 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.
|
||||
/// </summary>
|
||||
public AutumnOptions Autumn { get; set; }
|
||||
public SnowflakesOptions Snowflakes { get; set; }
|
||||
public SnowfallOptions Snowfall { get; set; }
|
||||
public SnowstormOptions Snowstorm { get; set; }
|
||||
public BirthdayOptions Birthday { get; set; }
|
||||
public CarnivalOptions Carnival { get; set; }
|
||||
public CherryBlossomOptions CherryBlossom { get; set; }
|
||||
public ChristmasOptions Christmas { get; set; }
|
||||
public EarthDayOptions EarthDay { get; set; }
|
||||
public EasterOptions Easter { get; set; }
|
||||
public EidOptions Eid { get; set; }
|
||||
public EurovisionOptions Eurovision { get; set; }
|
||||
public FilmNoirOptions FilmNoir { get; set; }
|
||||
public FireworksOptions Fireworks { get; set; }
|
||||
public Friday13Options Friday13 { get; set; }
|
||||
public FrostOptions Frost { get; set; }
|
||||
public HalloweenOptions Halloween { get; set; }
|
||||
public HeartsOptions Hearts { get; set; }
|
||||
public ChristmasOptions Christmas { get; set; }
|
||||
public SantaOptions Santa { get; set; }
|
||||
public EasterOptions Easter { get; set; }
|
||||
public MarioDayOptions MarioDay { get; set; }
|
||||
public MatrixOptions Matrix { get; set; }
|
||||
public OktoberfestOptions Oktoberfest { get; set; }
|
||||
public OlympiaOptions Olympia { get; set; }
|
||||
public OscarOptions Oscar { get; set; }
|
||||
public PrideOptions Pride { get; set; }
|
||||
public RainOptions Rain { get; set; }
|
||||
public ResurrectionOptions Resurrection { get; set; }
|
||||
public SantaOptions Santa { get; set; }
|
||||
public SnowfallOptions Snowfall { get; set; }
|
||||
public SnowflakesOptions Snowflakes { get; set; }
|
||||
public SnowstormOptions Snowstorm { get; set; }
|
||||
public SpaceOptions Space { get; set; }
|
||||
public SpookyOptions Spooky { get; set; }
|
||||
public SportsOptions Sports { get; set; }
|
||||
public SpringOptions Spring { get; set; }
|
||||
public StarWarsOptions StarWars { get; set; }
|
||||
public StormOptions Storm { get; set; }
|
||||
public SummerOptions Summer { get; set; }
|
||||
public CherryBlossomOptions CherryBlossom { get; set; }
|
||||
public CarnivalOptions Carnival { get; set; }
|
||||
public UnderwaterOptions Underwater { get; set; }
|
||||
}
|
||||
|
||||
public class AutumnOptions
|
||||
{
|
||||
public int LeafCount { get; set; } = 25;
|
||||
public class AutumnOptions {
|
||||
public bool EnableAutumn { get; set; } = true;
|
||||
public bool EnableRandomLeaves { get; set; } = true;
|
||||
public bool EnableRandomLeavesMobile { get; set; } = false;
|
||||
public int LeafCount { get; set; } = 35;
|
||||
public int LeafCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableRotation { get; set; } = false;
|
||||
}
|
||||
|
||||
public class SnowflakesOptions
|
||||
{
|
||||
public int SnowflakeCount { get; set; } = 25;
|
||||
public bool EnableSnowflakes { get; set; } = true;
|
||||
public bool EnableRandomSnowflakes { get; set; } = true;
|
||||
public bool EnableRandomSnowflakesMobile { get; set; } = false;
|
||||
public bool EnableColoredSnowflakes { get; set; } = true;
|
||||
public class BirthdayOptions {
|
||||
public bool EnableBirthday { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 12;
|
||||
public int SymbolCountMobile { get; set; } = 5;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public int ConfettiCount { get; set; } = 60;
|
||||
}
|
||||
|
||||
public class CarnivalOptions {
|
||||
public bool EnableCarnival { get; set; } = true;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableCarnivalSway { get; set; } = true;
|
||||
public int ObjectCount { get; set; } = 120;
|
||||
public int ObjectCountMobile { get; set; } = 60;
|
||||
}
|
||||
|
||||
public class CherryBlossomOptions {
|
||||
public bool EnableCherryBlossom { get; set; } = true;
|
||||
public int PetalCount { get; set; } = 25;
|
||||
public int PetalCountMobile { get; set; } = 15;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SnowfallOptions
|
||||
{
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double Speed { get; set; } = 3;
|
||||
public bool EnableSnowfall { get; set; } = true;
|
||||
public class ChristmasOptions {
|
||||
public bool EnableChristmas { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SnowstormOptions
|
||||
{
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double Speed { get; set; } = 6;
|
||||
public bool EnableSnowstorm { get; set; } = true;
|
||||
public double HorizontalWind { get; set; } = 4;
|
||||
public double VerticalVariation { get; set; } = 2;
|
||||
public class EarthDayOptions {
|
||||
public bool EnableEarthDay { get; set; } = true;
|
||||
public int VineCount { get; set; } = 4;
|
||||
}
|
||||
|
||||
public class FireworksOptions
|
||||
{
|
||||
public int ParticleCount { get; set; } = 50;
|
||||
public int LaunchInterval { get; set; } = 3200;
|
||||
public class EasterOptions {
|
||||
public bool EnableEaster { get; set; } = true;
|
||||
public bool EnableBunny { get; set; } = true;
|
||||
public int MinBunnyRestTime { get; set; } = 2000;
|
||||
public int MaxBunnyRestTime { get; set; } = 5000;
|
||||
public int EggCount { get; set; } = 15;
|
||||
}
|
||||
|
||||
public class EidOptions {
|
||||
public bool EnableEid { get; set; } = true;
|
||||
public int LanternCount { get; set; } = 8;
|
||||
public int LanternCountMobile { get; set; } = 3;
|
||||
}
|
||||
|
||||
public class EurovisionOptions {
|
||||
public bool EnableEurovision { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
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; } = 2;
|
||||
}
|
||||
|
||||
public class FilmNoirOptions {
|
||||
public bool EnableFilmNoir { get; set; } = true;
|
||||
}
|
||||
|
||||
public class FireworksOptions {
|
||||
public bool EnableFireworks { get; set; } = true;
|
||||
public bool ScrollFireworks { get; set; } = true;
|
||||
public int ParticleCount { get; set; } = 50;
|
||||
public int MinFireworks { get; set; } = 3;
|
||||
public int MaxFireworks { get; set; } = 6;
|
||||
public int LaunchInterval { get; set; } = 3200;
|
||||
}
|
||||
|
||||
public class HalloweenOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public class Friday13Options {
|
||||
public bool EnableFriday13 { get; set; } = true;
|
||||
}
|
||||
|
||||
public class FrostOptions {
|
||||
public bool EnableFrost { get; set; } = true;
|
||||
}
|
||||
|
||||
public class HalloweenOptions {
|
||||
public bool EnableHalloween { get; set; } = true;
|
||||
public bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableSpiders { get; set; } = true;
|
||||
public bool EnableMice { get; set; } = true;
|
||||
}
|
||||
|
||||
public class HeartsOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public class HeartsOptions {
|
||||
public bool EnableHearts { get; set; } = true;
|
||||
public bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class ChristmasOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public bool EnableChristmas { get; set; } = true;
|
||||
public bool EnableRandomChristmas { get; set; } = true;
|
||||
public bool EnableRandomChristmasMobile { get; set; } = false;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SantaOptions
|
||||
{
|
||||
public class MarioDayOptions {
|
||||
public bool EnableMarioDay { get; set; } = true;
|
||||
public bool LetMarioJump { get; set; } = true;
|
||||
}
|
||||
|
||||
public class MatrixOptions {
|
||||
public bool EnableMatrix { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public bool EnableMatrixBackground { get; set; } = false;
|
||||
public string MatrixChars { get; set; } = "0123456789";
|
||||
}
|
||||
|
||||
public class OktoberfestOptions {
|
||||
public bool EnableOktoberfest { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class OlympiaOptions {
|
||||
public bool EnableOlympia { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class OscarOptions {
|
||||
public bool EnableOscar { get; set; } = true;
|
||||
}
|
||||
|
||||
public class PrideOptions {
|
||||
public bool EnablePride { get; set; } = true;
|
||||
public int HeartCount { get; set; } = 20;
|
||||
public double HeartSize { get; set; } = 1.5;
|
||||
public bool ColorHeader { get; set; } = true;
|
||||
}
|
||||
|
||||
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.0;
|
||||
}
|
||||
|
||||
public class ResurrectionOptions {
|
||||
public bool EnableResurrection { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 12;
|
||||
public int SymbolCountMobile { get; set; } = 5;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SantaOptions {
|
||||
public bool EnableSanta { get; set; } = true;
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double SnowFallSpeed { get; set; } = 3;
|
||||
public double SantaSpeed { get; set; } = 10;
|
||||
public double SantaSpeedMobile { get; set; } = 8;
|
||||
public bool EnableSanta { get; set; } = true;
|
||||
public double SnowFallSpeed { get; set; } = 3;
|
||||
public double MaxSantaRestTime { get; set; } = 8;
|
||||
public double MinSantaRestTime { get; set; } = 3;
|
||||
public double MaxPresentFallSpeed { get; set; } = 5;
|
||||
public double MinPresentFallSpeed { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class EasterOptions
|
||||
{
|
||||
public int EggCount { get; set; } = 20;
|
||||
public bool EnableEaster { get; set; } = true;
|
||||
public bool EnableRandomEaster { get; set; } = true;
|
||||
public bool EnableRandomEasterMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableBunny { get; set; } = true;
|
||||
public int BunnyDuration { get; set; } = 12000;
|
||||
public int HopHeight { get; set; } = 12;
|
||||
public int MinBunnyRestTime { get; set; } = 2000;
|
||||
public int MaxBunnyRestTime { get; set; } = 5000;
|
||||
public class SnowfallOptions {
|
||||
public bool EnableSnowfall { get; set; } = true;
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double Speed { get; set; } = 3;
|
||||
}
|
||||
|
||||
public class ResurrectionOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 12;
|
||||
public bool EnableResurrection { get; set; } = true;
|
||||
public bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public class SnowflakesOptions {
|
||||
public bool EnableSnowflakes { get; set; } = true;
|
||||
public int SnowflakeCount { get; set; } = 25;
|
||||
public int SnowflakeCountMobile { get; set; } = 10;
|
||||
public bool EnableColoredSnowflakes { get; set; } = true;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SpringOptions
|
||||
{
|
||||
public int PollenCount { get; set; } = 15;
|
||||
public int LadybugCount { get; set; } = 5;
|
||||
public int SunbeamCount { get; set; } = 5;
|
||||
public class SnowstormOptions {
|
||||
public bool EnableSnowstorm { get; set; } = true;
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double Speed { get; set; } = 6;
|
||||
public double HorizontalWind { get; set; } = 4;
|
||||
public double VerticalVariation { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class SpaceOptions {
|
||||
public bool EnableSpace { get; set; } = true;
|
||||
public int PlanetCount { get; set; } = 6;
|
||||
public int AstronautCount { get; set; } = 1;
|
||||
public int SatelliteCount { get; set; } = 4;
|
||||
public int IssCount { get; set; } = 1;
|
||||
public int RocketCount { get; set; } = 1;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public int SymbolCountMobile { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class SpookyOptions {
|
||||
public bool EnableSpooky { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableSpookySway { get; set; } = true;
|
||||
public int SpookySize { get; set; } = 20;
|
||||
public int SpookyGlowSize { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class SportsOptions {
|
||||
public bool EnableSports { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 5;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public string TurfColor { get; set; } = "#228b22";
|
||||
public string SportsBalls { get; set; } = "football,basketball,tennis,volleyball";
|
||||
public bool EnableTrophy { get; set; } = false;
|
||||
public string ConfettiColors { get; set; } = "#000000,#FF0000,#FFCC00";
|
||||
}
|
||||
|
||||
public class SpringOptions {
|
||||
public bool EnableSpring { get; set; } = true;
|
||||
public bool EnableRandomSpring { get; set; } = true;
|
||||
public bool EnableRandomSpringMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public int PollenCount { get; set; } = 30;
|
||||
public bool EnableSpringSunbeams { get; set; } = true;
|
||||
public int SunbeamCount { get; set; } = 5;
|
||||
public int BirdCount { get; set; } = 3;
|
||||
public int ButterflyCount { get; set; } = 4;
|
||||
public int BeeCount { get; set; } = 2;
|
||||
public int LadybugCount { get; set; } = 2;
|
||||
public int SymbolCountMobile { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class SummerOptions
|
||||
{
|
||||
public int BubbleCount { get; set; } = 20;
|
||||
public int DustCount { get; set; } = 50;
|
||||
public class StarWarsOptions {
|
||||
public bool EnableStarWars { get; set; } = true;
|
||||
}
|
||||
|
||||
public class StormOptions {
|
||||
public bool EnableStorm { get; set; } = true;
|
||||
public int RaindropCount { get; set; } = 300;
|
||||
public int RaindropCountMobile { get; set; } = 150;
|
||||
public bool EnableLightning { get; set; } = true;
|
||||
public double RainSpeed { get; set; } = 1.0;
|
||||
}
|
||||
|
||||
public class SummerOptions {
|
||||
public bool EnableSummer { get; set; } = true;
|
||||
public bool EnableRandomSummer { get; set; } = true;
|
||||
public bool EnableRandomSummerMobile { get; set; } = false;
|
||||
public int BubbleCount { get; set; } = 30;
|
||||
public int DustCount { get; set; } = 50;
|
||||
public int SymbolCountMobile { get; set; } = 2;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class CarnivalOptions
|
||||
{
|
||||
public int ObjectCount { get; set; } = 25;
|
||||
public bool EnableCarnival { get; set; } = true;
|
||||
public bool EnableRandomCarnival { get; set; } = true;
|
||||
public bool EnableRandomCarnivalMobile { get; set; } = false;
|
||||
public class UnderwaterOptions {
|
||||
public bool EnableUnderwater { get; set; } = true;
|
||||
public int SymbolCountMobile { get; set; } = 2;
|
||||
public bool EnableDifferentDuration { 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 bool EnableLightRays { get; set; } = true;
|
||||
public int SeaweedCount { get; set; } = 50;
|
||||
public int CrabCount { get; set; } = 2;
|
||||
public int StarfishCount { get; set; } = 2;
|
||||
public int ShellCount { get; set; } = 2;
|
||||
public int FishCount { get; set; } = 15;
|
||||
public int SeahorseCount { get; set; } = 3;
|
||||
public int JellyfishCount { get; set; } = 3;
|
||||
public int TurtleCount { get; set; } = 1;
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||
<Title>Jellyfin Seasonals Plugin</Title>
|
||||
<Authors>CodeDevMLH</Authors>
|
||||
<Version>1.7.1.1</Version>
|
||||
<Version>2.0.0.4</Version>
|
||||
<RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -8,24 +8,21 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: leaf-fall, leaf-shake;
|
||||
-webkit-animation-duration: 7s, 4s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
-webkit-user-select: none;
|
||||
animation-name: leaf-fall, leaf-shake;
|
||||
animation-duration: 7s, 4s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
@@ -38,34 +35,17 @@
|
||||
--rotate-end: 0deg !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
translate: 0 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes leaf-shake {
|
||||
0%, 100% {
|
||||
-webkit-transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaf-shake {
|
||||
0%, 100% {
|
||||
@@ -75,87 +55,3 @@
|
||||
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
||||
}
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
--rotate-start: -25deg;
|
||||
--rotate-end: 22deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 0.5s;
|
||||
--rotate-start: -32deg;
|
||||
--rotate-end: 35deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 1s;
|
||||
--rotate-start: -28deg;
|
||||
--rotate-end: 28deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 1.5s;
|
||||
--rotate-start: -38deg;
|
||||
--rotate-end: 32deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 0.8s;
|
||||
--rotate-start: -22deg;
|
||||
--rotate-end: 38deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 2s;
|
||||
--rotate-start: -35deg;
|
||||
--rotate-end: 25deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 1.2s;
|
||||
--rotate-start: -40deg;
|
||||
--rotate-end: 40deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 0.3s;
|
||||
--rotate-start: -30deg;
|
||||
--rotate-end: 30deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 1.8s;
|
||||
--rotate-start: -26deg;
|
||||
--rotate-end: 36deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 0.7s;
|
||||
--rotate-start: -34deg;
|
||||
--rotate-end: 24deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 2.3s;
|
||||
--rotate-start: -29deg;
|
||||
--rotate-end: 33deg;
|
||||
}
|
||||
|
||||
.leaf:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 1.4s;
|
||||
--rotate-start: -37deg;
|
||||
--rotate-end: 27deg;
|
||||
}
|
||||
@@ -1,12 +1,30 @@
|
||||
const config = window.SeasonalsPluginConfig?.Autumn || {};
|
||||
|
||||
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable leaves
|
||||
const randomLeaves = config.EnableRandomLeaves !== undefined ? config.EnableRandomLeaves : true; // enable random leaves
|
||||
const randomLeavesMobile = config.EnableRandomLeavesMobile !== undefined ? config.EnableRandomLeavesMobile : false; // enable random leaves on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random leaves
|
||||
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
|
||||
const leafCount = config.LeafCount || 25; // count of random extra leaves
|
||||
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable autumn
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable rotation
|
||||
const leafCount = config.LeafCount !== undefined ? config.LeafCount : 35; // count of random extra leaves
|
||||
const leafCountMobile = config.LeafCountMobile !== undefined ? config.LeafCountMobile : 10; // count of random extra leaves on mobile
|
||||
|
||||
const images = [
|
||||
"../Seasonals/Resources/autumn_images/acorn1.png",
|
||||
"../Seasonals/Resources/autumn_images/acorn2.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf1.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf2.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf3.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf4.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf5.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf6.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf7.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf8.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf9.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf10.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf11.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf12.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf13.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf14.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf15.png",
|
||||
];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -38,40 +56,23 @@ function toggleAutumn() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleAutumn);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"../Seasonals/Resources/autumn_images/acorn1.png",
|
||||
"../Seasonals/Resources/autumn_images/acorn2.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf1.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf2.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf3.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf4.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf5.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf6.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf7.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf8.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf9.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf10.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf11.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf12.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf13.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf14.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf15.png",
|
||||
];
|
||||
function initLeaves(count) {
|
||||
let autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||
if (!autumnContainer) {
|
||||
autumnContainer = document.createElement("div");
|
||||
autumnContainer.className = "autumn-container";
|
||||
autumnContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(autumnContainer);
|
||||
}
|
||||
|
||||
function addRandomLeaves(count) {
|
||||
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||
if (!autumnContainer) return; // exit if leave container is not found
|
||||
|
||||
console.log('Adding random leaves');
|
||||
console.log('Adding leaves');
|
||||
|
||||
// Array of leave characters
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -90,7 +91,9 @@ function addRandomLeaves(count) {
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 4; // delay for shake+rotate (0s to 4s)
|
||||
// Display directly symbols on full screen (below) or let it build up (above)
|
||||
// const randomAnimationDelay = -(Math.random() * 16); // delay for fall (-16s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 4); // delay for shake+rotate (-4s to 0s)
|
||||
|
||||
// apply styles
|
||||
leaveDiv.style.left = `${randomLeft}%`;
|
||||
@@ -118,60 +121,18 @@ function addRandomLeaves(count) {
|
||||
// add the leave to the container
|
||||
autumnContainer.appendChild(leaveDiv);
|
||||
}
|
||||
console.log('Random leaves added');
|
||||
console.log('Leaves added');
|
||||
}
|
||||
|
||||
// initialize standard leaves
|
||||
function initLeaves() {
|
||||
const container = document.querySelector('.autumn-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.autumn-container')) {
|
||||
container.className = "autumn-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const leafDiv = document.createElement("div");
|
||||
leafDiv.className = enableRotation ? "leaf" : "leaf no-rotation";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = images[Math.floor(Math.random() * images.length)];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 16s)
|
||||
const randomAnimationDuration2 = Math.random() * 3 + 2; // shake+rotate duration (2s to 5s)
|
||||
leafDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// set random rotation angles for standard leaves too (only if rotation is enabled)
|
||||
if (enableRotation) {
|
||||
const randomRotateStart = -(Math.random() * 40 + 20); // -20deg to -60deg
|
||||
const randomRotateEnd = Math.random() * 40 + 20; // 20deg to 60deg
|
||||
leafDiv.style.setProperty('--rotate-start', `${randomRotateStart}deg`);
|
||||
leafDiv.style.setProperty('--rotate-end', `${randomRotateEnd}deg`);
|
||||
} else {
|
||||
// No rotation - set to 0 degrees
|
||||
leafDiv.style.setProperty('--rotate-start', '0deg');
|
||||
leafDiv.style.setProperty('--rotate-end', '0deg');
|
||||
}
|
||||
|
||||
leafDiv.appendChild(img);
|
||||
container.appendChild(leafDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize leaves and add random leaves
|
||||
// initialize leaves
|
||||
function initializeLeaves() {
|
||||
if (!leaves) return; // exit if leaves are disabled
|
||||
initLeaves();
|
||||
toggleAutumn();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomLeaves && (screenWidth > 768 || randomLeavesMobile)) { // add random leaves only on larger screens, unless enabled for mobile devices
|
||||
addRandomLeaves(leafCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? leafCount : leafCountMobile;
|
||||
|
||||
initLeaves(count);
|
||||
toggleAutumn();
|
||||
}
|
||||
|
||||
initializeLeaves();
|
||||
155
Jellyfin.Plugin.Seasonals/Web/birthday.css
Normal file
@@ -0,0 +1,155 @@
|
||||
.birthday-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.birthday-symbol {
|
||||
will-change: opacity;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
animation: birthday-rise linear infinite forwards;
|
||||
opacity: 0.95;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.birthday-sway {
|
||||
will-change: transform;
|
||||
animation-name: birthday-sway;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
.birthday-inner {
|
||||
pointer-events: auto;
|
||||
cursor: crosshair;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* MARK: Balloon Size */
|
||||
.birthday-symbol img {
|
||||
width: 18vh;
|
||||
height: auto;
|
||||
max-width: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.birthday-confetti-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
will-change: transform;
|
||||
animation-name: birthday-confetti-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.birthday-confetti {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
will-change: transform;
|
||||
animation-name: birthday-flutter;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.birthday-confetti.circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.birthday-confetti.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.birthday-confetti.triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes birthday-rise {
|
||||
0% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0) rotate(var(--start-rot, 0deg)); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translate3d(var(--x-pos, 0vw), -20vh, 0) rotate(calc(var(--start-rot, 0deg) * -1)); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes birthday-confetti-fall {
|
||||
0% { transform: translate3d(var(--x-pos, 0vw), -10vh, 0); }
|
||||
100% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0); }
|
||||
}
|
||||
|
||||
@keyframes birthday-sway {
|
||||
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
|
||||
100% { transform: translateX(var(--sway-amount, 50px)); }
|
||||
}
|
||||
|
||||
@keyframes birthday-flutter {
|
||||
0% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg); }
|
||||
100% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg)); }
|
||||
}
|
||||
|
||||
@keyframes birthday-pop {
|
||||
0% { transform: scale(1); opacity: 1; filter: brightness(1); }
|
||||
30% { transform: scale(1.3); opacity: 1; filter: brightness(1.5); }
|
||||
100% { transform: scale(0); opacity: 0; filter: brightness(2); }
|
||||
}
|
||||
|
||||
.birthday-burst-wrapper {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
will-change: transform, opacity;
|
||||
animation: birthday-burst-y 1.2s cubic-bezier(0.42, 0, 1, 1) forwards;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti {
|
||||
will-change: transform;
|
||||
animation: birthday-burst-x 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti.triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes birthday-burst-y {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(calc(var(--burst-y) + 150px));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes birthday-burst-x {
|
||||
0% {
|
||||
transform: translateX(0) rotate3d(var(--rx), var(--ry), var(--rz), 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(var(--burst-x) * 1.5)) rotate3d(var(--rx), var(--ry), var(--rz), var(--rot-dir));
|
||||
}
|
||||
}
|
||||
326
Jellyfin.Plugin.Seasonals/Web/birthday.js
Normal file
@@ -0,0 +1,326 @@
|
||||
const config = window.SeasonalsPluginConfig?.Birthday || {};
|
||||
|
||||
const birthday = config.EnableBirthday !== undefined ? config.EnableBirthday : true; // enable/disable birthday symbols
|
||||
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 15; // count of balloons
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the symbols
|
||||
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 5; // count of mobile balloons
|
||||
const baseConfettiCount = config.ConfettiCount !== undefined ? config.ConfettiCount : 60; // count of confetti
|
||||
|
||||
/**
|
||||
* Base ballon image: https://www.flaticon.com/de/kostenloses-icon/ballon_1512470
|
||||
* modified by CodeDevMLH
|
||||
*/
|
||||
const birthdayImages = [
|
||||
'../Seasonals/Resources/birthday_assets/balloon_blue.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_green.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_lightblue.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_orange.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_pink.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_red.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_yellow.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_turquoise.gif',
|
||||
'../Seasonals/Resources/birthday_assets/balloon_violet.gif'
|
||||
];
|
||||
|
||||
|
||||
const balloonColors = {
|
||||
'balloon_blue': ['#3498db', '#2980b9', '#1f618d'],
|
||||
'balloon_green': ['#2ecc71', '#27ae60', '#1e8449'],
|
||||
'balloon_lightblue': ['#36c5f0', '#81ecec', '#00cec9'],
|
||||
'balloon_orange': ['#e67e22', '#d35400', '#a04000'],
|
||||
'balloon_pink': ['#ff726d', '#f4306d', '#e84393'],
|
||||
'balloon_red': ['#e74c3c', '#c0392b', '#922b21'],
|
||||
'balloon_yellow': ['#f1c40f', '#f39c12', '#b7950b'],
|
||||
'balloon_turquoise': ['#36c5f0', '#81ecec', '#00cec9'],
|
||||
'balloon_violet': ['#9b59b6', '#8e44ad', '#6c3483']
|
||||
};
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleBirthday() {
|
||||
const container = document.querySelector('.birthday-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('Birthday hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Birthday visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleBirthday);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createBalloonPopConfetti(container, x, y, colors) {
|
||||
const popConfettiColors = colors || [
|
||||
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||
];
|
||||
|
||||
// Spawn 15-20 particles
|
||||
const particleCount = Math.floor(Math.random() * 5) + 15;
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'birthday-burst-wrapper';
|
||||
wrapper.style.position = 'absolute';
|
||||
wrapper.style.left = `${x}px`;
|
||||
wrapper.style.top = `${y}px`;
|
||||
wrapper.style.zIndex = '1000';
|
||||
|
||||
const particle = document.createElement('div');
|
||||
particle.classList.add('birthday-burst-confetti');
|
||||
|
||||
// Random color
|
||||
const color = popConfettiColors[Math.floor(Math.random() * popConfettiColors.length)];
|
||||
particle.style.backgroundColor = color;
|
||||
|
||||
// Random shape
|
||||
const shape = Math.random();
|
||||
if (shape > 0.66) {
|
||||
particle.classList.add('circle');
|
||||
const size = Math.random() * 4 + 4; // 4-8px
|
||||
particle.style.width = `${size}px`;
|
||||
particle.style.height = `${size}px`;
|
||||
} else if (shape > 0.33) {
|
||||
particle.classList.add('rect');
|
||||
const width = Math.random() * 3 + 3; // 3-6px
|
||||
const height = Math.random() * 4 + 6; // 6-10px
|
||||
particle.style.width = `${width}px`;
|
||||
particle.style.height = `${height}px`;
|
||||
} else {
|
||||
particle.classList.add('triangle');
|
||||
}
|
||||
|
||||
// Random direction for explosion (circular)
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const distance = Math.random() * 60 + 20; // 20-80px burst radius
|
||||
|
||||
const xOffset = Math.cos(angle) * distance;
|
||||
const yOffset = Math.sin(angle) * distance;
|
||||
|
||||
particle.style.setProperty('--burst-x', `${xOffset}px`);
|
||||
wrapper.style.setProperty('--burst-y', `${yOffset}px`);
|
||||
|
||||
// Random rotation during fall
|
||||
particle.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
|
||||
particle.style.setProperty('--rx', Math.random().toFixed(2));
|
||||
particle.style.setProperty('--ry', Math.random().toFixed(2));
|
||||
particle.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||
|
||||
wrapper.appendChild(particle);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Remove particle after animation
|
||||
setTimeout(() => wrapper.remove(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function createBirthday() {
|
||||
const container = document.querySelector('.birthday-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.birthday-container')) {
|
||||
container.className = 'birthday-container';
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Cake and Garland have been removed
|
||||
|
||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
let finalCount = isMobile ? symbolCountMobile : symbolCount;
|
||||
|
||||
const useRandomDuration = enableDifferentDuration !== false;
|
||||
|
||||
// Arrays moved to top of file
|
||||
|
||||
for (let i = 0; i < finalCount; i++) {
|
||||
let symbol = document.createElement('div');
|
||||
|
||||
const randomImage = birthdayImages[Math.floor(Math.random() * birthdayImages.length)];
|
||||
const randomItem = randomImage.split('/').pop().split('.')[0]; // Extracts "balloon_blue"
|
||||
symbol.className = `birthday-symbol birthday-${randomItem}`;
|
||||
|
||||
// Create inner div for sway
|
||||
let innerDiv = document.createElement('div');
|
||||
innerDiv.className = 'birthday-inner';
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.src = randomImage;
|
||||
img.onerror = function() {
|
||||
symbol.remove(); // Remove element completely on error
|
||||
};
|
||||
innerDiv.appendChild(img);
|
||||
|
||||
// Sway wrapper
|
||||
let swayWrapper = document.createElement('div');
|
||||
swayWrapper.className = 'birthday-sway';
|
||||
const swayDuration = Math.random() * 3 + 3; // 3-6s per cycle
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
const swayAmount = Math.random() * 60 + 20; // 20-80px
|
||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||
|
||||
swayWrapper.appendChild(innerDiv);
|
||||
symbol.appendChild(swayWrapper);
|
||||
|
||||
const leftPos = Math.random() * 95;
|
||||
|
||||
// Far away effect
|
||||
const depth = Math.random();
|
||||
// MARK: balloon size
|
||||
const scale = 0.85 + depth * 0.3; // 0.85 to 1.15
|
||||
const zIndex = Math.floor(depth * 30) + 10;
|
||||
|
||||
img.style.transform = `scale(${scale})`;
|
||||
symbol.style.zIndex = zIndex;
|
||||
|
||||
let durationSeconds = 9;
|
||||
if (useRandomDuration) {
|
||||
// Far strings climb slower
|
||||
durationSeconds = (1 - depth) * 6 + 7 + Math.random() * 4;
|
||||
}
|
||||
|
||||
// Negative delay correctly scatters them initially across the screen vertically
|
||||
// avoiding them all popping up at bottom edge together
|
||||
const delaySeconds = -(Math.random() * durationSeconds);
|
||||
|
||||
const isBalloon = randomItem.startsWith('balloon');
|
||||
|
||||
if (isBalloon) {
|
||||
// Sway animation is now handled natively by the GIF motion.
|
||||
|
||||
// Interaction to pop is handled visually by the GIF, but we can still remove it on hover
|
||||
innerDiv.addEventListener('mouseenter', function(e) {
|
||||
if (!this.classList.contains('popped')) {
|
||||
this.classList.add('popped');
|
||||
this.style.animation = 'birthday-pop 0.2s ease-out forwards';
|
||||
this.style.pointerEvents = 'none'; // avoid re-triggering
|
||||
|
||||
// Create confetti burst at balloon's screen position
|
||||
const rect = this.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
// explosion height
|
||||
const cy = rect.top + rect.height * -0.05;
|
||||
// Ensure the burst container is appended to the main document body or the birthday container
|
||||
createBalloonPopConfetti(document.body, cx, cy, balloonColors[randomItem]);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset the balloon when it reappears at the bottom of the screen
|
||||
symbol.addEventListener('animationiteration', function(e) {
|
||||
// Ignore bubbling events from the inner sway animation
|
||||
if (e.animationName === 'birthday-rise' || e.target === symbol) {
|
||||
if (innerDiv.classList.contains('popped')) {
|
||||
innerDiv.classList.remove('popped');
|
||||
innerDiv.style.animation = '';
|
||||
innerDiv.style.pointerEvents = 'auto';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const startRot = (Math.random() * 20) - 10; // -10 to +10 spread
|
||||
symbol.style.setProperty('--start-rot', `${startRot}deg`);
|
||||
symbol.style.setProperty('--x-pos', `${leftPos}vw`);
|
||||
|
||||
symbol.style.animationDuration = `${durationSeconds}s`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
|
||||
// Party Confetti
|
||||
const confettiCount = baseConfettiCount;
|
||||
const allColors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
|
||||
|
||||
for (let i = 0; i < confettiCount; i++) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('birthday-confetti-wrapper');
|
||||
|
||||
// Use carnival.js 3D advanced fluttering logic
|
||||
let swayWrapper = document.createElement('div');
|
||||
swayWrapper.classList.add('birthday-sway');
|
||||
wrapper.appendChild(swayWrapper);
|
||||
|
||||
const confetti = document.createElement('div');
|
||||
confetti.classList.add('birthday-confetti');
|
||||
|
||||
const color = allColors[Math.floor(Math.random() * allColors.length)];
|
||||
confetti.style.backgroundColor = color;
|
||||
|
||||
// Shape assignments
|
||||
const shape = Math.random();
|
||||
if (shape > 0.8) confetti.classList.add('circle');
|
||||
else if (shape > 0.6) confetti.classList.add('square');
|
||||
else if (shape > 0.4) confetti.classList.add('triangle');
|
||||
else confetti.classList.add('rect'); // default
|
||||
|
||||
// Sizing
|
||||
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
|
||||
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')) {
|
||||
const size = Math.random() * 5 + 5; // 5-10px
|
||||
confetti.style.width = `${size}px`;
|
||||
confetti.style.height = `${size}px`;
|
||||
}
|
||||
|
||||
const duration = Math.random() * 5 + 5;
|
||||
const delay = -Math.random() * duration; // Spawn fully integrated across screen width/height
|
||||
|
||||
wrapper.style.setProperty('--x-pos', `${Math.random() * 100}vw`);
|
||||
wrapper.style.animationDelay = `${delay}s`;
|
||||
wrapper.style.animationDuration = `${duration}s`;
|
||||
|
||||
// Sway handling
|
||||
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
const swayAmount = Math.random() * 70 + 30; // 30-100px
|
||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||
|
||||
// 3D Flutter Rotation
|
||||
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));
|
||||
const rotDir = Math.random() > 0.5 ? 1 : -1;
|
||||
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
|
||||
|
||||
swayWrapper.appendChild(confetti);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
/* Removed fallback logic */
|
||||
|
||||
function initializeBirthday() {
|
||||
if (!birthday) return;
|
||||
createBirthday();
|
||||
toggleBirthday();
|
||||
}
|
||||
|
||||
initializeBirthday();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_blue.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_green.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_orange.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_pink.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_red.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_violet.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_yellow.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
@@ -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;
|
||||
top: 0;
|
||||
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 {
|
||||
@@ -32,12 +34,11 @@
|
||||
.carnival-confetti {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background-color: #f0f;
|
||||
background-color: rgb(0, 0, 0);
|
||||
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: translate3d(0, -10vh, 0);
|
||||
}
|
||||
100% {
|
||||
top: 110%;
|
||||
transform: translate3d(0, 110vh, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 @@
|
||||
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 carnivalCount = config.ObjectCount !== undefined ? config.ObjectCount : 120; // Number of confetti pieces to spawn
|
||||
const carnivalCountMobile = config.ObjectCountMobile !== undefined ? config.ObjectCountMobile : 60; // Number of confetti pieces to spawn on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // enable/disable carnivalsway
|
||||
|
||||
const confettiColors = [
|
||||
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||
];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
@@ -35,22 +40,13 @@ function toggleCarnival() {
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleCarnival);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
const confettiColors = [
|
||||
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||
];
|
||||
|
||||
function createConfettiPiece(container, isInitial = false) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('carnival-wrapper');
|
||||
@@ -85,15 +81,20 @@ function createConfettiPiece(container, isInitial = false) {
|
||||
// Random position
|
||||
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;
|
||||
@@ -112,15 +113,21 @@ function createConfettiPiece(container, isInitial = false) {
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
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
|
||||
// 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,22 +136,19 @@ 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);
|
||||
}
|
||||
|
||||
function addRandomCarnivalObjects(count) {
|
||||
const carnivalContainer = document.querySelector('.carnival-container');
|
||||
if (!carnivalContainer) return;
|
||||
|
||||
console.log('Adding random carnival confetti');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
createConfettiPiece(carnivalContainer, true);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize standard carnival objects
|
||||
function initCarnivalObjects() {
|
||||
function initCarnivalObjects(count) {
|
||||
let container = document.querySelector('.carnival-container');
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
@@ -154,21 +158,20 @@ function initCarnivalObjects() {
|
||||
}
|
||||
|
||||
// Initial confetti
|
||||
for (let i = 0; i < 30; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
createConfettiPiece(container, true);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize carnival
|
||||
function initializeCarnival() {
|
||||
if (!carnival) return; // exit if carnival is disabled
|
||||
initCarnivalObjects();
|
||||
toggleCarnival();
|
||||
if (!carnival) return;
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
|
||||
addRandomCarnivalObjects(carnivalCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? carnivalCount : carnivalCountMobile;
|
||||
|
||||
initCarnivalObjects(count);
|
||||
toggleCarnival();
|
||||
}
|
||||
|
||||
initializeCarnival();
|
||||
|
||||
60
Jellyfin.Plugin.Seasonals/Web/cherryblossom.css
Normal file
@@ -0,0 +1,60 @@
|
||||
.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: 0;
|
||||
z-index: 1005;
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
background-color: #ffc0cb;
|
||||
border-radius: 15px 0px 15px 0px;
|
||||
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
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% { translate: 0 -10vh; }
|
||||
100% { translate: 0 110vh; }
|
||||
}
|
||||
|
||||
@keyframes cherryblossom-sway {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(30px) rotate(45deg);
|
||||
}
|
||||
}
|
||||
94
Jellyfin.Plugin.Seasonals/Web/cherryblossom.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
|
||||
|
||||
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true; // enable/disable cherryblossom
|
||||
const petalCount = config.PetalCount !== undefined ? config.PetalCount : 25; // count of petal
|
||||
const petalCountMobile = config.PetalCountMobile !== undefined ? config.PetalCountMobile : 10; // count of petal on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
|
||||
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 initObjects(count) {
|
||||
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 < count; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCherryBlossom() {
|
||||
if (!cherryBlossom) return;
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? petalCount : petalCountMobile;
|
||||
|
||||
initObjects(count);
|
||||
toggleCherryBlossom();
|
||||
}
|
||||
|
||||
initializeCherryBlossom();
|
||||
@@ -8,65 +8,40 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.christmas {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
-webkit-animation-name: christmas-fall, christmas-shake;
|
||||
-webkit-animation-duration: 10s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: christmas-fall, christmas-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes christmas-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes christmas-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
translate: 0 110vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
@@ -75,64 +50,4 @@
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 0s;
|
||||
}
|
||||
|
||||
.christmas:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 2.5s;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
const config = window.SeasonalsPluginConfig?.Christmas || {};
|
||||
|
||||
const christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
|
||||
const randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random Christmas
|
||||
const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ? config.EnableRandomChristmasMobile : false; // enable random Christmas on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols
|
||||
const christmasCount = config.SymbolCount || 25; // count of random extra christmas
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const christmasCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||
const christmasCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
|
||||
|
||||
// Array of christmas characters
|
||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -37,22 +38,22 @@ function toggleChristmas() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleChristmas);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// Array of christmas characters
|
||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||
function initChristmas(count) {
|
||||
let christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
||||
if (!christmasContainer) {
|
||||
christmasContainer = document.createElement("div");
|
||||
christmasContainer.className = "christmas-container";
|
||||
christmasContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(christmasContainer);
|
||||
}
|
||||
|
||||
function addRandomChristmas(count) {
|
||||
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
||||
if (!christmasContainer) return; // exit if christmas container is not found
|
||||
|
||||
console.log('Adding random christmas');
|
||||
console.log('Adding christmas');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new christmas element
|
||||
@@ -64,8 +65,8 @@ function addRandomChristmas(count) {
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 12 + 8; // delay (8s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 5 + 3; // delay (0s to 5s)
|
||||
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
christmasDiv.style.left = `${randomLeft}%`;
|
||||
@@ -81,46 +82,18 @@ function addRandomChristmas(count) {
|
||||
// add the christmas to the container
|
||||
christmasContainer.appendChild(christmasDiv);
|
||||
}
|
||||
console.log('Random christmas added');
|
||||
console.log('Christmas added');
|
||||
}
|
||||
|
||||
// initialize standard christmas
|
||||
function initChristmas() {
|
||||
const christmasContainer = document.querySelector('.christmas-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.christmas-container')) {
|
||||
christmasContainer.className = "christmas-container";
|
||||
christmasContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(christmasContainer);
|
||||
}
|
||||
|
||||
// create the 12 standard christmas
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const christmasDiv = document.createElement('div');
|
||||
christmasDiv.className = 'christmas';
|
||||
christmasDiv.textContent = christmasSymbols[Math.floor(Math.random() * christmasSymbols.length)];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
christmasDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
christmasContainer.appendChild(christmasDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize christmas and add random christmas symbols
|
||||
// initialize christmas
|
||||
function initializeChristmas() {
|
||||
if (!christmas) return; // exit if christmas is disabled
|
||||
initChristmas();
|
||||
toggleChristmas();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomChristmas && (screenWidth > 768 || randomChristmasMobile)) { // add random christmas only on larger screens, unless enabled for mobile devices
|
||||
addRandomChristmas(christmasCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? christmasCount : christmasCountMobile;
|
||||
|
||||
initChristmas(count);
|
||||
toggleChristmas();
|
||||
}
|
||||
|
||||
initializeChristmas();
|
||||
38
Jellyfin.Plugin.Seasonals/Web/earthday.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.earthday-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 8vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.earthday-meadow {
|
||||
will-change: transform;
|
||||
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 {
|
||||
will-change: transform;
|
||||
transform-origin: bottom center;
|
||||
animation: sway-grass 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes sway-grass {
|
||||
0% { transform: skewX(-2deg); }
|
||||
100% { transform: skewX(2deg); }
|
||||
}
|
||||
127
Jellyfin.Plugin.Seasonals/Web/earthday.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const config = window.SeasonalsPluginConfig?.EarthDay || {};
|
||||
|
||||
const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true; // enable/disable earthday
|
||||
const vineCount = config.VineCount !== undefined ? config.VineCount : 4; // count of vine
|
||||
|
||||
const flowerColors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleEarthDay);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
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;
|
||||
// MARK: GRASS HEIGHT CONFIGURATION
|
||||
// To prevent squishing, hSVG calculation MUST match the height in earthday.css exactly
|
||||
// earthday.css uses 8vh, so here it is 0.08
|
||||
const hSVG = Math.floor(window.innerHeight * 0.08) || 80;
|
||||
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() * 15 - 7.5); // curvature
|
||||
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 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 = flowerColors[Math.floor(Math.random() * flowerColors.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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
function initializeEarthDay() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleEarthDay();
|
||||
}
|
||||
|
||||
initializeEarthDay();
|
||||
@@ -1,160 +1,65 @@
|
||||
.easter-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.easter-grass-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 8vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.easter-meadow-layer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.easter-meadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* sway */
|
||||
.easter-sway {
|
||||
will-change: transform;
|
||||
transform-origin: bottom center;
|
||||
animation: easter-wind-sway 6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes easter-wind-sway {
|
||||
0% { transform: skewX(-3deg); }
|
||||
100% { transform: skewX(5deg); }
|
||||
}
|
||||
|
||||
.hopping-rabbit {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
bottom: 10px;
|
||||
width: 70px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 0;
|
||||
width: 160px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hopping-rabbit {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.easter {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: easter-fall, easter-shake;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: easter-fall, easter-shake;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
.easter img {
|
||||
z-index: 15;
|
||||
height: auto;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes easter-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes easter-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes easter-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes easter-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.easter:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 0s;
|
||||
}
|
||||
|
||||
.easter:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 2.5s;
|
||||
}
|
||||
@@ -1,66 +1,20 @@
|
||||
const config = window.SeasonalsPluginConfig?.Easter || {};
|
||||
|
||||
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
|
||||
const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter
|
||||
const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter
|
||||
const easterEggCount = config.EggCount || 20; // count of random extra easter
|
||||
const enableBunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable bunny
|
||||
const minBunnyRestTime = config.MinBunnyRestTime !== undefined ? config.MinBunnyRestTime : 2000; // timing parameter
|
||||
const maxBunnyRestTime = config.MaxBunnyRestTime !== undefined ? config.MaxBunnyRestTime : 5000; // timing parameter
|
||||
const eggCount = config.EggCount !== undefined ? config.EggCount : 15; // count of egg
|
||||
|
||||
const bunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable hopping bunny
|
||||
const bunnyDuration = config.BunnyDuration || 12000; // duration of the bunny animation in ms
|
||||
const hopHeight = config.HopHeight || 12; // height of the bunny hops in px
|
||||
const minBunnyRestTime = config.MinBunnyRestTime || 2000; // minimum time the bunny rests in ms
|
||||
const maxBunnyRestTime = config.MaxBunnyRestTime || 5000; // maximum time the bunny rests in ms
|
||||
/* MARK: Bunny movement config */
|
||||
const jumpDistanceVw = 5; // Distance in vw the bunny covers per jump
|
||||
const jumpDurationMs = 770; // Time in ms the bunny spends moving during a jump
|
||||
const pauseDurationMs = 116.6666; // Time in ms the bunny pauses between jumps
|
||||
|
||||
const rabbit = "../Seasonals/Resources/easter_images/Osterhase.gif";
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
let animationFrameId;
|
||||
|
||||
// function to check and control the easter
|
||||
function toggleEaster() {
|
||||
const easterContainer = document.querySelector('.easter-container');
|
||||
if (!easterContainer) 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');
|
||||
|
||||
// hide easter if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
easterContainer.style.display = 'none'; // hide easter
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
if (!msgPrinted) {
|
||||
console.log('Easter hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
easterContainer.style.display = 'block'; // show easter
|
||||
if (!animationFrameId) {
|
||||
animateRabbit(); // start animation
|
||||
}
|
||||
if (msgPrinted) {
|
||||
console.log('Easter visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleEaster);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
// Credit: https://flaticon.com
|
||||
const easterEggImages = [
|
||||
"../Seasonals/Resources/easter_images/egg_1.png",
|
||||
"../Seasonals/Resources/easter_images/egg_2.png",
|
||||
"../Seasonals/Resources/easter_images/egg_3.png",
|
||||
@@ -73,121 +27,238 @@ const images = [
|
||||
"../Seasonals/Resources/easter_images/egg_10.png",
|
||||
"../Seasonals/Resources/easter_images/egg_11.png",
|
||||
"../Seasonals/Resources/easter_images/egg_12.png",
|
||||
"../Seasonals/Resources/easter_images/eggs.png"
|
||||
];
|
||||
const rabbit = "../Seasonals/Resources/easter_images/easter-bunny.png";
|
||||
|
||||
function addRandomEaster(count) {
|
||||
const easterContainer = document.querySelector('.easter-container'); // get the leave container
|
||||
if (!easterContainer) return; // exit if leave container is not found
|
||||
|
||||
console.log('Adding random easter eggs');
|
||||
|
||||
// Array of leave characters
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new leave element
|
||||
const eggDiv = document.createElement('div');
|
||||
eggDiv.className = "easter";
|
||||
|
||||
// pick a random easter symbol
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
eggDiv.appendChild(img);
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 12; // delay (0s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
|
||||
// apply styles
|
||||
eggDiv.style.left = `${randomLeft}%`;
|
||||
eggDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
|
||||
// add the leave to the container
|
||||
easterContainer.appendChild(eggDiv);
|
||||
}
|
||||
console.log('Random easter added');
|
||||
}
|
||||
|
||||
function addHoppingRabbit() {
|
||||
if (!bunny) return; // Nur ausführen, wenn Easter aktiviert ist
|
||||
let msgPrinted = false;
|
||||
|
||||
// Check visibility
|
||||
function toggleEaster() {
|
||||
const easterContainer = document.querySelector('.easter-container');
|
||||
if (!easterContainer) return;
|
||||
|
||||
// Hase erstellen
|
||||
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) {
|
||||
easterContainer.style.display = 'none';
|
||||
if (rabbitTimeout) {
|
||||
clearTimeout(rabbitTimeout);
|
||||
isAnimating = false;
|
||||
}
|
||||
if (!msgPrinted) {
|
||||
console.log('Easter hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
easterContainer.style.display = 'block';
|
||||
if (!isAnimating && enableBunny) {
|
||||
animateRabbit(document.querySelector('#rabbit'));
|
||||
}
|
||||
if (msgPrinted) {
|
||||
console.log('Easter visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleEaster);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createEasterGrassAndEggs(container) {
|
||||
let grassContainer = container.querySelector('.easter-grass-container');
|
||||
if (!grassContainer) {
|
||||
grassContainer = document.createElement('div');
|
||||
grassContainer.className = 'easter-grass-container';
|
||||
container.appendChild(grassContainer);
|
||||
}
|
||||
|
||||
grassContainer.innerHTML = '';
|
||||
|
||||
let pathsBg = '';
|
||||
let pathsFg = '';
|
||||
const w = window.innerWidth;
|
||||
const hSVG = 80; // Grass 80px high
|
||||
|
||||
// Generate Grass
|
||||
const bladeCount = w / 5;
|
||||
for (let i = 0; i < bladeCount; i++) {
|
||||
const height = Math.random() * 40 + 20;
|
||||
const x = i * 5 + Math.random() * 3;
|
||||
const hue = 80 + Math.random() * 40; // slightly more yellow-green for spring/easter
|
||||
const color = `hsl(${hue}, 60%, 40%)`;
|
||||
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
|
||||
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
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' : '#8bc34a';
|
||||
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"/>`;
|
||||
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
|
||||
}
|
||||
|
||||
// Generate Flowers
|
||||
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
|
||||
for (let i = 0; i < 40; 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)];
|
||||
|
||||
let path = '';
|
||||
path += `<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;
|
||||
path += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||
path += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||
path += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||
path += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||
path += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
|
||||
|
||||
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
|
||||
}
|
||||
|
||||
grassContainer.innerHTML = `
|
||||
<div class="easter-meadow-layer" style="z-index: 1001;">
|
||||
<svg class="easter-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="easter-sway">
|
||||
${pathsBg}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="easter-meadow-layer" style="z-index: 1003;">
|
||||
<svg class="easter-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="easter-sway" style="animation-delay: -2s;">
|
||||
${pathsFg}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add Easter Eggs
|
||||
for (let i = 0; i < eggCount; i++) {
|
||||
const x = 2 + Math.random() * 96;
|
||||
const y = Math.random() * 18; // 0 to 18px off bottom
|
||||
const imageSrc = easterEggImages[Math.floor(Math.random() * easterEggImages.length)];
|
||||
|
||||
const eggImg = document.createElement('img');
|
||||
eggImg.src = imageSrc;
|
||||
eggImg.style.position = 'absolute';
|
||||
eggImg.style.left = `${x}vw`;
|
||||
eggImg.style.bottom = `${y}px`;
|
||||
eggImg.style.width = `${15 + Math.random() * 10}px`;
|
||||
eggImg.style.height = 'auto';
|
||||
eggImg.style.transform = `rotate(${Math.random() * 60 - 30}deg)`;
|
||||
eggImg.style.zIndex = Math.random() > 0.5 ? '1000' : '1004'; // Between grass layers
|
||||
|
||||
grassContainer.appendChild(eggImg);
|
||||
}
|
||||
}
|
||||
|
||||
let rabbitTimeout;
|
||||
let isAnimating = false;
|
||||
|
||||
function addHoppingRabbit(container) {
|
||||
if (!enableBunny) return;
|
||||
|
||||
const rabbitImg = document.createElement("img");
|
||||
rabbitImg.id = "rabbit";
|
||||
rabbitImg.src = rabbit; // Bildpfad aus der bestehenden Definition
|
||||
rabbitImg.alt = "Hoppelnder Osterhase";
|
||||
rabbitImg.src = rabbit;
|
||||
rabbitImg.alt = "Hopping Easter Bunny";
|
||||
rabbitImg.className = "hopping-rabbit";
|
||||
|
||||
rabbitImg.style.bottom = "-15px";
|
||||
rabbitImg.style.position = "absolute";
|
||||
|
||||
// CSS-Klassen hinzufügen
|
||||
rabbitImg.classList.add("hopping-rabbit");
|
||||
|
||||
easterContainer.appendChild(rabbitImg);
|
||||
|
||||
rabbitImg.style.bottom = (hopHeight / 2 + 6) + "px";
|
||||
container.appendChild(rabbitImg);
|
||||
|
||||
animateRabbit(rabbitImg);
|
||||
}
|
||||
|
||||
function animateRabbit(rabbitElement) {
|
||||
const rabbit = rabbitElement || document.querySelector('#rabbit');
|
||||
if (!rabbit) return;
|
||||
function animateRabbit(rabbit) {
|
||||
if (!rabbit || isAnimating) return;
|
||||
isAnimating = true;
|
||||
|
||||
const startFromLeft = Math.random() >= 0.5;
|
||||
const startX = startFromLeft ? -15 : 115;
|
||||
let currentX = startX;
|
||||
const endX = startFromLeft ? 115 : -15;
|
||||
const direction = startFromLeft ? 1 : -1;
|
||||
|
||||
rabbit.style.transition = 'none';
|
||||
const transformScale = startFromLeft ? 'scaleX(-1)' : '';
|
||||
// Set bounding box center-of-gravity shift when graphic is flipped
|
||||
rabbit.style.transformOrigin = startFromLeft ? '59% 50%' : '50% 50%';
|
||||
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
|
||||
|
||||
const loopDurationMs = jumpDurationMs + pauseDurationMs;
|
||||
|
||||
let startTime = null;
|
||||
|
||||
function animationStep(timestamp) {
|
||||
if (!document.querySelector('.easter-container') || rabbit.style.display === 'none') {
|
||||
isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTime) {
|
||||
startTime = timestamp;
|
||||
|
||||
// random start position and direction
|
||||
const startFromLeft = Math.random() >= 0.5;
|
||||
rabbit.startX = startFromLeft ? -10 : 110;
|
||||
rabbit.endX = startFromLeft ? 110 : -10;
|
||||
rabbit.direction = startFromLeft ? 1 : -1;
|
||||
|
||||
// mirror the rabbit image if it starts from the right
|
||||
rabbit.style.transform = startFromLeft ? '' : 'scaleX(-1)';
|
||||
const currSrc = rabbit.src.split('?')[0];
|
||||
rabbit.src = currSrc + '?t=' + Date.now();
|
||||
}
|
||||
const progress = timestamp - startTime;
|
||||
|
||||
// calculate the horizontal position (linear interpolation)
|
||||
const x = rabbit.startX + (progress / bunnyDuration) * (rabbit.endX - rabbit.startX);
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
const completedLoops = Math.floor(elapsed / loopDurationMs);
|
||||
const timeInCurrentLoop = elapsed % loopDurationMs;
|
||||
|
||||
// calculate the vertical position (sinus curve)
|
||||
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
|
||||
|
||||
// set the new position
|
||||
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`;
|
||||
|
||||
if (progress < bunnyDuration) {
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
// Determine if we are currently jumping or pausing
|
||||
let currentLoopDistance = 0;
|
||||
if (timeInCurrentLoop < jumpDurationMs) {
|
||||
// We are in the jumping phase
|
||||
currentLoopDistance = (timeInCurrentLoop / jumpDurationMs) * jumpDistanceVw;
|
||||
} else {
|
||||
// let the bunny rest for a while before hiding easter eggs again
|
||||
const pauseDuration = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
|
||||
setTimeout(() => {
|
||||
startTime = null;
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
}, pauseDuration);
|
||||
// We are in the paused phase
|
||||
currentLoopDistance = jumpDistanceVw;
|
||||
}
|
||||
|
||||
currentX = startX + (completedLoops * jumpDistanceVw + currentLoopDistance) * direction;
|
||||
|
||||
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
|
||||
|
||||
// Check if finished crossing
|
||||
if ((direction === 1 && currentX >= endX) || (direction === -1 && currentX <= endX)) {
|
||||
let restTime = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
|
||||
|
||||
isAnimating = false;
|
||||
rabbitTimeout = setTimeout(() => {
|
||||
if (!document.body.contains(rabbit)) return;
|
||||
animateRabbit(document.querySelector('#rabbit'));
|
||||
}, restTime);
|
||||
return;
|
||||
}
|
||||
|
||||
rabbitTimeout = requestAnimationFrame(animationStep);
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
// Start loop
|
||||
rabbitTimeout = requestAnimationFrame(animationStep);
|
||||
}
|
||||
|
||||
function initializeEaster() {
|
||||
if (!easter) return;
|
||||
|
||||
// initialize standard easter
|
||||
function initEaster() {
|
||||
const container = document.querySelector('.easter-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.easter-container')) {
|
||||
@@ -196,48 +267,17 @@ function initEaster() {
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// shuffle the easter images
|
||||
let currentIndex = images.length;
|
||||
let randomIndex;
|
||||
while (currentIndex != 0) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[images[currentIndex], images[randomIndex]] = [images[randomIndex], images[currentIndex]];
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const eggDiv = document.createElement("div");
|
||||
eggDiv.className = "easter";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = images[i];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
createEasterGrassAndEggs(container);
|
||||
addHoppingRabbit(container);
|
||||
|
||||
// Add resize listener to regenerate meadow
|
||||
window.addEventListener('resize', () => {
|
||||
if(document.querySelector('.easter-container')) {
|
||||
createEasterGrassAndEggs(container);
|
||||
}
|
||||
});
|
||||
|
||||
eggDiv.appendChild(img);
|
||||
container.appendChild(eggDiv);
|
||||
}
|
||||
|
||||
addHoppingRabbit();
|
||||
}
|
||||
|
||||
|
||||
// initialize easter and add random easter after the DOM is loaded
|
||||
function initializeEaster() {
|
||||
if (!easter) return; // exit if easter are disabled
|
||||
initEaster();
|
||||
toggleEaster();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomEaster && (screenWidth > 768 || randomEasterMobile)) { // add random easter only on larger screens, unless enabled for mobile devices
|
||||
addRandomEaster(easterEggCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
initializeEaster();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/Osterhase.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/Osterhase_1.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB |
56
Jellyfin.Plugin.Seasonals/Web/eid.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.eid-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.eid-symbol {
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.eid-symbol.floating-star {
|
||||
will-change: opacity;
|
||||
opacity: 0;
|
||||
animation: eid-twinkle 4s ease-in-out infinite;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.lantern-rope {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(to bottom, rgba(255, 215, 0, 0.1), rgba(255, 215, 0, 0.6));
|
||||
transform-origin: top center;
|
||||
animation: lantern-swing 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.lantern-emoji {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 2.5em;
|
||||
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5));
|
||||
}
|
||||
|
||||
@keyframes lantern-swing {
|
||||
0% { transform: rotate(-8deg); }
|
||||
100% { transform: rotate(8deg); }
|
||||
}
|
||||
|
||||
@keyframes eid-twinkle {
|
||||
0% { transform: scale(0.8); opacity: 0; text-shadow: 0 0 5px gold; }
|
||||
50% { transform: scale(1.2); opacity: 0.8; text-shadow: 0 0 20px gold; }
|
||||
100% { transform: scale(0.8); opacity: 0; text-shadow: 0 0 5px gold; }
|
||||
}
|
||||
100
Jellyfin.Plugin.Seasonals/Web/eid.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const config = window.SeasonalsPluginConfig?.Eid || {};
|
||||
const eid = config.EnableEid !== undefined ? config.EnableEid : true; // enable/disable eid
|
||||
const lanternCount = config.LanternCount !== undefined ? config.LanternCount : 8; // count of lantern
|
||||
const lanternCountMobile = config.LanternCountMobile !== undefined ? config.LanternCountMobile : 3; // count of lantern on mobile
|
||||
|
||||
const eidSymbols = ['🌙', '⭐', '✨'];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleEid() {
|
||||
const container = document.querySelector('.eid-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('Eid hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Eid visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleEid);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createEid(container) {
|
||||
const starCount = 20;
|
||||
|
||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
let activeLanternCount = isMobile ? lanternCountMobile : lanternCount;
|
||||
|
||||
// Create evenly spaced lanterns
|
||||
const segmentWidth = 100 / activeLanternCount;
|
||||
for (let i = 0; i < activeLanternCount; i++) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'eid-symbol lantern-rope';
|
||||
|
||||
// Base position within segment, with slight random jitter
|
||||
const baseLeft = (i * segmentWidth) + (segmentWidth * 0.2);
|
||||
const jitter = Math.random() * (segmentWidth * 0.6);
|
||||
symbol.style.left = `${baseLeft + jitter}%`;
|
||||
|
||||
symbol.style.animationDelay = `${Math.random() * -4}s`;
|
||||
const ropeLen = 15 + Math.random() * 15; // 15vh to 30vh
|
||||
symbol.style.height = `${ropeLen}vh`;
|
||||
|
||||
const lanternSpan = document.createElement('span');
|
||||
lanternSpan.className = 'lantern-emoji';
|
||||
lanternSpan.textContent = '🏮';
|
||||
symbol.appendChild(lanternSpan);
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
|
||||
// Create random floating stars
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'eid-symbol floating-star';
|
||||
symbol.textContent = eidSymbols[Math.floor(Math.random() * eidSymbols.length)];
|
||||
symbol.style.left = `${Math.random() * 100}%`;
|
||||
symbol.style.top = `${Math.random() * 100}%`;
|
||||
symbol.style.animationDelay = `${Math.random() * -5}s`;
|
||||
|
||||
symbol.addEventListener('animationiteration', () => {
|
||||
symbol.style.left = `${Math.random() * 90 + 5}%`;
|
||||
symbol.style.top = `${Math.random() * 100}%`;
|
||||
});
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeEid() {
|
||||
if (!eid) return;
|
||||
const container = document.querySelector('.eid-container') || document.createElement("div");
|
||||
if (!document.querySelector('.eid-container')) {
|
||||
container.className = "eid-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
createEid(container);
|
||||
toggleEid();
|
||||
}
|
||||
initializeEid();
|
||||
42
Jellyfin.Plugin.Seasonals/Web/eurovision.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.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;
|
||||
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); }
|
||||
}
|
||||
100
Jellyfin.Plugin.Seasonals/Web/eurovision.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const config = window.SeasonalsPluginConfig?.Eurovision || {};
|
||||
|
||||
const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true; // enable/disable eurovision
|
||||
const elementCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of notes
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true; // enable/disable colorful notes
|
||||
const eurovisionColorsStr = config.EurovisionColors !== undefined ? config.EurovisionColors : '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff'; // colors to use
|
||||
const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2; // size of eurovision glow
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleEurovision);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeEurovision() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleEurovision();
|
||||
}
|
||||
|
||||
initializeEurovision();
|
||||
89
Jellyfin.Plugin.Seasonals/Web/filmnoir.css
Normal file
@@ -0,0 +1,89 @@
|
||||
.filmnoir-tint {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
background-color: #8c7355;
|
||||
mix-blend-mode: color;
|
||||
}
|
||||
|
||||
.filmnoir-effects {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.filmnoir-grain {
|
||||
will-change: transform, opacity;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" stitchTiles="stitch"/></filter><rect width="200" height="200" filter="url(%23n)" opacity="0.4"/></svg>');
|
||||
animation: grain-dance 0.2s steps(4) infinite;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
opacity: 0.3;
|
||||
translate: 0 -50vh;
|
||||
}
|
||||
|
||||
/* Vignette */
|
||||
.filmnoir-vignette {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle at center, transparent 50%, rgba(0,0,0,0.8) 120%);
|
||||
box-shadow: inset 0 0 100px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
/* Occasional flicker and scratch */
|
||||
.filmnoir-scratches {
|
||||
will-change: opacity;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to right, transparent 50%, rgba(255,255,255,0.2) 51%, transparent 52%);
|
||||
background-size: 200% 100%;
|
||||
animation: scratch 4s infinite linear, flicker 6s infinite alternate;
|
||||
opacity: 0.2;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
@keyframes grain-dance {
|
||||
0% { transform: translate(0,0); }
|
||||
25% { transform: translate(-5%,-5%); }
|
||||
50% { transform: translate(-10%,5%); }
|
||||
75% { transform: translate(5%,-10%); }
|
||||
100% { transform: translate(0,0); }
|
||||
}
|
||||
|
||||
@keyframes scratch {
|
||||
0% { background-position: -200% 0; }
|
||||
10% { background-position: 200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% { opacity: 0.2; }
|
||||
5% { opacity: 0.1; }
|
||||
10% { opacity: 0.3; }
|
||||
15% { opacity: 0.2; }
|
||||
50% { opacity: 0.15; }
|
||||
55% { opacity: 0.25; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
79
Jellyfin.Plugin.Seasonals/Web/filmnoir.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const config = window.SeasonalsPluginConfig?.FilmNoir || {};
|
||||
const filmnoir = config.EnableFilmNoir !== undefined ? config.EnableFilmNoir : true; // enable/disable filmnoir
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleFilmNoir() {
|
||||
const tint = document.querySelector('.filmnoir-tint');
|
||||
const effects = document.querySelector('.filmnoir-effects');
|
||||
if (!tint || !effects) 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) {
|
||||
tint.style.display = 'none';
|
||||
effects.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('FilmNoir hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
tint.style.display = 'block';
|
||||
effects.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('FilmNoir visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleFilmNoir);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createFilmNoir() {
|
||||
if (!document.querySelector('.filmnoir-tint')) {
|
||||
const tint = document.createElement('div');
|
||||
tint.className = 'filmnoir-tint';
|
||||
tint.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(tint);
|
||||
}
|
||||
|
||||
let effects = document.querySelector('.filmnoir-effects');
|
||||
if (!effects) {
|
||||
effects = document.createElement('div');
|
||||
effects.className = 'filmnoir-effects';
|
||||
effects.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(effects);
|
||||
|
||||
const vignette = document.createElement('div');
|
||||
vignette.className = 'filmnoir-vignette';
|
||||
|
||||
const grain = document.createElement('div');
|
||||
grain.className = 'filmnoir-grain';
|
||||
|
||||
const scratches = document.createElement('div');
|
||||
scratches.className = 'filmnoir-scratches';
|
||||
|
||||
effects.appendChild(grain);
|
||||
effects.appendChild(scratches);
|
||||
effects.appendChild(vignette);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initializeFilmNoir() {
|
||||
if (!filmnoir) return;
|
||||
|
||||
createFilmNoir();
|
||||
toggleFilmNoir();
|
||||
}
|
||||
|
||||
initializeFilmNoir();
|
||||
@@ -7,12 +7,14 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.rocket-trail {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
left: var(--trailX);
|
||||
top: var(--trailStartY);
|
||||
top: 0;
|
||||
width: 4px;
|
||||
|
||||
/* activate the following for rocket trail */
|
||||
@@ -27,6 +29,7 @@
|
||||
box-shadow: 0 0 8px 2px white;*/
|
||||
|
||||
animation: rocket-trail-animation 1s linear forwards;
|
||||
translate: 0 var(--trailStartY);
|
||||
}
|
||||
|
||||
@keyframes rocket-trail-animation {
|
||||
@@ -55,6 +58,7 @@
|
||||
}
|
||||
|
||||
.firework {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
@@ -2,10 +2,10 @@ const config = window.SeasonalsPluginConfig?.Fireworks || {};
|
||||
|
||||
const fireworks = config.EnableFireworks !== undefined ? config.EnableFireworks : true; // enable/disable fireworks
|
||||
const scrollFireworks = config.ScrollFireworks !== undefined ? config.ScrollFireworks : true; // enable fireworks to scroll with page content
|
||||
const particlesPerFirework = config.ParticleCount || 50; // count of particles per firework (Warning: High values may affect performance)
|
||||
const minFireworks = config.MinFireworks || 3; // minimum number of simultaneous fireworks
|
||||
const maxFireworks = config.MaxFireworks || 6; // maximum number of simultaneous fireworks
|
||||
const intervalOfFireworks = config.LaunchInterval || 3200; // interval for the fireworks in milliseconds
|
||||
const particlesPerFirework = config.ParticleCount !== undefined ? config.ParticleCount : 50; // count of particles per firework
|
||||
const minFireworks = config.MinFireworks !== undefined ? config.MinFireworks : 3; // minimum number of simultaneous fireworks
|
||||
const maxFireworks = config.MaxFireworks !== undefined ? config.MaxFireworks : 6; // maximum number of simultaneous fireworks
|
||||
const intervalOfFireworks = config.LaunchInterval !== undefined ? config.LaunchInterval : 3200; // interval for the fireworks in milliseconds
|
||||
|
||||
// array of color palettes for the fireworks
|
||||
const colorPalettes = [
|
||||
@@ -60,9 +60,9 @@ const observer = new MutationObserver(toggleFirework);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ function startFireworks() {
|
||||
}
|
||||
|
||||
fireworksInterval = setInterval(() => {
|
||||
if (!document.body.contains(fireworkContainer)) { clearInterval(fireworksInterval); return; }
|
||||
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
|
||||
for (let i = 0; i < randomCount; i++) {
|
||||
setTimeout(() => {
|
||||
|
||||
34
Jellyfin.Plugin.Seasonals/Web/friday13.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.friday13-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.friday13-cat {
|
||||
position: absolute;
|
||||
width: 150px; /* MARK: Cat size */
|
||||
height: auto;
|
||||
user-select: none;
|
||||
animation-timing-function: linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes cat-walk-right {
|
||||
0% { left: -10vw; transform: scaleX(-1); opacity: 1; }
|
||||
99% { left: 110vw; transform: scaleX(-1); opacity: 1; }
|
||||
100% { opacity: 0; transform: scaleX(-1); left: 110vw; }
|
||||
}
|
||||
|
||||
@keyframes cat-walk-left {
|
||||
0% { left: 110vw; transform: scaleX(1); opacity: 1; }
|
||||
99% { left: -10vw; transform: scaleX(1); opacity: 1; }
|
||||
100% { opacity: 0; transform: scaleX(1); left: -10vw; }
|
||||
}
|
||||
|
||||
83
Jellyfin.Plugin.Seasonals/Web/friday13.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const config = window.SeasonalsPluginConfig?.Friday13 || {};
|
||||
const friday13 = config.EnableFriday13 !== undefined ? config.EnableFriday13 : true; // enable/disable friday13
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleFriday13() {
|
||||
const container = document.querySelector('.friday13-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('Friday13 hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Friday13 visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleFriday13);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createFriday13(container) {
|
||||
function spawnCat() {
|
||||
// MARK: Height of the cat from bottom
|
||||
const catBottomPosition = "-15px";
|
||||
// MARK: Time it takes for the cat to cross the screen
|
||||
const catWalkDurationSeconds = 20;
|
||||
|
||||
const cat = document.createElement('img');
|
||||
cat.className = 'friday13-cat';
|
||||
cat.src = '../Seasonals/Resources/friday_assets/black-cat.gif';
|
||||
cat.style.bottom = catBottomPosition;
|
||||
|
||||
// Either walk left to right or right to left
|
||||
const dir = Math.random() > 0.5 ? 'right' : 'left';
|
||||
cat.style.animationName = `cat-walk-${dir}`;
|
||||
cat.style.animationDuration = `${catWalkDurationSeconds}s`;
|
||||
cat.style.animationIterationCount = `1`; // play once and remove
|
||||
cat.style.animationFillMode = `forwards`;
|
||||
|
||||
container.appendChild(cat);
|
||||
|
||||
// Remove and respawn
|
||||
setTimeout(() => {
|
||||
if (cat.parentNode) {
|
||||
cat.parentNode.removeChild(cat);
|
||||
}
|
||||
// Respawn with random delay between 5 to 25 seconds
|
||||
setTimeout(() => { if (document.body.contains(container)) spawnCat(); }, Math.random() * 20000 + 5000);
|
||||
}, (catWalkDurationSeconds * 1000) + 500); // Wait for duration + 500ms safety margin
|
||||
}
|
||||
|
||||
// Initial spawn with random delay
|
||||
setTimeout(() => { if (document.body.contains(container)) spawnCat(); }, Math.random() * 5000);
|
||||
}
|
||||
|
||||
function initializeFriday13() {
|
||||
if (!friday13) return;
|
||||
const container = document.querySelector('.friday13-container') || document.createElement("div");
|
||||
if (!document.querySelector('.friday13-container')) {
|
||||
container.className = "friday13-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
createFriday13(container);
|
||||
toggleFriday13();
|
||||
}
|
||||
initializeFriday13();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/friday_assets/black-cat.gif
Normal file
|
After Width: | Height: | Size: 88 KiB |
71
Jellyfin.Plugin.Seasonals/Web/frost.css
Normal file
@@ -0,0 +1,71 @@
|
||||
.frost-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.frost-layer {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(ellipse at center, transparent 60%, rgba(180, 220, 255, 0.4) 100%);
|
||||
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
|
||||
|
||||
filter: url('#frost-filter');
|
||||
|
||||
animation: frost-creep 4s ease-out forwards;
|
||||
}
|
||||
|
||||
.frost-crystals {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -5%;
|
||||
width: 110%;
|
||||
height: 110%;
|
||||
background-image:
|
||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><circle cx="10" cy="10" r="1.5" fill="rgba(255,255,255,0.2)"/><circle cx="40" cy="30" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="20" cy="50" r="2" fill="rgba(255,255,255,0.1)"/><path d="M50 10 L51 15 L56 16 L51 17 L50 22 L49 17 L44 16 L49 15 Z" fill="rgba(255,255,255,0.2)"/></svg>'),
|
||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="25" cy="5" r="1.5" fill="rgba(255,255,255,0.1)"/><path d="M20 20 L21 23 L24 24 L21 25 L20 28 L19 25 L16 24 L19 23 Z" fill="rgba(255,255,255,0.15)"/></svg>'),
|
||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><circle cx="15" cy="15" r="1" fill="rgba(255,255,255,0.2)"/><circle cx="5" cy="5" r="0.8" fill="rgba(255,255,255,0.1)"/></svg>');
|
||||
background-repeat: repeat;
|
||||
background-size: 110px 110px, 60px 60px, 30px 30px;
|
||||
background-position: 0 0, 15px 15px, 5px 10px;
|
||||
mix-blend-mode: overlay;
|
||||
mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
|
||||
animation: frost-shimmer 6s infinite alternate ease-in-out;
|
||||
translate: 0 -5vh;
|
||||
}
|
||||
|
||||
@keyframes frost-creep {
|
||||
0% {
|
||||
opacity: 0;
|
||||
box-shadow: inset 0 0 10px rgba(200, 230, 255, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes frost-shimmer {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
75
Jellyfin.Plugin.Seasonals/Web/frost.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const config = window.SeasonalsPluginConfig?.Frost || {};
|
||||
|
||||
const frost = config.EnableFrost !== undefined ? config.EnableFrost : true; // enable/disable frost
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleFrost() {
|
||||
const container = document.querySelector('.frost-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('Frost hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Frost visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleFrost);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createFrost(container) {
|
||||
const frostLayer = document.createElement('div');
|
||||
frostLayer.className = 'frost-layer';
|
||||
|
||||
const frostCrystals = document.createElement('div');
|
||||
frostCrystals.className = 'frost-crystals';
|
||||
|
||||
// An SVG filter to make things look "frozen"/distorted around the edges
|
||||
const svgFilter = document.createElement('div');
|
||||
svgFilter.innerHTML = `
|
||||
<svg style="display:none;">
|
||||
<filter id="frost-filter">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="3" result="noise" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="noise" scale="5" xChannelSelector="R" yChannelSelector="G" />
|
||||
</filter>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
frostLayer.appendChild(frostCrystals);
|
||||
container.appendChild(frostLayer);
|
||||
container.appendChild(svgFilter);
|
||||
}
|
||||
|
||||
function initializeFrost() {
|
||||
if (!frost) return;
|
||||
|
||||
const container = document.querySelector('.frost-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.frost-container')) {
|
||||
container.className = "frost-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
createFrost(container);
|
||||
}
|
||||
|
||||
initializeFrost();
|
||||
@@ -8,23 +8,16 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.halloween {
|
||||
will-change: transform;
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
z-index: 15;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: halloween-fall, halloween-shake;
|
||||
-webkit-animation-duration: 10s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
-webkit-animation-play-state: running, running;
|
||||
animation-name: halloween-fall, halloween-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
@@ -32,37 +25,15 @@
|
||||
animation-play-state: running, running
|
||||
}
|
||||
|
||||
@-webkit-keyframes halloween-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes halloween-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes halloween-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
bottom: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
bottom: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,74 +49,117 @@
|
||||
}
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
/* --- Fog Layer --- */
|
||||
.halloween-fog-layer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(to top, black, transparent);
|
||||
}
|
||||
.halloween-fog-blob {
|
||||
position: absolute;
|
||||
bottom: -10vh;
|
||||
width: 150vw;
|
||||
height: 50vh;
|
||||
background: radial-gradient(ellipse at center, rgba(120, 130, 140, 0.4) 0%, transparent 60%);
|
||||
border-radius: 50%;
|
||||
filter: blur(15px);
|
||||
}
|
||||
.halloween-fog-blob:nth-child(1) {
|
||||
will-change: transform;
|
||||
left: -20vw;
|
||||
animation: fog-float1 25s ease-in-out infinite alternate;
|
||||
}
|
||||
.halloween-fog-blob:nth-child(2) {
|
||||
will-change: transform;
|
||||
left: -50vw;
|
||||
background: radial-gradient(ellipse at center, rgba(100, 110, 120, 0.3) 0%, transparent 65%);
|
||||
animation: fog-float2 35s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes fog-float1 {
|
||||
0% { transform: translateX(0) scale(1); opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(20vw) scale(1.1); opacity: 0.6; }
|
||||
}
|
||||
@keyframes fog-float2 {
|
||||
0% { transform: translateX(0) scale(1.1); opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(30vw) scale(1); opacity: 0.5; }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
/* --- Spiders --- */
|
||||
.halloween-spider-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1002;
|
||||
transform-origin: top;
|
||||
will-change: transform;
|
||||
pointer-events: auto;
|
||||
padding: 20px; /* Increase hit area */
|
||||
translate: 0 -50px;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
.halloween-thread {
|
||||
width: 30px; /* Wider hit area for mouse interaction */
|
||||
height: 100vh;
|
||||
margin-top: -100vh;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-webkit-animation-delay: 4s, 2s;
|
||||
animation-delay: 4s, 2s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(4) {
|
||||
left: 40%;
|
||||
-webkit-animation-delay: 2s, 2s;
|
||||
animation-delay: 2s, 2s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(5) {
|
||||
.halloween-thread::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, rgba(200, 200, 200, 0.1), rgba(200, 200, 200, 0.6));
|
||||
}
|
||||
.halloween-spider {
|
||||
will-change: transform;
|
||||
animation: spider-swing 3s ease-in-out infinite alternate;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
/* MARK: SPIDER SWAY CONFIGURATION */
|
||||
@keyframes wind-sway {
|
||||
0% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(2deg); }
|
||||
75% { transform: rotate(-2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
@keyframes spider-drop {
|
||||
0% { transform: translateY(-50px); }
|
||||
30% { transform: translateY(var(--drop-height, 50vh)); }
|
||||
60% { transform: translateY(var(--drop-height, 50vh)); }
|
||||
100% { transform: translateY(-50px); }
|
||||
}
|
||||
@keyframes spider-swing {
|
||||
0% { transform: rotate(-10deg); }
|
||||
100% { transform: rotate(10deg); }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
/* Mice */
|
||||
.halloween-mouse {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
will-change: left;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
@keyframes mouse-run-right {
|
||||
0% { left: -10vw; }
|
||||
100% { left: 110vw; }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 2s, 0s;
|
||||
animation-delay: 2s, 0s
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(11) {
|
||||
left: 65%;
|
||||
-webkit-animation-delay: 4s, 2.5s;
|
||||
animation-delay: 4s, 2.5s
|
||||
@keyframes mouse-run-left {
|
||||
0% { left: 110vw; }
|
||||
100% { left: -10vw; }
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
const config = window.SeasonalsPluginConfig?.Halloween || {};
|
||||
|
||||
const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween : true; // enable/disable halloween
|
||||
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
|
||||
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true; // enable/disable spiders
|
||||
const enableMice = config.EnableMice !== undefined ? config.EnableMice : true; // enable/disable mice
|
||||
const halloweenCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
|
||||
const halloweenCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbols on mobile
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
const images = [
|
||||
"../Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// function to check and control the halloween
|
||||
function toggleHalloween() {
|
||||
@@ -34,47 +41,40 @@ function toggleHalloween() {
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleHalloween);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"../Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
];
|
||||
|
||||
function addRandomSymbols(count) {
|
||||
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
|
||||
if (!halloweenContainer) return; // exit if halloween container is not found
|
||||
|
||||
console.log('Adding random halloween symbols');
|
||||
function initHalloween(count) {
|
||||
let halloweenContainer = document.querySelector('.halloween-container');
|
||||
if (!halloweenContainer) {
|
||||
halloweenContainer = document.createElement("div");
|
||||
halloweenContainer.className = "halloween-container";
|
||||
halloweenContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(halloweenContainer);
|
||||
}
|
||||
|
||||
console.log('Adding halloween symbols');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new halloween elements
|
||||
const halloweenDiv = document.createElement("div");
|
||||
halloweenDiv.className = "halloween";
|
||||
|
||||
// pick a random halloween symbol
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
halloweenDiv.appendChild(img);
|
||||
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s)
|
||||
const randomAnimationDelay2 = Math.random() * 3; // delay (0s to 3s)
|
||||
// Display directly symbols on full screen (below) or let it build up (above)
|
||||
// const randomAnimationDelay = -(Math.random() * 10); // delay (-10s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s)
|
||||
|
||||
// apply styles
|
||||
halloweenDiv.style.left = `${randomLeft}%`;
|
||||
@@ -87,52 +87,149 @@ function addRandomSymbols(count) {
|
||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// add the halloween to the container
|
||||
halloweenContainer.appendChild(halloweenDiv);
|
||||
}
|
||||
console.log('Random halloween symbols added');
|
||||
console.log('Halloween symbols added');
|
||||
}
|
||||
|
||||
// create halloween objects
|
||||
function createHalloween() {
|
||||
const container = document.querySelector('.halloween-container') || document.createElement("div");
|
||||
// create fog layer
|
||||
function createFog(container) {
|
||||
const fogContainer = document.createElement('div');
|
||||
fogContainer.className = 'halloween-fog-layer';
|
||||
|
||||
const fog1 = document.createElement('div');
|
||||
fog1.className = 'halloween-fog-blob';
|
||||
|
||||
const fog2 = document.createElement('div');
|
||||
fog2.className = 'halloween-fog-blob';
|
||||
|
||||
fogContainer.appendChild(fog1);
|
||||
fogContainer.appendChild(fog2);
|
||||
container.appendChild(fogContainer);
|
||||
}
|
||||
|
||||
if (!document.querySelector('.halloween-container')) {
|
||||
container.className = "halloween-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
images.forEach(imageSrc => {
|
||||
const halloweenDiv = document.createElement("div");
|
||||
halloweenDiv.className = "halloween";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
halloweenDiv.appendChild(img);
|
||||
container.appendChild(halloweenDiv);
|
||||
// create dropping spiders
|
||||
function createSpider(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'halloween-spider-wrapper';
|
||||
|
||||
wrapper.innerHTML = `
|
||||
<div class="halloween-sway" style="display:flex; flex-direction:column; align-items:center; transform-origin: 50% -100vh;">
|
||||
<div class="halloween-thread"></div>
|
||||
<svg class="halloween-spider" viewBox="0 0 24 24" width="30" height="30">
|
||||
<circle cx="12" cy="12" r="6" fill="#1a1a1a"/>
|
||||
<!-- left legs -->
|
||||
<path d="M12 12 l-8 -4 M12 12 l-9 0 M12 12 l-8 4 M12 12 l-6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<!-- right legs -->
|
||||
<path d="M12 12 l8 -4 M12 12 l9 0 M12 12 l8 4 M12 12 l6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<circle cx="10" cy="14" r="1.5" fill="#ff3333"/>
|
||||
<circle cx="14" cy="14" r="1.5" fill="#ff3333"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
wrapper.style.left = `${10 + Math.random() * 80}%`;
|
||||
const dropHeight = 30 + Math.random() * 50; // 30vh to 80vh
|
||||
wrapper.style.setProperty('--drop-height', `${dropHeight}vh`);
|
||||
|
||||
const duration = Math.random() * 6 + 6; // 6-12s drop
|
||||
wrapper.style.animation = `spider-drop ${duration}s ease-in-out forwards`;
|
||||
|
||||
// Start the sway animation only after the drop completes (30% of total duration)
|
||||
const sway = wrapper.querySelector('.halloween-sway');
|
||||
sway.style.animation = `wind-sway 8s ease-in-out ${duration * 0.3}s infinite`;
|
||||
|
||||
// Spider retreat logic
|
||||
let isRetreating = false;
|
||||
wrapper.addEventListener('mouseenter', () => {
|
||||
if (isRetreating) return;
|
||||
isRetreating = true;
|
||||
// Retreat smoothly by pushing margin up
|
||||
wrapper.style.transition = 'margin-top 0.4s ease-in';
|
||||
wrapper.style.marginTop = '-100vh';
|
||||
|
||||
setTimeout(() => {
|
||||
wrapper.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.addEventListener('animationend', () => {
|
||||
if (isRetreating) return;
|
||||
wrapper.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// create scurrying mice
|
||||
function createMouse(container) {
|
||||
const mouse = document.createElement('div');
|
||||
mouse.className = 'halloween-mouse';
|
||||
mouse.innerHTML = `
|
||||
<svg viewBox="0 0 30 15" width="40" height="20">
|
||||
<ellipse cx="15" cy="10" rx="10" ry="5" fill="#111"/>
|
||||
<circle cx="24" cy="10" r="4" fill="#111"/>
|
||||
<circle cx="24" cy="6" r="3" fill="#333"/>
|
||||
<path d="M 5 10 Q 0 10 0 2" stroke="#111" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
const duration = Math.random() * 3 + 2; // 2-5s run (fast)
|
||||
|
||||
if (direction === 'right') {
|
||||
mouse.style.animation = `mouse-run-right ${duration}s linear forwards`;
|
||||
mouse.style.transform = 'scaleX(1)';
|
||||
} else {
|
||||
mouse.style.animation = `mouse-run-left ${duration}s linear forwards`;
|
||||
mouse.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
mouse.style.bottom = `5px`; // Fixated bottom edge
|
||||
|
||||
mouse.addEventListener('animationend', () => {
|
||||
mouse.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createMouse(container), Math.random() * 4000 + 2000);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(mouse);
|
||||
}
|
||||
|
||||
// initialize halloween
|
||||
function initializeHalloween() {
|
||||
if (!halloween) return; // exit if halloween is disabled
|
||||
createHalloween();
|
||||
if (!halloween) return;
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? halloweenCount : halloweenCountMobile;
|
||||
|
||||
initHalloween(count);
|
||||
toggleHalloween();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
||||
addRandomSymbols(halloweenCount);
|
||||
const container = document.querySelector('.halloween-container');
|
||||
|
||||
if (container) {
|
||||
createFog(container);
|
||||
|
||||
// Add a few spiders
|
||||
if (enableSpiders) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a few mice
|
||||
if (enableMice) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => createMouse(container), Math.random() * 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,59 +8,33 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.heart {
|
||||
will-change: transform;
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
z-index: 15;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: heart-fall, heart-shake;
|
||||
-webkit-animation-duration: 14s, 5s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: heart-fall, heart-shake;
|
||||
animation-duration: 14s, 5s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes heart-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes heart-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heart-fall {
|
||||
0% {
|
||||
bottom: -10%
|
||||
bottom: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
bottom: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,75 +49,3 @@
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
.heart:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-webkit-animation-delay: 4s, 2s;
|
||||
animation-delay: 4s, 2s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(4) {
|
||||
left: 40%;
|
||||
-webkit-animation-delay: 2s, 2s;
|
||||
animation-delay: 2s, 2s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(5) {
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 2s, 0s;
|
||||
animation-delay: 2s, 0s
|
||||
}
|
||||
|
||||
.heart:nth-of-type(11) {
|
||||
left: 65%;
|
||||
-webkit-animation-delay: 4s, 2.5s;
|
||||
animation-delay: 4s, 2.5s
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
const config = window.SeasonalsPluginConfig?.Hearts || {};
|
||||
|
||||
const hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
|
||||
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols
|
||||
const heartsCount = config.SymbolCount || 25; // count of random extra symbols
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const heartsCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||
const heartsCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
|
||||
|
||||
// Array of hearts characters
|
||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -36,24 +38,23 @@ function toggleHearts() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleHearts);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
// Array of hearts characters
|
||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||
function initHearts(count) {
|
||||
let heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
||||
if (!heartsContainer) {
|
||||
heartsContainer = document.createElement("div");
|
||||
heartsContainer.className = "hearts-container";
|
||||
heartsContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(heartsContainer);
|
||||
}
|
||||
|
||||
|
||||
function addRandomSymbols(count) {
|
||||
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
||||
if (!heartsContainer) return; // exit if hearts container is not found
|
||||
|
||||
console.log('Adding random heart symbols');
|
||||
console.log('Adding heart symbols');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new hearts elements
|
||||
@@ -66,8 +67,8 @@ function addRandomSymbols(count) {
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 14; // delay (0s to 14s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
heartsDiv.style.left = `${randomLeft}%`;
|
||||
@@ -83,46 +84,18 @@ function addRandomSymbols(count) {
|
||||
// add the hearts to the container
|
||||
heartsContainer.appendChild(heartsDiv);
|
||||
}
|
||||
console.log('Random hearts symbols added');
|
||||
console.log('Heart symbols added');
|
||||
}
|
||||
|
||||
// create hearts objects
|
||||
function createHearts() {
|
||||
const container = document.querySelector('.hearts-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.hearts-container')) {
|
||||
container.className = "hearts-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const heartsDiv = document.createElement("div");
|
||||
heartsDiv.className = "heart";
|
||||
heartsDiv.textContent = heartSymbols[i % heartSymbols.length];
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 16 + 12; // delay (12s to 16s)
|
||||
const randomAnimationDuration2 = Math.random() * 7 + 3; // delay (3s to 7s)
|
||||
heartsDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
container.appendChild(heartsDiv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// initialize hearts
|
||||
function initializeHearts() {
|
||||
if (!hearts) return; // exit if hearts is disabled
|
||||
createHearts();
|
||||
toggleHearts();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random heartss only on larger screens, unless enabled for mobile devices
|
||||
addRandomSymbols(heartsCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? heartsCount : heartsCountMobile;
|
||||
|
||||
initHearts(count);
|
||||
toggleHearts();
|
||||
}
|
||||
|
||||
initializeHearts();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/mario-running.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/mario.gif
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/toad.gif
Normal file
|
After Width: | Height: | Size: 40 KiB |
80
Jellyfin.Plugin.Seasonals/Web/marioday.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.marioday-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mario-wrapper {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: -100px;
|
||||
animation: mario-run 15s linear infinite;
|
||||
will-change: left, transform;
|
||||
}
|
||||
|
||||
.mario-runner {
|
||||
width: 64px;
|
||||
height: auto;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
display: block;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.mario-jump {
|
||||
will-change: transform;
|
||||
animation: jump-arc 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
/* 8-bit coin styling */
|
||||
.mario-coin {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #ffd700;
|
||||
border: 4px solid #b8860b;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 4px 4px 0 #fffbea, inset -4px -4px 0 #daa520;
|
||||
animation: pop-up-arc 2s forwards;
|
||||
}
|
||||
|
||||
.mario-coin::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10px;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
background: #daa520;
|
||||
translate: 0 6px;
|
||||
}
|
||||
|
||||
@keyframes mario-run {
|
||||
0% { left: -100px; transform: scaleX(1); }
|
||||
45% { left: 110vw; transform: scaleX(1); }
|
||||
50% { left: 110vw; transform: scaleX(-1); }
|
||||
95% { left: -100px; transform: scaleX(-1); }
|
||||
100% { left: -100px; transform: scaleX(1); }
|
||||
}
|
||||
|
||||
@keyframes pop-up-arc {
|
||||
0% { transform: translateY(0) rotateY(0deg); opacity: 0; animation-timing-function: ease-out; }
|
||||
20% { opacity: 1; }
|
||||
50% { transform: translateY(-30vh) rotateY(360deg); opacity: 1; animation-timing-function: ease-in; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(20vh) rotateY(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes jump-arc {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-25vh); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
109
Jellyfin.Plugin.Seasonals/Web/marioday.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const config = window.SeasonalsPluginConfig?.MarioDay || {};
|
||||
const marioday = config.EnableMarioDay !== undefined ? config.EnableMarioDay : true; // enable/disable marioday
|
||||
const letMarioJump = config.LetMarioJump !== undefined ? config.LetMarioJump : true; // optionally let mario jump occasionally
|
||||
|
||||
// Credit: https://gifs.alphacoders.com/gifs/view/2585
|
||||
const marioImage = '../Seasonals/Resources/mario_assets/mario.gif';
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleMarioDay() {
|
||||
const container = document.querySelector('.marioday-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('MarioDay hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('MarioDay visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleMarioDay);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createMarioDay(container) {
|
||||
// MARK: Mario's running speed across the screen
|
||||
const marioSpeedSeconds = 18;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mario-wrapper';
|
||||
wrapper.style.animationDuration = `${marioSpeedSeconds}s`;
|
||||
|
||||
const mario = document.createElement('img');
|
||||
mario.className = 'mario-runner';
|
||||
mario.src = marioImage;
|
||||
|
||||
wrapper.appendChild(mario);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
let jumpCount = 0;
|
||||
let maxJumpsForThisRun = Math.floor(Math.random() * 3); // 0, 1, or 2
|
||||
|
||||
const resetJumpInterval = setInterval(() => {
|
||||
if (!document.body.contains(container)) { clearInterval(resetJumpInterval); return; }
|
||||
jumpCount = 0;
|
||||
maxJumpsForThisRun = Math.floor(Math.random() * 3); // Randomize jumps for the next pass
|
||||
}, (marioSpeedSeconds / 2) * 1000);
|
||||
|
||||
// Periodically throw out an 8-bit coin
|
||||
const intervalId = setInterval(() => {
|
||||
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
|
||||
if (container.style.display === 'none') return;
|
||||
|
||||
const marioRect = wrapper.getBoundingClientRect();
|
||||
if (marioRect.left < 0 || marioRect.right > window.innerWidth) return;
|
||||
|
||||
if (letMarioJump && !mario.classList.contains('mario-jump') && jumpCount < maxJumpsForThisRun) {
|
||||
mario.classList.add('mario-jump');
|
||||
jumpCount++;
|
||||
setTimeout(() => mario.classList.remove('mario-jump'), 800);
|
||||
}
|
||||
|
||||
const coin = document.createElement('div');
|
||||
coin.className = 'mario-coin';
|
||||
|
||||
// Grab Mario's current screen position to lock the coin's X coordinate
|
||||
coin.style.left = `${marioRect.left + 16}px`;
|
||||
coin.style.bottom = '35px'; // bottom offset
|
||||
|
||||
container.appendChild(coin);
|
||||
setTimeout(() => coin.remove(), 2000);
|
||||
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
|
||||
function initializeMarioDay() {
|
||||
if (!marioday) return;
|
||||
|
||||
const container = document.querySelector('.marioday-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.marioday-container')) {
|
||||
container.className = "marioday-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
createMarioDay(container);
|
||||
toggleMarioDay();
|
||||
}
|
||||
|
||||
initializeMarioDay();
|
||||
11
Jellyfin.Plugin.Seasonals/Web/matrix.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.matrix-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
165
Jellyfin.Plugin.Seasonals/Web/matrix.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const config = window.SeasonalsPluginConfig?.Matrix || {};
|
||||
|
||||
const enabled = config.EnableMatrix !== undefined ? config.EnableMatrix : true; // enable/disable matrix
|
||||
const maxTrails = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of max trails on screen
|
||||
const backgroundMode = config.EnableMatrixBackground !== undefined ? config.EnableMatrixBackground : false; // enable/disable matrix as background
|
||||
const matrixChars = config.MatrixChars !== undefined ? config.MatrixChars : '0123456789'; // characters to use in the matrix rain, default is '0123456789'
|
||||
|
||||
let msgPrinted = false;
|
||||
let isHidden = false;
|
||||
|
||||
// Toggle Function
|
||||
function toggleMatrix() {
|
||||
const container = document.querySelector('.matrix-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('Matrix hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isHidden) {
|
||||
container.style.display = 'block';
|
||||
isHidden = false;
|
||||
if (msgPrinted) {
|
||||
console.log('Matrix visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleMatrix);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createElements() {
|
||||
const container = document.querySelector('.matrix-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.matrix-container')) {
|
||||
container.className = 'matrix-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
if (backgroundMode) container.style.zIndex = '5';
|
||||
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 = matrixChars.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 (!document.body.contains(container)) { clearInterval(window.matrixInterval); return; }
|
||||
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.matrixInterval) clearInterval(window.matrixInterval);
|
||||
window.matrixInterval = setInterval(loop, 50);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
});
|
||||
}
|
||||
|
||||
function initializeMatrix() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleMatrix();
|
||||
}
|
||||
|
||||
initializeMatrix();
|
||||
35
Jellyfin.Plugin.Seasonals/Web/oktoberfest.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.oktoberfest-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.oktoberfest-symbol {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-size: 2.2em;
|
||||
user-select: none;
|
||||
animation-name: oktoberfest-fall, oktoberfest-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
@keyframes oktoberfest-fall {
|
||||
0% { transform: translateY(0); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
100% { transform: translateY(120vh); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes oktoberfest-sway {
|
||||
0%, 100% { margin-left: 0; }
|
||||
50% { margin-left: 50px; }
|
||||
}
|
||||
75
Jellyfin.Plugin.Seasonals/Web/oktoberfest.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const config = window.SeasonalsPluginConfig?.Oktoberfest || {};
|
||||
const oktoberfest = config.EnableOktoberfest !== undefined ? config.EnableOktoberfest : true; // enable/disable oktoberfest
|
||||
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
|
||||
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbols on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
|
||||
const oktoberfestSymbols = ['🥨', '🍺', '🍻', '🥨', '🥨'];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// function to check and control the oktoberfest
|
||||
function toggleOktoberfest() {
|
||||
const oktoberfestContainer = document.querySelector('.oktoberfest-container');
|
||||
if (!oktoberfestContainer) 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');
|
||||
|
||||
// hide oktoberfest if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
oktoberfestContainer.style.display = 'none'; // hide oktoberfest
|
||||
if (!msgPrinted) {
|
||||
console.log('Oktoberfest hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
oktoberfestContainer.style.display = 'block'; // show oktoberfest
|
||||
if (msgPrinted) {
|
||||
console.log('Oktoberfest visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleOktoberfest);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createOktoberfest(container, count) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'oktoberfest-symbol';
|
||||
symbol.textContent = oktoberfestSymbols[Math.floor(Math.random() * oktoberfestSymbols.length)];
|
||||
symbol.style.left = `${Math.random() * 100}%`;
|
||||
symbol.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 5}s`;
|
||||
const duration1 = Math.random() * 5 + 8;
|
||||
const duration2 = Math.random() * 3 + 3;
|
||||
if (enableDifferentDuration) {
|
||||
symbol.style.animationDuration = `${duration1}s, ${duration2}s`;
|
||||
}
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeOktoberfest() {
|
||||
if (!oktoberfest) return;
|
||||
const container = document.querySelector('.oktoberfest-container') || document.createElement("div");
|
||||
if (!document.querySelector('.oktoberfest-container')) {
|
||||
container.className = "oktoberfest-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? symbolCount : symbolCountMobile;
|
||||
|
||||
createOktoberfest(container, count);
|
||||
}
|
||||
initializeOktoberfest();
|
||||
142
Jellyfin.Plugin.Seasonals/Web/olympia.css
Normal file
@@ -0,0 +1,142 @@
|
||||
.olympia-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.olympia-symbol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.2);
|
||||
z-index: 40;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
.olympia-flame {
|
||||
position: absolute;
|
||||
bottom: 0vh;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.olympia-ring-css {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.olympia-ring-css::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
translate: -50% -50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 5px solid #0081C8; /* Default blue ring */
|
||||
border-radius: 50%;
|
||||
}
|
||||
.olympia-ring-css[style*="--ring-color"]::before {
|
||||
border-color: var(--ring-color);
|
||||
}
|
||||
.olympia-symbol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.2);
|
||||
z-index: 40;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
.olympia-inner {
|
||||
will-change: transform;
|
||||
display: inline-block;
|
||||
animation: olympia-sway linear infinite alternate;
|
||||
}
|
||||
|
||||
.olympia-symbol img {
|
||||
width: 6vh;
|
||||
height: auto;
|
||||
max-width: 60px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.olympia-confetti-wrapper {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
animation-name: olympia-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.olympia-confetti-sway {
|
||||
will-change: transform;
|
||||
animation-name: olympia-confetti-sway;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
@keyframes olympia-confetti-sway {
|
||||
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
|
||||
100% { transform: translateX(var(--sway-amount, 50px)); }
|
||||
}
|
||||
|
||||
.olympia-confetti {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
will-change: transform;
|
||||
animation-name: olympia-confetti-flutter;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.olympia-confetti.circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.olympia-confetti.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.olympia-confetti.triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes olympia-fall {
|
||||
0% { transform: translateY(-10vh); }
|
||||
100% { transform: translateY(110vh); }
|
||||
}
|
||||
|
||||
@keyframes olympia-sway {
|
||||
0% { transform: rotate(-25deg) translateX(-20px); }
|
||||
100% { transform: rotate(25deg) translateX(20px); }
|
||||
}
|
||||
|
||||
@keyframes olympia-tumble-3d {
|
||||
0% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 0deg); }
|
||||
100% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 360deg); }
|
||||
}
|
||||
|
||||
@keyframes olympia-confetti-flutter {
|
||||
0% {
|
||||
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
|
||||
}
|
||||
}
|
||||
263
Jellyfin.Plugin.Seasonals/Web/olympia.js
Normal file
@@ -0,0 +1,263 @@
|
||||
const config = window.SeasonalsPluginConfig?.Olympia || {};
|
||||
|
||||
const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true; // enable/disable olympia theme
|
||||
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of floating symbols
|
||||
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of floating symbols on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
|
||||
// Olympic Ring Colors (Carnival Config)
|
||||
const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E'];
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const confettiCount = isMobile ? 30 : 60;
|
||||
|
||||
/**
|
||||
* Credits:
|
||||
* https://lottiefiles.com/free-animation/gold-coin-5Spp5kJbLP
|
||||
* https://lottiefiles.com/free-animation/silver-coin-SIgIP59fII
|
||||
* https://lottiefiles.com/free-animation/bronze-coin-wWVCJMsOUq
|
||||
*/
|
||||
const olympicMedals = [
|
||||
"../Seasonals/Resources/olympic_assets/gold_coin.gif",
|
||||
"../Seasonals/Resources/olympic_assets/silver_coin.gif",
|
||||
"../Seasonals/Resources/olympic_assets/bronze_coin.gif"
|
||||
]
|
||||
|
||||
/**
|
||||
* Credits:
|
||||
* https://www.flaticon.com/de/kostenloses-icon/fackel_4683293
|
||||
* merged with:
|
||||
* https://lottiefiles.com/free-animation/abstract-flames-lottie-json-animation-oSb0IFoBrj
|
||||
*/
|
||||
const olympicTorch = "../Seasonals/Resources/olympic_assets/torch.gif";
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleOlympia() {
|
||||
const container = document.querySelector('.olympia-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('Olympia hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Olympia visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleOlympia);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createOlympia() {
|
||||
const container = document.querySelector('.olympia-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.olympia-container')) {
|
||||
container.className = 'olympia-container';
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const standardCount = 15;
|
||||
|
||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
let finalCount = isMobile ? symbolCountMobile : symbolCount;
|
||||
|
||||
const useRandomDuration = enableDifferentDuration !== false;
|
||||
|
||||
const olympicRings = ['ring_blue.css', 'ring_yellow.css', 'ring_black.css', 'ring_green.css', 'ring_red.css'];
|
||||
const activeItems = [...olympicMedals, ...olympicRings];
|
||||
|
||||
for (let i = 0; i < finalCount; i++) {
|
||||
let symbol = document.createElement('div');
|
||||
|
||||
const randomImgUrl = activeItems[Math.floor(Math.random() * activeItems.length)];
|
||||
const isRing = randomImgUrl.includes('ring_');
|
||||
const isMedal = randomImgUrl.includes('_coin');
|
||||
|
||||
symbol.className = `olympia-symbol`;
|
||||
|
||||
// Create inner div for sway/rotation
|
||||
let innerDiv = document.createElement('div');
|
||||
innerDiv.className = 'olympia-inner';
|
||||
let img = null;
|
||||
|
||||
if (isRing) {
|
||||
const colorName = randomImgUrl.split('ring_')[1].split('.')[0];
|
||||
const ringColorMap = {
|
||||
'blue': '#0081C8',
|
||||
'yellow': '#FCB131',
|
||||
'black': '#000000',
|
||||
'green': '#00A651',
|
||||
'red': '#EE334E'
|
||||
};
|
||||
let ringDiv = document.createElement('div');
|
||||
ringDiv.className = 'olympia-ring-css';
|
||||
ringDiv.style.setProperty('--ring-color', ringColorMap[colorName]);
|
||||
innerDiv.appendChild(ringDiv);
|
||||
|
||||
// Add a 3D flip animation for rings
|
||||
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
|
||||
innerDiv.style.animation = `olympia-tumble-3d ${Math.random() * 4 + 4}s linear infinite ${spinReverse}`;
|
||||
|
||||
// Random 3D Rotation Axis for Tumbling
|
||||
innerDiv.style.setProperty('--rot-x', (Math.random() * 2 - 1).toFixed(2));
|
||||
innerDiv.style.setProperty('--rot-y', (Math.random() * 2 - 1).toFixed(2));
|
||||
innerDiv.style.setProperty('--rot-z', (Math.random() * 2 - 1).toFixed(2));
|
||||
} else {
|
||||
img = document.createElement('img');
|
||||
img.src = randomImgUrl;
|
||||
img.onerror = function() {
|
||||
symbol.remove();
|
||||
};
|
||||
innerDiv.appendChild(img);
|
||||
|
||||
if (isMedal) {
|
||||
innerDiv.style.animation = `olympia-flip-3d ${Math.random() * 4 + 3}s linear infinite`;
|
||||
} else {
|
||||
// Torch sways, medals flip
|
||||
const swayDur = Math.random() * 2 + 2; // 2 to 4s
|
||||
const swayDir = Math.random() > 0.5 ? 'normal' : 'reverse';
|
||||
innerDiv.style.animation = `olympia-sway ${swayDur}s ease-in-out infinite alternate ${swayDir}`;
|
||||
}
|
||||
}
|
||||
|
||||
symbol.appendChild(innerDiv);
|
||||
|
||||
const leftPos = Math.random() * 95;
|
||||
const delaySeconds = Math.random() * 10;
|
||||
|
||||
// Depth logic for medals and rings
|
||||
const depth = Math.random();
|
||||
const scale = 0.8 + depth * 0.4; // 0.8 to 1.2
|
||||
const zIndex = Math.floor(depth * 30) + 10;
|
||||
|
||||
if (img) {
|
||||
img.style.transform = `scale(${scale})`;
|
||||
} else {
|
||||
innerDiv.firstChild.style.transform = `scale(${scale})`;
|
||||
}
|
||||
symbol.style.zIndex = zIndex;
|
||||
|
||||
let durationSeconds = 8;
|
||||
if (useRandomDuration) {
|
||||
durationSeconds = (1 - depth) * 5 + 6 + Math.random() * 4;
|
||||
}
|
||||
|
||||
symbol.style.animation = `olympia-fall ${durationSeconds}s linear infinite`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
symbol.style.left = `${leftPos}vw`;
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
|
||||
// Olympic Torches (Fixed at bottom corners, symmetrically rotated inward)
|
||||
// Generate one random inward rotation (10 to 25 deg) for both to share
|
||||
const sharedTilt = Math.random() * 15 + 10;
|
||||
|
||||
const createTorch = (isLeft) => {
|
||||
const torch = document.createElement('div');
|
||||
torch.className = 'olympia-flame';
|
||||
|
||||
if (isLeft) {
|
||||
torch.style.left = '5vw';
|
||||
// Lean right, face normal
|
||||
torch.style.transform = `rotate(${sharedTilt}deg) scaleX(1)`;
|
||||
} else {
|
||||
torch.style.right = '5vw';
|
||||
// Lean left, mirror image
|
||||
torch.style.transform = `rotate(-${sharedTilt}deg) scaleX(-1)`;
|
||||
}
|
||||
|
||||
let torchImg = document.createElement('img');
|
||||
torchImg.src = `../Seasonals/Resources/olympic_assets/torch.gif`;
|
||||
torchImg.style.height = '25vh';
|
||||
torchImg.style.objectFit = 'contain';
|
||||
torchImg.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
};
|
||||
torch.appendChild(torchImg);
|
||||
container.appendChild(torch);
|
||||
};
|
||||
|
||||
createTorch(true);
|
||||
createTorch(false);
|
||||
|
||||
for (let i = 0; i < confettiCount; i++) {
|
||||
let wrapper = document.createElement('div');
|
||||
wrapper.className = 'olympia-confetti-wrapper';
|
||||
|
||||
let leftPos = Math.random() * 100;
|
||||
wrapper.style.left = `${leftPos}vw`;
|
||||
|
||||
let fallDuration = Math.random() * 3 + 4; // 4 to 7 seconds to fall
|
||||
wrapper.style.animationDuration = `${fallDuration}s`;
|
||||
wrapper.style.animationDelay = `-${Math.random() * fallDuration}s`; // Negative delay so it distributes perfectly immediately
|
||||
|
||||
let swayWrapper = document.createElement('div');
|
||||
swayWrapper.className = 'olympia-confetti-sway';
|
||||
let swayDuration = Math.random() * 2 + 1.5; // 1.5s to 3.5s
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
let swayAmount = Math.random() * 30 + 30; // 30px to 60px
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount}px`);
|
||||
let initSwayDelay = Math.random() * swayDuration;
|
||||
swayWrapper.style.animationDelay = `-${initSwayDelay}s`;
|
||||
|
||||
let confetti = document.createElement('div');
|
||||
confetti.className = 'olympia-confetti';
|
||||
|
||||
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
|
||||
confetti.style.backgroundColor = color;
|
||||
|
||||
// Random shape
|
||||
const shape = Math.random();
|
||||
if (shape > 0.66) {
|
||||
confetti.classList.add('circle');
|
||||
const size = Math.random() * 5 + 5;
|
||||
confetti.style.width = `${size}px`;
|
||||
confetti.style.height = `${size}px`;
|
||||
} else if (shape > 0.33) {
|
||||
confetti.classList.add('rect');
|
||||
const width = Math.random() * 4 + 4;
|
||||
const height = Math.random() * 5 + 8;
|
||||
confetti.style.width = `${width}px`;
|
||||
confetti.style.height = `${height}px`;
|
||||
} else {
|
||||
confetti.classList.add('triangle');
|
||||
}
|
||||
|
||||
// Random 3D Rotation for flutter
|
||||
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));
|
||||
confetti.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
|
||||
let rotateDuration = Math.random() * 0.8 + 0.4;
|
||||
confetti.style.animationDuration = `${rotateDuration}s`;
|
||||
|
||||
swayWrapper.appendChild(confetti);
|
||||
wrapper.appendChild(swayWrapper);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeOlympia() {
|
||||
if (!olympia) return;
|
||||
createOlympia();
|
||||
toggleOlympia();
|
||||
}
|
||||
|
||||
initializeOlympia();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/bronze_coin.gif
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/gold_coin.gif
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/silver_coin.gif
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/torch.gif
Normal file
|
After Width: | Height: | Size: 123 KiB |
70
Jellyfin.Plugin.Seasonals/Web/oscar.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.oscar-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.oscar-carpet {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
background: linear-gradient(to top, rgba(139, 0, 0, 0.8) 0%, transparent 100%);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.oscar-spotlights {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.oscar-spotlight {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/* MARK: SPOTLIGHT WIDTH CONFIGURATION */
|
||||
/* To adjust bottom width (spread), change 'width' property (e.g., 20vw for narrow, 40vw for wide). */
|
||||
/* To adjust top width (origin), modify first two percentages in 'clip-path' (e.g., 48% 0, 52% 0 for a very thin start). */
|
||||
width: 30vw;
|
||||
height: 120vh;
|
||||
background: linear-gradient(to bottom, rgba(255, 215, 0, 0.4) 0%, transparent 80%);
|
||||
clip-path: polygon(45% 0, 55% 0, 100% 100%, 0 100%);
|
||||
transform-origin: top center;
|
||||
animation: spotlight-sweep 12s infinite alternate ease-in-out;
|
||||
mix-blend-mode: screen;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
.oscar-flash {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 50px 30px rgba(255, 255, 255, 0.8), 0 0 100px 50px rgba(255, 255, 255, 0.5);
|
||||
animation: flash-pop 0.2s cubic-bezier(0.1, 0.8, 0.1, 1);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
@keyframes spotlight-sweep {
|
||||
0% { transform: rotate(-30deg); }
|
||||
100% { transform: rotate(30deg); }
|
||||
}
|
||||
|
||||
@keyframes flash-pop {
|
||||
0% { transform: scale(0.5); opacity: 1; }
|
||||
50% { transform: scale(2); opacity: 1; }
|
||||
100% { transform: scale(3); opacity: 0; }
|
||||
}
|
||||
94
Jellyfin.Plugin.Seasonals/Web/oscar.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const config = window.SeasonalsPluginConfig?.Oscar || {};
|
||||
const oscar = config.EnableOscar !== undefined ? config.EnableOscar : true; // enable/disable oscar
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleOscar() {
|
||||
const container = document.querySelector('.oscar-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('Oscar hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Oscar visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleOscar);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createOscar(container) {
|
||||
// Red carpet floor
|
||||
const carpet = document.createElement('div');
|
||||
carpet.className = 'oscar-carpet';
|
||||
|
||||
// Spotlights
|
||||
const spotlights = document.createElement('div');
|
||||
spotlights.className = 'oscar-spotlights';
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const spot = document.createElement('div');
|
||||
spot.className = 'oscar-spotlight';
|
||||
spot.style.animationDelay = `-${Math.random() * 8}s`;
|
||||
spot.style.left = `${20 + (i * 30)}%`;
|
||||
spot.style.top = `${-5 - Math.random() * 15}vh`; // randomize top origin
|
||||
spotlights.appendChild(spot);
|
||||
}
|
||||
|
||||
container.appendChild(carpet);
|
||||
container.appendChild(spotlights);
|
||||
|
||||
function flashLoop() {
|
||||
if (!document.body.contains(container)) return; // Kill the loop if container is removed
|
||||
if (container.style.display === 'none') {
|
||||
setTimeout(flashLoop, 1000); // Check again later if hidden
|
||||
return;
|
||||
}
|
||||
const flash = document.createElement('div');
|
||||
flash.className = 'oscar-flash';
|
||||
flash.style.left = `${Math.random() * 100}%`;
|
||||
flash.style.top = `${Math.random() * 100}%`;
|
||||
container.appendChild(flash);
|
||||
setTimeout(() => flash.remove(), 200);
|
||||
|
||||
// Randomize next flash between 200ms and 1500ms
|
||||
const nextDelay = Math.random() * 1300 + 200;
|
||||
setTimeout(flashLoop, nextDelay);
|
||||
}
|
||||
flashLoop();
|
||||
}
|
||||
|
||||
function initializeOscar() {
|
||||
if (!oscar) return;
|
||||
|
||||
const container = document.querySelector('.oscar-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.oscar-container')) {
|
||||
container.className = "oscar-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
createOscar(container);
|
||||
toggleOscar();
|
||||
}
|
||||
|
||||
initializeOscar();
|
||||
33
Jellyfin.Plugin.Seasonals/Web/pride.css
Normal file
@@ -0,0 +1,33 @@
|
||||
.pride-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
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 */
|
||||
body.pride-active .skinHeader,
|
||||
body.pride-active .skinHeader-withBackground {
|
||||
background: linear-gradient(90deg, #E40303, #FF8C00, #FFED00, #008026, #24408E, #732982) !important;
|
||||
}
|
||||
84
Jellyfin.Plugin.Seasonals/Web/pride.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const config = window.SeasonalsPluginConfig?.Pride || {};
|
||||
|
||||
const enabled = config.EnablePride !== undefined ? config.EnablePride : true; // enable/disable pride
|
||||
const elementCount = config.HeartCount !== undefined ? config.HeartCount : 20; // count of heart
|
||||
const heartSize = config.HeartSize !== undefined ? config.HeartSize : 1.5; // size of hearts
|
||||
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true; // optionally color the header with pride colors
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(togglePride);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
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) {
|
||||
document.body.classList.add('pride-active');
|
||||
}
|
||||
|
||||
const cleanupObserver = new MutationObserver(() => {
|
||||
if (!document.querySelector('.pride-container')) {
|
||||
document.body.classList.remove('pride-active');
|
||||
}
|
||||
});
|
||||
cleanupObserver.observe(document.body, { childList: true });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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(180vh) translateX(-60vh) rotate(20deg); opacity: 0; }
|
||||
}
|
||||
73
Jellyfin.Plugin.Seasonals/Web/rain.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const config = window.SeasonalsPluginConfig?.Rain || {};
|
||||
|
||||
const enabled = config.EnableRain !== undefined ? config.EnableRain : true; // enable/disable rain
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300); // count of raindrops
|
||||
const rainSpeed = config.RainSpeed !== undefined ? config.RainSpeed : 1.0; // speed of rain
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleRain);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeRain() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleRain();
|
||||
}
|
||||
|
||||
initializeRain();
|
||||
@@ -1,59 +1,66 @@
|
||||
.resurrection-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resurrection-symbol {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -15%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
animation-name: resurrection-fall, resurrection-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
will-change: transform, top;
|
||||
}
|
||||
|
||||
.resurrection-symbol img {
|
||||
z-index: 15;
|
||||
height: auto;
|
||||
width: 56px;
|
||||
opacity: 0.95;
|
||||
filter: drop-shadow(0 0 8px rgba(255, 215, 130, 0.5));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.resurrection-symbol img {
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resurrection-fall {
|
||||
0% {
|
||||
top: -15%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 105%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resurrection-sway {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(65px);
|
||||
}
|
||||
}
|
||||
.resurrection-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.resurrection-symbol {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: 0;
|
||||
translate: 0 -15vh;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
animation-name: resurrection-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.resurrection-sway-wrapper {
|
||||
will-change: transform;
|
||||
animation-name: resurrection-sway;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.resurrection-symbol img {
|
||||
z-index: 15;
|
||||
height: auto;
|
||||
width: 56px;
|
||||
opacity: 0.95;
|
||||
filter: drop-shadow(0 0 8px rgba(255, 215, 130, 0.5));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.resurrection-symbol img {
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resurrection-fall {
|
||||
0% {
|
||||
transform: translate3d(0, -15vh, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 105vh, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resurrection-sway {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(65px);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const config = window.SeasonalsPluginConfig?.Resurrection || {};
|
||||
|
||||
const enableResurrection = config.EnableResurrection !== undefined ? config.EnableResurrection : true;
|
||||
const enableRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||
const enableRandomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const symbolCount = config.SymbolCount || 12;
|
||||
const enableResurrection = config.EnableResurrection !== undefined ? config.EnableResurrection : true; // enable/disable resurrection
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 12; // count of symbols
|
||||
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 5; // count of symbols on mobile
|
||||
|
||||
let animationEnabled = true;
|
||||
let statusLogged = false;
|
||||
@@ -52,39 +51,43 @@ function createSymbol(imageSrc, leftPercent, delaySeconds) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'resurrection-symbol';
|
||||
|
||||
const swayWrapper = document.createElement('div');
|
||||
swayWrapper.className = 'resurrection-sway-wrapper';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = imageSrc;
|
||||
img.alt = '';
|
||||
|
||||
symbol.style.left = `${leftPercent}%`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s, ${Math.random() * 3}s`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
const fallDuration = Math.random() * 7 + 7;
|
||||
const swayDuration = Math.random() * 4 + 2;
|
||||
symbol.style.animationDuration = `${fallDuration}s, ${swayDuration}s`;
|
||||
symbol.style.animationDuration = `${fallDuration}s`;
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
}
|
||||
|
||||
symbol.appendChild(img);
|
||||
swayWrapper.style.animationDelay = `${Math.random() * 3}s`;
|
||||
|
||||
swayWrapper.appendChild(img);
|
||||
symbol.appendChild(swayWrapper);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function addSymbols(count) {
|
||||
const container = document.querySelector('.resurrection-container');
|
||||
if (!container || !enableRandomSymbols) return;
|
||||
|
||||
const isDesktop = window.innerWidth > 768;
|
||||
if (!isDesktop && !enableRandomSymbolsMobile) return;
|
||||
if (!container) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const left = Math.random() * 100;
|
||||
const delay = Math.random() * 12;
|
||||
const delay = -(Math.random() * 12);
|
||||
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||
}
|
||||
}
|
||||
|
||||
function initResurrection() {
|
||||
function initResurrection(count) {
|
||||
let container = document.querySelector('.resurrection-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
@@ -96,17 +99,21 @@ function initResurrection() {
|
||||
// Place one of each of the 8 provided resurrection images first.
|
||||
images.forEach((imageSrc, index) => {
|
||||
const left = (index + 1) * (100 / (images.length + 1));
|
||||
const delay = Math.random() * 8;
|
||||
const delay = -(Math.random() * 8);
|
||||
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||
});
|
||||
|
||||
const extraCount = Math.max(symbolCount - images.length, 0);
|
||||
const extraCount = Math.max(count - images.length, 0);
|
||||
addSymbols(extraCount);
|
||||
}
|
||||
|
||||
function initializeResurrection() {
|
||||
if (!enableResurrection) return;
|
||||
initResurrection();
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? symbolCount : symbolCountMobile;
|
||||
|
||||
initResurrection(count);
|
||||
toggleResurrection();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
const config = window.SeasonalsPluginConfig?.Santa || {};
|
||||
|
||||
const santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
|
||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
||||
const snowFallSpeed = config.SnowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
|
||||
const santaSpeed = config.SantaSpeed || 10; // speed of santa in seconds (recommended values: 5-15)
|
||||
const santaSpeedMobile = config.SantaSpeedMobile || 8; // speed of santa on mobile devices in seconds
|
||||
const maxSantaRestTime = config.MaxSantaRestTime || 8; // maximum time santa rests in seconds
|
||||
const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa rests in seconds
|
||||
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
|
||||
const minPresentFallSpeed = config.MinPresentFallSpeed || 2; // minimum speed of falling presents in seconds
|
||||
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||
const snowFallSpeed = config.SnowFallSpeed !== undefined ? config.SnowFallSpeed : 3; // speed of snowfall
|
||||
const santaSpeed = config.SantaSpeed !== undefined ? config.SantaSpeed : 10; // speed of santa in seconds
|
||||
const santaSpeedMobile = config.SantaSpeedMobile !== undefined ? config.SantaSpeedMobile : 8; // speed of santa on mobile devices in seconds
|
||||
const maxSantaRestTime = config.MaxSantaRestTime !== undefined ? config.MaxSantaRestTime : 8; // maximum time santa rests in seconds
|
||||
const minSantaRestTime = config.MinSantaRestTime !== undefined ? config.MinSantaRestTime : 3; // minimum time santa rests in seconds
|
||||
const maxPresentFallSpeed = config.MaxPresentFallSpeed !== undefined ? config.MaxPresentFallSpeed : 5; // maximum speed of falling presents in seconds
|
||||
const minPresentFallSpeed = config.MinPresentFallSpeed !== undefined ? config.MinPresentFallSpeed : 2; // minimum speed of falling presents in seconds
|
||||
|
||||
// credits: flaticon.com
|
||||
const presentImages = [
|
||||
'../Seasonals/Resources/santa_images/gift1.png',
|
||||
'../Seasonals/Resources/santa_images/gift2.png',
|
||||
'../Seasonals/Resources/santa_images/gift3.png',
|
||||
'../Seasonals/Resources/santa_images/gift4.png',
|
||||
'../Seasonals/Resources/santa_images/gift5.png',
|
||||
'../Seasonals/Resources/santa_images/gift6.png',
|
||||
'../Seasonals/Resources/santa_images/gift7.png',
|
||||
'../Seasonals/Resources/santa_images/gift8.png',
|
||||
];
|
||||
|
||||
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
|
||||
const santaImage = '../Seasonals/Resources/santa_images/santa.gif';
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
let isMobile = false; // flag to detect mobile devices
|
||||
@@ -52,12 +67,10 @@ function toggleSnowfall() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowfall);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
@@ -179,22 +192,6 @@ function updateSnowflakes() {
|
||||
});
|
||||
}
|
||||
|
||||
// credits: flaticon.com
|
||||
const presentImages = [
|
||||
'../Seasonals/Resources/santa_images/gift1.png',
|
||||
'../Seasonals/Resources/santa_images/gift2.png',
|
||||
'../Seasonals/Resources/santa_images/gift3.png',
|
||||
'../Seasonals/Resources/santa_images/gift4.png',
|
||||
'../Seasonals/Resources/santa_images/gift5.png',
|
||||
'../Seasonals/Resources/santa_images/gift6.png',
|
||||
'../Seasonals/Resources/santa_images/gift7.png',
|
||||
'../Seasonals/Resources/santa_images/gift8.png',
|
||||
];
|
||||
|
||||
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
|
||||
const santaImage = '../Seasonals/Resources/santa_images/santa.gif';
|
||||
|
||||
|
||||
function createSantaElement() {
|
||||
const santa = document.createElement('img');
|
||||
santa.src = santaImage;
|
||||
@@ -241,7 +238,7 @@ function animateSanta() {
|
||||
function startAnimation() {
|
||||
const santaHeight = santa.offsetHeight;
|
||||
if (santaHeight === 0) {
|
||||
setTimeout(startAnimation, 100);
|
||||
setTimeout(() => { if (document.body.contains(santa)) startAnimation(); }, 100);
|
||||
return;
|
||||
}
|
||||
// console.log('Santa height: ', santaHeight);
|
||||
@@ -286,7 +283,7 @@ function animateSanta() {
|
||||
animationFrameIdSanta = requestAnimationFrame(move);
|
||||
} else {
|
||||
const pause = Math.random() * ((maxSantaRestTime - minSantaRestTime) * 1000) + minSantaRestTime * 1000;
|
||||
setTimeout(animateSanta, pause);
|
||||
setTimeout(() => { if (document.body.contains(santa)) animateSanta(); }, pause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,8 +310,8 @@ function initializeSanta() {
|
||||
}
|
||||
const container = document.querySelector('.santa-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
|
||||
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||
isMobile = true;
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
|
||||
@@ -73,6 +73,111 @@ const ThemeConfigs = {
|
||||
js: '../Seasonals/Resources/carnival.js',
|
||||
containerClass: 'carnival-container'
|
||||
},
|
||||
cherryblossom: {
|
||||
css: '../Seasonals/Resources/cherryblossom.css',
|
||||
js: '../Seasonals/Resources/cherryblossom.js',
|
||||
containerClass: 'cherryblossom-container'
|
||||
},
|
||||
matrix: {
|
||||
css: '../Seasonals/Resources/matrix.css',
|
||||
js: '../Seasonals/Resources/matrix.js',
|
||||
containerClass: 'matrix-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'
|
||||
},
|
||||
frost: {
|
||||
css: '../Seasonals/Resources/frost.css',
|
||||
js: '../Seasonals/Resources/frost.js',
|
||||
containerClass: 'frost-container'
|
||||
},
|
||||
filmnoir: {
|
||||
css: '../Seasonals/Resources/filmnoir.css',
|
||||
js: '../Seasonals/Resources/filmnoir.js',
|
||||
containerClass: 'filmnoir-container'
|
||||
},
|
||||
oscar: {
|
||||
css: '../Seasonals/Resources/oscar.css',
|
||||
js: '../Seasonals/Resources/oscar.js',
|
||||
containerClass: 'oscar-container'
|
||||
},
|
||||
marioday: {
|
||||
css: '../Seasonals/Resources/marioday.css',
|
||||
js: '../Seasonals/Resources/marioday.js',
|
||||
containerClass: 'marioday-container'
|
||||
},
|
||||
starwars: {
|
||||
css: '../Seasonals/Resources/starwars.css',
|
||||
js: '../Seasonals/Resources/starwars.js',
|
||||
containerClass: 'starwars-container'
|
||||
},
|
||||
oktoberfest: {
|
||||
css: '../Seasonals/Resources/oktoberfest.css',
|
||||
js: '../Seasonals/Resources/oktoberfest.js',
|
||||
containerClass: 'oktoberfest-container'
|
||||
},
|
||||
friday13: {
|
||||
css: '../Seasonals/Resources/friday13.css',
|
||||
js: '../Seasonals/Resources/friday13.js',
|
||||
containerClass: 'friday13-container'
|
||||
},
|
||||
eid: {
|
||||
css: '../Seasonals/Resources/eid.css',
|
||||
js: '../Seasonals/Resources/eid.js',
|
||||
containerClass: 'eid-container'
|
||||
},
|
||||
spooky: {
|
||||
css: '../Seasonals/Resources/spooky.css',
|
||||
js: '../Seasonals/Resources/spooky.js',
|
||||
containerClass: 'spooky-container'
|
||||
},
|
||||
sports: {
|
||||
css: '../Seasonals/Resources/sports.css',
|
||||
js: '../Seasonals/Resources/sports.js',
|
||||
containerClass: 'sports-container'
|
||||
},
|
||||
olympia: {
|
||||
css: '../Seasonals/Resources/olympia.css',
|
||||
js: '../Seasonals/Resources/olympia.js',
|
||||
containerClass: 'olympia-container'
|
||||
},
|
||||
space: {
|
||||
css: '../Seasonals/Resources/space.css',
|
||||
js: '../Seasonals/Resources/space.js',
|
||||
containerClass: 'space-container'
|
||||
},
|
||||
underwater: {
|
||||
css: '../Seasonals/Resources/underwater.css',
|
||||
js: '../Seasonals/Resources/underwater.js',
|
||||
containerClass: 'underwater-container'
|
||||
},
|
||||
birthday: {
|
||||
css: '../Seasonals/Resources/birthday.css',
|
||||
js: '../Seasonals/Resources/birthday.js',
|
||||
containerClass: 'birthday-container'
|
||||
},
|
||||
none: {
|
||||
containerClass: 'none'
|
||||
},
|
||||
@@ -246,6 +351,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 {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const config = window.SeasonalsPluginConfig?.Snowfall || {};
|
||||
|
||||
const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
|
||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
||||
const snowFallSpeed = config.Speed || 3; // speed of snowfall (recommended values: 0-5)
|
||||
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 3; // speed of snowfall
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -47,12 +47,10 @@ function toggleSnowfall() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowfall);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
@@ -183,8 +181,8 @@ function initializeSnowfall() {
|
||||
}
|
||||
const container = document.querySelector('.snowfall-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
|
||||
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
}
|
||||
|
||||
@@ -8,60 +8,36 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: heart-fall, heart-shake;
|
||||
-webkit-animation-duration: 12s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
animation-name: snowflakes-fall, snowflakes-shake;
|
||||
animation-duration: 12s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes snowflakes-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes snowflakes-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
translate: 0 110vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,64 +51,4 @@
|
||||
50% {
|
||||
transform: translateX(80px);
|
||||
}
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(0) {
|
||||
left: 0%;
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(1) {
|
||||
left: 10%;
|
||||
animation-delay: 1s, 1s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(2) {
|
||||
left: 20%;
|
||||
animation-delay: 6s, 0.5s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s, 2s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(4) {
|
||||
left: 40%;
|
||||
animation-delay: 2s, 2s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s, 3s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(6) {
|
||||
left: 60%;
|
||||
animation-delay: 6s, 2s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(7) {
|
||||
left: 70%;
|
||||
animation-delay: 2.5s, 1s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(8) {
|
||||
left: 80%;
|
||||
animation-delay: 1s, 0s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(9) {
|
||||
left: 90%;
|
||||
animation-delay: 3s, 1.5s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(10) {
|
||||
left: 25%;
|
||||
animation-delay: 2s, 0s;
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(11) {
|
||||
left: 65%;
|
||||
animation-delay: 4s, 2.5s;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
const config = window.SeasonalsPluginConfig?.Snowflakes || {};
|
||||
|
||||
const snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
|
||||
const randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random Snowflakes
|
||||
const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices
|
||||
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable colored snowflakes
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
||||
const snowflakeCount = config.SnowflakeCount || 25; // count of random extra snowflakes
|
||||
const snowflakeCount = config.SnowflakeCount !== undefined ? config.SnowflakeCount : 25; // count of snowflakes
|
||||
const snowflakeCountMobile = config.SnowflakeCountMobile !== undefined ? config.SnowflakeCountMobile : 10; // count of snowflakes on mobile
|
||||
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable/disable colored snowflakes
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
|
||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -19,7 +20,7 @@ function toggleSnowflakes() {
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
|
||||
// hide snowflakes if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
snowflakeContainer.style.display = 'none'; // hide snowflakes
|
||||
@@ -38,22 +39,22 @@ function toggleSnowflakes() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowflakes);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function addRandomSnowflakes(count) {
|
||||
const snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
||||
if (!snowflakeContainer) return; // exit if snowflake container is not found
|
||||
function initSnowflakes(count) {
|
||||
let snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
||||
if (!snowflakeContainer) {
|
||||
snowflakeContainer = document.createElement("div");
|
||||
snowflakeContainer.className = "snowflakes";
|
||||
snowflakeContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(snowflakeContainer);
|
||||
}
|
||||
|
||||
console.log('Adding random snowflakes');
|
||||
|
||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||
console.log('Adding snowflakes');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new snowflake element
|
||||
@@ -69,8 +70,8 @@ function addRandomSnowflakes(count) {
|
||||
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
const randomAnimationDelay = Math.random() * 8; // delay (0s to 8s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
const randomAnimationDelay = -(Math.random() * 14); // delay (-14s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
snowflake.style.left = `${randomLeft}%`;
|
||||
@@ -86,49 +87,18 @@ function addRandomSnowflakes(count) {
|
||||
// add the snowflake to the container
|
||||
snowflakeContainer.appendChild(snowflake);
|
||||
}
|
||||
console.log('Random snowflakes added');
|
||||
console.log('Snowflakes added');
|
||||
}
|
||||
|
||||
// initialize standard snowflakes
|
||||
function initSnowflakes() {
|
||||
const snowflakesContainer = document.querySelector('.snowflakes') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.snowflakes')) {
|
||||
snowflakesContainer.className = "snowflakes";
|
||||
snowflakesContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(snowflakesContainer);
|
||||
}
|
||||
|
||||
// Array of snowflake characters
|
||||
const snowflakeSymbols = ['❅', '❆'];
|
||||
|
||||
// create the 12 standard snowflakes
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const snowflake = document.createElement('div');
|
||||
snowflake.className = 'snowflake';
|
||||
snowflake.textContent = snowflakeSymbols[i % 2]; // change between ❅ and ❆
|
||||
|
||||
// set random animation duration
|
||||
if (enableDiffrentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 14 + 10; // delay (10s to 14s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 3; // delay (3s to 5s)
|
||||
snowflake.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
snowflakesContainer.appendChild(snowflake);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize snowflakes and add random snowflakes
|
||||
// initialize snowflakes
|
||||
function initializeSnowflakes() {
|
||||
if (!snowflakes) return; // exit if snowflakes are disabled
|
||||
initSnowflakes();
|
||||
toggleSnowflakes();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSnowflakes && (screenWidth > 768 || randomSnowflakesMobile)) { // add random snowflakes only on larger screens, unless enabled for mobile devices
|
||||
addRandomSnowflakes(snowflakeCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? snowflakeCount : snowflakeCountMobile;
|
||||
|
||||
initSnowflakes(count);
|
||||
toggleSnowflakes();
|
||||
}
|
||||
|
||||
initializeSnowflakes();
|
||||
@@ -8,6 +8,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const config = window.SeasonalsPluginConfig?.Snowstorm || {};
|
||||
|
||||
const snowstorm = config.enableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
|
||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
||||
const snowFallSpeed = config.Speed || 6; // speed of snowfall (recommended values: 4-8)
|
||||
const horizontalWind = config.HorizontalWind || 4; // horizontal wind speed (recommended value: 4)
|
||||
const verticalVariation = config.VerticalVariation || 2; // vertical variation (recommended value: 2)
|
||||
const snowstorm = config.EnableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
|
||||
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 6; // speed of snowstorm
|
||||
const horizontalWind = config.HorizontalWind !== undefined ? config.HorizontalWind : 4; // horizontal wind strength
|
||||
const verticalVariation = config.VerticalVariation !== undefined ? config.VerticalVariation : 2; // vertical variation
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -49,12 +49,10 @@ function toggleSnowstorm() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowstorm);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
@@ -186,8 +184,8 @@ function initializeSnowstorm() {
|
||||
}
|
||||
const container = document.querySelector('.snowstorm-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
}
|
||||
|
||||
101
Jellyfin.Plugin.Seasonals/Web/space.css
Normal file
@@ -0,0 +1,101 @@
|
||||
.space-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.space-bg-glow {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100vw; height: 100vh;
|
||||
background: radial-gradient(circle at 70% 30%, rgba(138, 43, 226, 0.15), transparent 60%),
|
||||
radial-gradient(circle at 20% 80%, rgba(65, 105, 225, 0.15), transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
animation: space-nebula-pulse 10s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes space-nebula-pulse {
|
||||
0% { opacity: 0.6; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.space-starfield {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100vw; height: 100vh;
|
||||
background: transparent;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.space-shooting-star {
|
||||
will-change: opacity;
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
|
||||
border-radius: 50%;
|
||||
animation: space-shoot 25s linear infinite;
|
||||
opacity: 0;
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
@keyframes space-shoot {
|
||||
0% { transform: rotate(var(--shoot-angle)) translateX(0); opacity: 0; }
|
||||
5% { opacity: 1; }
|
||||
35% { opacity: 1; }
|
||||
40% { transform: rotate(var(--shoot-angle)) translateX(var(--shoot-distance)); opacity: 0; }
|
||||
100% { transform: rotate(var(--shoot-angle)) translateX(var(--shoot-distance)); opacity: 0; }
|
||||
}
|
||||
|
||||
.space-symbol {
|
||||
position: absolute;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
font-size: 3rem;
|
||||
opacity: 0.85;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.space-symbol img {
|
||||
will-change: transform;
|
||||
width: 6vh;
|
||||
height: auto;
|
||||
max-width: 60px;
|
||||
object-fit: contain;
|
||||
animation: space-slow-spin var(--rot-dur, 20s) linear infinite;
|
||||
}
|
||||
|
||||
/* Specific elements scaling */
|
||||
.space-planet img { width: 8vh; max-width: 80px; }
|
||||
.space-astronaut img { width: 10vh; max-width: 100px; }
|
||||
.space-satellite img { width: 12vh; max-width: 120px; }
|
||||
.space-iss img { width: 25vh; max-width: 180px; }
|
||||
.space-rocket img { width: 12vh; max-width: 120px; }
|
||||
|
||||
@keyframes space-drift-right {
|
||||
0% { transform: translateX(-10vw) translateY(0) scaleX(-1); }
|
||||
50% { transform: translateX(60vw) translateY(-30vh) scaleX(-1); }
|
||||
100% { transform: translateX(140vw) translateY(0) scaleX(-1); }
|
||||
}
|
||||
|
||||
@keyframes space-drift-left {
|
||||
0% { transform: translateX(10vw) translateY(0); }
|
||||
50% { transform: translateX(-60vw) translateY(30vh); }
|
||||
100% { transform: translateX(-140vw) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes space-slow-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes space-star-drift {
|
||||
from { transform: translateY(0); }
|
||||
to { transform: translateY(-100vh); }
|
||||
}
|
||||
285
Jellyfin.Plugin.Seasonals/Web/space.js
Normal file
@@ -0,0 +1,285 @@
|
||||
const config = window.SeasonalsPluginConfig?.Space || {};
|
||||
|
||||
const space = config.EnableSpace !== undefined ? config.EnableSpace : true; // enable/disable space
|
||||
const planetCountConf = config.PlanetCount !== undefined ? config.PlanetCount : 6; // count of planets
|
||||
const astronautCountConf = config.AstronautCount !== undefined ? config.AstronautCount : 1; // count of astronaut
|
||||
const satelliteCountConf = config.SatelliteCount !== undefined ? config.SatelliteCount : 4; // count of satellite
|
||||
const issCountConf = config.IssCount !== undefined ? config.IssCount : 1; // count of iss
|
||||
const rocketCountConf = config.RocketCount !== undefined ? config.RocketCount : 1; // count of rocket/space shuttle
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 2; // Devisor to reduce number of objects on mobile
|
||||
|
||||
// Credit: https://lottiefiles.com/free-animation/astronaut-63lcWG4Xnh
|
||||
const astronautImages = [
|
||||
"../Seasonals/Resources/space_assets/astronaut_1.gif"
|
||||
];
|
||||
|
||||
// Credits: https://flaticon.com
|
||||
const planetImages = [
|
||||
"../Seasonals/Resources/space_assets/planet_1.png",
|
||||
"../Seasonals/Resources/space_assets/planet_2.png",
|
||||
"../Seasonals/Resources/space_assets/planet_3.png",
|
||||
"../Seasonals/Resources/space_assets/planet_4.png",
|
||||
"../Seasonals/Resources/space_assets/planet_5.png",
|
||||
"../Seasonals/Resources/space_assets/planet_6.png",
|
||||
"../Seasonals/Resources/space_assets/planet_7.png",
|
||||
"../Seasonals/Resources/space_assets/planet_8.png",
|
||||
"../Seasonals/Resources/space_assets/planet_9.png"
|
||||
];
|
||||
|
||||
// Credits: https://lottiefiles.com/free-animation/s-satellite-vfnNE8AALo
|
||||
const satelliteImages = [
|
||||
"../Seasonals/Resources/space_assets/Satellite_1.gif",
|
||||
"../Seasonals/Resources/space_assets/Satellite_2.gif"
|
||||
];
|
||||
|
||||
// Credit: https://pixabay.com/de/illustrations/raumstation-raum-struktur-8023777/
|
||||
const issImage = "../Seasonals/Resources/space_assets/iss.png";
|
||||
|
||||
/**
|
||||
* Credits:
|
||||
* https://lottiefiles.com/free-animation/rocket-MYUQ3UFq3k
|
||||
* https://pixabay.com/de/vectors/space-shuttle-atlantis-nasa-156012/
|
||||
*/
|
||||
const rocketImages = [
|
||||
"../Seasonals/Resources/space_assets/rocket.gif",
|
||||
"../Seasonals/Resources/space_assets/space-shuttle.png"
|
||||
]
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleSpace() {
|
||||
const container = document.querySelector('.space-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('Space hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Space visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleSpace);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createSpace() {
|
||||
const container = document.querySelector('.space-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.space-container')) {
|
||||
container.className = 'space-container';
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// const standardPlanetCount = 4;
|
||||
// const standardAstronautCount = 1;
|
||||
// const standardSatelliteCount = 2;
|
||||
// const standardIssCount = 1;
|
||||
// const standardRocketCount = 1;
|
||||
|
||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
let divisor = isMobile ? Math.max(1, symbolCountMobile) : 1;
|
||||
let pCount = Math.floor(planetCountConf / divisor);
|
||||
let aCount = Math.floor(astronautCountConf / divisor);
|
||||
let sCount = Math.floor(satelliteCountConf / divisor);
|
||||
let iCount = Math.floor(issCountConf / divisor);
|
||||
let rCount = Math.floor(rocketCountConf / divisor);
|
||||
|
||||
// Add Nebula Glow
|
||||
const bgGlow = document.createElement('div');
|
||||
bgGlow.className = 'space-bg-glow';
|
||||
container.appendChild(bgGlow);
|
||||
|
||||
// Add CSS Starfield
|
||||
const starfield = document.createElement('div');
|
||||
starfield.className = 'space-starfield';
|
||||
let boxShadows1 = [];
|
||||
let boxShadows2 = [];
|
||||
let boxShadows3 = [];
|
||||
|
||||
// Generate random stars for parallax starfield using CSS % / vw sizes for responsiveness
|
||||
for (let i = 0; i < 150; i++) {
|
||||
let x = (Math.random() * 100).toFixed(2);
|
||||
let y = (Math.random() * 100).toFixed(2);
|
||||
boxShadows1.push(`${x}vw ${y}vh #FFF`);
|
||||
boxShadows1.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
|
||||
}
|
||||
for (let i = 0; i < 50; i++) {
|
||||
let x = (Math.random() * 100).toFixed(2);
|
||||
let y = (Math.random() * 100).toFixed(2);
|
||||
boxShadows2.push(`${x}vw ${y}vh #FFF`);
|
||||
boxShadows2.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
|
||||
}
|
||||
for (let i = 0; i < 20; i++) {
|
||||
let x = (Math.random() * 100).toFixed(2);
|
||||
let y = (Math.random() * 100).toFixed(2);
|
||||
boxShadows3.push(`${x}vw ${y}vh #FFF`);
|
||||
boxShadows3.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
|
||||
}
|
||||
|
||||
const starLayer1 = document.createElement('div');
|
||||
starLayer1.style.width = '1px'; starLayer1.style.height = '1px';
|
||||
starLayer1.style.background = 'transparent';
|
||||
starLayer1.style.boxShadow = boxShadows1.join(", ");
|
||||
starLayer1.style.animation = 'space-star-drift 200s linear infinite';
|
||||
starfield.appendChild(starLayer1);
|
||||
|
||||
const starLayer2 = document.createElement('div');
|
||||
starLayer2.style.width = '2px'; starLayer2.style.height = '2px';
|
||||
starLayer2.style.background = 'transparent';
|
||||
starLayer2.style.boxShadow = boxShadows2.join(", ");
|
||||
starLayer2.style.animation = 'space-star-drift 150s linear infinite';
|
||||
starfield.appendChild(starLayer2);
|
||||
|
||||
const starLayer3 = document.createElement('div');
|
||||
starLayer3.style.width = '3px'; starLayer3.style.height = '3px';
|
||||
starLayer3.style.background = 'transparent';
|
||||
starLayer3.style.boxShadow = boxShadows3.join(", ");
|
||||
starLayer3.style.animation = 'space-star-drift 100s linear infinite';
|
||||
starfield.appendChild(starLayer3);
|
||||
|
||||
container.appendChild(starfield);
|
||||
|
||||
// Shooting stars
|
||||
const shootingStarCount = isMobile ? 1 : 2; // Less frequent
|
||||
for (let i = 0; i < shootingStarCount; i++) {
|
||||
const streak = document.createElement('div');
|
||||
streak.className = 'space-shooting-star';
|
||||
// Pick a random tail direction and fall direction to match
|
||||
const isFromLeft = Math.random() > 0.5;
|
||||
// Direction angle: random between 15deg-75deg (left) or 105deg-165deg (right)
|
||||
// so they don't always fall in the exact same quadrant trajectory
|
||||
let angle = isFromLeft
|
||||
? Math.random() * 60 + 15
|
||||
: Math.random() * 60 + 105;
|
||||
|
||||
streak.style.setProperty('--shoot-angle', `${angle}deg`);
|
||||
|
||||
const topStart = Math.random() * 50;
|
||||
streak.style.left = isFromLeft ? '-20vw' : '120vw';
|
||||
streak.style.top = `${topStart}vh`;
|
||||
|
||||
// Travel 200 viewport widths exactly along the rotated angle
|
||||
streak.style.setProperty('--shoot-distance', '200vw');
|
||||
|
||||
streak.style.animationDelay = `${Math.random() * 20}s`;
|
||||
|
||||
// MARK: Shooting Star Speed
|
||||
const flightCycleDuration = Math.random() * 10 + 15; // 15-25s
|
||||
streak.style.animationDuration = `${flightCycleDuration}s`;
|
||||
|
||||
container.appendChild(streak);
|
||||
}
|
||||
|
||||
const useRandomDuration = enableDifferentDuration !== false;
|
||||
|
||||
function createSpaceItem(imageArr, cCount, addedClass) {
|
||||
for (let i = 0; i < cCount; i++) {
|
||||
let symbol = document.createElement('div');
|
||||
|
||||
const randomImage = imageArr[Math.floor(Math.random() * imageArr.length)];
|
||||
symbol.className = `space-symbol ${addedClass}`;
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.src = randomImage;
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
};
|
||||
symbol.appendChild(img);
|
||||
|
||||
const topPos = Math.random() * 90; // 0 to 90vh
|
||||
|
||||
// Zero gravity sizes / speeds
|
||||
const depth = Math.random();
|
||||
// Make background elements (depth close to 0) much smaller than foreground
|
||||
const distanceScale = 0.15 + (depth * 0.85); // 0.15 to 1.0
|
||||
|
||||
symbol.style.zIndex = Math.floor(depth * 30) + 20;
|
||||
|
||||
let durationSeconds = 30; // Very slow
|
||||
if (useRandomDuration) {
|
||||
durationSeconds = (1 - depth) * 40 + 30 + Math.random() * 10 - 5;
|
||||
}
|
||||
|
||||
// Randomly pick direction: left-to-right OR right-to-left
|
||||
const goRight = Math.random() > 0.5;
|
||||
const baseTransformScale = goRight ? 'scaleX(-1)' : 'scaleX(1)';
|
||||
|
||||
if (goRight) {
|
||||
symbol.style.animationName = 'space-drift-right';
|
||||
symbol.style.left = '-20vw';
|
||||
symbol.style.right = 'auto';
|
||||
} else {
|
||||
symbol.style.animationName = 'space-drift-left';
|
||||
symbol.style.right = '-20vw';
|
||||
symbol.style.left = 'auto';
|
||||
}
|
||||
|
||||
symbol.style.top = `${topPos}vh`;
|
||||
symbol.style.animationDuration = `${durationSeconds}s`;
|
||||
|
||||
// Negative delay correctly scatters them initially across the screen
|
||||
// so they don't all appear to spawn from the edge at the start
|
||||
const delaySeconds = -(Math.random() * durationSeconds);
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
|
||||
// Slow rotation inside inner div
|
||||
const rotationDiv = document.createElement('div');
|
||||
const rotDur = Math.random() * 20 + 20; // 20-40s spin
|
||||
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
|
||||
rotationDiv.style.animation = `space-slow-spin ${rotDur}s linear infinite ${spinReverse}`;
|
||||
|
||||
// Apply final static scaling and facing to inner image directly
|
||||
img.style.transform = `scale(${distanceScale}) ${baseTransformScale}`;
|
||||
|
||||
rotationDiv.appendChild(img);
|
||||
symbol.appendChild(rotationDiv);
|
||||
|
||||
// Swap to a random image from the pool every time it completes an orbit (disappears)
|
||||
if (imageArr.length > 1) {
|
||||
// The animation delay pushes the initial cycle, so we use setInterval matched to duration
|
||||
const intervalId = setInterval(() => {
|
||||
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
|
||||
// Update only if currently out of bounds to avoid popping
|
||||
const rect = symbol.getBoundingClientRect();
|
||||
if (rect.right < 0 || rect.left > window.innerWidth) {
|
||||
img.src = imageArr[Math.floor(Math.random() * imageArr.length)];
|
||||
}
|
||||
}, 2000); // Check occasionally if it's off screen
|
||||
}
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
createSpaceItem(planetImages, pCount, 'space-planet');
|
||||
createSpaceItem(astronautImages, aCount, 'space-astronaut');
|
||||
createSpaceItem(satelliteImages, sCount, 'space-satellite');
|
||||
createSpaceItem([issImage], iCount, 'space-iss');
|
||||
createSpaceItem(rocketImages, rCount, 'space-rocket');
|
||||
}
|
||||
|
||||
function initializeSpace() {
|
||||
if (!space) return;
|
||||
createSpace();
|
||||
toggleSpace();
|
||||
}
|
||||
|
||||
initializeSpace();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/Satellite_1.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/Satellite_2.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/astronaut_1.gif
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/iss.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_1.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_2.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_3.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_4.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_5.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_6.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_7.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_8.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_9.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/rocket.gif
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/space-shuttle.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
84
Jellyfin.Plugin.Seasonals/Web/spooky.css
Normal file
@@ -0,0 +1,84 @@
|
||||
.spooky-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.spooky {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 120vh;
|
||||
z-index: 15;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
animation-name: spooky-float;
|
||||
animation-duration: 10s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
.spooky-inner {
|
||||
width: 30px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
animation-name: spooky-shake;
|
||||
animation-duration: 3s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
.spooky-inner img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spooky-float {
|
||||
0% {
|
||||
translate: 0 120vh;
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
translate: 0 -150px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes spooky-shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0) scale(1) rotate(15deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(80px) scale(1.2) rotate(-15deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spooky:nth-of-type(0) { left: 1%; }
|
||||
.spooky:nth-of-type(1) { left: 10%; }
|
||||
.spooky:nth-of-type(2) { left: 20%; }
|
||||
.spooky:nth-of-type(3) { left: 30%; }
|
||||
.spooky:nth-of-type(4) { left: 40%; }
|
||||
.spooky:nth-of-type(5) { left: 50%; }
|
||||
.spooky:nth-of-type(6) { left: 60%; }
|
||||
.spooky:nth-of-type(7) { left: 70%; }
|
||||
.spooky:nth-of-type(8) { left: 80%; }
|
||||
.spooky:nth-of-type(9) { left: 90%; }
|
||||
.spooky:nth-of-type(10) { left: 25%; }
|
||||
.spooky:nth-of-type(11) { left: 65%; }
|
||||
142
Jellyfin.Plugin.Seasonals/Web/spooky.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const config = window.SeasonalsPluginConfig?.Spooky || {};
|
||||
|
||||
const spooky = config.EnableSpooky !== undefined ? config.EnableSpooky : true; // enable/disable spooky
|
||||
const spookyCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableSpookySway = config.EnableSpookySway !== undefined ? config.EnableSpookySway : true; // enable/disable spooky sway
|
||||
const spookySize = config.SpookySize !== undefined ? config.SpookySize : 20; // size of elements
|
||||
const spookyGlowSize = config.SpookyGlowSize !== undefined ? config.SpookyGlowSize : 2; // size of element glow
|
||||
|
||||
const spookyImages = [
|
||||
"../Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// function to check and control the spooky theme
|
||||
function toggleSpooky() {
|
||||
const spookyContainer = document.querySelector('.spooky-container');
|
||||
if (!spookyContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
spookyContainer.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('Spooky Theme hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
spookyContainer.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Spooky Theme visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleSpooky);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createSpooky() {
|
||||
const container = document.querySelector('.spooky-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.spooky-container')) {
|
||||
container.className = "spooky-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Base items per image
|
||||
for (let i = 0; i < 4; i++) {
|
||||
spookyImages.forEach(imageSrc => {
|
||||
const spookyOuter = document.createElement("div");
|
||||
spookyOuter.className = "spooky";
|
||||
|
||||
const spookyInner = document.createElement("div");
|
||||
spookyInner.className = "spooky-inner";
|
||||
spookyInner.style.width = `${spookySize}px`;
|
||||
if (!enableSpookySway) spookyInner.style.animationName = 'none';
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
img.style.filter = spookyGlowSize > 0 ? `drop-shadow(0 0 ${spookyGlowSize}px rgba(255, 120, 0, 0.4))` : 'none';
|
||||
|
||||
// randomize fall and sway (shake) speeds like halloween.js
|
||||
if (enableDifferentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // shake duration (2s to 5s)
|
||||
spookyOuter.style.animationDuration = `${randomAnimationDuration}s`;
|
||||
spookyInner.style.animationDuration = `${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
const randomLeft = Math.random() * 100;
|
||||
const randomAnimationDelay = Math.random() * 10;
|
||||
const randomAnimationDelay2 = Math.random() * 3;
|
||||
|
||||
spookyOuter.style.left = `${randomLeft}%`;
|
||||
spookyOuter.style.animationDelay = `${randomAnimationDelay}s`;
|
||||
spookyInner.style.animationDelay = `${randomAnimationDelay2}s`;
|
||||
|
||||
spookyInner.appendChild(img);
|
||||
spookyOuter.appendChild(spookyInner);
|
||||
container.appendChild(spookyOuter);
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < spookyCount; i++) {
|
||||
const spookyOuter = document.createElement("div");
|
||||
spookyOuter.className = "spooky";
|
||||
|
||||
const spookyInner = document.createElement("div");
|
||||
spookyInner.className = "spooky-inner";
|
||||
spookyInner.style.width = `${spookySize}px`;
|
||||
if (!enableSpookySway) spookyInner.style.animationName = 'none';
|
||||
|
||||
const imageSrc = spookyImages[Math.floor(Math.random() * spookyImages.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
img.style.filter = spookyGlowSize > 0 ? `drop-shadow(0 0 ${spookyGlowSize}px rgba(255, 120, 0, 0.4))` : 'none';
|
||||
|
||||
const randomLeft = Math.random() * 100;
|
||||
const randomAnimationDelay = Math.random() * 10;
|
||||
const randomAnimationDelay2 = Math.random() * 3;
|
||||
|
||||
spookyOuter.style.left = `${randomLeft}%`;
|
||||
spookyOuter.style.animationDelay = `${randomAnimationDelay}s`;
|
||||
spookyInner.style.animationDelay = `${randomAnimationDelay2}s`;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
||||
spookyOuter.style.animationDuration = `${randomAnimationDuration}s`;
|
||||
spookyInner.style.animationDuration = `${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
spookyInner.appendChild(img);
|
||||
spookyOuter.appendChild(spookyInner);
|
||||
container.appendChild(spookyOuter);
|
||||
}
|
||||
|
||||
console.log('Spooky symbols added');
|
||||
}
|
||||
|
||||
// initialize spooky
|
||||
function initializeSpooky() {
|
||||
if (!spooky) return;
|
||||
createSpooky();
|
||||
toggleSpooky();
|
||||
}
|
||||
|
||||
// initialize script
|
||||
initializeSpooky();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/sport_assets/badminton_1.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/sport_assets/badminton_2.png
Normal file
|
After Width: | Height: | Size: 22 KiB |