52 Commits

Author SHA1 Message Date
CodeDevMLH
7e33cb8fb7 fix: remove MenuIcon property from plugin configuration 2025-12-28 20:38:21 +01:00
CodeDevMLH
15d6f98427 fix: update legacy script tag comment and modify checksum in manifest 2025-12-28 20:11:33 +01:00
CodeDevMLH
3b1d0982e5 feat: update version to 1.4.0.0 in project file and manifest 2025-12-28 18:58:21 +01:00
CodeDevMLH
737e510a6c feat: add enable/disable toggle for the plugin and update configuration handling 2025-12-28 18:53:25 +01:00
CodeDevMLH
fbd77e56fd feat: add SeasonalsPlugin class and update manifest for version 1.4.0.0 2025-12-28 17:43:09 +01:00
CodeDevMLH
63dadb4ffa feat: update version to 1.3.4.0 in Jellyfin.Plugin.Seasonals.csproj
Some checks failed
🔬 Run CodeQL / call (push) Has been cancelled
2025-12-24 03:29:49 +01:00
CodeDevMLH
7167472f10 feat: update version to 1.3.4.0 in build.yaml 2025-12-24 03:27:15 +01:00
MLH
21b0382cfd bin/Publish/Jellyfin.Plugin.Seasonals.zip gelöscht 2025-12-24 03:04:50 +01:00
MLH
496d274a56 bin/Publish/Jellyfin.Plugin.Seasonals.pdb gelöscht 2025-12-24 03:04:46 +01:00
MLH
3cce9dd3f1 bin/Publish/Jellyfin.Plugin.Seasonals.dll gelöscht 2025-12-24 03:04:43 +01:00
MLH
9beb14d6b6 bin/Publish/Jellyfin.Plugin.Seasonals.deps.json gelöscht 2025-12-24 03:04:39 +01:00
CodeDevMLH
b311fe4e2a feat: update disabled select option styles and manifest for version 1.3.4.0 2025-12-24 03:03:22 +01:00
CodeDevMLH
70eac57e4d feat: add styles for disabled select options in configuration page 2025-12-24 02:54:27 +01:00
CodeDevMLH
4e64e863e3 feat: update seasonal options in config page and improve descriptions; add version 1.3.4.0 to manifest 2025-12-24 02:41:56 +01:00
CodeDevMLH
726d172625 feat: add client compatibility section to README for better user guidance 2025-12-20 23:18:13 +01:00
CodeDevMLH
1c6ed0aaed fix typo 2025-12-19 00:13:53 +01:00
CodeDevMLH
26eecc9ec4 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0
Some checks failed
🔬 Run CodeQL / call (push) Has been cancelled
2025-12-18 01:12:03 +01:00
CodeDevMLH
912bf7f544 feat: update z-index values for seasonal elements to improve layering and visibility 2025-12-18 01:11:29 +01:00
CodeDevMLH
0e37d3a291 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0 2025-12-18 00:53:19 +01:00
CodeDevMLH
83a8c7fc74 feat: add z-index to seasonal styles for improved layering 2025-12-18 00:52:45 +01:00
CodeDevMLH
b05b35b8f8 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0 2025-12-18 00:32:50 +01:00
CodeDevMLH
e90ea952bd feat: update seasonal styles to use fixed positioning and full coverage for better display 2025-12-18 00:32:19 +01:00
CodeDevMLH
7216588fef feat: enhance autumn and snowflakes styles with fixed positioning and full coverage 2025-12-18 00:21:30 +01:00
CodeDevMLH
f4a7c3f2e5 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0 2025-12-18 00:01:59 +01:00
CodeDevMLH
176aa97c1c feat: update configuration keys to use PascalCase for consistency across seasonal scripts 2025-12-18 00:01:19 +01:00
CodeDevMLH
d73b1d17ff feat: update version to 1.3.3.0 and modify manifest with new changelog, checksum, and timestamp 2025-12-17 23:36:09 +01:00
CodeDevMLH
4bb37f89f3 typo 2025-12-17 22:47:50 +01:00
CodeDevMLH
f67e08ef89 feat: update plugin version to 1.3.2.0 and modify manifest with new checksum and timestamp 2025-12-17 22:47:16 +01:00
CodeDevMLH
94709b63fb bump version 2025-12-17 22:42:21 +01:00
CodeDevMLH
988368b6f5 feat: update manifest with new checksum and timestamp for version 1.3.0.0 2025-12-17 22:31:18 +01:00
CodeDevMLH
2216ed90c8 fix changelog 2025-12-17 22:30:38 +01:00
CodeDevMLH
607042536c feat: fix image paths for seasonal effects and enhance z-index for better visibility 2025-12-17 22:30:31 +01:00
CodeDevMLH
009bd75374 feat: update seasonal effects settings and enhance z-index for better visibility 2025-12-17 22:23:41 +01:00
CodeDevMLH
490a61fc1f feat: add theme settings section to README for advanced customization options 2025-12-17 22:15:09 +01:00
CodeDevMLH
2e7df40241 revert test 2025-12-17 22:09:30 +01:00
CodeDevMLH
bd8b67c2ca test string 2025-12-17 22:03:08 +01:00
CodeDevMLH
93a59a832e feat: add version 1.2.0.0 with advanced settings for seasonal effects customization 2025-12-17 21:59:57 +01:00
CodeDevMLH
fbf9b2189b feat: enhance configuration page with seasonal options and improve user guidance 2025-12-17 21:58:48 +01:00
CodeDevMLH
0df664302d chore: remove deprecated manifest-v1.json file 2025-12-17 17:52:28 +01:00
CodeDevMLH
9db00ae365 fix: revert version to 1.1.0.0 and update source URL in manifest.json 2025-12-17 17:51:18 +01:00
CodeDevMLH
ae9f718f46 docs: Update README to include Configuration and Troubleshooting sections 2025-12-17 17:49:36 +01:00
CodeDevMLH
4433cbb949 feat: Update to version 1.2.0.0 with script injection improvements and fallback support 2025-12-17 16:34:02 +01:00
CodeDevMLH
ca813bacb7 add possible solution for later 2025-12-17 01:13:10 +01:00
CodeDevMLH
9bc6aedf87 fix path 2025-12-17 01:12:58 +01:00
MLH
ddfbf40bf7 manifest-v1.json hinzugefügt 2025-12-16 23:40:40 +01:00
CodeDevMLH
9ce88e19ad fix: Update sourceUrl for version 1.1.1.0 in manifest 2025-12-16 01:59:05 +01:00
CodeDevMLH
da6d868067 feat: Add version 1.1.1.0 with bug fixes and advanced configuration UI 2025-12-16 01:58:44 +01:00
CodeDevMLH
d97f017e32 fix: Update checksum and timestamp for version 1.1.0.0 in manifest 2025-12-16 01:40:56 +01:00
CodeDevMLH
29c6255904 fix: Update imageUrl in manifest to correct logo path 2025-12-16 01:40:23 +01:00
CodeDevMLH
baa1ddde66 fix: Update README for clarity and correct repository links 2025-12-16 01:37:40 +01:00
CodeDevMLH
93f09f42cf fix: Update source URLs in manifest for version downloads 2025-12-16 01:28:10 +01:00
CodeDevMLH
25a0be221b feat: Bump version to 1.1.0.0 and add advanced configuration options for seasonal effects
Enhanced JavaScript files for autumn, christmas, easter, fireworks, halloween, hearts, santa, snowfall, snowflakes, and snowstorm to support configuration options via window.SeasonalsPluginConfig.

Added automatic theme selection based on date in README.md.
2025-12-16 01:26:27 +01:00
36 changed files with 1419 additions and 850 deletions

1
.gitignore vendored
View File

@@ -4,4 +4,5 @@ obj/
.idea/ .idea/
artifacts artifacts
build.yaml
RELEASE_GUIDE.md RELEASE_GUIDE.md

View File

@@ -21,12 +21,7 @@ public class SeasonalsController : ControllerBase
[Produces("application/json")] [Produces("application/json")]
public ActionResult<object> GetConfig() public ActionResult<object> GetConfig()
{ {
var config = Plugin.Instance?.Configuration; return SeasonalsPlugin.Instance?.Configuration ?? new object();
return new
{
selectedSeason = config?.SelectedSeason ?? "none",
automateSeasonSelection = config?.AutomateSeasonSelection ?? true
};
} }
/// <summary> /// <summary>

View File

@@ -12,10 +12,27 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public PluginConfiguration() public PluginConfiguration()
{ {
IsEnabled = true;
SelectedSeason = "none"; SelectedSeason = "none";
AutomateSeasonSelection = true; AutomateSeasonSelection = true;
Autumn = new AutumnOptions();
Snowflakes = new SnowflakesOptions();
Snowfall = new SnowfallOptions();
Snowstorm = new SnowstormOptions();
Fireworks = new FireworksOptions();
Halloween = new HalloweenOptions();
Hearts = new HeartsOptions();
Christmas = new ChristmasOptions();
Santa = new SantaOptions();
Easter = new EasterOptions();
} }
/// <summary>
/// Gets or sets a value indicating whether the plugin is enabled.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets the selected season. /// Gets or sets the selected season.
/// </summary> /// </summary>
@@ -25,4 +42,118 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets a value indicating whether to automate season selection. /// Gets or sets a value indicating whether to automate season selection.
/// </summary> /// </summary>
public bool AutomateSeasonSelection { get; set; } public bool AutomateSeasonSelection { get; set; }
public AutumnOptions Autumn { get; set; }
public SnowflakesOptions Snowflakes { get; set; }
public SnowfallOptions Snowfall { get; set; }
public SnowstormOptions Snowstorm { get; set; }
public FireworksOptions Fireworks { 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 class AutumnOptions
{
public int LeafCount { get; set; } = 25;
public bool EnableAutumn { get; set; } = true;
public bool EnableRandomLeaves { get; set; } = true;
public bool EnableRandomLeavesMobile { get; set; } = false;
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 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 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 FireworksOptions
{
public int ParticleCount { get; set; } = 50;
public int LaunchInterval { get; set; } = 3200;
public bool EnableFireworks { get; set; } = true;
public bool ScrollFireworks { get; set; } = true;
public int MinFireworks { get; set; } = 3;
public int MaxFireworks { get; set; } = 6;
}
public class HalloweenOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableHalloween { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class HeartsOptions
{
public int SymbolCount { get; set; } = 25;
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 bool EnableDifferentDuration { get; set; } = true;
}
public class SantaOptions
{
public int SnowflakesCount { get; set; } = 500;
public int SnowflakesCountMobile { get; set; } = 250;
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;
} }

View File

@@ -6,9 +6,31 @@
</head> </head>
<body> <body>
<div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox"> <div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<style>
select option:disabled {
color: #a3a3a3 !important;
}
</style>
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">Seasonals</h2>
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals">
<i class="md-icon button-icon button-icon-left secondaryText"></i>
<span>Help</span>
</a>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<br>
<form id="SeasonalsConfigForm"> <form id="SeasonalsConfigForm">
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="IsEnabled" name="IsEnabled" type="checkbox" is="emby-checkbox" />
<span>Enable Seasonals</span>
</label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label"> <label class="emby-checkbox-label">
<input id="AutomateSeasonSelection" name="AutomateSeasonSelection" type="checkbox" is="emby-checkbox" /> <input id="AutomateSeasonSelection" name="AutomateSeasonSelection" type="checkbox" is="emby-checkbox" />
@@ -30,14 +52,468 @@
<option value="santa">Santa</option> <option value="santa">Santa</option>
<option value="autumn">Autumn</option> <option value="autumn">Autumn</option>
<option value="easter">Easter</option> <option value="easter">Easter</option>
<option value="summer" disabled>Summer (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="spring" disabled>Spring (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="oktoberfest" disabled>Oktoberfest (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="carnival" disabled>Carnival (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="championships" disabled>European/World Championships (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="patrick" disabled>St. Patrick's Day (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="pride" disabled>Pride (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
</select> </select>
<div class="fieldDescription">The season to display if automation is disabled.</div> <div class="fieldDescription">The season to display if automation is disabled.</div>
</div> </div>
<br>
<details>
<summary>Advanced Configuration</summary>
<p>Configure specific settings for each seasonal theme below.</p>
<p>All symbol count settings add this number in addition to the standard 12 symbols (if additional symbols is enabled).</p>
<details>
<summary>Autumn</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableAutumn" name="EnableAutumn" type="checkbox" is="emby-checkbox" />
<span>Enable Autumn Seasonal</span>
</label>
<div class="fieldDescription">Enable the autumn theme effects in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomLeaves" name="EnableRandomLeaves" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Leaves</span>
</label>
<div class="fieldDescription">Displays additional leaves randomly distributed across the screen</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomLeavesMobile" name="EnableRandomLeavesMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Leaves on Mobile</span>
</label>
<div class="fieldDescription">Displays additional leaves randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="AutumnLeafCount">Leaf Count</label>
<input is="emby-input" type="number" id="AutumnLeafCount" name="AutumnLeafCount" />
<div class="fieldDescription">Number of additional leaves displayed on screen (if enabled)</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationAutumn" name="EnableDifferentDurationAutumn" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the falling speed of each leaf.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRotation" name="EnableRotation" type="checkbox" is="emby-checkbox" />
<span>Enable Rotation</span>
</label>
<div class="fieldDescription">Rotate leaves as they fall. Notice: May affect performance</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Snowflakes</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSnowflakes" name="EnableSnowflakes" type="checkbox" is="emby-checkbox" />
<span>Enable Snowflakes Seasonal</span>
</label>
<div class="fieldDescription">Enable the snowflakes theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomSnowflakes" name="EnableRandomSnowflakes" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Snowflakes</span>
</label>
<div class="fieldDescription">Displays additional snowflakes randomly distributed across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomSnowflakesMobile" name="EnableRandomSnowflakesMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Snowflakes on Mobile</span>
</label>
<div class="fieldDescription">Displays additional snowflakes randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowflakesCount">Snowflake Count</label>
<input is="emby-input" type="number" id="SnowflakesCount" name="SnowflakesCount" />
<div class="fieldDescription">Number of additional snowflakes displayed (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableColoredSnowflakes" name="EnableColoredSnowflakes" type="checkbox" is="emby-checkbox" />
<span>Enable Colored Snowflakes</span>
</label>
<div class="fieldDescription">Display snowflakes in different colors/shapes.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationSnowflakes" name="EnableDifferentDurationSnowflakes" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the falling speed of snowflakes.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Snowfall</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSnowfall" name="EnableSnowfall" type="checkbox" is="emby-checkbox" />
<span>Enable Snowfall Seasonal</span>
</label>
<div class="fieldDescription">Enable the snowfall effect in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowfallCount">Snowflake Count</label>
<input is="emby-input" type="number" id="SnowfallCount" name="SnowfallCount" />
<div class="fieldDescription">Total number of snowflakes for the snowfall effect (recommended values: 300 - 600).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowfallCountMobile">Snowflake Count (Mobile)</label>
<input is="emby-input" type="number" id="SnowfallCountMobile" name="SnowfallCountMobile" />
<div class="fieldDescription">Total number of snowflakes on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowfallSpeed">Speed</label>
<input is="emby-input" type="number" id="SnowfallSpeed" name="SnowfallSpeed" step="0.1" />
<div class="fieldDescription">The speed of the snowfall animation (recommended values: 0 - 5).</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Snowstorm</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSnowstorm" name="EnableSnowstorm" type="checkbox" is="emby-checkbox" />
<span>Enable Snowstorm Seasonal</span>
</label>
<div class="fieldDescription">Enable the snowstorm effect in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowstormCount">Snowflake Count</label>
<input is="emby-input" type="number" id="SnowstormCount" name="SnowstormCount" />
<div class="fieldDescription">Total number of snowflakes in the storm (recommended values: 300 - 600).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowstormCountMobile">Snowflake Count (Mobile)</label>
<input is="emby-input" type="number" id="SnowstormCountMobile" name="SnowstormCountMobile" />
<div class="fieldDescription">Total number of snowflakes on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowstormSpeed">Speed</label>
<input is="emby-input" type="number" id="SnowstormSpeed" name="SnowstormSpeed" step="0.1" />
<div class="fieldDescription">The speed of the snowstorm (falling).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowstormHorizontalWind">Horizontal Wind</label>
<input is="emby-input" type="number" id="SnowstormHorizontalWind" name="SnowstormHorizontalWind" step="0.1" />
<div class="fieldDescription">Strength of the horizontal wind effect (recommended values: 3 - 6).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SnowstormVerticalVariation">Vertical Variation</label>
<input is="emby-input" type="number" id="SnowstormVerticalVariation" name="SnowstormVerticalVariation" step="0.1" />
<div class="fieldDescription">Amount of vertical movement variation (recommended values: 1 - 3).</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Fireworks</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableFireworks" name="EnableFireworks" type="checkbox" is="emby-checkbox" />
<span>Enable Fireworks Seasonal</span>
</label>
<div class="fieldDescription">Enable the fireworks effect in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="FireworksParticles">Particle Count</label>
<input is="emby-input" type="number" id="FireworksParticles" name="FireworksParticles" />
<div class="fieldDescription">Number of particles per firework explosion. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="FireworksInterval">Launch Interval (ms)</label>
<input is="emby-input" type="number" id="FireworksInterval" name="FireworksInterval" />
<div class="fieldDescription">Time in milliseconds between firework launches.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="ScrollFireworks" name="ScrollFireworks" type="checkbox" is="emby-checkbox" />
<span>Scroll Fireworks</span>
</label>
<div class="fieldDescription">Allow fireworks to scroll with the page.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MinFireworks">Min Fireworks</label>
<input is="emby-input" type="number" id="MinFireworks" name="MinFireworks" />
<div class="fieldDescription">Minimum number of concurrent fireworks.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MaxFireworks">Max Fireworks</label>
<input is="emby-input" type="number" id="MaxFireworks" name="MaxFireworks" />
<div class="fieldDescription">Maximum number of concurrent fireworks.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Halloween</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableHalloween" name="EnableHalloween" type="checkbox" is="emby-checkbox" />
<span>Enable Halloween Seasonal</span>
</label>
<div class="fieldDescription">Enable the Halloween theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomHalloween" name="EnableRandomHalloween" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Symbols</span>
</label>
<div class="fieldDescription">Displays additional Halloween symbols randomly distributed across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomHalloweenMobile" name="EnableRandomHalloweenMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Symbols on Mobile</span>
</label>
<div class="fieldDescription">Displays additional Halloween symbols randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="HalloweenCount">Symbol Count</label>
<input is="emby-input" type="number" id="HalloweenCount" name="HalloweenCount" />
<div class="fieldDescription">Number of additional Halloween symbols (pumpkins, ghosts, etc.) on screen (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationHalloween" name="EnableDifferentDurationHalloween" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the movement speed.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Hearts</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableHearts" name="EnableHearts" type="checkbox" is="emby-checkbox" />
<span>Enable Hearts Seasonal</span>
</label>
<div class="fieldDescription">Enable the Hearts theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomHearts" name="EnableRandomHearts" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Symbols</span>
</label>
<div class="fieldDescription">Displays additional hearts randomly distributed across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomHeartsMobile" name="EnableRandomHeartsMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Symbols on Mobile</span>
</label>
<div class="fieldDescription">Displays additional hearts randomly distributed across the screen. on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="HeartsCount">Symbol Count</label>
<input is="emby-input" type="number" id="HeartsCount" name="HeartsCount" />
<div class="fieldDescription">Number of additional floating hearts.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationHearts" name="EnableDifferentDurationHearts" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the floating speed.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Christmas</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableChristmas" name="EnableChristmas" type="checkbox" is="emby-checkbox" />
<span>Enable Christmas Seasonal</span>
</label>
<div class="fieldDescription">Enable the Christmas theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomChristmas" name="EnableRandomChristmas" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Christmas</span>
</label>
<div class="fieldDescription">Displays additional Christmas-themed icons randomly distributed across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomChristmasMobile" name="EnableRandomChristmasMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Christmas on Mobile</span>
</label>
<div class="fieldDescription">Displays additional Christmas-themed icons randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="ChristmasCount">Symbol Count</label>
<input is="emby-input" type="number" id="ChristmasCount" name="ChristmasCount" />
<div class="fieldDescription">Number of additional Christmas symbols.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationChristmas" name="EnableDifferentDurationChristmas" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the movement speed.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Santa</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSanta" name="EnableSanta" type="checkbox" is="emby-checkbox" />
<span>Enable Santa Seasonal</span>
</label>
<div class="fieldDescription">Enable the Santa theme in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SantaSnowflakes">Snowflakes Count</label>
<input is="emby-input" type="number" id="SantaSnowflakes" name="SantaSnowflakes" />
<div class="fieldDescription">Number of snowflakes accompanying Santa (recommended values: 300-600).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SantaSnowflakesMobile">Snowflakes Count (Mobile)</label>
<input is="emby-input" type="number" id="SantaSnowflakesMobile" name="SantaSnowflakesMobile" />
<div class="fieldDescription">Number of snowflakes accompanying Santa on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SantaSpeed">Santa Speed (seconds)</label>
<input is="emby-input" type="number" id="SantaSpeed" name="SantaSpeed" step="0.1" />
<div class="fieldDescription">Time in seconds for Santa to cross the screen (recommended values: 5 - 15).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SantaSpeedMobile">Santa Speed Mobile (seconds)</label>
<input is="emby-input" type="number" id="SantaSpeedMobile" name="SantaSpeedMobile" step="0.1" />
<div class="fieldDescription">Time in seconds for Santa to cross the screen on mobile (recommended values: 3 - 12).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SantaSnowFallSpeed">Snowfall Speed</label>
<input is="emby-input" type="number" id="SantaSnowFallSpeed" name="SantaSnowFallSpeed" step="0.1" />
<div class="fieldDescription">Speed of the falling snow (recommended values: 0-5).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MaxSantaRestTime">Max Santa Rest Time (seconds)</label>
<input is="emby-input" type="number" id="MaxSantaRestTime" name="MaxSantaRestTime" step="0.1" />
<div class="fieldDescription">Maximum time Santa waits before appearing again.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MinSantaRestTime">Min Santa Rest Time (seconds)</label>
<input is="emby-input" type="number" id="MinSantaRestTime" name="MinSantaRestTime" step="0.1" />
<div class="fieldDescription">Minimum time Santa waits before appearing again.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MaxPresentFallSpeed">Max Present Fall Speed (seconds)</label>
<input is="emby-input" type="number" id="MaxPresentFallSpeed" name="MaxPresentFallSpeed" step="0.1" />
<div class="fieldDescription">Maximum speed of falling presents.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MinPresentFallSpeed">Min Present Fall Speed (seconds)</label>
<input is="emby-input" type="number" id="MinPresentFallSpeed" name="MinPresentFallSpeed" step="0.1" />
<div class="fieldDescription">Minimum speed of falling presents.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Easter</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableEaster" name="EnableEaster" type="checkbox" is="emby-checkbox" />
<span>Enable Easter Seasonal</span>
</label>
<div class="fieldDescription">Enable the Easter theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomEaster" name="EnableRandomEaster" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Easter Eggs</span>
</label>
<div class="fieldDescription">Displays additional easter eggs randomly distributed across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomEasterMobile" name="EnableRandomEasterMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Easter Eggs on Mobile</span>
</label>
<div class="fieldDescription">Displays additional easter eggs randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="EasterEggCount">Egg Count</label>
<input is="emby-input" type="number" id="EasterEggCount" name="EasterEggCount" />
<div class="fieldDescription">Number of additional Easter eggs (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationEaster" name="EnableDifferentDurationEaster" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the movement speed.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EasterBunny" name="EasterBunny" type="checkbox" is="emby-checkbox" />
<span>Enable Bunny</span>
</label>
<div class="fieldDescription">Show the Easter Bunny hopping across the screen.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="BunnyDuration">Bunny Duration (ms)</label>
<input is="emby-input" type="number" id="BunnyDuration" name="BunnyDuration" />
<div class="fieldDescription">Time in milliseconds for one hop cycle.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="HopHeight">Hop Height (px)</label>
<input is="emby-input" type="number" id="HopHeight" name="HopHeight" />
<div class="fieldDescription">Height of the bunny's hop in pixels.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MinBunnyRestTime">Min Bunny Rest Time (ms)</label>
<input is="emby-input" type="number" id="MinBunnyRestTime" name="MinBunnyRestTime" />
<div class="fieldDescription">Minimum time the bunny waits before appearing again.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="MaxBunnyRestTime">Max Bunny Rest Time (ms)</label>
<input is="emby-input" type="number" id="MaxBunnyRestTime" name="MaxBunnyRestTime" />
<div class="fieldDescription">Maximum time the bunny waits before appearing again.</div>
</div>
</details>
</details>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>
All changes require a page refresh (ctrl + r or F5) after saving for changes to take effect. <br/>
If old settings persist, please force clear browser cache.
</div>
</div>
<div> <div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button"> <button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span> <span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
<span>${ButtonCancel}</span>
</button> </button>
<div class="fieldDescription" style="margin-top: 1em;">Please reload the page (F5) after saving for changes to take effect.</div>
</div> </div>
</form> </form>
</div> </div>
@@ -51,8 +527,94 @@
.addEventListener('pageshow', function() { .addEventListener('pageshow', function() {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
document.querySelector('#IsEnabled').checked = config.IsEnabled;
document.querySelector('#SelectedSeason').value = config.SelectedSeason; document.querySelector('#SelectedSeason').value = config.SelectedSeason;
document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection; document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection;
// Advanced Config
// Autumn
document.querySelector('#EnableAutumn').checked = config.Autumn.EnableAutumn;
document.querySelector('#AutumnLeafCount').value = config.Autumn.LeafCount;
document.querySelector('#EnableRandomLeaves').checked = config.Autumn.EnableRandomLeaves;
document.querySelector('#EnableRandomLeavesMobile').checked = config.Autumn.EnableRandomLeavesMobile;
document.querySelector('#EnableDifferentDurationAutumn').checked = config.Autumn.EnableDifferentDuration;
document.querySelector('#EnableRotation').checked = config.Autumn.EnableRotation;
// Snowflakes
document.querySelector('#SnowflakesCount').value = config.Snowflakes.SnowflakeCount;
document.querySelector('#EnableSnowflakes').checked = config.Snowflakes.EnableSnowflakes;
document.querySelector('#EnableRandomSnowflakes').checked = config.Snowflakes.EnableRandomSnowflakes;
document.querySelector('#EnableRandomSnowflakesMobile').checked = config.Snowflakes.EnableRandomSnowflakesMobile;
document.querySelector('#EnableColoredSnowflakes').checked = config.Snowflakes.EnableColoredSnowflakes;
document.querySelector('#EnableDifferentDurationSnowflakes').checked = config.Snowflakes.EnableDifferentDuration;
// Snowfall
document.querySelector('#EnableSnowfall').checked = config.Snowfall.EnableSnowfall;
document.querySelector('#SnowfallCount').value = config.Snowfall.SnowflakesCount;
document.querySelector('#SnowfallCountMobile').value = config.Snowfall.SnowflakesCountMobile;
document.querySelector('#SnowfallSpeed').value = config.Snowfall.Speed;
// Snowstorm
document.querySelector('#EnableSnowstorm').checked = config.Snowstorm.EnableSnowstorm;
document.querySelector('#SnowstormCount').value = config.Snowstorm.SnowflakesCount;
document.querySelector('#SnowstormCountMobile').value = config.Snowstorm.SnowflakesCountMobile;
document.querySelector('#SnowstormSpeed').value = config.Snowstorm.Speed;
document.querySelector('#SnowstormHorizontalWind').value = config.Snowstorm.HorizontalWind;
document.querySelector('#SnowstormVerticalVariation').value = config.Snowstorm.VerticalVariation;
// Fireworks
document.querySelector('#EnableFireworks').checked = config.Fireworks.EnableFireworks;
document.querySelector('#FireworksParticles').value = config.Fireworks.ParticleCount;
document.querySelector('#FireworksInterval').value = config.Fireworks.LaunchInterval;
document.querySelector('#ScrollFireworks').checked = config.Fireworks.ScrollFireworks;
document.querySelector('#MinFireworks').value = config.Fireworks.MinFireworks;
document.querySelector('#MaxFireworks').value = config.Fireworks.MaxFireworks;
// Halloween
document.querySelector('#EnableHalloween').checked = config.Halloween.EnableHalloween;
document.querySelector('#HalloweenCount').value = config.Halloween.SymbolCount;
document.querySelector('#EnableRandomHalloween').checked = config.Halloween.EnableRandomSymbols;
document.querySelector('#EnableRandomHalloweenMobile').checked = config.Halloween.EnableRandomSymbolsMobile;
document.querySelector('#EnableDifferentDurationHalloween').checked = config.Halloween.EnableDifferentDuration;
// Hearts
document.querySelector('#EnableHearts').checked = config.Hearts.EnableHearts;
document.querySelector('#HeartsCount').value = config.Hearts.SymbolCount;
document.querySelector('#EnableRandomHearts').checked = config.Hearts.EnableRandomSymbols;
document.querySelector('#EnableRandomHeartsMobile').checked = config.Hearts.EnableRandomSymbolsMobile;
document.querySelector('#EnableDifferentDurationHearts').checked = config.Hearts.EnableDifferentDuration;
// Christmas
document.querySelector('#EnableChristmas').checked = config.Christmas.EnableChristmas;
document.querySelector('#ChristmasCount').value = config.Christmas.SymbolCount;
document.querySelector('#EnableRandomChristmas').checked = config.Christmas.EnableRandomChristmas;
document.querySelector('#EnableRandomChristmasMobile').checked = config.Christmas.EnableRandomChristmasMobile;
document.querySelector('#EnableDifferentDurationChristmas').checked = config.Christmas.EnableDifferentDuration;
// Santa
document.querySelector('#EnableSanta').checked = config.Santa.EnableSanta;
document.querySelector('#SantaSnowflakes').value = config.Santa.SnowflakesCount;
document.querySelector('#SantaSnowflakesMobile').value = config.Santa.SnowflakesCountMobile;
document.querySelector('#SantaSpeed').value = config.Santa.SantaSpeed;
document.querySelector('#SantaSpeedMobile').value = config.Santa.SantaSpeedMobile;
document.querySelector('#SantaSnowFallSpeed').value = config.Santa.SnowFallSpeed;
document.querySelector('#MaxSantaRestTime').value = config.Santa.MaxSantaRestTime;
document.querySelector('#MinSantaRestTime').value = config.Santa.MinSantaRestTime;
document.querySelector('#MaxPresentFallSpeed').value = config.Santa.MaxPresentFallSpeed;
document.querySelector('#MinPresentFallSpeed').value = config.Santa.MinPresentFallSpeed;
// Easter
document.querySelector('#EnableEaster').checked = config.Easter.EnableEaster;
document.querySelector('#EasterEggCount').value = config.Easter.EggCount;
document.querySelector('#EnableRandomEaster').checked = config.Easter.EnableRandomEaster;
document.querySelector('#EnableRandomEasterMobile').checked = config.Easter.EnableRandomEasterMobile;
document.querySelector('#EnableDifferentDurationEaster').checked = config.Easter.EnableDifferentDuration;
document.querySelector('#EasterBunny').checked = config.Easter.EnableBunny;
document.querySelector('#BunnyDuration').value = config.Easter.BunnyDuration;
document.querySelector('#HopHeight').value = config.Easter.HopHeight;
document.querySelector('#MinBunnyRestTime').value = config.Easter.MinBunnyRestTime;
document.querySelector('#MaxBunnyRestTime').value = config.Easter.MaxBunnyRestTime;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
}); });
@@ -61,8 +623,94 @@
.addEventListener('submit', function(e) { .addEventListener('submit', function(e) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
config.IsEnabled = document.querySelector('#IsEnabled').checked;
config.SelectedSeason = document.querySelector('#SelectedSeason').value; config.SelectedSeason = document.querySelector('#SelectedSeason').value;
config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked; config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked;
// Advanced Config
// Autumn
config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
// Snowflakes
config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value);
config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked;
config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked;
config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked;
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked;
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked;
// Snowfall
config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked;
config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value);
config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value);
config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value);
// Snowstorm
config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked;
config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value);
config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value);
config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value);
config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
// Fireworks
config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked;
config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value);
config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value);
config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked;
config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value);
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value);
// Halloween
config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked;
config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value);
config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked;
config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked;
config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked;
// Hearts
config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked;
config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value);
config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked;
config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked;
config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked;
// Christmas
config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked;
config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value);
config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked;
config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked;
config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked;
// Santa
config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked;
config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value);
config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value);
config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value);
config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value);
config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
// Easter
config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked;
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value);
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked;
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked;
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked;
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked;
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value);
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value);
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value);
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value);
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) { ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result); Dashboard.processPluginConfigurationUpdateResult(result);
}); });

View File

@@ -12,13 +12,14 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title> <Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.0.0.0</Version> <Version>1.4.0.0</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="$(JellyfinVersion)" /> <PackageReference Include="Jellyfin.Controller" Version="$(JellyfinVersion)" />
<PackageReference Include="Jellyfin.Model" Version="$(JellyfinVersion)" /> <PackageReference Include="Jellyfin.Model" Version="$(JellyfinVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,57 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Jellyfin.Plugin.Seasonals.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// The main plugin.
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
private readonly ScriptInjector _scriptInjector;
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILoggerFactory loggerFactory)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
_scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>());
_scriptInjector.Inject();
}
/// <inheritdoc />
public override string Name => "Seasonals";
/// <inheritdoc />
public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
/// <summary>
/// Gets the current plugin instance.
/// </summary>
public static Plugin? Instance { get; private set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return
[
new PluginPageInfo
{
Name = Name,
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
}
];
}
}

View File

@@ -13,7 +13,7 @@ public class ScriptInjector
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; private readonly ILogger<ScriptInjector> _logger;
private const string ScriptTag = "<script src=\"Seasonals/Resources/seasonals.js\"></script>"; private const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
private const string Marker = "</body>"; private const string Marker = "</body>";
/// <summary> /// <summary>
@@ -30,7 +30,8 @@ public class ScriptInjector
/// <summary> /// <summary>
/// Injects the script tag into index.html if it's not already present. /// Injects the script tag into index.html if it's not already present.
/// </summary> /// </summary>
public void Inject() /// <returns>True if injection was successful or already present, false otherwise.</returns>
public bool Inject()
{ {
try try
{ {
@@ -38,21 +39,21 @@ public class ScriptInjector
if (string.IsNullOrEmpty(webPath)) if (string.IsNullOrEmpty(webPath))
{ {
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped."); _logger.LogWarning("Could not find Jellyfin web path. Script injection skipped.");
return; return false;
} }
var indexPath = Path.Combine(webPath, "index.html"); var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
_logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath); _logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath);
return; return false;
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (content.Contains(ScriptTag, StringComparison.Ordinal)) if (content.Contains(ScriptTag, StringComparison.Ordinal))
{ {
_logger.LogInformation("Seasonals script already injected."); _logger.LogInformation("Seasonals script already injected.");
return; return true;
} }
// Insert before the closing body tag // Insert before the closing body tag
@@ -60,41 +61,48 @@ public class ScriptInjector
if (string.Equals(newContent, content, StringComparison.Ordinal)) if (string.Equals(newContent, content, StringComparison.Ordinal))
{ {
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped."); _logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
return; return false;
} }
File.WriteAllText(indexPath, newContent); File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully injected Seasonals script into index.html."); _logger.LogInformation("Successfully injected Seasonals script into index.html.");
return true;
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Access was denied when attempting to inject script into index.html. Automatic injection failed. Please ensure the Jellyfin web directory is writable by the process, or manually add the script tag: {ScriptTag}", ScriptTag);
return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error injecting Seasonals script."); _logger.LogError(ex, "Error injecting Seasonals script.");
return false;
} }
} }
/// <summary> /// <summary>
/// Removes the script tag from index.html. /// Removes the script tag from index.html.
/// </summary> /// </summary>
public void Remove() public bool Remove()
{ {
try try
{ {
var webPath = GetWebPath(); var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath)) if (string.IsNullOrEmpty(webPath))
{ {
return; return false;
} }
var indexPath = Path.Combine(webPath, "index.html"); var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
return; return false;
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (!content.Contains(ScriptTag, StringComparison.Ordinal)) if (!content.Contains(ScriptTag, StringComparison.Ordinal))
{ {
return; return true;
} }
// Try to remove with newline first, then just the tag to ensure clean removal // Try to remove with newline first, then just the tag to ensure clean removal
@@ -103,10 +111,17 @@ public class ScriptInjector
File.WriteAllText(indexPath, newContent); File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully removed Seasonals script from index.html."); _logger.LogInformation("Successfully removed Seasonals script from index.html.");
return true;
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Permission denied when attempting to remove script from index.html.");
return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error removing Seasonals script."); _logger.LogError(ex, "Error removing Seasonals script.");
return false;
} }
} }

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Jellyfin.Plugin.Seasonals.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// The main plugin.
/// </summary>
public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
private readonly ScriptInjector _scriptInjector;
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public SeasonalsPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILoggerFactory loggerFactory)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
_loggerFactory = loggerFactory;
_scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>());
if (Configuration.IsEnabled)
{
if (!_scriptInjector.Inject())
{
TryRegisterFallback(loggerFactory.CreateLogger("FileTransformationFallback"));
}
}
else
{
if (!_scriptInjector.Remove()) {
TryRemoveFallback(loggerFactory.CreateLogger("FileTransformationFallback"));
}
}
}
/// <inheritdoc />
public override void UpdateConfiguration(BasePluginConfiguration configuration)
{
var oldConfig = Configuration;
base.UpdateConfiguration(configuration);
if (Configuration.IsEnabled != oldConfig.IsEnabled)
{
if (Configuration.IsEnabled)
{
if (!_scriptInjector.Inject())
{
TryRegisterFallback(_loggerFactory.CreateLogger("FileTransformationFallback"));
}
}
else
{
if (!_scriptInjector.Remove()) {
TryRemoveFallback(_loggerFactory.CreateLogger("FileTransformationFallback"));
}
}
}
}
/// <inheritdoc />
public override string Name => "Seasonals";
/// <inheritdoc />
public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
/// <summary>
/// Gets the current plugin instance.
/// </summary>
public static SeasonalsPlugin? Instance { get; private set; }
/// <summary>
/// Callback method for FileTransformation plugin.
/// </summary>
/// <param name="payload">The payload containing the file contents.</param>
/// <returns>The modified file contents.</returns>
public static string TransformIndexHtml(JObject payload)
{
// CRITICAL: Always return original content if something fails or is null
string? originalContents = payload?["contents"]?.ToString();
if (string.IsNullOrEmpty(originalContents))
{
return originalContents ?? string.Empty;
}
try
{
var html = originalContents;
const string scriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
// MARK: Remember me to remove legacy script tag support in future versions...
const string legacyScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\"></script>";
if (Instance?.Configuration.IsEnabled != true)
{
// Remove script if present
if (html.Contains("seasonals.js", StringComparison.Ordinal))
{
return html.Replace(scriptTag, "", StringComparison.OrdinalIgnoreCase)
.Replace(legacyScriptTag, "", StringComparison.OrdinalIgnoreCase);
}
return html;
}
if (!html.Contains("seasonals.js", StringComparison.Ordinal))
{
var inject = $"{scriptTag}\n<body";
return html.Replace("<body", inject, StringComparison.OrdinalIgnoreCase);
}
return html;
}
catch
{
// On error, return original content to avoid breaking the UI
return originalContents;
}
}
private void TryRegisterFallback(ILogger logger)
{
try
{
// Find the FileTransformation assembly across all load contexts
var assembly = AssemblyLoadContext.All
.SelectMany(x => x.Assemblies)
.FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false);
if (assembly == null)
{
logger.LogWarning("FileTransformation plugin not found. Fallback injection skipped.");
return;
}
var type = assembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (type == null)
{
logger.LogWarning("Jellyfin.Plugin.FileTransformation.PluginInterface not found.");
return;
}
var method = type.GetMethod("RegisterTransformation");
if (method == null)
{
logger.LogWarning("RegisterTransformation method not found.");
return;
}
// Create JObject payload directly using Newtonsoft.Json
var payload = new JObject
{
{ "id", Id.ToString() },
{ "fileNamePattern", "index.html" },
{ "callbackAssembly", this.GetType().Assembly.FullName },
{ "callbackClass", this.GetType().FullName },
{ "callbackMethod", nameof(TransformIndexHtml) }
};
// Invoke RegisterTransformation with the JObject payload
method.Invoke(null, new object[] { payload });
logger.LogInformation("Successfully registered fallback transformation via FileTransformation plugin.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to register fallback transformation.");
}
}
private void TryRemoveFallback(ILogger logger)
{
try
{
var assembly = AssemblyLoadContext.All
.SelectMany(x => x.Assemblies)
.FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false);
if (assembly == null)
{
logger.LogWarning("FileTransformation plugin not found. Fallback removal skipped.");
return;
}
var type = assembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (type == null)
{
logger.LogWarning("Jellyfin.Plugin.FileTransformation.PluginInterface not found.");
return;
}
var method = type.GetMethod("RemoveTransformation");
if (method == null)
{
logger.LogWarning("RemoveTransformation method not found.");
return;
}
Guid pluginId = Id is Guid g ? g : Guid.Parse(Id.ToString());
method.Invoke(null, new object[] { pluginId });
logger.LogInformation("Successfully unregistered fallback transformation via FileTransformation plugin.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to unregister fallback transformation.");
}
}
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
new PluginPageInfo
{
Name = Name,
EnableInMainMenu = true,
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
}
};
}
}

View File

@@ -1,12 +1,18 @@
.autumn-container { .autumn-container {
display: block; display: block;
pointer-events: none; position: fixed;
z-index: 0;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
} }
.leaf { .leaf {
position: fixed; position: fixed;
z-index: 15;
top: -10%; top: -10%;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;

View File

@@ -1,9 +1,11 @@
const leaves = true; // enable/disable leaves const config = window.SeasonalsPluginConfig?.Autumn || {};
const randomLeaves = true; // enable random leaves
const randomLeavesMobile = false; // enable random leaves on mobile devices const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable leaves
const enableDiffrentDuration = true; // enable different duration for the random leaves const randomLeaves = config.EnableRandomLeaves !== undefined ? config.EnableRandomLeaves : true; // enable random leaves
const enableRotation = false; // enable/disable leaf rotation const randomLeavesMobile = config.EnableRandomLeavesMobile !== undefined ? config.EnableRandomLeavesMobile : false; // enable random leaves on mobile devices (Warning: High values may affect performance)
const leafCount = 25; // count of random extra leaves 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -46,23 +48,23 @@ observer.observe(document.body, {
const images = [ const images = [
"Seasonals/Resources/autumn_images/acorn1.png", "/Seasonals/Resources/autumn_images/acorn1.png",
"Seasonals/Resources/autumn_images/acorn2.png", "/Seasonals/Resources/autumn_images/acorn2.png",
"Seasonals/Resources/autumn_images/leaf1.png", "/Seasonals/Resources/autumn_images/leaf1.png",
"Seasonals/Resources/autumn_images/leaf2.png", "/Seasonals/Resources/autumn_images/leaf2.png",
"Seasonals/Resources/autumn_images/leaf3.png", "/Seasonals/Resources/autumn_images/leaf3.png",
"Seasonals/Resources/autumn_images/leaf4.png", "/Seasonals/Resources/autumn_images/leaf4.png",
"Seasonals/Resources/autumn_images/leaf5.png", "/Seasonals/Resources/autumn_images/leaf5.png",
"Seasonals/Resources/autumn_images/leaf6.png", "/Seasonals/Resources/autumn_images/leaf6.png",
"Seasonals/Resources/autumn_images/leaf7.png", "/Seasonals/Resources/autumn_images/leaf7.png",
"Seasonals/Resources/autumn_images/leaf8.png", "/Seasonals/Resources/autumn_images/leaf8.png",
"Seasonals/Resources/autumn_images/leaf9.png", "/Seasonals/Resources/autumn_images/leaf9.png",
"Seasonals/Resources/autumn_images/leaf10.png", "/Seasonals/Resources/autumn_images/leaf10.png",
"Seasonals/Resources/autumn_images/leaf11.png", "/Seasonals/Resources/autumn_images/leaf11.png",
"Seasonals/Resources/autumn_images/leaf12.png", "/Seasonals/Resources/autumn_images/leaf12.png",
"Seasonals/Resources/autumn_images/leaf13.png", "/Seasonals/Resources/autumn_images/leaf13.png",
"Seasonals/Resources/autumn_images/leaf14.png", "/Seasonals/Resources/autumn_images/leaf14.png",
"Seasonals/Resources/autumn_images/leaf15.png", "/Seasonals/Resources/autumn_images/leaf15.png",
]; ];
function addRandomLeaves(count) { function addRandomLeaves(count) {

View File

@@ -1,12 +1,18 @@
.christmas-container { .christmas-container {
display: block; display: block;
pointer-events: none; position: fixed;
z-index: 0;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
} }
.christmas { .christmas {
position: fixed; position: fixed;
z-index: 15;
top: -10%; top: -10%;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;

View File

@@ -1,8 +1,10 @@
const christmas = true; // enable/disable christmas const config = window.SeasonalsPluginConfig?.Christmas || {};
const randomChristmas = true; // enable random Christmas
const randomChristmasMobile = false; // enable random Christmas on mobile devices const christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
const enableDiffrentDuration = true; // enable different duration for the random Christmas symbols const randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random Christmas
const christmasCount = 25; // count of random extra 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

@@ -1,12 +1,18 @@
.easter-container { .easter-container {
display: block; display: block;
pointer-events: none; position: fixed;
z-index: 0;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
} }
.hopping-rabbit { .hopping-rabbit {
position: fixed; position: fixed;
z-index: 15;
bottom: 10px; bottom: 10px;
width: 70px; width: 70px;
overflow: hidden; overflow: hidden;
@@ -23,6 +29,7 @@
.easter { .easter {
position: fixed; position: fixed;
z-index: 15;
top: -10%; top: -10%;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;
@@ -40,6 +47,7 @@
} }
.easter img { .easter img {
z-index: 15;
height: auto; height: auto;
width: 20px; width: 20px;
} }

View File

@@ -1,14 +1,16 @@
const easter = true; // enable/disable easter const config = window.SeasonalsPluginConfig?.Easter || {};
const randomEaster = true; // enable random easter
const randomEasterMobile = false; // enable random easter on mobile devices
const enableDiffrentDuration = true; // enable different duration for the random easter
const easterEggCount = 20; // count of random extra easter
const bunny = true; // enable/disable hopping bunny const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
const bunnyDuration = 12000; // duration of the bunny animation in ms const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter
const hopHeight = 12; // height of the bunny hops in px const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance)
const minBunnyRestTime = 2000; // minimum time the bunny rests in ms const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter
const maxBunnyRestTime = 5000; // maximum time the bunny rests in ms const easterEggCount = config.EggCount || 20; // count of random extra easter
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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -59,20 +61,20 @@ observer.observe(document.body, {
const images = [ const images = [
"Seasonals/Resources/easter_images/egg_1.png", "/Seasonals/Resources/easter_images/egg_1.png",
"Seasonals/Resources/easter_images/egg_2.png", "/Seasonals/Resources/easter_images/egg_2.png",
"Seasonals/Resources/easter_images/egg_3.png", "/Seasonals/Resources/easter_images/egg_3.png",
"Seasonals/Resources/easter_images/egg_4.png", "/Seasonals/Resources/easter_images/egg_4.png",
"Seasonals/Resources/easter_images/egg_5.png", "/Seasonals/Resources/easter_images/egg_5.png",
"Seasonals/Resources/easter_images/egg_6.png", "/Seasonals/Resources/easter_images/egg_6.png",
"Seasonals/Resources/easter_images/egg_7.png", "/Seasonals/Resources/easter_images/egg_7.png",
"Seasonals/Resources/easter_images/egg_8.png", "/Seasonals/Resources/easter_images/egg_8.png",
"Seasonals/Resources/easter_images/egg_9.png", "/Seasonals/Resources/easter_images/egg_9.png",
"Seasonals/Resources/easter_images/egg_10.png", "/Seasonals/Resources/easter_images/egg_10.png",
"Seasonals/Resources/easter_images/egg_11.png", "/Seasonals/Resources/easter_images/egg_11.png",
"Seasonals/Resources/easter_images/egg_12.png", "/Seasonals/Resources/easter_images/egg_12.png",
]; ];
const rabbit = "Seasonals/Resources/easter_images/easter-bunny.png"; const rabbit = "/Seasonals/Resources/easter_images/easter-bunny.png";
function addRandomEaster(count) { function addRandomEaster(count) {
const easterContainer = document.querySelector('.easter-container'); // get the leave container const easterContainer = document.querySelector('.easter-container'); // get the leave container

View File

@@ -34,6 +34,7 @@
transform: translateY(0); transform: translateY(0);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateY(calc(var(--trailEndY) - var(--trailStartY))); transform: translateY(calc(var(--trailEndY) - var(--trailStartY)));
opacity: 0; opacity: 0;
@@ -46,6 +47,7 @@
opacity: 1; opacity: 1;
transform: translate(0, 0); transform: translate(0, 0);
} }
100% { 100% {
opacity: 0; opacity: 0;
transform: translate(var(--x), var(--y)); transform: translate(var(--x), var(--y));

View File

@@ -1,9 +1,11 @@
const fireworks = true; // enable/disable fireworks const config = window.SeasonalsPluginConfig?.Fireworks || {};
const scrollFireworks = true; // enable fireworks to scroll with page content
const particlesPerFirework = 50; // count of particles per firework const fireworks = config.EnableFireworks !== undefined ? config.EnableFireworks : true; // enable/disable fireworks
const minFireworks = 3; // minimum number of simultaneous fireworks const scrollFireworks = config.ScrollFireworks !== undefined ? config.ScrollFireworks : true; // enable fireworks to scroll with page content
const maxFireworks = 6; // maximum number of simultaneous fireworks const particlesPerFirework = config.ParticleCount || 50; // count of particles per firework (Warning: High values may affect performance)
const intervalOfFireworks = 3200; // interval for the fireworks in milliseconds 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
// array of color palettes for the fireworks // array of color palettes for the fireworks
const colorPalettes = [ const colorPalettes = [

View File

@@ -1,14 +1,19 @@
.halloween-container { .halloween-container {
display: block; display: block;
pointer-events: none; position: fixed;
z-index: 0;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
} }
.halloween { .halloween {
position: fixed; position: fixed;
bottom: -10%; bottom: -10%;
z-index: 0; z-index: 15;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;

View File

@@ -1,8 +1,10 @@
const halloween = true; // enable/disable halloween const config = window.SeasonalsPluginConfig?.Halloween || {};
const randomSymbols = true; // enable more random symbols
const randomSymbolsMobile = false; // enable random symbols on mobile devices const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween : true; // enable/disable halloween
const enableDiffrentDuration = true; // enable different duration for the random halloween symbols const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
const halloweenCount = 25; // count of random extra 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -44,9 +46,9 @@ observer.observe(document.body, {
const images = [ const images = [
"Seasonals/Resources/halloween_images/ghost_20x20.png", "/Seasonals/Resources/halloween_images/ghost_20x20.png",
"Seasonals/Resources/halloween_images/bat_20x20.png", "/Seasonals/Resources/halloween_images/bat_20x20.png",
"Seasonals/Resources/halloween_images/pumpkin_20x20.png", "/Seasonals/Resources/halloween_images/pumpkin_20x20.png",
]; ];
function addRandomSymbols(count) { function addRandomSymbols(count) {

View File

@@ -1,14 +1,19 @@
.hearts-container { .hearts-container {
display: block; display: block;
pointer-events: none; position: fixed;
z-index: 0;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
} }
.heart { .heart {
position: fixed; position: fixed;
bottom: -10%; bottom: -10%;
z-index: 0; z-index: 15;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;

View File

@@ -1,8 +1,10 @@
const hearts = true; // enable/disable hearts const config = window.SeasonalsPluginConfig?.Hearts || {};
const randomSymbols = true; // enable more random symbols
const randomSymbolsMobile = false; // enable random symbols on mobile devices const hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
const enableDiffrentDuration = true; // enable different animation duration for random symbols const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
const heartsCount = 25; // count of random extra 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

@@ -1,10 +1,13 @@
.santa-container { .santa-container {
position: fixed; position: fixed;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background: transparent; background: transparent;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
z-index: 10;
} }
#snowfallCanvas { #snowfallCanvas {
@@ -18,6 +21,7 @@
.santa { .santa {
position: fixed; position: fixed;
z-index: 12;
width: 220px; width: 220px;
height: auto; height: auto;
z-index: 1000; z-index: 1000;

View File

@@ -1,13 +1,15 @@
const santaIsFlying = true; // enable/disable santa const config = window.SeasonalsPluginConfig?.Santa || {};
let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = 250; // count of snowflakes on mobile devices const santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
const snowFallSpeed = 3; // speed of snowfall (recommended values: 0-5) let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const santaSpeed = 10; // speed of santa in seconds (recommended values: 5000-15000) const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const santaSpeedMobile = 8; // speed of santa on mobile devices in seconds const snowFallSpeed = config.SnowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
const maxSantaRestTime = 8; // maximum time santa rests in seconds const santaSpeed = config.SantaSpeed || 10; // speed of santa in seconds (recommended values: 5-15)
const minSantaRestTime = 3; // minimum time santa rests in seconds const santaSpeedMobile = config.SantaSpeedMobile || 8; // speed of santa on mobile devices in seconds
const maxPresentFallSpeed = 5; // maximum speed of falling presents in seconds const maxSantaRestTime = config.MaxSantaRestTime || 8; // maximum time santa rests in seconds
const minPresentFallSpeed = 2; // minimum speed of falling presents 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 msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
let isMobile = false; // flag to detect mobile devices let isMobile = false; // flag to detect mobile devices
@@ -152,18 +154,18 @@ function updateSnowflakes() {
// credits: flaticon.com // credits: flaticon.com
const presentImages = [ const presentImages = [
'Seasonals/Resources/santa_images/gift1.png', '/Seasonals/Resources/santa_images/gift1.png',
'Seasonals/Resources/santa_images/gift2.png', '/Seasonals/Resources/santa_images/gift2.png',
'Seasonals/Resources/santa_images/gift3.png', '/Seasonals/Resources/santa_images/gift3.png',
'Seasonals/Resources/santa_images/gift4.png', '/Seasonals/Resources/santa_images/gift4.png',
'Seasonals/Resources/santa_images/gift5.png', '/Seasonals/Resources/santa_images/gift5.png',
'Seasonals/Resources/santa_images/gift6.png', '/Seasonals/Resources/santa_images/gift6.png',
'Seasonals/Resources/santa_images/gift7.png', '/Seasonals/Resources/santa_images/gift7.png',
'Seasonals/Resources/santa_images/gift8.png', '/Seasonals/Resources/santa_images/gift8.png',
]; ];
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm // credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
const santaImage = 'Seasonals/Resources/santa_images/santa.gif'; const santaImage = '/Seasonals/Resources/santa_images/santa.gif';
function createSantaElement() { function createSantaElement() {

View File

@@ -121,6 +121,7 @@ function loadThemeJS(jsPath) {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = jsPath; script.src = jsPath;
script.defer = true;
script.onerror = () => { script.onerror = () => {
console.error(`Failed to load JS: ${jsPath}`); console.error(`Failed to load JS: ${jsPath}`);
@@ -159,8 +160,10 @@ async function initializeTheme() {
const response = await fetch('/Seasonals/Config'); const response = await fetch('/Seasonals/Config');
if (response.ok) { if (response.ok) {
const config = await response.json(); const config = await response.json();
automateThemeSelection = config.automateSeasonSelection; automateThemeSelection = config.AutomateSeasonSelection;
defaultTheme = config.selectedSeason; defaultTheme = config.SelectedSeason;
window.SeasonalsPluginConfig = config;
console.log('Seasonals Config loaded:', config);
} else { } else {
console.error('Failed to fetch Seasonals config'); console.error('Failed to fetch Seasonals config');
} }
@@ -169,7 +172,7 @@ async function initializeTheme() {
} }
let currentTheme; let currentTheme;
if (!automateThemeSelection) { if (automateThemeSelection === false) {
currentTheme = defaultTheme; currentTheme = defaultTheme;
} else { } else {
currentTheme = determineCurrentTheme(); currentTheme = determineCurrentTheme();
@@ -177,7 +180,7 @@ async function initializeTheme() {
console.log(`Selected theme: ${currentTheme}`); console.log(`Selected theme: ${currentTheme}`);
if (currentTheme === 'none') { if (!currentTheme || currentTheme === 'none') {
console.log('No theme selected.'); console.log('No theme selected.');
removeSelf(); removeSelf();
return; return;
@@ -200,7 +203,4 @@ async function initializeTheme() {
} }
//document.addEventListener('DOMContentLoaded', initializeTheme);
document.addEventListener('DOMContentLoaded', () => {
initializeTheme(); initializeTheme();
});

View File

@@ -5,6 +5,9 @@
background: transparent; background: transparent;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
top: 0;
left: 0;
z-index: 10;
} }
#snowfallCanvas { #snowfallCanvas {

View File

@@ -1,7 +1,9 @@
const snowfall = true; // enable/disable snowfall const config = window.SeasonalsPluginConfig?.Snowfall || {};
let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = 250; // count of snowflakes on mobile devices const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
const snowFallSpeed = 3; // speed of snowfall (recommended values: 0-5) 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 msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

@@ -1,12 +1,18 @@
.snowflakes { .snowflakes {
display: block; display: block;
pointer-events: none; position: fixed;
z-index: 0;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
} }
.snowflake { .snowflake {
position: fixed; position: fixed;
z-index: 15;
top: -10%; top: -10%;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;

View File

@@ -1,9 +1,11 @@
const snowflakes = true; // enable/disable snowflakes const config = window.SeasonalsPluginConfig?.Snowflakes || {};
const randomSnowflakes = true; // enable random Snowflakes
const randomSnowflakesMobile = false; // enable random Snowflakes on mobile devices const snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
const enableColoredSnowflakes = true; // enable colored snowflakes on mobile devices const randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random Snowflakes
const enableDiffrentDuration = true; // enable different animation duration for random symbols const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices
const snowflakeCount = 25; // count of random extra snowflakes 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

@@ -1,7 +1,20 @@
.snowflake { .snowstorm-container {
position: fixed; position: fixed;
background-color: rgba(255, 255, 255, 0.8); width: 100%;
border-radius: 50%; height: 100vh;
background: transparent;
overflow: hidden;
pointer-events: none;
top: 0;
left: 0;
z-index: 10;
}
#snowfallCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; pointer-events: none;
opacity: 0.7;
} }

View File

@@ -1,9 +1,11 @@
const snowstorm = true; // enable/disable snowstorm const config = window.SeasonalsPluginConfig?.Snowstorm || {};
let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = 250; // count of snowflakes on mobile devices const snowstorm = config.enableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
const snowFallSpeed = 6; // speed of snowfall (recommended values: 4-8) let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const horizontalWind = 4; // horizontal wind speed (recommended value: 4) const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const verticalVariation = 2; // vertical variation (recommended value: 2) 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)
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -64,7 +66,7 @@ function initializeCanvas() {
const container = document.querySelector('.snowstorm-container'); const container = document.querySelector('.snowstorm-container');
if (!container) { if (!container) {
console.error('Error: No element with class "snowfall-container" found.'); console.error('Error: No element with class "snowstorm-container" found.');
return; return;
} }

122
README.md
View File

@@ -2,7 +2,9 @@
Jellyfin Seasonals is a plugin that adds seasonal themes to your Jellyfin web interface. Depending on the configuration, it automatically selects a theme based on the current date or allows you to manually set a default theme. Jellyfin Seasonals is a plugin that adds seasonal themes to your Jellyfin web interface. Depending on the configuration, it automatically selects a theme based on the current date or allows you to manually set a default theme.
This plugin is based on my manual mod (see the `manual` branch), which builds up on the awesome work of [BobHasNoSoul-jellyfin-mods](https://github.com/BobHasNoSoul/jellyfin-mods). This plugin is based on my manual mod (see the [legacy branch](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/legacy)), which builds up on the awesome work of [BobHasNoSoul-jellyfin-mods](https://github.com/BobHasNoSoul/jellyfin-mods).
![logo](https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/logo.png)
--- ---
@@ -12,14 +14,19 @@ This plugin is based on my manual mod (see the `manual` branch), which builds up
- [Features](#features) - [Features](#features)
- [Overview](#overview) - [Overview](#overview)
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Client Compatibility](#client-compatibility)
- [Configuration](#configuration)
- [Automatic Theme Selection](#automatic-theme-selection)
- [Theme Settings](#theme-settings)
- [Build Process](#build-process) - [Build Process](#build-process)
- [Troubleshooting](#troubleshooting)
- [Effects Not Showing](#effects-not-showing)
- [Docker Permission Issues](#docker-permission-issues)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Legacy Manual Installation](#legacy-manual-installation) - [Legacy Manual Installation](#legacy-manual-installation)
- [Installation](#installation-1) - [Installation](#installation-1)
- [Theme Settings](#theme-settings) - [Usage](#usage)
- [Additional Directory: Separate Single Seasonals](#additional-directory-separate-single-seasonals) - [Additional Directory: Separate Single Seasonals](#additional-directory-separate-single-seasonals)
- [Troubleshooting](#troubleshooting)
--- ---
@@ -63,34 +70,71 @@ This plugin is based on my manual mod (see the `manual` branch), which builds up
## Installation ## Installation
This plugin is based on Jellyfin Version `10.11.x`
To install this plugin, you will first need to add the repository in Jellyfin. To install this plugin, you will first need to add the repository in Jellyfin.
1. Open your Jellyfin Dashboard. 1. Open your Jellyfin Dashboard.
2. Navigate to **Plugins** > **Repositories**. 2. Navigate to **Plugins** > **Manage Repositories**.
3. Click the **+** sign to add a new repository. 3. Click the **+ New Repository** button to add a new repository.
4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field: 4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field:
```bash ```bash
https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-seasonals/main/manifest.json https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/manifest.json
``` ```
5. Click **Save**. 5. Click **Add**.
6. Go to the **Catalog** tab at the top. 6. Go to the **Available** tab at the top.
7. Under **General**, find the **Seasonals** plugin. 7. Find the **Seasonals** plugin (Under **General**)
8. Click on it and select **Install**. 8. Click on it and select **Install**.
9. **Restart your Jellyfin server.** 9. **Restart your Jellyfin server.**
10. **You may need to refresh your browser page** (F5 or Ctrl+R) to see the changes. 10. **You may need to refresh your browser page** (F5 or Ctrl+R) to see the changes.
## Usage ## Client Compatibility
Since this plugin relies on modifying the web interface (CSS/JS injection), it only works on clients that use the web wrapper. Native clients that use their own UI rendering engine are not supported.
| Client Platform | Status | Notes |
| :--- | :---: | :--- |
| **Web Browsers** (Firefox, Chrome etc.) | ✅ | Direct JS injection |
| **Jellyfin Media Player** (Windows/Linux/macOS) | ✅ | Uses jellyfin web |
| **Android App** | ✅ | Uses a web wrapper |
| **iOS App** | ✅ | Uses a web wrapper |
| **Android TV / Fire TV** | ❌ | **Not supported.** Uses a native Java/Kotlin UI. |
| **Roku** | ❌ | **Not supported.** Uses a native UI. |
| **Swiftfin** (iOS/tvOS) | ❌ | **Not supported.** Uses a native Swift UI. |
| **Kodi** (via Jellyfin Addon) | ❌ | **Not supported.** Uses Kodi's native skinning engine. |
## Configuration
After installation and restart: After installation and restart:
1. Go to **Dashboard** > **Plugins** > **Seasonals**. 1. Go to **Dashboard** > **Plugins** > **Seasonals**.
2. **Enable Seasonals**: Toggle the plugin on or off. 2. **Enable Seasonals**: Toggle the plugin on or off.
3. **Automatic Selection**: 3. **Automatic Selection**:
* If enabled, the plugin selects the theme based on the current date (e.g., Snow in Winter, Hearts in February). * If enabled, the plugin selects the theme based on the current date (e.g., Snow in Winter, Hearts in February). See the table below for details.
* If disabled, you can manually select a theme from the dropdown list. * If disabled, you can manually select a theme from the dropdown list.
4. **Save** your settings. 4. **Save** your settings.
5. **Reload your browser page** (F5 or Ctrl+R) to see the changes. 5. **Reload your browser page** (F5 or Ctrl+R) to see the changes.
## Automatic Theme Selection
If automatic selection is enabled, the following themes are applied based on the date. Specific holiday events take precedence over general seasonal themes.:
| Theme | Active Period | Description |
| :--- | :--- | :--- |
| **`santa`** | Dec 22 Dec 27 | Christmas theme |
| **`fireworks`** | Dec 28 Jan 05 | New Year's celebration |
| **`hearts`** | Feb 10 Feb 18 | Valentine's Day |
| **`easter`** | Mar 25 Apr 25 | Easter theme |
| **`halloween`** | Oct 24 Nov 05 | Halloween theme |
| **`snowflakes`** | December (Remainder) | General December winter theme |
| **`snowfall`** | January & February | General winter theme (outside of holidays) |
| **`autumn`** | Sep, Oct, Nov | Fall theme (when not Halloween) |
| **`none`** | All other dates | Default appearance |
> **Note:** Holiday themes (like `santa` or `fireworks`) override monthly seasonal themes (like `snowflakes`).
## Theme Settings
Each theme contains additional settings to customize its behavior. Expand the advanced configuration section to configure each theme, adjust parameters like particle count, animation speed etc.
## Build Process ## Build Process
If you want to build the plugin yourself: If you want to build the plugin yourself:
@@ -103,6 +147,58 @@ If you want to build the plugin yourself:
``` ```
4. The compiled DLL and resources will be in bin/Publish. 4. The compiled DLL and resources will be in bin/Publish.
## Troubleshooting
### Effects Not Showing
1. **Verify plugin installation**:
- Check that the plugin appears in the jellyfin admin panel
- Ensure that the plugin is enabled and active
2. **Clear browser cache**:
- Force refresh browser (Ctrl+F5)
- Clear jellyfin web client cache (--> mostly you have to clear the whole browser cache)
### Docker Permission Issues
If you encounter the message `Access was denied when attempting to inject script into index.html. Automatic direct injection failed. Automatic direct insertion failed. The system will now attempt to use the File Transformation plugin.` in the log or similar permission errors in Docker:
**Option 1: Use File Transformation Plugin (Recommended)**
Seasonals now automatically detects and uses the [File Transformation](https://github.com/IAmParadox27/jellyfin-plugin-file-transformation) plugin (v2.5.0.0+) if it's installed. This eliminates permission issues by transforming content at runtime without modifying files on disk.
**Installation Steps:**
1. Install the File Transformation plugin from the Jellyfin plugin catalog
2. Restart Jellyfin
3. Seasonals will automatically detect and use it (no configuration needed)
4. Check logs to confirm: Look for "Successfully registered transformation with File Transformation plugin"
**Benefits:**
- No file permission issues in Docker environments
- Works with read-only web directories
- Survives Jellyfin updates without re-injection
- No manual file modifications required
**Option 2: Fix File Permissions**
```bash
# Find the actual index.html location
docker exec -it jellyfin find / -name index.html
# Fix ownership (replace 'jellyfin' with your container name and adjust user:group if needed)
docker exec -it --user root jellyfin chown jellyfin:jellyfin /jellyfin/jellyfin-web/index.html
# Restart container
docker restart jellyfin
```
**Option 3: Manual Volume Mapping**
```bash
# Extract index.html from container
docker cp jellyfin:/jellyfin/jellyfin-web/index.html /path/to/jellyfin/config/index.html
# Add to docker-compose.yml volumes section:
volumes:
- /path/to/jellyfin/config/index.html:/jellyfin/jellyfin-web/index.html
```
## Contributing ## Contributing
Feel free to contribute to this project by creating pull requests or reporting issues. Feel free to contribute to this project by creating pull requests or reporting issues.
@@ -127,7 +223,7 @@ Feel free to contribute to this project by creating pull requests or reporting i
<script src="seasonals/seasonals.js"></script> <script src="seasonals/seasonals.js"></script>
``` ```
2. **Deploy Files** 2. **Deploy Files**
Place the seasonals folder (including seasonals.js, CSS, and additional JavaScript files for each theme [this one](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/main/seasonals)) inside the Jellyfin web server directory (labeld with "web"). Place the seasonals folder (including seasonals.js, CSS, and additional JavaScript files for each theme [this one](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/legacy/seasonals)) inside the Jellyfin web server directory (labeld with "web").
3. **Configure Themes** 3. **Configure Themes**
Customize the theme-configs.js file to modify or add new themes. The default configuration is shown below: Customize the theme-configs.js file to modify or add new themes. The default configuration is shown below:

View File

@@ -1,624 +0,0 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v9.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v9.0": {
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
"dependencies": {
"Jellyfin.Controller": "10.11.0",
"Jellyfin.Model": "10.11.0"
},
"runtime": {
"Jellyfin.Plugin.Seasonals.dll": {}
}
},
"BitFaster.Caching/2.5.4": {
"runtime": {
"lib/net6.0/BitFaster.Caching.dll": {
"assemblyVersion": "2.5.4.0",
"fileVersion": "2.5.4.0"
}
}
},
"Diacritics/4.0.17": {
"runtime": {
"lib/net9.0/Diacritics.dll": {
"assemblyVersion": "4.0.17.0",
"fileVersion": "4.0.17.0"
}
}
},
"ICU4N/60.1.0-alpha.356": {
"dependencies": {
"J2N": "2.0.0",
"Microsoft.Extensions.Caching.Memory": "9.0.10"
},
"runtime": {
"lib/netstandard2.0/ICU4N.dll": {
"assemblyVersion": "60.0.0.0",
"fileVersion": "60.1.0.0"
}
}
},
"ICU4N.Transliterator/60.1.0-alpha.356": {
"dependencies": {
"ICU4N": "60.1.0-alpha.356"
},
"runtime": {
"lib/netstandard2.0/ICU4N.Transliterator.dll": {
"assemblyVersion": "60.0.0.0",
"fileVersion": "60.1.0.0"
}
}
},
"J2N/2.0.0": {
"runtime": {
"lib/net6.0/J2N.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
}
}
},
"Jellyfin.Common/10.11.0": {
"dependencies": {
"Jellyfin.Model": "10.11.0",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/MediaBrowser.Common.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Controller/10.11.0": {
"dependencies": {
"BitFaster.Caching": "2.5.4",
"Jellyfin.Common": "10.11.0",
"Jellyfin.MediaEncoding.Keyframes": "10.11.0",
"Jellyfin.Model": "10.11.0",
"Jellyfin.Naming": "10.11.0",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
"Microsoft.Extensions.Configuration.Binder": "9.0.10",
"System.Threading.Tasks.Dataflow": "9.0.10"
},
"runtime": {
"lib/net9.0/MediaBrowser.Controller.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Data/10.11.0": {
"dependencies": {
"Jellyfin.Database.Implementations": "10.11.0",
"Microsoft.Extensions.Logging": "9.0.10"
},
"runtime": {
"lib/net9.0/Jellyfin.Data.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Database.Implementations/10.11.0": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Relational": "9.0.10",
"Polly": "8.6.4"
},
"runtime": {
"lib/net9.0/Jellyfin.Database.Implementations.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Extensions/10.11.0": {
"dependencies": {
"Diacritics": "4.0.17",
"ICU4N.Transliterator": "60.1.0-alpha.356"
},
"runtime": {
"lib/net9.0/Jellyfin.Extensions.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"NEbml": "1.1.0.5"
},
"runtime": {
"lib/net9.0/Jellyfin.MediaEncoding.Keyframes.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Model/10.11.0": {
"dependencies": {
"Jellyfin.Data": "10.11.0",
"Jellyfin.Extensions": "10.11.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"System.Globalization": "4.3.0",
"System.Text.Json": "9.0.10"
},
"runtime": {
"lib/net9.0/MediaBrowser.Model.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Naming/10.11.0": {
"dependencies": {
"Jellyfin.Common": "10.11.0",
"Jellyfin.Model": "10.11.0"
},
"runtime": {
"lib/net9.0/Emby.Naming.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Microsoft.EntityFrameworkCore/9.0.10": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.10",
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.10",
"Microsoft.Extensions.Caching.Memory": "9.0.10",
"Microsoft.Extensions.Logging": "9.0.10"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
"assemblyVersion": "9.0.10.0",
"fileVersion": "9.0.1025.47514"
}
}
},
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
"assemblyVersion": "9.0.10.0",
"fileVersion": "9.0.1025.47514"
}
}
},
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {},
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
"dependencies": {
"Microsoft.EntityFrameworkCore": "9.0.10",
"Microsoft.Extensions.Caching.Memory": "9.0.10",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
"Microsoft.Extensions.Logging": "9.0.10"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
"assemblyVersion": "9.0.10.0",
"fileVersion": "9.0.1025.47514"
}
}
},
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Caching.Memory/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "9.0.10",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"Microsoft.Extensions.Options": "9.0.10",
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Caching.Memory.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Configuration.Binder.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.DependencyInjection/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.DependencyInjection.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
"runtime": {
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Logging/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.10",
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"Microsoft.Extensions.Options": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Logging.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Options/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Options.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Primitives/9.0.10": {
"runtime": {
"lib/net9.0/Microsoft.Extensions.Primitives.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.NETCore.Platforms/1.1.0": {},
"Microsoft.NETCore.Targets/1.1.0": {},
"NEbml/1.1.0.5": {
"runtime": {
"lib/netstandard2.0/NEbml.Core.dll": {
"assemblyVersion": "1.1.0.5",
"fileVersion": "1.1.0.5"
}
}
},
"Polly/8.6.4": {
"dependencies": {
"Polly.Core": "8.6.4"
},
"runtime": {
"lib/net6.0/Polly.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.6.4.5033"
}
}
},
"Polly.Core/8.6.4": {
"runtime": {
"lib/net8.0/Polly.Core.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.6.4.5033"
}
}
},
"System.Globalization/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Text.Json/9.0.10": {},
"System.Threading.Tasks.Dataflow/9.0.10": {}
}
},
"libraries": {
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"BitFaster.Caching/2.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ==",
"path": "bitfaster.caching/2.5.4",
"hashPath": "bitfaster.caching.2.5.4.nupkg.sha512"
},
"Diacritics/4.0.17": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FmMvVQRsfon+x5P+dxz4mvV8wt45xr25EAOCkuo/Cjtc7lVYV5cZUSsNXwmKQpwO+TokIHpzxb8ENpqrm4yBlQ==",
"path": "diacritics/4.0.17",
"hashPath": "diacritics.4.0.17.nupkg.sha512"
},
"ICU4N/60.1.0-alpha.356": {
"type": "package",
"serviceable": true,
"sha512": "sha512-YMZtDnjcqWzziOKiE7w6Ma7Rl5vuFDxzOsUlHh1QyfghbNEIZQOLRs9MMfwCWAjX6n9UitrF6vLXy55Z5q+4Fg==",
"path": "icu4n/60.1.0-alpha.356",
"hashPath": "icu4n.60.1.0-alpha.356.nupkg.sha512"
},
"ICU4N.Transliterator/60.1.0-alpha.356": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lFOSO6bbEtB6HkWMNDJAq+rFwVyi9g6xVc5O/2xHa6iZnV7wLVDqCbaQ4W4vIeBSQZAafqhxciaEkmAvSdzlCg==",
"path": "icu4n.transliterator/60.1.0-alpha.356",
"hashPath": "icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512"
},
"J2N/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-M5bwDajAARZiyqupU+rHQJnsVLxNBOHJ8vKYHd8LcLIb1FgLfzzcJvc31Qo5Xz/GEHFjDF9ScjKL/ks/zRTXuA==",
"path": "j2n/2.0.0",
"hashPath": "j2n.2.0.0.nupkg.sha512"
},
"Jellyfin.Common/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-TitN7+qWFt2l0V5b+KTRt7ABDCvfZdvSC6qBG1uHS18Y80xrbrSCJ9O6BH/of310h6a4lxKlQjUtTPHCzeG2AA==",
"path": "jellyfin.common/10.11.0",
"hashPath": "jellyfin.common.10.11.0.nupkg.sha512"
},
"Jellyfin.Controller/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WV+PQy9AHdZLvYqUsNq6ZyQoxaiaEWLz0EwZGOiu8xSrepQLFope2U1VFHVCNbARwesg7s/B+9uB03eXDsQw9w==",
"path": "jellyfin.controller/10.11.0",
"hashPath": "jellyfin.controller.10.11.0.nupkg.sha512"
},
"Jellyfin.Data/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-YEz7/85b98Rj14IJJIVqmzJsi69LDOKo4Ox+VHbh1vj3tkWomSPayzvG3kyU8I0yFMrd6+Ta55C20kZ2XC7vQg==",
"path": "jellyfin.data/10.11.0",
"hashPath": "jellyfin.data.10.11.0.nupkg.sha512"
},
"Jellyfin.Database.Implementations/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-oLblVZzqF9zuLMdfqp8pbusSVQq6b40/RcHjGF1hxYozVNEi+UhiDX8aJipYBOrh33FFAofoQq468BvZixpPcw==",
"path": "jellyfin.database.implementations/10.11.0",
"hashPath": "jellyfin.database.implementations.10.11.0.nupkg.sha512"
},
"Jellyfin.Extensions/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1ufj+Rm0Bn6C990i2wwiT5UHPZfD65GOtJK6NcDU//DDMbuoGX1LQZxuCx+rhhRg1XdHPWzYASARYyNlFQa6cg==",
"path": "jellyfin.extensions/10.11.0",
"hashPath": "jellyfin.extensions.10.11.0.nupkg.sha512"
},
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/OBcg4Qj825elOGNj5bNRfABKzfAf4qNQj0/d/DwhG/+V/wsKuxS0Pc/xOEagVVjXOnqGPZz/+k8D4UvnvMoHw==",
"path": "jellyfin.mediaencoding.keyframes/10.11.0",
"hashPath": "jellyfin.mediaencoding.keyframes.10.11.0.nupkg.sha512"
},
"Jellyfin.Model/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-h+61RSXn4sk8fS6Zx9RkDyVnI5VnNbrsR2p8WcvybtNSW2pgU2uZ9pwEv2awD3ifX69weqYpQLMh91f6aidW2A==",
"path": "jellyfin.model/10.11.0",
"hashPath": "jellyfin.model.10.11.0.nupkg.sha512"
},
"Jellyfin.Naming/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2++xSbhdFSb1J3XySjC6UU+uII6OdKc0DfkYx/E1oN7mSjoftyZR8eU045kVWBwsAxr+UcMI6t2DYfES2tJkRA==",
"path": "jellyfin.naming/10.11.0",
"hashPath": "jellyfin.naming.10.11.0.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WjjxVyOTVs85V7SUe+lZjtGOEeVzF4RO8amrqL4adgbyThNq7vGCFzPw8buZj44gHeQYD5V/uZ/6XuqG9Jq+kA==",
"path": "microsoft.entityframeworkcore/9.0.10",
"hashPath": "microsoft.entityframeworkcore.9.0.10.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-I3TWAs5Lbzmzu8S0T6qXhzBiO3CznYLrfE59W0npkqNHfWGH8CgA66LrHMWxWOXVTD4145QwYqiWNCdLwpJ1Ew==",
"path": "microsoft.entityframeworkcore.abstractions/9.0.10",
"hashPath": "microsoft.entityframeworkcore.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mXNl0Gg3l3zGrClLCHepB+b7rYVuFfB9qQJwya0dUSHFuR1T0jMD8KxuNVyhQSfoWIepanhzjcG8TUNGXOcU0Q==",
"path": "microsoft.entityframeworkcore.analyzers/9.0.10",
"hashPath": "microsoft.entityframeworkcore.analyzers.9.0.10.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-IJNrG5vdmFUvHR8FLLFg9AWpuE8qW1DTEN+fNLGbNTu6cnpZzzqU6+aknAGtTSAEVWosJ3BZ3TOO9wpifUvv3A==",
"path": "microsoft.entityframeworkcore.relational/9.0.10",
"hashPath": "microsoft.entityframeworkcore.relational.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-cL6iTxgJ4u5zP3eFOdBiDAtmE/B2WKTRhyJfEne7n6qvHpsMwwIDxljs210mWSO1ucBy7lR1Lo7/7kjeZeLcqQ==",
"path": "microsoft.extensions.caching.abstractions/9.0.10",
"hashPath": "microsoft.extensions.caching.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Memory/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2iuzwIoCoqZJfH2VLk1xvlQS4PQDEuhj4dWiGb+Qpt1vHFHyffp497cTO6ucsV54W/h4JmM1vzDBv8pVAFazZg==",
"path": "microsoft.extensions.caching.memory/9.0.10",
"hashPath": "microsoft.extensions.caching.memory.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ad3JxmFj0uxuFa1CT6oxTCC1lQ0xeRuOvzBRFT/I/ofIXVOnNsH/v2GZkAJWhlpZqKUvSexQZzp3EEAB2CdtJg==",
"path": "microsoft.extensions.configuration.abstractions/9.0.10",
"hashPath": "microsoft.extensions.configuration.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-D6Kng+9I+w1SQPxJybc6wzw9nnnyUQPutycjtI0svv1RHaWOpUk9PPlwIRfhhoQZ3yihejkEI2wNv/7VnVtkGA==",
"path": "microsoft.extensions.configuration.binder/9.0.10",
"hashPath": "microsoft.extensions.configuration.binder.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iEtXCkNd5XhjNJAOb/wO4IhDRdLIE2CsPxZggZQWJ/q2+sa8dmEPC393nnsiqdH8/4KV8Xn25IzgKPR1UEQ0og==",
"path": "microsoft.extensions.dependencyinjection/9.0.10",
"hashPath": "microsoft.extensions.dependencyinjection.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-r9waLiOPe9ZF1PvzUT+RDoHvpMmY8MW+lb4lqjYGObwKpnyPMLI3odVvlmshwuZcdoHynsGWOrCPA0hxZ63lIA==",
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.10",
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Logging/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UBXHqE9vyptVhaFnT1R7YJKCve7TqVI10yjjUZBNGMlW2lZ4c031Slt9hxsOzWCzlpPxxIFyf1Yk4a6Iubxx7w==",
"path": "microsoft.extensions.logging/9.0.10",
"hashPath": "microsoft.extensions.logging.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MFUPv/nN1rAQ19w43smm6bbf0JDYN/1HEPHoiMYY50pvDMFpglzWAuoTavByDmZq7UuhjaxwrET3joU69ZHoHQ==",
"path": "microsoft.extensions.logging.abstractions/9.0.10",
"hashPath": "microsoft.extensions.logging.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Options/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zMNABt8eBv0B0XrWjFy9nZNgddavaOeq3ZdaD5IlHhRH65MrU7HM+Hd8GjWE3e2VDGFPZFfSAc6XVXC17f9fOA==",
"path": "microsoft.extensions.options/9.0.10",
"hashPath": "microsoft.extensions.options.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Primitives/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3pl8D1O5ZwMpDkZAT2uXrhQ6NipkwEgDLMFuURiHTf72TvkoMP61QYH3Vk1yrzVHnHBdNZk3cQACz8Zc7YGNhQ==",
"path": "microsoft.extensions.primitives/9.0.10",
"hashPath": "microsoft.extensions.primitives.9.0.10.nupkg.sha512"
},
"Microsoft.NETCore.Platforms/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
"path": "microsoft.netcore.platforms/1.1.0",
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
},
"Microsoft.NETCore.Targets/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
"path": "microsoft.netcore.targets/1.1.0",
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
},
"NEbml/1.1.0.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-svtqDc+hue9kbnqNN2KkK4om/hDrc7K127cNb5FIYfgKgzo+JNDPXNLp8NioCchHhBO3lxWd4Cp/iiZZ3aoUqg==",
"path": "nebml/1.1.0.5",
"hashPath": "nebml.1.1.0.5.nupkg.sha512"
},
"Polly/8.6.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-uuBsDoBw0oYrMe3uTWRjkT2sIkKh+ZZnnDrLb4Z+QANfeA4+7FJacx6E8CY5GAxXRoSgFrvUADEAQ7DPF6fGiw==",
"path": "polly/8.6.4",
"hashPath": "polly.8.6.4.nupkg.sha512"
},
"Polly.Core/8.6.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-4AWqYnQ2TME0E+Mzovt1Uu+VyvpR84ymUldMcPw7Mbj799Phaag14CKrMtlJGx5jsvYP+S3oR1QmysgmXoD5cw==",
"path": "polly.core/8.6.4",
"hashPath": "polly.core.8.6.4.nupkg.sha512"
},
"System.Globalization/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
"path": "system.globalization/4.3.0",
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
},
"System.Runtime/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"path": "system.runtime/4.3.0",
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
},
"System.Text.Json/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XM02ZBnzxk7Ti6l9YRy8Bp639wANqJzJzw4W4VYiCh+HXY9hBOWkGB4k89OLP/s/RxgM02P4a/mWcJTgFiLf1Q==",
"path": "system.text.json/9.0.10",
"hashPath": "system.text.json.9.0.10.nupkg.sha512"
},
"System.Threading.Tasks.Dataflow/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-k1o6v6V3+4mznSnPnO0FBaRjiAPL1ouKPfPQH7hO9Z2SwJHt8E45F4wX5yQh1aeja1JHPYEungQedXibng654Q==",
"path": "system.threading.tasks.dataflow/9.0.10",
"hashPath": "system.threading.tasks.dataflow.9.0.10.nupkg.sha512"
}
}
}

View File

@@ -1,7 +1,7 @@
--- ---
name: "Seasonals" name: "Seasonals"
guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4" guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"
version: "1.0.0.0" version: "1.3.4.0"
targetAbi: "10.11.0.0" targetAbi: "10.11.0.0"
framework: "net9.0" framework: "net9.0"
overview: "Seasonal effects for Jellyfin" overview: "Seasonal effects for Jellyfin"
@@ -12,4 +12,4 @@ owner: "CodeDevMLH"
artifacts: artifacts:
- "Jellyfin.Plugin.Seasonals.dll" - "Jellyfin.Plugin.Seasonals.dll"
changelog: > changelog: >
Initial release Added Advanced Configuration UI for customizing individual seasonal effects.

View File

@@ -6,13 +6,61 @@
"overview": "Seasonal effects for Jellyfin", "overview": "Seasonal effects for Jellyfin",
"owner": "CodeDevMLH", "owner": "CodeDevMLH",
"category": "General", "category": "General",
"imageUrl": "", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [ "versions": [
{
"version": "1.4.0.0",
"changelog": "- settings linked directly in the main menu\n- renamed main plugin script\n- added enable/disable toggle for the entire plugin",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "205606075eec5f93d3da37efaecdeab5",
"timestamp": "2025-12-28T19:11:04Z"
},
{
"version": "1.3.4.0",
"changelog": "- some fixes for js loading\n- adapted config page descriptions",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.4.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "4869a9a0c08317d2cb0e0fc8f454cf2b",
"timestamp": "2025-12-24T02:02:15Z"
},
{
"version": "1.3.3.0",
"changelog": "- fixed: load config",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.3.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "9908c9357b2d8850e6349ce134bad2bd",
"timestamp": "2025-12-18T00:11:39Z"
},
{
"version": "1.3.0.0",
"changelog": "- fixed: image paths to ensure proper loading of resources.\n- fix: z-index issue to ensure seasonal effects appear above other UI elements.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "362fd94ab11f03e345a911a95d2d763b",
"timestamp": "2025-12-17T21:46:53Z"
},
{
"version": "1.2.0.0",
"changelog": "Advanced settings added: Users can now customize the intensity and speed of seasonal effects through the settings panel.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.2.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "5e18022c914c06072035dc0b4429e0fe",
"timestamp": "2025-12-17T20:59:37Z"
},
{
"version": "1.1.0.0",
"changelog": "Bug fixing: Fix path, fix injection issue, added File Transformator as fallback if direct injection is blocked due to permissions.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.1.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be2e93364396b6e0e02368d5a7db53bc",
"timestamp": "2025-12-17T15:32:08Z"
},
{ {
"version": "1.0.0.0", "version": "1.0.0.0",
"changelog": "Initial release", "changelog": "Initial release",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/bin/Publish/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.0.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be6d06a959b3e18e5058a6d8fb6d800c", "checksum": "be6d06a959b3e18e5058a6d8fb6d800c",
"timestamp": "2025-12-15T15:33:15Z" "timestamp": "2025-12-15T15:33:15Z"
} }