Compare commits

..

7 Commits

Author SHA1 Message Date
CodeDevMLH
5c10583601 Update manifest.json for release v1.7.2.0 [skip ci] 2026-02-23 00:34:14 +00:00
CodeDevMLH
20dcf08bda Bump version to 1.7.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-23 01:33:23 +01:00
CodeDevMLH
e4b3a132b1 Add seasonal effects for Pi Day, Pride, Rain, and Storm; enhance existing styles
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
- Introduced new CSS and JS files for Pi Day, Pride, Rain, and Storm effects.
- Updated existing seasonal styles (e.g., Halloween, Hearts, Resurrection) to improve performance with 'contain: layout paint'.
- Enhanced animations for seasonal effects, including adjustments to keyframes and element creation logic.
- Added configuration options for new effects in the main seasonals.js file.
- Updated test-site.html to include new seasonal options in the dropdown.
2026-02-23 01:31:52 +01:00
CodeDevMLH
63ec6d5e52 Update disabled options descriptions in seasonal configuration [skip ci] 2026-02-21 16:06:24 +01:00
CodeDevMLH
ec89f2d48d Update manifest.json for release v1.7.1.5 [skip ci] 2026-02-21 14:28:31 +00:00
CodeDevMLH
61b21de566 Bump version to 1.7.1.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-21 15:27:36 +01:00
CodeDevMLH
590f2c3606 Add Cherry Blossom option and update Resurrection description in seasonal options 2026-02-21 15:27:24 +01:00
33 changed files with 1468 additions and 89 deletions

View File

@@ -32,6 +32,12 @@ public class PluginConfiguration : BasePluginConfiguration
Summer = new SummerOptions();
CherryBlossom = new CherryBlossomOptions();
Carnival = new CarnivalOptions();
PiDay = new PiDayOptions();
Eurovision = new EurovisionOptions();
Storm = new StormOptions();
Pride = new PrideOptions();
EarthDay = new EarthDayOptions();
Rain = new RainOptions();
}
/// <summary>
@@ -57,7 +63,7 @@ public class PluginConfiguration : BasePluginConfiguration
/// <summary>
/// Gets or sets the seasonal rules configuration as JSON.
/// </summary>
public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"}]";
public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"},{\"Name\":\"Cherry Blossom\",\"StartDay\":1,\"StartMonth\":4,\"EndDay\":30,\"EndMonth\":4,\"Theme\":\"cherryblossom\"}]";
/// <summary>
/// Gets or sets the Seasonals options.
@@ -77,6 +83,12 @@ public class PluginConfiguration : BasePluginConfiguration
public SummerOptions Summer { get; set; }
public CherryBlossomOptions CherryBlossom { get; set; }
public CarnivalOptions Carnival { get; set; }
public PiDayOptions PiDay { get; set; }
public EurovisionOptions Eurovision { get; set; }
public StormOptions Storm { get; set; }
public PrideOptions Pride { get; set; }
public EarthDayOptions EarthDay { get; set; }
public RainOptions Rain { get; set; }
}
public class AutumnOptions
@@ -234,3 +246,55 @@ public class CherryBlossomOptions
public bool EnableRandomCherryBlossomMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class PiDayOptions
{
public int SymbolCount { get; set; } = 50;
public bool EnablePiDay { get; set; } = true;
public bool EnableRandomPiDay { get; set; } = true;
public bool EnableRandomPiDayMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class EurovisionOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableEurovision { get; set; } = true;
public bool EnableRandomEurovision { get; set; } = true;
public bool EnableRandomEurovisionMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableColorfulNotes { get; set; } = true;
public string EurovisionColors { get; set; } = "#ff0026ff,#17a6ffff,#32d432ff,#FFD700,#f0821bff,#f826f8ff";
public int EurovisionGlowSize { get; set; } = 8;
}
public class StormOptions
{
public int RaindropCount { get; set; } = 300;
public int RaindropCountMobile { get; set; } = 150;
public bool EnableStorm { get; set; } = true;
public bool EnableLightning { get; set; } = true;
public double RainSpeed { get; set; } = 1;
}
public class PrideOptions
{
public bool EnablePride { get; set; } = true;
public int HeartCount { get; set; } = 20;
public int HeartSize { get; set; } = 2;
public bool ColorHeader { get; set; } = true;
}
public class EarthDayOptions
{
public bool EnableEarthDay { get; set; } = true;
public int VineCount { get; set; } = 4;
}
public class RainOptions
{
public bool EnableRain { get; set; } = true;
public int RaindropCount { get; set; } = 300;
public int RaindropCountMobile { get; set; } = 150;
public double RainSpeed { get; set; } = 1;
}

View File

@@ -65,17 +65,22 @@
<option value="santa">Santa (flying santa & snowfall)</option>
<option value="autumn">Autumn (falling leaves)</option>
<option value="easter">Easter</option>
<option value="resurrection">Resurrection - by Bioflash257</option>
<option value="summer">Summer (Bubbles)</option>
<option value="spring">Spring</option>
<option value="carnival">Carnival (Confetti)</option>
<option value="championships" disabled>European/World Championships (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="patrick" disabled>St. Patrick's Day (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Pride (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Oscar Awards (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Eurovison Awards (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Sugar Feast (Eid al-Fitr, Ramadan) (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="cherryblossom">Cherry Blossom</option>
<option value="resurrection">Resurrection by Bioflash257</option>
<option value="championships" disabled>European/World Championships (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
<option value="patrick" disabled>St. Patrick's Day (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
<option value="earthday">Earth Day (Growing Vines)</option>
<option value="eurovision">Eurovision (Dancing Notes)</option>
<option value="oscar" disabled>Oscar Awards (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
<option value="piday">Pi-Day (Matrix Rain)</option>
<option value="pride">Pride (Rainbow Border)</option>
<option value="rain">Rain (Pure Rain)</option>
<option value="storm">Storm (Heavy Rain & Lightning (Epilepsy Warning!))</option>
<option value="sugarfeast" disabled>Sugar Feast (Eid al-Fitr, Ramadan) (not implemented yet. Please commit ideas/implementation in a issue or PR)</option>
</select>
<div class="fieldDescription">The season to display if automation is disabled or no "Auto Selection" rule matches the current date.</div>
</div>
@@ -774,6 +779,211 @@
<div class="fieldDescription">Randomize the falling speed of cherry blossoms.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Earth Day</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableEarthDay" name="EnableEarthDay" type="checkbox" is="emby-checkbox" />
<span>Enable Earth Day Seasonal</span>
</label>
<div class="fieldDescription">Enable the Earth Day theme in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="EarthDayVineCount">Vine Count</label>
<input is="emby-input" type="number" id="EarthDayVineCount" name="EarthDayVineCount" />
<div class="fieldDescription">Number of animated vines (if enabled).</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Eurovision / Musik</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableEurovision" name="EnableEurovision" type="checkbox" is="emby-checkbox" />
<span>Enable Eurovision Seasonal</span>
</label>
<div class="fieldDescription">Enable the Eurovision/Music theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomEurovision" name="EnableRandomEurovision" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Music Notes</span>
</label>
<div class="fieldDescription">Displays dancing music notes.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomEurovisionMobile" name="EnableRandomEurovisionMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Music Notes on Mobile</span>
</label>
<div class="fieldDescription">Displays dancing music notes on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="EurovisionSymbolCount">Symbol Count</label>
<input is="emby-input" type="number" id="EurovisionSymbolCount" name="EurovisionSymbolCount" />
<div class="fieldDescription">Number of additional dancing music notes (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationEurovision" name="EnableDifferentDurationEurovision" type="checkbox" is="emby-checkbox" />
<span>Enable Different Falling Speed</span>
</label>
<div class="fieldDescription">Randomize the movement speed of music notes.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableColorfulNotes" name="EnableColorfulNotes" type="checkbox" is="emby-checkbox" />
<span>Colorful Notes Mode</span>
</label>
<div class="fieldDescription">If checked, notes will pick colors from the array below. If unchecked, notes will be white.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="EurovisionColors">Color Array (Comma-separated)</label>
<input is="emby-input" type="text" id="EurovisionColors" name="EurovisionColors" />
<div class="fieldDescription">Example: #FFB6C1,#87CEFA,#98FB98 (Hex or CSS colors separated by commas).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="EurovisionGlowSize">Note Glow/Shadow Size (px)</label>
<input is="emby-input" type="number" id="EurovisionGlowSize" name="EurovisionGlowSize" />
<div class="fieldDescription">Set the text-shadow size of the notes. Set this to 0 to remove the shadow/glow completely.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Pi-Day</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnablePiDay" name="EnablePiDay" type="checkbox" is="emby-checkbox" />
<span>Enable Pi-Day Seasonal</span>
</label>
<div class="fieldDescription">Enable the Pi-Day theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomPiDay" name="EnableRandomPiDay" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Pi Symbols</span>
</label>
<div class="fieldDescription">Displays additional digital rain elements.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomPiDayMobile" name="EnableRandomPiDayMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Pi Symbols on Mobile</span>
</label>
<div class="fieldDescription">Displays additional digital rain elements on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="PiDaySymbolCount">Symbol Count</label>
<input is="emby-input" type="number" id="PiDaySymbolCount" name="PiDaySymbolCount" />
<div class="fieldDescription">Number of additional digital rain columns (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationPiDay" name="EnableDifferentDurationPiDay" type="checkbox" is="emby-checkbox" />
<span>Enable Different Falling Speed</span>
</label>
<div class="fieldDescription">Randomize the digital rain falling speed.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Pride</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnablePride" name="EnablePride" type="checkbox" is="emby-checkbox" />
<span>Enable Pride Seasonal</span>
</label>
<div class="fieldDescription">Enable the Pride theme in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="PrideHeartCount">Heart Count</label>
<input is="emby-input" type="number" id="PrideHeartCount" name="PrideHeartCount" />
<div class="fieldDescription">Number of rising rainbow hearts.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="PrideHeartSize">Heart Size (rem)</label>
<input is="emby-input" type="number" id="PrideHeartSize" name="PrideHeartSize" step="0.1" />
<div class="fieldDescription">Base size of the Pride hearts (default 2).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="PrideConfettiCount">Confetti Count</label>
<input is="emby-input" type="number" id="PrideConfettiCount" name="PrideConfettiCount" />
<div class="fieldDescription">Number of falling rainbow confetti pieces.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="PrideColorHeader" name="PrideColorHeader" type="checkbox" is="emby-checkbox" />
<span>Rainbow Header</span>
</label>
<div class="fieldDescription">Color the top navigation bar with a rainbow gradient.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Rain (Pure)</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRain" name="EnableRain" type="checkbox" is="emby-checkbox" />
<span>Enable Rain Seasonal</span>
</label>
<div class="fieldDescription">Enable the pure Rain theme.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="RaindropCount">Raindrop Count</label>
<input is="emby-input" type="number" id="RaindropCount" name="RaindropCount" />
<div class="fieldDescription">Total number of raindrops.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="RaindropCountMobile">Raindrop Count (Mobile)</label>
<input is="emby-input" type="number" id="RaindropCountMobile" name="RaindropCountMobile" />
<div class="fieldDescription">Total number of raindrops on mobile devices.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="RainSpeed">Rain Speed</label>
<input is="emby-input" type="number" id="RainSpeed" name="RainSpeed" step="0.1" />
<div class="fieldDescription">The speed of the falling rain.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Storm</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableStorm" name="EnableStorm" type="checkbox" is="emby-checkbox" />
<span>Enable Storm Seasonal</span>
</label>
<div class="fieldDescription">Enable the Storm theme in general (e.g. for automation).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="StormRaindropCount">Raindrop Count</label>
<input is="emby-input" type="number" id="StormRaindropCount" name="StormRaindropCount" />
<div class="fieldDescription">Total number of raindrops in the storm.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="StormRaindropCountMobile">Raindrop Count (Mobile)</label>
<input is="emby-input" type="number" id="StormRaindropCountMobile" name="StormRaindropCountMobile" />
<div class="fieldDescription">Total number of raindrops on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="StormRainSpeed">Rain Speed</label>
<input is="emby-input" type="number" id="StormRainSpeed" name="StormRainSpeed" step="0.1" />
<div class="fieldDescription">The speed of the falling rain.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="StormEnableLightning" name="StormEnableLightning" type="checkbox" is="emby-checkbox" />
<span>Enable Lightning Flashes</span>
</label>
<div class="fieldDescription">Periodically flash the screen white to simulate lightning.</div>
</div>
</details>
</div>
<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;">
@@ -949,14 +1159,20 @@
' <option value="halloween">Halloween</option>' +
' <option value="hearts">Hearts</option>' +
' <option value="christmas">Christmas</option>' +
' <option value="santa">Santa</option>' +
' <option value="autumn">Autumn</option>' +
' <option value="santa">Santa (flying santa & snowfall)</option>' +
' <option value="autumn">Autumn (falling leaves)</option>' +
' <option value="easter">Easter</option>' +
' <option value="resurrection">Resurrection</option>' +
' <option value="summer">Summer (Bubbles)</option>' +
' <option value="spring">Spring</option>' +
' <option value="carnival">Carnival (Confetti)</option>' +
' <option value="cherryblossom">Cherry Blossom</option>' +
' <option value="summer">Summer</option>' +
' <option value="carnival">Carnival</option>' +
' <option value="earthday">Earth Day</option>' +
' <option value="eurovision">Eurovision</option>' +
' <option value="piday">Pi-Day</option>' +
' <option value="pride">Pride</option>' +
' <option value="rain">Rain</option>' +
' <option value="storm">Storm (Epilepsy Warning!)</option>' +
' <option value="resurrection">Resurrection by Bioflash257</option>' +
' </select>' +
' </div>' +
'</div>';
@@ -1165,6 +1381,46 @@
document.querySelector('#EnableRandomCherryBlossomMobile').checked = config.CherryBlossom.EnableRandomCherryBlossomMobile;
document.querySelector('#EnableDifferentDurationCherryBlossom').checked = config.CherryBlossom.EnableDifferentDuration;
// Earth Day
document.querySelector('#EnableEarthDay').checked = config.EarthDay.EnableEarthDay;
document.querySelector('#EarthDayVineCount').value = config.EarthDay.VineCount;
// Eurovision
document.querySelector('#EnableEurovision').checked = config.Eurovision.EnableEurovision;
document.querySelector('#EurovisionSymbolCount').value = config.Eurovision.SymbolCount;
document.querySelector('#EnableRandomEurovision').checked = config.Eurovision.EnableRandomEurovision;
document.querySelector('#EnableRandomEurovisionMobile').checked = config.Eurovision.EnableRandomEurovisionMobile;
document.querySelector('#EnableDifferentDurationEurovision').checked = config.Eurovision.EnableDifferentDuration;
document.querySelector('#EnableColorfulNotes').checked = config.Eurovision.EnableColorfulNotes;
document.querySelector('#EurovisionColors').value = config.Eurovision.EurovisionColors;
document.querySelector('#EurovisionGlowSize').value = config.Eurovision.EurovisionGlowSize;
// Pi-Day
document.querySelector('#EnablePiDay').checked = config.PiDay.EnablePiDay;
document.querySelector('#PiDaySymbolCount').value = config.PiDay.SymbolCount;
document.querySelector('#EnableRandomPiDay').checked = config.PiDay.EnableRandomPiDay;
document.querySelector('#EnableRandomPiDayMobile').checked = config.PiDay.EnableRandomPiDayMobile;
document.querySelector('#EnableDifferentDurationPiDay').checked = config.PiDay.EnableDifferentDuration;
// Pride
document.querySelector('#EnablePride').checked = config.Pride.EnablePride;
document.querySelector('#PrideHeartCount').value = config.Pride.HeartCount;
document.querySelector('#PrideHeartSize').value = config.Pride.HeartSize;
document.querySelector('#PrideColorHeader').checked = config.Pride.ColorHeader;
// Rain
document.querySelector('#EnableRain').checked = config.Rain.EnableRain;
document.querySelector('#RaindropCount').value = config.Rain.RaindropCount;
document.querySelector('#RaindropCountMobile').value = config.Rain.RaindropCountMobile;
document.querySelector('#RainSpeed').value = config.Rain.RainSpeed;
// Storm
document.querySelector('#EnableStorm').checked = config.Storm.EnableStorm;
document.querySelector('#StormRaindropCount').value = config.Storm.RaindropCount;
document.querySelector('#StormRaindropCountMobile').value = config.Storm.RaindropCountMobile;
document.querySelector('#StormRainSpeed').value = config.Storm.RainSpeed;
document.querySelector('#StormEnableLightning').checked = config.Storm.EnableLightning;
Dashboard.hideLoadingMsg();
});
});
@@ -1308,6 +1564,46 @@
config.CherryBlossom.EnableRandomCherryBlossomMobile = document.querySelector('#EnableRandomCherryBlossomMobile').checked;
config.CherryBlossom.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationCherryBlossom').checked;
// Earth Day
config.EarthDay.EnableEarthDay = document.querySelector('#EnableEarthDay').checked;
config.EarthDay.VineCount = parseInt(document.querySelector('#EarthDayVineCount').value);
// Eurovision
config.Eurovision.EnableEurovision = document.querySelector('#EnableEurovision').checked;
config.Eurovision.SymbolCount = parseInt(document.querySelector('#EurovisionSymbolCount').value);
config.Eurovision.EnableRandomEurovision = document.querySelector('#EnableRandomEurovision').checked;
config.Eurovision.EnableRandomEurovisionMobile = document.querySelector('#EnableRandomEurovisionMobile').checked;
config.Eurovision.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEurovision').checked;
config.Eurovision.EnableColorfulNotes = document.querySelector('#EnableColorfulNotes').checked;
config.Eurovision.EurovisionColors = document.querySelector('#EurovisionColors').value;
config.Eurovision.EurovisionGlowSize = parseInt(document.querySelector('#EurovisionGlowSize').value);
// Pi-Day
config.PiDay.EnablePiDay = document.querySelector('#EnablePiDay').checked;
config.PiDay.SymbolCount = parseInt(document.querySelector('#PiDaySymbolCount').value);
config.PiDay.EnableRandomPiDay = document.querySelector('#EnableRandomPiDay').checked;
config.PiDay.EnableRandomPiDayMobile = document.querySelector('#EnableRandomPiDayMobile').checked;
config.PiDay.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationPiDay').checked;
// Pride
config.Pride.EnablePride = document.querySelector('#EnablePride').checked;
config.Pride.HeartCount = parseInt(document.querySelector('#PrideHeartCount').value);
config.Pride.HeartSize = parseFloat(document.querySelector('#PrideHeartSize').value);
config.Pride.ColorHeader = document.querySelector('#PrideColorHeader').checked;
// Rain
config.Rain.EnableRain = document.querySelector('#EnableRain').checked;
config.Rain.RaindropCount = parseInt(document.querySelector('#RaindropCount').value);
config.Rain.RaindropCountMobile = parseInt(document.querySelector('#RaindropCountMobile').value);
config.Rain.RainSpeed = parseFloat(document.querySelector('#RainSpeed').value);
// Storm
config.Storm.EnableStorm = document.querySelector('#EnableStorm').checked;
config.Storm.RaindropCount = parseInt(document.querySelector('#StormRaindropCount').value);
config.Storm.RaindropCountMobile = parseInt(document.querySelector('#StormRaindropCountMobile').value);
config.Storm.RainSpeed = parseFloat(document.querySelector('#StormRainSpeed').value);
config.Storm.EnableLightning = document.querySelector('#StormEnableLightning').checked;
ApiClient.updatePluginConfiguration(SeasonalsConfigPage.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors>
<Version>1.7.1.4</Version>
<Version>1.7.2.0</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
</PropertyGroup>

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.leaf {
@@ -44,7 +45,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -54,7 +55,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -9,13 +9,14 @@
pointer-events: none;
z-index: 10;
perspective: 600px;
contain: layout paint;
}
.carnival-wrapper {
position: fixed;
z-index: 15;
top: -20px;
will-change: top;
will-change: transform;
animation-name: carnival-fall;
animation-timing-function: linear;
animation-iteration-count: 1;
@@ -59,10 +60,10 @@
@keyframes carnival-fall {
0% {
top: -10%;
transform: translateY(0);
}
100% {
top: 110%;
transform: translateY(120vh);
}
}

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 1000;
contain: layout paint;
}
/* Petals */
@@ -45,7 +46,7 @@
@keyframes cherryblossom-fall {
0% { top: -10%; }
100% { top: 100%; }
100% { top: 110%; }
}
@keyframes cherryblossom-sway {

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.christmas {
@@ -37,7 +38,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -61,7 +62,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -0,0 +1,35 @@
.earthday-container {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 15vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
}
.earthday-meadow {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
transform-origin: bottom;
animation: grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
}
@keyframes grow-meadow {
0% { transform: translateY(100%); opacity: 0; }
100% { transform: translateY(0); opacity: 0.95; }
}
.earthday-sway {
transform-origin: bottom center;
animation: sway-grass 4s ease-in-out infinite alternate;
}
@keyframes sway-grass {
0% { transform: skewX(-2deg); }
100% { transform: skewX(2deg); }
}

View File

@@ -0,0 +1,126 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.EarthDay || {};
const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true;
const vineCount = config.VineCount || 4;
let msgPrinted = false;
// 2. Toggle Function
function toggleEarthDay() {
const container = document.querySelector('.earthday-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('EarthDay hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('EarthDay visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleEarthDay);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.earthday-container') || document.createElement('div');
if (!document.querySelector('.earthday-container')) {
container.className = 'earthday-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
const w = window.innerWidth;
const hSVG = Math.floor(window.innerHeight * 0.15) || 100; // 15vh roughly
let paths = '';
// Generate Grass
for (let i = 0; i < 400; i++) {
const x = Math.random() * w;
const h = hSVG * 0.2 + Math.random() * (hSVG * 0.8);
const cY = hSVG - h;
const bend = x + (Math.random() * 40 - 20);
const color = Math.random() > 0.5 ? '#2E8B57' : '#3CB371';
const width = 1 + Math.random() * 2;
paths += `<path d="M ${x} ${hSVG} Q ${bend} ${cY+hSVG*0.2} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
}
// Generate Flowers
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
const flowerCount = Math.max(10, vineCount * 15);
for (let i = 0; i < flowerCount; i++) {
const x = 10 + Math.random() * (w - 20);
const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
const col = colors[Math.floor(Math.random() * colors.length)];
paths += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#006400" stroke-width="1.5" fill="none"/>`;
const r = 2 + Math.random() * 1.5;
paths += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
}
const svgContent = `
<svg class="earthday-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="earthday-sway">
${paths}
</g>
</svg>
`;
container.innerHTML = svgContent;
}
// 5. Responsive Resize
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const handleResize = debounce(() => {
const container = document.querySelector('.earthday-container');
if (container) {
container.innerHTML = '';
createElements();
}
}, 250);
window.addEventListener('resize', handleResize);
// 6. Initialization
function initializeEarthDay() {
if (!enabled) return;
createElements();
toggleEarthDay();
}
initializeEarthDay();

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.hopping-rabbit {
@@ -58,7 +59,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -82,7 +83,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -0,0 +1,43 @@
.eurovision-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.music-note-wrapper {
position: absolute;
left: 0;
/* initial top will be set via JS */
opacity: 0;
animation: move-right linear infinite;
will-change: transform, opacity;
}
.music-note {
display: block;
font-size: 2rem;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
animation: sway ease-in-out infinite alternate;
will-change: transform;
}
/* Horizontal scroll from left to right */
@keyframes move-right {
0% { transform: translateX(-10vw); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateX(110vw); opacity: 0; }
}
/* Sine-wave style vertical bouncing for the note itself */
@keyframes sway {
0% { transform: translateY(-30px); }
100% { transform: translateY(30px); }
}

View File

@@ -0,0 +1,105 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Eurovision || {};
const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true;
const elementCount = config.SymbolCount || 25;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true;
const eurovisionColorsStr = config.EurovisionColors || '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff';
const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2;
let msgPrinted = false;
// 2. Toggle Function
function toggleEurovision() {
const container = document.querySelector('.eurovision-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Eurovision hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Eurovision visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleEurovision);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.eurovision-container') || document.createElement('div');
if (!document.querySelector('.eurovision-container')) {
container.className = 'eurovision-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
const notesSymbols = ['♪', '♫', '♬', '♭', '♮', '♯', '𝄞', '𝄢'];
const pColors = eurovisionColorsStr.split(',').map(s => s.trim()).filter(s => s);
for (let i = 0; i < elementCount; i++) {
const wrapper = document.createElement('div');
wrapper.className = 'music-note-wrapper';
const note = document.createElement('span');
note.className = 'music-note';
note.textContent = notesSymbols[Math.floor(Math.random() * notesSymbols.length)];
wrapper.appendChild(note);
wrapper.style.top = `${Math.random() * 90}vh`;
const minMoveDur = 10;
const maxMoveDur = 25;
const moveDur = enableDifferentDuration
? minMoveDur + Math.random() * (maxMoveDur - minMoveDur)
: (minMoveDur + maxMoveDur) / 2;
wrapper.style.animationDuration = `${moveDur}s`;
wrapper.style.animationDelay = `${Math.random() * 15}s`;
const minSwayDur = 1;
const maxSwayDur = 3;
const swayDur = minSwayDur + Math.random() * (maxSwayDur - minSwayDur);
note.style.animationDuration = `${swayDur}s`;
note.style.animationDelay = `${Math.random() * 2}s`;
note.style.fontSize = `${Math.random() * 1.5 + 1.5}rem`;
if (enableColorfulNotes && pColors.length > 0) {
note.style.color = pColors[Math.floor(Math.random() * pColors.length)];
note.style.textShadow = `0 0 ${glowSize}px ${note.style.color}`;
} else {
note.style.color = `rgba(255, 255, 255, 0.9)`;
note.style.textShadow = `0 0 ${glowSize}px rgba(255, 255, 255, 0.6)`;
}
container.appendChild(wrapper);
}
}
// 5. Initialization
function initializeEurovision() {
if (!enabled) return;
createElements();
toggleEurovision();
}
initializeEurovision();

View File

@@ -7,6 +7,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.rocket-trail {

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.halloween {
@@ -34,11 +35,11 @@
@-webkit-keyframes halloween-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}
@@ -58,11 +59,11 @@
@keyframes halloween-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.heart {
@@ -32,11 +33,11 @@
@-webkit-keyframes heart-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}
@@ -56,11 +57,11 @@
@keyframes heart-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}

View File

@@ -0,0 +1,11 @@
.piday-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}

View File

@@ -0,0 +1,165 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.PiDay || {};
const enabled = config.EnablePiDay !== undefined ? config.EnablePiDay : true;
const maxTrails = config.SymbolCount || 25; // Directly mapped, smaller default
let msgPrinted = false;
let isHidden = false;
// 2. Toggle Function
function togglePiDay() {
const container = document.querySelector('.piday-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
if (!isHidden) {
container.style.display = 'none';
isHidden = true;
if (!msgPrinted) {
console.log('PiDay hidden');
msgPrinted = true;
}
}
} else {
if (isHidden) {
container.style.display = 'block';
isHidden = false;
if (msgPrinted) {
console.log('PiDay visible');
msgPrinted = false;
}
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(togglePiDay);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.piday-container') || document.createElement('div');
if (!document.querySelector('.piday-container')) {
container.className = 'piday-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.display = 'block';
container.appendChild(canvas);
const ctx = canvas.getContext('2d');
// const chars = '0123456789πABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const chars = '0123456789'.split('');
const fontSize = 18;
class Trail {
constructor() {
this.reset();
this.y = Math.random() * -100; // Allow initial staggered start
}
reset() {
const cols = Math.floor(canvas.width / fontSize);
this.x = Math.floor(Math.random() * cols);
this.y = -Math.round(Math.random() * 20);
this.speed = 0.5 + Math.random() * 0.5;
this.len = 10 + Math.floor(Math.random() * 20);
this.chars = [];
for(let i=0; i<this.len; i++) {
this.chars.push(chars[Math.floor(Math.random() * chars.length)]);
}
}
update() {
const oldY = Math.floor(this.y);
this.y += this.speed;
const newY = Math.floor(this.y);
// If crossed a full vertical unit, push a new character and pop the old one to preserve screen positions
if (newY > oldY) {
this.chars.unshift(chars[Math.floor(Math.random() * chars.length)]);
this.chars.pop();
}
// Randomly mutate some characters (heads mutate faster)
for (let i = 0; i < this.len; i++) {
const chance = i < 3 ? 0.90 : 0.98;
if (Math.random() > chance) {
this.chars[i] = chars[Math.floor(Math.random() * chars.length)];
}
}
if (this.y - this.len > Math.ceil(canvas.height / fontSize)) {
this.reset();
}
}
draw(ctx) {
const headY = Math.floor(this.y);
for (let i = 0; i < this.len; i++) {
const charY = headY - i;
if (charY < 0 || charY * fontSize > canvas.height + fontSize) continue;
const ratio = i / this.len;
const alpha = 1 - ratio;
if (i === 0) {
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.shadowBlur = 8;
ctx.shadowColor = '#0F0';
} else if (i === 1) {
ctx.fillStyle = `rgba(150, 255, 150, ${alpha})`;
ctx.shadowBlur = 4;
ctx.shadowColor = '#0F0';
} else {
ctx.fillStyle = `rgba(0, 255, 0, ${alpha * 0.8})`;
ctx.shadowBlur = 0;
}
ctx.fillText(this.chars[i], this.x * fontSize + fontSize/2, charY * fontSize);
}
}
}
const trails = [];
for(let i=0; i<maxTrails; i++) trails.push(new Trail());
function loop() {
if (isHidden) return; // Pause drawing when hidden
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold ' + fontSize + 'px monospace';
ctx.textAlign = 'center';
for (const t of trails) {
t.update();
t.draw(ctx);
}
}
if (window.pidayInterval) clearInterval(window.pidayInterval);
window.pidayInterval = setInterval(loop, 50);
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
}
// 5. Initialization
function initializePiDay() {
if (!enabled) return;
createElements();
togglePiDay();
}
initializePiDay();

View File

@@ -0,0 +1,32 @@
.pride-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
contain: layout paint;
}
.pride-heart {
position: absolute;
bottom: -50px;
animation: pride-rise ease-in infinite;
will-change: transform;
}
@keyframes pride-rise {
0% { transform: translateY(0) scale(0.8); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-115vh) scale(1.2); opacity: 0; }
}
/* Coloring the Jellyfin Header */
.skinHeader.pride-header {
background: linear-gradient(90deg, #E40303, #FF8C00, #FFED00, #008026, #24408E, #732982) !important;
}

View File

@@ -0,0 +1,88 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Pride || {};
const enabled = config.EnablePride !== undefined ? config.EnablePride : true;
const elementCount = config.HeartCount || 20;
const heartSize = config.HeartSize || 1.5;
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true;
let msgPrinted = false;
// 2. Toggle Function
// Hides the effect when a video player, trailer (in full width mode), dashboard, or user menu is active.
function togglePride() {
const container = document.querySelector('.pride-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Pride hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Pride visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
// Watches the DOM for changes so the effect can auto-hide/show.
const observer = new MutationObserver(togglePride);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
// Create and append your animated elements to the container.
function createElements() {
const container = document.querySelector('.pride-container') || document.createElement('div');
if (!document.querySelector('.pride-container')) {
container.className = 'pride-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
if (colorHeader) {
const header = document.querySelector('.skinHeader');
if (header) {
header.classList.add('pride-header');
}
}
const heartEmojis = ['❤️', '🧡', '💛', '💚', '💙', '💜'];
for (let i = 0; i < elementCount; i++) {
const el = document.createElement('div');
el.className = 'pride-heart';
el.innerText = heartEmojis[Math.floor(Math.random() * heartEmojis.length)];
el.style.fontSize = `${heartSize}rem`;
el.style.left = `${Math.random() * 100}vw`;
el.style.animationDuration = `${5 + Math.random() * 5}s`;
el.style.animationDelay = `${Math.random() * 5}s`;
el.style.marginLeft = `${(Math.random() - 0.5) * 100}px`;
container.appendChild(el);
}
}
// 5. Initialization
function initializePride() {
if (!enabled) return;
createElements();
togglePride();
}
initializePride();

View File

@@ -0,0 +1,26 @@
.rain-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.raindrop-pure {
position: absolute;
width: 2px;
height: 40px;
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
transform-origin: bottom;
}
@keyframes pure-rain {
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { transform: translateY(110vh) translateX(-40vh) rotate(20deg); opacity: 0; }
}

View File

@@ -0,0 +1,77 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Rain || {};
const enabled = config.EnableRain !== undefined ? config.EnableRain : true;
const isMobile = window.innerWidth <= 768;
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
const rainSpeed = config.RainSpeed || 1.0;
let msgPrinted = false;
// 2. Toggle Function
function toggleRain() {
const container = document.querySelector('.rain-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Rain hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Rain visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleRain);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.rain-container') || document.createElement('div');
if (!document.querySelector('.rain-container')) {
container.className = 'rain-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
for (let i = 0; i < elementCount; i++) {
const drop = document.createElement('div');
drop.className = 'raindrop-pure';
drop.style.left = `${Math.random() * 140}vw`;
drop.style.top = `${-20 - Math.random() * 50}vh`;
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
drop.style.animation = `pure-rain ${duration}s linear infinite`;
drop.style.animationDelay = `${Math.random() * 2}s`;
drop.style.opacity = Math.random() * 0.5 + 0.3;
container.appendChild(drop);
}
}
// 5. Initialization
function initializeRain() {
if (!enabled) return;
createElements();
toggleRain();
}
initializeRain();

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.resurrection-symbol {
@@ -43,7 +44,7 @@
}
100% {
top: 105%;
top: 110%;
}
}

View File

@@ -78,6 +78,36 @@ const ThemeConfigs = {
js: '../Seasonals/Resources/cherryblossom.js',
containerClass: 'cherryblossom-container'
},
piday: {
css: '../Seasonals/Resources/piday.css',
js: '../Seasonals/Resources/piday.js',
containerClass: 'piday-container'
},
eurovision: {
css: '../Seasonals/Resources/eurovision.css',
js: '../Seasonals/Resources/eurovision.js',
containerClass: 'eurovision-container'
},
storm: {
css: '../Seasonals/Resources/storm.css',
js: '../Seasonals/Resources/storm.js',
containerClass: 'storm-container'
},
pride: {
css: '../Seasonals/Resources/pride.css',
js: '../Seasonals/Resources/pride.js',
containerClass: 'pride-container'
},
rain: {
css: '../Seasonals/Resources/rain.css',
js: '../Seasonals/Resources/rain.js',
containerClass: 'rain-container'
},
earthday: {
css: '../Seasonals/Resources/earthday.css',
js: '../Seasonals/Resources/earthday.js',
containerClass: 'earthday-container'
},
none: {
containerClass: 'none'
},

View File

@@ -8,6 +8,7 @@
top: 0;
left: 0;
z-index: 10;
contain: layout paint;
}
#snowfallCanvas {

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.snowflake {
@@ -37,7 +38,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -61,7 +62,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -8,6 +8,7 @@
top: 0;
left: 0;
z-index: 10;
contain: layout paint;
}
#snowfallCanvas {

View File

@@ -8,6 +8,7 @@
height: 100vh;
pointer-events: none;
z-index: 1000;
contain: layout paint;
}
/* Pollen */
@@ -37,7 +38,7 @@
opacity: 0;
}
/* Grass */
/* Grass Container (Wrapper) */
.spring-grass-container {
position: absolute;
bottom: 0;
@@ -45,8 +46,10 @@
width: 100%;
height: 80px;
pointer-events: none;
transform-origin: bottom;
}
/* HTML Grass Overlayer */
.spring-grass {
position: absolute;
bottom: 0;
@@ -61,6 +64,45 @@
animation-iteration-count: infinite;
}
@keyframes spring-grass-sway {
0% { transform: rotate(0deg); }
50% { transform: rotate(8deg); }
100% { transform: rotate(0deg); }
}
/* SVG Meadow Layer */
.spring-meadow-layer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
animation: spring-grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
}
.spring-meadow {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
}
@keyframes spring-grow-meadow {
0% { transform: translateY(100%); opacity: 0; }
100% { transform: translateY(0); opacity: 0.95; }
}
.spring-sway {
transform-origin: bottom center;
animation: spring-meadow-sway 4s ease-in-out infinite alternate;
}
@keyframes spring-meadow-sway {
0% { transform: skewX(-2deg); }
100% { transform: skewX(2deg); }
}
/* Birds */
.spring-bird {
position: static !important;
@@ -123,6 +165,11 @@
top: 0; left: 0;
}
.spring-align-y {
width: 100%; height: 100%;
will-change: transform;
}
.spring-mirror-wrapper {
width: 100%; height: 100%;
will-change: transform;
@@ -144,11 +191,7 @@
100% { opacity: 0; transform: rotate(var(--beam-rotation, 45deg)) scaleX(0.8); }
}
@keyframes spring-grass-sway {
0% { transform: rotate(0deg); }
50% { transform: rotate(8deg); }
100% { transform: rotate(0deg); }
}
/* Wrapper animations (Flight across screen) */
@keyframes spring-fly-right-wrapper {
@@ -163,8 +206,8 @@
/* Vertical Drift for Sloped Flight */
@keyframes spring-vertical-drift {
0% { top: var(--start-y, 10%); }
100% { top: var(--end-y, 10%); }
0% { transform: translateY(var(--start-y, 10vh)); }
100% { transform: translateY(var(--end-y, 10vh)); }
}
/* Inner animations (Bobbing/Fluttering) */

View File

@@ -121,30 +121,80 @@ function createGrass(container) {
grassContainer.innerHTML = '';
const bladeCount = window.innerWidth / 3;
let pathsBg = '';
let pathsFg = '';
const w = window.innerWidth;
const hSVG = 80;
// 1. Generate Straight Line HTML-Style Grass (converted to SVG Paths)
const bladeCount = w / 5; // Reduced from w/3
for (let i = 0; i < bladeCount; i++) {
const blade = document.createElement('div');
blade.classList.add('spring-grass');
// MARK: GRASS HEIGHT
const height = Math.random() * 40 + 20; // 20-60px height
blade.style.height = `${height}px`;
blade.style.left = `${i * 3 + Math.random() * 2}px`;
const duration = Math.random() * 2 + 3;
blade.style.animationDuration = `${duration}s`;
blade.style.animationDelay = `-${Math.random() * 5}s`;
const x = i * 5 + Math.random() * 3;
const hue = 100 + Math.random() * 40;
blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`;
const color = `hsl(${hue}, 60%, 40%)`;
// Random z-index to interleave with Ladybug (1002)
// Values: 1001 (behind) or 1003 (front)
const z = Math.random() > 0.5 ? 1001 : 1003;
blade.style.zIndex = z;
grassContainer.appendChild(blade);
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
// ~66% chance to be in background (1001), 33% foreground (1003)
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
}
// 2. Generate Curved Earth-Day Style Grass
for (let i = 0; i < 200; i++) { // Reduced from 400
const x = Math.random() * w;
const h = 20 + Math.random() * 50;
const cY = hSVG - h;
const bend = x + (Math.random() * 40 - 20);
const color = Math.random() > 0.5 ? '#4caf50' : '#45a049';
const width = 1 + Math.random() * 2;
const path = `<path d="M ${x} ${hSVG} Q ${bend} ${cY+20} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
// ~66% chance to be in background (1001), 33% foreground (1003)
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
}
// 3. Generate SVG Flowers
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493', '#FFFFFF'];
const flowerCount = Math.floor(w / 40); // Reduced from w/30
for (let i = 0; i < flowerCount; i++) {
const x = 10 + Math.random() * (w - 20);
const y = 10 + Math.random() * 40; // 10-50px from top of SVG
const col = colors[Math.floor(Math.random() * colors.length)];
let flower = '';
// Stem
flower += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#2e7d32" stroke-width="1.5" fill="none"/>`;
// Petals
const r = 2 + Math.random() * 1.5;
flower += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
flower += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
flower += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
flower += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
// Center
flower += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
// ~66% chance to be in background (1001), 33% foreground (1003)
if (Math.random() > 0.33) pathsBg += flower; else pathsFg += flower;
}
// Inject purely SVG based grass container
grassContainer.innerHTML = `
<div class="spring-meadow-layer" style="z-index: 1001;">
<svg class="spring-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="spring-sway">
${pathsBg}
</g>
</svg>
</div>
<div class="spring-meadow-layer" style="z-index: 1003;">
<svg class="spring-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="spring-sway" style="animation-delay: -2s;">
${pathsFg}
</g>
</svg>
</div>
`;
}
function initSpringObjects() {
@@ -205,6 +255,9 @@ function createBird(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-bird-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -218,19 +271,20 @@ function createBird(container) {
// MARK: BIRD SPEED (10-15s)
const duration = Math.random() * 5 + 10;
// MARK: BIRD HEIGHT RANGE (in %)
const startY = Math.random() * 55 + 5; // Start 5-60%
const endY = Math.random() * 55 + 5; // End 5-60%
wrapper.style.setProperty('--start-y', `${startY}%`);
wrapper.style.setProperty('--end-y', `${endY}%`);
// MARK: BIRD HEIGHT RANGE (in vh)
const startY = Math.random() * 55 + 5; // Start 5-60vh
const endY = Math.random() * 55 + 5; // End 5-60vh
alignY.style.setProperty('--start-y', `${startY}vh`);
alignY.style.setProperty('--end-y', `${endY}vh`);
if (direction === 'right') {
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards, spring-vertical-drift ${duration}s linear forwards`;
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(-1)';
} else {
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards, spring-vertical-drift ${duration}s linear forwards`;
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(1)';
}
alignY.style.animation = `spring-vertical-drift ${duration}s linear forwards`;
wrapper.addEventListener('animationend', (e) => {
if (e.animationName.includes('fly-')) {
@@ -241,10 +295,9 @@ function createBird(container) {
bird.style.animation = `spring-bob 2s ease-in-out infinite`;
wrapper.style.top = `${startY}%`;
mirror.appendChild(bird);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}
@@ -253,6 +306,9 @@ function createButterfly(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-butterfly-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -284,12 +340,13 @@ function createButterfly(container) {
butterfly.style.animation = `spring-flutter 3s ease-in-out infinite`;
butterfly.style.animationDelay = `-${Math.random() * 3}s`;
// MARK: BUTTERFLY HEIGHT (in %)
const top = Math.random() * 35 + 30; // 30-65%
wrapper.style.top = `${top}%`;
// MARK: BUTTERFLY HEIGHT (in vh)
const top = Math.random() * 35 + 30; // 30-65vh
alignY.style.transform = `translateY(${top}vh)`;
mirror.appendChild(butterfly);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}
@@ -298,6 +355,9 @@ function createBee(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-bee-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -323,12 +383,13 @@ function createBee(container) {
}
});
// MARK: BEE HEIGHT (in %)
const top = Math.random() * 60 + 20; // 20-80%
wrapper.style.top = `${top}%`;
// MARK: BEE HEIGHT (in vh)
const top = Math.random() * 60 + 20; // 20-80vh
alignY.style.transform = `translateY(${top}vh)`;
mirror.appendChild(bee);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}
@@ -337,6 +398,9 @@ function createLadybugGif(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-ladybug-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -364,11 +428,12 @@ function createLadybugGif(container) {
bug.style.animation = `spring-crawl 2s ease-in-out infinite`;
wrapper.style.top = 'auto';
wrapper.style.bottom = '5px';
// Target the Ladybug to walk on the ground visually (aligning properly with the CSS/SVG grass size)
alignY.style.transform = `translateY(calc(100vh - 5px - 30px))`;
mirror.appendChild(bug);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}

View File

@@ -0,0 +1,39 @@
.storm-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.raindrop {
position: absolute;
width: 2px;
height: 40px;
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
transform-origin: bottom;
}
@keyframes stormy-rain {
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { transform: translateY(110vh) translateX(-40vh) rotate(20deg); opacity: 0; }
}
.lightning-flash {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #fff;
opacity: 0;
pointer-events: none;
z-index: 999;
will-change: opacity;
}

View File

@@ -0,0 +1,99 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Storm || {};
const enabled = config.EnableStorm !== undefined ? config.EnableStorm : true;
const isMobile = window.innerWidth <= 768;
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
const enableLightning = config.EnableLightning !== undefined ? config.EnableLightning : true;
const rainSpeed = config.RainSpeed || 1.0;
let msgPrinted = false;
// 2. Toggle Function
function toggleStorm() {
const container = document.querySelector('.storm-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Storm hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Storm visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleStorm);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.storm-container') || document.createElement('div');
if (!document.querySelector('.storm-container')) {
container.className = 'storm-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
for (let i = 0; i < elementCount; i++) {
const drop = document.createElement('div');
drop.className = 'raindrop';
drop.style.left = `${Math.random() * 140}vw`;
drop.style.top = `${-20 - Math.random() * 50}vh`;
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
drop.style.animation = `stormy-rain ${duration}s linear infinite`;
drop.style.animationDelay = `${Math.random() * 2}s`;
drop.style.opacity = Math.random() * 0.5 + 0.3;
container.appendChild(drop);
}
if (enableLightning) {
const flash = document.createElement('div');
flash.className = 'lightning-flash';
container.appendChild(flash);
function triggerFlash() {
const nextFlashDelay = 5000 + Math.random() * 10000;
setTimeout(() => {
flash.style.opacity = '0.8';
setTimeout(() => { flash.style.opacity = '0'; }, 50);
setTimeout(() => { flash.style.opacity = '0.5'; }, 100);
setTimeout(() => { flash.style.opacity = '0'; }, 150);
triggerFlash();
}, nextFlashDelay);
}
triggerFlash();
}
}
// 5. Initialization
function initializeStorm() {
if (!enabled) return;
createElements();
toggleStorm();
}
initializeStorm();

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.summer-bubble {

View File

@@ -249,6 +249,12 @@
<option value="summer">Summer (Bubbles)</option>
<option value="carnival">Carnival (Confetti)</option>
<option value="cherryblossom">Cherryblossom</option>
<option value="earthday">Earth Day</option>
<option value="eurovision">Eurovision</option>
<option value="piday">Pi-Day</option>
<option value="pride">Pride</option>
<option value="rain">Rain</option>
<option value="storm">Storm (Epilepsy Warning!)</option>
<option value="custom">⚙ Custom (Local Files)</option>
</select>
@@ -335,6 +341,12 @@
summer: { css: 'summer.css', js: 'summer.js', container: 'summer-container' },
carnival: { css: 'carnival.css', js: 'carnival.js', container: 'carnival-container' },
cherryblossom: { css: 'cherryblossom.css', js: 'cherryblossom.js', container: 'cherryblossom-container' },
earthday: { css: 'earthday.css', js: 'earthday.js', container: 'earthday-container' },
eurovision: { css: 'eurovision.css', js: 'eurovision.js', container: 'eurovision-container' },
piday: { css: 'piday.css', js: 'piday.js', container: 'piday-container' },
pride: { css: 'pride.css', js: 'pride.js', container: 'pride-container' },
rain: { css: 'rain.css', js: 'rain.js', container: 'rain-container' },
storm: { css: 'storm.css', js: 'storm.js', container: 'storm-container' },
};
const select = document.getElementById('theme-select');
@@ -362,7 +374,9 @@
'.christmas-container', '.santa-container', '.autumn-container',
'.christmas-container', '.santa-container', '.autumn-container',
'.easter-container', '.resurrection-container', '.spring-container',
'.summer-container', '.carnival-container'
'.summer-container', '.carnival-container', '.cherryblossom-container',
'.earthday-container', '.eurovision-container', '.piday-container',
'.pride-container', '.rain-container', '.storm-container'
];
knownContainers.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {

View File

@@ -9,12 +9,20 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [
{
"version": "1.7.1.4",
"version": "1.7.2.0",
"changelog": "- feat: add Pi Day, Pride, Rain, and Storm themes\n- fix: improve performance",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.2.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "34c8426c48bd7d470c3e8dc7f02f86da",
"timestamp": "2026-02-23T00:34:13Z"
},
{
"version": "1.7.1.5",
"changelog": "- feat: add summer, spring and carnival themes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.4/Jellyfin.Plugin.Seasonals.zip",
"checksum": "549d2d3d6451c828a5e1284a74d53ab2",
"timestamp": "2026-02-21T14:24:25Z"
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.5/Jellyfin.Plugin.Seasonals.zip",
"checksum": "f6447d476189e69fb96fe1675c55a1a0",
"timestamp": "2026-02-21T14:28:30Z"
},
{
"version": "1.7.0.15",