Compare commits

...

308 Commits

Author SHA1 Message Date
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
CodeDevMLH
0b4434c51c Update manifest.json for release v1.6.2.0 [skip ci] 2026-02-03 18:18:35 +00:00
CodeDevMLH
dd6583c055 Bump version to 1.6.2.0 and update changelog for new features
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 19:17:44 +01:00
CodeDevMLH
a0b0514159 Update manifest.json for release v1.6.1.0 [skip ci] 2026-02-03 18:08:59 +00:00
CodeDevMLH
e977c83e8f Bump version to 1.6.1.0 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 19:08:07 +01:00
CodeDevMLH
e281f5c579 test other ui design 2026-02-03 19:08:02 +01:00
CodeDevMLH
e6b646e478 Update .gitignore to include test-site-new.html [skip CI] 2026-02-03 18:50:22 +01:00
CodeDevMLH
8d6bc12fa4 Update manifest.json for release v1.6.0.0 [skip ci] 2026-02-03 17:49:06 +00:00
CodeDevMLH
f036e748da Add client-side toggle option for seasonal settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-03 18:48:11 +01:00
CodeDevMLH
0177a7caea Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 22:40:50 +00:00
CodeDevMLH
c45e9f6156 Auto-Update MediaBar Enhanced to v1.2.3.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 22:40:00 +00:00
CodeDevMLH
8dc9b9f157 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 21:32:51 +00:00
CodeDevMLH
e73c6c14bb Auto-Update MediaBar Enhanced to v1.2.3.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 21:32:01 +00:00
CodeDevMLH
aa1c60f9ce Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 20:22:17 +00:00
CodeDevMLH
d1e668bcff Auto-Update MediaBar Enhanced to v1.2.3.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 20:21:27 +00:00
CodeDevMLH
0454d43f32 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 01:10:34 +00:00
CodeDevMLH
61b3b51139 Auto-Update MediaBar Enhanced to v1.2.3.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 01:09:45 +00:00
CodeDevMLH
60d1a546a2 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 01:06:40 +00:00
CodeDevMLH
669933d270 Auto-Update MediaBar Enhanced to v1.2.3.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 01:05:53 +00:00
CodeDevMLH
053f0ccfa7 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:46:16 +00:00
CodeDevMLH
60e998fc7f Auto-Update MediaBar Enhanced to v1.2.3.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 00:45:27 +00:00
CodeDevMLH
3aa631198a Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:31:33 +00:00
CodeDevMLH
b1876d655e Auto-Update MediaBar Enhanced to v1.2.3.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 00:30:43 +00:00
CodeDevMLH
9a9f89c1fc Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:18:24 +00:00
CodeDevMLH
2fb41f6442 Auto-Update MediaBar Enhanced to v1.2.3.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 00:17:35 +00:00
CodeDevMLH
4ab949e6d7 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-27 23:55:34 +00:00
CodeDevMLH
7d1024c917 Auto-Update MediaBar Enhanced to v1.2.3.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-27 23:54:49 +00:00
CodeDevMLH
c2e3d55110 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-24 22:54:52 +00:00
CodeDevMLH
c6503a90bb Auto-Update MediaBar Enhanced to v1.2.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-01-24 22:54:01 +00:00
CodeDevMLH
b70787d5ec Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-22 23:51:52 +00:00
CodeDevMLH
a9eb8113a6 Auto-Update MediaBar Enhanced to v1.2.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-22 23:51:03 +00:00
CodeDevMLH
aaf15d8934 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-21 23:12:11 +00:00
CodeDevMLH
ec298ebde0 bump version to 1.5.1.0 in project file
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-01-22 00:11:19 +01:00
CodeDevMLH
bb108d0f49 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-21 22:53:15 +00:00
MLH
f271e1715d Merge pull request 'fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes' (#1) from dev into main
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 57s
Reviewed-on: #1
2026-01-21 23:52:18 +01:00
CodeDevMLH
bd0e2779e5 fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes
All checks were successful
🏗️ Build Plugin / build (push) Successful in 53s
🏗️ Build Plugin / build (pull_request) Successful in 58s
2026-01-21 23:51:23 +01:00
CodeDevMLH
53a1682868 Enhance descriptions and overviews in manifest.json for Seasonals and Media Bar Enhanced plugins [skip ci] 2026-01-09 00:32:40 +01:00
CodeDevMLH
a7df2fd832 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 23:31:54 +00:00
CodeDevMLH
c56cde860b Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-08 23:31:04 +00:00
CodeDevMLH
59211e27c6 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 23:15:57 +00:00
CodeDevMLH
a2b1179353 Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-01-08 23:15:12 +00:00
CodeDevMLH
c7f34ec92f Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 22:16:58 +00:00
CodeDevMLH
4c011cf560 Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-01-08 22:16:11 +00:00
CodeDevMLH
e5f78c711d Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 15:27:46 +00:00
CodeDevMLH
98a536315b Auto-Update MediaBar Enhanced to v1.1.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 15:27:00 +00:00
CodeDevMLH
01343848e3 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 14:55:53 +00:00
CodeDevMLH
113e7dd0f7 Auto-Update MediaBar Enhanced to v1.1.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 14:55:04 +00:00
CodeDevMLH
1bc4176771 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 02:16:44 +00:00
CodeDevMLH
b091e5592d Auto-Update MediaBar Enhanced to v1.1.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 02:15:57 +00:00
CodeDevMLH
cb2d86340e Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:34:26 +00:00
CodeDevMLH
57a92e94de Remove build.yaml configuration file for Seasonals plugin
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 57s
2026-01-07 00:33:36 +01:00
CodeDevMLH
30bc8bef39 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:27:26 +00:00
CodeDevMLH
2d237063a3 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-01-06 23:26:36 +00:00
CodeDevMLH
049a5075b5 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:18:28 +00:00
CodeDevMLH
cb2e9c4b07 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:17:39 +00:00
CodeDevMLH
5a39f85082 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:13:12 +00:00
CodeDevMLH
b863d201b9 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:12:24 +00:00
CodeDevMLH
78b50b41c2 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:11:24 +00:00
CodeDevMLH
0ba1545fd6 test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-01-07 00:10:33 +01:00
CodeDevMLH
16c4e0f29b fix link 2026-01-07 00:09:41 +01:00
CodeDevMLH
3581b8cbb2 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:57:27 +00:00
CodeDevMLH
7184c93f6f Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-06 22:56:39 +00:00
CodeDevMLH
0969f0238f Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:45:32 +00:00
CodeDevMLH
12f868d3f9 test ..
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-06 23:44:42 +01:00
CodeDevMLH
dc7be56807 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2026-01-06 23:41:57 +01:00
CodeDevMLH
cb60690e6b fix log name 2026-01-06 23:41:55 +01:00
CodeDevMLH
e0397bb2e8 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:40:15 +00:00
CodeDevMLH
822bcafd11 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-06 22:39:29 +00:00
CodeDevMLH
429d96c816 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:37:52 +00:00
CodeDevMLH
e948055f0f test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:37:01 +01:00
CodeDevMLH
5129d46163 fix remote repo 2026-01-06 23:36:39 +01:00
CodeDevMLH
660f7142ef Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:33:07 +00:00
CodeDevMLH
9f657588d8 fix changelog
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-06 23:32:16 +01:00
CodeDevMLH
0457f1a764 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2026-01-06 23:30:37 +01:00
CodeDevMLH
f196c6c296 fix url 2026-01-06 23:30:36 +01:00
216 changed files with 10969 additions and 933 deletions

View File

@@ -40,13 +40,42 @@ jobs:
echo "Detected Version: $VERSION" echo "Detected Version: $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "TARGET_ABI=$TARGET_ABI" >> $GITHUB_ENV
# Also export GUID for later use
PLUGIN_GUID=$(jq -r '.[0].guid' manifest.json)
echo "PLUGIN_GUID=$PLUGIN_GUID" >> $GITHUB_ENV
# Escape newlines in changelog for GITHUB_ENV # Escape newlines in changelog for GITHUB_ENV
echo "CHANGELOG<<EOF" >> $GITHUB_ENV echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $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 - name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
# Inject version from manifest into the build # Inject version from manifest into the build
@@ -66,12 +95,13 @@ jobs:
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
- name: Update manifest.json - name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}" REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}" VERSION="${{ env.VERSION }}"
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/${{ github.repository }}/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip" DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
echo "Updating manifest.json with:" echo "Updating manifest.json with:"
echo "Hash: ${{ env.ZIP_HASH }}" echo "Hash: ${{ env.ZIP_HASH }}"
@@ -85,12 +115,14 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json - name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]" commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json file_pattern: manifest.json
- name: Create Release - name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
server_url: "https://git.mahom03-spacecloud.de" server_url: "https://git.mahom03-spacecloud.de"
@@ -104,6 +136,7 @@ jobs:
# Update Message in Remote Repository # Update Message in Remote Repository
- name: Checkout Central Manifest Repo - name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
@@ -111,15 +144,19 @@ jobs:
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }} token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest - name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
cd central-manifest cd central-manifest
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
# 1. Get info from previous steps # 1. Get info from previous steps
VERSION="${{ env.VERSION }}" VERSION="${{ env.VERSION }}"
HASH="${{ env.ZIP_HASH }}" HASH="${{ env.ZIP_HASH }}"
TIME="${{ env.BUILD_TIME }}" TIME="${{ env.BUILD_TIME }}"
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/${{ github.repository }}/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip" DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
# 2. Get info from env # 2. Get info from env
PLUGIN_GUID="${{ env.PLUGIN_GUID }}" PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
@@ -163,6 +200,7 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest - name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: | run: |
cd central-manifest cd central-manifest
git config user.name "CodeDevMLH" git config user.name "CodeDevMLH"
@@ -171,7 +209,7 @@ jobs:
# Check if there are changes # Check if there are changes
if [[ -n $(git status -s) ]]; then if [[ -n $(git status -s) ]]; then
git add manifest.json git add manifest.json
git commit -m "Auto-Update MediaBar Enhanced to v${{ env.VERSION }}" git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
git push git push
else else
echo "No changes to central manifest." echo "No changes to central manifest."

View File

@@ -39,6 +39,11 @@ jobs:
echo "Detected Version: $VERSION" echo "Detected Version: $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "TARGET_ABI=$TARGET_ABI" >> $GITHUB_ENV
# Also export GUID for later use
PLUGIN_GUID=$(jq -r '.[0].guid' manifest.json)
echo "PLUGIN_GUID=$PLUGIN_GUID" >> $GITHUB_ENV
# Escape newlines in changelog for GITHUB_ENV # Escape newlines in changelog for GITHUB_ENV
echo "CHANGELOG<<EOF" >> $GITHUB_ENV echo "CHANGELOG<<EOF" >> $GITHUB_ENV
@@ -113,6 +118,9 @@ jobs:
run: | run: |
cd central-manifest cd central-manifest
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
# 1. Get info from previous steps # 1. Get info from previous steps
VERSION="${{ env.VERSION }}" VERSION="${{ env.VERSION }}"
HASH="${{ env.ZIP_HASH }}" HASH="${{ env.ZIP_HASH }}"
@@ -169,7 +177,7 @@ jobs:
# Check if there are changes # Check if there are changes
if [[ -n $(git status -s) ]]; then if [[ -n $(git status -s) ]]; then
git add manifest.json git add manifest.json
git commit -m "Auto-Update MediaBar Enhanced to v${{ env.VERSION }}" git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
git push git push
else else
echo "No changes to central manifest." echo "No changes to central manifest."

2
.gitignore vendored
View File

@@ -4,5 +4,5 @@ obj/
.idea/ .idea/
artifacts artifacts
test-site.html test-site-old.html
RELEASE_GUIDE.md 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(".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg"; if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif"; if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif";
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return "image/svg+xml";
return "application/octet-stream"; return "application/octet-stream";
} }
} }

View File

@@ -15,6 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration
IsEnabled = true; IsEnabled = true;
SelectedSeason = "none"; SelectedSeason = "none";
AutomateSeasonSelection = true; AutomateSeasonSelection = true;
EnableClientSideToggle = true;
Autumn = new AutumnOptions(); Autumn = new AutumnOptions();
Snowflakes = new SnowflakesOptions(); Snowflakes = new SnowflakesOptions();
@@ -26,6 +27,31 @@ public class PluginConfiguration : BasePluginConfiguration
Christmas = new ChristmasOptions(); Christmas = new ChristmasOptions();
Santa = new SantaOptions(); Santa = new SantaOptions();
Easter = new EasterOptions(); Easter = new EasterOptions();
Resurrection = new ResurrectionOptions();
Spring = new SpringOptions();
Summer = new SummerOptions();
CherryBlossom = new CherryBlossomOptions();
Carnival = new CarnivalOptions();
PiDay = new PiDayOptions();
Eurovision = new EurovisionOptions();
Storm = new StormOptions();
Pride = new PrideOptions();
EarthDay = new EarthDayOptions();
Rain = new RainOptions();
Frost = new FrostOptions();
FilmNoir = new FilmNoirOptions();
Oscar = new OscarOptions();
MarioDay = new MarioDayOptions();
StarWars = new StarWarsOptions();
Oktoberfest = new OktoberfestOptions();
Friday13 = new Friday13Options();
Eid = new EidOptions();
Spooky = new SpookyOptions();
Sports = new SportsOptions();
Olympia = new OlympiaOptions();
Space = new SpaceOptions();
Underwater = new UnderwaterOptions();
Birthday = new BirthdayOptions();
} }
/// <summary> /// <summary>
@@ -43,6 +69,19 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public bool AutomateSeasonSelection { get; set; } public bool AutomateSeasonSelection { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable client-side toggle for users.
/// </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 AutumnOptions Autumn { get; set; }
public SnowflakesOptions Snowflakes { get; set; } public SnowflakesOptions Snowflakes { get; set; }
public SnowfallOptions Snowfall { get; set; } public SnowfallOptions Snowfall { get; set; }
@@ -53,6 +92,31 @@ public class PluginConfiguration : BasePluginConfiguration
public ChristmasOptions Christmas { get; set; } public ChristmasOptions Christmas { get; set; }
public SantaOptions Santa { get; set; } public SantaOptions Santa { get; set; }
public EasterOptions Easter { get; set; } public EasterOptions Easter { get; set; }
public ResurrectionOptions Resurrection { get; set; }
public SpringOptions Spring { get; set; }
public SummerOptions Summer { get; set; }
public CherryBlossomOptions CherryBlossom { get; set; }
public CarnivalOptions Carnival { get; set; }
public PiDayOptions PiDay { get; set; }
public EurovisionOptions Eurovision { get; set; }
public StormOptions Storm { get; set; }
public PrideOptions Pride { get; set; }
public EarthDayOptions EarthDay { get; set; }
public RainOptions Rain { get; set; }
public FrostOptions Frost { get; set; }
public FilmNoirOptions FilmNoir { get; set; }
public OscarOptions Oscar { get; set; }
public MarioDayOptions MarioDay { get; set; }
public StarWarsOptions StarWars { get; set; }
public OktoberfestOptions Oktoberfest { get; set; }
public Friday13Options Friday13 { get; set; }
public EidOptions Eid { get; set; }
public SpookyOptions Spooky { get; set; }
public SportsOptions Sports { get; set; }
public OlympiaOptions Olympia { get; set; }
public SpaceOptions Space { get; set; }
public UnderwaterOptions Underwater { get; set; }
public BirthdayOptions Birthday { get; set; }
} }
public class AutumnOptions public class AutumnOptions
@@ -110,6 +174,8 @@ public class HalloweenOptions
public bool EnableRandomSymbols { get; set; } = true; public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false; public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true; public bool EnableDifferentDuration { get; set; } = true;
public bool EnableSpiders { get; set; } = true;
public bool EnableMice { get; set; } = true;
} }
public class HeartsOptions public class HeartsOptions
@@ -157,3 +223,203 @@ public class EasterOptions
public int MinBunnyRestTime { get; set; } = 2000; public int MinBunnyRestTime { get; set; } = 2000;
public int MaxBunnyRestTime { get; set; } = 5000; public int MaxBunnyRestTime { get; set; } = 5000;
} }
public class ResurrectionOptions
{
public int SymbolCount { get; set; } = 12;
public bool EnableResurrection { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SpringOptions
{
public int PollenCount { get; set; } = 30;
public int SunbeamCount { get; set; } = 5;
public int BirdCount { get; set; } = 4;
public int ButterflyCount { get; set; } = 4;
public int BeeCount { get; set; } = 2;
public int LadybugCount { get; set; } = 2;
public bool EnableSpring { get; set; } = true;
public bool EnableSpringSunbeams { get; set; } = true;
public bool EnableRandomSpring { get; set; } = true;
public bool EnableRandomSpringMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SummerOptions
{
public int BubbleCount { get; set; } = 20;
public int DustCount { get; set; } = 50;
public bool EnableSummer { get; set; } = true;
public bool EnableRandomSummer { get; set; } = true;
public bool EnableRandomSummerMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class CarnivalOptions
{
public int ObjectCount { get; set; } = 25;
public bool EnableCarnival { get; set; } = true;
public bool EnableRandomCarnival { get; set; } = true;
public bool EnableRandomCarnivalMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableCarnivalSway { get; set; } = true;
}
public class CherryBlossomOptions
{
public int PetalCount { get; set; } = 25;
public bool EnableCherryBlossom { get; set; } = true;
public bool EnableRandomCherryBlossom { get; set; } = true;
public bool EnableRandomCherryBlossomMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class PiDayOptions
{
public int SymbolCount { get; set; } = 50;
public bool EnablePiDay { get; set; } = true;
public bool EnableRandomPiDay { get; set; } = true;
public bool EnableRandomPiDayMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnablePiDayBackground { get; set; } = false;
}
public class EurovisionOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableEurovision { get; set; } = true;
public bool EnableRandomEurovision { get; set; } = true;
public bool EnableRandomEurovisionMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableColorfulNotes { get; set; } = true;
public string EurovisionColors { get; set; } = "#ff0026ff,#17a6ffff,#32d432ff,#FFD700,#f0821bff,#f826f8ff";
public int EurovisionGlowSize { get; set; } = 8;
}
public class StormOptions
{
public int RaindropCount { get; set; } = 300;
public int RaindropCountMobile { get; set; } = 150;
public bool EnableStorm { get; set; } = true;
public bool EnableLightning { get; set; } = true;
public double RainSpeed { get; set; } = 1;
}
public class PrideOptions
{
public bool EnablePride { get; set; } = true;
public int HeartCount { get; set; } = 20;
public int HeartSize { get; set; } = 2;
public bool ColorHeader { get; set; } = true;
}
public class EarthDayOptions
{
public bool EnableEarthDay { get; set; } = true;
public int VineCount { get; set; } = 4;
}
public class RainOptions
{
public bool EnableRain { get; set; } = true;
public int RaindropCount { get; set; } = 300;
public int RaindropCountMobile { get; set; } = 150;
public double RainSpeed { get; set; } = 1;
}
public class FrostOptions
{
public bool EnableFrost { get; set; } = true;
}
public class FilmNoirOptions
{
public bool EnableFilmNoir { get; set; } = true;
}
public class OscarOptions
{
public bool EnableOscar { get; set; } = true;
}
public class MarioDayOptions
{
public bool EnableMarioDay { get; set; } = true;
}
public class StarWarsOptions
{
public bool EnableStarWars { get; set; } = true;
}
public class OktoberfestOptions
{
public bool EnableOktoberfest { get; set; } = true;
}
public class Friday13Options
{
public bool EnableFriday13 { get; set; } = true;
}
public class EidOptions
{
public bool EnableEid { get; set; } = true;
}
public class SpookyOptions
{
public bool EnableSpooky { get; set; } = true;
public int SymbolCount { get; set; } = 25;
public bool EnableSpookySway { get; set; } = true;
public int SpookySize { get; set; } = 20;
public int SpookyGlowSize { get; set; } = 2;
}
public class SportsOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableSports { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class OlympiaOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableOlympia { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SpaceOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableSpace { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class UnderwaterOptions
{
public int SymbolCount { get; set; } = 15;
public bool EnableUnderwater { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class BirthdayOptions
{
public int SymbolCount { get; set; } = 25;
public bool EnableBirthday { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -18,7 +18,7 @@ public class ScriptInjector
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; 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>"; public const string Marker = "</body>";
/// <summary> /// <summary>
@@ -56,6 +56,18 @@ public class ScriptInjector
} }
var content = File.ReadAllText(indexPath); 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)) if (!content.Contains(ScriptTag))
{ {
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase); var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
@@ -113,6 +125,17 @@ public class ScriptInjector
} else { } else {
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary."); _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) catch (UnauthorizedAccessException)
{ {
@@ -204,4 +227,21 @@ public class ScriptInjector
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered."); _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;
}
} }

View File

@@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Seasonals; namespace Jellyfin.Plugin.Seasonals;
/// <summary> /// <summary>
/// The main plugin. /// The main plugin.
/// </summary> /// </summary>
public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
{ {

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,12 +8,15 @@
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 10;
contain: layout paint;
} }
.leaf { .leaf {
position: fixed; position: fixed;
z-index: 15; z-index: 15;
top: -10%; top: 0;
will-change: transform;
translate: 0 -10vh;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@@ -40,30 +43,30 @@
@-webkit-keyframes leaf-fall { @-webkit-keyframes leaf-fall {
0% { 0% {
top: -10%; translate: 0 -10vh;
} }
100% { 100% {
top: 100%; translate: 0 100vh;
} }
} }
@keyframes leaf-fall { @keyframes leaf-fall {
0% { 0% {
top: -10%; translate: 0 -10vh;
} }
100% { 100% {
top: 100%; translate: 0 100vh;
} }
} }
@-webkit-keyframes leaf-shake { @-webkit-keyframes leaf-shake {
0%, 100% { 0%, 100% {
-webkit-transform: translateX(0) rotate(var(--rotate-start, -20deg)); transform: translateX(0) rotate(var(--rotate-start, -20deg));
} }
50% { 50% {
-webkit-transform: translateX(80px) rotate(var(--rotate-end, 20deg)); transform: translateX(80px) rotate(var(--rotate-end, 20deg));
} }
} }

View File

@@ -7,6 +7,25 @@ const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? co
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
const leafCount = config.LeafCount || 25; // count of random extra leaves const leafCount = config.LeafCount || 25; // count of random extra leaves
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 let msgPrinted = false; // flag to prevent multiple console messages
@@ -38,35 +57,13 @@ function toggleAutumn() {
// observe changes in the DOM // observe changes in the DOM
const observer = new MutationObserver(toggleAutumn); const observer = new MutationObserver(toggleAutumn);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) 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 addRandomLeaves(count) { function addRandomLeaves(count) {
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
if (!autumnContainer) return; // exit if leave container is not found if (!autumnContainer) return; // exit if leave container is not found
@@ -90,7 +87,7 @@ function addRandomLeaves(count) {
// set random horizontal position, animation delay and size(uncomment lines to enable) // set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%) const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s) const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s)
const randomAnimationDelay2 = Math.random() * 4; // delay for shake+rotate (0s to 4s) const randomAnimationDelay2 = -(Math.random() * 4); // delay for shake+rotate (-4s to 0s)
// apply styles // apply styles
leaveDiv.style.left = `${randomLeft}%`; leaveDiv.style.left = `${randomLeft}%`;

View File

@@ -0,0 +1,90 @@
.birthday-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
contain: strict;
}
.birthday-cake {
position: absolute;
bottom: 2vh;
left: 50vw;
transform: translateX(-50%);
font-size: 8rem;
z-index: 50;
filter: drop-shadow(0 0 10px rgba(255,255,255,0.4));
}
.birthday-cake img {
height: 15vh;
width: auto;
object-fit: contain;
max-height: 150px;
}
.birthday-symbol {
position: absolute;
bottom: -10vh; /* balloons rise from bottom */
animation: birthday-rise linear infinite;
font-size: 3rem;
opacity: 0.95;
z-index: 40;
}
.birthday-symbol img {
width: 6vh;
height: auto;
max-width: 60px;
object-fit: contain;
}
.birthday-confetti {
position: absolute;
top: -5vh;
width: 10px;
height: 10px;
opacity: 0.9;
animation: birthday-confetti-fall linear infinite;
z-index: 30;
/* Mix of circles and squares by using CSS variables or random in JS. For simplicity, we make all slightly rounded rectangles */
border-radius: 2px;
}
@keyframes birthday-rise {
0% {
transform: translateY(10vh) rotate(var(--start-rot, 0deg));
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(-110vh) rotate(calc(var(--start-rot, 0deg) * -1));
opacity: 0;
}
}
@keyframes birthday-confetti-fall {
0% {
transform: translateY(-5vh) rotateX(0deg) rotateY(0deg) rotateZ(0deg);
opacity: 0;
}
5% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(105vh) rotateX(720deg) rotateY(360deg) rotateZ(180deg);
opacity: 0;
}
}

View File

@@ -0,0 +1,145 @@
const config = window.SeasonalsPluginConfig?.Birthday || {};
const birthday = config.EnableBirthday !== undefined ? config.EnableBirthday : true;
const symbolCount = config.SymbolCount || 25;
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
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 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);
}
// Spawn Birthday Cake at the bottom
const cake = document.createElement('div');
cake.className = 'birthday-cake';
let cakeImg = document.createElement('img');
cakeImg.src = `../Seasonals/Resources/birthday_images/cake.png`;
cakeImg.onerror = function() {
this.style.display = 'none';
this.parentElement.innerHTML = '🎂';
};
cake.appendChild(cakeImg);
container.appendChild(cake);
const standardCount = 15;
const totalSymbols = symbolCount + standardCount;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
let finalCount = totalSymbols;
if (isMobile) {
finalCount = enableRandomMobile ? totalSymbols : standardCount;
}
const useRandomDuration = enableDifferentDuration !== false;
// We'll treat balloons and gifts as rising symbols
const activeItems = ['balloon_red', 'balloon_blue', 'balloon_yellow', 'gift'];
for (let i = 0; i < finalCount; i++) {
let symbol = document.createElement('div');
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
symbol.className = `birthday-symbol birthday-${randomItem}`;
let img = document.createElement('img');
img.src = `../Seasonals/Resources/birthday_images/${randomItem}.png`;
img.onerror = function() {
this.style.display = 'none';
this.parentElement.innerHTML = getBirthdayEmojiFallback(randomItem);
};
symbol.appendChild(img);
const leftPos = Math.random() * 95;
const delaySeconds = Math.random() * 10;
let durationSeconds = 9;
if (useRandomDuration) {
durationSeconds = Math.random() * 5 + 7; // 7 to 12 seconds
}
const startRot = (Math.random() * 20) - 10; // -10 to +10 spread
symbol.style.setProperty('--start-rot', `${startRot}deg`);
symbol.style.left = `${leftPos}vw`;
symbol.style.animationDuration = `${durationSeconds}s`;
symbol.style.animationDelay = `${delaySeconds}s`;
container.appendChild(symbol);
}
// Party Confetti
const confettiColors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
const confettiCount = isMobile ? 40 : 80;
for (let i = 0; i < confettiCount; i++) {
let confetti = document.createElement('div');
confetti.className = 'birthday-confetti';
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
confetti.style.backgroundColor = color;
const leftPos = Math.random() * 100;
const delaySeconds = Math.random() * 8;
const duration = Math.random() * 3 + 4;
confetti.style.left = `${leftPos}vw`;
confetti.style.animationDuration = `${duration}s`;
confetti.style.animationDelay = `${delaySeconds}s`;
container.appendChild(confetti);
}
}
function getBirthdayEmojiFallback(type) {
if (type.startsWith('balloon')) return '🎈';
if (type === 'gift') return '🎁';
return '';
}
function initializeBirthday() {
if (!birthday) return;
createBirthday();
toggleBirthday();
}
initializeBirthday();

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 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,191 @@
const config = window.SeasonalsPluginConfig?.Carnival || {};
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // Enable/disable carnival
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true; // Enable random carnival objects
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false; // Enable random carnival objects on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // Randomize falling and flutter speeds
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // Enable side-to-side sway animation
const carnivalCount = config.ObjectCount || 120; // Number of confetti pieces to spawn
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;
}
}
}
// observe changes in the DOM
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);
}
function addRandomCarnivalObjects(count) {
const carnivalContainer = document.querySelector('.carnival-container');
if (!carnivalContainer) return;
console.log('Adding random carnival confetti');
for (let i = 0; i < count; i++) {
createConfettiPiece(carnivalContainer, true);
}
}
// initialize standard carnival objects
function initCarnivalObjects() {
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 < 60; i++) {
createConfettiPiece(container, true);
}
}
// initialize carnival
function initializeCarnival() {
if (!carnival) return;
initCarnivalObjects();
toggleCarnival();
const screenWidth = window.innerWidth;
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
addRandomCarnivalObjects(carnivalCount);
}
}
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,105 @@
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true;
const petalCount = config.PetalCount || 25;
const randomCherryBlossom = config.EnableRandomCherryBlossom !== undefined ? config.EnableRandomCherryBlossom : true;
const randomCherryBlossomMobile = config.EnableRandomCherryBlossomMobile !== undefined ? config.EnableRandomCherryBlossomMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
let msgPrinted = false;
function toggleCherryBlossom() {
const container = document.querySelector('.cherryblossom-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('CherryBlossom hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('CherryBlossom visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleCherryBlossom);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createPetal(container) {
const petal = document.createElement('div');
petal.classList.add('cherryblossom-petal');
const type = Math.random() > 0.5 ? 'type1' : 'type2';
petal.classList.add(type);
const color = Math.random() > 0.7 ? 'darker' : 'lighter';
petal.classList.add(color);
const randomLeft = Math.random() * 100;
petal.style.left = `${randomLeft}%`;
const size = Math.random() * 0.5 + 0.5;
petal.style.transform = `scale(${size})`;
const duration = Math.random() * 5 + 8;
const delay = Math.random() * 10;
const swayDuration = Math.random() * 2 + 2;
if (enableDifferentDuration) {
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
}
petal.style.animationDelay = `${delay}s, ${Math.random() * 3}s`;
container.appendChild(petal);
}
function addRandomObjects() {
const container = document.querySelector('.cherryblossom-container');
if (!container) return;
for (let i = 0; i < petalCount; i++) {
createPetal(container);
}
}
function initObjects() {
let container = document.querySelector('.cherryblossom-container');
if (!container) {
container = document.createElement("div");
container.className = "cherryblossom-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// Initial batch
for (let i = 0; i < 15; i++) {
createPetal(container);
}
}
function initializeCherryBlossom() {
if (!cherryBlossom) return;
initObjects();
toggleCherryBlossom();
const screenWidth = window.innerWidth;
if (randomCherryBlossom && (screenWidth > 768 || randomCherryBlossomMobile)) {
addRandomObjects();
}
}
initializeCherryBlossom();

View File

@@ -8,12 +8,15 @@
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 10;
contain: layout paint;
} }
.christmas { .christmas {
position: fixed; position: fixed;
z-index: 15; z-index: 15;
top: -10%; top: 0;
will-change: transform;
translate: 0 -10vh;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@@ -33,11 +36,11 @@
@-webkit-keyframes christmas-fall { @-webkit-keyframes christmas-fall {
0% { 0% {
top: -10%; translate: 0 -10vh;
} }
100% { 100% {
top: 100%; translate: 0 110vh;
} }
} }
@@ -45,28 +48,25 @@
0%, 0%,
100% { 100% {
-webkit-transform: translateX(0);
transform: translateX(0); transform: translateX(0);
} }
50% { 50% {
-webkit-transform: translateX(80px);
transform: translateX(80px); transform: translateX(80px);
} }
} }
@keyframes christmas-fall { @keyframes christmas-fall {
0% { 0% {
top: -10%; translate: 0 -10vh;
} }
100% { 100% {
top: 100%; translate: 0 110vh;
} }
} }
@keyframes christmas-shake { @keyframes christmas-shake {
0%, 0%,
100% { 100% {
transform: translateX(0); transform: translateX(0);

View File

@@ -6,6 +6,8 @@ const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ?
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols 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 christmasCount = config.SymbolCount || 25; // count of random extra christmas
// Array of christmas characters
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -37,17 +39,12 @@ function toggleChristmas() {
// observe changes in the DOM // observe changes in the DOM
const observer = new MutationObserver(toggleChristmas); const observer = new MutationObserver(toggleChristmas);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) attributes: true
}); });
// Array of christmas characters
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
function addRandomChristmas(count) { function addRandomChristmas(count) {
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
if (!christmasContainer) return; // exit if christmas container is not found if (!christmasContainer) return; // exit if christmas container is not found

View File

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

View File

@@ -0,0 +1,127 @@
const config = window.SeasonalsPluginConfig?.EarthDay || {};
const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true;
const vineCount = config.VineCount || 4;
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,63 @@
.easter-container { .easter-container {
display: block; display: block;
position: fixed; position: fixed;
overflow: hidden; top: 0;
top: 0; left: 0;
left: 0; width: 100%;
width: 100%; height: 100%;
height: 100%; pointer-events: none;
pointer-events: none; z-index: 10000;
z-index: 10; contain: strict;
overflow: hidden;
}
.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 {
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 { .hopping-rabbit {
position: fixed; position: absolute;
z-index: 15; bottom: -15px;
bottom: 10px; left: 0;
width: 70px; width: 160px;
overflow: hidden; height: auto;
pointer-events: none; will-change: transform;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.hopping-rabbit { .hopping-rabbit {
width: 60px; 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,37 @@
const config = window.SeasonalsPluginConfig?.Easter || {}; const config = window.SeasonalsPluginConfig?.Easter || {};
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter const easter = config.EnableEaster !== undefined ? config.EnableEaster : true;
const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter const enableBunny = config.EnableBunny !== undefined ? config.EnableBunny : true;
const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance) /* MARK: Bunny movement config */
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter const jumpDistanceVw = 5; // Distance in vw the bunny covers per jump
const easterEggCount = config.EggCount || 20; // count of random extra easter 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 bunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable hopping bunny const minBunnyRestTime = config.MinBunnyRestTime || 2000;
const bunnyDuration = config.BunnyDuration || 12000; // duration of the bunny animation in ms const maxBunnyRestTime = config.MaxBunnyRestTime || 5000;
const hopHeight = config.HopHeight || 12; // height of the bunny hops in px const eggCount = config.EggCount || 15;
const minBunnyRestTime = config.MinBunnyRestTime || 2000; // minimum time the bunny rests in ms
const maxBunnyRestTime = config.MaxBunnyRestTime || 5000; // maximum time the bunny rests in ms
const rabbit = "../Seasonals/Resources/easter_images/Osterhase.gif";
let msgPrinted = false; // flag to prevent multiple console messages const easterEggImages = [
let animationFrameId; "../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() { function toggleEaster() {
const easterContainer = document.querySelector('.easter-container'); const easterContainer = document.querySelector('.easter-container');
if (!easterContainer) return; if (!easterContainer) return;
@@ -26,21 +41,20 @@ function toggleEaster() {
const isDashboard = document.body.classList.contains('dashboardDocument'); const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu'); const hasUserMenu = document.querySelector('#app-user-menu');
// hide easter if video/trailer player is active or dashboard is visible
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
easterContainer.style.display = 'none'; // hide easter easterContainer.style.display = 'none';
if (animationFrameId) { if (rabbitTimeout) {
cancelAnimationFrame(animationFrameId); clearTimeout(rabbitTimeout);
animationFrameId = null; isAnimating = false;
} }
if (!msgPrinted) { if (!msgPrinted) {
console.log('Easter hidden'); console.log('Easter hidden');
msgPrinted = true; msgPrinted = true;
} }
} else { } else {
easterContainer.style.display = 'block'; // show easter easterContainer.style.display = 'block';
if (!animationFrameId) { if (!isAnimating && enableBunny) {
animateRabbit(); // start animation animateRabbit(document.querySelector('#rabbit'));
} }
if (msgPrinted) { if (msgPrinted) {
console.log('Easter visible'); console.log('Easter visible');
@@ -49,145 +63,201 @@ function toggleEaster() {
} }
} }
// observe changes in the DOM
const observer = new MutationObserver(toggleEaster); const observer = new MutationObserver(toggleEaster);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) attributes: true
}); });
const images = [ function createEasterGrassAndEggs(container) {
"/Seasonals/Resources/easter_images/egg_1.png", let grassContainer = container.querySelector('.easter-grass-container');
"/Seasonals/Resources/easter_images/egg_2.png", if (!grassContainer) {
"/Seasonals/Resources/easter_images/egg_3.png", grassContainer = document.createElement('div');
"/Seasonals/Resources/easter_images/egg_4.png", grassContainer.className = 'easter-grass-container';
"/Seasonals/Resources/easter_images/egg_5.png", container.appendChild(grassContainer);
"/Seasonals/Resources/easter_images/egg_6.png", }
"/Seasonals/Resources/easter_images/egg_7.png",
"/Seasonals/Resources/easter_images/egg_8.png", grassContainer.innerHTML = '';
"/Seasonals/Resources/easter_images/egg_9.png",
"/Seasonals/Resources/easter_images/egg_10.png", let pathsBg = '';
"/Seasonals/Resources/easter_images/egg_11.png", let pathsFg = '';
"/Seasonals/Resources/easter_images/egg_12.png", const w = window.innerWidth;
]; const hSVG = 80; // Grass 80px high
const rabbit = "/Seasonals/Resources/easter_images/easter-bunny.png";
// Generate Grass
function addRandomEaster(count) { const bladeCount = w / 5;
const easterContainer = document.querySelector('.easter-container'); // get the leave container for (let i = 0; i < bladeCount; i++) {
if (!easterContainer) return; // exit if leave container is not found const height = Math.random() * 40 + 20;
const x = i * 5 + Math.random() * 3;
console.log('Adding random easter eggs'); const hue = 80 + Math.random() * 40; // slightly more yellow-green for spring/easter
const color = `hsl(${hue}, 60%, 40%)`;
// Array of leave characters const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
for (let i = 0; i < count; i++) { if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
// create a new leave element }
const eggDiv = document.createElement('div');
eggDiv.className = "easter"; for (let i = 0; i < 200; i++) {
const x = Math.random() * w;
// pick a random easter symbol const h = 20 + Math.random() * 50;
const imageSrc = images[Math.floor(Math.random() * images.length)]; const cY = hSVG - h;
const img = document.createElement("img"); const bend = x + (Math.random() * 40 - 20);
img.src = imageSrc; const color = Math.random() > 0.5 ? '#4caf50' : '#8bc34a';
const width = 1 + Math.random() * 2;
eggDiv.appendChild(img); 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;
// 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) // Generate Flowers
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s) const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
for (let i = 0; i < 40; i++) {
// apply styles const x = 10 + Math.random() * (w - 20);
eggDiv.style.left = `${randomLeft}%`; const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
eggDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`; const col = colors[Math.floor(Math.random() * colors.length)];
// set random animation duration let path = '';
if (enableDiffrentDuration) { 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 randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s) const r = 2 + Math.random() * 1.5;
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`; 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}"/>`;
// add the leave to the container path += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
easterContainer.appendChild(eggDiv);
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() { let rabbitTimeout;
if (!bunny) return; // Nur ausführen, wenn Easter aktiviert ist let isAnimating = false;
const easterContainer = document.querySelector('.easter-container'); function addHoppingRabbit(container) {
if (!easterContainer) return; if (!enableBunny) return;
// Hase erstellen
const rabbitImg = document.createElement("img"); const rabbitImg = document.createElement("img");
rabbitImg.id = "rabbit"; rabbitImg.id = "rabbit";
rabbitImg.src = rabbit; // Bildpfad aus der bestehenden Definition rabbitImg.src = rabbit;
rabbitImg.alt = "Hoppelnder Osterhase"; rabbitImg.alt = "Hopping Easter Bunny";
rabbitImg.className = "hopping-rabbit";
rabbitImg.style.bottom = "-15px";
rabbitImg.style.position = "absolute";
// CSS-Klassen hinzufügen container.appendChild(rabbitImg);
rabbitImg.classList.add("hopping-rabbit");
easterContainer.appendChild(rabbitImg);
rabbitImg.style.bottom = (hopHeight / 2 + 6) + "px";
animateRabbit(rabbitImg); animateRabbit(rabbitImg);
} }
function animateRabbit(rabbitElement) { function animateRabbit(rabbit) {
const rabbit = rabbitElement || document.querySelector('#rabbit'); if (!rabbit || isAnimating) return;
if (!rabbit) 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)' : '';
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
const loopDurationMs = jumpDurationMs + pauseDurationMs;
let startTime = null; let startTime = null;
function animationStep(timestamp) { function animationStep(timestamp) {
if (!document.querySelector('.easter-container') || rabbit.style.display === 'none') {
isAnimating = false;
return;
}
if (!startTime) { if (!startTime) {
startTime = timestamp; startTime = timestamp;
// resetting gif, forces the browser to restart the GIF from the first frame (crucial for syncing hops with movement)
// random start position and direction const currSrc = rabbit.src.split('?')[0];
const startFromLeft = Math.random() >= 0.5; rabbit.src = '';
rabbit.startX = startFromLeft ? -10 : 110; rabbit.src = currSrc;
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 progress = timestamp - startTime;
// calculate the horizontal position (linear interpolation) const elapsed = timestamp - startTime;
const x = rabbit.startX + (progress / bunnyDuration) * (rabbit.endX - rabbit.startX);
const completedLoops = Math.floor(elapsed / loopDurationMs);
const timeInCurrentLoop = elapsed % loopDurationMs;
// calculate the vertical position (sinus curve) // Determine if we are currently jumping or pausing
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop let currentLoopDistance = 0;
if (timeInCurrentLoop < jumpDurationMs) {
// set the new position // We are in the jumping phase
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`; currentLoopDistance = (timeInCurrentLoop / jumpDurationMs) * jumpDistanceVw;
if (progress < bunnyDuration) {
animationFrameId = requestAnimationFrame(animationStep);
} else { } else {
// let the bunny rest for a while before hiding easter eggs again // We are in the paused phase
const pauseDuration = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime; currentLoopDistance = jumpDistanceVw;
setTimeout(() => {
startTime = null;
animationFrameId = requestAnimationFrame(animationStep);
}, pauseDuration);
} }
currentX = startX + (completedLoops * jumpDistanceVw + currentLoopDistance) * direction;
// Update DOM without CSS transitions
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(() => {
animateRabbit(document.querySelector('#rabbit'));
}, restTime);
return;
}
rabbitTimeout = requestAnimationFrame(animationStep);
} }
animationFrameId = requestAnimationFrame(animationStep); // Start loop
rabbitTimeout = requestAnimationFrame(animationStep);
} }
function initializeEaster() {
if (!easter) return;
// initialize standard easter
function initEaster() {
const container = document.querySelector('.easter-container') || document.createElement("div"); const container = document.querySelector('.easter-container') || document.createElement("div");
if (!document.querySelector('.easter-container')) { if (!document.querySelector('.easter-container')) {
@@ -196,48 +266,17 @@ function initEaster() {
document.body.appendChild(container); document.body.appendChild(container);
} }
// shuffle the easter images createEasterGrassAndEggs(container);
let currentIndex = images.length; addHoppingRabbit(container);
let randomIndex;
while (currentIndex != 0) { // Add resize listener to regenerate meadow
randomIndex = Math.floor(Math.random() * currentIndex); window.addEventListener('resize', () => {
currentIndex--; if(document.querySelector('.easter-container')) {
[images[currentIndex], images[randomIndex]] = [images[randomIndex], images[currentIndex]]; 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(); 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(); 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,53 @@
.eid-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
contain: strict;
overflow: hidden;
}
.eid-symbol {
position: absolute;
user-select: none;
font-size: 1.5em;
}
.eid-symbol.floating-star {
opacity: 0;
animation: eid-twinkle 4s ease-in-out infinite;
mix-blend-mode: screen;
}
.lantern-rope {
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,96 @@
const config = window.SeasonalsPluginConfig?.Eid || {};
const eid = config.EnableEid !== undefined ? config.EnableEid : true;
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;
const lanternCount = Math.floor(Math.random() * 3) + 4; // 4 to 6 lanterns
// Create evenly spaced lanterns
const segmentWidth = 100 / lanternCount;
for (let i = 0; i < lanternCount; 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,101 @@
const config = window.SeasonalsPluginConfig?.Eurovision || {};
const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true;
const elementCount = config.SymbolCount || 25;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true;
const eurovisionColorsStr = config.EurovisionColors || '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff';
const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2;
let msgPrinted = false;
// Toggle Function
function toggleEurovision() {
const container = document.querySelector('.eurovision-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Eurovision hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Eurovision visible');
msgPrinted = false;
}
}
}
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,86 @@
.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 {
position: absolute;
top: -50%;
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;
}
/* 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 {
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;
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,6 +7,7 @@
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 10;
contain: layout paint;
} }
.rocket-trail { .rocket-trail {

View File

@@ -60,9 +60,9 @@ const observer = new MutationObserver(toggleFirework);
// start observation // start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) attributes: true
}); });

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;
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(spawnCat, Math.random() * 20000 + 5000);
}, (catWalkDurationSeconds * 1000) + 500); // Wait for duration + 500ms safety margin
}
// Initial spawn with random delay
setTimeout(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,74 @@
.frost-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
overflow: hidden;
contain: strict;
}
.frost-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
/* A glowing white-blue gradient from edges */
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;
}
/* Subtle repeating star/crystal pattern */
.frost-crystals {
position: absolute;
top: -5%;
left: -5%;
width: 110%;
height: 110%;
/* Use multi-layered star patterns for a random, crystalline spread */
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 out the center so crystals only appear strongly on the edges */
-webkit-mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
animation: frost-shimmer 6s infinite alternate ease-in-out;
}
@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;
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

@@ -7,7 +7,8 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 10000;
contain: layout paint;
} }
.halloween { .halloween {
@@ -34,11 +35,11 @@
@-webkit-keyframes halloween-fall { @-webkit-keyframes halloween-fall {
0% { 0% {
bottom: -10% bottom: -10%;
} }
100% { 100% {
bottom: 100% bottom: 110%;
} }
} }
@@ -58,11 +59,11 @@
@keyframes halloween-fall { @keyframes halloween-fall {
0% { 0% {
bottom: -10% bottom: -10%;
} }
100% { 100% {
bottom: 100% bottom: 110%;
} }
} }
@@ -81,71 +82,183 @@
.halloween:nth-of-type(0) { .halloween:nth-of-type(0) {
left: 1%; left: 1%;
-webkit-animation-delay: 0s, 0s; -webkit-animation-delay: 0s, 0s;
animation-delay: 0s, 0s animation-delay: 0s, 0s;
} }
.halloween:nth-of-type(1) { .halloween:nth-of-type(1) {
left: 10%; left: 10%;
-webkit-animation-delay: 1s, 1s; -webkit-animation-delay: -1s, -1s;
animation-delay: 1s, 1s animation-delay: -1s, -1s;
} }
.halloween:nth-of-type(2) { .halloween:nth-of-type(2) {
left: 20%; left: 20%;
-webkit-animation-delay: 6s, .5s; -webkit-animation-delay: -2s, -2s;
animation-delay: 6s, .5s animation-delay: -2s, -2s;
} }
.halloween:nth-of-type(3) { .halloween:nth-of-type(3) {
left: 30%; left: 30%;
-webkit-animation-delay: 4s, 2s; -webkit-animation-delay: -3s, -3s;
animation-delay: 4s, 2s animation-delay: -3s, -3s;
} }
.halloween:nth-of-type(4) { .halloween:nth-of-type(4) {
left: 40%; left: 40%;
-webkit-animation-delay: 2s, 2s; -webkit-animation-delay: -4s, -4s;
animation-delay: 2s, 2s animation-delay: -4s, -4s;
} }
.halloween:nth-of-type(5) { .halloween:nth-of-type(5) {
left: 50%; left: 50%;
-webkit-animation-delay: 8s, 3s; -webkit-animation-delay: -5s, -5s;
animation-delay: 8s, 3s animation-delay: -5s, -5s;
} }
.halloween:nth-of-type(6) { .halloween:nth-of-type(6) {
left: 60%; left: 60%;
-webkit-animation-delay: 6s, 2s; -webkit-animation-delay: -6s, -6s;
animation-delay: 6s, 2s animation-delay: -6s, -6s;
} }
.halloween:nth-of-type(7) { .halloween:nth-of-type(7) {
left: 70%; left: 70%;
-webkit-animation-delay: 2.5s, 1s; -webkit-animation-delay: -7s, -7s;
animation-delay: 2.5s, 1s animation-delay: -7s, -7s;
} }
.halloween:nth-of-type(8) { .halloween:nth-of-type(8) {
left: 80%; left: 80%;
-webkit-animation-delay: 1s, 0s; -webkit-animation-delay: -8s, -8s;
animation-delay: 1s, 0s animation-delay: -8s, -8s;
} }
.halloween:nth-of-type(9) { .halloween:nth-of-type(9) {
left: 90%; left: 90%;
-webkit-animation-delay: 3s, 1.5s; -webkit-animation-delay: -9s, -9s;
animation-delay: 3s, 1.5s animation-delay: -9s, -9s;
} }
.halloween:nth-of-type(10) { .halloween:nth-of-type(10) {
left: 25%; left: 25%;
-webkit-animation-delay: 2s, 0s; -webkit-animation-delay: -10s, -10s;
animation-delay: 2s, 0s animation-delay: -10s, -10s;
} }
.halloween:nth-of-type(11) { .halloween:nth-of-type(11) {
left: 65%; left: 65%;
-webkit-animation-delay: 4s, 2.5s; -webkit-animation-delay: -11s, -11s;
animation-delay: 4s, 2.5s animation-delay: -11s, -11s;
}
/* --- 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);
-webkit-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) {
left: -20vw;
animation: fog-float1 25s ease-in-out infinite alternate;
}
.halloween-fog-blob:nth-child(2) {
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; }
}
/* --- Spiders --- */
.halloween-spider-wrapper {
position: absolute;
top: -50px;
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 safely */
}
.halloween-thread {
width: 30px; /* Wider hit area for mouse interaction */
height: 100vh;
margin-top: -100vh;
position: relative;
cursor: pointer;
}
.halloween-thread::after {
content: '';
position: absolute;
left: 50%;
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 {
animation: spider-swing 3s ease-in-out infinite alternate;
transform-origin: top center;
}
/* MARK: SPIDER SWAY CONFIGURATION */
/* Adjust degrees in 'rotate(...)' to change how far spider and thread swing in wind. */
@keyframes wind-sway {
0% { transform: rotate(0deg); }
25% { transform: rotate(2deg); }
75% { transform: rotate(-2deg); }
100% { transform: rotate(0deg); }
}
@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); }
}
/* Mice */
.halloween-mouse {
position: absolute;
z-index: 10000;
pointer-events: none;
will-change: left;
}
@keyframes mouse-run-right {
0% { left: -10vw; }
100% { left: 110vw; }
}
@keyframes mouse-run-left {
0% { left: 110vw; }
100% { left: -10vw; }
} }

View File

@@ -4,8 +4,16 @@ const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols 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 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 enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true;
const enableMice = config.EnableMice !== undefined ? config.EnableMice : true;
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
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; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
// function to check and control the halloween // function to check and control the halloween
@@ -36,21 +44,13 @@ function toggleHalloween() {
// observe changes in the DOM // observe changes in the DOM
const observer = new MutationObserver(toggleHalloween); const observer = new MutationObserver(toggleHalloween);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) 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) { function addRandomSymbols(count) {
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
if (!halloweenContainer) return; // exit if halloween container is not found if (!halloweenContainer) return; // exit if halloween container is not found
@@ -74,7 +74,7 @@ function addRandomSymbols(count) {
// set random horizontal position, animation delay and size(uncomment lines to enable) // set random horizontal position, animation delay and size(uncomment lines to enable)
const randomLeft = Math.random() * 100; // position (0% to 100%) const randomLeft = Math.random() * 100; // position (0% to 100%)
const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s) const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s)
const randomAnimationDelay2 = Math.random() * 3; // delay (0s to 3s) const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s)
// apply styles // apply styles
halloweenDiv.style.left = `${randomLeft}%`; halloweenDiv.style.left = `${randomLeft}%`;
@@ -124,12 +124,137 @@ function createHalloween() {
} }
} }
// create fog layer
function createFog(container) {
const fogContainer = document.createElement('div');
fogContainer.className = 'halloween-fog-layer';
const fog1 = document.createElement('div');
fog1.className = 'halloween-fog-blob';
const fog2 = document.createElement('div');
fog2.className = 'halloween-fog-blob';
fogContainer.appendChild(fog1);
fogContainer.appendChild(fog2);
container.appendChild(fogContainer);
}
// 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();
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
}, 500);
});
wrapper.addEventListener('animationend', () => {
if (isRetreating) return;
wrapper.remove();
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();
setTimeout(() => createMouse(container), Math.random() * 4000 + 2000);
});
container.appendChild(mouse);
}
// initialize halloween // initialize halloween
function initializeHalloween() { function initializeHalloween() {
if (!halloween) return; // exit if halloween is disabled if (!halloween) return; // exit if halloween is disabled
createHalloween(); createHalloween();
toggleHalloween(); toggleHalloween();
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);
}
}
}
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices 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 if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
addRandomSymbols(halloweenCount); addRandomSymbols(halloweenCount);

View File

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

View File

@@ -6,6 +6,9 @@ const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? con
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols 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 heartsCount = config.SymbolCount || 25; // count of random extra symbols
// Array of hearts characters
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
// function to check and control the hearts // function to check and control the hearts
@@ -36,19 +39,13 @@ function toggleHearts() {
// observe changes in the DOM // observe changes in the DOM
const observer = new MutationObserver(toggleHearts); const observer = new MutationObserver(toggleHearts);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) attributes: true
}); });
// Array of hearts characters
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
function addRandomSymbols(count) { function addRandomSymbols(count) {
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
if (!heartsContainer) return; // exit if hearts container is not found if (!heartsContainer) return; // exit if hearts container is not found

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,77 @@
.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 {
animation: jump-arc 0.8s ease-in-out;
}
/* 8-bit coin styling */
.mario-coin {
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: 6px;
left: 10px;
width: 4px;
height: 12px;
background: #daa520;
}
@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,82 @@
const config = window.SeasonalsPluginConfig?.MarioDay || {};
const marioday = config.EnableMarioDay !== undefined ? config.EnableMarioDay : true;
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) {
const wrapper = document.createElement('div');
wrapper.className = 'mario-wrapper';
const mario = document.createElement('img');
mario.className = 'mario-runner';
mario.src = '../Seasonals/Resources/mario_assets/mario.gif';
wrapper.appendChild(mario);
container.appendChild(wrapper);
// Periodically throw out an 8-bit coin
setInterval(() => {
if (!document.querySelector('.marioday-container')) return;
const coin = document.createElement('div');
coin.className = 'mario-coin';
// Grab Mario's current screen position to lock the coin's X coordinate
const marioRect = wrapper.getBoundingClientRect();
coin.style.left = `${marioRect.left + 16}px`;
coin.style.bottom = '35px'; // Adjust for wrapper's 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: 1000;
overflow: hidden;
contain: layout paint;
}

View File

@@ -0,0 +1,164 @@
const config = window.SeasonalsPluginConfig?.Matrix || {};
const enabled = config.EnableMatrix !== undefined ? config.EnableMatrix : true;
const maxTrails = config.SymbolCount || 25;
const backgroundMode = config.EnableMatrixBackground !== undefined ? config.EnableMatrixBackground : false;
const matrixChars = config.MatrixChars || '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 (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,33 @@
.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 {
position: absolute;
top: -10%;
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;
}
@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,67 @@
const config = window.SeasonalsPluginConfig?.Oktoberfest || {};
const oktoberfest = config.EnableOktoberfest !== undefined ? config.EnableOktoberfest : true;
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;
}
}
}
// observe changes in the DOM
const observer = new MutationObserver(toggleOktoberfest);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
function createOktoberfest(container) {
for (let i = 0; i < 20; 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;
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);
}
createOktoberfest(container);
}
initializeOktoberfest();

View File

@@ -0,0 +1,71 @@
.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: -10vh;
animation: olympia-fall linear infinite;
font-size: 3rem;
opacity: 0.95;
text-shadow: 0 0 10px rgba(255,255,255,0.2);
}
.olympia-symbol img {
width: 6vh;
height: auto;
max-width: 60px;
object-fit: contain;
}
.olympia-confetti {
position: absolute;
top: -5vh;
width: 8px;
height: 16px;
opacity: 0.85;
animation: olympia-confetti-fall linear infinite;
border-radius: 4px; /* slightly rounder confetti */
}
@keyframes olympia-fall {
0% {
transform: translateY(-10vh) rotate(var(--start-rot, 0deg));
opacity: 0;
}
10% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateY(110vh) rotate(var(--end-rot, 360deg));
opacity: 0;
}
}
@keyframes olympia-confetti-fall {
0% {
transform: translateY(-5vh) rotateX(0deg) rotateY(0deg);
opacity: 0;
}
5% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(105vh) rotateX(720deg) rotateY(360deg);
opacity: 0;
}
}

View File

@@ -0,0 +1,135 @@
const config = window.SeasonalsPluginConfig?.Olympia || {};
const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true;
const symbolCount = config.SymbolCount || 25;
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
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;
const totalSymbols = symbolCount + standardCount;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
let finalCount = totalSymbols;
if (isMobile) {
finalCount = enableRandomMobile ? totalSymbols : standardCount;
}
const useRandomDuration = enableDifferentDuration !== false;
const activeItems = ['gold', 'silver', 'bronze', 'torch'];
for (let i = 0; i < finalCount; i++) {
let symbol = document.createElement('div');
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
symbol.className = `olympia-symbol olympia-${randomItem}`;
let img = document.createElement('img');
img.src = `../Seasonals/Resources/olympia_images/${randomItem}.png`;
img.onerror = function() {
this.style.display = 'none';
this.parentElement.innerHTML = getOlympiaEmojiFallback(randomItem);
};
symbol.appendChild(img);
const leftPos = Math.random() * 100;
const delaySeconds = Math.random() * 10;
let durationSeconds = 8;
if (useRandomDuration) {
durationSeconds = Math.random() * 5 + 6; // 6 to 11 seconds
}
const startRot = Math.random() * 360;
symbol.style.setProperty('--start-rot', `${startRot}deg`);
symbol.style.setProperty('--end-rot', `${startRot + (Math.random() > 0.5 ? 360 : -360)}deg`);
symbol.style.left = `${leftPos}vw`;
symbol.style.animationDuration = `${durationSeconds}s`;
symbol.style.animationDelay = `${delaySeconds}s`;
container.appendChild(symbol);
}
// Olympic Ring Colors
const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E'];
const confettiCount = isMobile ? 30 : 60;
for (let i = 0; i < confettiCount; i++) {
let confetti = document.createElement('div');
confetti.className = 'olympia-confetti';
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
confetti.style.backgroundColor = color;
const leftPos = Math.random() * 100;
const delaySeconds = Math.random() * 8;
const duration = Math.random() * 3 + 5;
confetti.style.left = `${leftPos}vw`;
confetti.style.animationDuration = `${duration}s`;
confetti.style.animationDelay = `${delaySeconds}s`;
container.appendChild(confetti);
}
}
function getOlympiaEmojiFallback(type) {
if (type === 'gold') return '🥇';
if (type === 'silver') return '🥈';
if (type === 'bronze') return '🥉';
if (type === 'torch') return '🔥';
return '';
}
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: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 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,67 @@
.oscar-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10; /* Behind popups but over background */
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 {
position: absolute;
top: -10vh;
/* 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;
}
.oscar-flash {
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;
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);
// Paparazzi flashes with randomized intervals
function flashLoop() {
if (!document.querySelector('.oscar-container')) {
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: 9999;
overflow: hidden;
contain: layout paint;
}
.pride-heart {
position: absolute;
bottom: -50px;
animation: pride-rise ease-in infinite;
will-change: transform;
}
@keyframes pride-rise {
0% { transform: translateY(0) scale(0.8); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-115vh) scale(1.2); opacity: 0; }
}
/* Coloring the Jellyfin Header */
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,92 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Pride || {};
const enabled = config.EnablePride !== undefined ? config.EnablePride : true;
const elementCount = config.HeartCount || 20;
const heartSize = config.HeartSize || 1.5;
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true;
let msgPrinted = false;
// 2. Toggle Function
// Hides the effect when a video player, trailer (in full width mode), dashboard, or user menu is active.
function togglePride() {
const container = document.querySelector('.pride-container');
if (!container) return;
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isDashboard = document.body.classList.contains('dashboardDocument');
const hasUserMenu = document.querySelector('#app-user-menu');
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Pride hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Pride visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
// Watches the DOM for changes so the effect can auto-hide/show.
const observer = new MutationObserver(togglePride);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
// Create and append your animated elements to the container.
function createElements() {
const container = document.querySelector('.pride-container') || document.createElement('div');
if (!document.querySelector('.pride-container')) {
container.className = 'pride-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
if (colorHeader) {
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);
}
}
// 5. Initialization
function initializePride() {
if (!enabled) return;
createElements();
togglePride();
}
initializePride();

View File

@@ -0,0 +1,26 @@
.rain-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.raindrop-pure {
position: absolute;
width: 2px;
height: 40px;
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
transform-origin: bottom;
}
@keyframes pure-rain {
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { transform: translateY(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;
const isMobile = window.innerWidth <= 768;
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
const rainSpeed = config.RainSpeed || 1.0;
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,68 @@
.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;
-webkit-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;
const enableRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
const enableRandomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const symbolCount = config.SymbolCount || 12;
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 || !enableRandomSymbols) return;
const isDesktop = window.innerWidth > 768;
if (!isDesktop && !enableRandomSymbolsMobile) 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() {
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(symbolCount - images.length, 0);
addSymbols(extraCount);
}
function initializeResurrection() {
if (!enableResurrection) return;
initResurrection();
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

@@ -11,6 +11,21 @@ const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa res
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents 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 const minPresentFallSpeed = 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 msgPrinted = false; // flag to prevent multiple console messages
let isMobile = false; // flag to detect mobile devices let isMobile = false; // flag to detect mobile devices
let canvas, ctx; // canvas and context for drawing snowflakes let canvas, ctx; // canvas and context for drawing snowflakes
@@ -52,14 +67,13 @@ function toggleSnowfall() {
// observe changes in the DOM // observe changes in the DOM
const observer = new MutationObserver(toggleSnowfall); const observer = new MutationObserver(toggleSnowfall);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) attributes: true
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -78,8 +92,12 @@ function initializeCanvas() {
container.appendChild(canvas); container.appendChild(canvas);
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
// Initial resize
resizeCanvas(container); resizeCanvas(container);
window.addEventListener('resize', () => resizeCanvas(container));
// Initialize ResizeObserver
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
resizeObserver.observe(container);
} }
function removeCanvas() { function removeCanvas() {
@@ -96,15 +114,37 @@ function removeCanvas() {
animationFrameIdSanta = null; animationFrameIdSanta = null;
console.log('Santa animation frame canceled'); console.log('Santa animation frame canceled');
} }
// Disconnect ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
console.log('Canvas removed'); console.log('Canvas removed');
} }
} }
function resizeCanvas(container) { function resizeCanvas(container) {
if (!canvas) return; if (!canvas) return;
const oldWidth = canvas.width;
const oldHeight = canvas.height;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
canvas.width = rect.width; canvas.width = rect.width;
canvas.height = rect.height; canvas.height = rect.height;
// Scale snowflakes positions if dimensions changed (to avoid clustering)
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
const scaleX = canvas.width / oldWidth;
const scaleY = canvas.height / oldHeight;
snowflakes.forEach(flake => {
flake.x *= scaleX;
flake.y *= scaleY;
});
}
} }
function createSnowflakes(container) { function createSnowflakes(container) {
@@ -152,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() { function createSantaElement() {
const santa = document.createElement('img'); const santa = document.createElement('img');
santa.src = santaImage; santa.src = santaImage;
@@ -210,7 +234,7 @@ function reloadSantaGif() {
function animateSanta() { function animateSanta() {
const santa = document.querySelector('.santa'); const santa = document.querySelector('.santa');
function startAnimation() { function startAnimation() {
const santaHeight = santa.offsetHeight; const santaHeight = santa.offsetHeight;
if (santaHeight === 0) { if (santaHeight === 0) {

View File

@@ -1,206 +1,513 @@
// theme-configs.js /*
* Seasonals Plugin (Client Side Manager Logic)
*/
// theme configurations const ThemeConfigs = {
const themeConfigs = {
snowflakes: { snowflakes: {
css: '/Seasonals/Resources/snowflakes.css', css: '../Seasonals/Resources/snowflakes.css',
js: '/Seasonals/Resources/snowflakes.js', js: '../Seasonals/Resources/snowflakes.js',
containerClass: 'snowflakes' containerClass: 'snowflakes'
}, },
snowfall: { snowfall: {
css: '/Seasonals/Resources/snowfall.css', css: '../Seasonals/Resources/snowfall.css',
js: '/Seasonals/Resources/snowfall.js', js: '../Seasonals/Resources/snowfall.js',
containerClass: 'snowfall-container' containerClass: 'snowfall-container'
}, },
snowstorm: { snowstorm: {
css: '/Seasonals/Resources/snowstorm.css', css: '../Seasonals/Resources/snowstorm.css',
js: '/Seasonals/Resources/snowstorm.js', js: '../Seasonals/Resources/snowstorm.js',
containerClass: 'snowstorm-container' containerClass: 'snowstorm-container'
}, },
fireworks: { fireworks: {
css: '/Seasonals/Resources/fireworks.css', css: '../Seasonals/Resources/fireworks.css',
js: '/Seasonals/Resources/fireworks.js', js: '../Seasonals/Resources/fireworks.js',
containerClass: 'fireworks' containerClass: 'fireworks'
}, },
halloween: { halloween: {
css: '/Seasonals/Resources/halloween.css', css: '../Seasonals/Resources/halloween.css',
js: '/Seasonals/Resources/halloween.js', js: '../Seasonals/Resources/halloween.js',
containerClass: 'halloween-container' containerClass: 'halloween-container'
}, },
hearts: { hearts: {
css: '/Seasonals/Resources/hearts.css', css: '../Seasonals/Resources/hearts.css',
js: '/Seasonals/Resources/hearts.js', js: '../Seasonals/Resources/hearts.js',
containerClass: 'hearts-container' containerClass: 'hearts-container'
}, },
christmas: { christmas: {
css: '/Seasonals/Resources/christmas.css', css: '../Seasonals/Resources/christmas.css',
js: '/Seasonals/Resources/christmas.js', js: '../Seasonals/Resources/christmas.js',
containerClass: 'christmas-container' containerClass: 'christmas-container'
}, },
santa: { santa: {
css: '/Seasonals/Resources/santa.css', css: '../Seasonals/Resources/santa.css',
js: '/Seasonals/Resources/santa.js', js: '../Seasonals/Resources/santa.js',
containerClass: 'santa-container' containerClass: 'santa-container'
}, },
autumn: { autumn: {
css: '/Seasonals/Resources/autumn.css', css: '../Seasonals/Resources/autumn.css',
js: '/Seasonals/Resources/autumn.js', js: '../Seasonals/Resources/autumn.js',
containerClass: 'autumn-container' containerClass: 'autumn-container'
}, },
easter: { easter: {
css: '/Seasonals/Resources/easter.css', css: '../Seasonals/Resources/easter.css',
js: '/Seasonals/Resources/easter.js', js: '../Seasonals/Resources/easter.js',
containerClass: 'easter-container' containerClass: 'easter-container'
}, },
resurrection: {
css: '../Seasonals/Resources/resurrection.css',
js: '../Seasonals/Resources/resurrection.js',
containerClass: 'resurrection-container'
},
summer: { summer: {
css: '/Seasonals/Resources/summer.css', css: '../Seasonals/Resources/summer.css',
js: '/Seasonals/Resources/summer.js', js: '../Seasonals/Resources/summer.js',
containerClass: 'summer-container' containerClass: 'summer-container'
}, },
spring: { spring: {
css: '/Seasonals/Resources/spring.css', css: '../Seasonals/Resources/spring.css',
js: '/Seasonals/Resources/spring.js', js: '../Seasonals/Resources/spring.js',
containerClass: 'spring-container' 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: { none: {
containerClass: 'none' containerClass: 'none'
}, },
}; };
// determine current theme based on the current month const SeasonalSettingsManager = {
function determineCurrentTheme() { initialized: false,
const date = new Date(); config: null,
const month = date.getMonth(); // 0-11
const day = date.getDate(); // 1-31
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5 init(config) {
if (this.initialized) return;
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18 this.config = config;
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27 // Only inject settings if enabled on server by admin
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27 if (this.config && this.config.EnableClientSideToggle !== false) {
this.injectSettingsIcon();
if (month === 11) return 'snowflakes'; // snowflakes december this.initialized = true;
if (month === 0 || month === 1) return 'snowfall'; // snow january, february console.log("Seasonals: Client-Side Settings Manager initialized.");
// 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
getSetting(key, defaultValue) {
//NOT IMPLEMENTED YET const value = localStorage.getItem(`seasonals-${key}`);
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may return value !== null ? value : defaultValue;
},
//NOT IMPLEMENTED YET
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august setSetting(key, value) {
localStorage.setItem(`seasonals-${key}`, value);
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 createIcon() {
const button = document.createElement('button');
return 'none'; // Fallback (nothing) button.type = 'button';
} button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
button.title = 'Seasonal Settings';
// load theme csss // button.innerHTML = '<span class="material-icons">ac_unit</span>';
function loadThemeCSS(cssPath) { button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
if (!cssPath) return; button.style.verticalAlign = 'middle';
const link = document.createElement('link'); button.addEventListener('click', (e) => {
link.rel = 'stylesheet'; e.stopPropagation();
link.href = cssPath; this.toggleSettingsPopup(button);
});
link.onerror = () => {
console.error(`Failed to load CSS: ${cssPath}`); return button;
}; },
document.body.appendChild(link); injectSettingsIcon() {
console.log(`CSS file "${cssPath}" loaded.`); const observer = new MutationObserver((mutations, obs) => {
} const headerRight = document.querySelector('.headerRight');
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
// load theme js const icon = this.createIcon();
function loadThemeJS(jsPath) { headerRight.prepend(icon);
if (!jsPath) return; }
});
const script = document.createElement('script');
script.src = jsPath; observer.observe(document.body, {
script.defer = true; childList: true,
subtree: true
script.onerror = () => { });
console.error(`Failed to load JS: ${jsPath}`); },
};
createPopup(anchorElement) {
document.body.appendChild(script); const existing = document.querySelector('.seasonal-settings-popup');
console.log(`JS file "${jsPath}" loaded.`); if (existing) existing.remove();
}
const popup = document.createElement('div');
// update theme container class name popup.className = 'seasonal-settings-popup dialog';
function updateThemeContainer(containerClass) {
// Create container if it doesn't exist Object.assign(popup.style, {
let container = document.querySelector('.seasonals-container'); position: 'fixed',
if (!container) { zIndex: '10000',
container = document.createElement('div'); backgroundColor: '#202020',
container.className = 'seasonals-container'; padding: '1em',
document.body.appendChild(container); borderRadius: '0.3em',
} boxShadow: '0 0 20px rgba(0,0,0,0.5)',
minWidth: '200px',
container.className = `seasonals-container ${containerClass}`; color: '#fff',
console.log(`Seasonals-Container class updated to "${containerClass}".`); maxWidth: '250px'
} });
function removeSelf() { const rect = anchorElement.getBoundingClientRect();
const script = document.currentScript;
if (script) script.parentNode.removeChild(script); // Positioning logic
console.log('External script removed:', script); let rightPos = window.innerWidth - rect.right;
} if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
popup.style.right = '1rem';
// initialize theme popup.style.left = 'auto';
async function initializeTheme() { } else {
let automateThemeSelection = true; popup.style.right = `${rightPos}px`;
let defaultTheme = 'none'; popup.style.left = 'auto';
}
try { popup.style.top = `${rect.bottom + 10}px`;
const response = await fetch('/Seasonals/Config');
if (response.ok) { // Popup HTML
const config = await response.json(); let html = `
automateThemeSelection = config.AutomateSeasonSelection; <h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
defaultTheme = config.SelectedSeason;
window.SeasonalsPluginConfig = config; <div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
console.log('Seasonals Config loaded:', config); <label class="emby-checkbox-label">
} else { <input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
console.error('Failed to fetch Seasonals config'); <span class="checkboxLabel">Enable Seasonals</span>
</label>
</div>
<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 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 => {
if (key === 'none') return;
const option = document.createElement('option');
option.value = key;
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
themeSelect.appendChild(option);
});
// Set Initial Values
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
themeSelect.value = this.getSetting('theme', 'auto');
// Event Listeners
enabledCheckbox.addEventListener('change', (e) => {
this.setSetting('enabled', e.target.checked);
location.reload();
});
themeSelect.addEventListener('change', (e) => {
this.setSetting('theme', e.target.value);
location.reload();
});
// Close on outside click
const closeHandler = (e) => {
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
popup.remove();
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
document.body.appendChild(popup);
},
toggleSettingsPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) {
existing.remove();
} else {
this.createPopup(anchorElement);
} }
} catch (error) {
console.error('Error fetching Seasonals config:', error);
} }
};
let currentTheme; const SeasonalsManager = {
if (automateThemeSelection === false) { config: null,
currentTheme = defaultTheme;
} else {
currentTheme = determineCurrentTheme();
}
console.log(`Selected theme: ${currentTheme}`); 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;
}
if (!currentTheme || currentTheme === 'none') { console.log('Seasonals: Seasonals Config loaded:', this.config);
console.log('No theme selected.'); }
removeSelf(); } catch (error) {
return; console.error('Seasonals: Error fetching Seasonals config:', error);
} }
const theme = themeConfigs[currentTheme]; // Initialize Settings UI
SeasonalSettingsManager.init(this.config);
// 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;
}
}
if (!theme) { return 'none'; // No rule matched
console.error(`Theme "${currentTheme}" not found.`); },
return;
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);
}
} }
updateThemeContainer(theme.containerClass); };
if (theme.css) loadThemeCSS(theme.css); SeasonalsManager.init();
if (theme.js) loadThemeJS(theme.js);
console.log(`Theme "${currentTheme}" loaded.`);
removeSelf();
}
initializeTheme();

View File

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

View File

@@ -47,14 +47,13 @@ function toggleSnowfall() {
// observe changes in the DOM // observe changes in the DOM
const observer = new MutationObserver(toggleSnowfall); const observer = new MutationObserver(toggleSnowfall);
// start observation
observer.observe(document.body, { observer.observe(document.body, {
childList: true, // observe adding/removing of child elements childList: true,
subtree: true, // observe all levels of the DOM tree subtree: true,
attributes: true // observe changes to attributes (e.g. class changes) attributes: true
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -73,8 +72,12 @@ function initializeCanvas() {
container.appendChild(canvas); container.appendChild(canvas);
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
// Initial resize
resizeCanvas(container); resizeCanvas(container);
window.addEventListener('resize', () => resizeCanvas(container));
// Initialize ResizeObserver
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
resizeObserver.observe(container);
} }
function removeCanvas() { function removeCanvas() {
@@ -86,15 +89,37 @@ function removeCanvas() {
animationFrameId = null; animationFrameId = null;
console.log('Animation frame canceled'); console.log('Animation frame canceled');
} }
// Disconnect ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
console.log('Canvas removed'); console.log('Canvas removed');
} }
} }
function resizeCanvas(container) { function resizeCanvas(container) {
if (!canvas) return; if (!canvas) return;
const oldWidth = canvas.width;
const oldHeight = canvas.height;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
canvas.width = rect.width; canvas.width = rect.width;
canvas.height = rect.height; canvas.height = rect.height;
// Scale snowflakes positions if dimensions changed (to avoid clustering)
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
const scaleX = canvas.width / oldWidth;
const scaleY = canvas.height / oldHeight;
snowflakes.forEach(flake => {
flake.x *= scaleX;
flake.y *= scaleY;
});
}
} }
function createSnowflakes(container) { function createSnowflakes(container) {

View File

@@ -8,12 +8,15 @@
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 10;
contain: layout paint;
} }
.snowflake { .snowflake {
position: fixed; position: fixed;
z-index: 15; z-index: 15;
top: -10%; top: 0;
will-change: transform;
translate: 0 -10vh;
font-size: 1em; font-size: 1em;
color: #fff; color: #fff;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@@ -33,11 +36,11 @@
@-webkit-keyframes snowflakes-fall { @-webkit-keyframes snowflakes-fall {
0% { 0% {
top: -10%; translate: 0 -10vh;
} }
100% { 100% {
top: 100%; translate: 0 110vh;
} }
} }
@@ -45,23 +48,21 @@
0%, 0%,
100% { 100% {
-webkit-transform: translateX(0);
transform: translateX(0); transform: translateX(0);
} }
50% { 50% {
-webkit-transform: translateX(80px);
transform: translateX(80px); transform: translateX(80px);
} }
} }
@keyframes snowflakes-fall { @keyframes snowflakes-fall {
0% { 0% {
top: -10%; translate: 0 -10vh;
} }
100% { 100% {
top: 100%; translate: 0 110vh;
} }
} }

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