Compare commits

...

298 Commits

Author SHA1 Message Date
CodeDevMLH
0a301564ac Update manifest.json for release v2.0.0.4 [skip ci] 2026-02-28 02:11:13 +00:00
CodeDevMLH
85e69a0b34 Bump version to 2.0.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-28 03:10:22 +01:00
CodeDevMLH
5adaf202ae Refactor seasonal configuration: remove unused input fields and add Oktoberfest options 2026-02-28 03:10:05 +01:00
CodeDevMLH
99ac46a384 Update manifest.json for release v2.0.0.3 [skip ci] 2026-02-28 01:30:55 +00:00
CodeDevMLH
3a2750388b Bump version to 2.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-28 02:30:04 +01:00
CodeDevMLH
33e89ec16b Enhance seasonal options in configuration: add new seasons and improve descriptions 2026-02-28 02:29:41 +01:00
CodeDevMLH
9adbe92e7c Update manifest.json for release v2.0.0.2 [skip ci] 2026-02-28 00:56:16 +00:00
CodeDevMLH
103d63f1b1 Bump version to 2.0.0.2 in project and manifest files
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-28 01:55:24 +01:00
CodeDevMLH
49bad2e880 updated to latest settings 2026-02-28 01:55:05 +01:00
CodeDevMLH
04616c2ac4 Fix HeartSize property type in PrideOptions to double for improved precision 2026-02-28 01:54:19 +01:00
CodeDevMLH
3b73dd1728 Add comments for birthday image source and modification details [skip ci] 2026-02-28 01:40:16 +01:00
CodeDevMLH
d5df90a6ae Refactor animation delays in seasonal effects for improved performance and consistency [skip ci] 2026-02-28 01:29:49 +01:00
CodeDevMLH
494e475f42 Refactor snowflakes.js and snowflakes.css to streamline snowflake initialization and enhance mobile responsiveness 2026-02-28 01:22:18 +01:00
CodeDevMLH
4703ba48ed Refactor santa.js to improve mobile detection for snowflake count adjustment 2026-02-28 01:21:37 +01:00
CodeDevMLH
93d5686b77 Refactor resurrection.js to remove random symbol functionality and adjust symbol count initialization for mobile responsiveness 2026-02-28 01:21:33 +01:00
CodeDevMLH
71d07aa0f3 Refactor halloween.js and halloween.css to streamline symbol initialization and remove random symbol functionality 2026-02-28 01:20:02 +01:00
CodeDevMLH
c43f031617 Refactor autumn configuration to enhance leaf count settings for mobile and desktop 2026-02-28 01:19:09 +01:00
CodeDevMLH
89ce903e8a Refactor seasonal scripts to enhance mobile detection using matchMedia for responsive behavior 2026-02-28 01:16:01 +01:00
CodeDevMLH
cbf5d73629 Refactor autumn.js and autumn.css to streamline leaf initialization and enhance configuration handling 2026-02-28 01:15:03 +01:00
CodeDevMLH
8be17dae74 Refactor theme options in test-site.html to enhance selection clarity and add new themes [skip ci] 2026-02-28 00:59:36 +01:00
CodeDevMLH
76006dc162 Refactor olympia.js to enhance configuration handling and streamline symbol creation logic 2026-02-28 00:59:22 +01:00
CodeDevMLH
492acb4052 Refactor createMarioDay function to enhance jump logic and coin spawning conditions [skip ci] 2026-02-28 00:58:10 +01:00
CodeDevMLH
68dc9efa4d Refactor oktoberfest.js to enhance configuration handling for symbol counts and durations 2026-02-28 00:52:27 +01:00
CodeDevMLH
7b15ed46c1 Refactor carnival and cherry blossom initialization to streamline object creation and enhance configuration handling 2026-02-28 00:45:33 +01:00
CodeDevMLH
8019ba760f Refactor carnival and cherry blossom options to improve configuration clarity and add mobile settings 2026-02-28 00:45:23 +01:00
CodeDevMLH
9d1a268875 Refactor halloween.js and marioday.js to enhance clarity and maintainability of configuration handling [skip ci] 2026-02-28 00:00:54 +01:00
CodeDevMLH
9e5feafd64 Refactor birthday.js to improve configuration handling and enhance clarity for symbol counts and confetti settings 2026-02-27 23:57:54 +01:00
CodeDevMLH
5283a69bb8 Remove unused Olympic ring asset images to declutter project resources [skip ci] 2026-02-27 23:56:53 +01:00
CodeDevMLH
a4b2d2edd5 Refactor seasonal plugin configurations to improve clarity and consistency
- Updated comments for clarity on enabling/disabling features across various seasonal scripts.
- Changed default value checks to ensure undefined values are handled consistently.
- Enhanced readability by providing descriptive comments for configuration variables.

[skip ci]
2026-02-27 23:56:36 +01:00
CodeDevMLH
d0634e4487 Refactor sports.js to streamline configuration handling, remove unused variables, and enhance code readability 2026-02-27 23:18:07 +01:00
CodeDevMLH
79c4f988f2 Refactor eid.js to define lantern counts for mobile and desktop, ensuring consistent lantern creation across devices 2026-02-27 23:16:10 +01:00
CodeDevMLH
cee1fa6736 Refactor spring.js to adjust object counts for mobile, streamline pollen and creature creation, and update code comments 2026-02-27 23:11:33 +01:00
CodeDevMLH
8510674d58 Refactor space.js to remove unused randomization options, streamline object counts for mobile, and enhance code readability with updated credits 2026-02-27 23:05:52 +01:00
CodeDevMLH
5ee724201b Refactor summer.js to streamline bubble and dust creation, enhance mobile handling, and remove unused randomization options 2026-02-27 23:03:42 +01:00
CodeDevMLH
b85c038df0 Refactor underwater.js to enhance mobile symbol handling and update credits 2026-02-27 22:56:13 +01:00
CodeDevMLH
3d4e04ab0f Change SymbolCountMobile property type from bool to int in seasonal options 2026-02-27 22:23:39 +01:00
CodeDevMLH
b1d1ce79e6 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-27 21:57:27 +01:00
CodeDevMLH
0b7b506b8d Remove unnecessary comments and streamline DOM observation in seasonal scripts 2026-02-27 21:57:24 +01:00
CodeDevMLH
f3ea84cc80 Refactor seasonal options in PluginConfiguration to streamline and enhance organization 2026-02-27 21:55:04 +01:00
CodeDevMLH
3d9a474aae Update manifest.json for release v2.0.0.1 [skip ci] 2026-02-27 03:27:03 +00:00
CodeDevMLH
db5baa1fd7 Bump version to 2.0.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-27 04:26:12 +01:00
CodeDevMLH
72ad4ee1a4 Add option to enable different symbol durations for spooky theme; fix casing in snowstorm config 2026-02-27 04:25:53 +01:00
CodeDevMLH
bb6c7796d5 Reduce z-index of matrix-container to improve stacking context and prevent overlap issues [skip ci] 2026-02-27 04:08:00 +01:00
CodeDevMLH
bd8088c52b Add checks for container presence in animation functions to prevent errors when elements are removed from the DOM [skip ci] 2026-02-27 04:07:29 +01:00
CodeDevMLH
3c1bd01373 Fix positioning of olympia-symbol and adjust ring CSS for improved alignment [skip ci] 2026-02-27 03:59:19 +01:00
CodeDevMLH
669ac6d3da Fix spider and mouse creation logic to ensure animations only continue if the container is present in the DOM 2026-02-27 03:58:34 +01:00
CodeDevMLH
73f9be91ef Refactor z-index values and optimize CSS properties for various seasonal styles; enhance performance with will-change property [skip ci] 2026-02-27 03:53:05 +01:00
CodeDevMLH
f14785c54a Enhance CSS animations and performance across seasonal styles; add will-change property for smoother transitions and optimize keyframes. [skip ci] 2026-02-27 03:42:23 +01:00
CodeDevMLH
296873f89e Fix rabbit animation transform origin and improve GIF reset logic
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 40s
2026-02-27 01:58:53 +01:00
CodeDevMLH
d6a9ff7176 Fix z-index for pride container and clean up comments in pride.js
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 42s
2026-02-27 01:35:25 +01:00
CodeDevMLH
ef15857533 Update manifest.json for release v2.0.0.0 [skip ci] 2026-02-27 00:20:16 +00:00
CodeDevMLH
19b21ba94f Bump version to 2.0.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-27 01:19:21 +01:00
CodeDevMLH
8f322fd6cf Refactor birthday, olympia, space, and sports animations; enhance symbol management and visual effects
- Updated birthday.js to improve balloon visuals and confetti effects, including new color schemes and enhanced pop animations.
- Removed legacy fallback logic in olympia.js and improved image error handling.
- Enhanced space.js with configurable counts for planets, astronauts, satellites, ISS, and rockets; improved shooting star animations and added random image swapping.
- Simplified sports.js by removing legacy emoji fallback logic and optimizing ball creation based on selected categories.
- Adjusted CSS styles across birthday, olympia, space, and sports for better visual consistency and performance.
[skip ci]
2026-02-27 01:18:08 +01:00
CodeDevMLH
bdc7d2e325 Refine options in seasonal themes: update epilepsy warning and simplify sports options 2026-02-27 01:17:54 +01:00
CodeDevMLH
8afe397c23 add new balloons 2026-02-27 01:17:41 +01:00
CodeDevMLH
30c29d440f Enhance seasonal themes: replace Pi-Day options with Matrix options and add new configuration settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 43s
2026-02-26 21:53:40 +01:00
CodeDevMLH
69adc64a44 Enhance CSS and JS injection: add cache-buster to prevent caching issues 2026-02-26 21:53:34 +01:00
CodeDevMLH
b0fae10aa1 Enhance seasonal themes: update Pi-Day to Matrix, add Underwater theme options, and refine descriptions for various settings 2026-02-26 21:53:29 +01:00
CodeDevMLH
cee4dae769 Enhance birthday feature: add garland decoration, improve balloon pop confetti effect, and refine animation dynamics 2026-02-26 21:53:24 +01:00
CodeDevMLH
f9aeeadccf Enhance Mario Day feature: adjust animation duration for Mario's running speed and refine coin positioning logic 2026-02-26 21:53:10 +01:00
CodeDevMLH
fc35fcd3c4 Enhance Olympic animation: add new ring types, implement 3D effects for rings and medals, and improve confetti generation with random shapes and animations 2026-02-26 21:52:55 +01:00
CodeDevMLH
6a83981e1d Enhance space animation: add nebula glow effect, shooting stars, and random space images for improved visual experience 2026-02-26 21:52:48 +01:00
CodeDevMLH
540d7f9baa Enhance sports feature: add asset management for various sports, improve animation effects, and refine confetti generation logic 2026-02-26 21:52:42 +01:00
CodeDevMLH
a162b30bcd Enhance underwater animation: add new creature types, improve movement animations, and implement light rays effect 2026-02-26 21:52:36 +01:00
CodeDevMLH
c6d04b9b3b add/change assets
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-26 21:52:02 +01:00
CodeDevMLH
1ceb9cef7f add assets [skip ci] 2026-02-26 03:03:06 +01:00
CodeDevMLH
eb06a979f6 Enhance cat animation: adjust size, spawn logic, and animation duration for improved visual effect 2026-02-26 03:02:32 +01:00
CodeDevMLH
9b6d48a5fe Refactor createEid function to enhance lantern and star generation logic [skip ci] 2026-02-26 02:54:47 +01:00
CodeDevMLH
e3ea4fa599 Add credits for bird and butterfly animations in spring.js [skip ci] 2026-02-26 02:54:31 +01:00
CodeDevMLH
c5093073d0 Add Matrix theme: replace Pi-Day option with Matrix and update related configurations [skip ci] 2026-02-26 02:54:06 +01:00
CodeDevMLH
85cabf29bb Add Matrix theme: implement configuration for matrix CSS and JavaScript resources 2026-02-26 02:53:50 +01:00
CodeDevMLH
b008221cf4 Add Matrix effect: implement matrix animation with toggle functionality and responsive canvas 2026-02-26 02:53:42 +01:00
CodeDevMLH
2bbf13c044 Add Spooky theme: replace Legacy Halloween with Spooky options and implement related CSS and JavaScript for visual effects 2026-02-25 00:22:24 +01:00
CodeDevMLH
082120b70b Refactor seasonal CSS animations: update positioning and transform properties for autumn, carnival, cherry blossom, christmas, resurrection, and snowflakes styles 2026-02-24 23:58:08 +01:00
CodeDevMLH
c66ccf970e Add new seasonal themes: implement Frost, Film Noir, Oscar, Mario Day, Star Wars, Oktoberfest, Friday the 13th, Eid, Legacy Halloween, Sports, Olympia, Space, Underwater, and Birthday configurations [skip ci] 2026-02-24 23:40:46 +01:00
CodeDevMLH
861f431e50 Add seasonal themes for Birthday, Olympia, Space, Sports, and Underwater: implement CSS and JavaScript for visual effects and configurations 2026-02-24 23:40:21 +01:00
CodeDevMLH
be4313d776 Add new seasonal themes: implement Frost, Film Noir, Oscar, Mario Day, Star Wars, Oktoberfest, Friday the 13th, Eid, Sports, Olympia, Space, Underwater, and Birthday options in configuration 2026-02-24 23:39:49 +01:00
CodeDevMLH
9b8a563e43 Add new seasonal options: implement Frost, Film Noir, Oscar, Mario Day, Star Wars, Oktoberfest, Friday the 13th, Eid, Legacy Halloween, Sports, Olympia, Space, Underwater, and Birthday features in configuration [skip ci] 2026-02-24 19:25:42 +01:00
CodeDevMLH
8255683714 Refactor observer configuration in summer.js: format observer options for improved readability 2026-02-24 19:25:03 +01:00
CodeDevMLH
c24abcbd59 Refactor storm animation: update final keyframe values for improved visual effect 2026-02-24 19:24:57 +01:00
CodeDevMLH
b17c2a6efe Add Star Wars feature: implement CSS and JavaScript for hyperspace effect and visibility toggle 2026-02-24 19:24:48 +01:00
CodeDevMLH
ad4fb7964b Refactor CSS comments and improve observer configuration in spring.js for clarity 2026-02-24 19:24:38 +01:00
CodeDevMLH
306b0c5e6e Refactor snowflake symbol definitions: move symbol arrays to the top for better organization and remove redundant declarations 2026-02-24 19:24:25 +01:00
CodeDevMLH
6cc344e0db Refactor toggleSnowstorm function: streamline observer setup by removing unnecessary comments 2026-02-24 19:24:18 +01:00
CodeDevMLH
3ea0709c77 Refactor toggleSnowfall function: clean up observer setup by removing unnecessary comments 2026-02-24 19:24:12 +01:00
CodeDevMLH
b74c8ad2a1 Refactor Santa feature: reorganize present images and Santa image constants for clarity 2026-02-24 19:24:04 +01:00
CodeDevMLH
8f0c2ac7df Fix animation delay calculation for symbols in resurrection feature 2026-02-24 19:23:54 +01:00
CodeDevMLH
fa658c0057 Update rain animation and refactor rain.js comments for clarity 2026-02-24 19:23:42 +01:00
CodeDevMLH
de7e04c926 Refactor PiDay feature: clean up comments, adjust element creation, and add background mode support 2026-02-24 19:23:33 +01:00
CodeDevMLH
892be062d3 Add Pride feature: implement header color change and cleanup observer for pride effects 2026-02-24 19:23:24 +01:00
CodeDevMLH
042d89f5b8 Add Oscar feature: implement CSS and JS for Oscar animations, visibility control, and dynamic spotlight effects 2026-02-24 19:23:15 +01:00
CodeDevMLH
22709c38d1 Add Halloween feature: implement fog layer, spiders, and mice animations with visibility control 2026-02-24 19:23:07 +01:00
CodeDevMLH
22d40fb248 Add Oktoberfest feature: implement CSS and JS for Oktoberfest animations and visibility control 2026-02-24 19:22:57 +01:00
CodeDevMLH
97dbc09daa Refactor hearts.js: reorganize heartSymbols array and clean up observer initialization 2026-02-24 19:22:16 +01:00
CodeDevMLH
df29e12699 Add Frost feature: implement CSS and JS for frost effects and visibility toggle 2026-02-24 19:22:10 +01:00
CodeDevMLH
6632cc81de Add Mario Day feature: implement CSS and JS for Mario animations and visibility toggle 2026-02-24 19:22:03 +01:00
CodeDevMLH
437569ec1d Add Friday the 13th feature: implement CSS and JS for themed animations and visibility toggle 2026-02-24 19:21:49 +01:00
CodeDevMLH
5c0d8af5d8 Refactor fireworks.js: clean up observer options for improved readability 2026-02-24 19:20:02 +01:00
CodeDevMLH
5b98b442e5 Add Film Noir feature: implement CSS and JS for film noir effects and toggle functionality 2026-02-24 19:19:56 +01:00
CodeDevMLH
e81ce3cab1 Refactor eurovision.js and eurovision.css: remove commented code for clarity 2026-02-24 19:19:49 +01:00
CodeDevMLH
066ad6fc84 Add Eid feature: create CSS and JS for Eid symbols and animations 2026-02-24 19:19:38 +01:00
CodeDevMLH
8baaa936e1 Refactor easter.js: adjust observer initialization for consistency 2026-02-24 19:19:31 +01:00
CodeDevMLH
f9b4b3c25d Refactor earthday.js: move flowerColors definition to a constant and use it in flower generation 2026-02-24 19:19:25 +01:00
CodeDevMLH
f4f472e6ec Refactor christmas.js: reorganize christmasSymbols definition and streamline observer setup 2026-02-24 19:19:18 +01:00
CodeDevMLH
e8effa7dfe Format observer configuration for better readability 2026-02-24 19:19:11 +01:00
CodeDevMLH
ff2df0196a Refactor carnival.js: reorganize confettiColors definition and clean up observer setup 2026-02-24 19:19:03 +01:00
CodeDevMLH
3e5da3dda2 Refactor autumn.js: consolidate image array definition and clean up code structure 2026-02-24 19:18:50 +01:00
CodeDevMLH
509d198cd0 Add console log messages for Easter visibility changes; improve debugging 2026-02-24 18:38:14 +01:00
CodeDevMLH
26eb40e282 Adjust Earth Day CSS height and update JavaScript height configuration for consistency 2026-02-24 18:34:26 +01:00
CodeDevMLH
08b2ae987e Refactor bunny visibility toggle logic; streamline code and improve readability 2026-02-24 18:32:08 +01:00
CodeDevMLH
599518d627 Add new Easter and Friday-themed assets; remove outdated images 2026-02-24 18:24:37 +01:00
CodeDevMLH
23c5ab7e9d Fix random animation delay for leaf shake and rotation effect 2026-02-24 18:22:56 +01:00
CodeDevMLH
589a360729 Refactor Easter animation and grass generation; streamline code and improve performance 2026-02-24 18:21:51 +01:00
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
CodeDevMLH
fdadc00a0c Update manifest.json for release v1.7.1.4 [skip ci] 2026-02-21 14:24:25 +00:00
CodeDevMLH
2ab88fd5ac Bump version to 1.7.1.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-21 15:23:35 +01:00
CodeDevMLH
9a41c0a2ce Adjust sunbeam and butterfly positioning for improved visuals 2026-02-21 15:23:29 +01:00
CodeDevMLH
816f58cf02 Update SpringOptions configuration and HTML for seasonal options
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-02-21 15:11:29 +01:00
CodeDevMLH
5be9a60eed Update manifest.json for release v1.7.1.3 [skip ci] 2026-02-21 13:50:20 +00:00
CodeDevMLH
133808105e Bump version to 1.7.1.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-21 14:49:26 +01:00
CodeDevMLH
c631aca44f Add Cherry Blossom seasonal options to configuration 2026-02-21 14:49:08 +01:00
CodeDevMLH
241450d132 Increase default counts for pollen and butterflies in SpringOptions configuration 2026-02-21 14:44:09 +01:00
CodeDevMLH
d50d71bde1 Update manifest.json for release v1.7.1.2 [skip ci] 2026-02-21 03:33:25 +00:00
CodeDevMLH
262dd98519 Bump version to 1.7.1.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m42s
2026-02-21 04:31:42 +01:00
CodeDevMLH
b45ec73a67 Enhance spring configuration options by adding counts for birds, butterflies, bees, and ladybugs; update UI labels and descriptions for clarity. 2026-02-21 04:31:22 +01:00
CodeDevMLH
4e8a37540f Refactor spring and carnival animations, enhance configuration options, and improve asset management 2026-02-21 04:31:15 +01:00
CodeDevMLH
cde5201991 Add Cherry Blossom option to theme selector and configuration 2026-02-21 04:31:04 +01:00
CodeDevMLH
b2420b8eb4 Add Rotkehlchen GIF asset for spring animations 2026-02-21 04:30:54 +01:00
CodeDevMLH
dacec7d03c Add new spring-themed GIF assets for animations 2026-02-21 04:30:47 +01:00
CodeDevMLH
65f8261fb7 Add Cherry Blossom feature with configuration options and animations [skip ci] 2026-02-20 01:09:23 +01:00
CodeDevMLH
78872e7f96 Remove petal and ladybug functionality from spring animation 2026-02-20 00:55:54 +01:00
CodeDevMLH
45c9a199c2 Update manifest.json for release v1.7.1.1 [skip ci] 2026-02-19 18:00:57 +00:00
CodeDevMLH
1df6fb37b1 now?
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-19 18:51:28 +01:00
CodeDevMLH
82a1e8a178 Refactor RemoveLegacyTags method to include modification tracking and update logging
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 12s
2026-02-19 18:36:08 +01:00
CodeDevMLH
22bf887d10 Bump version to 1.7.1.1
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 52s
2026-02-19 18:31:49 +01:00
CodeDevMLH
07600766cf Add legacy tag removal functionality to ScriptInjector 2026-02-19 18:31:10 +01:00
CodeDevMLH
56298487f4 Update manifest.json for release v1.7.1.0 [skip ci] 2026-02-19 02:20:24 +00:00
CodeDevMLH
89fc1c38f0 Update version to 1.7.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-19 03:19:31 +01:00
CodeDevMLH
4c168a5ec2 Add Spring, Summer, and Carnival options to plugin configuration and UI 2026-02-19 03:18:39 +01:00
CodeDevMLH
92d9e1a9ad Add spring, summer, and carnival themes to the theme selector 2026-02-19 03:18:32 +01:00
CodeDevMLH
007e55a612 Refactor summer object creation to use configurable bubble and dust counts 2026-02-19 03:18:26 +01:00
CodeDevMLH
20da9899e4 Fix typo in resurrection CSS class and animation names 2026-02-19 03:18:20 +01:00
CodeDevMLH
9b9cad1caa Add carnival animation effects with CSS and JavaScript implementation 2026-02-19 03:18:13 +01:00
CodeDevMLH
e8e3424cc9 Add spring animation effects with CSS and JavaScript implementation 2026-02-19 03:18:06 +01:00
CodeDevMLH
0eeed99508 Add summer animation effects with CSS and JavaScript implementation 2026-02-19 02:22:54 +01:00
CodeDevMLH
a0f261f597 Update CONTRIBUTING.md to enhance clarity on testing themes with test-site.html [skip ci] 2026-02-17 14:23:03 +01:00
CodeDevMLH
35d92862aa Update manifest.json for release v1.7.0.15 [skip ci] 2026-02-17 13:11:22 +00:00
CodeDevMLH
693bb35aac Bump version to 1.7.0.15 and update embedded resources in project files
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-17 14:10:26 +01:00
CodeDevMLH
1ddaab325e Update CONTRIBUTING.md for clarity and consistency in theme development guidelines 2026-02-17 14:10:19 +01:00
CodeDevMLH
81facbdb00 Update README.md to reference contributing guidelines 2026-02-17 14:10:13 +01:00
CodeDevMLH
34a58ac4bd Add test-site.html for seasonal theme testing interface 2026-02-17 14:10:07 +01:00
CodeDevMLH
2d8444701d Remove test-site.html and test-site-new.html from .gitignore 2026-02-17 14:09:58 +01:00
CodeDevMLH
66f5353659 Add input fields for custom JavaScript and CSS in the control panel 2026-02-17 13:47:43 +01:00
CodeDevMLH
b58264998a Enhance theme loading by tracking animation frames, MutationObservers, and intervals; wrap JS in IIFE for scope isolation
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 37s
2026-02-17 02:01:05 +01:00
CodeDevMLH
76c0bc5b3b Add CONTRIBUTING.md for theme development guidelines and update test site styles
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 36s
2026-02-17 01:58:58 +01:00
CodeDevMLH
1428db3e1e Update manifest.json for release v1.7.0.14 [skip ci] 2026-02-16 23:02:32 +00:00
CodeDevMLH
1f5f436e44 Bump version to 1.7.0.14
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-17 00:01:41 +01:00
CodeDevMLH
46f5c3648d Refactor seasonal rule HTML generation for improved maintainability 2026-02-17 00:01:21 +01:00
CodeDevMLH
555e2ab8be Update manifest.json for release v1.7.0.13 [skip ci] 2026-02-16 22:37:20 +00:00
CodeDevMLH
26eadfc0aa Bump version to 1.7.0.13
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-16 23:36:26 +01:00
CodeDevMLH
142f538939 Enhance select elements in date range group with consistent styling 2026-02-16 23:36:08 +01:00
CodeDevMLH
b64e80fd60 Update manifest.json for release v1.7.0.12 [skip ci] 2026-02-16 22:18:09 +00:00
CodeDevMLH
fbf5fc7edf Bump version to 1.7.0.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 23:17:17 +01:00
CodeDevMLH
8defba4623 Refactor date selection logic in SeasonalsConfigPage to use helper function for generating options 2026-02-16 23:17:02 +01:00
CodeDevMLH
7f968ee050 Update manifest.json for release v1.7.0.11 [skip ci] 2026-02-16 22:06:11 +00:00
CodeDevMLH
dec5bbe39e Bump version to 1.7.0.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 23:05:18 +01:00
CodeDevMLH
63f3211cc4 Refactor date selection options to use SeasonalsConfigPage methods for consistency 2026-02-16 23:05:06 +01:00
CodeDevMLH
4270235c78 Update manifest.json for release v1.7.0.10 [skip ci] 2026-02-16 22:02:53 +00:00
CodeDevMLH
76d8a67914 Bump version to 1.7.0.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 23:02:00 +01:00
CodeDevMLH
1a3caf5da6 Enhance configuration page: replace input fields with dropdowns for date selection and add utility functions for generating options 2026-02-16 23:01:46 +01:00
CodeDevMLH
3b3ef77e61 fix ui [skip ci] 2026-02-16 19:31:42 +01:00
CodeDevMLH
ba580b1b52 Update manifest.json for release v1.7.0.9 [skip ci] 2026-02-16 18:27:34 +00:00
CodeDevMLH
0a6284c716 Bump version to 1.7.0.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-16 19:26:43 +01:00
CodeDevMLH
f83e863664 .. 2026-02-16 19:26:26 +01:00
CodeDevMLH
747e8ed6bc Update manifest.json for release v1.7.0.8 [skip ci] 2026-02-16 18:01:43 +00:00
CodeDevMLH
30845442b2 Bump version to 1.7.0.8
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-16 19:00:51 +01:00
CodeDevMLH
bb83201736 .. 2026-02-16 18:59:28 +01:00
CodeDevMLH
457ae404ba Update manifest.json for release v1.7.0.7 [skip ci] 2026-02-16 17:49:49 +00:00
CodeDevMLH
b6d679f6ef Update version to 1.7.0.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-16 18:48:51 +01:00
CodeDevMLH
3b88a1809d small UI changes 2026-02-16 18:48:07 +01:00
CodeDevMLH
4614ce4a7a Update manifest.json for release v1.7.0.5 [skip ci] 2026-02-16 17:16:03 +00:00
CodeDevMLH
57840bb149 Bump version to 1.7.0.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m5s
2026-02-16 18:14:59 +01:00
CodeDevMLH
dd90a4630a Update layout of seasonal rules in configuration page for improved responsiveness 2026-02-16 18:14:45 +01:00
CodeDevMLH
b5d5e5706e Update manifest.json for release v1.7.0.4 [skip ci] 2026-02-16 16:31:00 +00:00
CodeDevMLH
a4b5cf5b6b Bump version to 1.7.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m3s
2026-02-16 17:29:57 +01:00
CodeDevMLH
353bda10df Enhance configuration page: add section titles and improve input handling for seasonal themes 2026-02-16 17:29:41 +01:00
CodeDevMLH
0e1b91d93c Update manifest.json for release v1.7.0.3 [skip ci] 2026-02-16 15:55:27 +00:00
CodeDevMLH
9363008d07 Bump version to 1.7.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 16:54:37 +01:00
CodeDevMLH
faec7d8941 Refactor configuration page: update button classes, form input names, and tab handling for consistency 2026-02-16 16:54:22 +01:00
CodeDevMLH
7cc70854c4 Update manifest.json for release v1.7.0.2 [skip ci] 2026-02-16 15:14:02 +00:00
CodeDevMLH
9432f7aa86 Bump version to 1.7.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-16 16:13:07 +01:00
CodeDevMLH
4f7243bc74 test 2026-02-16 16:12:52 +01:00
CodeDevMLH
ee724fedc8 Update manifest.json for release v1.7.0.1 [skip ci] 2026-02-16 14:41:02 +00:00
CodeDevMLH
a1dbd4eb12 Bump version to 1.7.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m1s
2026-02-16 15:40:03 +01:00
CodeDevMLH
236d8d9e70 fix tabs 2026-02-16 15:39:42 +01:00
CodeDevMLH
6d55ae7524 Update manifest.json for release v1.7.0.0 [skip ci] 2026-02-16 01:31:56 +00:00
CodeDevMLH
99a0613893 Update version to 1.7.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 02:31:06 +01:00
CodeDevMLH
61952a0af7 Add seasonal rules configuration and enhance settings UI 2026-02-16 02:28:47 +01:00
CodeDevMLH
eca6ba96fb Add resurrection theme and enhance seasonal rules parsing logic [skip ci] 2026-02-16 02:28:36 +01:00
CodeDevMLH
c2f0f01689 Add release existence check to automation workflow 2026-02-16 02:28:16 +01:00
CodeDevMLH
30d17baff4 add (komisches) neues PR theme 2026-02-16 02:28:03 +01:00
CodeDevMLH
96bb1a3744 Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 01:12:58 +00:00
CodeDevMLH
772a0dae40 rebuild
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-15 02:12:09 +01:00
CodeDevMLH
40c4454397 Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 01:08:22 +00:00
CodeDevMLH
e5915e715a fix path issue on subpath installations
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-15 02:07:30 +01:00
CodeDevMLH
c171fc15f5 Update changelog for version 1.6.13.5 to include additional improvements [skip ci] 2026-02-04 19:37:34 +01:00
CodeDevMLH
a749b1f98e Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 18:08:31 +00:00
CodeDevMLH
6ccf6201b4 Auto-Update MediaBar Enhanced to v1.4.0.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 18:07:46 +00:00
CodeDevMLH
a69c741a39 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:59:08 +00:00
CodeDevMLH
d54b4f9b07 Auto-Update MediaBar Enhanced to v1.4.0.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 17:58:20 +00:00
CodeDevMLH
2cd427b6e9 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:52:16 +00:00
CodeDevMLH
55c1f8b191 Auto-Update MediaBar Enhanced to v1.4.0.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-02-04 17:51:32 +00:00
CodeDevMLH
fc3d6efd1c Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:40:04 +00:00
CodeDevMLH
5ba5940e5f Auto-Update MediaBar Enhanced to v1.4.0.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 17:39:18 +00:00
CodeDevMLH
621b7da344 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:28:13 +00:00
CodeDevMLH
268ce5e307 Auto-Update MediaBar Enhanced to v1.4.0.8
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 17:27:28 +00:00
CodeDevMLH
412cc2d981 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:10:23 +00:00
CodeDevMLH
949df24bdb Auto-Update MediaBar Enhanced to v1.4.0.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 17:09:35 +00:00
CodeDevMLH
b987969200 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:41:16 +00:00
CodeDevMLH
3306bb703d Auto-Update MediaBar Enhanced to v1.4.0.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 16:40:28 +00:00
CodeDevMLH
6587a4e3d0 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:24:07 +00:00
CodeDevMLH
f794b71f44 Auto-Update MediaBar Enhanced to v1.4.0.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 16:23:20 +00:00
CodeDevMLH
34363c502a Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:18:09 +00:00
CodeDevMLH
add2f7a551 Auto-Update MediaBar Enhanced to v1.4.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 16:17:22 +00:00
CodeDevMLH
1d7e9e27ec Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:16:56 +00:00
CodeDevMLH
6459653328 Bump version to 1.6.13.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m0s
2026-02-04 17:15:56 +01:00
CodeDevMLH
9d738e6061 Disable dragging and pointer events for seasonal settings button image 2026-02-04 17:15:39 +01:00
CodeDevMLH
8f5a3650e6 Update button image size in SeasonalSettingsManager 2026-02-04 17:13:27 +01:00
CodeDevMLH
229f9fe5ab Update manifest.json for release v1.6.13.4 [skip ci] 2026-02-04 15:53:04 +00:00
CodeDevMLH
0686129590 Auto-Update MediaBar Enhanced to v1.4.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-04 15:52:17 +00:00
CodeDevMLH
cb0392eb0d Update manifest.json for release v1.6.13.4 [skip ci] 2026-02-04 15:51:34 +00:00
CodeDevMLH
ed13e05b82 Bump version to 1.6.13.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-04 16:50:44 +01:00
CodeDevMLH
310fb4d496 Rename SettingsManager to SeasonalSettingsManager and update related log messages 2026-02-04 16:50:26 +01:00
CodeDevMLH
78d25106db Update manifest.json for release v1.6.13.3 [skip ci] 2026-02-04 15:46:34 +00:00
CodeDevMLH
a328171a8a Auto-Update MediaBar Enhanced to v1.4.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 15:45:45 +00:00
CodeDevMLH
361559cbec Update manifest.json for release v1.6.13.3 [skip ci] 2026-02-04 15:22:39 +00:00
CodeDevMLH
e08bf66a53 Bump version to 1.6.13.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-04 16:21:48 +01:00
CodeDevMLH
d6ef81138d typo 2026-02-04 16:21:02 +01:00
CodeDevMLH
35f21e680a Update manifest.json for release v1.6.13.2 [skip ci] 2026-02-04 15:02:42 +00:00
CodeDevMLH
705fbaed9d Auto-Update MediaBar Enhanced to v1.4.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 15:01:55 +00:00
CodeDevMLH
9e52198ef7 Update manifest.json for release v1.6.13.2 [skip ci] 2026-02-04 15:01:13 +00:00
CodeDevMLH
b1943dfe17 Bump version to 1.6.13.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-04 16:00:20 +01:00
CodeDevMLH
c55e900c0f Enhance logging messages in SeasonalsManager for better clarity 2026-02-04 14:05:41 +01:00
CodeDevMLH
503e9addee Update manifest.json for release v1.6.13.1 [skip ci] 2026-02-04 12:50:35 +00:00
CodeDevMLH
d630fdd217 Auto-Update MediaBar Enhanced to v1.4.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-04 12:49:50 +00:00
CodeDevMLH
7e4a7c2a6e Update manifest.json for release v1.6.13.1 [skip ci] 2026-02-04 12:47:05 +00:00
CodeDevMLH
1716a771f3 Auto-Update MediaBar Enhanced to v1.4.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-04 12:46:21 +00:00
CodeDevMLH
36347cc4b0 Update manifest.json for release v1.6.13.1 [skip ci] 2026-02-04 12:39:14 +00:00
CodeDevMLH
7f94164e55 Update version to 1.6.13.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 13:38:23 +01:00
CodeDevMLH
cbab7de546 Refactor seasonals.js 2026-02-04 13:37:49 +01:00
CodeDevMLH
d0de5cd021 Update seasonals settings to use namespaced localStorage keys and enhance field description for client-side toggle 2026-02-04 12:40:50 +01:00
CodeDevMLH
16628e9902 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 01:42:27 +00:00
CodeDevMLH
72bfe0a14a Auto-Update MediaBar Enhanced to v1.3.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 01:41:42 +00:00
CodeDevMLH
6498ec4216 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 01:28:50 +00:00
CodeDevMLH
0d350fc76b Auto-Update MediaBar Enhanced to v1.3.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-04 01:28:02 +00:00
CodeDevMLH
2c6e4ce610 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 01:15:12 +00:00
CodeDevMLH
0c552774dc Auto-Update MediaBar Enhanced to v1.3.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 01:14:26 +00:00
CodeDevMLH
9ab605bb74 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 00:08:09 +00:00
CodeDevMLH
3d6cba0fe4 Auto-Update MediaBar Enhanced to v1.3.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 00:07:22 +00:00
CodeDevMLH
32e5e2b690 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-03 23:12:29 +00:00
CodeDevMLH
c967c1e308 Bump version to 1.6.13.0 and update changelog for new features and fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-04 00:11:38 +01:00
CodeDevMLH
ae28d5219b Enhance README.md with additional details and images for configuration UI and user toggle features 2026-02-03 23:47:58 +01:00
CodeDevMLH
e4228f889e Add guidance for safely resetting 'dev' branch to 'main' in Git [skip ci] 2026-02-03 23:22:33 +01:00
CodeDevMLH
6d721c755e Update README.md to enhance documentation and add new seasonal themes 2026-02-03 23:06:47 +01:00
CodeDevMLH
6948953778 Add optional stash commands to Git rebase instructions 2026-02-03 23:04:58 +01:00
CodeDevMLH
8a50cef330 Update Git rebase instructions to include fetching from origin [skip ci] 2026-02-03 22:37:20 +01:00
CodeDevMLH
a0bf5370bd Update manifest.json for release v1.6.12.0 [skip ci] 2026-02-03 21:28:05 +00:00
CodeDevMLH
c5800b431d Bump version to 1.6.12.0 and update disabled options in configuration page
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-03 22:27:15 +01:00
CodeDevMLH
9a4056651d Update manifest.json for release v1.6.11.0 [skip ci] 2026-02-03 21:10:34 +00:00
CodeDevMLH
87382db78e Bump version to 1.6.11.0 and update configuration styles in the settings page
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-03 22:09:43 +01:00
CodeDevMLH
5d9afa762f Update manifest.json for release v1.6.10.0 [skip ci] 2026-02-03 21:03:41 +00:00
CodeDevMLH
2f88587dab Bump version to 1.6.10.0 and update logo source in settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 22:02:50 +01:00
CodeDevMLH
360a959b69 rename logo [skip ci] 2026-02-03 22:02:01 +01:00
CodeDevMLH
36fba545cf Update manifest.json for release v1.6.9.0 [skip ci] 2026-02-03 21:01:40 +00:00
CodeDevMLH
7faa2cc766 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-02-03 22:00:43 +01:00
CodeDevMLH
aa832e93aa add logo [skip ci] 2026-02-03 22:00:32 +01:00
CodeDevMLH
86bbeb583d Update manifest.json for release v1.6.9.0 [skip ci] 2026-02-03 20:59:03 +00:00
CodeDevMLH
7a642b34b8 add assets [skip CI]
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 21:58:01 +01:00
CodeDevMLH
926b30b8ce Update manifest.json for release v1.6.9.0 [skip ci] 2026-02-03 20:30:56 +00:00
CodeDevMLH
5b672cef42 Bump version to 1.6.9.0; update logo asset path and manifest for new release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 21:30:03 +01:00
CodeDevMLH
ceccbf4ded Update manifest.json for release v1.6.8.0 [skip ci] 2026-02-03 20:24:42 +00:00
CodeDevMLH
9cba2a0755 Bump version to 1.6.8.0; update manifest and add new logo assets
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m6s
2026-02-03 21:23:36 +01:00
CodeDevMLH
af036a9aa4 Update manifest.json for release v1.6.7.0 [skip ci] 2026-02-03 19:59:06 +00:00
CodeDevMLH
cfefd2d2d3 Bump version to 1.6.7.0; update manifest and configuration files for improved seasonal settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 20:58:14 +01:00
CodeDevMLH
8e7299508b Update manifest.json for release v1.6.6.0 [skip ci] 2026-02-03 19:51:20 +00:00
CodeDevMLH
fc7aa36f41 Bump version to 1.6.6.0; update manifest and settings popup for improved theme selection
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 20:50:28 +01:00
CodeDevMLH
fc9896048f Update manifest.json for release v1.6.5.0 [skip ci] 2026-02-03 19:37:37 +00:00
CodeDevMLH
572c4d9ace Bump version to 1.6.5.0 and update manifest; add new select options in settings popup
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-03 20:36:45 +01:00
CodeDevMLH
2572e085f6 Update manifest.json for release v1.6.4.0 [skip ci] 2026-02-03 18:46:36 +00:00
CodeDevMLH
8297f989fd Bump version to 1.6.4.0 and update changelog for new features; modify select classes in config page and settings popup
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-03 19:45:38 +01:00
CodeDevMLH
636aaa2a4a Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-03 18:33:25 +00:00
CodeDevMLH
5e70621e93 Update version to 1.6.3.0, modify checkbox layout, and adjust popup styles
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 19:32:34 +01:00
209 changed files with 12052 additions and 1988 deletions

View File

@@ -51,7 +51,31 @@ jobs:
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Check if Release Already Exists
id: check_release
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
TAG="v$VERSION"
SERVER_URL="https://git.mahom03-spacecloud.de"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "Release $TAG already exists. Skipping release-related steps."
echo "release_exists=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "No existing release for $TAG. Continuing."
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "Unexpected response when checking release: $HTTP_STATUS"
exit 1
fi
- name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash
run: |
# Inject version from manifest into the build
@@ -71,6 +95,7 @@ jobs:
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
- name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
@@ -90,12 +115,14 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json
- name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: akkuman/gitea-release-action@v1
with:
server_url: "https://git.mahom03-spacecloud.de"
@@ -109,6 +136,7 @@ jobs:
# Update Message in Remote Repository
- name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6
with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
@@ -116,6 +144,7 @@ jobs:
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash
run: |
cd central-manifest
@@ -171,6 +200,7 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: |
cd central-manifest
git config user.name "CodeDevMLH"

3
.gitignore vendored
View File

@@ -4,6 +4,5 @@ obj/
.idea/
artifacts
test-site.html
test-site-new.html
test-site-old.html
RELEASE_GUIDE.md

343
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,343 @@
# Contributing to Jellyfin Seasonals Plugin
Thank you for your interest in contributing seasonal themes to the Jellyfin Seasonals Plugin! This guide explains how seasonal themes are structured, how to create your own, and how to test them locally before submitting a pull request.
---
## Table of Contents
- [Contributing to Jellyfin Seasonals Plugin](#contributing-to-jellyfin-seasonals-plugin)
- [Table of Contents](#table-of-contents)
- [Theme Architecture Overview](#theme-architecture-overview)
- [Standard Theme File Structure](#standard-theme-file-structure)
- [JavaScript File Pattern](#javascript-file-pattern)
- [Key Rules](#key-rules)
- [CSS File Pattern](#css-file-pattern)
- [Key Rules](#key-rules-1)
- [Image Assets (Optional)](#image-assets-optional)
- [Registering Your Theme](#registering-your-theme)
- [1. `seasonals.js` — Client-Side Registration](#1-seasonalsjs--client-side-registration)
- [2. `PluginConfiguration.cs` and `configPage.html` - Server-Side Registration](#2-pluginconfigurationcs-and-configpagehtml---server-side-registration)
- [Testing Your Theme Locally](#testing-your-theme-locally)
- [Steps](#steps)
- [What to Verify](#what-to-verify)
- [Submitting Your Contribution](#submitting-your-contribution)
- [Pull Request Checklist](#pull-request-checklist)
- [PR Description Template](#pr-description-template)
- [GitHub Issue Template for Theme Ideas](#github-issue-template-for-theme-ideas)
- [Questions?](#questions)
---
## Theme Architecture Overview
Each seasonal theme consists of **23 components** that live in `Jellyfin.Plugin.Seasonals/Web/`:
| Component | File(s) | Purpose |
| :--- | :--- | :--- |
| **JavaScript** | `{themeName}.js` | Animation logic, DOM manipulation, element creation |
| **CSS** | `{themeName}.css` | Container styling, element appearance, keyframe animations |
| **Images** *(optional)* | `{themeName}_images/` | Image assets (PNGs, SVGs) used by the theme |
The orchestrator file `seasonals.js` manages theme loading at runtime. It reads the plugin configuration, determines which theme should be active, and dynamically injects the correct CSS and JS files.
---
## Standard Theme File Structure
Here is a complete file layout for a theme called `mytheme`:
```
Jellyfin.Plugin.Seasonals/
└── Web/
├── mytheme.js # Animation/DOM logic
├── mytheme.css # Styles & animations
├── mytheme_images/ # (Optional) image assets
│ ├── sprite1.png
│ └── sprite2.png
└── seasonals.js # (Existing) Add your theme to ThemeConfigs
```
---
## JavaScript File Pattern
Every theme JS file follows a **consistent skeleton**. Use this as your starting template:
```javascript
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.MyTheme || {};
const enabled = config.EnableMyTheme !== undefined ? config.EnableMyTheme : true;
const elementCount = config.ElementCount || 25;
// ... add more config options as needed
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 toggleMyTheme() {
const container = document.querySelector('.mytheme-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('MyTheme hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('MyTheme visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
// Watches the DOM for changes so the effect can auto-hide/show.
const observer = new MutationObserver(toggleMyTheme);
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('.mytheme-container') || document.createElement('div');
if (!document.querySelector('.mytheme-container')) {
container.className = 'mytheme-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
for (let i = 0; i < elementCount; i++) {
const el = document.createElement('div');
el.className = 'mytheme-element';
// Set random position, delay, duration, etc.
el.style.left = `${Math.random() * 100}%`;
el.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 4}s`;
// If using images:
// const img = document.createElement('img');
// img.src = '../Seasonals/Resources/mytheme_images/sprite1.png';
// el.appendChild(img);
// If using text/emoji:
// el.textContent = '⭐';
container.appendChild(el);
}
}
// 5. Initialization
function initializeMyTheme() {
if (!enabled) return;
createElements();
toggleMyTheme();
}
initializeMyTheme();
```
### Key Rules
- **Always** read config from `window.SeasonalsPluginConfig?.{ThemeName}`.
- **Always** implement the toggle function with the same selectors (`.videoPlayerContainer`, `.youtubePlayerContainer`, `.dashboardDocument`, `#app-user-menu`, just use the above template).
- **Always** use `aria-hidden="true"` on the container for accessibility.
- Call your `initialize` function at the end of the file.
- For **canvas-based** themes (like `snowfall.js`), use a `<canvas>` element with `requestAnimationFrame` instead of CSS animations. Make sure to clean up with `cancelAnimationFrame` when hidden.
---
## CSS File Pattern
Every theme CSS file follows this structure:
```css
/* Container */
/* Full-screen overlay, transparent, non-interactive */
.mytheme-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* IMPORTANT: don't block user interaction */
z-index: 10;
}
/* Animated Element */
.mytheme-element {
position: fixed;
z-index: 15;
user-select: none;
cursor: default;
/* Two animations: movement + secondary effect (shake, rotate, etc.) */
animation-name: mytheme-fall, mytheme-shake;
animation-duration: 10s, 3s;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
}
/* Keyframes */
@keyframes mytheme-fall {
0% { top: -10%; }
100% { top: 100%; }
}
@keyframes mytheme-shake {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(80px); }
}
/* Staggered Delays for Base Elements */
/* Spread the initial 12 elements across the screen */
.mytheme-element:nth-of-type(1) { left: 10%; animation-delay: 1s, 1s; }
.mytheme-element:nth-of-type(2) { left: 20%; animation-delay: 6s, 0.5s; }
.mytheme-element:nth-of-type(3) { left: 30%; animation-delay: 4s, 2s; }
/* ... continue for each base element */
```
### Key Rules
- **Container** must be `position: fixed`, full-screen, with `pointer-events: none` and at least `z-index: 10`.
- **Elements** should use `position: fixed` with at least `z-index: 15`.
- Use **animations** (eg. primary movement + secondary effect for natural-looking motion).
- Include **`nth-of-type` rules** for the initial set of base elements to stagger them.
- Include **webkit prefixes** (`-webkit-animation-*`, `@-webkit-keyframes`) for broader compatibility (see existing themes for examples).
---
## Image Assets (Optional)
If your theme uses images (e.g., leaves, ghosts, eggs):
1. Create a folder: `Jellyfin.Plugin.Seasonals/Web/{themeName}_images/`
2. Place your assets inside (PNG recommended, keep files small)
3. Reference them in JS using the production path:
```javascript
img.src = '../Seasonals/Resources/mytheme_images/sprite1.png';
```
---
## Registering Your Theme
After creating your JS and CSS files, you need to register the theme in two places:
### 1. `seasonals.js` — Client-Side Registration
Add your theme to the `ThemeConfigs` object:
```javascript
const ThemeConfigs = {
// ... existing themes ...
mytheme: {
css: '../Seasonals/Resources/mytheme.css',
js: '../Seasonals/Resources/mytheme.js',
containerClass: 'mytheme-container'
},
// ...
};
```
### 2. `PluginConfiguration.cs` and `configPage.html` - Server-Side Registration
> [!NOTE]
> The backend registration is handled by the plugin maintainers. You do **not** need to modify C# files for your theme submission. Just focus on the JS/CSS/images.
>
> However, if you'd like to include full backend integration, add your theme to the enum/configuration in `Configuration/PluginConfiguration.cs` and the selectors in `configPage.html`.
---
## Testing Your Theme Locally
You can test your theme without a Jellyfin server by using the included test site.
### Steps
1. Navigate to the `Jellyfin.Plugin.Seasonals/Web/` directory
2. Open [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html) in your browser (just double-click the file) or vscode or what ever you use...
3. Use the **theme selector dropdown** to pick an existing theme or select **"Custom (Local Files)"** to test your own
4. When "Custom" is selected, enter your theme's JS and CSS filenames (e.g., `mytheme.js` and `mytheme.css` (must be in the same folder as [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html) for this to work))
5. Click **"Load Theme"** to apply. Click **"Clear & Reload"** to reset and try again
### What to Verify
- ✅ The effect is visible on the background
- ✅ The animation runs smoothly
- ✅ Elements are spread across the full viewport
- ✅ The mock header is **not blocked** by the effect (thanks to `pointer-events: none`)
- ✅ No theme related console errors appear (check DevTools → Console)
---
## Submitting Your Contribution
### Pull Request Checklist
- [ ] Created `{themeName}.js` following the [JS pattern](#javascript-file-pattern)
- [ ] Created `{themeName}.css` following the [CSS pattern](#css-file-pattern)
- [ ] (If applicable) Created `{themeName}_images/` with optimized assets
- [ ] Added theme to `ThemeConfigs` in `seasonals.js`
- [ ] Tested locally with [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html)
- [ ] No theme related console errors
- [ ] Effect has `pointer-events: none` (doesn't block the UI)
- [ ] Effect hides during video/trailer playback (toggle function implemented)
- [ ] (Optional) Included a screenshot or short recording of the effect to the readme
### PR Description Template
```
## New Seasonal Theme: {Theme Name}
**Description:** Brief description of the theme and what occasion/season it's for.
**Screenshot / Recording:**
[Attach a screenshot or GIF showcasing the theme in action]
**Testing:**
- Tested locally with test-site-new.html ✅
- No console errors ✅
- pointer-events: none verified ✅
```
---
## GitHub Issue Template for Theme Ideas
If you have an idea for a seasonal theme but don't want to implement it yourself, feel free to open an issue using the following template:
**Title:** `[Theme Idea] {Season/Holiday Name} Theme`
**Body:**
```
## 🎨 Theme Idea: {Season/Holiday Name}
**Occasion/Season:** What time of year is this for?
**Description:** Describe the visual effect you have in mind.
**Visual References:** Links to images, GIFs, or videos that capture the aesthetic.
**Suggested Active Period:** e.g. "March 1 March 17" for St. Patrick's Day
```
---
## Questions?
If you have any questions about contributing, feel free to open an issue. Happy theming! 🎉

207
Injector_new.cs Normal file
View File

@@ -0,0 +1,207 @@
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using MediaBrowser.Common.Configuration;
using Jellyfin.Plugin.Seasonals.Helpers;
using System.Collections.Generic;
using System.Linq;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Handles the injection of the Seasonals script into the Jellyfin web interface.
/// </summary>
public class ScriptInjector
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
public const string Marker = "</body>";
/// <summary>
/// Initializes a new instance of the <see cref="ScriptInjector"/> class.
/// </summary>
/// <param name="appPaths">The application paths.</param>
/// <param name="logger">The logger.</param>
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
{
_appPaths = appPaths;
_logger = logger;
}
/// <summary>
/// Injects the script tag into index.html if it's not already present.
/// </summary>
public void Inject()
{
try
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath))
{
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
RegisterFileTransformation();
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath))
{
_logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
RegisterFileTransformation();
return;
}
var content = File.ReadAllText(indexPath);
if (!content.Contains(ScriptTag))
{
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
content = content.Insert(index, ScriptTag + Environment.NewLine);
File.WriteAllText(indexPath, content);
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
}
else
{
_logger.LogWarning("Script already present in index.html. Or could not be injected.");
}
}
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
RegisterFileTransformation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
RegisterFileTransformation();
}
}
/// <summary>
/// Removes the script tag from index.html.
/// </summary>
public void Remove()
{
UnregisterFileTransformation();
try
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath))
{
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath))
{
return;
}
var content = File.ReadAllText(indexPath);
if (content.Contains(ScriptTag))
{
content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
File.WriteAllText(indexPath, content);
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
} else {
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
}
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing Seasonals script.");
}
}
/// <summary>
/// Retrieves the path to the Jellyfin web interface directory.
/// </summary>
/// <returns>The path to the web directory, or null if not found.</returns>
private string? GetWebPath()
{
// Use reflection to access WebPath property to ensure compatibility across different Jellyfin versions
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
return prop?.GetValue(_appPaths) as string;
}
private void RegisterFileTransformation()
{
_logger.LogInformation("Seasonals Fallback. Registering file transformations.");
List<JObject> payloads = new List<JObject>();
{
JObject payload = new JObject();
payload.Add("id", "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
payload.Add("fileNamePattern", "index.html");
payload.Add("callbackAssembly", GetType().Assembly.FullName);
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
payloads.Add(payload);
}
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
foreach (JObject payload in payloads)
{
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
}
_logger.LogInformation("File transformations registered successfully.");
}
else
{
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
}
}
else
{
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback injection skipped.");
}
}
private void UnregisterFileTransformation()
{
try
{
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
Guid id = Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
_logger.LogInformation("File transformation unregistered successfully.");
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
}
}
}

View File

@@ -62,6 +62,7 @@ public class SeasonalsController : ControllerBase
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif";
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return "image/svg+xml";
return "application/octet-stream";
}
}

View File

@@ -18,15 +18,40 @@ public class PluginConfiguration : BasePluginConfiguration
EnableClientSideToggle = true;
Autumn = new AutumnOptions();
Snowflakes = new SnowflakesOptions();
Snowfall = new SnowfallOptions();
Snowstorm = new SnowstormOptions();
Birthday = new BirthdayOptions();
Carnival = new CarnivalOptions();
CherryBlossom = new CherryBlossomOptions();
Christmas = new ChristmasOptions();
EarthDay = new EarthDayOptions();
Easter = new EasterOptions();
Eid = new EidOptions();
Eurovision = new EurovisionOptions();
FilmNoir = new FilmNoirOptions();
Fireworks = new FireworksOptions();
Friday13 = new Friday13Options();
Frost = new FrostOptions();
Halloween = new HalloweenOptions();
Hearts = new HeartsOptions();
Christmas = new ChristmasOptions();
MarioDay = new MarioDayOptions();
Matrix = new MatrixOptions();
Oktoberfest = new OktoberfestOptions();
Olympia = new OlympiaOptions();
Oscar = new OscarOptions();
Rain = new RainOptions();
Pride = new PrideOptions();
Resurrection = new ResurrectionOptions();
Santa = new SantaOptions();
Easter = new EasterOptions();
Snowfall = new SnowfallOptions();
Snowflakes = new SnowflakesOptions();
Snowstorm = new SnowstormOptions();
Space = new SpaceOptions();
Spooky = new SpookyOptions();
Sports = new SportsOptions();
Spring = new SpringOptions();
StarWars = new StarWarsOptions();
Storm = new StormOptions();
Summer = new SummerOptions();
Underwater = new UnderwaterOptions();
}
/// <summary>
@@ -49,120 +74,315 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary>
public bool EnableClientSideToggle { get; set; }
/// <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\"},{\"Name\":\"Cherry Blossom\",\"StartDay\":1,\"StartMonth\":4,\"EndDay\":30,\"EndMonth\":4,\"Theme\":\"cherryblossom\"}]";
/// <summary>
/// Gets or sets the Seasonals options.
/// </summary>
public AutumnOptions Autumn { get; set; }
public SnowflakesOptions Snowflakes { get; set; }
public SnowfallOptions Snowfall { get; set; }
public SnowstormOptions Snowstorm { get; set; }
public BirthdayOptions Birthday { get; set; }
public CarnivalOptions Carnival { get; set; }
public CherryBlossomOptions CherryBlossom { get; set; }
public ChristmasOptions Christmas { get; set; }
public EarthDayOptions EarthDay { get; set; }
public EasterOptions Easter { get; set; }
public EidOptions Eid { get; set; }
public EurovisionOptions Eurovision { get; set; }
public FilmNoirOptions FilmNoir { get; set; }
public FireworksOptions Fireworks { get; set; }
public Friday13Options Friday13 { get; set; }
public FrostOptions Frost { get; set; }
public HalloweenOptions Halloween { get; set; }
public HeartsOptions Hearts { get; set; }
public ChristmasOptions Christmas { get; set; }
public MarioDayOptions MarioDay { get; set; }
public MatrixOptions Matrix { get; set; }
public OktoberfestOptions Oktoberfest { get; set; }
public OlympiaOptions Olympia { get; set; }
public OscarOptions Oscar { get; set; }
public PrideOptions Pride { get; set; }
public RainOptions Rain { get; set; }
public ResurrectionOptions Resurrection { get; set; }
public SantaOptions Santa { get; set; }
public EasterOptions Easter { get; set; }
public SnowfallOptions Snowfall { get; set; }
public SnowflakesOptions Snowflakes { get; set; }
public SnowstormOptions Snowstorm { get; set; }
public SpaceOptions Space { get; set; }
public SpookyOptions Spooky { get; set; }
public SportsOptions Sports { get; set; }
public SpringOptions Spring { get; set; }
public StarWarsOptions StarWars { get; set; }
public StormOptions Storm { get; set; }
public SummerOptions Summer { get; set; }
public UnderwaterOptions Underwater { get; set; }
}
public class AutumnOptions
{
public int LeafCount { get; set; } = 25;
public class AutumnOptions {
public bool EnableAutumn { get; set; } = true;
public bool EnableRandomLeaves { get; set; } = true;
public bool EnableRandomLeavesMobile { get; set; } = false;
public int LeafCount { get; set; } = 35;
public int LeafCountMobile { get; set; } = 10;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableRotation { get; set; } = false;
}
public class SnowflakesOptions
{
public int SnowflakeCount { get; set; } = 25;
public bool EnableSnowflakes { get; set; } = true;
public bool EnableRandomSnowflakes { get; set; } = true;
public bool EnableRandomSnowflakesMobile { get; set; } = false;
public bool EnableColoredSnowflakes { get; set; } = true;
public class BirthdayOptions {
public bool EnableBirthday { get; set; } = true;
public int SymbolCount { get; set; } = 12;
public int SymbolCountMobile { get; set; } = 5;
public bool EnableDifferentDuration { get; set; } = true;
public int ConfettiCount { get; set; } = 60;
}
public class CarnivalOptions {
public bool EnableCarnival { get; set; } = true;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableCarnivalSway { get; set; } = true;
public int ObjectCount { get; set; } = 120;
public int ObjectCountMobile { get; set; } = 60;
}
public class CherryBlossomOptions {
public bool EnableCherryBlossom { get; set; } = true;
public int PetalCount { get; set; } = 25;
public int PetalCountMobile { get; set; } = 15;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SnowfallOptions
{
public int SnowflakesCount { get; set; } = 500;
public int SnowflakesCountMobile { get; set; } = 250;
public double Speed { get; set; } = 3;
public bool EnableSnowfall { get; set; } = true;
public class ChristmasOptions {
public bool EnableChristmas { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public int SymbolCountMobile { get; set; } = 10;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SnowstormOptions
{
public int SnowflakesCount { get; set; } = 500;
public int SnowflakesCountMobile { get; set; } = 250;
public double Speed { get; set; } = 6;
public bool EnableSnowstorm { get; set; } = true;
public double HorizontalWind { get; set; } = 4;
public double VerticalVariation { get; set; } = 2;
public class EarthDayOptions {
public bool EnableEarthDay { get; set; } = true;
public int VineCount { get; set; } = 4;
}
public class FireworksOptions
{
public int ParticleCount { get; set; } = 50;
public int LaunchInterval { get; set; } = 3200;
public class EasterOptions {
public bool EnableEaster { get; set; } = true;
public bool EnableBunny { get; set; } = true;
public int MinBunnyRestTime { get; set; } = 2000;
public int MaxBunnyRestTime { get; set; } = 5000;
public int EggCount { get; set; } = 15;
}
public class EidOptions {
public bool EnableEid { get; set; } = true;
public int LanternCount { get; set; } = 8;
public int LanternCountMobile { get; set; } = 3;
}
public class EurovisionOptions {
public bool EnableEurovision { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableColorfulNotes { get; set; } = true;
public string EurovisionColors { get; set; } = "#ff0026ff,#17a6ffff,#32d432ff,#FFD700,#f0821bff,#f826f8ff";
public int EurovisionGlowSize { get; set; } = 2;
}
public class FilmNoirOptions {
public bool EnableFilmNoir { get; set; } = true;
}
public class FireworksOptions {
public bool EnableFireworks { get; set; } = true;
public bool ScrollFireworks { get; set; } = true;
public int ParticleCount { get; set; } = 50;
public int MinFireworks { get; set; } = 3;
public int MaxFireworks { get; set; } = 6;
public int LaunchInterval { get; set; } = 3200;
}
public class HalloweenOptions
{
public int SymbolCount { get; set; } = 25;
public class Friday13Options {
public bool EnableFriday13 { get; set; } = true;
}
public class FrostOptions {
public bool EnableFrost { get; set; } = true;
}
public class HalloweenOptions {
public bool EnableHalloween { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public int SymbolCount { get; set; } = 25;
public int SymbolCountMobile { get; set; } = 10;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableSpiders { get; set; } = true;
public bool EnableMice { get; set; } = true;
}
public class HeartsOptions
{
public int SymbolCount { get; set; } = 25;
public class HeartsOptions {
public bool EnableHearts { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class ChristmasOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableChristmas { get; set; } = true;
public bool EnableRandomChristmas { get; set; } = true;
public bool EnableRandomChristmasMobile { get; set; } = false;
public int SymbolCountMobile { get; set; } = 10;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SantaOptions
{
public class MarioDayOptions {
public bool EnableMarioDay { get; set; } = true;
public bool LetMarioJump { get; set; } = true;
}
public class MatrixOptions {
public bool EnableMatrix { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public bool EnableMatrixBackground { get; set; } = false;
public string MatrixChars { get; set; } = "0123456789";
}
public class OktoberfestOptions {
public bool EnableOktoberfest { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public int SymbolCountMobile { get; set; } = 10;
public bool EnableDifferentDuration { get; set; } = true;
}
public class OlympiaOptions {
public bool EnableOlympia { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public int SymbolCountMobile { get; set; } = 10;
public bool EnableDifferentDuration { get; set; } = true;
}
public class OscarOptions {
public bool EnableOscar { get; set; } = true;
}
public class PrideOptions {
public bool EnablePride { get; set; } = true;
public int HeartCount { get; set; } = 20;
public double HeartSize { get; set; } = 1.5;
public bool ColorHeader { get; set; } = true;
}
public class RainOptions {
public bool EnableRain { get; set; } = true;
public int RaindropCount { get; set; } = 300;
public int RaindropCountMobile { get; set; } = 150;
public double RainSpeed { get; set; } = 1.0;
}
public class ResurrectionOptions {
public bool EnableResurrection { get; set; } = true;
public int SymbolCount { get; set; } = 12;
public int SymbolCountMobile { get; set; } = 5;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SantaOptions {
public bool EnableSanta { get; set; } = true;
public int SnowflakesCount { get; set; } = 500;
public int SnowflakesCountMobile { get; set; } = 250;
public double SnowFallSpeed { get; set; } = 3;
public double SantaSpeed { get; set; } = 10;
public double SantaSpeedMobile { get; set; } = 8;
public bool EnableSanta { get; set; } = true;
public double SnowFallSpeed { get; set; } = 3;
public double MaxSantaRestTime { get; set; } = 8;
public double MinSantaRestTime { get; set; } = 3;
public double MaxPresentFallSpeed { get; set; } = 5;
public double MinPresentFallSpeed { get; set; } = 2;
}
public class EasterOptions
{
public int EggCount { get; set; } = 20;
public bool EnableEaster { get; set; } = true;
public bool EnableRandomEaster { get; set; } = true;
public bool EnableRandomEasterMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableBunny { get; set; } = true;
public int BunnyDuration { get; set; } = 12000;
public int HopHeight { get; set; } = 12;
public int MinBunnyRestTime { get; set; } = 2000;
public int MaxBunnyRestTime { get; set; } = 5000;
public class SnowfallOptions {
public bool EnableSnowfall { get; set; } = true;
public int SnowflakesCount { get; set; } = 500;
public int SnowflakesCountMobile { get; set; } = 250;
public double Speed { get; set; } = 3;
}
public class SnowflakesOptions {
public bool EnableSnowflakes { get; set; } = true;
public int SnowflakeCount { get; set; } = 25;
public int SnowflakeCountMobile { get; set; } = 10;
public bool EnableColoredSnowflakes { get; set; } = true;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SnowstormOptions {
public bool EnableSnowstorm { get; set; } = true;
public int SnowflakesCount { get; set; } = 500;
public int SnowflakesCountMobile { get; set; } = 250;
public double Speed { get; set; } = 6;
public double HorizontalWind { get; set; } = 4;
public double VerticalVariation { get; set; } = 2;
}
public class SpaceOptions {
public bool EnableSpace { get; set; } = true;
public int PlanetCount { get; set; } = 6;
public int AstronautCount { get; set; } = 1;
public int SatelliteCount { get; set; } = 4;
public int IssCount { get; set; } = 1;
public int RocketCount { get; set; } = 1;
public bool EnableDifferentDuration { get; set; } = true;
public int SymbolCountMobile { get; set; } = 2;
}
public class SpookyOptions {
public bool EnableSpooky { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableSpookySway { get; set; } = true;
public int SpookySize { get; set; } = 20;
public int SpookyGlowSize { get; set; } = 2;
}
public class SportsOptions {
public bool EnableSports { get; set; } = true;
public int SymbolCount { get; set; } = 5;
public bool EnableDifferentDuration { get; set; } = true;
public string TurfColor { get; set; } = "#228b22";
public string SportsBalls { get; set; } = "football,basketball,tennis,volleyball";
public bool EnableTrophy { get; set; } = false;
public string ConfettiColors { get; set; } = "#000000,#FF0000,#FFCC00";
}
public class SpringOptions {
public bool EnableSpring { get; set; } = true;
public int PollenCount { get; set; } = 30;
public bool EnableSpringSunbeams { get; set; } = true;
public int SunbeamCount { get; set; } = 5;
public int BirdCount { get; set; } = 3;
public int ButterflyCount { get; set; } = 4;
public int BeeCount { get; set; } = 2;
public int LadybugCount { get; set; } = 2;
public int SymbolCountMobile { get; set; } = 2;
}
public class StarWarsOptions {
public bool EnableStarWars { get; set; } = true;
}
public class StormOptions {
public bool EnableStorm { get; set; } = true;
public int RaindropCount { get; set; } = 300;
public int RaindropCountMobile { get; set; } = 150;
public bool EnableLightning { get; set; } = true;
public double RainSpeed { get; set; } = 1.0;
}
public class SummerOptions {
public bool EnableSummer { get; set; } = true;
public int BubbleCount { get; set; } = 30;
public int DustCount { get; set; } = 50;
public int SymbolCountMobile { get; set; } = 2;
public bool EnableDifferentDuration { get; set; } = true;
}
public class UnderwaterOptions {
public bool EnableUnderwater { get; set; } = true;
public int SymbolCountMobile { get; set; } = 2;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableLightRays { get; set; } = true;
public int SeaweedCount { get; set; } = 50;
public int CrabCount { get; set; } = 2;
public int StarfishCount { get; set; } = 2;
public int ShellCount { get; set; } = 2;
public int FishCount { get; set; } = 15;
public int SeahorseCount { get; set; } = 3;
public int JellyfishCount { get; set; } = 3;
public int TurtleCount { get; set; } = 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors>
<Version>1.6.2.0</Version>
<Version>2.0.0.4</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
</PropertyGroup>
@@ -26,7 +26,7 @@
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />
<None Remove="Web\**" />
<EmbeddedResource Include="Web\**" />
<EmbeddedResource Include="Web\**" Exclude="Web\test-site.html" />
<None Include="..\README.md" />
<None Include="..\logo.png" CopyToOutputDirectory="Always" />

View File

@@ -18,7 +18,7 @@ public class ScriptInjector
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
public const string Marker = "</body>";
/// <summary>
@@ -56,6 +56,18 @@ public class ScriptInjector
}
var content = File.ReadAllText(indexPath);
// MARK: Legacy Tags, remove in future versions
bool modified = false;
// Cleanup legacy tags first to avoid duplicates or conflicts
content = RemoveLegacyTags(content, ref modified);
if (modified)
{
_logger.LogInformation("Removed legacy tags from index.html.");
}
if (!content.Contains(ScriptTag))
{
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
@@ -113,6 +125,17 @@ public class ScriptInjector
} else {
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
}
// MARK: Legacy Tags, remove in future versions
// Remove legacy tags
bool modified = false;
content = RemoveLegacyTags(content, ref modified);
if (modified)
{
_logger.LogInformation("Removed legacy tags from index.html.");
}
}
catch (UnauthorizedAccessException)
{
@@ -204,4 +227,21 @@ public class ScriptInjector
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
}
}
// MARK: Legacy Tags, remove in future versions
/// <summary>
/// Removes legacy script tags from the content.
/// </summary>
private string RemoveLegacyTags(string content, ref bool modified)
{
// Legacy tags (used in versions prior to 1.6.3.0 where paths started with / instead of ../)
const string LegacyScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
if (content.Contains(LegacyScriptTag))
{
content = content.Replace(LegacyScriptTag + Environment.NewLine, "").Replace(LegacyScriptTag, "");
modified = true;
}
return content;
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -8,24 +8,21 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.leaf {
position: fixed;
z-index: 15;
top: -10%;
top: 0;
will-change: transform;
translate: 0 -10vh;
font-size: 1em;
color: #fff;
font-family: Arial, sans-serif;
text-shadow: 0 0 5px #000;
user-select: none;
-webkit-user-select: none;
cursor: default;
-webkit-animation-name: leaf-fall, leaf-shake;
-webkit-animation-duration: 7s, 4s;
-webkit-animation-timing-function: linear, ease-in-out;
-webkit-animation-iteration-count: infinite, infinite;
-webkit-user-select: none;
animation-name: leaf-fall, leaf-shake;
animation-duration: 7s, 4s;
animation-timing-function: linear, ease-in-out;
@@ -38,34 +35,17 @@
--rotate-end: 0deg !important;
}
@-webkit-keyframes leaf-fall {
0% {
top: -10%;
}
100% {
top: 100%;
}
}
@keyframes leaf-fall {
0% {
top: -10%;
translate: 0 -10vh;
}
100% {
top: 100%;
translate: 0 100vh;
}
}
@-webkit-keyframes leaf-shake {
0%, 100% {
-webkit-transform: translateX(0) rotate(var(--rotate-start, -20deg));
}
50% {
-webkit-transform: translateX(80px) rotate(var(--rotate-end, 20deg));
}
}
@keyframes leaf-shake {
0%, 100% {
@@ -75,87 +55,3 @@
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
}
}
.leaf:nth-of-type(0) {
left: 0%;
animation-delay: 0s, 0s;
--rotate-start: -25deg;
--rotate-end: 22deg;
}
.leaf:nth-of-type(1) {
left: 10%;
animation-delay: 1s, 0.5s;
--rotate-start: -32deg;
--rotate-end: 35deg;
}
.leaf:nth-of-type(2) {
left: 20%;
animation-delay: 6s, 1s;
--rotate-start: -28deg;
--rotate-end: 28deg;
}
.leaf:nth-of-type(3) {
left: 30%;
animation-delay: 4s, 1.5s;
--rotate-start: -38deg;
--rotate-end: 32deg;
}
.leaf:nth-of-type(4) {
left: 40%;
animation-delay: 2s, 0.8s;
--rotate-start: -22deg;
--rotate-end: 38deg;
}
.leaf:nth-of-type(5) {
left: 50%;
animation-delay: 8s, 2s;
--rotate-start: -35deg;
--rotate-end: 25deg;
}
.leaf:nth-of-type(6) {
left: 60%;
animation-delay: 6s, 1.2s;
--rotate-start: -40deg;
--rotate-end: 40deg;
}
.leaf:nth-of-type(7) {
left: 70%;
animation-delay: 2.5s, 0.3s;
--rotate-start: -30deg;
--rotate-end: 30deg;
}
.leaf:nth-of-type(8) {
left: 80%;
animation-delay: 1s, 1.8s;
--rotate-start: -26deg;
--rotate-end: 36deg;
}
.leaf:nth-of-type(9) {
left: 90%;
animation-delay: 3s, 0.7s;
--rotate-start: -34deg;
--rotate-end: 24deg;
}
.leaf:nth-of-type(10) {
left: 25%;
animation-delay: 2s, 2.3s;
--rotate-start: -29deg;
--rotate-end: 33deg;
}
.leaf:nth-of-type(11) {
left: 65%;
animation-delay: 4s, 1.4s;
--rotate-start: -37deg;
--rotate-end: 27deg;
}

View File

@@ -1,12 +1,30 @@
const config = window.SeasonalsPluginConfig?.Autumn || {};
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable leaves
const randomLeaves = config.EnableRandomLeaves !== undefined ? config.EnableRandomLeaves : true; // enable random leaves
const randomLeavesMobile = config.EnableRandomLeavesMobile !== undefined ? config.EnableRandomLeavesMobile : false; // enable random leaves on mobile devices (Warning: High values may affect performance)
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random leaves
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
const leafCount = config.LeafCount || 25; // count of random extra leaves
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable autumn
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable rotation
const leafCount = config.LeafCount !== undefined ? config.LeafCount : 35; // count of random extra leaves
const leafCountMobile = config.LeafCountMobile !== undefined ? config.LeafCountMobile : 10; // count of random extra leaves on mobile
const images = [
"../Seasonals/Resources/autumn_images/acorn1.png",
"../Seasonals/Resources/autumn_images/acorn2.png",
"../Seasonals/Resources/autumn_images/leaf1.png",
"../Seasonals/Resources/autumn_images/leaf2.png",
"../Seasonals/Resources/autumn_images/leaf3.png",
"../Seasonals/Resources/autumn_images/leaf4.png",
"../Seasonals/Resources/autumn_images/leaf5.png",
"../Seasonals/Resources/autumn_images/leaf6.png",
"../Seasonals/Resources/autumn_images/leaf7.png",
"../Seasonals/Resources/autumn_images/leaf8.png",
"../Seasonals/Resources/autumn_images/leaf9.png",
"../Seasonals/Resources/autumn_images/leaf10.png",
"../Seasonals/Resources/autumn_images/leaf11.png",
"../Seasonals/Resources/autumn_images/leaf12.png",
"../Seasonals/Resources/autumn_images/leaf13.png",
"../Seasonals/Resources/autumn_images/leaf14.png",
"../Seasonals/Resources/autumn_images/leaf15.png",
];
let msgPrinted = false; // flag to prevent multiple console messages
@@ -38,40 +56,23 @@ function toggleAutumn() {
// observe changes in the DOM
const observer = new MutationObserver(toggleAutumn);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
const images = [
"/Seasonals/Resources/autumn_images/acorn1.png",
"/Seasonals/Resources/autumn_images/acorn2.png",
"/Seasonals/Resources/autumn_images/leaf1.png",
"/Seasonals/Resources/autumn_images/leaf2.png",
"/Seasonals/Resources/autumn_images/leaf3.png",
"/Seasonals/Resources/autumn_images/leaf4.png",
"/Seasonals/Resources/autumn_images/leaf5.png",
"/Seasonals/Resources/autumn_images/leaf6.png",
"/Seasonals/Resources/autumn_images/leaf7.png",
"/Seasonals/Resources/autumn_images/leaf8.png",
"/Seasonals/Resources/autumn_images/leaf9.png",
"/Seasonals/Resources/autumn_images/leaf10.png",
"/Seasonals/Resources/autumn_images/leaf11.png",
"/Seasonals/Resources/autumn_images/leaf12.png",
"/Seasonals/Resources/autumn_images/leaf13.png",
"/Seasonals/Resources/autumn_images/leaf14.png",
"/Seasonals/Resources/autumn_images/leaf15.png",
];
function initLeaves(count) {
let autumnContainer = document.querySelector('.autumn-container'); // get the leave container
if (!autumnContainer) {
autumnContainer = document.createElement("div");
autumnContainer.className = "autumn-container";
autumnContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(autumnContainer);
}
function addRandomLeaves(count) {
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
if (!autumnContainer) return; // exit if leave container is not found
console.log('Adding random leaves');
console.log('Adding leaves');
// Array of leave characters
for (let i = 0; i < count; i++) {
@@ -90,7 +91,9 @@ function addRandomLeaves(count) {
// set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s)
const randomAnimationDelay2 = Math.random() * 4; // delay for shake+rotate (0s to 4s)
// Display directly symbols on full screen (below) or let it build up (above)
// const randomAnimationDelay = -(Math.random() * 16); // delay for fall (-16s to 0s)
const randomAnimationDelay2 = -(Math.random() * 4); // delay for shake+rotate (-4s to 0s)
// apply styles
leaveDiv.style.left = `${randomLeft}%`;
@@ -118,60 +121,18 @@ function addRandomLeaves(count) {
// add the leave to the container
autumnContainer.appendChild(leaveDiv);
}
console.log('Random leaves added');
console.log('Leaves added');
}
// initialize standard leaves
function initLeaves() {
const container = document.querySelector('.autumn-container') || document.createElement("div");
if (!document.querySelector('.autumn-container')) {
container.className = "autumn-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
for (let i = 0; i < 12; i++) {
const leafDiv = document.createElement("div");
leafDiv.className = enableRotation ? "leaf" : "leaf no-rotation";
const img = document.createElement("img");
img.src = images[Math.floor(Math.random() * images.length)];
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 16s)
const randomAnimationDuration2 = Math.random() * 3 + 2; // shake+rotate duration (2s to 5s)
leafDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
// set random rotation angles for standard leaves too (only if rotation is enabled)
if (enableRotation) {
const randomRotateStart = -(Math.random() * 40 + 20); // -20deg to -60deg
const randomRotateEnd = Math.random() * 40 + 20; // 20deg to 60deg
leafDiv.style.setProperty('--rotate-start', `${randomRotateStart}deg`);
leafDiv.style.setProperty('--rotate-end', `${randomRotateEnd}deg`);
} else {
// No rotation - set to 0 degrees
leafDiv.style.setProperty('--rotate-start', '0deg');
leafDiv.style.setProperty('--rotate-end', '0deg');
}
leafDiv.appendChild(img);
container.appendChild(leafDiv);
}
}
// initialize leaves and add random leaves
// initialize leaves
function initializeLeaves() {
if (!leaves) return; // exit if leaves are disabled
initLeaves();
toggleAutumn();
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (randomLeaves && (screenWidth > 768 || randomLeavesMobile)) { // add random leaves only on larger screens, unless enabled for mobile devices
addRandomLeaves(leafCount);
}
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? leafCount : leafCountMobile;
initLeaves(count);
toggleAutumn();
}
initializeLeaves();

View File

@@ -0,0 +1,155 @@
.birthday-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 10;
overflow: hidden;
contain: strict;
contain: layout paint;
}
.birthday-symbol {
will-change: opacity;
position: fixed;
top: 0;
left: 0;
animation: birthday-rise linear infinite forwards;
opacity: 0.95;
z-index: 40;
pointer-events: none;
}
.birthday-sway {
will-change: transform;
animation-name: birthday-sway;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.birthday-inner {
pointer-events: auto;
cursor: crosshair;
display: inline-block;
}
/* MARK: Balloon Size */
.birthday-symbol img {
width: 18vh;
height: auto;
max-width: 100px;
object-fit: contain;
}
.birthday-confetti-wrapper {
position: fixed;
top: 0;
left: 0;
z-index: 30;
will-change: transform;
animation-name: birthday-confetti-fall;
animation-timing-function: linear;
animation-iteration-count: infinite;
pointer-events: none;
}
.birthday-confetti {
width: 8px;
height: 16px;
background-color: rgb(0, 0, 0);
will-change: transform;
animation-name: birthday-flutter;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.birthday-confetti.circle {
width: 8px;
height: 8px;
border-radius: 50%;
}
.birthday-confetti.square {
width: 8px;
height: 8px;
}
.birthday-confetti.triangle {
width: 10px;
height: 10px;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
@keyframes birthday-rise {
0% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0) rotate(var(--start-rot, 0deg)); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translate3d(var(--x-pos, 0vw), -20vh, 0) rotate(calc(var(--start-rot, 0deg) * -1)); opacity: 0; }
}
@keyframes birthday-confetti-fall {
0% { transform: translate3d(var(--x-pos, 0vw), -10vh, 0); }
100% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0); }
}
@keyframes birthday-sway {
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
100% { transform: translateX(var(--sway-amount, 50px)); }
}
@keyframes birthday-flutter {
0% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg); }
100% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg)); }
}
@keyframes birthday-pop {
0% { transform: scale(1); opacity: 1; filter: brightness(1); }
30% { transform: scale(1.3); opacity: 1; filter: brightness(1.5); }
100% { transform: scale(0); opacity: 0; filter: brightness(2); }
}
.birthday-burst-wrapper {
position: absolute;
pointer-events: none;
z-index: 1000;
will-change: transform, opacity;
animation: birthday-burst-y 1.2s cubic-bezier(0.42, 0, 1, 1) forwards;
}
.birthday-burst-confetti {
will-change: transform;
animation: birthday-burst-x 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
}
.birthday-burst-confetti.circle {
border-radius: 50%;
}
.birthday-burst-confetti.triangle {
width: 10px;
height: 10px;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
@keyframes birthday-burst-y {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(calc(var(--burst-y) + 150px));
opacity: 0;
}
}
@keyframes birthday-burst-x {
0% {
transform: translateX(0) rotate3d(var(--rx), var(--ry), var(--rz), 0deg);
}
100% {
transform: translateX(calc(var(--burst-x) * 1.5)) rotate3d(var(--rx), var(--ry), var(--rz), var(--rot-dir));
}
}

View File

@@ -0,0 +1,326 @@
const config = window.SeasonalsPluginConfig?.Birthday || {};
const birthday = config.EnableBirthday !== undefined ? config.EnableBirthday : true; // enable/disable birthday symbols
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 15; // count of balloons
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the symbols
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 5; // count of mobile balloons
const baseConfettiCount = config.ConfettiCount !== undefined ? config.ConfettiCount : 60; // count of confetti
/**
* Base ballon image: https://www.flaticon.com/de/kostenloses-icon/ballon_1512470
* modified by CodeDevMLH
*/
const birthdayImages = [
'../Seasonals/Resources/birthday_assets/balloon_blue.gif',
'../Seasonals/Resources/birthday_assets/balloon_green.gif',
'../Seasonals/Resources/birthday_assets/balloon_lightblue.gif',
'../Seasonals/Resources/birthday_assets/balloon_orange.gif',
'../Seasonals/Resources/birthday_assets/balloon_pink.gif',
'../Seasonals/Resources/birthday_assets/balloon_red.gif',
'../Seasonals/Resources/birthday_assets/balloon_yellow.gif',
'../Seasonals/Resources/birthday_assets/balloon_turquoise.gif',
'../Seasonals/Resources/birthday_assets/balloon_violet.gif'
];
const balloonColors = {
'balloon_blue': ['#3498db', '#2980b9', '#1f618d'],
'balloon_green': ['#2ecc71', '#27ae60', '#1e8449'],
'balloon_lightblue': ['#36c5f0', '#81ecec', '#00cec9'],
'balloon_orange': ['#e67e22', '#d35400', '#a04000'],
'balloon_pink': ['#ff726d', '#f4306d', '#e84393'],
'balloon_red': ['#e74c3c', '#c0392b', '#922b21'],
'balloon_yellow': ['#f1c40f', '#f39c12', '#b7950b'],
'balloon_turquoise': ['#36c5f0', '#81ecec', '#00cec9'],
'balloon_violet': ['#9b59b6', '#8e44ad', '#6c3483']
};
let msgPrinted = false;
function toggleBirthday() {
const container = document.querySelector('.birthday-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Birthday hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Birthday visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleBirthday);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createBalloonPopConfetti(container, x, y, colors) {
const popConfettiColors = colors || [
'#fce18a', '#ff726d', '#b48def', '#f4306d',
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
];
// Spawn 15-20 particles
const particleCount = Math.floor(Math.random() * 5) + 15;
for (let i = 0; i < particleCount; i++) {
const wrapper = document.createElement('div');
wrapper.className = 'birthday-burst-wrapper';
wrapper.style.position = 'absolute';
wrapper.style.left = `${x}px`;
wrapper.style.top = `${y}px`;
wrapper.style.zIndex = '1000';
const particle = document.createElement('div');
particle.classList.add('birthday-burst-confetti');
// Random color
const color = popConfettiColors[Math.floor(Math.random() * popConfettiColors.length)];
particle.style.backgroundColor = color;
// Random shape
const shape = Math.random();
if (shape > 0.66) {
particle.classList.add('circle');
const size = Math.random() * 4 + 4; // 4-8px
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
} else if (shape > 0.33) {
particle.classList.add('rect');
const width = Math.random() * 3 + 3; // 3-6px
const height = Math.random() * 4 + 6; // 6-10px
particle.style.width = `${width}px`;
particle.style.height = `${height}px`;
} else {
particle.classList.add('triangle');
}
// Random direction for explosion (circular)
const angle = Math.random() * 2 * Math.PI;
const distance = Math.random() * 60 + 20; // 20-80px burst radius
const xOffset = Math.cos(angle) * distance;
const yOffset = Math.sin(angle) * distance;
particle.style.setProperty('--burst-x', `${xOffset}px`);
wrapper.style.setProperty('--burst-y', `${yOffset}px`);
// Random rotation during fall
particle.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
particle.style.setProperty('--rx', Math.random().toFixed(2));
particle.style.setProperty('--ry', Math.random().toFixed(2));
particle.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
wrapper.appendChild(particle);
container.appendChild(wrapper);
// Remove particle after animation
setTimeout(() => wrapper.remove(), 1000);
}
}
function createBirthday() {
const container = document.querySelector('.birthday-container') || document.createElement('div');
if (!document.querySelector('.birthday-container')) {
container.className = 'birthday-container';
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// Cake and Garland have been removed
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
let finalCount = isMobile ? symbolCountMobile : symbolCount;
const useRandomDuration = enableDifferentDuration !== false;
// Arrays moved to top of file
for (let i = 0; i < finalCount; i++) {
let symbol = document.createElement('div');
const randomImage = birthdayImages[Math.floor(Math.random() * birthdayImages.length)];
const randomItem = randomImage.split('/').pop().split('.')[0]; // Extracts "balloon_blue"
symbol.className = `birthday-symbol birthday-${randomItem}`;
// Create inner div for sway
let innerDiv = document.createElement('div');
innerDiv.className = 'birthday-inner';
let img = document.createElement('img');
img.src = randomImage;
img.onerror = function() {
symbol.remove(); // Remove element completely on error
};
innerDiv.appendChild(img);
// Sway wrapper
let swayWrapper = document.createElement('div');
swayWrapper.className = 'birthday-sway';
const swayDuration = Math.random() * 3 + 3; // 3-6s per cycle
swayWrapper.style.animationDuration = `${swayDuration}s`;
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
const swayAmount = Math.random() * 60 + 20; // 20-80px
const direction = Math.random() > 0.5 ? 1 : -1;
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
swayWrapper.appendChild(innerDiv);
symbol.appendChild(swayWrapper);
const leftPos = Math.random() * 95;
// Far away effect
const depth = Math.random();
// MARK: balloon size
const scale = 0.85 + depth * 0.3; // 0.85 to 1.15
const zIndex = Math.floor(depth * 30) + 10;
img.style.transform = `scale(${scale})`;
symbol.style.zIndex = zIndex;
let durationSeconds = 9;
if (useRandomDuration) {
// Far strings climb slower
durationSeconds = (1 - depth) * 6 + 7 + Math.random() * 4;
}
// Negative delay correctly scatters them initially across the screen vertically
// avoiding them all popping up at bottom edge together
const delaySeconds = -(Math.random() * durationSeconds);
const isBalloon = randomItem.startsWith('balloon');
if (isBalloon) {
// Sway animation is now handled natively by the GIF motion.
// Interaction to pop is handled visually by the GIF, but we can still remove it on hover
innerDiv.addEventListener('mouseenter', function(e) {
if (!this.classList.contains('popped')) {
this.classList.add('popped');
this.style.animation = 'birthday-pop 0.2s ease-out forwards';
this.style.pointerEvents = 'none'; // avoid re-triggering
// Create confetti burst at balloon's screen position
const rect = this.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
// explosion height
const cy = rect.top + rect.height * -0.05;
// Ensure the burst container is appended to the main document body or the birthday container
createBalloonPopConfetti(document.body, cx, cy, balloonColors[randomItem]);
}
});
// Reset the balloon when it reappears at the bottom of the screen
symbol.addEventListener('animationiteration', function(e) {
// Ignore bubbling events from the inner sway animation
if (e.animationName === 'birthday-rise' || e.target === symbol) {
if (innerDiv.classList.contains('popped')) {
innerDiv.classList.remove('popped');
innerDiv.style.animation = '';
innerDiv.style.pointerEvents = 'auto';
}
}
});
}
const startRot = (Math.random() * 20) - 10; // -10 to +10 spread
symbol.style.setProperty('--start-rot', `${startRot}deg`);
symbol.style.setProperty('--x-pos', `${leftPos}vw`);
symbol.style.animationDuration = `${durationSeconds}s`;
symbol.style.animationDelay = `${delaySeconds}s`;
container.appendChild(symbol);
}
// Party Confetti
const confettiCount = baseConfettiCount;
const allColors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
for (let i = 0; i < confettiCount; i++) {
const wrapper = document.createElement('div');
wrapper.classList.add('birthday-confetti-wrapper');
// Use carnival.js 3D advanced fluttering logic
let swayWrapper = document.createElement('div');
swayWrapper.classList.add('birthday-sway');
wrapper.appendChild(swayWrapper);
const confetti = document.createElement('div');
confetti.classList.add('birthday-confetti');
const color = allColors[Math.floor(Math.random() * allColors.length)];
confetti.style.backgroundColor = color;
// Shape assignments
const shape = Math.random();
if (shape > 0.8) confetti.classList.add('circle');
else if (shape > 0.6) confetti.classList.add('square');
else if (shape > 0.4) confetti.classList.add('triangle');
else confetti.classList.add('rect'); // default
// Sizing
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
const width = Math.random() * 3 + 4; // 4-7px
const height = Math.random() * 5 + 8; // 8-13px
confetti.style.width = `${width}px`;
confetti.style.height = `${height}px`;
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
const size = Math.random() * 5 + 5; // 5-10px
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
}
const duration = Math.random() * 5 + 5;
const delay = -Math.random() * duration; // Spawn fully integrated across screen width/height
wrapper.style.setProperty('--x-pos', `${Math.random() * 100}vw`);
wrapper.style.animationDelay = `${delay}s`;
wrapper.style.animationDuration = `${duration}s`;
// Sway handling
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
swayWrapper.style.animationDuration = `${swayDuration}s`;
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
const swayAmount = Math.random() * 70 + 30; // 30-100px
const direction = Math.random() > 0.5 ? 1 : -1;
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
// 3D Flutter Rotation
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
confetti.style.setProperty('--rx', Math.random().toFixed(2));
confetti.style.setProperty('--ry', Math.random().toFixed(2));
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
const rotDir = Math.random() > 0.5 ? 1 : -1;
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
swayWrapper.appendChild(confetti);
container.appendChild(wrapper);
}
}
/* Removed fallback logic */
function initializeBirthday() {
if (!birthday) return;
createBirthday();
toggleBirthday();
}
initializeBirthday();

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,86 @@
.carnival-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
perspective: 600px;
contain: layout paint;
}
.carnival-wrapper {
position: fixed;
z-index: 15;
top: 0;
will-change: transform;
animation-name: carnival-fall;
animation-timing-function: linear;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
.carnival-sway-wrapper {
will-change: transform;
animation-name: carnival-sway;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.carnival-confetti {
width: 8px;
height: 16px;
background-color: rgb(0, 0, 0);
will-change: transform;
animation-name: carnival-flutter;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.carnival-confetti.circle {
width: 8px;
height: 8px;
border-radius: 50%;
}
.carnival-confetti.square {
width: 8px;
height: 8px;
}
.carnival-confetti.triangle {
width: 10px;
height: 10px;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
@keyframes carnival-fall {
0% {
transform: translate3d(0, -10vh, 0);
}
100% {
transform: translate3d(0, 110vh, 0);
}
}
@keyframes carnival-sway {
0% {
transform: translateX(calc(var(--sway-amount, 50px) * -1));
}
100% {
transform: translateX(var(--sway-amount, 50px));
}
}
@keyframes carnival-flutter {
0% {
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
}
100% {
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
}
}

View File

@@ -0,0 +1,177 @@
const config = window.SeasonalsPluginConfig?.Carnival || {};
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // enable/disable carnival
const carnivalCount = config.ObjectCount !== undefined ? config.ObjectCount : 120; // Number of confetti pieces to spawn
const carnivalCountMobile = config.ObjectCountMobile !== undefined ? config.ObjectCountMobile : 60; // Number of confetti pieces to spawn on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // enable/disable carnivalsway
const confettiColors = [
'#fce18a', '#ff726d', '#b48def', '#f4306d',
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
];
let msgPrinted = false;
// function to check and control the carnival animation
function toggleCarnival() {
const carnivalContainer = document.querySelector('.carnival-container');
if (!carnivalContainer) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
// hide carnival if video/trailer player is active or dashboard is visible
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
carnivalContainer.style.display = 'none'; // hide carnival
if (!msgPrinted) {
console.log('Carnival hidden');
msgPrinted = true;
}
} else {
carnivalContainer.style.display = 'block'; // show carnival
if (msgPrinted) {
console.log('Carnival visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleCarnival);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createConfettiPiece(container, isInitial = false) {
const wrapper = document.createElement('div');
wrapper.classList.add('carnival-wrapper');
let swayWrapper = wrapper;
if (enableSway) {
swayWrapper = document.createElement('div');
swayWrapper.classList.add('carnival-sway-wrapper');
wrapper.appendChild(swayWrapper);
}
const confetti = document.createElement('div');
confetti.classList.add('carnival-confetti');
// Random color
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
confetti.style.backgroundColor = color;
// Random shape
const shape = Math.random();
if (shape > 0.8) {
confetti.classList.add('circle');
} else if (shape > 0.6) {
confetti.classList.add('square');
} else if (shape > 0.4) {
confetti.classList.add('triangle');
} else {
confetti.classList.add('rect');
}
// Random position
wrapper.style.left = `${Math.random() * 100}%`;
// MARK: CONFETTI SIZE (RECTANGLES)
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
const width = Math.random() * 3 + 4; // 4-7px
const height = Math.random() * 5 + 8; // 8-13px
confetti.style.width = `${width}px`;
confetti.style.height = `${height}px`;
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
// MARK: CONFETTI SIZE (CIRCLES/SQUARES)
const size = Math.random() * 5 + 5; // 5-10px
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
}
// MARK: CONFETTI FALLING SPEED (in seconds)
const duration = Math.random() * 5 + 5;
let delay = 0;
if (isInitial) {
delay = -Math.random() * duration;
} else {
delay = Math.random() * 10;
}
wrapper.style.animationDelay = `${delay}s`;
wrapper.style.animationDuration = `${duration}s`;
if (enableSway) {
// Random sway duration
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
swayWrapper.style.animationDuration = `${swayDuration}s`;
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
// MARK: SWAY DISTANCE RANGE (in px)
const swayAmount = Math.random() * 70 + 30; // 30-100px
const direction = Math.random() > 0.5 ? 1 : -1;
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
}
// MARK: CONFETTI FLUTTER ROTATION SPEED
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
confetti.style.setProperty('--rx', Math.random().toFixed(2));
confetti.style.setProperty('--ry', Math.random().toFixed(2));
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
// Random direction for 3D rotation
const rotDir = Math.random() > 0.5 ? 1 : -1;
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
if (enableSway) {
swayWrapper.appendChild(confetti);
wrapper.appendChild(swayWrapper);
} else {
wrapper.appendChild(confetti);
}
// Respawn confetti when it hits the bottom
wrapper.addEventListener('animationend', (e) => {
if (e.animationName === 'carnival-fall') {
wrapper.remove();
createConfettiPiece(container, false); // respawn without initial huge delay
}
});
container.appendChild(wrapper);
}
// initialize standard carnival objects
function initCarnivalObjects(count) {
let container = document.querySelector('.carnival-container');
if (!container) {
container = document.createElement("div");
container.className = "carnival-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// Initial confetti
for (let i = 0; i < count; i++) {
createConfettiPiece(container, true);
}
}
// initialize carnival
function initializeCarnival() {
if (!carnival) return;
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? carnivalCount : carnivalCountMobile;
initCarnivalObjects(count);
toggleCarnival();
}
initializeCarnival();

View File

@@ -0,0 +1,60 @@
.cherryblossom-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
contain: layout paint;
}
/* Petals */
.cherryblossom-petal {
position: fixed;
top: 0;
z-index: 1005;
width: 15px;
height: 10px;
background-color: #ffc0cb;
border-radius: 15px 0px 15px 0px;
will-change: transform;
translate: 0 -10vh;
animation-name: cherryblossom-fall, cherryblossom-sway;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-duration: 10s, 3s;
}
.cherryblossom-petal.lighter {
background-color: #ffd1dc;
opacity: 0.8;
}
.cherryblossom-petal.darker {
background-color: #ffb7c5;
opacity: 0.9;
}
.cherryblossom-petal.type2 {
width: 12px;
height: 12px;
border-radius: 10px 0px 10px 5px;
}
@keyframes cherryblossom-fall {
0% { translate: 0 -10vh; }
100% { translate: 0 110vh; }
}
@keyframes cherryblossom-sway {
0%, 100% {
transform: translateX(0) rotate(0deg);
}
50% {
transform: translateX(30px) rotate(45deg);
}
}

View File

@@ -0,0 +1,94 @@
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true; // enable/disable cherryblossom
const petalCount = config.PetalCount !== undefined ? config.PetalCount : 25; // count of petal
const petalCountMobile = config.PetalCountMobile !== undefined ? config.PetalCountMobile : 10; // count of petal on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
let msgPrinted = false;
function toggleCherryBlossom() {
const container = document.querySelector('.cherryblossom-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('CherryBlossom hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('CherryBlossom visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleCherryBlossom);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createPetal(container) {
const petal = document.createElement('div');
petal.classList.add('cherryblossom-petal');
const type = Math.random() > 0.5 ? 'type1' : 'type2';
petal.classList.add(type);
const color = Math.random() > 0.7 ? 'darker' : 'lighter';
petal.classList.add(color);
const randomLeft = Math.random() * 100;
petal.style.left = `${randomLeft}%`;
const size = Math.random() * 0.5 + 0.5;
petal.style.transform = `scale(${size})`;
const duration = Math.random() * 5 + 8;
const delay = Math.random() * 10;
const swayDuration = Math.random() * 2 + 2;
if (enableDifferentDuration) {
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
}
petal.style.animationDelay = `${delay}s, ${Math.random() * 3}s`;
container.appendChild(petal);
}
function initObjects(count) {
let container = document.querySelector('.cherryblossom-container');
if (!container) {
container = document.createElement("div");
container.className = "cherryblossom-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// Initial batch
for (let i = 0; i < count; i++) {
createPetal(container);
}
}
function initializeCherryBlossom() {
if (!cherryBlossom) return;
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? petalCount : petalCountMobile;
initObjects(count);
toggleCherryBlossom();
}
initializeCherryBlossom();

View File

@@ -8,65 +8,40 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.christmas {
position: fixed;
z-index: 15;
top: -10%;
top: 0;
will-change: transform;
translate: 0 -10vh;
font-size: 1em;
color: #fff;
font-family: Arial, sans-serif;
text-shadow: 0 0 5px #000;
user-select: none;
cursor: default;
-webkit-user-select: none;
-webkit-animation-name: christmas-fall, christmas-shake;
-webkit-animation-duration: 10s, 3s;
-webkit-animation-timing-function: linear, ease-in-out;
-webkit-animation-iteration-count: infinite, infinite;
animation-name: christmas-fall, christmas-shake;
animation-duration: 10s, 3s;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
}
@-webkit-keyframes christmas-fall {
0% {
top: -10%;
}
100% {
top: 100%;
}
}
@-webkit-keyframes christmas-shake {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
50% {
-webkit-transform: translateX(80px);
transform: translateX(80px);
}
}
@keyframes christmas-fall {
0% {
top: -10%;
translate: 0 -10vh;
}
100% {
top: 100%;
translate: 0 110vh;
}
}
@keyframes christmas-shake {
0%,
100% {
transform: translateX(0);
@@ -76,63 +51,3 @@
transform: translateX(80px);
}
}
.christmas:nth-of-type(0) {
left: 0%;
animation-delay: 0s, 0s;
}
.christmas:nth-of-type(1) {
left: 10%;
animation-delay: 1s, 1s;
}
.christmas:nth-of-type(2) {
left: 20%;
animation-delay: 6s, 0.5s;
}
.christmas:nth-of-type(3) {
left: 30%;
animation-delay: 4s, 2s;
}
.christmas:nth-of-type(4) {
left: 40%;
animation-delay: 2s, 2s;
}
.christmas:nth-of-type(5) {
left: 50%;
animation-delay: 8s, 3s;
}
.christmas:nth-of-type(6) {
left: 60%;
animation-delay: 6s, 2s;
}
.christmas:nth-of-type(7) {
left: 70%;
animation-delay: 2.5s, 1s;
}
.christmas:nth-of-type(8) {
left: 80%;
animation-delay: 1s, 0s;
}
.christmas:nth-of-type(9) {
left: 90%;
animation-delay: 3s, 1.5s;
}
.christmas:nth-of-type(10) {
left: 25%;
animation-delay: 2s, 0s;
}
.christmas:nth-of-type(11) {
left: 65%;
animation-delay: 4s, 2.5s;
}

View File

@@ -1,11 +1,12 @@
const config = window.SeasonalsPluginConfig?.Christmas || {};
const christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
const randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random Christmas
const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ? config.EnableRandomChristmasMobile : false; // enable random Christmas on mobile devices (Warning: High values may affect performance)
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols
const christmasCount = config.SymbolCount || 25; // count of random extra christmas
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const christmasCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
const christmasCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
// Array of christmas characters
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
let msgPrinted = false; // flag to prevent multiple console messages
@@ -37,22 +38,22 @@ function toggleChristmas() {
// observe changes in the DOM
const observer = new MutationObserver(toggleChristmas);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
// Array of christmas characters
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
function initChristmas(count) {
let christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
if (!christmasContainer) {
christmasContainer = document.createElement("div");
christmasContainer.className = "christmas-container";
christmasContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(christmasContainer);
}
function addRandomChristmas(count) {
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
if (!christmasContainer) return; // exit if christmas container is not found
console.log('Adding random christmas');
console.log('Adding christmas');
for (let i = 0; i < count; i++) {
// create a new christmas element
@@ -64,8 +65,8 @@ function addRandomChristmas(count) {
// set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 12 + 8; // delay (8s to 12s)
const randomAnimationDelay2 = Math.random() * 5 + 3; // delay (0s to 5s)
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
// apply styles
christmasDiv.style.left = `${randomLeft}%`;
@@ -81,46 +82,18 @@ function addRandomChristmas(count) {
// add the christmas to the container
christmasContainer.appendChild(christmasDiv);
}
console.log('Random christmas added');
console.log('Christmas added');
}
// initialize standard christmas
function initChristmas() {
const christmasContainer = document.querySelector('.christmas-container') || document.createElement("div");
if (!document.querySelector('.christmas-container')) {
christmasContainer.className = "christmas-container";
christmasContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(christmasContainer);
}
// create the 12 standard christmas
for (let i = 0; i < 12; i++) {
const christmasDiv = document.createElement('div');
christmasDiv.className = 'christmas';
christmasDiv.textContent = christmasSymbols[Math.floor(Math.random() * christmasSymbols.length)];
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
christmasDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
christmasContainer.appendChild(christmasDiv);
}
}
// initialize christmas and add random christmas symbols
// initialize christmas
function initializeChristmas() {
if (!christmas) return; // exit if christmas is disabled
initChristmas();
toggleChristmas();
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (randomChristmas && (screenWidth > 768 || randomChristmasMobile)) { // add random christmas only on larger screens, unless enabled for mobile devices
addRandomChristmas(christmasCount);
}
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? christmasCount : christmasCountMobile;
initChristmas(count);
toggleChristmas();
}
initializeChristmas();

View File

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

View File

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

View File

@@ -1,160 +1,65 @@
.easter-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
z-index: 10000;
contain: strict;
overflow: hidden;
contain: layout paint;
}
.easter-grass-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 8vh;
pointer-events: none;
z-index: 1000;
}
.easter-meadow-layer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
}
.easter-meadow {
width: 100%;
height: 100%;
display: block;
overflow: visible;
}
/* sway */
.easter-sway {
will-change: transform;
transform-origin: bottom center;
animation: easter-wind-sway 6s ease-in-out infinite alternate;
}
@keyframes easter-wind-sway {
0% { transform: skewX(-3deg); }
100% { transform: skewX(5deg); }
}
.hopping-rabbit {
position: fixed;
z-index: 15;
bottom: 10px;
width: 70px;
overflow: hidden;
pointer-events: none;
position: absolute;
bottom: -15px;
left: 0;
width: 160px;
height: auto;
will-change: transform;
}
@media (max-width: 768px) {
.hopping-rabbit {
width: 60px;
}
}
.easter {
position: fixed;
z-index: 15;
top: -10%;
font-size: 1em;
color: #fff;
font-family: Arial, sans-serif;
text-shadow: 0 0 5px #000;
user-select: none;
-webkit-user-select: none;
cursor: default;
-webkit-animation-name: easter-fall, easter-shake;
-webkit-animation-timing-function: linear, ease-in-out;
-webkit-animation-iteration-count: infinite, infinite;
animation-name: easter-fall, easter-shake;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
}
.easter img {
z-index: 15;
height: auto;
width: 20px;
}
@-webkit-keyframes easter-fall {
0% {
top: -10%;
}
100% {
top: 100%;
}
}
@-webkit-keyframes easter-shake {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
50% {
-webkit-transform: translateX(80px);
transform: translateX(80px);
}
}
@keyframes easter-fall {
0% {
top: -10%;
}
100% {
top: 100%;
}
}
@keyframes easter-shake {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(80px);
}
}
.easter:nth-of-type(0) {
left: 0%;
animation-delay: 0s, 0s;
}
.easter:nth-of-type(1) {
left: 10%;
animation-delay: 1s, 1s;
}
.easter:nth-of-type(2) {
left: 20%;
animation-delay: 6s, 0.5s;
}
.easter:nth-of-type(3) {
left: 30%;
animation-delay: 4s, 2s;
}
.easter:nth-of-type(4) {
left: 40%;
animation-delay: 2s, 2s;
}
.easter:nth-of-type(5) {
left: 50%;
animation-delay: 8s, 3s;
}
.easter:nth-of-type(6) {
left: 60%;
animation-delay: 6s, 2s;
}
.easter:nth-of-type(7) {
left: 70%;
animation-delay: 2.5s, 1s;
}
.easter:nth-of-type(8) {
left: 80%;
animation-delay: 1s, 0s;
}
.easter:nth-of-type(9) {
left: 90%;
animation-delay: 3s, 1.5s;
}
.easter:nth-of-type(10) {
left: 25%;
animation-delay: 2s, 0s;
}
.easter:nth-of-type(11) {
left: 65%;
animation-delay: 4s, 2.5s;
}

View File

@@ -1,22 +1,38 @@
const config = window.SeasonalsPluginConfig?.Easter || {};
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter
const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance)
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter
const easterEggCount = config.EggCount || 20; // count of random extra easter
const enableBunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable bunny
const minBunnyRestTime = config.MinBunnyRestTime !== undefined ? config.MinBunnyRestTime : 2000; // timing parameter
const maxBunnyRestTime = config.MaxBunnyRestTime !== undefined ? config.MaxBunnyRestTime : 5000; // timing parameter
const eggCount = config.EggCount !== undefined ? config.EggCount : 15; // count of egg
const bunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable hopping bunny
const bunnyDuration = config.BunnyDuration || 12000; // duration of the bunny animation in ms
const hopHeight = config.HopHeight || 12; // height of the bunny hops in px
const minBunnyRestTime = config.MinBunnyRestTime || 2000; // minimum time the bunny rests in ms
const maxBunnyRestTime = config.MaxBunnyRestTime || 5000; // maximum time the bunny rests in ms
/* MARK: Bunny movement config */
const jumpDistanceVw = 5; // Distance in vw the bunny covers per jump
const jumpDurationMs = 770; // Time in ms the bunny spends moving during a jump
const pauseDurationMs = 116.6666; // Time in ms the bunny pauses between jumps
const rabbit = "../Seasonals/Resources/easter_images/Osterhase.gif";
let msgPrinted = false; // flag to prevent multiple console messages
let animationFrameId;
// Credit: https://flaticon.com
const easterEggImages = [
"../Seasonals/Resources/easter_images/egg_1.png",
"../Seasonals/Resources/easter_images/egg_2.png",
"../Seasonals/Resources/easter_images/egg_3.png",
"../Seasonals/Resources/easter_images/egg_4.png",
"../Seasonals/Resources/easter_images/egg_5.png",
"../Seasonals/Resources/easter_images/egg_6.png",
"../Seasonals/Resources/easter_images/egg_7.png",
"../Seasonals/Resources/easter_images/egg_8.png",
"../Seasonals/Resources/easter_images/egg_9.png",
"../Seasonals/Resources/easter_images/egg_10.png",
"../Seasonals/Resources/easter_images/egg_11.png",
"../Seasonals/Resources/easter_images/egg_12.png",
"../Seasonals/Resources/easter_images/eggs.png"
];
// function to check and control the easter
let msgPrinted = false;
// Check visibility
function toggleEaster() {
const easterContainer = document.querySelector('.easter-container');
if (!easterContainer) return;
@@ -26,21 +42,20 @@ function toggleEaster() {
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
// hide easter if video/trailer player is active or dashboard is visible
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
easterContainer.style.display = 'none'; // hide easter
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
easterContainer.style.display = 'none';
if (rabbitTimeout) {
clearTimeout(rabbitTimeout);
isAnimating = false;
}
if (!msgPrinted) {
console.log('Easter hidden');
msgPrinted = true;
}
} else {
easterContainer.style.display = 'block'; // show easter
if (!animationFrameId) {
animateRabbit(); // start animation
easterContainer.style.display = 'block';
if (!isAnimating && enableBunny) {
animateRabbit(document.querySelector('#rabbit'));
}
if (msgPrinted) {
console.log('Easter visible');
@@ -49,145 +64,201 @@ function toggleEaster() {
}
}
// observe changes in the DOM
const observer = new MutationObserver(toggleEaster);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
const images = [
"/Seasonals/Resources/easter_images/egg_1.png",
"/Seasonals/Resources/easter_images/egg_2.png",
"/Seasonals/Resources/easter_images/egg_3.png",
"/Seasonals/Resources/easter_images/egg_4.png",
"/Seasonals/Resources/easter_images/egg_5.png",
"/Seasonals/Resources/easter_images/egg_6.png",
"/Seasonals/Resources/easter_images/egg_7.png",
"/Seasonals/Resources/easter_images/egg_8.png",
"/Seasonals/Resources/easter_images/egg_9.png",
"/Seasonals/Resources/easter_images/egg_10.png",
"/Seasonals/Resources/easter_images/egg_11.png",
"/Seasonals/Resources/easter_images/egg_12.png",
];
const rabbit = "/Seasonals/Resources/easter_images/easter-bunny.png";
function addRandomEaster(count) {
const easterContainer = document.querySelector('.easter-container'); // get the leave container
if (!easterContainer) return; // exit if leave container is not found
console.log('Adding random easter eggs');
// Array of leave characters
for (let i = 0; i < count; i++) {
// create a new leave element
const eggDiv = document.createElement('div');
eggDiv.className = "easter";
// pick a random easter symbol
const imageSrc = images[Math.floor(Math.random() * images.length)];
const img = document.createElement("img");
img.src = imageSrc;
eggDiv.appendChild(img);
// set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 12; // delay (0s to 12s)
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
// apply styles
eggDiv.style.left = `${randomLeft}%`;
eggDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
function createEasterGrassAndEggs(container) {
let grassContainer = container.querySelector('.easter-grass-container');
if (!grassContainer) {
grassContainer = document.createElement('div');
grassContainer.className = 'easter-grass-container';
container.appendChild(grassContainer);
}
grassContainer.innerHTML = '';
// add the leave to the container
easterContainer.appendChild(eggDiv);
let pathsBg = '';
let pathsFg = '';
const w = window.innerWidth;
const hSVG = 80; // Grass 80px high
// Generate Grass
const bladeCount = w / 5;
for (let i = 0; i < bladeCount; i++) {
const height = Math.random() * 40 + 20;
const x = i * 5 + Math.random() * 3;
const hue = 80 + Math.random() * 40; // slightly more yellow-green for spring/easter
const color = `hsl(${hue}, 60%, 40%)`;
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
}
for (let i = 0; i < 200; i++) {
const x = Math.random() * w;
const h = 20 + Math.random() * 50;
const cY = hSVG - h;
const bend = x + (Math.random() * 40 - 20);
const color = Math.random() > 0.5 ? '#4caf50' : '#8bc34a';
const width = 1 + Math.random() * 2;
const path = `<path d="M ${x} ${hSVG} Q ${bend} ${cY+20} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
}
// Generate Flowers
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
for (let i = 0; i < 40; i++) {
const x = 10 + Math.random() * (w - 20);
const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
const col = colors[Math.floor(Math.random() * colors.length)];
let path = '';
path += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#006400" stroke-width="1.5" fill="none"/>`;
const r = 2 + Math.random() * 1.5;
path += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
path += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
path += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
path += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
path += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
}
grassContainer.innerHTML = `
<div class="easter-meadow-layer" style="z-index: 1001;">
<svg class="easter-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="easter-sway">
${pathsBg}
</g>
</svg>
</div>
<div class="easter-meadow-layer" style="z-index: 1003;">
<svg class="easter-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="easter-sway" style="animation-delay: -2s;">
${pathsFg}
</g>
</svg>
</div>
`;
// Add Easter Eggs
for (let i = 0; i < eggCount; i++) {
const x = 2 + Math.random() * 96;
const y = Math.random() * 18; // 0 to 18px off bottom
const imageSrc = easterEggImages[Math.floor(Math.random() * easterEggImages.length)];
const eggImg = document.createElement('img');
eggImg.src = imageSrc;
eggImg.style.position = 'absolute';
eggImg.style.left = `${x}vw`;
eggImg.style.bottom = `${y}px`;
eggImg.style.width = `${15 + Math.random() * 10}px`;
eggImg.style.height = 'auto';
eggImg.style.transform = `rotate(${Math.random() * 60 - 30}deg)`;
eggImg.style.zIndex = Math.random() > 0.5 ? '1000' : '1004'; // Between grass layers
grassContainer.appendChild(eggImg);
}
console.log('Random easter added');
}
function addHoppingRabbit() {
if (!bunny) return; // Nur ausführen, wenn Easter aktiviert ist
let rabbitTimeout;
let isAnimating = false;
const easterContainer = document.querySelector('.easter-container');
if (!easterContainer) return;
function addHoppingRabbit(container) {
if (!enableBunny) return;
// Hase erstellen
const rabbitImg = document.createElement("img");
rabbitImg.id = "rabbit";
rabbitImg.src = rabbit; // Bildpfad aus der bestehenden Definition
rabbitImg.alt = "Hoppelnder Osterhase";
rabbitImg.src = rabbit;
rabbitImg.alt = "Hopping Easter Bunny";
rabbitImg.className = "hopping-rabbit";
// CSS-Klassen hinzufügen
rabbitImg.classList.add("hopping-rabbit");
rabbitImg.style.bottom = "-15px";
rabbitImg.style.position = "absolute";
easterContainer.appendChild(rabbitImg);
rabbitImg.style.bottom = (hopHeight / 2 + 6) + "px";
container.appendChild(rabbitImg);
animateRabbit(rabbitImg);
}
function animateRabbit(rabbitElement) {
const rabbit = rabbitElement || document.querySelector('#rabbit');
if (!rabbit) return;
function animateRabbit(rabbit) {
if (!rabbit || isAnimating) return;
isAnimating = true;
const startFromLeft = Math.random() >= 0.5;
const startX = startFromLeft ? -15 : 115;
let currentX = startX;
const endX = startFromLeft ? 115 : -15;
const direction = startFromLeft ? 1 : -1;
rabbit.style.transition = 'none';
const transformScale = startFromLeft ? 'scaleX(-1)' : '';
// Set bounding box center-of-gravity shift when graphic is flipped
rabbit.style.transformOrigin = startFromLeft ? '59% 50%' : '50% 50%';
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
const loopDurationMs = jumpDurationMs + pauseDurationMs;
let startTime = null;
function animationStep(timestamp) {
if (!document.querySelector('.easter-container') || rabbit.style.display === 'none') {
isAnimating = false;
return;
}
if (!startTime) {
startTime = timestamp;
// random start position and direction
const startFromLeft = Math.random() >= 0.5;
rabbit.startX = startFromLeft ? -10 : 110;
rabbit.endX = startFromLeft ? 110 : -10;
rabbit.direction = startFromLeft ? 1 : -1;
// mirror the rabbit image if it starts from the right
rabbit.style.transform = startFromLeft ? '' : 'scaleX(-1)';
const currSrc = rabbit.src.split('?')[0];
rabbit.src = currSrc + '?t=' + Date.now();
}
const progress = timestamp - startTime;
// calculate the horizontal position (linear interpolation)
const x = rabbit.startX + (progress / bunnyDuration) * (rabbit.endX - rabbit.startX);
const elapsed = timestamp - startTime;
// calculate the vertical position (sinus curve)
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
const completedLoops = Math.floor(elapsed / loopDurationMs);
const timeInCurrentLoop = elapsed % loopDurationMs;
// set the new position
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`;
if (progress < bunnyDuration) {
animationFrameId = requestAnimationFrame(animationStep);
// Determine if we are currently jumping or pausing
let currentLoopDistance = 0;
if (timeInCurrentLoop < jumpDurationMs) {
// We are in the jumping phase
currentLoopDistance = (timeInCurrentLoop / jumpDurationMs) * jumpDistanceVw;
} else {
// let the bunny rest for a while before hiding easter eggs again
const pauseDuration = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
setTimeout(() => {
startTime = null;
animationFrameId = requestAnimationFrame(animationStep);
}, pauseDuration);
}
// We are in the paused phase
currentLoopDistance = jumpDistanceVw;
}
animationFrameId = requestAnimationFrame(animationStep);
currentX = startX + (completedLoops * jumpDistanceVw + currentLoopDistance) * direction;
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
// Check if finished crossing
if ((direction === 1 && currentX >= endX) || (direction === -1 && currentX <= endX)) {
let restTime = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
isAnimating = false;
rabbitTimeout = setTimeout(() => {
if (!document.body.contains(rabbit)) return;
animateRabbit(document.querySelector('#rabbit'));
}, restTime);
return;
}
rabbitTimeout = requestAnimationFrame(animationStep);
}
// Start loop
rabbitTimeout = requestAnimationFrame(animationStep);
}
function initializeEaster() {
if (!easter) return;
// initialize standard easter
function initEaster() {
const container = document.querySelector('.easter-container') || document.createElement("div");
if (!document.querySelector('.easter-container')) {
@@ -196,48 +267,17 @@ function initEaster() {
document.body.appendChild(container);
}
// shuffle the easter images
let currentIndex = images.length;
let randomIndex;
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[images[currentIndex], images[randomIndex]] = [images[randomIndex], images[currentIndex]];
createEasterGrassAndEggs(container);
addHoppingRabbit(container);
// Add resize listener to regenerate meadow
window.addEventListener('resize', () => {
if(document.querySelector('.easter-container')) {
createEasterGrassAndEggs(container);
}
});
for (let i = 0; i < 12; i++) {
const eggDiv = document.createElement("div");
eggDiv.className = "easter";
const img = document.createElement("img");
img.src = images[i];
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
eggDiv.appendChild(img);
container.appendChild(eggDiv);
}
addHoppingRabbit();
}
// initialize easter and add random easter after the DOM is loaded
function initializeEaster() {
if (!easter) return; // exit if easter are disabled
initEaster();
toggleEaster();
const screenWidth = window.innerWidth;
if (randomEaster && (screenWidth > 768 || randomEasterMobile)) { // add random easter only on larger screens, unless enabled for mobile devices
addRandomEaster(easterEggCount);
}
}
initializeEaster();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,56 @@
.eid-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
contain: strict;
overflow: hidden;
contain: layout paint;
}
.eid-symbol {
position: absolute;
user-select: none;
font-size: 1.5em;
}
.eid-symbol.floating-star {
will-change: opacity;
opacity: 0;
animation: eid-twinkle 4s ease-in-out infinite;
mix-blend-mode: screen;
}
.lantern-rope {
will-change: transform;
position: absolute;
top: 0;
width: 2px;
background: linear-gradient(to bottom, rgba(255, 215, 0, 0.1), rgba(255, 215, 0, 0.6));
transform-origin: top center;
animation: lantern-swing 4s ease-in-out infinite alternate;
}
.lantern-emoji {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 2.5em;
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5));
}
@keyframes lantern-swing {
0% { transform: rotate(-8deg); }
100% { transform: rotate(8deg); }
}
@keyframes eid-twinkle {
0% { transform: scale(0.8); opacity: 0; text-shadow: 0 0 5px gold; }
50% { transform: scale(1.2); opacity: 0.8; text-shadow: 0 0 20px gold; }
100% { transform: scale(0.8); opacity: 0; text-shadow: 0 0 5px gold; }
}

View File

@@ -0,0 +1,100 @@
const config = window.SeasonalsPluginConfig?.Eid || {};
const eid = config.EnableEid !== undefined ? config.EnableEid : true; // enable/disable eid
const lanternCount = config.LanternCount !== undefined ? config.LanternCount : 8; // count of lantern
const lanternCountMobile = config.LanternCountMobile !== undefined ? config.LanternCountMobile : 3; // count of lantern on mobile
const eidSymbols = ['🌙', '⭐', '✨'];
let msgPrinted = false;
function toggleEid() {
const container = document.querySelector('.eid-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Eid hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Eid visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleEid);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createEid(container) {
const starCount = 20;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
let activeLanternCount = isMobile ? lanternCountMobile : lanternCount;
// Create evenly spaced lanterns
const segmentWidth = 100 / activeLanternCount;
for (let i = 0; i < activeLanternCount; i++) {
const symbol = document.createElement('div');
symbol.className = 'eid-symbol lantern-rope';
// Base position within segment, with slight random jitter
const baseLeft = (i * segmentWidth) + (segmentWidth * 0.2);
const jitter = Math.random() * (segmentWidth * 0.6);
symbol.style.left = `${baseLeft + jitter}%`;
symbol.style.animationDelay = `${Math.random() * -4}s`;
const ropeLen = 15 + Math.random() * 15; // 15vh to 30vh
symbol.style.height = `${ropeLen}vh`;
const lanternSpan = document.createElement('span');
lanternSpan.className = 'lantern-emoji';
lanternSpan.textContent = '🏮';
symbol.appendChild(lanternSpan);
container.appendChild(symbol);
}
// Create random floating stars
for (let i = 0; i < starCount; i++) {
const symbol = document.createElement('div');
symbol.className = 'eid-symbol floating-star';
symbol.textContent = eidSymbols[Math.floor(Math.random() * eidSymbols.length)];
symbol.style.left = `${Math.random() * 100}%`;
symbol.style.top = `${Math.random() * 100}%`;
symbol.style.animationDelay = `${Math.random() * -5}s`;
symbol.addEventListener('animationiteration', () => {
symbol.style.left = `${Math.random() * 90 + 5}%`;
symbol.style.top = `${Math.random() * 100}%`;
});
container.appendChild(symbol);
}
}
function initializeEid() {
if (!eid) return;
const container = document.querySelector('.eid-container') || document.createElement("div");
if (!document.querySelector('.eid-container')) {
container.className = "eid-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
createEid(container);
toggleEid();
}
initializeEid();

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
.filmnoir-tint {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 10000;
background-color: #8c7355;
mix-blend-mode: color;
}
.filmnoir-effects {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1100;
}
/* Film grain */
.filmnoir-grain {
will-change: transform, opacity;
position: absolute;
top: 0;
left: -50%;
width: 200%;
height: 200%;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" stitchTiles="stitch"/></filter><rect width="200" height="200" filter="url(%23n)" opacity="0.4"/></svg>');
animation: grain-dance 0.2s steps(4) infinite;
pointer-events: none;
mix-blend-mode: overlay;
opacity: 0.3;
translate: 0 -50vh;
}
/* Vignette */
.filmnoir-vignette {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: radial-gradient(circle at center, transparent 50%, rgba(0,0,0,0.8) 120%);
box-shadow: inset 0 0 100px rgba(0,0,0,0.6);
}
/* Occasional flicker and scratch */
.filmnoir-scratches {
will-change: opacity;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: linear-gradient(to right, transparent 50%, rgba(255,255,255,0.2) 51%, transparent 52%);
background-size: 200% 100%;
animation: scratch 4s infinite linear, flicker 6s infinite alternate;
opacity: 0.2;
mix-blend-mode: screen;
}
@keyframes grain-dance {
0% { transform: translate(0,0); }
25% { transform: translate(-5%,-5%); }
50% { transform: translate(-10%,5%); }
75% { transform: translate(5%,-10%); }
100% { transform: translate(0,0); }
}
@keyframes scratch {
0% { background-position: -200% 0; }
10% { background-position: 200% 0; }
100% { background-position: 200% 0; }
}
@keyframes flicker {
0% { opacity: 0.2; }
5% { opacity: 0.1; }
10% { opacity: 0.3; }
15% { opacity: 0.2; }
50% { opacity: 0.15; }
55% { opacity: 0.25; }
100% { opacity: 0.2; }
}

View File

@@ -0,0 +1,79 @@
const config = window.SeasonalsPluginConfig?.FilmNoir || {};
const filmnoir = config.EnableFilmNoir !== undefined ? config.EnableFilmNoir : true; // enable/disable filmnoir
let msgPrinted = false;
function toggleFilmNoir() {
const tint = document.querySelector('.filmnoir-tint');
const effects = document.querySelector('.filmnoir-effects');
if (!tint || !effects) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
tint.style.display = 'none';
effects.style.display = 'none';
if (!msgPrinted) {
console.log('FilmNoir hidden');
msgPrinted = true;
}
} else {
tint.style.display = 'block';
effects.style.display = 'block';
if (msgPrinted) {
console.log('FilmNoir visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleFilmNoir);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createFilmNoir() {
if (!document.querySelector('.filmnoir-tint')) {
const tint = document.createElement('div');
tint.className = 'filmnoir-tint';
tint.setAttribute('aria-hidden', 'true');
document.body.appendChild(tint);
}
let effects = document.querySelector('.filmnoir-effects');
if (!effects) {
effects = document.createElement('div');
effects.className = 'filmnoir-effects';
effects.setAttribute('aria-hidden', 'true');
document.body.appendChild(effects);
const vignette = document.createElement('div');
vignette.className = 'filmnoir-vignette';
const grain = document.createElement('div');
grain.className = 'filmnoir-grain';
const scratches = document.createElement('div');
scratches.className = 'filmnoir-scratches';
effects.appendChild(grain);
effects.appendChild(scratches);
effects.appendChild(vignette);
}
}
function initializeFilmNoir() {
if (!filmnoir) return;
createFilmNoir();
toggleFilmNoir();
}
initializeFilmNoir();

View File

@@ -7,12 +7,14 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.rocket-trail {
will-change: transform;
position: absolute;
left: var(--trailX);
top: var(--trailStartY);
top: 0;
width: 4px;
/* activate the following for rocket trail */
@@ -27,6 +29,7 @@
box-shadow: 0 0 8px 2px white;*/
animation: rocket-trail-animation 1s linear forwards;
translate: 0 var(--trailStartY);
}
@keyframes rocket-trail-animation {
@@ -55,6 +58,7 @@
}
.firework {
will-change: transform;
position: absolute;
width: 5px;
height: 5px;

View File

@@ -2,10 +2,10 @@ const config = window.SeasonalsPluginConfig?.Fireworks || {};
const fireworks = config.EnableFireworks !== undefined ? config.EnableFireworks : true; // enable/disable fireworks
const scrollFireworks = config.ScrollFireworks !== undefined ? config.ScrollFireworks : true; // enable fireworks to scroll with page content
const particlesPerFirework = config.ParticleCount || 50; // count of particles per firework (Warning: High values may affect performance)
const minFireworks = config.MinFireworks || 3; // minimum number of simultaneous fireworks
const maxFireworks = config.MaxFireworks || 6; // maximum number of simultaneous fireworks
const intervalOfFireworks = config.LaunchInterval || 3200; // interval for the fireworks in milliseconds
const particlesPerFirework = config.ParticleCount !== undefined ? config.ParticleCount : 50; // count of particles per firework
const minFireworks = config.MinFireworks !== undefined ? config.MinFireworks : 3; // minimum number of simultaneous fireworks
const maxFireworks = config.MaxFireworks !== undefined ? config.MaxFireworks : 6; // maximum number of simultaneous fireworks
const intervalOfFireworks = config.LaunchInterval !== undefined ? config.LaunchInterval : 3200; // interval for the fireworks in milliseconds
// array of color palettes for the fireworks
const colorPalettes = [
@@ -60,9 +60,9 @@ const observer = new MutationObserver(toggleFirework);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
@@ -162,6 +162,7 @@ function startFireworks() {
}
fireworksInterval = setInterval(() => {
if (!document.body.contains(fireworkContainer)) { clearInterval(fireworksInterval); return; }
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
for (let i = 0; i < randomCount; i++) {
setTimeout(() => {

View File

@@ -0,0 +1,34 @@
.friday13-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10000;
contain: strict;
overflow: hidden;
}
.friday13-cat {
position: absolute;
width: 150px; /* MARK: Cat size */
height: auto;
user-select: none;
animation-timing-function: linear;
opacity: 0;
}
@keyframes cat-walk-right {
0% { left: -10vw; transform: scaleX(-1); opacity: 1; }
99% { left: 110vw; transform: scaleX(-1); opacity: 1; }
100% { opacity: 0; transform: scaleX(-1); left: 110vw; }
}
@keyframes cat-walk-left {
0% { left: 110vw; transform: scaleX(1); opacity: 1; }
99% { left: -10vw; transform: scaleX(1); opacity: 1; }
100% { opacity: 0; transform: scaleX(1); left: -10vw; }
}

View File

@@ -0,0 +1,83 @@
const config = window.SeasonalsPluginConfig?.Friday13 || {};
const friday13 = config.EnableFriday13 !== undefined ? config.EnableFriday13 : true; // enable/disable friday13
let msgPrinted = false;
function toggleFriday13() {
const container = document.querySelector('.friday13-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Friday13 hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Friday13 visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleFriday13);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createFriday13(container) {
function spawnCat() {
// MARK: Height of the cat from bottom
const catBottomPosition = "-15px";
// MARK: Time it takes for the cat to cross the screen
const catWalkDurationSeconds = 20;
const cat = document.createElement('img');
cat.className = 'friday13-cat';
cat.src = '../Seasonals/Resources/friday_assets/black-cat.gif';
cat.style.bottom = catBottomPosition;
// Either walk left to right or right to left
const dir = Math.random() > 0.5 ? 'right' : 'left';
cat.style.animationName = `cat-walk-${dir}`;
cat.style.animationDuration = `${catWalkDurationSeconds}s`;
cat.style.animationIterationCount = `1`; // play once and remove
cat.style.animationFillMode = `forwards`;
container.appendChild(cat);
// Remove and respawn
setTimeout(() => {
if (cat.parentNode) {
cat.parentNode.removeChild(cat);
}
// Respawn with random delay between 5 to 25 seconds
setTimeout(() => { if (document.body.contains(container)) spawnCat(); }, Math.random() * 20000 + 5000);
}, (catWalkDurationSeconds * 1000) + 500); // Wait for duration + 500ms safety margin
}
// Initial spawn with random delay
setTimeout(() => { if (document.body.contains(container)) spawnCat(); }, Math.random() * 5000);
}
function initializeFriday13() {
if (!friday13) return;
const container = document.querySelector('.friday13-container') || document.createElement("div");
if (!document.querySelector('.friday13-container')) {
container.className = "friday13-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
createFriday13(container);
toggleFriday13();
}
initializeFriday13();

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1,71 @@
.frost-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
overflow: hidden;
contain: strict;
contain: layout paint;
}
.frost-layer {
will-change: transform;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 60%, rgba(180, 220, 255, 0.4) 100%);
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
filter: url('#frost-filter');
animation: frost-creep 4s ease-out forwards;
}
.frost-crystals {
will-change: transform;
position: absolute;
top: 0;
left: -5%;
width: 110%;
height: 110%;
background-image:
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><circle cx="10" cy="10" r="1.5" fill="rgba(255,255,255,0.2)"/><circle cx="40" cy="30" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="20" cy="50" r="2" fill="rgba(255,255,255,0.1)"/><path d="M50 10 L51 15 L56 16 L51 17 L50 22 L49 17 L44 16 L49 15 Z" fill="rgba(255,255,255,0.2)"/></svg>'),
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="25" cy="5" r="1.5" fill="rgba(255,255,255,0.1)"/><path d="M20 20 L21 23 L24 24 L21 25 L20 28 L19 25 L16 24 L19 23 Z" fill="rgba(255,255,255,0.15)"/></svg>'),
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><circle cx="15" cy="15" r="1" fill="rgba(255,255,255,0.2)"/><circle cx="5" cy="5" r="0.8" fill="rgba(255,255,255,0.1)"/></svg>');
background-repeat: repeat;
background-size: 110px 110px, 60px 60px, 30px 30px;
background-position: 0 0, 15px 15px, 5px 10px;
mix-blend-mode: overlay;
mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
animation: frost-shimmer 6s infinite alternate ease-in-out;
translate: 0 -5vh;
}
@keyframes frost-creep {
0% {
opacity: 0;
box-shadow: inset 0 0 10px rgba(200, 230, 255, 0);
}
100% {
opacity: 1;
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
}
}
@keyframes frost-shimmer {
0% {
opacity: 0.4;
transform: scale(1);
}
100% {
opacity: 0.8;
transform: scale(1.02);
}
}

View File

@@ -0,0 +1,75 @@
const config = window.SeasonalsPluginConfig?.Frost || {};
const frost = config.EnableFrost !== undefined ? config.EnableFrost : true; // enable/disable frost
let msgPrinted = false;
function toggleFrost() {
const container = document.querySelector('.frost-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Frost hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Frost visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleFrost);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createFrost(container) {
const frostLayer = document.createElement('div');
frostLayer.className = 'frost-layer';
const frostCrystals = document.createElement('div');
frostCrystals.className = 'frost-crystals';
// An SVG filter to make things look "frozen"/distorted around the edges
const svgFilter = document.createElement('div');
svgFilter.innerHTML = `
<svg style="display:none;">
<filter id="frost-filter">
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="3" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="5" xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
`;
frostLayer.appendChild(frostCrystals);
container.appendChild(frostLayer);
container.appendChild(svgFilter);
}
function initializeFrost() {
if (!frost) return;
const container = document.querySelector('.frost-container') || document.createElement("div");
if (!document.querySelector('.frost-container')) {
container.className = "frost-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
createFrost(container);
}
initializeFrost();

View File

@@ -8,23 +8,16 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.halloween {
will-change: transform;
position: fixed;
bottom: -10%;
z-index: 15;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-select: none;
cursor: default;
-webkit-animation-name: halloween-fall, halloween-shake;
-webkit-animation-duration: 10s, 3s;
-webkit-animation-timing-function: linear, ease-in-out;
-webkit-animation-iteration-count: infinite, infinite;
-webkit-animation-play-state: running, running;
animation-name: halloween-fall, halloween-shake;
animation-duration: 10s, 3s;
animation-timing-function: linear, ease-in-out;
@@ -32,37 +25,15 @@
animation-play-state: running, running
}
@-webkit-keyframes halloween-fall {
0% {
bottom: -10%
}
100% {
bottom: 100%
}
}
@-webkit-keyframes halloween-shake {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0)
}
50% {
-webkit-transform: translateX(80px);
transform: translateX(80px)
}
}
@keyframes halloween-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}
@@ -78,74 +49,117 @@
}
}
.halloween:nth-of-type(0) {
left: 1%;
-webkit-animation-delay: 0s, 0s;
animation-delay: 0s, 0s
/* --- Fog Layer --- */
.halloween-fog-layer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
mask-image: linear-gradient(to top, black, transparent);
}
.halloween-fog-blob {
position: absolute;
bottom: -10vh;
width: 150vw;
height: 50vh;
background: radial-gradient(ellipse at center, rgba(120, 130, 140, 0.4) 0%, transparent 60%);
border-radius: 50%;
filter: blur(15px);
}
.halloween-fog-blob:nth-child(1) {
will-change: transform;
left: -20vw;
animation: fog-float1 25s ease-in-out infinite alternate;
}
.halloween-fog-blob:nth-child(2) {
will-change: transform;
left: -50vw;
background: radial-gradient(ellipse at center, rgba(100, 110, 120, 0.3) 0%, transparent 65%);
animation: fog-float2 35s ease-in-out infinite alternate;
}
@keyframes fog-float1 {
0% { transform: translateX(0) scale(1); opacity: 0.8; }
50% { opacity: 1; }
100% { transform: translateX(20vw) scale(1.1); opacity: 0.6; }
}
@keyframes fog-float2 {
0% { transform: translateX(0) scale(1.1); opacity: 0.7; }
50% { opacity: 1; }
100% { transform: translateX(30vw) scale(1); opacity: 0.5; }
}
.halloween:nth-of-type(1) {
left: 10%;
-webkit-animation-delay: 1s, 1s;
animation-delay: 1s, 1s
/* --- Spiders --- */
.halloween-spider-wrapper {
position: absolute;
top: 0;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1002;
transform-origin: top;
will-change: transform;
pointer-events: auto;
padding: 20px; /* Increase hit area */
translate: 0 -50px;
}
.halloween:nth-of-type(2) {
left: 20%;
-webkit-animation-delay: 6s, .5s;
animation-delay: 6s, .5s
.halloween-thread {
width: 30px; /* Wider hit area for mouse interaction */
height: 100vh;
margin-top: -100vh;
position: relative;
cursor: pointer;
}
.halloween:nth-of-type(3) {
left: 30%;
-webkit-animation-delay: 4s, 2s;
animation-delay: 4s, 2s
}
.halloween:nth-of-type(4) {
left: 40%;
-webkit-animation-delay: 2s, 2s;
animation-delay: 2s, 2s
}
.halloween:nth-of-type(5) {
.halloween-thread::after {
content: '';
position: absolute;
left: 50%;
-webkit-animation-delay: 8s, 3s;
animation-delay: 8s, 3s
transform: translateX(-50%);
width: 2px;
height: 100%;
background: linear-gradient(to bottom, rgba(200, 200, 200, 0.1), rgba(200, 200, 200, 0.6));
}
.halloween-spider {
will-change: transform;
animation: spider-swing 3s ease-in-out infinite alternate;
transform-origin: top center;
}
.halloween:nth-of-type(6) {
left: 60%;
-webkit-animation-delay: 6s, 2s;
animation-delay: 6s, 2s
/* MARK: SPIDER SWAY CONFIGURATION */
@keyframes wind-sway {
0% { transform: rotate(0deg); }
25% { transform: rotate(2deg); }
75% { transform: rotate(-2deg); }
100% { transform: rotate(0deg); }
}
.halloween:nth-of-type(7) {
left: 70%;
-webkit-animation-delay: 2.5s, 1s;
animation-delay: 2.5s, 1s
@keyframes spider-drop {
0% { transform: translateY(-50px); }
30% { transform: translateY(var(--drop-height, 50vh)); }
60% { transform: translateY(var(--drop-height, 50vh)); }
100% { transform: translateY(-50px); }
}
@keyframes spider-swing {
0% { transform: rotate(-10deg); }
100% { transform: rotate(10deg); }
}
.halloween:nth-of-type(8) {
left: 80%;
-webkit-animation-delay: 1s, 0s;
animation-delay: 1s, 0s
/* Mice */
.halloween-mouse {
position: absolute;
z-index: 10000;
pointer-events: none;
will-change: left;
}
.halloween:nth-of-type(9) {
left: 90%;
-webkit-animation-delay: 3s, 1.5s;
animation-delay: 3s, 1.5s
@keyframes mouse-run-right {
0% { left: -10vw; }
100% { left: 110vw; }
}
.halloween:nth-of-type(10) {
left: 25%;
-webkit-animation-delay: 2s, 0s;
animation-delay: 2s, 0s
}
.halloween:nth-of-type(11) {
left: 65%;
-webkit-animation-delay: 4s, 2.5s;
animation-delay: 4s, 2.5s
@keyframes mouse-run-left {
0% { left: 110vw; }
100% { left: -10vw; }
}

View File

@@ -1,12 +1,19 @@
const config = window.SeasonalsPluginConfig?.Halloween || {};
const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween : true; // enable/disable halloween
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true; // enable/disable spiders
const enableMice = config.EnableMice !== undefined ? config.EnableMice : true; // enable/disable mice
const halloweenCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
const halloweenCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbols on mobile
let msgPrinted = false; // flag to prevent multiple console messages
const images = [
"../Seasonals/Resources/halloween_images/ghost_20x20.png",
"../Seasonals/Resources/halloween_images/bat_20x20.png",
"../Seasonals/Resources/halloween_images/pumpkin_20x20.png",
];
let msgPrinted = false;
// function to check and control the halloween
function toggleHalloween() {
@@ -34,47 +41,40 @@ function toggleHalloween() {
}
}
// observe changes in the DOM
const observer = new MutationObserver(toggleHalloween);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
const images = [
"/Seasonals/Resources/halloween_images/ghost_20x20.png",
"/Seasonals/Resources/halloween_images/bat_20x20.png",
"/Seasonals/Resources/halloween_images/pumpkin_20x20.png",
];
function addRandomSymbols(count) {
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
if (!halloweenContainer) return; // exit if halloween container is not found
console.log('Adding random halloween symbols');
function initHalloween(count) {
let halloweenContainer = document.querySelector('.halloween-container');
if (!halloweenContainer) {
halloweenContainer = document.createElement("div");
halloweenContainer.className = "halloween-container";
halloweenContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(halloweenContainer);
}
console.log('Adding halloween symbols');
for (let i = 0; i < count; i++) {
// create a new halloween elements
const halloweenDiv = document.createElement("div");
halloweenDiv.className = "halloween";
// pick a random halloween symbol
const imageSrc = images[Math.floor(Math.random() * images.length)];
const img = document.createElement("img");
img.src = imageSrc;
halloweenDiv.appendChild(img);
// set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s)
const randomAnimationDelay2 = Math.random() * 3; // delay (0s to 3s)
// Display directly symbols on full screen (below) or let it build up (above)
// const randomAnimationDelay = -(Math.random() * 10); // delay (-10s to 0s)
const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s)
// apply styles
halloweenDiv.style.left = `${randomLeft}%`;
@@ -87,52 +87,149 @@ function addRandomSymbols(count) {
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
// add the halloween to the container
halloweenContainer.appendChild(halloweenDiv);
}
console.log('Random halloween symbols added');
console.log('Halloween symbols added');
}
// create halloween objects
function createHalloween() {
const container = document.querySelector('.halloween-container') || document.createElement("div");
// create fog layer
function createFog(container) {
const fogContainer = document.createElement('div');
fogContainer.className = 'halloween-fog-layer';
if (!document.querySelector('.halloween-container')) {
container.className = "halloween-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
const fog1 = document.createElement('div');
fog1.className = 'halloween-fog-blob';
const fog2 = document.createElement('div');
fog2.className = 'halloween-fog-blob';
fogContainer.appendChild(fog1);
fogContainer.appendChild(fog2);
container.appendChild(fogContainer);
}
// create dropping spiders
function createSpider(container) {
const wrapper = document.createElement('div');
wrapper.className = 'halloween-spider-wrapper';
wrapper.innerHTML = `
<div class="halloween-sway" style="display:flex; flex-direction:column; align-items:center; transform-origin: 50% -100vh;">
<div class="halloween-thread"></div>
<svg class="halloween-spider" viewBox="0 0 24 24" width="30" height="30">
<circle cx="12" cy="12" r="6" fill="#1a1a1a"/>
<!-- left legs -->
<path d="M12 12 l-8 -4 M12 12 l-9 0 M12 12 l-8 4 M12 12 l-6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<!-- right legs -->
<path d="M12 12 l8 -4 M12 12 l9 0 M12 12 l8 4 M12 12 l6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="10" cy="14" r="1.5" fill="#ff3333"/>
<circle cx="14" cy="14" r="1.5" fill="#ff3333"/>
</svg>
</div>
`;
wrapper.style.left = `${10 + Math.random() * 80}%`;
const dropHeight = 30 + Math.random() * 50; // 30vh to 80vh
wrapper.style.setProperty('--drop-height', `${dropHeight}vh`);
const duration = Math.random() * 6 + 6; // 6-12s drop
wrapper.style.animation = `spider-drop ${duration}s ease-in-out forwards`;
// Start the sway animation only after the drop completes (30% of total duration)
const sway = wrapper.querySelector('.halloween-sway');
sway.style.animation = `wind-sway 8s ease-in-out ${duration * 0.3}s infinite`;
// Spider retreat logic
let isRetreating = false;
wrapper.addEventListener('mouseenter', () => {
if (isRetreating) return;
isRetreating = true;
// Retreat smoothly by pushing margin up
wrapper.style.transition = 'margin-top 0.4s ease-in';
wrapper.style.marginTop = '-100vh';
setTimeout(() => {
wrapper.remove();
if (document.body.contains(container)) {
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
}
for (let i = 0; i < 4; i++) {
images.forEach(imageSrc => {
const halloweenDiv = document.createElement("div");
halloweenDiv.className = "halloween";
const img = document.createElement("img");
img.src = imageSrc;
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
halloweenDiv.appendChild(img);
container.appendChild(halloweenDiv);
}, 500);
});
wrapper.addEventListener('animationend', () => {
if (isRetreating) return;
wrapper.remove();
if (document.body.contains(container)) {
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
}
});
container.appendChild(wrapper);
}
// create scurrying mice
function createMouse(container) {
const mouse = document.createElement('div');
mouse.className = 'halloween-mouse';
mouse.innerHTML = `
<svg viewBox="0 0 30 15" width="40" height="20">
<ellipse cx="15" cy="10" rx="10" ry="5" fill="#111"/>
<circle cx="24" cy="10" r="4" fill="#111"/>
<circle cx="24" cy="6" r="3" fill="#333"/>
<path d="M 5 10 Q 0 10 0 2" stroke="#111" stroke-width="1.5" fill="none"/>
</svg>
`;
const direction = Math.random() > 0.5 ? 'right' : 'left';
const duration = Math.random() * 3 + 2; // 2-5s run (fast)
if (direction === 'right') {
mouse.style.animation = `mouse-run-right ${duration}s linear forwards`;
mouse.style.transform = 'scaleX(1)';
} else {
mouse.style.animation = `mouse-run-left ${duration}s linear forwards`;
mouse.style.transform = 'scaleX(-1)';
}
mouse.style.bottom = `5px`; // Fixated bottom edge
mouse.addEventListener('animationend', () => {
mouse.remove();
if (document.body.contains(container)) {
setTimeout(() => createMouse(container), Math.random() * 4000 + 2000);
}
});
container.appendChild(mouse);
}
// initialize halloween
function initializeHalloween() {
if (!halloween) return; // exit if halloween is disabled
createHalloween();
if (!halloween) return;
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? halloweenCount : halloweenCountMobile;
initHalloween(count);
toggleHalloween();
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
addRandomSymbols(halloweenCount);
const container = document.querySelector('.halloween-container');
if (container) {
createFog(container);
// Add a few spiders
if (enableSpiders) {
for (let i = 0; i < 4; i++) {
setTimeout(() => createSpider(container), Math.random() * 5000);
}
}
// Add a few mice
if (enableMice) {
for (let i = 0; i < 3; i++) {
setTimeout(() => createMouse(container), Math.random() * 3000);
}
}
}
}

View File

@@ -8,59 +8,33 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.heart {
will-change: transform;
position: fixed;
bottom: -10%;
z-index: 15;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-select: none;
cursor: default;
-webkit-animation-name: heart-fall, heart-shake;
-webkit-animation-duration: 14s, 5s;
-webkit-animation-timing-function: linear, ease-in-out;
-webkit-animation-iteration-count: infinite, infinite;
animation-name: heart-fall, heart-shake;
animation-duration: 14s, 5s;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
}
@-webkit-keyframes heart-fall {
0% {
bottom: -10%
}
100% {
bottom: 100%
}
}
@-webkit-keyframes heart-shake {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0)
}
50% {
-webkit-transform: translateX(80px);
transform: translateX(80px)
}
}
@keyframes heart-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}
@@ -75,75 +49,3 @@
transform: translateX(80px)
}
}
.heart:nth-of-type(0) {
left: 1%;
-webkit-animation-delay: 0s, 0s;
animation-delay: 0s, 0s
}
.heart:nth-of-type(1) {
left: 10%;
-webkit-animation-delay: 1s, 1s;
animation-delay: 1s, 1s
}
.heart:nth-of-type(2) {
left: 20%;
-webkit-animation-delay: 6s, .5s;
animation-delay: 6s, .5s
}
.heart:nth-of-type(3) {
left: 30%;
-webkit-animation-delay: 4s, 2s;
animation-delay: 4s, 2s
}
.heart:nth-of-type(4) {
left: 40%;
-webkit-animation-delay: 2s, 2s;
animation-delay: 2s, 2s
}
.heart:nth-of-type(5) {
left: 50%;
-webkit-animation-delay: 8s, 3s;
animation-delay: 8s, 3s
}
.heart:nth-of-type(6) {
left: 60%;
-webkit-animation-delay: 6s, 2s;
animation-delay: 6s, 2s
}
.heart:nth-of-type(7) {
left: 70%;
-webkit-animation-delay: 2.5s, 1s;
animation-delay: 2.5s, 1s
}
.heart:nth-of-type(8) {
left: 80%;
-webkit-animation-delay: 1s, 0s;
animation-delay: 1s, 0s
}
.heart:nth-of-type(9) {
left: 90%;
-webkit-animation-delay: 3s, 1.5s;
animation-delay: 3s, 1.5s
}
.heart:nth-of-type(10) {
left: 25%;
-webkit-animation-delay: 2s, 0s;
animation-delay: 2s, 0s
}
.heart:nth-of-type(11) {
left: 65%;
-webkit-animation-delay: 4s, 2.5s;
animation-delay: 4s, 2.5s
}

View File

@@ -1,10 +1,12 @@
const config = window.SeasonalsPluginConfig?.Hearts || {};
const hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols
const heartsCount = config.SymbolCount || 25; // count of random extra symbols
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const heartsCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
const heartsCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
// Array of hearts characters
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
let msgPrinted = false; // flag to prevent multiple console messages
@@ -36,24 +38,23 @@ function toggleHearts() {
// observe changes in the DOM
const observer = new MutationObserver(toggleHearts);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
// Array of hearts characters
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
function initHearts(count) {
let heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
if (!heartsContainer) {
heartsContainer = document.createElement("div");
heartsContainer.className = "hearts-container";
heartsContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(heartsContainer);
}
function addRandomSymbols(count) {
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
if (!heartsContainer) return; // exit if hearts container is not found
console.log('Adding random heart symbols');
console.log('Adding heart symbols');
for (let i = 0; i < count; i++) {
// create a new hearts elements
@@ -66,8 +67,8 @@ function addRandomSymbols(count) {
// set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 14; // delay (0s to 14s)
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
// apply styles
heartsDiv.style.left = `${randomLeft}%`;
@@ -83,46 +84,18 @@ function addRandomSymbols(count) {
// add the hearts to the container
heartsContainer.appendChild(heartsDiv);
}
console.log('Random hearts symbols added');
console.log('Heart symbols added');
}
// create hearts objects
function createHearts() {
const container = document.querySelector('.hearts-container') || document.createElement("div");
if (!document.querySelector('.hearts-container')) {
container.className = "hearts-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
for (let i = 0; i < 12; i++) {
const heartsDiv = document.createElement("div");
heartsDiv.className = "heart";
heartsDiv.textContent = heartSymbols[i % heartSymbols.length];
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 16 + 12; // delay (12s to 16s)
const randomAnimationDuration2 = Math.random() * 7 + 3; // delay (3s to 7s)
heartsDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
container.appendChild(heartsDiv);
}
}
// initialize hearts
function initializeHearts() {
if (!hearts) return; // exit if hearts is disabled
createHearts();
toggleHearts();
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random heartss only on larger screens, unless enabled for mobile devices
addRandomSymbols(heartsCount);
}
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? heartsCount : heartsCountMobile;
initHearts(count);
toggleHearts();
}
initializeHearts();

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,80 @@
.marioday-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10000;
contain: strict;
overflow: hidden;
}
.mario-wrapper {
position: absolute;
bottom: 5px;
left: -100px;
animation: mario-run 15s linear infinite;
will-change: left, transform;
}
.mario-runner {
width: 64px;
height: auto;
image-rendering: crisp-edges;
image-rendering: pixelated;
display: block;
will-change: transform;
}
.mario-jump {
will-change: transform;
animation: jump-arc 0.8s ease-in-out;
}
/* 8-bit coin styling */
.mario-coin {
will-change: transform;
position: absolute;
width: 32px;
height: 32px;
background-color: #ffd700;
border: 4px solid #b8860b;
border-radius: 50%;
box-shadow: inset 4px 4px 0 #fffbea, inset -4px -4px 0 #daa520;
animation: pop-up-arc 2s forwards;
}
.mario-coin::after {
content: '';
position: absolute;
top: 0;
left: 10px;
width: 4px;
height: 12px;
background: #daa520;
translate: 0 6px;
}
@keyframes mario-run {
0% { left: -100px; transform: scaleX(1); }
45% { left: 110vw; transform: scaleX(1); }
50% { left: 110vw; transform: scaleX(-1); }
95% { left: -100px; transform: scaleX(-1); }
100% { left: -100px; transform: scaleX(1); }
}
@keyframes pop-up-arc {
0% { transform: translateY(0) rotateY(0deg); opacity: 0; animation-timing-function: ease-out; }
20% { opacity: 1; }
50% { transform: translateY(-30vh) rotateY(360deg); opacity: 1; animation-timing-function: ease-in; }
90% { opacity: 1; }
100% { transform: translateY(20vh) rotateY(720deg); opacity: 0; }
}
@keyframes jump-arc {
0% { transform: translateY(0); }
50% { transform: translateY(-25vh); }
100% { transform: translateY(0); }
}

View File

@@ -0,0 +1,109 @@
const config = window.SeasonalsPluginConfig?.MarioDay || {};
const marioday = config.EnableMarioDay !== undefined ? config.EnableMarioDay : true; // enable/disable marioday
const letMarioJump = config.LetMarioJump !== undefined ? config.LetMarioJump : true; // optionally let mario jump occasionally
// Credit: https://gifs.alphacoders.com/gifs/view/2585
const marioImage = '../Seasonals/Resources/mario_assets/mario.gif';
let msgPrinted = false;
function toggleMarioDay() {
const container = document.querySelector('.marioday-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('MarioDay hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('MarioDay visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleMarioDay);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createMarioDay(container) {
// MARK: Mario's running speed across the screen
const marioSpeedSeconds = 18;
const wrapper = document.createElement('div');
wrapper.className = 'mario-wrapper';
wrapper.style.animationDuration = `${marioSpeedSeconds}s`;
const mario = document.createElement('img');
mario.className = 'mario-runner';
mario.src = marioImage;
wrapper.appendChild(mario);
container.appendChild(wrapper);
let jumpCount = 0;
let maxJumpsForThisRun = Math.floor(Math.random() * 3); // 0, 1, or 2
const resetJumpInterval = setInterval(() => {
if (!document.body.contains(container)) { clearInterval(resetJumpInterval); return; }
jumpCount = 0;
maxJumpsForThisRun = Math.floor(Math.random() * 3); // Randomize jumps for the next pass
}, (marioSpeedSeconds / 2) * 1000);
// Periodically throw out an 8-bit coin
const intervalId = setInterval(() => {
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
if (container.style.display === 'none') return;
const marioRect = wrapper.getBoundingClientRect();
if (marioRect.left < 0 || marioRect.right > window.innerWidth) return;
if (letMarioJump && !mario.classList.contains('mario-jump') && jumpCount < maxJumpsForThisRun) {
mario.classList.add('mario-jump');
jumpCount++;
setTimeout(() => mario.classList.remove('mario-jump'), 800);
}
const coin = document.createElement('div');
coin.className = 'mario-coin';
// Grab Mario's current screen position to lock the coin's X coordinate
coin.style.left = `${marioRect.left + 16}px`;
coin.style.bottom = '35px'; // bottom offset
container.appendChild(coin);
setTimeout(() => coin.remove(), 2000);
}, 4000);
}
function initializeMarioDay() {
if (!marioday) return;
const container = document.querySelector('.marioday-container') || document.createElement("div");
if (!document.querySelector('.marioday-container')) {
container.className = "marioday-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
createMarioDay(container);
toggleMarioDay();
}
initializeMarioDay();

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
.oktoberfest-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
contain: strict;
overflow: hidden;
}
.oktoberfest-symbol {
will-change: transform;
position: absolute;
top: 0;
font-size: 2.2em;
user-select: none;
animation-name: oktoberfest-fall, oktoberfest-sway;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
translate: 0 -10vh;
}
@keyframes oktoberfest-fall {
0% { transform: translateY(0); opacity: 0; }
10% { opacity: 1; }
100% { transform: translateY(120vh); opacity: 1; }
}
@keyframes oktoberfest-sway {
0%, 100% { margin-left: 0; }
50% { margin-left: 50px; }
}

View File

@@ -0,0 +1,75 @@
const config = window.SeasonalsPluginConfig?.Oktoberfest || {};
const oktoberfest = config.EnableOktoberfest !== undefined ? config.EnableOktoberfest : true; // enable/disable oktoberfest
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbols on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const oktoberfestSymbols = ['🥨', '🍺', '🍻', '🥨', '🥨'];
let msgPrinted = false;
// function to check and control the oktoberfest
function toggleOktoberfest() {
const oktoberfestContainer = document.querySelector('.oktoberfest-container');
if (!oktoberfestContainer) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
// hide oktoberfest if video/trailer player is active or dashboard is visible
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
oktoberfestContainer.style.display = 'none'; // hide oktoberfest
if (!msgPrinted) {
console.log('Oktoberfest hidden');
msgPrinted = true;
}
} else {
oktoberfestContainer.style.display = 'block'; // show oktoberfest
if (msgPrinted) {
console.log('Oktoberfest visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleOktoberfest);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createOktoberfest(container, count) {
for (let i = 0; i < count; i++) {
const symbol = document.createElement('div');
symbol.className = 'oktoberfest-symbol';
symbol.textContent = oktoberfestSymbols[Math.floor(Math.random() * oktoberfestSymbols.length)];
symbol.style.left = `${Math.random() * 100}%`;
symbol.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 5}s`;
const duration1 = Math.random() * 5 + 8;
const duration2 = Math.random() * 3 + 3;
if (enableDifferentDuration) {
symbol.style.animationDuration = `${duration1}s, ${duration2}s`;
}
container.appendChild(symbol);
}
}
function initializeOktoberfest() {
if (!oktoberfest) return;
const container = document.querySelector('.oktoberfest-container') || document.createElement("div");
if (!document.querySelector('.oktoberfest-container')) {
container.className = "oktoberfest-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? symbolCount : symbolCountMobile;
createOktoberfest(container, count);
}
initializeOktoberfest();

View File

@@ -0,0 +1,142 @@
.olympia-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
contain: strict;
}
.olympia-symbol {
position: absolute;
top: 0;
opacity: 0.95;
text-shadow: 0 0 10px rgba(255,255,255,0.2);
z-index: 40;
translate: 0 -10vh;
}
.olympia-flame {
position: absolute;
bottom: 0vh;
z-index: 50;
pointer-events: none;
transform-origin: bottom center;
}
.olympia-ring-css {
position: relative;
width: 40px;
height: 40px;
}
.olympia-ring-css::before {
content: '';
position: absolute;
top: 0;
translate: -50% -50%;
width: 30px;
height: 30px;
border: 5px solid #0081C8; /* Default blue ring */
border-radius: 50%;
}
.olympia-ring-css[style*="--ring-color"]::before {
border-color: var(--ring-color);
}
.olympia-symbol {
position: absolute;
top: 0;
opacity: 0.95;
text-shadow: 0 0 10px rgba(255,255,255,0.2);
z-index: 40;
translate: 0 -10vh;
}
.olympia-inner {
will-change: transform;
display: inline-block;
animation: olympia-sway linear infinite alternate;
}
.olympia-symbol img {
width: 6vh;
height: auto;
max-width: 60px;
object-fit: contain;
}
.olympia-confetti-wrapper {
position: fixed;
z-index: 15;
top: 0;
will-change: transform;
animation-name: olympia-fall;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.olympia-confetti-sway {
will-change: transform;
animation-name: olympia-confetti-sway;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes olympia-confetti-sway {
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
100% { transform: translateX(var(--sway-amount, 50px)); }
}
.olympia-confetti {
width: 8px;
height: 16px;
background-color: rgb(0, 0, 0);
will-change: transform;
animation-name: olympia-confetti-flutter;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.olympia-confetti.circle {
width: 8px;
height: 8px;
border-radius: 50%;
}
.olympia-confetti.square {
width: 8px;
height: 8px;
}
.olympia-confetti.triangle {
width: 10px;
height: 10px;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
@keyframes olympia-fall {
0% { transform: translateY(-10vh); }
100% { transform: translateY(110vh); }
}
@keyframes olympia-sway {
0% { transform: rotate(-25deg) translateX(-20px); }
100% { transform: rotate(25deg) translateX(20px); }
}
@keyframes olympia-tumble-3d {
0% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 0deg); }
100% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 360deg); }
}
@keyframes olympia-confetti-flutter {
0% {
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
}
100% {
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
}
}

View File

@@ -0,0 +1,263 @@
const config = window.SeasonalsPluginConfig?.Olympia || {};
const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true; // enable/disable olympia theme
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of floating symbols
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of floating symbols on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
// Olympic Ring Colors (Carnival Config)
const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E'];
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const confettiCount = isMobile ? 30 : 60;
/**
* Credits:
* https://lottiefiles.com/free-animation/gold-coin-5Spp5kJbLP
* https://lottiefiles.com/free-animation/silver-coin-SIgIP59fII
* https://lottiefiles.com/free-animation/bronze-coin-wWVCJMsOUq
*/
const olympicMedals = [
"../Seasonals/Resources/olympic_assets/gold_coin.gif",
"../Seasonals/Resources/olympic_assets/silver_coin.gif",
"../Seasonals/Resources/olympic_assets/bronze_coin.gif"
]
/**
* Credits:
* https://www.flaticon.com/de/kostenloses-icon/fackel_4683293
* merged with:
* https://lottiefiles.com/free-animation/abstract-flames-lottie-json-animation-oSb0IFoBrj
*/
const olympicTorch = "../Seasonals/Resources/olympic_assets/torch.gif";
let msgPrinted = false;
function toggleOlympia() {
const container = document.querySelector('.olympia-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Olympia hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Olympia visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleOlympia);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createOlympia() {
const container = document.querySelector('.olympia-container') || document.createElement('div');
if (!document.querySelector('.olympia-container')) {
container.className = 'olympia-container';
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
const standardCount = 15;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
let finalCount = isMobile ? symbolCountMobile : symbolCount;
const useRandomDuration = enableDifferentDuration !== false;
const olympicRings = ['ring_blue.css', 'ring_yellow.css', 'ring_black.css', 'ring_green.css', 'ring_red.css'];
const activeItems = [...olympicMedals, ...olympicRings];
for (let i = 0; i < finalCount; i++) {
let symbol = document.createElement('div');
const randomImgUrl = activeItems[Math.floor(Math.random() * activeItems.length)];
const isRing = randomImgUrl.includes('ring_');
const isMedal = randomImgUrl.includes('_coin');
symbol.className = `olympia-symbol`;
// Create inner div for sway/rotation
let innerDiv = document.createElement('div');
innerDiv.className = 'olympia-inner';
let img = null;
if (isRing) {
const colorName = randomImgUrl.split('ring_')[1].split('.')[0];
const ringColorMap = {
'blue': '#0081C8',
'yellow': '#FCB131',
'black': '#000000',
'green': '#00A651',
'red': '#EE334E'
};
let ringDiv = document.createElement('div');
ringDiv.className = 'olympia-ring-css';
ringDiv.style.setProperty('--ring-color', ringColorMap[colorName]);
innerDiv.appendChild(ringDiv);
// Add a 3D flip animation for rings
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
innerDiv.style.animation = `olympia-tumble-3d ${Math.random() * 4 + 4}s linear infinite ${spinReverse}`;
// Random 3D Rotation Axis for Tumbling
innerDiv.style.setProperty('--rot-x', (Math.random() * 2 - 1).toFixed(2));
innerDiv.style.setProperty('--rot-y', (Math.random() * 2 - 1).toFixed(2));
innerDiv.style.setProperty('--rot-z', (Math.random() * 2 - 1).toFixed(2));
} else {
img = document.createElement('img');
img.src = randomImgUrl;
img.onerror = function() {
symbol.remove();
};
innerDiv.appendChild(img);
if (isMedal) {
innerDiv.style.animation = `olympia-flip-3d ${Math.random() * 4 + 3}s linear infinite`;
} else {
// Torch sways, medals flip
const swayDur = Math.random() * 2 + 2; // 2 to 4s
const swayDir = Math.random() > 0.5 ? 'normal' : 'reverse';
innerDiv.style.animation = `olympia-sway ${swayDur}s ease-in-out infinite alternate ${swayDir}`;
}
}
symbol.appendChild(innerDiv);
const leftPos = Math.random() * 95;
const delaySeconds = Math.random() * 10;
// Depth logic for medals and rings
const depth = Math.random();
const scale = 0.8 + depth * 0.4; // 0.8 to 1.2
const zIndex = Math.floor(depth * 30) + 10;
if (img) {
img.style.transform = `scale(${scale})`;
} else {
innerDiv.firstChild.style.transform = `scale(${scale})`;
}
symbol.style.zIndex = zIndex;
let durationSeconds = 8;
if (useRandomDuration) {
durationSeconds = (1 - depth) * 5 + 6 + Math.random() * 4;
}
symbol.style.animation = `olympia-fall ${durationSeconds}s linear infinite`;
symbol.style.animationDelay = `${delaySeconds}s`;
symbol.style.left = `${leftPos}vw`;
container.appendChild(symbol);
}
// Olympic Torches (Fixed at bottom corners, symmetrically rotated inward)
// Generate one random inward rotation (10 to 25 deg) for both to share
const sharedTilt = Math.random() * 15 + 10;
const createTorch = (isLeft) => {
const torch = document.createElement('div');
torch.className = 'olympia-flame';
if (isLeft) {
torch.style.left = '5vw';
// Lean right, face normal
torch.style.transform = `rotate(${sharedTilt}deg) scaleX(1)`;
} else {
torch.style.right = '5vw';
// Lean left, mirror image
torch.style.transform = `rotate(-${sharedTilt}deg) scaleX(-1)`;
}
let torchImg = document.createElement('img');
torchImg.src = `../Seasonals/Resources/olympic_assets/torch.gif`;
torchImg.style.height = '25vh';
torchImg.style.objectFit = 'contain';
torchImg.onerror = function() {
this.style.display = 'none';
};
torch.appendChild(torchImg);
container.appendChild(torch);
};
createTorch(true);
createTorch(false);
for (let i = 0; i < confettiCount; i++) {
let wrapper = document.createElement('div');
wrapper.className = 'olympia-confetti-wrapper';
let leftPos = Math.random() * 100;
wrapper.style.left = `${leftPos}vw`;
let fallDuration = Math.random() * 3 + 4; // 4 to 7 seconds to fall
wrapper.style.animationDuration = `${fallDuration}s`;
wrapper.style.animationDelay = `-${Math.random() * fallDuration}s`; // Negative delay so it distributes perfectly immediately
let swayWrapper = document.createElement('div');
swayWrapper.className = 'olympia-confetti-sway';
let swayDuration = Math.random() * 2 + 1.5; // 1.5s to 3.5s
swayWrapper.style.animationDuration = `${swayDuration}s`;
let swayAmount = Math.random() * 30 + 30; // 30px to 60px
swayWrapper.style.setProperty('--sway-amount', `${swayAmount}px`);
let initSwayDelay = Math.random() * swayDuration;
swayWrapper.style.animationDelay = `-${initSwayDelay}s`;
let confetti = document.createElement('div');
confetti.className = 'olympia-confetti';
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
confetti.style.backgroundColor = color;
// Random shape
const shape = Math.random();
if (shape > 0.66) {
confetti.classList.add('circle');
const size = Math.random() * 5 + 5;
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
} else if (shape > 0.33) {
confetti.classList.add('rect');
const width = Math.random() * 4 + 4;
const height = Math.random() * 5 + 8;
confetti.style.width = `${width}px`;
confetti.style.height = `${height}px`;
} else {
confetti.classList.add('triangle');
}
// Random 3D Rotation for flutter
confetti.style.setProperty('--rx', Math.random().toFixed(2));
confetti.style.setProperty('--ry', Math.random().toFixed(2));
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
confetti.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
let rotateDuration = Math.random() * 0.8 + 0.4;
confetti.style.animationDuration = `${rotateDuration}s`;
swayWrapper.appendChild(confetti);
wrapper.appendChild(swayWrapper);
container.appendChild(wrapper);
}
}
function initializeOlympia() {
if (!olympia) return;
createOlympia();
toggleOlympia();
}
initializeOlympia();

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -0,0 +1,70 @@
.oscar-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
contain: strict;
overflow: hidden;
}
.oscar-carpet {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 15vh;
background: linear-gradient(to top, rgba(139, 0, 0, 0.8) 0%, transparent 100%);
opacity: 0.9;
}
.oscar-spotlights {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.oscar-spotlight {
will-change: transform;
position: absolute;
top: 0;
/* MARK: SPOTLIGHT WIDTH CONFIGURATION */
/* To adjust bottom width (spread), change 'width' property (e.g., 20vw for narrow, 40vw for wide). */
/* To adjust top width (origin), modify first two percentages in 'clip-path' (e.g., 48% 0, 52% 0 for a very thin start). */
width: 30vw;
height: 120vh;
background: linear-gradient(to bottom, rgba(255, 215, 0, 0.4) 0%, transparent 80%);
clip-path: polygon(45% 0, 55% 0, 100% 100%, 0 100%);
transform-origin: top center;
animation: spotlight-sweep 12s infinite alternate ease-in-out;
mix-blend-mode: screen;
translate: 0 -10vh;
}
.oscar-flash {
will-change: transform;
position: absolute;
width: 10px;
height: 10px;
background: white;
border-radius: 50%;
box-shadow: 0 0 50px 30px rgba(255, 255, 255, 0.8), 0 0 100px 50px rgba(255, 255, 255, 0.5);
animation: flash-pop 0.2s cubic-bezier(0.1, 0.8, 0.1, 1);
mix-blend-mode: screen;
}
@keyframes spotlight-sweep {
0% { transform: rotate(-30deg); }
100% { transform: rotate(30deg); }
}
@keyframes flash-pop {
0% { transform: scale(0.5); opacity: 1; }
50% { transform: scale(2); opacity: 1; }
100% { transform: scale(3); opacity: 0; }
}

View File

@@ -0,0 +1,94 @@
const config = window.SeasonalsPluginConfig?.Oscar || {};
const oscar = config.EnableOscar !== undefined ? config.EnableOscar : true; // enable/disable oscar
let msgPrinted = false;
function toggleOscar() {
const container = document.querySelector('.oscar-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Oscar hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Oscar visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleOscar);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createOscar(container) {
// Red carpet floor
const carpet = document.createElement('div');
carpet.className = 'oscar-carpet';
// Spotlights
const spotlights = document.createElement('div');
spotlights.className = 'oscar-spotlights';
for (let i = 0; i < 3; i++) {
const spot = document.createElement('div');
spot.className = 'oscar-spotlight';
spot.style.animationDelay = `-${Math.random() * 8}s`;
spot.style.left = `${20 + (i * 30)}%`;
spot.style.top = `${-5 - Math.random() * 15}vh`; // randomize top origin
spotlights.appendChild(spot);
}
container.appendChild(carpet);
container.appendChild(spotlights);
function flashLoop() {
if (!document.body.contains(container)) return; // Kill the loop if container is removed
if (container.style.display === 'none') {
setTimeout(flashLoop, 1000); // Check again later if hidden
return;
}
const flash = document.createElement('div');
flash.className = 'oscar-flash';
flash.style.left = `${Math.random() * 100}%`;
flash.style.top = `${Math.random() * 100}%`;
container.appendChild(flash);
setTimeout(() => flash.remove(), 200);
// Randomize next flash between 200ms and 1500ms
const nextDelay = Math.random() * 1300 + 200;
setTimeout(flashLoop, nextDelay);
}
flashLoop();
}
function initializeOscar() {
if (!oscar) return;
const container = document.querySelector('.oscar-container') || document.createElement("div");
if (!document.querySelector('.oscar-container')) {
container.className = "oscar-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
createOscar(container);
toggleOscar();
}
initializeOscar();

View File

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

View File

@@ -0,0 +1,84 @@
const config = window.SeasonalsPluginConfig?.Pride || {};
const enabled = config.EnablePride !== undefined ? config.EnablePride : true; // enable/disable pride
const elementCount = config.HeartCount !== undefined ? config.HeartCount : 20; // count of heart
const heartSize = config.HeartSize !== undefined ? config.HeartSize : 1.5; // size of hearts
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true; // optionally color the header with pride colors
let msgPrinted = false;
function togglePride() {
const container = document.querySelector('.pride-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Pride hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Pride visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(togglePride);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createElements() {
const container = document.querySelector('.pride-container') || document.createElement('div');
if (!document.querySelector('.pride-container')) {
container.className = 'pride-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
if (colorHeader) {
document.body.classList.add('pride-active');
}
const cleanupObserver = new MutationObserver(() => {
if (!document.querySelector('.pride-container')) {
document.body.classList.remove('pride-active');
}
});
cleanupObserver.observe(document.body, { childList: true });
const heartEmojis = ['❤️', '🧡', '💛', '💚', '💙', '💜'];
for (let i = 0; i < elementCount; i++) {
const el = document.createElement('div');
el.className = 'pride-heart';
el.innerText = heartEmojis[Math.floor(Math.random() * heartEmojis.length)];
el.style.fontSize = `${heartSize}rem`;
el.style.left = `${Math.random() * 100}vw`;
el.style.animationDuration = `${5 + Math.random() * 5}s`;
el.style.animationDelay = `${Math.random() * 5}s`;
el.style.marginLeft = `${(Math.random() - 0.5) * 100}px`;
container.appendChild(el);
}
}
function initializePride() {
if (!enabled) return;
createElements();
togglePride();
}
initializePride();

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(180vh) translateX(-60vh) rotate(20deg); opacity: 0; }
}

View File

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

View File

@@ -0,0 +1,66 @@
.resurrection-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.resurrection-symbol {
position: fixed;
z-index: 15;
top: 0;
translate: 0 -15vh;
user-select: none;
cursor: default;
animation-name: resurrection-fall;
animation-timing-function: linear;
animation-iteration-count: infinite;
will-change: transform;
}
.resurrection-sway-wrapper {
will-change: transform;
animation-name: resurrection-sway;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
.resurrection-symbol img {
z-index: 15;
height: auto;
width: 56px;
opacity: 0.95;
filter: drop-shadow(0 0 8px rgba(255, 215, 130, 0.5));
}
@media (max-width: 768px) {
.resurrection-symbol img {
width: 42px;
}
}
@keyframes resurrection-fall {
0% {
transform: translate3d(0, -15vh, 0);
}
100% {
transform: translate3d(0, 105vh, 0);
}
}
@keyframes resurrection-sway {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(65px);
}

View File

@@ -0,0 +1,120 @@
const config = window.SeasonalsPluginConfig?.Resurrection || {};
const enableResurrection = config.EnableResurrection !== undefined ? config.EnableResurrection : true; // enable/disable resurrection
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 12; // count of symbols
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 5; // count of symbols on mobile
let animationEnabled = true;
let statusLogged = false;
const images = [
'../Seasonals/Resources/resurrection_images/crosses.png',
'../Seasonals/Resources/resurrection_images/palm-branch.png',
'../Seasonals/Resources/resurrection_images/draped-cross.png',
'../Seasonals/Resources/resurrection_images/empty-tomb.png',
'../Seasonals/Resources/resurrection_images/he-is-risen.png',
'../Seasonals/Resources/resurrection_images/crown-of-thorns.png',
'../Seasonals/Resources/resurrection_images/risen-lord.png',
'../Seasonals/Resources/resurrection_images/dove.png'
];
function toggleResurrection() {
const container = document.querySelector('.resurrection-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');
animationEnabled = !(videoPlayer || trailerPlayer || isDashboard || hasUserMenu);
container.style.display = animationEnabled ? 'block' : 'none';
if (!animationEnabled && !statusLogged) {
console.log('Resurrection hidden');
statusLogged = true;
} else if (animationEnabled && statusLogged) {
console.log('Resurrection visible');
statusLogged = false;
}
}
const observer = new MutationObserver(toggleResurrection);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createSymbol(imageSrc, leftPercent, delaySeconds) {
const symbol = document.createElement('div');
symbol.className = 'resurrection-symbol';
const swayWrapper = document.createElement('div');
swayWrapper.className = 'resurrection-sway-wrapper';
const img = document.createElement('img');
img.src = imageSrc;
img.alt = '';
symbol.style.left = `${leftPercent}%`;
symbol.style.animationDelay = `${delaySeconds}s`;
if (enableDifferentDuration) {
const fallDuration = Math.random() * 7 + 7;
const swayDuration = Math.random() * 4 + 2;
symbol.style.animationDuration = `${fallDuration}s`;
swayWrapper.style.animationDuration = `${swayDuration}s`;
}
swayWrapper.style.animationDelay = `${Math.random() * 3}s`;
swayWrapper.appendChild(img);
symbol.appendChild(swayWrapper);
return symbol;
}
function addSymbols(count) {
const container = document.querySelector('.resurrection-container');
if (!container) return;
for (let i = 0; i < count; i++) {
const imageSrc = images[Math.floor(Math.random() * images.length)];
const left = Math.random() * 100;
const delay = -(Math.random() * 12);
container.appendChild(createSymbol(imageSrc, left, delay));
}
}
function initResurrection(count) {
let container = document.querySelector('.resurrection-container');
if (!container) {
container = document.createElement('div');
container.className = 'resurrection-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
// Place one of each of the 8 provided resurrection images first.
images.forEach((imageSrc, index) => {
const left = (index + 1) * (100 / (images.length + 1));
const delay = -(Math.random() * 8);
container.appendChild(createSymbol(imageSrc, left, delay));
});
const extraCount = Math.max(count - images.length, 0);
addSymbols(extraCount);
}
function initializeResurrection() {
if (!enableResurrection) return;
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? symbolCount : symbolCountMobile;
initResurrection(count);
toggleResurrection();
}
initializeResurrection();

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

View File

@@ -1,15 +1,30 @@
const config = window.SeasonalsPluginConfig?.Santa || {};
const santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.SnowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
const santaSpeed = config.SantaSpeed || 10; // speed of santa in seconds (recommended values: 5-15)
const santaSpeedMobile = config.SantaSpeedMobile || 8; // speed of santa on mobile devices in seconds
const maxSantaRestTime = config.MaxSantaRestTime || 8; // maximum time santa rests in seconds
const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa rests in seconds
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
const minPresentFallSpeed = config.MinPresentFallSpeed || 2; // minimum speed of falling presents in seconds
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
const snowFallSpeed = config.SnowFallSpeed !== undefined ? config.SnowFallSpeed : 3; // speed of snowfall
const santaSpeed = config.SantaSpeed !== undefined ? config.SantaSpeed : 10; // speed of santa in seconds
const santaSpeedMobile = config.SantaSpeedMobile !== undefined ? config.SantaSpeedMobile : 8; // speed of santa on mobile devices in seconds
const maxSantaRestTime = config.MaxSantaRestTime !== undefined ? config.MaxSantaRestTime : 8; // maximum time santa rests in seconds
const minSantaRestTime = config.MinSantaRestTime !== undefined ? config.MinSantaRestTime : 3; // minimum time santa rests in seconds
const maxPresentFallSpeed = config.MaxPresentFallSpeed !== undefined ? config.MaxPresentFallSpeed : 5; // maximum speed of falling presents in seconds
const minPresentFallSpeed = config.MinPresentFallSpeed !== undefined ? config.MinPresentFallSpeed : 2; // minimum speed of falling presents in seconds
// credits: flaticon.com
const presentImages = [
'../Seasonals/Resources/santa_images/gift1.png',
'../Seasonals/Resources/santa_images/gift2.png',
'../Seasonals/Resources/santa_images/gift3.png',
'../Seasonals/Resources/santa_images/gift4.png',
'../Seasonals/Resources/santa_images/gift5.png',
'../Seasonals/Resources/santa_images/gift6.png',
'../Seasonals/Resources/santa_images/gift7.png',
'../Seasonals/Resources/santa_images/gift8.png',
];
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
const santaImage = '../Seasonals/Resources/santa_images/santa.gif';
let msgPrinted = false; // flag to prevent multiple console messages
let isMobile = false; // flag to detect mobile devices
@@ -52,12 +67,10 @@ function toggleSnowfall() {
// observe changes in the DOM
const observer = new MutationObserver(toggleSnowfall);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
let resizeObserver; // Observer for resize events
@@ -179,22 +192,6 @@ function updateSnowflakes() {
});
}
// credits: flaticon.com
const presentImages = [
'/Seasonals/Resources/santa_images/gift1.png',
'/Seasonals/Resources/santa_images/gift2.png',
'/Seasonals/Resources/santa_images/gift3.png',
'/Seasonals/Resources/santa_images/gift4.png',
'/Seasonals/Resources/santa_images/gift5.png',
'/Seasonals/Resources/santa_images/gift6.png',
'/Seasonals/Resources/santa_images/gift7.png',
'/Seasonals/Resources/santa_images/gift8.png',
];
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
const santaImage = '/Seasonals/Resources/santa_images/santa.gif';
function createSantaElement() {
const santa = document.createElement('img');
santa.src = santaImage;
@@ -241,7 +238,7 @@ function animateSanta() {
function startAnimation() {
const santaHeight = santa.offsetHeight;
if (santaHeight === 0) {
setTimeout(startAnimation, 100);
setTimeout(() => { if (document.body.contains(santa)) startAnimation(); }, 100);
return;
}
// console.log('Santa height: ', santaHeight);
@@ -286,7 +283,7 @@ function animateSanta() {
animationFrameIdSanta = requestAnimationFrame(move);
} else {
const pause = Math.random() * ((maxSantaRestTime - minSantaRestTime) * 1000) + minSantaRestTime * 1000;
setTimeout(animateSanta, pause);
setTimeout(() => { if (document.body.contains(santa)) animateSanta(); }, pause);
}
}
@@ -313,8 +310,8 @@ function initializeSanta() {
}
const container = document.querySelector('.santa-container');
if (container) {
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
if (isMobile) { // lower count of snowflakes on mobile devices
isMobile = true;
console.log('Mobile device detected. Reducing snowflakes count.');
snowflakesCount = snowflakesCountMobile;

View File

@@ -1,260 +1,246 @@
// theme-configs.js
/*
* Seasonals Plugin (Client Side Manager Logic)
*/
// theme configurations
const themeConfigs = {
const ThemeConfigs = {
snowflakes: {
css: '/Seasonals/Resources/snowflakes.css',
js: '/Seasonals/Resources/snowflakes.js',
css: '../Seasonals/Resources/snowflakes.css',
js: '../Seasonals/Resources/snowflakes.js',
containerClass: 'snowflakes'
},
snowfall: {
css: '/Seasonals/Resources/snowfall.css',
js: '/Seasonals/Resources/snowfall.js',
css: '../Seasonals/Resources/snowfall.css',
js: '../Seasonals/Resources/snowfall.js',
containerClass: 'snowfall-container'
},
snowstorm: {
css: '/Seasonals/Resources/snowstorm.css',
js: '/Seasonals/Resources/snowstorm.js',
css: '../Seasonals/Resources/snowstorm.css',
js: '../Seasonals/Resources/snowstorm.js',
containerClass: 'snowstorm-container'
},
fireworks: {
css: '/Seasonals/Resources/fireworks.css',
js: '/Seasonals/Resources/fireworks.js',
css: '../Seasonals/Resources/fireworks.css',
js: '../Seasonals/Resources/fireworks.js',
containerClass: 'fireworks'
},
halloween: {
css: '/Seasonals/Resources/halloween.css',
js: '/Seasonals/Resources/halloween.js',
css: '../Seasonals/Resources/halloween.css',
js: '../Seasonals/Resources/halloween.js',
containerClass: 'halloween-container'
},
hearts: {
css: '/Seasonals/Resources/hearts.css',
js: '/Seasonals/Resources/hearts.js',
css: '../Seasonals/Resources/hearts.css',
js: '../Seasonals/Resources/hearts.js',
containerClass: 'hearts-container'
},
christmas: {
css: '/Seasonals/Resources/christmas.css',
js: '/Seasonals/Resources/christmas.js',
css: '../Seasonals/Resources/christmas.css',
js: '../Seasonals/Resources/christmas.js',
containerClass: 'christmas-container'
},
santa: {
css: '/Seasonals/Resources/santa.css',
js: '/Seasonals/Resources/santa.js',
css: '../Seasonals/Resources/santa.css',
js: '../Seasonals/Resources/santa.js',
containerClass: 'santa-container'
},
autumn: {
css: '/Seasonals/Resources/autumn.css',
js: '/Seasonals/Resources/autumn.js',
css: '../Seasonals/Resources/autumn.css',
js: '../Seasonals/Resources/autumn.js',
containerClass: 'autumn-container'
},
easter: {
css: '/Seasonals/Resources/easter.css',
js: '/Seasonals/Resources/easter.js',
css: '../Seasonals/Resources/easter.css',
js: '../Seasonals/Resources/easter.js',
containerClass: 'easter-container'
},
resurrection: {
css: '../Seasonals/Resources/resurrection.css',
js: '../Seasonals/Resources/resurrection.js',
containerClass: 'resurrection-container'
},
summer: {
css: '/Seasonals/Resources/summer.css',
js: '/Seasonals/Resources/summer.js',
css: '../Seasonals/Resources/summer.css',
js: '../Seasonals/Resources/summer.js',
containerClass: 'summer-container'
},
spring: {
css: '/Seasonals/Resources/spring.css',
js: '/Seasonals/Resources/spring.js',
css: '../Seasonals/Resources/spring.css',
js: '../Seasonals/Resources/spring.js',
containerClass: 'spring-container'
},
carnival: {
css: '../Seasonals/Resources/carnival.css',
js: '../Seasonals/Resources/carnival.js',
containerClass: 'carnival-container'
},
cherryblossom: {
css: '../Seasonals/Resources/cherryblossom.css',
js: '../Seasonals/Resources/cherryblossom.js',
containerClass: 'cherryblossom-container'
},
matrix: {
css: '../Seasonals/Resources/matrix.css',
js: '../Seasonals/Resources/matrix.js',
containerClass: 'matrix-container'
},
eurovision: {
css: '../Seasonals/Resources/eurovision.css',
js: '../Seasonals/Resources/eurovision.js',
containerClass: 'eurovision-container'
},
storm: {
css: '../Seasonals/Resources/storm.css',
js: '../Seasonals/Resources/storm.js',
containerClass: 'storm-container'
},
pride: {
css: '../Seasonals/Resources/pride.css',
js: '../Seasonals/Resources/pride.js',
containerClass: 'pride-container'
},
rain: {
css: '../Seasonals/Resources/rain.css',
js: '../Seasonals/Resources/rain.js',
containerClass: 'rain-container'
},
earthday: {
css: '../Seasonals/Resources/earthday.css',
js: '../Seasonals/Resources/earthday.js',
containerClass: 'earthday-container'
},
frost: {
css: '../Seasonals/Resources/frost.css',
js: '../Seasonals/Resources/frost.js',
containerClass: 'frost-container'
},
filmnoir: {
css: '../Seasonals/Resources/filmnoir.css',
js: '../Seasonals/Resources/filmnoir.js',
containerClass: 'filmnoir-container'
},
oscar: {
css: '../Seasonals/Resources/oscar.css',
js: '../Seasonals/Resources/oscar.js',
containerClass: 'oscar-container'
},
marioday: {
css: '../Seasonals/Resources/marioday.css',
js: '../Seasonals/Resources/marioday.js',
containerClass: 'marioday-container'
},
starwars: {
css: '../Seasonals/Resources/starwars.css',
js: '../Seasonals/Resources/starwars.js',
containerClass: 'starwars-container'
},
oktoberfest: {
css: '../Seasonals/Resources/oktoberfest.css',
js: '../Seasonals/Resources/oktoberfest.js',
containerClass: 'oktoberfest-container'
},
friday13: {
css: '../Seasonals/Resources/friday13.css',
js: '../Seasonals/Resources/friday13.js',
containerClass: 'friday13-container'
},
eid: {
css: '../Seasonals/Resources/eid.css',
js: '../Seasonals/Resources/eid.js',
containerClass: 'eid-container'
},
spooky: {
css: '../Seasonals/Resources/spooky.css',
js: '../Seasonals/Resources/spooky.js',
containerClass: 'spooky-container'
},
sports: {
css: '../Seasonals/Resources/sports.css',
js: '../Seasonals/Resources/sports.js',
containerClass: 'sports-container'
},
olympia: {
css: '../Seasonals/Resources/olympia.css',
js: '../Seasonals/Resources/olympia.js',
containerClass: 'olympia-container'
},
space: {
css: '../Seasonals/Resources/space.css',
js: '../Seasonals/Resources/space.js',
containerClass: 'space-container'
},
underwater: {
css: '../Seasonals/Resources/underwater.css',
js: '../Seasonals/Resources/underwater.js',
containerClass: 'underwater-container'
},
birthday: {
css: '../Seasonals/Resources/birthday.css',
js: '../Seasonals/Resources/birthday.js',
containerClass: 'birthday-container'
},
none: {
containerClass: 'none'
},
};
// determine current theme based on the current month
function determineCurrentTheme() {
const date = new Date();
const month = date.getMonth(); // 0-11
const day = date.getDate(); // 1-31
const SeasonalSettingsManager = {
initialized: false,
config: null,
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5
init(config) {
if (this.initialized) return;
this.config = config;
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27
if (month === 11) return 'snowflakes'; // snowflakes december
if (month === 0 || month === 1) return 'snowfall'; // snow january, february
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25
//NOT IMPLEMENTED YET
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
//NOT IMPLEMENTED YET
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
return 'none'; // Fallback (nothing)
}
// helper to resolve paths for local testing vs production
function resolvePath(path) {
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return path.replace('/Seasonals/Resources/', './');
// Only inject settings if enabled on server by admin
if (this.config && this.config.EnableClientSideToggle !== false) {
this.injectSettingsIcon();
this.initialized = true;
console.log("Seasonals: Client-Side Settings Manager initialized.");
}
return path;
}
},
// load theme csss
function loadThemeCSS(cssPath) {
if (!cssPath) return;
const link = document.createElement('link');
link.rel = 'stylesheet';
// link.href = resolvePath(cssPath);
link.href = cssPath;
link.onerror = () => {
console.error(`Failed to load CSS: ${cssPath}`);
};
document.body.appendChild(link);
console.log(`CSS file "${cssPath}" loaded.`);
}
// load theme js
function loadThemeJS(jsPath) {
if (!jsPath) return;
const script = document.createElement('script');
script.src = jsPath;
// script.src = resolvePath(jsPath);
script.defer = true;
script.onerror = () => {
console.error(`Failed to load JS: ${jsPath}`);
};
document.body.appendChild(script);
console.log(`JS file "${jsPath}" loaded.`);
}
// update theme container class name
function updateThemeContainer(containerClass) {
// Create container if it doesn't exist
let container = document.querySelector('.seasonals-container');
if (!container) {
container = document.createElement('div');
container.className = 'seasonals-container';
document.body.appendChild(container);
}
container.className = `seasonals-container ${containerClass}`;
console.log(`Seasonals-Container class updated to "${containerClass}".`);
}
// function removeSelf() {
// const script = document.currentScript;
// if (script) script.parentNode.removeChild(script);
// console.log('External script removed:', script);
// }
// initialize theme
async function initializeTheme() {
// Check local user preference
const isEnabled = getSavedSetting('seasonals-enabled', 'true') === 'true';
if (!isEnabled) {
console.log('Seasonals disabled by user preference.');
return;
}
const forcedTheme = getSavedSetting('seasonals-theme', 'auto');
let automateThemeSelection = true;
let defaultTheme = 'none';
try {
const response = await fetch('/Seasonals/Config');
if (response.ok) {
const config = await response.json();
automateThemeSelection = config.AutomateSeasonSelection;
defaultTheme = config.SelectedSeason;
window.SeasonalsPluginConfig = config;
console.log('Seasonals Config loaded:', config);
} else {
console.error('Failed to fetch Seasonals config');
}
} catch (error) {
console.error('Error fetching Seasonals config:', error);
}
let currentTheme;
if (forcedTheme !== 'auto') {
currentTheme = forcedTheme;
console.log(`User forced theme: ${currentTheme}`);
} else if (automateThemeSelection === false) {
currentTheme = defaultTheme;
} else {
currentTheme = determineCurrentTheme();
}
console.log(`Selected theme: ${currentTheme}`);
if (!currentTheme || currentTheme === 'none') {
console.log('No theme selected.');
return;
}
const theme = themeConfigs[currentTheme];
if (!theme) {
console.error(`Theme "${currentTheme}" not found.`);
return;
}
updateThemeContainer(theme.containerClass);
if (theme.css) loadThemeCSS(theme.css);
if (theme.js) loadThemeJS(theme.js);
console.log(`Theme "${currentTheme}" loaded.`);
}
initializeTheme();
// User UI Seasonal Settings
function getSavedSetting(key, defaultValue) {
const value = localStorage.getItem(key);
getSetting(key, defaultValue) {
const value = localStorage.getItem(`seasonals-${key}`);
return value !== null ? value : defaultValue;
}
},
function setSavedSetting(key, value) {
localStorage.setItem(key, value);
}
setSetting(key, value) {
localStorage.setItem(`seasonals-${key}`, value);
},
function createSettingsIcon() {
createIcon() {
const button = document.createElement('button');
button.type = 'button';
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
button.title = 'Seasonal Settings';
button.innerHTML = '<span class="material-icons">ac_unit</span>';
// button.innerHTML = '<span class="material-icons">ac_unit</span>';
button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
button.style.verticalAlign = 'middle';
button.addEventListener('click', (e) => {
e.stopPropagation();
toggleSettingsPopup(button);
this.toggleSettingsPopup(button);
});
return button;
}
},
function createSettingsPopup(anchorElement) {
injectSettingsIcon() {
const observer = new MutationObserver((mutations, obs) => {
const headerRight = document.querySelector('.headerRight');
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
const icon = this.createIcon();
headerRight.prepend(icon);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
},
createPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) existing.remove();
@@ -265,63 +251,75 @@ function createSettingsPopup(anchorElement) {
position: 'fixed',
zIndex: '10000',
backgroundColor: '#202020',
padding: '1.5em',
borderRadius: '1em',
padding: '1em',
borderRadius: '0.3em',
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
minWidth: '250px',
minWidth: '200px',
color: '#fff',
maxWidth: '300px'
maxWidth: '250px'
});
const rect = anchorElement.getBoundingClientRect();
popup.style.top = `${rect.bottom + 10}px`;
popup.style.right = `${window.innerWidth - rect.right}px`;
popup.innerHTML = `
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 1.5em;">
// Positioning logic
let rightPos = window.innerWidth - rect.right;
if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
popup.style.right = '1rem';
popup.style.left = 'auto';
} else {
popup.style.right = `${rightPos}px`;
popup.style.left = 'auto';
}
popup.style.top = `${rect.bottom + 10}px`;
// Popup HTML
let html = `
<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
<label class="emby-checkbox-label">
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
<span class="checkboxLabel">Enable Seasonals</span>
</label>
</div>
<div class="selectContainer">
<div class="selectContainer" style="margin-bottom: 0.5em;">
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
<select is="emby-select" id="seasonal-theme-select" class="emby-select-withcolor emby-select" style="width: 100%; padding: 0.5em; background-color: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.1); color: inherit; border-radius: 4px;">
<option value="auto">Auto (Date Based)</option>
<select id="seasonal-theme-select" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
<option value="auto">Server-Side</option>
</select>
</div>
`;
popup.innerHTML = html;
// Populate Select Options
const themeSelect = popup.querySelector('#seasonal-theme-select');
Object.keys(themeConfigs).forEach(key => {
Object.keys(ThemeConfigs).forEach(key => {
if (key === 'none') return;
const option = document.createElement('option');
option.value = key;
// Capitalize first letter
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
themeSelect.appendChild(option);
});
// Set Initial Values
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
enabledCheckbox.checked = getSavedSetting('seasonals-enabled', 'true') === 'true';
themeSelect.value = getSavedSetting('seasonals-theme', 'auto');
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
themeSelect.value = this.getSetting('theme', 'auto');
// Event Listeners
enabledCheckbox.addEventListener('change', (e) => {
setSavedSetting('seasonals-enabled', e.target.checked);
this.setSetting('enabled', e.target.checked);
location.reload();
});
themeSelect.addEventListener('change', (e) => {
setSavedSetting('seasonals-theme', e.target.value);
this.setSetting('theme', e.target.value);
location.reload();
});
// Close on click outside
// Close on outside click
const closeHandler = (e) => {
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
popup.remove();
@@ -331,36 +329,185 @@ function createSettingsPopup(anchorElement) {
setTimeout(() => document.addEventListener('click', closeHandler), 0);
document.body.appendChild(popup);
}
},
function toggleSettingsPopup(anchorElement) {
toggleSettingsPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) {
existing.remove();
} else {
createSettingsPopup(anchorElement);
this.createPopup(anchorElement);
}
}
}
};
function injectSettingsIcon() {
const observer = new MutationObserver((mutations, obs) => {
// Check if admin has enabled this feature
if (window.SeasonalsPluginConfig && window.SeasonalsPluginConfig.EnableClientSideToggle === false) {
const SeasonalsManager = {
config: null,
async init() {
// Fetch Config
try {
const response = await fetch('../Seasonals/Config');
if (response.ok) {
this.config = await response.json();
window.SeasonalsPluginConfig = this.config;
if (this.config.IsEnabled === false) {
console.log('Seasonals: Plugin is disabled globally.');
return;
}
const headerRight = document.querySelector('.headerRight');
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
const icon = createSettingsIcon();
headerRight.prepend(icon);
// obs.disconnect();
console.log('Seasonals: Seasonals Config loaded:', this.config);
}
} catch (error) {
console.error('Seasonals: Error fetching Seasonals config:', error);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Initialize Settings UI
SeasonalSettingsManager.init(this.config);
injectSettingsIcon();
// User Preference Check
const isEnabled = SeasonalSettingsManager.getSetting('enabled', 'true') === 'true';
if (!isEnabled) {
console.log('Seasonals: Disabled by user preference.');
return;
}
// Determine Theme
const themeName = this.selectTheme();
console.log(`Seasonals: Selected theme: ${themeName}`);
if (!themeName || themeName === 'none') {
return;
}
// Apply Theme
this.applyTheme(themeName);
},
selectTheme() {
// Check local override
const forcedTheme = SeasonalSettingsManager.getSetting('theme', 'auto');
if (forcedTheme !== 'auto') {
console.log(`Seasonals: User forced theme: ${forcedTheme}`);
return forcedTheme;
}
const automate = this.config ? this.config.AutomateSeasonSelection : true;
const defaultTheme = this.config ? this.config.SelectedSeason : 'none';
if (!automate) {
return defaultTheme;
}
return this.determineCurrentThemeDate();
},
determineCurrentThemeDate() {
var rules = [];
try {
rules = JSON.parse(this.config.SeasonalRules || "[]");
} catch (e) {
console.error("Seasonals: Error parsing SeasonalRules", e);
}
if (rules.length === 0) {
// Fallback to empty/none if no rules are defined (though default should exist)
console.log("Seasonals: No auto-selection rules found.");
return 'none';
}
const date = new Date();
const month = date.getMonth() + 1; // 1-12
const day = date.getDate(); // 1-31
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
return rule.Theme;
}
}
return 'none'; // No rule matched
},
isDateInRange: function(day, month, startDay, startMonth, endDay, endMonth) {
if (startMonth > endMonth) {
// Wrapping year (e.g. Dec to Jan)
return this.isDateAfterOrEqual(day, month, startDay, startMonth) ||
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
} else {
// Normal range
return this.isDateAfterOrEqual(day, month, startDay, startMonth) &&
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
}
},
isDateAfterOrEqual: function(day, month, targetDay, targetMonth) {
if (month > targetMonth) return true;
if (month === targetMonth && day >= targetDay) return true;
return false;
},
isDateBeforeOrEqual: function(day, month, targetDay, targetMonth) {
if (month < targetMonth) return true;
if (month === targetMonth && day <= targetDay) return true;
return false;
},
applyTheme(themeName) {
const theme = ThemeConfigs[themeName];
if (!theme) {
console.error(`Seasonals: Theme "${themeName}" not found.`);
return;
}
this.updateThemeContainer(theme.containerClass);
if (theme.css) this.loadResource('css', theme.css);
if (theme.js) this.loadResource('js', theme.js);
console.log(`Seasonals: Theme "${themeName}" applied.`);
},
updateThemeContainer(containerClass) {
let container = document.querySelector('.seasonals-container');
if (!container) {
container = document.createElement('div');
container.className = 'seasonals-container';
document.body.appendChild(container);
}
container.className = `seasonals-container ${containerClass}`;
},
// helper to resolve paths for local testing vs production
resolvePath(path) {
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return path.replace('/Seasonals/Resources/', './');
}
return path;
},
loadResource(type, path) {
if (!path) return;
if (type === 'css') {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = path;
// link.href = resolvePath(cssPath);
link.onerror = () => console.error(`Seasonals: Failed to load CSS: ${path}`);
document.body.appendChild(link);
} else if (type === 'js') {
const script = document.createElement('script');
script.src = path;
// script.src = resolvePath(jsPath);
script.defer = true;
script.onerror = () => console.error(`Seasonals: Failed to load JS: ${path}`);
document.body.appendChild(script);
}
}
};
SeasonalsManager.init();

View File

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

View File

@@ -1,9 +1,9 @@
const config = window.SeasonalsPluginConfig?.Snowfall || {};
const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.Speed || 3; // speed of snowfall (recommended values: 0-5)
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 3; // speed of snowfall
let msgPrinted = false; // flag to prevent multiple console messages
@@ -47,12 +47,10 @@ function toggleSnowfall() {
// observe changes in the DOM
const observer = new MutationObserver(toggleSnowfall);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
let resizeObserver; // Observer for resize events
@@ -183,8 +181,8 @@ function initializeSnowfall() {
}
const container = document.querySelector('.snowfall-container');
if (container) {
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
if (isMobile) { // lower count of snowflakes on mobile devices
console.log('Mobile device detected. Reducing snowflakes count.');
snowflakesCount = snowflakesCountMobile;
}

View File

@@ -8,60 +8,36 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.snowflake {
position: fixed;
z-index: 15;
top: -10%;
top: 0;
will-change: transform;
translate: 0 -10vh;
font-size: 1em;
color: #fff;
font-family: Arial, sans-serif;
text-shadow: 0 0 5px #000;
user-select: none;
-webkit-user-select: none;
cursor: default;
-webkit-animation-name: heart-fall, heart-shake;
-webkit-animation-duration: 12s, 3s;
-webkit-animation-timing-function: linear, ease-in-out;
-webkit-animation-iteration-count: infinite, infinite;
animation-name: snowflakes-fall, snowflakes-shake;
animation-duration: 12s, 3s;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
}
@-webkit-keyframes snowflakes-fall {
0% {
top: -10%;
}
100% {
top: 100%;
}
}
@-webkit-keyframes snowflakes-shake {
0%,
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
50% {
-webkit-transform: translateX(80px);
transform: translateX(80px);
}
}
@keyframes snowflakes-fall {
0% {
top: -10%;
translate: 0 -10vh;
}
100% {
top: 100%;
translate: 0 110vh;
}
}
@@ -76,63 +52,3 @@
transform: translateX(80px);
}
}
.snowflake:nth-of-type(0) {
left: 0%;
animation-delay: 0s, 0s;
}
.snowflake:nth-of-type(1) {
left: 10%;
animation-delay: 1s, 1s;
}
.snowflake:nth-of-type(2) {
left: 20%;
animation-delay: 6s, 0.5s;
}
.snowflake:nth-of-type(3) {
left: 30%;
animation-delay: 4s, 2s;
}
.snowflake:nth-of-type(4) {
left: 40%;
animation-delay: 2s, 2s;
}
.snowflake:nth-of-type(5) {
left: 50%;
animation-delay: 8s, 3s;
}
.snowflake:nth-of-type(6) {
left: 60%;
animation-delay: 6s, 2s;
}
.snowflake:nth-of-type(7) {
left: 70%;
animation-delay: 2.5s, 1s;
}
.snowflake:nth-of-type(8) {
left: 80%;
animation-delay: 1s, 0s;
}
.snowflake:nth-of-type(9) {
left: 90%;
animation-delay: 3s, 1.5s;
}
.snowflake:nth-of-type(10) {
left: 25%;
animation-delay: 2s, 0s;
}
.snowflake:nth-of-type(11) {
left: 65%;
animation-delay: 4s, 2.5s;
}

View File

@@ -1,12 +1,13 @@
const config = window.SeasonalsPluginConfig?.Snowflakes || {};
const snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
const randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random Snowflakes
const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable colored snowflakes
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
const snowflakeCount = config.SnowflakeCount || 25; // count of random extra snowflakes
const snowflakeCount = config.SnowflakeCount !== undefined ? config.SnowflakeCount : 25; // count of snowflakes
const snowflakeCountMobile = config.SnowflakeCountMobile !== undefined ? config.SnowflakeCountMobile : 10; // count of snowflakes on mobile
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable/disable colored snowflakes
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
let msgPrinted = false; // flag to prevent multiple console messages
@@ -38,22 +39,22 @@ function toggleSnowflakes() {
// observe changes in the DOM
const observer = new MutationObserver(toggleSnowflakes);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
function addRandomSnowflakes(count) {
const snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
if (!snowflakeContainer) return; // exit if snowflake container is not found
function initSnowflakes(count) {
let snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
if (!snowflakeContainer) {
snowflakeContainer = document.createElement("div");
snowflakeContainer.className = "snowflakes";
snowflakeContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(snowflakeContainer);
}
console.log('Adding random snowflakes');
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
console.log('Adding snowflakes');
for (let i = 0; i < count; i++) {
// create a new snowflake element
@@ -69,8 +70,8 @@ function addRandomSnowflakes(count) {
// set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 8; // delay (0s to 8s)
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
const randomAnimationDelay = -(Math.random() * 14); // delay (-14s to 0s)
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
// apply styles
snowflake.style.left = `${randomLeft}%`;
@@ -86,49 +87,18 @@ function addRandomSnowflakes(count) {
// add the snowflake to the container
snowflakeContainer.appendChild(snowflake);
}
console.log('Random snowflakes added');
console.log('Snowflakes added');
}
// initialize standard snowflakes
function initSnowflakes() {
const snowflakesContainer = document.querySelector('.snowflakes') || document.createElement("div");
if (!document.querySelector('.snowflakes')) {
snowflakesContainer.className = "snowflakes";
snowflakesContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(snowflakesContainer);
}
// Array of snowflake characters
const snowflakeSymbols = ['❅', '❆'];
// create the 12 standard snowflakes
for (let i = 0; i < 12; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'snowflake';
snowflake.textContent = snowflakeSymbols[i % 2]; // change between ❅ and ❆
// set random animation duration
if (enableDiffrentDuration) {
const randomAnimationDuration = Math.random() * 14 + 10; // delay (10s to 14s)
const randomAnimationDuration2 = Math.random() * 5 + 3; // delay (3s to 5s)
snowflake.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
}
snowflakesContainer.appendChild(snowflake);
}
}
// initialize snowflakes and add random snowflakes
// initialize snowflakes
function initializeSnowflakes() {
if (!snowflakes) return; // exit if snowflakes are disabled
initSnowflakes();
toggleSnowflakes();
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (randomSnowflakes && (screenWidth > 768 || randomSnowflakesMobile)) { // add random snowflakes only on larger screens, unless enabled for mobile devices
addRandomSnowflakes(snowflakeCount);
}
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
const count = !isMobile ? snowflakeCount : snowflakeCountMobile;
initSnowflakes(count);
toggleSnowflakes();
}
initializeSnowflakes();

View File

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

View File

@@ -1,11 +1,11 @@
const config = window.SeasonalsPluginConfig?.Snowstorm || {};
const snowstorm = config.enableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.Speed || 6; // speed of snowfall (recommended values: 4-8)
const horizontalWind = config.HorizontalWind || 4; // horizontal wind speed (recommended value: 4)
const verticalVariation = config.VerticalVariation || 2; // vertical variation (recommended value: 2)
const snowstorm = config.EnableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 6; // speed of snowstorm
const horizontalWind = config.HorizontalWind !== undefined ? config.HorizontalWind : 4; // horizontal wind strength
const verticalVariation = config.VerticalVariation !== undefined ? config.VerticalVariation : 2; // vertical variation
let msgPrinted = false; // flag to prevent multiple console messages
@@ -49,12 +49,10 @@ function toggleSnowstorm() {
// observe changes in the DOM
const observer = new MutationObserver(toggleSnowstorm);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
childList: true,
subtree: true,
attributes: true
});
let resizeObserver; // Observer for resize events
@@ -186,8 +184,8 @@ function initializeSnowstorm() {
}
const container = document.querySelector('.snowstorm-container');
if (container) {
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
if (isMobile) { // lower count of snowflakes on mobile devices
console.log('Mobile device detected. Reducing snowflakes count.');
snowflakesCount = snowflakesCountMobile;
}

View File

@@ -0,0 +1,101 @@
.space-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 10;
overflow: hidden;
contain: strict;
}
.space-bg-glow {
will-change: transform;
position: absolute;
top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(circle at 70% 30%, rgba(138, 43, 226, 0.15), transparent 60%),
radial-gradient(circle at 20% 80%, rgba(65, 105, 225, 0.15), transparent 50%);
pointer-events: none;
z-index: 10;
animation: space-nebula-pulse 10s ease-in-out infinite alternate;
}
@keyframes space-nebula-pulse {
0% { opacity: 0.6; }
100% { opacity: 1; }
}
.space-starfield {
position: absolute;
top: 0; left: 0; width: 100vw; height: 100vh;
background: transparent;
z-index: 11;
}
.space-shooting-star {
will-change: opacity;
position: absolute;
width: 250px;
height: 3px;
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
border-radius: 50%;
animation: space-shoot 25s linear infinite;
opacity: 0;
z-index: 12;
}
@keyframes space-shoot {
0% { transform: rotate(var(--shoot-angle)) translateX(0); opacity: 0; }
5% { opacity: 1; }
35% { opacity: 1; }
40% { transform: rotate(var(--shoot-angle)) translateX(var(--shoot-distance)); opacity: 0; }
100% { transform: rotate(var(--shoot-angle)) translateX(var(--shoot-distance)); opacity: 0; }
}
.space-symbol {
position: absolute;
animation-timing-function: linear;
animation-iteration-count: infinite;
font-size: 3rem;
opacity: 0.85;
z-index: 20;
}
.space-symbol img {
will-change: transform;
width: 6vh;
height: auto;
max-width: 60px;
object-fit: contain;
animation: space-slow-spin var(--rot-dur, 20s) linear infinite;
}
/* Specific elements scaling */
.space-planet img { width: 8vh; max-width: 80px; }
.space-astronaut img { width: 10vh; max-width: 100px; }
.space-satellite img { width: 12vh; max-width: 120px; }
.space-iss img { width: 25vh; max-width: 180px; }
.space-rocket img { width: 12vh; max-width: 120px; }
@keyframes space-drift-right {
0% { transform: translateX(-10vw) translateY(0) scaleX(-1); }
50% { transform: translateX(60vw) translateY(-30vh) scaleX(-1); }
100% { transform: translateX(140vw) translateY(0) scaleX(-1); }
}
@keyframes space-drift-left {
0% { transform: translateX(10vw) translateY(0); }
50% { transform: translateX(-60vw) translateY(30vh); }
100% { transform: translateX(-140vw) translateY(0); }
}
@keyframes space-slow-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes space-star-drift {
from { transform: translateY(0); }
to { transform: translateY(-100vh); }
}

View File

@@ -0,0 +1,285 @@
const config = window.SeasonalsPluginConfig?.Space || {};
const space = config.EnableSpace !== undefined ? config.EnableSpace : true; // enable/disable space
const planetCountConf = config.PlanetCount !== undefined ? config.PlanetCount : 6; // count of planets
const astronautCountConf = config.AstronautCount !== undefined ? config.AstronautCount : 1; // count of astronaut
const satelliteCountConf = config.SatelliteCount !== undefined ? config.SatelliteCount : 4; // count of satellite
const issCountConf = config.IssCount !== undefined ? config.IssCount : 1; // count of iss
const rocketCountConf = config.RocketCount !== undefined ? config.RocketCount : 1; // count of rocket/space shuttle
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 2; // Devisor to reduce number of objects on mobile
// Credit: https://lottiefiles.com/free-animation/astronaut-63lcWG4Xnh
const astronautImages = [
"../Seasonals/Resources/space_assets/astronaut_1.gif"
];
// Credits: https://flaticon.com
const planetImages = [
"../Seasonals/Resources/space_assets/planet_1.png",
"../Seasonals/Resources/space_assets/planet_2.png",
"../Seasonals/Resources/space_assets/planet_3.png",
"../Seasonals/Resources/space_assets/planet_4.png",
"../Seasonals/Resources/space_assets/planet_5.png",
"../Seasonals/Resources/space_assets/planet_6.png",
"../Seasonals/Resources/space_assets/planet_7.png",
"../Seasonals/Resources/space_assets/planet_8.png",
"../Seasonals/Resources/space_assets/planet_9.png"
];
// Credits: https://lottiefiles.com/free-animation/s-satellite-vfnNE8AALo
const satelliteImages = [
"../Seasonals/Resources/space_assets/Satellite_1.gif",
"../Seasonals/Resources/space_assets/Satellite_2.gif"
];
// Credit: https://pixabay.com/de/illustrations/raumstation-raum-struktur-8023777/
const issImage = "../Seasonals/Resources/space_assets/iss.png";
/**
* Credits:
* https://lottiefiles.com/free-animation/rocket-MYUQ3UFq3k
* https://pixabay.com/de/vectors/space-shuttle-atlantis-nasa-156012/
*/
const rocketImages = [
"../Seasonals/Resources/space_assets/rocket.gif",
"../Seasonals/Resources/space_assets/space-shuttle.png"
]
let msgPrinted = false;
function toggleSpace() {
const container = document.querySelector('.space-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Space hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Space visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleSpace);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createSpace() {
const container = document.querySelector('.space-container') || document.createElement('div');
if (!document.querySelector('.space-container')) {
container.className = 'space-container';
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// const standardPlanetCount = 4;
// const standardAstronautCount = 1;
// const standardSatelliteCount = 2;
// const standardIssCount = 1;
// const standardRocketCount = 1;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
let divisor = isMobile ? Math.max(1, symbolCountMobile) : 1;
let pCount = Math.floor(planetCountConf / divisor);
let aCount = Math.floor(astronautCountConf / divisor);
let sCount = Math.floor(satelliteCountConf / divisor);
let iCount = Math.floor(issCountConf / divisor);
let rCount = Math.floor(rocketCountConf / divisor);
// Add Nebula Glow
const bgGlow = document.createElement('div');
bgGlow.className = 'space-bg-glow';
container.appendChild(bgGlow);
// Add CSS Starfield
const starfield = document.createElement('div');
starfield.className = 'space-starfield';
let boxShadows1 = [];
let boxShadows2 = [];
let boxShadows3 = [];
// Generate random stars for parallax starfield using CSS % / vw sizes for responsiveness
for (let i = 0; i < 150; i++) {
let x = (Math.random() * 100).toFixed(2);
let y = (Math.random() * 100).toFixed(2);
boxShadows1.push(`${x}vw ${y}vh #FFF`);
boxShadows1.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
}
for (let i = 0; i < 50; i++) {
let x = (Math.random() * 100).toFixed(2);
let y = (Math.random() * 100).toFixed(2);
boxShadows2.push(`${x}vw ${y}vh #FFF`);
boxShadows2.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
}
for (let i = 0; i < 20; i++) {
let x = (Math.random() * 100).toFixed(2);
let y = (Math.random() * 100).toFixed(2);
boxShadows3.push(`${x}vw ${y}vh #FFF`);
boxShadows3.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
}
const starLayer1 = document.createElement('div');
starLayer1.style.width = '1px'; starLayer1.style.height = '1px';
starLayer1.style.background = 'transparent';
starLayer1.style.boxShadow = boxShadows1.join(", ");
starLayer1.style.animation = 'space-star-drift 200s linear infinite';
starfield.appendChild(starLayer1);
const starLayer2 = document.createElement('div');
starLayer2.style.width = '2px'; starLayer2.style.height = '2px';
starLayer2.style.background = 'transparent';
starLayer2.style.boxShadow = boxShadows2.join(", ");
starLayer2.style.animation = 'space-star-drift 150s linear infinite';
starfield.appendChild(starLayer2);
const starLayer3 = document.createElement('div');
starLayer3.style.width = '3px'; starLayer3.style.height = '3px';
starLayer3.style.background = 'transparent';
starLayer3.style.boxShadow = boxShadows3.join(", ");
starLayer3.style.animation = 'space-star-drift 100s linear infinite';
starfield.appendChild(starLayer3);
container.appendChild(starfield);
// Shooting stars
const shootingStarCount = isMobile ? 1 : 2; // Less frequent
for (let i = 0; i < shootingStarCount; i++) {
const streak = document.createElement('div');
streak.className = 'space-shooting-star';
// Pick a random tail direction and fall direction to match
const isFromLeft = Math.random() > 0.5;
// Direction angle: random between 15deg-75deg (left) or 105deg-165deg (right)
// so they don't always fall in the exact same quadrant trajectory
let angle = isFromLeft
? Math.random() * 60 + 15
: Math.random() * 60 + 105;
streak.style.setProperty('--shoot-angle', `${angle}deg`);
const topStart = Math.random() * 50;
streak.style.left = isFromLeft ? '-20vw' : '120vw';
streak.style.top = `${topStart}vh`;
// Travel 200 viewport widths exactly along the rotated angle
streak.style.setProperty('--shoot-distance', '200vw');
streak.style.animationDelay = `${Math.random() * 20}s`;
// MARK: Shooting Star Speed
const flightCycleDuration = Math.random() * 10 + 15; // 15-25s
streak.style.animationDuration = `${flightCycleDuration}s`;
container.appendChild(streak);
}
const useRandomDuration = enableDifferentDuration !== false;
function createSpaceItem(imageArr, cCount, addedClass) {
for (let i = 0; i < cCount; i++) {
let symbol = document.createElement('div');
const randomImage = imageArr[Math.floor(Math.random() * imageArr.length)];
symbol.className = `space-symbol ${addedClass}`;
let img = document.createElement('img');
img.src = randomImage;
img.onerror = function() {
this.style.display = 'none';
};
symbol.appendChild(img);
const topPos = Math.random() * 90; // 0 to 90vh
// Zero gravity sizes / speeds
const depth = Math.random();
// Make background elements (depth close to 0) much smaller than foreground
const distanceScale = 0.15 + (depth * 0.85); // 0.15 to 1.0
symbol.style.zIndex = Math.floor(depth * 30) + 20;
let durationSeconds = 30; // Very slow
if (useRandomDuration) {
durationSeconds = (1 - depth) * 40 + 30 + Math.random() * 10 - 5;
}
// Randomly pick direction: left-to-right OR right-to-left
const goRight = Math.random() > 0.5;
const baseTransformScale = goRight ? 'scaleX(-1)' : 'scaleX(1)';
if (goRight) {
symbol.style.animationName = 'space-drift-right';
symbol.style.left = '-20vw';
symbol.style.right = 'auto';
} else {
symbol.style.animationName = 'space-drift-left';
symbol.style.right = '-20vw';
symbol.style.left = 'auto';
}
symbol.style.top = `${topPos}vh`;
symbol.style.animationDuration = `${durationSeconds}s`;
// Negative delay correctly scatters them initially across the screen
// so they don't all appear to spawn from the edge at the start
const delaySeconds = -(Math.random() * durationSeconds);
symbol.style.animationDelay = `${delaySeconds}s`;
// Slow rotation inside inner div
const rotationDiv = document.createElement('div');
const rotDur = Math.random() * 20 + 20; // 20-40s spin
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
rotationDiv.style.animation = `space-slow-spin ${rotDur}s linear infinite ${spinReverse}`;
// Apply final static scaling and facing to inner image directly
img.style.transform = `scale(${distanceScale}) ${baseTransformScale}`;
rotationDiv.appendChild(img);
symbol.appendChild(rotationDiv);
// Swap to a random image from the pool every time it completes an orbit (disappears)
if (imageArr.length > 1) {
// The animation delay pushes the initial cycle, so we use setInterval matched to duration
const intervalId = setInterval(() => {
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
// Update only if currently out of bounds to avoid popping
const rect = symbol.getBoundingClientRect();
if (rect.right < 0 || rect.left > window.innerWidth) {
img.src = imageArr[Math.floor(Math.random() * imageArr.length)];
}
}, 2000); // Check occasionally if it's off screen
}
container.appendChild(symbol);
}
}
createSpaceItem(planetImages, pCount, 'space-planet');
createSpaceItem(astronautImages, aCount, 'space-astronaut');
createSpaceItem(satelliteImages, sCount, 'space-satellite');
createSpaceItem([issImage], iCount, 'space-iss');
createSpaceItem(rocketImages, rCount, 'space-rocket');
}
function initializeSpace() {
if (!space) return;
createSpace();
toggleSpace();
}
initializeSpace();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Some files were not shown because too many files have changed in this diff Show More