Compare commits

..

329 Commits

Author SHA1 Message Date
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
CodeDevMLH
2d2dcaee71 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:21:50 +00:00
CodeDevMLH
399b19f384 fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-01-06 23:20:57 +01:00
CodeDevMLH
cba5bc8c95 add media bar
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 46s
2026-01-06 23:12:45 +01:00
CodeDevMLH
0c97e2f4c4 Update build workflow to include branch specification for push events 2026-01-06 23:10:35 +01:00
CodeDevMLH
631fbfb1cb Refactor SeasonalsPlugin: Simplify script injection logic (v1.5.0.0)
Some checks failed
🏗️ Build Plugin / build (push) Failing after 55s
Auto Release Plugin / build-and-release (push) Failing after 1m5s
2026-01-06 22:59:18 +01:00
CodeDevMLH
5f14caeeb7 Add central manifest update step to release automation workflow 2026-01-06 22:14:09 +01:00
CodeDevMLH
bb4d35fc00 Update manifest.json for release v1.4.1.0 [skip ci] 2026-01-06 21:12:47 +00:00
CodeDevMLH
a30729cf0b small fixes
All checks were successful
🏗️ Build Plugin / build (push) Successful in 58s
Auto Release Plugin / build-and-release (push) Successful in 1m9s
2026-01-06 22:11:34 +01:00
CodeDevMLH
83bc5f6b1d Update manifest.json for release v1.4.1.0 [skip ci] 2026-01-06 20:29:59 +00:00
CodeDevMLH
8bcfaee22e Update logos for improved branding
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m4s
🏗️ Build Plugin / build (push) Successful in 1m8s
2026-01-06 21:28:51 +01:00
CodeDevMLH
6882922c5b Update release guide to remove build.yaml version update instructions 2026-01-02 22:58:19 +01:00
CodeDevMLH
35ea057110 modded logo
Some checks failed
🏗️ Build Plugin / build (push) Successful in 59s
Auto Release Plugin / build-and-release (push) Failing after 1m6s
2026-01-02 22:58:09 +01:00
CodeDevMLH
2656606f8c Add new build workflow for Jellyfin Plugin and deprecate old configuration 2026-01-02 21:33:19 +01:00
CodeDevMLH
024343f045 fix wrong plugin name in build workflow 2026-01-02 21:27:52 +01:00
CodeDevMLH
887e697a90 Remove unused environment variable from build workflow 2026-01-02 21:26:16 +01:00
CodeDevMLH
6e623668d5 Add build workflow for Jellyfin Plugin with artifact upload 2026-01-02 21:23:35 +01:00
CodeDevMLH
17c60d2fb3 Remove internal build workflow configuration 2026-01-02 19:51:31 +01:00
CodeDevMLH
b4ce6a7d18 Rename artifact upload name to 'plugin-build-artifact' for clarity 2026-01-02 19:50:33 +01:00
CodeDevMLH
d4ec7cf31d Update artifact upload action and retention period in build workflow 2026-01-02 19:48:08 +01:00
CodeDevMLH
7f01da0301 Downgrade upload-artifact action to v4 for compatibility 2026-01-02 19:46:20 +01:00
CodeDevMLH
0e19769d79 Downgrade upload-artifact action to v5 for compatibility 2026-01-02 19:43:31 +01:00
CodeDevMLH
0cda35f636 Add environment variable to build job for package management 2026-01-02 19:37:45 +01:00
CodeDevMLH
f8e30efe66 Update .NET version specification in build workflow to use "9.x" 2026-01-02 19:34:19 +01:00
CodeDevMLH
4160ac59ca Update jellyfin-plugin-repository-manager to version 1.1.1 in build workflow 2026-01-02 19:32:39 +01:00
CodeDevMLH
62ebe8b293 Refactor build workflow to streamline plugin build process and enhance artifact management 2026-01-02 19:25:49 +01:00
CodeDevMLH
fd891e84c1 Update release automation to ignore all markdown files 2025-12-31 01:38:04 +01:00
CodeDevMLH
de211d4567 Update release automation to ignore changes in the .gitea directory 2025-12-31 01:26:19 +01:00
CodeDevMLH
dc292504b0 Update manifest.json for release v1.4.1.0 [skip ci] 2025-12-31 00:24:59 +00:00
CodeDevMLH
9cf4da92ac Add GitHub Actions workflows for building and releasing the Jellyfin plugin
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2025-12-31 01:24:07 +01:00
CodeDevMLH
817b8786c8 Update manifest.json for release v1.4.1.0 [skip ci] 2025-12-31 00:19:26 +00:00
CodeDevMLH
fe7f1a30bb test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m8s
2025-12-31 01:18:17 +01:00
MLH
2d72c6e957 .github/renovate.json gelöscht 2025-12-31 01:17:37 +01:00
MLH
e8c4d5a044 .github/workflows/test.yaml gelöscht 2025-12-31 01:15:14 +01:00
MLH
781c605e12 .github/workflows/test.yaml aktualisiert
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 11s
2025-12-31 01:13:17 +01:00
CodeDevMLH
c712777d61 Fix workflow path for internal build and add build_internal.yaml
Some checks failed
🏗️ Build Plugin / call (push) Failing after 1m10s
2025-12-31 00:35:10 +01:00
MLH
9755763e36 .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
2025-12-31 00:30:55 +01:00
MLH
3d24c84c0f .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
2025-12-31 00:30:17 +01:00
MLH
73b64c7302 Dateien nach ".github" hochladen
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
🏗️ Build Plugin / test_connection (push) Successful in 3s
2025-12-31 00:29:28 +01:00
MLH
f42bba5dc3 .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 1s
🏗️ Build Plugin / test_connection (push) Successful in 3s
2025-12-31 00:21:42 +01:00
MLH
8095196b1e .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
🏗️ Build Plugin / test_connection (push) Successful in 3s
2025-12-30 22:42:13 +01:00
MLH
41ef12d970 .github/workflows/build.yaml aktualisiert 2025-12-30 22:41:13 +01:00
MLH
c984464f78 .github/workflows/test.yaml hinzugefügt
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
2025-12-30 22:37:28 +01:00
CodeDevMLH
29af8b8671 add gitea actions
Some checks failed
🏗️ Build Plugin / call (push) Failing after 1s
2025-12-30 22:16:56 +01:00
CodeDevMLH
2df173de8e remove build from gitignore 2025-12-30 21:54:12 +01:00
CodeDevMLH
6ea83e777a Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2025-12-30 16:00:05 +01:00
CodeDevMLH
96d847a5f8 fix: comment out changelog body in release automation 2025-12-30 16:00:03 +01:00
CodeDevMLH
2e6a1c9322 feat: Add GitHub Actions workflow for automated plugin releases. 2025-12-30 15:44:43 +01:00
CodeDevMLH
90e028eb3b feat: Add GitHub Actions workflow for automated plugin releases. 2025-12-30 15:26:47 +01:00
CodeDevMLH
0db833c4dc fix: improve fireworks display handling for scrolling and container positioning 2025-12-30 02:09:18 +01:00
CodeDevMLH
8c00a35a22 fix: update checksum and timestamp for version 1.4.1.0 in manifest 2025-12-30 01:46:50 +01:00
CodeDevMLH
8d6d202ee9 fix: update checksum and timestamp for version 1.4.1.0 in manifest 2025-12-30 00:12:44 +01:00
CodeDevMLH
d9a4f623a0 fix: update checksum and timestamp for version 1.4.1.0 in manifest 2025-12-29 23:21:33 +01:00
CodeDevMLH
6d32293127 feat: update manifest for version 1.4.1.0 and fix fireworks display issue on scroll 2025-12-29 23:06:18 +01:00
CodeDevMLH
7e33cb8fb7 fix: remove MenuIcon property from plugin configuration 2025-12-28 20:38:21 +01:00
CodeDevMLH
15d6f98427 fix: update legacy script tag comment and modify checksum in manifest 2025-12-28 20:11:33 +01:00
CodeDevMLH
3b1d0982e5 feat: update version to 1.4.0.0 in project file and manifest 2025-12-28 18:58:21 +01:00
CodeDevMLH
737e510a6c feat: add enable/disable toggle for the plugin and update configuration handling 2025-12-28 18:53:25 +01:00
CodeDevMLH
fbd77e56fd feat: add SeasonalsPlugin class and update manifest for version 1.4.0.0 2025-12-28 17:43:09 +01:00
CodeDevMLH
63dadb4ffa feat: update version to 1.3.4.0 in Jellyfin.Plugin.Seasonals.csproj
Some checks failed
🔬 Run CodeQL / call (push) Has been cancelled
2025-12-24 03:29:49 +01:00
CodeDevMLH
7167472f10 feat: update version to 1.3.4.0 in build.yaml 2025-12-24 03:27:15 +01:00
MLH
21b0382cfd bin/Publish/Jellyfin.Plugin.Seasonals.zip gelöscht 2025-12-24 03:04:50 +01:00
MLH
496d274a56 bin/Publish/Jellyfin.Plugin.Seasonals.pdb gelöscht 2025-12-24 03:04:46 +01:00
MLH
3cce9dd3f1 bin/Publish/Jellyfin.Plugin.Seasonals.dll gelöscht 2025-12-24 03:04:43 +01:00
MLH
9beb14d6b6 bin/Publish/Jellyfin.Plugin.Seasonals.deps.json gelöscht 2025-12-24 03:04:39 +01:00
CodeDevMLH
b311fe4e2a feat: update disabled select option styles and manifest for version 1.3.4.0 2025-12-24 03:03:22 +01:00
CodeDevMLH
70eac57e4d feat: add styles for disabled select options in configuration page 2025-12-24 02:54:27 +01:00
CodeDevMLH
4e64e863e3 feat: update seasonal options in config page and improve descriptions; add version 1.3.4.0 to manifest 2025-12-24 02:41:56 +01:00
CodeDevMLH
726d172625 feat: add client compatibility section to README for better user guidance 2025-12-20 23:18:13 +01:00
CodeDevMLH
1c6ed0aaed fix typo 2025-12-19 00:13:53 +01:00
CodeDevMLH
26eecc9ec4 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0
Some checks failed
🔬 Run CodeQL / call (push) Has been cancelled
2025-12-18 01:12:03 +01:00
CodeDevMLH
912bf7f544 feat: update z-index values for seasonal elements to improve layering and visibility 2025-12-18 01:11:29 +01:00
CodeDevMLH
0e37d3a291 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0 2025-12-18 00:53:19 +01:00
CodeDevMLH
83a8c7fc74 feat: add z-index to seasonal styles for improved layering 2025-12-18 00:52:45 +01:00
CodeDevMLH
b05b35b8f8 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0 2025-12-18 00:32:50 +01:00
CodeDevMLH
e90ea952bd feat: update seasonal styles to use fixed positioning and full coverage for better display 2025-12-18 00:32:19 +01:00
CodeDevMLH
7216588fef feat: enhance autumn and snowflakes styles with fixed positioning and full coverage 2025-12-18 00:21:30 +01:00
CodeDevMLH
f4a7c3f2e5 feat: update plugin checksum and timestamp in manifest for version 1.3.3.0 2025-12-18 00:01:59 +01:00
CodeDevMLH
176aa97c1c feat: update configuration keys to use PascalCase for consistency across seasonal scripts 2025-12-18 00:01:19 +01:00
CodeDevMLH
d73b1d17ff feat: update version to 1.3.3.0 and modify manifest with new changelog, checksum, and timestamp 2025-12-17 23:36:09 +01:00
CodeDevMLH
4bb37f89f3 typo 2025-12-17 22:47:50 +01:00
CodeDevMLH
f67e08ef89 feat: update plugin version to 1.3.2.0 and modify manifest with new checksum and timestamp 2025-12-17 22:47:16 +01:00
CodeDevMLH
94709b63fb bump version 2025-12-17 22:42:21 +01:00
CodeDevMLH
988368b6f5 feat: update manifest with new checksum and timestamp for version 1.3.0.0 2025-12-17 22:31:18 +01:00
CodeDevMLH
2216ed90c8 fix changelog 2025-12-17 22:30:38 +01:00
CodeDevMLH
607042536c feat: fix image paths for seasonal effects and enhance z-index for better visibility 2025-12-17 22:30:31 +01:00
CodeDevMLH
009bd75374 feat: update seasonal effects settings and enhance z-index for better visibility 2025-12-17 22:23:41 +01:00
CodeDevMLH
490a61fc1f feat: add theme settings section to README for advanced customization options 2025-12-17 22:15:09 +01:00
CodeDevMLH
2e7df40241 revert test 2025-12-17 22:09:30 +01:00
CodeDevMLH
bd8b67c2ca test string 2025-12-17 22:03:08 +01:00
CodeDevMLH
93a59a832e feat: add version 1.2.0.0 with advanced settings for seasonal effects customization 2025-12-17 21:59:57 +01:00
CodeDevMLH
fbf9b2189b feat: enhance configuration page with seasonal options and improve user guidance 2025-12-17 21:58:48 +01:00
CodeDevMLH
0df664302d chore: remove deprecated manifest-v1.json file 2025-12-17 17:52:28 +01:00
CodeDevMLH
9db00ae365 fix: revert version to 1.1.0.0 and update source URL in manifest.json 2025-12-17 17:51:18 +01:00
CodeDevMLH
ae9f718f46 docs: Update README to include Configuration and Troubleshooting sections 2025-12-17 17:49:36 +01:00
84 changed files with 6603 additions and 2513 deletions

View File

@@ -0,0 +1,45 @@
name: '🏗️ Build Plugin'
on:
push:
branches:
- dev
paths-ignore:
- '**/*.md'
- '.gitea/**'
- '.github/**'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
pull_request:
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.x"
- name: Build Jellyfin Plugin
run: |
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
- name: Upload Artifact
uses: christopherHX/gitea-upload-artifact@v4
with:
name: plugin-build-artifact
retention-days: 5
if-no-files-found: error
path: bin/Publish/Jellyfin.Plugin.Seasonals.zip

View File

@@ -0,0 +1,45 @@
name: '🏗️ Build Plugin'
on:
push:
paths-ignore:
- '**/*.md'
- '.gitea/**'
- '.github/**'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
pull_request:
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.x"
- name: Build Jellyfin Plugin
uses: oddstr13/jellyfin-plugin-repository-manager@v1.1.1
id: jprm
with:
dotnet-target: "net9.0"
- name: Upload Artifact
uses: christopherHX/gitea-upload-artifact@v4
with:
name: plugin-build-artifact
retention-days: 5
if-no-files-found: error
path: ${{ steps.jprm.outputs.artifact }}

View File

@@ -0,0 +1,216 @@
name: Auto Release Plugin
on:
push:
branches:
- main
paths-ignore:
- '.gitea/**'
- '.github/**'
- '*.md'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
- name: Read Version from Manifest
id: read_version
run: |
VERSION=$(jq -r '.[0].versions[0].version' manifest.json)
CHANGELOG=$(jq -r '.[0].versions[0].changelog' manifest.json)
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' manifest.json)
echo "Detected Version: $VERSION"
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
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Check if Release Already Exists
id: check_release
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
TAG="v$VERSION"
SERVER_URL="https://git.mahom03-spacecloud.de"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "Release $TAG already exists. Skipping release-related steps."
echo "release_exists=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "No existing release for $TAG. Continuing."
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "Unexpected response when checking release: $HTTP_STATUS"
exit 1
fi
- name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash
run: |
# Inject version from manifest into the build
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish /p:Version=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }}
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
cd ../..
# Calculate hash
HASH=$(md5sum bin/Publish/Jellyfin.Plugin.Seasonals.zip | awk '{ print $1 }')
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Export variables for next steps
echo "ZIP_HASH=$HASH" >> $GITHUB_ENV
echo "BUILD_TIME=$TIME" >> $GITHUB_ENV
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
- name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
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 "Hash: ${{ env.ZIP_HASH }}"
echo "Time: ${{ env.BUILD_TIME }}"
echo "Url: $DOWNLOAD_URL"
jq --arg hash "${{ env.ZIP_HASH }}" \
--arg time "${{ env.BUILD_TIME }}" \
--arg url "$DOWNLOAD_URL" \
'.[0].versions[0].checksum = $hash | .[0].versions[0].timestamp = $time | .[0].versions[0].sourceUrl = $url' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json
- name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: akkuman/gitea-release-action@v1
with:
server_url: "https://git.mahom03-spacecloud.de"
body: ${{ env.CHANGELOG }}
token: ${{ secrets.GITHUB_TOKEN }}
files: ${{ env.ZIP_PATH }}
name: "v${{ env.VERSION }}"
tag_name: "v${{ env.VERSION }}"
draft: false
prerelease: false
# Update Message in Remote Repository
- name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6
with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
path: central-manifest
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash
run: |
cd central-manifest
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
# 1. Get info from previous steps
VERSION="${{ env.VERSION }}"
HASH="${{ env.ZIP_HASH }}"
TIME="${{ env.BUILD_TIME }}"
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
# 2. Get info from env
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
CHANGELOG="${{ env.CHANGELOG }}"
TARGET_ABI="${{ env.TARGET_ABI }}"
echo "Updating Central Manifest for Plugin GUID: $PLUGIN_GUID"
# 3. Update/Prepend entry in central manifest.json
# Logic:
# - If array is empty or new version != old version: PREPEND new entry
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
jq --arg guid "$PLUGIN_GUID" \
--arg hash "$HASH" \
--arg time "$TIME" \
--arg url "$DOWNLOAD_URL" \
--arg ver "$VERSION" \
--arg changelog "$CHANGELOG" \
--arg abi "$TARGET_ABI" \
'map(if .guid == $guid then
if .versions[0].version != $ver then
# New Version -> Prepend
.versions = [{
"version": $ver,
"changelog": $changelog,
"targetAbi": $abi,
"sourceUrl": $url,
"checksum": $hash,
"timestamp": $time
}] + .versions
else
# Same Version -> Update existing (overwrite top)
.versions[0].changelog = $changelog |
.versions[0].targetAbi = $abi |
.versions[0].sourceUrl = $url |
.versions[0].checksum = $hash |
.versions[0].timestamp = $time
end
else . end)' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: |
cd central-manifest
git config user.name "CodeDevMLH"
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
# Check if there are changes
if [[ -n $(git status -s) ]]; then
git add manifest.json
git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
git push
else
echo "No changes to central manifest."
fi

View File

@@ -3,16 +3,43 @@ name: '🏗️ Build Plugin'
on: on:
push: push:
branches: branches:
- master - dev
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
- '.gitea/**'
- '.github/**'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
pull_request: pull_request:
branches:
- master
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
call: build:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.x"
- name: Build Jellyfin Plugin
run: |
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: plugin-build-artifact
retention-days: 5
if-no-files-found: error
path: bin/Publish/Jellyfin.Plugin.Seasonals.zip

View File

@@ -1,20 +0,0 @@
name: '📝 Create/Update Release Draft & Release Bump PR'
on:
push:
branches:
- master
paths-ignore:
- build.yaml
workflow_dispatch:
repository_dispatch:
types:
- update-prep-command
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/changelog.yaml@master
with:
repository-name: jellyfin/jellyfin-plugin-template
secrets:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,13 +0,0 @@
# Allows for the definition of PR and Issue /commands
name: '📟 Slash Command Dispatcher'
on:
issue_comment:
types:
- created
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-dispatch.yaml@master
secrets:
token: .

View File

@@ -1,16 +0,0 @@
name: '🔀 PR Rebase Command'
on:
repository_dispatch:
types:
- rebase-command
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-rebase.yaml@master
with:
rebase-head: ${{ github.event.client_payload.pull_request.head.label }}
repository-full-name: ${{ github.event.client_payload.github.payload.repository.full_name }}
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
secrets:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,18 +0,0 @@
name: '🚀 Publish Plugin'
on:
release:
types:
- released
workflow_dispatch:
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/publish.yaml@master
with:
version: ${{ github.event.release.tag_name }}
is-unstable: ${{ github.event.release.prerelease }}
secrets:
deploy-host: ${{ secrets.DEPLOY_HOST }}
deploy-user: ${{ secrets.DEPLOY_USER }}
deploy-key: ${{ secrets.DEPLOY_KEY }}

184
.github/workflows/release_automation.yml vendored Normal file
View File

@@ -0,0 +1,184 @@
name: Auto Release Plugin
on:
push:
branches:
- main
paths-ignore:
- '.github/**'
- 'README.md'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
- name: Read Version from Manifest
id: read_version
run: |
VERSION=$(jq -r '.[0].versions[0].version' manifest.json)
CHANGELOG=$(jq -r '.[0].versions[0].changelog' manifest.json)
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' manifest.json)
echo "Detected Version: $VERSION"
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
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Build and Zip
shell: bash
run: |
# Inject version from manifest into the build
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish /p:Version=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }}
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
cd ../..
# Calculate hash
HASH=$(md5sum bin/Publish/Jellyfin.Plugin.Seasonals.zip | awk '{ print $1 }')
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Export variables for next steps
echo "ZIP_HASH=$HASH" >> $GITHUB_ENV
echo "BUILD_TIME=$TIME" >> $GITHUB_ENV
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
- name: Update manifest.json
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
echo "Updating manifest.json with:"
echo "Hash: ${{ env.ZIP_HASH }}"
echo "Time: ${{ env.BUILD_TIME }}"
echo "Url: $DOWNLOAD_URL"
jq --arg hash "${{ env.ZIP_HASH }}" \
--arg time "${{ env.BUILD_TIME }}" \
--arg url "$DOWNLOAD_URL" \
'.[0].versions[0].checksum = $hash | .[0].versions[0].timestamp = $time | .[0].versions[0].sourceUrl = $url' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ env.VERSION }}"
name: "v${{ env.VERSION }}"
# body: ${{ env.CHANGELOG }}
files: ${{ env.ZIP_PATH }}
draft: false
prerelease: false
generate_release_notes: true
# Update Message in Remote Repository
- name: Checkout Central Manifest Repo
uses: actions/checkout@v6
with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
path: central-manifest
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest
shell: bash
run: |
cd central-manifest
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
# 1. Get info from previous steps
VERSION="${{ env.VERSION }}"
HASH="${{ env.ZIP_HASH }}"
TIME="${{ env.BUILD_TIME }}"
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
# 2. Get info from env
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
CHANGELOG="${{ env.CHANGELOG }}"
TARGET_ABI="${{ env.TARGET_ABI }}"
echo "Updating Central Manifest for Plugin GUID: $PLUGIN_GUID"
# 3. Update/Prepend entry in central manifest.json
# Logic:
# - If array is empty or new version != old version: PREPEND new entry
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
jq --arg guid "$PLUGIN_GUID" \
--arg hash "$HASH" \
--arg time "$TIME" \
--arg url "$DOWNLOAD_URL" \
--arg ver "$VERSION" \
--arg changelog "$CHANGELOG" \
--arg abi "$TARGET_ABI" \
'map(if .guid == $guid then
if .versions[0].version != $ver then
# New Version -> Prepend
.versions = [{
"version": $ver,
"changelog": $changelog,
"targetAbi": $abi,
"sourceUrl": $url,
"checksum": $hash,
"timestamp": $time
}] + .versions
else
# Same Version -> Update existing (overwrite top)
.versions[0].changelog = $changelog |
.versions[0].targetAbi = $abi |
.versions[0].sourceUrl = $url |
.versions[0].checksum = $hash |
.versions[0].timestamp = $time
end
else . end)' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest
run: |
cd central-manifest
git config user.name "CodeDevMLH"
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
# Check if there are changes
if [[ -n $(git status -s) ]]; then
git add manifest.json
git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
git push
else
echo "No changes to central manifest."
fi

View File

@@ -1,20 +0,0 @@
name: '🔬 Run CodeQL'
on:
push:
branches: [ master ]
paths-ignore:
- '**/*.md'
pull_request:
branches: [ master ]
paths-ignore:
- '**/*.md'
schedule:
- cron: '24 2 * * 4'
workflow_dispatch:
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master
with:
repository-name: jellyfin/jellyfin-plugin-template

View File

@@ -1,18 +0,0 @@
name: '🧪 Test Plugin'
on:
push:
branches:
- master
paths-ignore:
- '**/*.md'
pull_request:
branches:
- master
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/test.yaml@master

1
.gitignore vendored
View File

@@ -4,4 +4,5 @@ obj/
.idea/ .idea/
artifacts artifacts
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

@@ -3,6 +3,8 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Jellyfin.Plugin.Seasonals;
using Jellyfin.Plugin.Seasonals.Configuration;
namespace Jellyfin.Plugin.Seasonals.Api; namespace Jellyfin.Plugin.Seasonals.Api;
@@ -19,9 +21,9 @@ public class SeasonalsController : ControllerBase
/// <returns>The configuration object.</returns> /// <returns>The configuration object.</returns>
[HttpGet("Config")] [HttpGet("Config")]
[Produces("application/json")] [Produces("application/json")]
public ActionResult<object> GetConfig() public ActionResult<PluginConfiguration> GetConfig()
{ {
return Plugin.Instance?.Configuration ?? new object(); return SeasonalsPlugin.Instance?.Configuration ?? new PluginConfiguration();
} }
/// <summary> /// <summary>
@@ -38,7 +40,7 @@ public class SeasonalsController : ControllerBase
return BadRequest(); return BadRequest();
} }
var assembly = Assembly.GetExecutingAssembly(); var assembly = typeof(SeasonalsPlugin).Assembly;
// Convert path to resource name // Convert path to resource name
var resourcePath = path.Replace('/', '.').Replace('\\', '.'); var resourcePath = path.Replace('/', '.').Replace('\\', '.');
var resourceName = $"Jellyfin.Plugin.Seasonals.Web.{resourcePath}"; var resourceName = $"Jellyfin.Plugin.Seasonals.Web.{resourcePath}";
@@ -60,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

@@ -12,8 +12,10 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public PluginConfiguration() public PluginConfiguration()
{ {
IsEnabled = true;
SelectedSeason = "none"; SelectedSeason = "none";
AutomateSeasonSelection = true; AutomateSeasonSelection = true;
EnableClientSideToggle = true;
Autumn = new AutumnOptions(); Autumn = new AutumnOptions();
Snowflakes = new SnowflakesOptions(); Snowflakes = new SnowflakesOptions();
@@ -25,8 +27,18 @@ 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();
} }
/// <summary>
/// Gets or sets a value indicating whether the plugin is enabled.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets the selected season. /// Gets or sets the selected season.
/// </summary> /// </summary>
@@ -37,6 +49,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\"}]";
/// <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; }
@@ -47,6 +72,11 @@ 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 class AutumnOptions public class AutumnOptions
@@ -151,3 +181,51 @@ 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; } = 15;
public int LadybugCount { get; set; } = 5;
public int SunbeamCount { get; set; } = 5;
public bool EnableSpring { get; set; } = true;
public bool EnableRandomSpring { get; set; } = true;
public bool EnableRandomSpringMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public 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 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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
using System;
using Jellyfin.Plugin.Seasonals.Model;
namespace Jellyfin.Plugin.Seasonals.Helpers
{
public static class TransformationPatches
{
public static string IndexHtml(PatchRequestPayload payload)
{
// Always return original content if something fails or is null
string? originalContents = payload?.Contents;
if (string.IsNullOrEmpty(originalContents))
{
return originalContents ?? string.Empty;
}
try
{
// Safety Check: If plugin is disabled, do nothing
if (SeasonalsPlugin.Instance?.Configuration.IsEnabled != true)
{
return originalContents;
}
// Use StringBuilder for efficient modification
var builder = new System.Text.StringBuilder(originalContents);
// Inject Script if missing
if (!originalContents.Contains("seasonals.js", StringComparison.Ordinal))
{
var scriptIndex = originalContents.LastIndexOf(ScriptInjector.Marker, StringComparison.OrdinalIgnoreCase);
if (scriptIndex != -1)
{
builder.Insert(scriptIndex, ScriptInjector.ScriptTag + Environment.NewLine);
}
}
return builder.ToString();
}
catch
{
// On error, return original content to avoid breaking the UI
return originalContents;
}
}
}
}

View File

@@ -12,8 +12,8 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title> <Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.2.0.0</Version> <Version>1.7.1.1</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -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

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Seasonals.Model
{
public class PatchRequestPayload
{
[JsonPropertyName("contents")]
public string? Contents { get; set; }
}
}

View File

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

View File

@@ -1,8 +1,13 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using Jellyfin.Plugin.Seasonals.Helpers;
using System.Collections.Generic;
using System.Linq;
namespace Jellyfin.Plugin.Seasonals; namespace Jellyfin.Plugin.Seasonals;
@@ -13,8 +18,8 @@ public class ScriptInjector
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; private readonly ILogger<ScriptInjector> _logger;
private const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\"></script>"; public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
private const string Marker = "</body>"; public const string Marker = "</body>";
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ScriptInjector"/> class. /// Initializes a new instance of the <see cref="ScriptInjector"/> class.
@@ -30,53 +35,63 @@ public class ScriptInjector
/// <summary> /// <summary>
/// Injects the script tag into index.html if it's not already present. /// Injects the script tag into index.html if it's not already present.
/// </summary> /// </summary>
/// <returns>True if injection was successful or already present, false otherwise.</returns> public void Inject()
public bool Inject()
{ {
try try
{ {
var webPath = GetWebPath(); var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath)) if (string.IsNullOrEmpty(webPath))
{ {
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped."); _logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
return false; RegisterFileTransformation();
return;
} }
var indexPath = Path.Combine(webPath, "index.html"); var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
_logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath); _logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
return false; RegisterFileTransformation();
return;
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (content.Contains(ScriptTag, StringComparison.Ordinal))
// 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("Seasonals script already injected."); _logger.LogInformation("Removed legacy tags from index.html.");
return true;
} }
// Insert before the closing body tag
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}", StringComparison.Ordinal);
if (string.Equals(newContent, content, StringComparison.Ordinal))
{
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
return false;
}
File.WriteAllText(indexPath, newContent); 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."); _logger.LogInformation("Successfully injected Seasonals script into index.html.");
return true; }
else
{
_logger.LogWarning("Script already present in index.html. Or could not be injected.");
}
}
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
_logger.LogWarning("Permission denied when attempting to inject script into index.html. Automatic injection failed. Please ensure the Jellyfin web directory is writable by the process, or manually add the script tag: {ScriptTag}", ScriptTag); _logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
return false; RegisterFileTransformation();
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error injecting Seasonals script."); _logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
return false; RegisterFileTransformation();
} }
} }
@@ -85,6 +100,8 @@ public class ScriptInjector
/// </summary> /// </summary>
public void Remove() public void Remove()
{ {
UnregisterFileTransformation();
try try
{ {
var webPath = GetWebPath(); var webPath = GetWebPath();
@@ -100,21 +117,29 @@ public class ScriptInjector
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (!content.Contains(ScriptTag, StringComparison.Ordinal)) if (content.Contains(ScriptTag))
{ {
return; 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.");
}
// 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.");
} }
// Try to remove with newline first, then just the tag to ensure clean removal
var newContent = content.Replace($"{ScriptTag}\n", "", StringComparison.Ordinal)
.Replace(ScriptTag, "", StringComparison.Ordinal);
File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
_logger.LogWarning("Permission denied when attempting to remove script from index.html."); _logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -132,4 +157,91 @@ public class ScriptInjector
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public); var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
return prop?.GetValue(_appPaths) as string; 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.");
}
}
// 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

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

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

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

View File

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

View File

@@ -0,0 +1,101 @@
.carnival-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
perspective: 600px;
}
.carnival-wrapper {
position: fixed;
z-index: 15;
top: -20px;
will-change: top;
animation-name: carnival-fall;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.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: #f0f;
will-change: transform;
animation-name: carnival-flutter;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-duration: 2s;
}
.carnival-confetti.circle {
width: 8px;
height: 8px;
border-radius: 50%;
}
.carnival-confetti.square {
width: 8px;
height: 8px;
}
.carnival-confetti.triangle {
width: 0;
height: 0;
background-color: transparent !important;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 10px solid;
width: 10px;
height: 10px;
background-color: inherit;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
@keyframes carnival-fall {
0% {
top: -10%;
}
100% {
top: 110%;
}
}
@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(1, 1, 1, 0deg);
}
25% {
transform: rotate3d(1, 0.5, 0, 90deg);
}
50% {
transform: rotate3d(0.5, 1, 0.5, 180deg);
}
75% {
transform: rotate3d(0, 0.5, 1, 270deg);
}
100% {
transform: rotate3d(1, 1, 1, 360deg);
}
}

View File

@@ -0,0 +1,174 @@
const config = window.SeasonalsPluginConfig?.Carnival || {};
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true;
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true;
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true;
const carnivalCount = config.ObjectCount || 80;
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);
// start observation
observer.observe(document.body, {
childList: true, // observe adding/removing of child elements
subtree: true, // observe all levels of the DOM tree
attributes: true // observe changes to attributes (e.g. class changes)
});
const confettiColors = [
'#fce18a', '#ff726d', '#b48def', '#f4306d',
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
];
function createConfettiPiece(container, isInitial = false) {
const wrapper = document.createElement('div');
wrapper.classList.add('carnival-wrapper');
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}%`;
// Random dimensions
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
const width = Math.random() * 5 + 4;
const height = Math.random() * 6 + 8;
confetti.style.width = `${width}px`;
confetti.style.height = `${height}px`;
}
// Animation settings
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`;
// Random sway amplitude (using CSS variable for dynamic keyframe)
// Sway between 30px and 100px
const swayAmount = Math.random() * 70 + 30;
const direction = Math.random() > 0.5 ? 1 : -1;
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
}
// Flutter speed variation
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
if (enableSway) {
swayWrapper.appendChild(confetti);
wrapper.appendChild(swayWrapper);
} else {
wrapper.appendChild(confetti);
}
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 < 30; i++) {
createConfettiPiece(container, true);
}
}
// initialize carnival
function initializeCarnival() {
if (!carnival) return; // exit if carnival is disabled
initCarnivalObjects();
toggleCarnival();
const screenWidth = window.innerWidth;
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
addRandomCarnivalObjects(carnivalCount);
}
}
initializeCarnival();

View File

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

View File

@@ -1,10 +1,10 @@
const config = window.SeasonalsPluginConfig?.christmas || {}; const config = window.SeasonalsPluginConfig?.Christmas || {};
const christmas = config.enableChristmas !== undefined ? config.enableChristmas : true; // enable/disable christmas const christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
const randomChristmas = config.enableRandomChristmas !== undefined ? config.enableRandomChristmas : true; // enable random Christmas const randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random Christmas
const randomChristmasMobile = config.enableRandomChristmasMobile !== undefined ? config.enableRandomChristmasMobile : false; // enable random Christmas on mobile devices (Warning: High values may affect performance) const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ? config.EnableRandomChristmasMobile : false; // enable random Christmas on mobile devices (Warning: High values may affect performance)
const enableDiffrentDuration = config.enableDifferentDuration !== undefined ? config.enableDifferentDuration : true; // enable different duration for the random Christmas symbols const 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
const config = window.SeasonalsPluginConfig?.fireworks || {}; const config = window.SeasonalsPluginConfig?.Fireworks || {};
const fireworks = config.enableFireworks !== undefined ? config.enableFireworks : true; // enable/disable fireworks const fireworks = config.EnableFireworks !== undefined ? config.EnableFireworks : true; // enable/disable fireworks
const scrollFireworks = config.scrollFireworks !== undefined ? config.scrollFireworks : true; // enable fireworks to scroll with page content const scrollFireworks = config.ScrollFireworks !== undefined ? config.ScrollFireworks : true; // enable fireworks to scroll with page content
const particlesPerFirework = config.particleCount || 50; // count of particles per firework (Warning: High values may affect performance) const particlesPerFirework = config.ParticleCount || 50; // count of particles per firework (Warning: High values may affect performance)
const minFireworks = config.minFireworks || 3; // minimum number of simultaneous fireworks const minFireworks = config.MinFireworks || 3; // minimum number of simultaneous fireworks
const maxFireworks = config.maxFireworks || 6; // maximum number of simultaneous fireworks const maxFireworks = config.MaxFireworks || 6; // maximum number of simultaneous fireworks
const intervalOfFireworks = config.launchInterval || 3200; // interval for the fireworks in milliseconds const intervalOfFireworks = config.LaunchInterval || 3200; // interval for the fireworks in milliseconds
// array of color palettes for the fireworks // array of color palettes for the fireworks
const colorPalettes = [ const colorPalettes = [
@@ -42,6 +42,11 @@ function toggleFirework() {
} }
} else { } else {
fireworksContainer.style.display = 'block'; // show fireworks fireworksContainer.style.display = 'block'; // show fireworks
if (scrollFireworks) {
fireworksContainer.style.height = `${document.documentElement.scrollHeight}px`;
}
if (msgPrinted) { if (msgPrinted) {
console.log('Fireworks visible'); console.log('Fireworks visible');
startFireworks(); startFireworks();
@@ -117,8 +122,8 @@ function launchFirework() {
let startY, endY; let startY, endY;
if (scrollFireworks) { if (scrollFireworks) {
// Y-position considers scrolling // Y-position considers scrolling
startY = window.scrollY + window.innerHeight; // Bottom edge of the window plus the scroll offset startY = window.scrollY + window.innerHeight;
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2 + window.scrollY; // Area around the middle, but also with scrolling endY = window.scrollY + (Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2);
} else { } else {
startY = window.innerHeight; // Bottom edge of the window startY = window.innerHeight; // Bottom edge of the window
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2; // Area around the middle endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2; // Area around the middle
@@ -135,14 +140,27 @@ function launchFirework() {
// Start the firework routine // Start the firework routine
function startFireworks() { function startFireworks() {
const fireworkContainer = document.querySelector('.fireworks') || document.createElement("div"); let fireworkContainer = document.querySelector('.fireworks');
if (!document.querySelector('.fireworks')) { if (!fireworkContainer) {
fireworkContainer = document.createElement("div");
fireworkContainer.className = "fireworks"; fireworkContainer.className = "fireworks";
fireworkContainer.setAttribute("aria-hidden", "true"); fireworkContainer.setAttribute("aria-hidden", "true");
document.body.appendChild(fireworkContainer); document.body.appendChild(fireworkContainer);
} }
fireworkContainer.style.position = scrollFireworks ? 'absolute' : 'fixed';
if (scrollFireworks) {
fireworkContainer.style.height = `${document.documentElement.scrollHeight}px`;
fireworkContainer.style.width = '100%';
fireworkContainer.style.top = '0';
fireworkContainer.style.left = '0';
} else {
fireworkContainer.style.height = '100%';
fireworkContainer.style.width = '100%';
}
fireworksInterval = setInterval(() => { fireworksInterval = setInterval(() => {
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks; const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
for (let i = 0; i < randomCount; i++) { for (let i = 0; i < randomCount; i++) {

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
const config = window.SeasonalsPluginConfig?.hearts || {}; const config = window.SeasonalsPluginConfig?.Hearts || {};
const hearts = config.enableHearts !== undefined ? config.enableHearts : true; // enable/disable hearts const hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
const randomSymbols = config.enableRandomSymbols !== undefined ? config.enableRandomSymbols : true; // enable more random symbols const 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 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
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

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

View File

@@ -0,0 +1,113 @@
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 img = document.createElement('img');
img.src = imageSrc;
img.alt = '';
symbol.style.left = `${leftPercent}%`;
symbol.style.animationDelay = `${delaySeconds}s, ${Math.random() * 3}s`;
if (enableDifferentDuration) {
const fallDuration = Math.random() * 7 + 7;
const swayDuration = Math.random() * 4 + 2;
symbol.style.animationDuration = `${fallDuration}s, ${swayDuration}s`;
}
symbol.appendChild(img);
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

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

View File

@@ -1,15 +1,15 @@
const config = window.SeasonalsPluginConfig?.santa || {}; const config = window.SeasonalsPluginConfig?.Santa || {};
const santaIsFlying = config.enableSanta !== undefined ? config.enableSanta : true; // enable/disable santa const santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
let snowflakesCount = config.snowflakesCount || 500; // count of snowflakes (recommended values: 300-600) let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.snowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance) const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.snowFallSpeed || 3; // speed of snowfall (recommended values: 0-5) const snowFallSpeed = config.SnowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
const santaSpeed = config.santaSpeed || 10; // speed of santa in seconds (recommended values: 5000-15000) const santaSpeed = config.SantaSpeed || 10; // speed of santa in seconds (recommended values: 5-15)
const santaSpeedMobile = config.santaSpeedMobile || 8; // speed of santa on mobile devices in seconds const santaSpeedMobile = config.SantaSpeedMobile || 8; // speed of santa on mobile devices in seconds
const maxSantaRestTime = config.maxSantaRestTime || 8; // maximum time santa rests in seconds const maxSantaRestTime = config.MaxSantaRestTime || 8; // maximum time santa rests in seconds
const minSantaRestTime = config.minSantaRestTime || 3; // minimum time santa rests in seconds const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa rests in seconds
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
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
@@ -60,6 +60,7 @@ observer.observe(document.body, {
attributes: true // observe changes to attributes (e.g. class changes) attributes: true // observe changes to attributes (e.g. class changes)
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -78,8 +79,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 +101,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) {
@@ -154,18 +181,18 @@ function updateSnowflakes() {
// credits: flaticon.com // credits: flaticon.com
const presentImages = [ const presentImages = [
'Seasonals/Resources/santa_images/gift1.png', '../Seasonals/Resources/santa_images/gift1.png',
'Seasonals/Resources/santa_images/gift2.png', '../Seasonals/Resources/santa_images/gift2.png',
'Seasonals/Resources/santa_images/gift3.png', '../Seasonals/Resources/santa_images/gift3.png',
'Seasonals/Resources/santa_images/gift4.png', '../Seasonals/Resources/santa_images/gift4.png',
'Seasonals/Resources/santa_images/gift5.png', '../Seasonals/Resources/santa_images/gift5.png',
'Seasonals/Resources/santa_images/gift6.png', '../Seasonals/Resources/santa_images/gift6.png',
'Seasonals/Resources/santa_images/gift7.png', '../Seasonals/Resources/santa_images/gift7.png',
'Seasonals/Resources/santa_images/gift8.png', '../Seasonals/Resources/santa_images/gift8.png',
]; ];
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm // credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
const santaImage = 'Seasonals/Resources/santa_images/santa.gif'; const santaImage = '../Seasonals/Resources/santa_images/santa.gif';
function createSantaElement() { function createSantaElement() {

View File

@@ -1,209 +1,402 @@
// 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'
},
none: { none: {
containerClass: 'none' containerClass: 'none'
}, },
}; };
// determine current theme based on the current month const SeasonalSettingsManager = {
function determineCurrentTheme() { initialized: false,
config: null,
init(config) {
if (this.initialized) return;
this.config = config;
// Only inject settings if enabled on server by admin
if (this.config && this.config.EnableClientSideToggle !== false) {
this.injectSettingsIcon();
this.initialized = true;
console.log("Seasonals: Client-Side Settings Manager initialized.");
}
},
getSetting(key, defaultValue) {
const value = localStorage.getItem(`seasonals-${key}`);
return value !== null ? value : defaultValue;
},
setSetting(key, value) {
localStorage.setItem(`seasonals-${key}`, value);
},
createIcon() {
const button = document.createElement('button');
button.type = 'button';
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
button.title = 'Seasonal Settings';
// button.innerHTML = '<span class="material-icons">ac_unit</span>';
button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
button.style.verticalAlign = 'middle';
button.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleSettingsPopup(button);
});
return button;
},
injectSettingsIcon() {
const observer = new MutationObserver((mutations, obs) => {
const headerRight = document.querySelector('.headerRight');
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
const icon = this.createIcon();
headerRight.prepend(icon);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
},
createPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) existing.remove();
const popup = document.createElement('div');
popup.className = 'seasonal-settings-popup dialog';
Object.assign(popup.style, {
position: 'fixed',
zIndex: '10000',
backgroundColor: '#202020',
padding: '1em',
borderRadius: '0.3em',
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
minWidth: '200px',
color: '#fff',
maxWidth: '250px'
});
const rect = anchorElement.getBoundingClientRect();
// Positioning logic
let rightPos = window.innerWidth - rect.right;
if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
popup.style.right = '1rem';
popup.style.left = 'auto';
} else {
popup.style.right = `${rightPos}px`;
popup.style.left = 'auto';
}
popup.style.top = `${rect.bottom + 10}px`;
// Popup HTML
let html = `
<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
<label class="emby-checkbox-label">
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
<span class="checkboxLabel">Enable Seasonals</span>
</label>
</div>
<div class="selectContainer" 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);
}
}
};
const SeasonalsManager = {
config: null,
async init() {
// Fetch Config
try {
const response = await fetch('../Seasonals/Config');
if (response.ok) {
this.config = await response.json();
window.SeasonalsPluginConfig = this.config;
console.log('Seasonals: Seasonals Config loaded:', this.config);
}
} catch (error) {
console.error('Seasonals: Error fetching Seasonals config:', error);
}
// 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 date = new Date();
const month = date.getMonth(); // 0-11 const month = date.getMonth() + 1; // 1-12
const day = date.getDate(); // 1-31 const day = date.getDate(); // 1-31
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5 for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18 if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27 return rule.Theme;
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27 }
if (month === 11) return 'snowflakes'; // snowflakes december
if (month === 0 || month === 1) return 'snowfall'; // snow january, february
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25
//NOT IMPLEMENTED YET
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
//NOT IMPLEMENTED YET
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
return 'none'; // Fallback (nothing)
} }
// load theme csss return 'none'; // No rule matched
function loadThemeCSS(cssPath) { },
if (!cssPath) return;
const link = document.createElement('link'); isDateInRange: function(day, month, startDay, startMonth, endDay, endMonth) {
link.rel = 'stylesheet'; if (startMonth > endMonth) {
link.href = cssPath; // 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);
}
},
link.onerror = () => { isDateAfterOrEqual: function(day, month, targetDay, targetMonth) {
console.error(`Failed to load CSS: ${cssPath}`); if (month > targetMonth) return true;
}; if (month === targetMonth && day >= targetDay) return true;
return false;
},
document.body.appendChild(link); isDateBeforeOrEqual: function(day, month, targetDay, targetMonth) {
console.log(`CSS file "${cssPath}" loaded.`); 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;
} }
// load theme js this.updateThemeContainer(theme.containerClass);
function loadThemeJS(jsPath) {
if (!jsPath) return;
const script = document.createElement('script'); if (theme.css) this.loadResource('css', theme.css);
script.src = jsPath; if (theme.js) this.loadResource('js', theme.js);
script.onerror = () => { console.log(`Seasonals: Theme "${themeName}" applied.`);
console.error(`Failed to load JS: ${jsPath}`); },
};
document.body.appendChild(script); updateThemeContainer(containerClass) {
console.log(`JS file "${jsPath}" loaded.`);
}
// update theme container class name
function updateThemeContainer(containerClass) {
// Create container if it doesn't exist
let container = document.querySelector('.seasonals-container'); let container = document.querySelector('.seasonals-container');
if (!container) { if (!container) {
container = document.createElement('div'); container = document.createElement('div');
container.className = 'seasonals-container'; container.className = 'seasonals-container';
document.body.appendChild(container); document.body.appendChild(container);
} }
container.className = `seasonals-container ${containerClass}`; container.className = `seasonals-container ${containerClass}`;
console.log(`Seasonals-Container class updated to "${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;
},
function removeSelf() { loadResource(type, path) {
const script = document.currentScript; if (!path) return;
if (script) script.parentNode.removeChild(script);
console.log('External script removed:', script); 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);
} }
// initialize theme
async function initializeTheme() {
let automateThemeSelection = true;
let defaultTheme = 'none';
try {
const response = await fetch('/Seasonals/Config');
if (response.ok) {
const config = await response.json();
automateThemeSelection = config.automateSeasonSelection;
defaultTheme = config.selectedSeason;
window.SeasonalsPluginConfig = config;
console.log('Seasonals Config loaded:', config);
} else {
console.error('Failed to fetch Seasonals config');
}
} catch (error) {
console.error('Error fetching Seasonals config:', error);
} }
};
let currentTheme; SeasonalsManager.init();
if (automateThemeSelection === false) {
currentTheme = defaultTheme;
} else {
currentTheme = determineCurrentTheme();
}
console.log(`Selected theme: ${currentTheme}`);
if (!currentTheme || currentTheme === 'none') {
console.log('No theme selected.');
removeSelf();
return;
}
const theme = themeConfigs[currentTheme];
if (!theme) {
console.error(`Theme "${currentTheme}" not found.`);
return;
}
updateThemeContainer(theme.containerClass);
if (theme.css) loadThemeCSS(theme.css);
if (theme.js) loadThemeJS(theme.js);
console.log(`Theme "${currentTheme}" loaded.`);
removeSelf();
}
// Ensure DOM is ready before initializing
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeTheme);
} else {
initializeTheme();
}

View File

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

View File

@@ -1,9 +1,9 @@
const config = window.SeasonalsPluginConfig?.snowfall || {}; const config = window.SeasonalsPluginConfig?.Snowfall || {};
const snowfall = config.enableSnowfall !== undefined ? config.enableSnowfall : true; // enable/disable snowfall const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
let snowflakesCount = config.snowflakesCount || 500; // count of snowflakes (recommended values: 300-600) let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.snowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance) const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.speed || 3; // speed of snowfall (recommended values: 0-5) const snowFallSpeed = config.Speed || 3; // speed of snowfall (recommended values: 0-5)
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -55,6 +55,7 @@ observer.observe(document.body, {
attributes: true // observe changes to attributes (e.g. class changes) attributes: true // observe changes to attributes (e.g. class changes)
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -73,8 +74,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 +91,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

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

View File

@@ -1,11 +1,11 @@
const config = window.SeasonalsPluginConfig?.snowflakes || {}; const config = window.SeasonalsPluginConfig?.Snowflakes || {};
const snowflakes = config.enableSnowflakes !== undefined ? config.enableSnowflakes : true; // enable/disable snowflakes const snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
const randomSnowflakes = config.enableRandomSnowflakes !== undefined ? config.enableRandomSnowflakes : true; // enable random Snowflakes const randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random Snowflakes
const randomSnowflakesMobile = config.enableRandomSnowflakesMobile !== undefined ? config.enableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices (Warning: High values may affect performance) const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices
const enableColoredSnowflakes = config.enableColoredSnowflakes !== undefined ? config.enableColoredSnowflakes : true; // enable colored snowflakes on mobile devices const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable colored snowflakes
const enableDiffrentDuration = config.enableDifferentDuration !== undefined ? config.enableDifferentDuration : true; // enable different animation duration for random symbols const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
const snowflakeCount = config.snowflakeCount || 25; // count of random extra snowflakes const snowflakeCount = config.SnowflakeCount || 25; // count of random extra snowflakes
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages

View File

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

View File

@@ -1,11 +1,11 @@
const config = window.SeasonalsPluginConfig?.snowstorm || {}; const config = window.SeasonalsPluginConfig?.Snowstorm || {};
const snowstorm = config.enableSnowstorm !== undefined ? config.enableSnowstorm : true; // enable/disable snowstorm const snowstorm = config.enableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
let snowflakesCount = config.snowflakesCount || 500; // count of snowflakes (recommended values: 300-600) let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.snowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance) const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.speed || 6; // speed of snowfall (recommended values: 4-8) const snowFallSpeed = config.Speed || 6; // speed of snowfall (recommended values: 4-8)
const horizontalWind = config.horizontalWind || 4; // horizontal wind speed (recommended value: 4) const horizontalWind = config.HorizontalWind || 4; // horizontal wind speed (recommended value: 4)
const verticalVariation = config.verticalVariation || 2; // vertical variation (recommended value: 2) const verticalVariation = config.VerticalVariation || 2; // vertical variation (recommended value: 2)
let msgPrinted = false; // flag to prevent multiple console messages let msgPrinted = false; // flag to prevent multiple console messages
@@ -57,6 +57,7 @@ observer.observe(document.body, {
attributes: true // observe changes to attributes (e.g. class changes) attributes: true // observe changes to attributes (e.g. class changes)
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -66,7 +67,7 @@ function initializeCanvas() {
const container = document.querySelector('.snowstorm-container'); const container = document.querySelector('.snowstorm-container');
if (!container) { if (!container) {
console.error('Error: No element with class "snowfall-container" found.'); console.error('Error: No element with class "snowstorm-container" found.');
return; return;
} }
@@ -75,8 +76,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() {
@@ -88,15 +93,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

@@ -0,0 +1,179 @@
.spring-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
/* Petals */
.spring-petal {
position: fixed;
top: -20px;
z-index: 1005;
width: 15px;
height: 10px;
background-color: #ffc0cb;
border-radius: 15px 0px 15px 0px;
will-change: transform, top;
animation-name: spring-fall, spring-sway;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-duration: 10s, 3s;
}
.spring-petal.lighter {
background-color: #ffd1dc;
opacity: 0.8;
}
.spring-petal.darker {
background-color: #ffb7c5;
opacity: 0.9;
}
.spring-petal.type2 {
width: 12px;
height: 12px;
border-radius: 10px 0px 10px 5px;
}
/* Pollen */
.spring-pollen {
position: fixed;
z-index: 14;
background-color: #fffacd;
border-radius: 50%;
opacity: 0.6;
box-shadow: 0 0 4px rgba(255, 250, 205, 0.4);
will-change: transform;
animation-name: spring-float;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
/* Sunbeams */
.spring-sunbeam {
position: fixed;
top: -50%;
height: 200%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 200, 0.08) 50%, rgba(255, 255, 255, 0));
z-index: 5;
transform-origin: top center;
pointer-events: none;
animation-name: spring-beam-pulse;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
/* Grass */
.spring-grass-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 80px;
z-index: 1002;
overflow: hidden;
pointer-events: none;
}
.spring-grass {
position: absolute;
bottom: 0;
width: 3px;
border-radius: 100% 0 0 0;
transform-origin: bottom center;
background-color: #4caf50;
will-change: transform;
animation-name: spring-grass-sway;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
/* Ladybugs */
.spring-ladybug {
position: absolute;
width: 6px;
height: 4px;
background-color: #e74c3c; /* Red */
border-radius: 3px 3px 0 0;
z-index: 1003;
will-change: left, transform;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.spring-ladybug.right {
animation-name: spring-bug-crawl-right;
transform: scaleX(1);
}
.spring-ladybug.left {
animation-name: spring-bug-crawl-left;
transform: scaleX(-1);
}
.spring-ladybug::before {
content: '';
position: absolute;
right: -2px;
top: 1px;
width: 2px;
height: 2px;
background-color: #000;
border-radius: 50%;
}
@keyframes spring-fall {
0% { top: -10%; }
100% { top: 100%; }
}
@keyframes spring-sway {
0%, 100% {
transform: translateX(0) rotate(0deg);
}
50% {
transform: translateX(30px) rotate(45deg);
}
}
@keyframes spring-float {
0% { transform: translateX(0) translateY(0); }
25% { transform: translateX(20px) translateY(-10px); }
50% { transform: translateX(40px) translateY(0); }
75% { transform: translateX(20px) translateY(10px); }
100% { transform: translateX(0) translateY(0); }
}
@keyframes spring-beam-pulse {
0% { opacity: 0.3; transform: rotate(45deg) scaleX(1); }
50% { opacity: 0.6; transform: rotate(45deg) scaleX(1.1); }
100% { opacity: 0.3; transform: rotate(45deg) scaleX(1); }
}
@keyframes spring-grass-sway {
0% { transform: rotate(0deg); }
50% { transform: rotate(8deg); }
100% { transform: rotate(0deg); }
}
@keyframes spring-bug-crawl-right {
0% { left: -5%; }
100% { left: 105%; }
}
@keyframes spring-bug-crawl-left {
0% { left: 105%; }
100% { left: -5%; }
}

View File

@@ -0,0 +1,245 @@
const config = window.SeasonalsPluginConfig?.Spring || {};
const spring = config.EnableSpring !== undefined ? config.EnableSpring : true;
const petalCount = config.PetalCount || 25;
const pollenCount = config.PollenCount || 15;
const ladybugCountConfig = config.LadybugCount || 5;
const sunbeamCount = config.SunbeamCount || 5;
const randomSpring = config.EnableRandomSpring !== undefined ? config.EnableRandomSpring : true;
const randomSpringMobile = config.EnableRandomSpringMobile !== undefined ? config.EnableRandomSpringMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const enablePetals = config.EnableSpringPetals !== undefined ? config.EnableSpringPetals : true;
const enableSunbeams = config.EnableSpringSunbeams !== undefined ? config.EnableSpringSunbeams : true;
let msgPrinted = false;
function toggleSpring() {
const springContainer = document.querySelector('.spring-container');
if (!springContainer) 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) {
springContainer.style.display = 'none';
if (!msgPrinted) {
console.log('Spring hidden');
msgPrinted = true;
}
} else {
springContainer.style.display = 'block';
if (msgPrinted) {
console.log('Spring visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleSpring);
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
function createPetal(container) {
if (!enablePetals) return;
const petal = document.createElement('div');
petal.classList.add('spring-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 createPollen(container) {
const pollen = document.createElement('div');
pollen.classList.add('spring-pollen');
const startY = Math.random() * 60 + 20;
pollen.style.top = `${startY}%`;
pollen.style.left = `${Math.random() * 100}%`;
const size = Math.random() * 3 + 1;
pollen.style.width = `${size}px`;
pollen.style.height = `${size}px`;
const duration = Math.random() * 20 + 20;
pollen.style.animationDuration = `${duration}s`;
pollen.style.animationDelay = `-${Math.random() * 20}s`;
container.appendChild(pollen);
}
function createSunbeam(container) {
if (!enableSunbeams) return;
const beam = document.createElement('div');
beam.classList.add('spring-sunbeam');
const left = Math.random() * 100; // Spread across full width
beam.style.left = `${left}%`;
// Thinner beams as requested
const width = Math.random() * 20 + 10; // 10-30px wide
beam.style.width = `${width}px`;
const rotate = Math.random() * 20 - 10 + 45;
beam.style.transform = `rotate(${rotate}deg)`;
const duration = Math.random() * 10 + 10;
beam.style.animationDuration = `${duration}s`;
beam.style.animationDelay = `-${Math.random() * 10}s`;
container.appendChild(beam);
}
function createGrass(container) {
let grassContainer = container.querySelector('.spring-grass-container');
if (!grassContainer) {
grassContainer = document.createElement('div');
grassContainer.className = 'spring-grass-container';
container.appendChild(grassContainer);
}
// More grass: 1 blade every 3px (was 15px)
const bladeCount = window.innerWidth / 3;
for (let i = 0; i < bladeCount; i++) {
const blade = document.createElement('div');
blade.classList.add('spring-grass');
const height = Math.random() * 40 + 20; // 20-60px height
blade.style.height = `${height}px`;
blade.style.left = `${i * 3 + Math.random() * 2}px`;
const duration = Math.random() * 2 + 3;
blade.style.animationDuration = `${duration}s`;
blade.style.animationDelay = `-${Math.random() * 5}s`;
const hue = 100 + Math.random() * 40;
blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`;
grassContainer.appendChild(blade);
}
// Add Ladybugs
const bugs = ladybugCountConfig;
for (let i = 0; i < bugs; i++) {
createLadybug(grassContainer);
}
}
function createLadybug(container) {
const bug = document.createElement('div');
bug.classList.add('spring-ladybug');
const direction = Math.random() > 0.5 ? 'right' : 'left';
bug.classList.add(direction);
// Position lower (bottom of grass), but ensure visibility
const bottomOffset = direction === 'right' ? Math.random() * 5 + 6 : Math.random() * 5 + 2;
bug.style.bottom = `${bottomOffset}px`;
// Start position depends on direction
if (direction === 'right') {
bug.style.left = '-5%'; // Start off-screen left
} else {
bug.style.left = '105%'; // Start off-screen right
}
const duration = Math.random() * 20 + 20; // Slow crawl
bug.style.animationDuration = `${duration}s`;
bug.style.animationDelay = `-${Math.random() * 20}s`;
container.appendChild(bug);
}
function addRandomSpringObjects() {
const container = document.querySelector('.spring-container');
if (!container) return;
if (enablePetals) {
for (let i = 0; i < petalCount; i++) {
createPetal(container);
}
}
for (let i = 0; i < pollenCount; i++) {
createPollen(container);
}
}
function initSpringObjects() {
let container = document.querySelector('.spring-container');
if (!container) {
container = document.createElement("div");
container.className = "spring-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
createGrass(container);
if (enablePetals) {
for (let i = 0; i < 15; i++) {
const petal = document.createElement('div');
petal.classList.add('spring-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 swayDuration = Math.random() * 2 + 2;
if (enableDifferentDuration) {
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
}
petal.style.animationDelay = `-${Math.random() * 10}s, -${Math.random() * 3}s`;
container.appendChild(petal);
}
}
if (enableSunbeams) {
// Initial sunbeams
for (let i = 0; i < sunbeamCount; i++) {
createSunbeam(container);
}
}
}
function initializeSpring() {
if (!spring) return;
initSpringObjects();
toggleSpring();
const screenWidth = window.innerWidth;
if (randomSpring && (screenWidth > 768 || randomSpringMobile)) {
addRandomSpringObjects();
}
}
initializeSpring();

View File

@@ -0,0 +1,80 @@
.summer-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.summer-bubble {
position: fixed;
bottom: -50px;
z-index: 15;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1) 60%, rgba(255, 255, 255, 0.05));
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
will-change: transform, bottom;
animation-name: summer-rise, summer-wobble;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-duration: 10s, 3s;
}
.summer-dust {
position: fixed;
bottom: -10px;
z-index: 12;
background-color: rgba(255, 223, 186, 0.6);
border-radius: 50%;
box-shadow: 0 0 5px rgba(255, 223, 186, 0.4);
will-change: transform, bottom;
animation-name: summer-rise, summer-drift;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-duration: 20s, 5s;
}
@keyframes summer-rise {
0% {
bottom: -10%;
}
100% {
bottom: 110%;
}
}
@keyframes summer-wobble {
0% {
transform: translateX(0);
}
33% {
transform: translateX(15px);
}
66% {
transform: translateX(-15px);
}
100% {
transform: translateX(0);
}
}
@keyframes summer-drift {
0% {
transform: translateX(0) translateY(0);
}
50% {
transform: translateX(30px) translateY(-20px);
}
100% {
transform: translateX(0) translateY(0);
}
}

View File

@@ -0,0 +1,143 @@
const config = window.SeasonalsPluginConfig?.Summer || {};
const summer = config.EnableSummer !== undefined ? config.EnableSummer : true; // enable/disable summer
const bubbleCount = config.BubbleCount || 20;
const dustCount = config.DustCount || 50;
const randomSummer = config.EnableRandomSummer !== undefined ? config.EnableRandomSummer : true; // enable random objects
const randomSummerMobile = config.EnableRandomSummerMobile !== undefined ? config.EnableRandomSummerMobile : false; // enable random objects on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
let msgPrinted = false;
function toggleSummer() {
const summerContainer = document.querySelector('.summer-container');
if (!summerContainer) 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) {
summerContainer.style.display = 'none';
if (!msgPrinted) {
console.log('Summer hidden');
msgPrinted = true;
}
} else {
summerContainer.style.display = 'block';
if (msgPrinted) {
console.log('Summer visible');
msgPrinted = false;
}
}
}
const observer = new MutationObserver(toggleSummer);
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
function createBubble(container, isDust = false) {
const bubble = document.createElement('div');
if (isDust) {
bubble.classList.add('summer-dust');
} else {
bubble.classList.add('summer-bubble');
}
// Random horizontal position
const randomLeft = Math.random() * 100;
bubble.style.left = `${randomLeft}%`;
// Random size
if (!isDust) {
const size = Math.random() * 20 + 10; // 10-30px bubbles
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
} else {
const size = Math.random() * 3 + 1; // 1-4px dust
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
}
// Animation properties
const duration = isDust ? (Math.random() * 20 + 10) : (Math.random() * 10 + 5); // Dust is slower
const delay = Math.random() * 10;
if (enableDifferentDuration) {
bubble.style.animationDuration = `${duration}s`;
}
bubble.style.animationDelay = `${delay}s`;
container.appendChild(bubble);
}
function addRandomSummerObjects() {
const container = document.querySelector('.summer-container');
if (!container) return;
// Add bubbles
for (let i = 0; i < bubbleCount; i++) {
createBubble(container, false);
}
// Add some dust particles (more of them, they are subtle)
for (let i = 0; i < dustCount; i++) {
createBubble(container, true);
}
}
function initSummerObjects() {
let container = document.querySelector('.summer-container');
if (!container) {
container = document.createElement("div");
container.className = "summer-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// Initial bubbles/dust
for (let i = 0; i < 10; i++) {
const bubble = document.createElement('div');
const isDust = Math.random() > 0.5;
if (isDust) {
bubble.classList.add('summer-dust');
} else {
bubble.classList.add('summer-bubble');
}
const randomLeft = Math.random() * 100;
bubble.style.left = `${randomLeft}%`;
if (!isDust) {
const size = Math.random() * 20 + 10;
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
} else {
const size = Math.random() * 3 + 1;
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
}
const duration = isDust ? (Math.random() * 20 + 10) : (Math.random() * 10 + 5);
if (enableDifferentDuration) {
bubble.style.animationDuration = `${duration}s`;
}
bubble.style.animationDelay = `-${Math.random() * 10}s`;
container.appendChild(bubble);
}
}
function initializeSummer() {
if (!summer) return;
initSummerObjects();
toggleSummer();
const screenWidth = window.innerWidth;
if (randomSummer && (screenWidth > 768 || randomSummerMobile)) {
addRandomSummerObjects();
}
}
initializeSummer();

View File

@@ -0,0 +1,506 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Seasonals Theme Tester</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: #101010;
color: #e0e0e0;
font-family: 'Inter', sans-serif;
}
/* ── Mock Jellyfin Header ── */
.skinHeader {
background-color: #181818;
height: 60px;
display: flex;
align-items: center;
padding: 0 1.5em;
border-bottom: 1px solid #282828;
position: relative;
z-index: 100;
}
.skinHeader-content { font-size: 1.1em; font-weight: 500; }
.headerRight {
margin-left: auto;
display: flex;
align-items: center;
}
.paper-icon-button-light {
background: none;
border: none;
color: #ccc;
cursor: pointer;
padding: 10px;
border-radius: 50%;
transition: background 0.2s;
}
.paper-icon-button-light:hover { background: rgba(255,255,255,0.1); }
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
}
/* ── Control Panel ── */
.control-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(20, 20, 20, 0.95);
backdrop-filter: blur(12px);
border-top: 1px solid #333;
padding: 1em 1.5em;
z-index: 200;
display: flex;
align-items: center;
gap: 1em;
flex-wrap: wrap;
}
.control-panel label {
font-size: 0.85em;
color: #aaa;
font-weight: 500;
white-space: nowrap;
}
.control-panel select,
.control-panel input[type="text"] {
background: #2a2a2a;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 6px;
padding: 0.5em 0.75em;
font-size: 0.9em;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.control-panel select:focus,
.control-panel input[type="text"]:focus {
border-color: #00a4dc;
}
.control-panel select { min-width: 160px; }
.control-panel input[type="text"] { width: 160px; }
.control-panel button {
background: #00a4dc;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5em 1.2em;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
font-family: inherit;
}
.control-panel button:hover { background: #008bbd; }
.control-panel button.btn-secondary {
background: #333;
color: #ccc;
}
.control-panel button.btn-secondary:hover { background: #444; }
.custom-fields {
display: none;
align-items: center;
gap: 0.75em;
}
.custom-fields.visible { display: flex; }
/* ── Mock Content ── */
.mock-content {
padding: 2em 1.5em 6em;
}
.mock-content h2 {
font-size: 1.4em;
margin-bottom: 1em;
color: #fff;
}
.mock-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1em;
}
.mock-card {
background: #1a1a1a;
border-radius: 8px;
aspect-ratio: 2/3;
position: relative;
overflow: hidden;
}
.mock-card::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40%;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
}
.info-bar {
position: fixed;
bottom: 65px;
left: 0;
right: 0;
background: rgba(0, 164, 220, 0.15);
border-top: 1px solid rgba(0, 164, 220, 0.3);
padding: 0.5em 1.5em;
z-index: 199;
font-size: 0.8em;
color: #aaa;
text-align: center;
}
.info-bar a {
color: #00a4dc;
text-decoration: none;
}
</style>
</head>
<body>
<!-- Mock Header -->
<div class="skinHeader">
<div class="skinHeader-content">
<span>Jellyfin</span>
</div>
<div class="headerRight">
<button class="paper-icon-button-light" title="Search">
<span class="material-icons">search</span>
</button>
<button class="paper-icon-button-light" title="User">
<span class="material-icons">person</span>
</button>
</div>
</div>
<!-- Seasonals Container (themes inject here) -->
<div class="seasonals-container"></div>
<!-- Mock Library Content -->
<div class="mock-content">
<h2>My Media</h2>
<div class="mock-grid">
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
</div>
</div>
<!-- Info Bar -->
<div class="info-bar">
📖 See <a href="../../CONTRIBUTING.md">CONTRIBUTING.md</a> for how to create your own seasonal theme
</div>
<!-- Control Panel -->
<div class="control-panel">
<label for="theme-select">Theme:</label>
<select id="theme-select">
<option value="" selected disabled>— Select a theme —</option>
<option value="snowfall">Snowfall</option>
<option value="snowflakes">Snowflakes</option>
<option value="snowstorm">Snowstorm</option>
<option value="fireworks">Fireworks</option>
<option value="halloween">Halloween</option>
<option value="hearts">Hearts</option>
<option value="christmas">Christmas</option>
<option value="santa">Santa</option>
<option value="autumn">Autumn</option>
<option value="easter">Easter</option>
<option value="resurrection">Resurrection</option>
<option value="spring">Spring</option>
<option value="summer">Summer (Bubbles)</option>
<option value="carnival">Carnival (Confetti)</option>
<option value="custom">⚙ Custom (Local Files)</option>
</select>
<div id="custom-fields" class="custom-fields">
<p>Javascript:</p>
<input type="text" id="custom-js" placeholder="mytheme.js">
<p>CSS:</p>
<input type="text" id="custom-css" placeholder="mytheme.css">
</div>
<button id="btn-load" onclick="loadTheme()">Load Theme</button>
<button id="btn-clear" class="btn-secondary" onclick="clearTheme()">Clear & Reload</button>
</div>
<script>
// ── Path Rewriter ──────────────────────────────────────────
// Theme JS files reference images using the production path:
// ../Seasonals/Resources/santa_images/gift1.png
// When testing locally, the images live next to this HTML file:
// ./santa_images/gift1.png
// This observer intercepts all new <img> elements and rewrites
// their src attribute so image-based themes (santa, halloween,
// autumn, easter, resurrection) work out of the box.
const PRODUCTION_PREFIX = '../Seasonals/Resources/';
const LOCAL_PREFIX = './';
function rewritePath(src) {
if (!src) return src;
// Handle both full URLs and relative paths
const idx = src.indexOf('Seasonals/Resources/');
if (idx !== -1) {
return LOCAL_PREFIX + src.substring(idx + 'Seasonals/Resources/'.length);
}
return src;
}
function rewriteElement(el) {
if (el.tagName === 'IMG' && el.src && el.src.includes('Seasonals/Resources/')) {
const newSrc = rewritePath(el.src);
console.log(`[Path Rewriter] ${el.src}${newSrc}`);
el.src = newSrc;
}
}
// Watch for dynamically added images and rewrite their paths
const pathRewriter = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue; // skip non-elements
rewriteElement(node);
// Also check children (e.g. a div with img inside)
if (node.querySelectorAll) {
node.querySelectorAll('img').forEach(rewriteElement);
}
}
// Also catch src attribute changes on existing elements
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
rewriteElement(mutation.target);
}
}
});
pathRewriter.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src']
});
// ── Built-in theme map (local file paths for testing) ──
const themes = {
snowfall: { css: 'snowfall.css', js: 'snowfall.js', container: 'snowfall-container' },
snowflakes: { css: 'snowflakes.css', js: 'snowflakes.js', container: 'snowflakes' },
snowstorm: { css: 'snowstorm.css', js: 'snowstorm.js', container: 'snowstorm-container' },
fireworks: { css: 'fireworks.css', js: 'fireworks.js', container: 'fireworks' },
halloween: { css: 'halloween.css', js: 'halloween.js', container: 'halloween-container' },
hearts: { css: 'hearts.css', js: 'hearts.js', container: 'hearts-container' },
christmas: { css: 'christmas.css', js: 'christmas.js', container: 'christmas-container' },
santa: { css: 'santa.css', js: 'santa.js', container: 'santa-container' },
autumn: { css: 'autumn.css', js: 'autumn.js', container: 'autumn-container' },
easter: { css: 'easter.css', js: 'easter.js', container: 'easter-container' },
resurrection: { css: 'resurrection.css', js: 'resurrection.js', container: 'resurrection-container' },
spring: { css: 'spring.css', js: 'spring.js', container: 'spring-container' },
summer: { css: 'summer.css', js: 'summer.js', container: 'summer-container' },
carnival: { css: 'carnival.css', js: 'carnival.js', container: 'carnival-container' },
};
const select = document.getElementById('theme-select');
const customFields = document.getElementById('custom-fields');
select.addEventListener('change', () => {
customFields.classList.toggle('visible', select.value === 'custom');
});
function clearTheme() {
// Remove injected CSS
document.querySelectorAll('link[data-seasonal]').forEach(el => el.remove());
// Remove injected JS
document.querySelectorAll('script[data-seasonal]').forEach(el => el.remove());
// Reset the seasonals container
const container = document.querySelector('.seasonals-container');
container.className = 'seasonals-container';
container.innerHTML = '';
// Remove any theme-created containers on body
const knownContainers = [
'.snowfall-container', '.snowflakes', '.snowstorm-container',
'.fireworks', '.halloween-container', '.hearts-container',
'.christmas-container', '.santa-container', '.autumn-container',
'.christmas-container', '.santa-container', '.autumn-container',
'.easter-container', '.resurrection-container', '.spring-container',
'.summer-container', '.carnival-container'
];
knownContainers.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
if (!el.classList.contains('seasonals-container')) el.remove();
});
});
// Remove any canvas elements left over
document.querySelectorAll('#snowfallCanvas, #snowstormCanvas').forEach(el => el.remove());
// Remove rabbit element
document.querySelectorAll('#rabbit, .hopping-rabbit').forEach(el => el.remove());
// Remove santa element
document.querySelectorAll('.santa, .present').forEach(el => el.remove());
console.log('[Test Site] Theme cleared.');
}
// Track active animation frames and observers for cleanup
let activeAnimationFrames = [];
let activeBlobUrls = [];
// Patch requestAnimationFrame and MutationObserver to track them
const origRAF = window.requestAnimationFrame;
const origCAF = window.cancelAnimationFrame;
let trackingEnabled = false;
window.requestAnimationFrame = function(cb) {
const id = origRAF.call(window, cb);
if (trackingEnabled) activeAnimationFrames.push(id);
return id;
};
window.cancelAnimationFrame = function(id) {
origCAF.call(window, id);
activeAnimationFrames = activeAnimationFrames.filter(f => f !== id);
};
// Track MutationObservers created by themes
let activeObservers = [];
const OrigMO = window.MutationObserver;
window.MutationObserver = class extends OrigMO {
constructor(cb) {
super(cb);
if (trackingEnabled) activeObservers.push(this);
}
};
// Track intervals created by themes
let activeIntervals = [];
const origSetInterval = window.setInterval;
const origClearInterval = window.clearInterval;
window.setInterval = function(...args) {
const id = origSetInterval.apply(window, args);
if (trackingEnabled) activeIntervals.push(id);
return id;
};
window.clearInterval = function(id) {
origClearInterval.call(window, id);
activeIntervals = activeIntervals.filter(i => i !== id);
};
function loadTheme() {
clearTheme();
// Cancel all tracked animation frames
activeAnimationFrames.forEach(id => origCAF.call(window, id));
activeAnimationFrames = [];
// Disconnect all tracked MutationObservers
activeObservers.forEach(obs => obs.disconnect());
activeObservers = [];
// Clear all tracked intervals
activeIntervals.forEach(id => origClearInterval.call(window, id));
activeIntervals = [];
// Revoke old blob URLs
activeBlobUrls.forEach(url => URL.revokeObjectURL(url));
activeBlobUrls = [];
const value = select.value;
if (!value || value === '') return;
let cssFile, jsFile, containerClass;
if (value === 'custom') {
cssFile = document.getElementById('custom-css').value.trim();
jsFile = document.getElementById('custom-js').value.trim();
containerClass = cssFile ? cssFile.replace('.css', '-container') : 'custom-container';
} else {
const theme = themes[value];
cssFile = theme.css;
jsFile = theme.js;
containerClass = theme.container;
}
// Update the seasonals-container class
const container = document.querySelector('.seasonals-container');
container.className = `seasonals-container ${containerClass}`;
// Inject CSS
if (cssFile) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssFile;
link.setAttribute('data-seasonal', 'true');
link.onerror = () => console.error(`[Test Site] Failed to load CSS: ${cssFile}`);
document.head.appendChild(link);
}
// Inject JS wrapped in IIFE to avoid const redeclaration errors
if (jsFile) {
setTimeout(async () => {
try {
const response = await fetch(jsFile);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const code = await response.text();
// Wrap in IIFE so each theme has its own scope
const wrappedCode = `(function() {\n${code}\n})();`;
const blob = new Blob([wrappedCode], { type: 'application/javascript' });
const blobUrl = URL.createObjectURL(blob);
activeBlobUrls.push(blobUrl);
trackingEnabled = true;
const script = document.createElement('script');
script.src = blobUrl;
script.setAttribute('data-seasonal', 'true');
script.onerror = () => console.error(`[Test Site] Failed to execute JS: ${jsFile}`);
document.body.appendChild(script);
console.log(`[Test Site] Loaded theme: ${value} (${jsFile}) [IIFE-wrapped]`);
} catch (err) {
console.error(`[Test Site] Failed to load JS: ${jsFile}`, err);
}
}, 150);
}
}
</script>
</body>
</html>

View File

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

274
README.md
View File

@@ -13,15 +13,17 @@ This plugin is based on my manual mod (see the [legacy branch](https://github.co
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Features](#features) - [Features](#features)
- [Overview](#overview) - [Overview](#overview)
- [Ideas for additional seasonals](#ideas-for-additional-seasonals)
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Client Compatibility](#client-compatibility)
- [Configuration](#configuration)
- [Automatic Theme Selection](#automatic-theme-selection) - [Automatic Theme Selection](#automatic-theme-selection)
- [Build Process](#build-process) - [Theme Settings](#theme-settings)
- [Build the plugin by yourself](#build-the-plugin-by-yourself)
- [Troubleshooting](#troubleshooting)
- [Effects Not Showing](#effects-not-showing)
- [Docker Permission Issues](#docker-permission-issues)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Legacy Manual Installation](#legacy-manual-installation)
- [Installation](#installation-1)
- [Usage](#usage-1)
- [Additional Directory: Separate Single Seasonals](#additional-directory-separate-single-seasonals)
--- ---
@@ -29,39 +31,83 @@ This plugin is based on my manual mod (see the [legacy branch](https://github.co
- **Automatic Theme Selection**: Dynamically updates the theme based on the date (e.g., snowflakes in December, fireworks for new year's eve). - **Automatic Theme Selection**: Dynamically updates the theme based on the date (e.g., snowflakes in December, fireworks for new year's eve).
- **Easy Integration**: No manual file editing required. The plugin injects everything automatically. - **Easy Integration**: No manual file editing required. The plugin injects everything automatically.
- **Configuration UI**: Configure settings directly in the Jellyfin Dashboard (very basic for now, needs some work in the future). - **Configuration UI**: Configure settings directly in the Jellyfin Dashboard.
<details>
<summary>Have a look:</summary>
<img width="852" height="782" alt="Admin-Settings" src="https://github.com/user-attachments/assets/03d04ea8-7dd9-418e-88f8-9ae2937c06bb" />
</details>
- **User Toggle**: Optionally allow users to enable/disable seasonal effects from their client.
<details>
<summary>Have a look:</summary>
<img width="467" height="263" alt="Client-Settings" src="https://github.com/user-attachments/assets/a8dfc90a-16c8-409c-9133-4139f6527b0b" />
</details>
## Overview ## Overview
Click on the following themes to expand them and see the theme in action:
<details>
<summary>Easter</summary>
**Easter**
![easter](https://github.com/user-attachments/assets/63665099-5c3c-4209-be6e-dda3686b2a49) ![easter](https://github.com/user-attachments/assets/63665099-5c3c-4209-be6e-dda3686b2a49)
</details>
<details>
<summary>Autumn</summary>
**Autumn**
![autumn](https://github.com/user-attachments/assets/df27d61c-d2d6-4776-82d7-89bf789a7462) ![autumn](https://github.com/user-attachments/assets/df27d61c-d2d6-4776-82d7-89bf789a7462)
</details>
<details>
<summary>Santa</summary>
**Santa**
![Santa-10](https://github.com/user-attachments/assets/a69b0aa3-537d-4463-b6bc-166f0a316c37) ![Santa-10](https://github.com/user-attachments/assets/a69b0aa3-537d-4463-b6bc-166f0a316c37)
</details>
<details>
<summary>Christmas</summary>
**Christmas**
![christmas](https://github.com/user-attachments/assets/e70a425d-866f-4617-bbfe-3c03e3654717) ![christmas](https://github.com/user-attachments/assets/e70a425d-866f-4617-bbfe-3c03e3654717)
</details>
<details>
<summary>Fireworks</summary>
**Fireworks**
![fireworks](https://github.com/user-attachments/assets/6c8b629e-b338-4dde-910b-c832aa29d77d) ![fireworks](https://github.com/user-attachments/assets/6c8b629e-b338-4dde-910b-c832aa29d77d)
</details>
<details>
<summary>Halloween</summary>
**Halloween**
![halloween](https://github.com/user-attachments/assets/221a1390-847e-45a4-b8eb-dc5b45d5df5c) ![halloween](https://github.com/user-attachments/assets/221a1390-847e-45a4-b8eb-dc5b45d5df5c)
</details>
<details>
<summary>Hearts</summary>
**Hearts**
![hearts](https://github.com/user-attachments/assets/e084cb0c-246e-4234-b6dd-d561923c6c91) ![hearts](https://github.com/user-attachments/assets/e084cb0c-246e-4234-b6dd-d561923c6c91)
</details>
<details>
<summary>Snowfall</summary>
**Snowfall**
![snowfall](https://github.com/user-attachments/assets/24bfdd84-f354-4129-933c-bb29b4180517) ![snowfall](https://github.com/user-attachments/assets/24bfdd84-f354-4129-933c-bb29b4180517)
</details>
<details>
<summary>Snowflakes</summary>
**Snowflakes**
![snowflakes](https://github.com/user-attachments/assets/78f2e925-8cf6-4a0b-8a25-f05594de4efd) ![snowflakes](https://github.com/user-attachments/assets/78f2e925-8cf6-4a0b-8a25-f05594de4efd)
</details>
<details>
<summary>Snowstorm</summary>
**Snowstorm**
![snowstorm](https://github.com/user-attachments/assets/6fd726d2-34d1-4f80-8ed6-2f482155059f) ![snowstorm](https://github.com/user-attachments/assets/6fd726d2-34d1-4f80-8ed6-2f482155059f)
</details>
## Ideas for additional seasonals
If you have any (specific) ideas for additional seasonals, feel free to open an issue or create a pull request. See the [contributing guidelines](CONTRIBUTING.md) for more details.
## Installation ## Installation
@@ -72,9 +118,9 @@ To install this plugin, you will first need to add the repository in Jellyfin.
1. Open your Jellyfin Dashboard. 1. Open your Jellyfin Dashboard.
2. Navigate to **Plugins** > **Manage Repositories**. 2. Navigate to **Plugins** > **Manage Repositories**.
3. Click the **+ New Repository** button to add a new repository. 3. Click the **+ New Repository** button to add a new repository.
4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field: 4. Enter a name for the repo and paste the following URL into the 'Repository URL' field:
```bash ```bash
https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/manifest.json https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-manifest/refs/heads/main/manifest.json
``` ```
5. Click **Add**. 5. Click **Add**.
6. Go to the **Available** tab at the top. 6. Go to the **Available** tab at the top.
@@ -83,14 +129,29 @@ To install this plugin, you will first need to add the repository in Jellyfin.
9. **Restart your Jellyfin server.** 9. **Restart your Jellyfin server.**
10. **You may need to refresh your browser page** (F5 or Ctrl+R) to see the changes. 10. **You may need to refresh your browser page** (F5 or Ctrl+R) to see the changes.
## Usage ## Client Compatibility
Since this plugin relies on modifying the web interface (CSS/JS injection), it only works on clients that use the web wrapper. Native clients that use their own UI rendering engine are not supported.
| Client Platform | Status | Notes |
| :--- | :---: | :--- |
| **Web Browsers** (Firefox, Chrome etc.) | ✅ | Direct JS injection |
| **Jellyfin Media Player** (Windows/Linux/macOS) | ✅ | Uses jellyfin web |
| **Android App** | ✅ | Uses a web wrapper |
| **iOS App** | ✅ | Uses a web wrapper |
| **Android TV / Fire TV** | ❌ | **Not supported.** Uses a native Java/Kotlin UI. |
| **Roku** | ❌ | **Not supported.** Uses a native UI. |
| **Swiftfin** (iOS/tvOS) | ❌ | **Not supported.** Uses a native Swift UI. |
| **Kodi** (via Jellyfin Addon) | ❌ | **Not supported.** Uses Kodi's native skinning engine. |
## Configuration
After installation and restart: After installation and restart:
1. Go to **Dashboard** > **Plugins** > **Seasonals**. 1. Go to **Dashboard** > **Plugins** > **Seasonals**.
2. **Enable Seasonals**: Toggle the plugin on or off. 2. **Enable Seasonals**: Toggle the plugin on or off.
3. **Automatic Selection**: 3. **Automatic Selection**:
* If enabled, the plugin selects the theme based on the current date (e.g., Snow in Winter, Hearts in February). * If enabled, the plugin selects the theme based on the current date (e.g., Snow in Winter, Hearts in February). See the table below for details.
* If disabled, you can manually select a theme from the dropdown list. * If disabled, you can manually select a theme from the dropdown list.
4. **Save** your settings. 4. **Save** your settings.
5. **Reload your browser page** (F5 or Ctrl+R) to see the changes. 5. **Reload your browser page** (F5 or Ctrl+R) to see the changes.
@@ -112,7 +173,10 @@ If automatic selection is enabled, the following themes are applied based on the
> **Note:** Holiday themes (like `santa` or `fireworks`) override monthly seasonal themes (like `snowflakes`). > **Note:** Holiday themes (like `santa` or `fireworks`) override monthly seasonal themes (like `snowflakes`).
## Build Process ## Theme Settings
Each theme contains additional settings to customize its behavior. Expand the advanced configuration section to configure each theme, adjust parameters like particle count, animation speed etc.
## Build the plugin by yourself
If you want to build the plugin yourself: If you want to build the plugin yourself:
@@ -124,120 +188,60 @@ If you want to build the plugin yourself:
``` ```
4. The compiled DLL and resources will be in bin/Publish. 4. The compiled DLL and resources will be in bin/Publish.
## Troubleshooting
### Effects Not Showing
1. **Verify plugin installation**:
- Check that the plugin appears in the jellyfin admin panel
- Ensure that the plugin is enabled and active
2. **Clear browser cache**:
- Force refresh browser (Ctrl+F5)
- Clear jellyfin web client cache (--> mostly you have to clear the whole browser cache)
### Docker Permission Issues
If you encounter the message `Access was denied when attempting to inject script into index.html. Automatic direct injection failed. Automatic direct insertion failed. The system will now attempt to use the File Transformation plugin.` in the log or similar permission errors in Docker:
**Option 1: Use File Transformation Plugin (Recommended)**
Seasonals now automatically detects and uses the [File Transformation](https://github.com/IAmParadox27/jellyfin-plugin-file-transformation) plugin (v2.5.0.0+) if it's installed. This eliminates permission issues by transforming content at runtime without modifying files on disk.
**Installation Steps:**
1. Install the File Transformation plugin from the Jellyfin plugin catalog
2. Restart Jellyfin
3. Seasonals will automatically detect and use it (no configuration needed)
4. Check logs to confirm: Look for "Successfully registered transformation with File Transformation plugin"
**Benefits:**
- No file permission issues in Docker environments
- Works with read-only web directories
- Survives Jellyfin updates without re-injection
- No manual file modifications required
**Option 2: Fix File Permissions**
```bash
# Find the actual index.html location
docker exec -it jellyfin find / -name index.html
# Fix ownership (replace 'jellyfin' with your container name and adjust user:group if needed)
docker exec -it --user root jellyfin chown jellyfin:jellyfin /jellyfin/jellyfin-web/index.html
# Restart container
docker restart jellyfin
```
**Option 3: Manual Volume Mapping**
```bash
# Extract index.html from container
docker cp jellyfin:/jellyfin/jellyfin-web/index.html /path/to/jellyfin/config/index.html
# Add to docker-compose.yml volumes section:
volumes:
- /path/to/jellyfin/config/index.html:/jellyfin/jellyfin-web/index.html
```
## Contributing ## Contributing
Feel free to contribute to this project by creating pull requests or reporting issues. Feel free to contribute to this project by creating pull requests or reporting issues.
For detailed contribution guidelines, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file.
---
## Legacy Manual Installation
<details>
<summary>Click to expand instructions for the old manual installation method (without plugin)</summary>
### Installation
> [!TIP]
> Take a look at [CodeDevMLH/Jellyfin-Mods-Automated-Script](https://github.com/CodeDevMLH/Jellyfin-Mods-Automated-Script)
1. **Add Seasonal Container to `index.html`**
Edit the `index.html` file of your Jellyfin web instance. Add the following code inside the `<body>` tag:
```html
<div class="seasonals-container"></div>
<script src="seasonals/seasonals.js"></script>
```
2. **Deploy Files**
Place the seasonals folder (including seasonals.js, CSS, and additional JavaScript files for each theme [this one](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/legacy/seasonals)) inside the Jellyfin web server directory (labeld with "web").
3. **Configure Themes**
Customize the theme-configs.js file to modify or add new themes. The default configuration is shown below:
```javascript
const automateThemeSelection = true; // Set to false to disable automatic theme selection based on current date
const defaultTheme = 'none'; // Default theme if automatic selection is off
const themeConfigs = {
snowflakes: {
css: 'seasonals/snowflakes.css',
js: 'seasonals/snowflakes.js',
containerClass: 'snowflakes'
},
snowfall: {
css: 'seasonals/snowfall.css',
js: 'seasonals/snowfall.js',
containerClass: 'snowfall-container'
},
// more configs...
none: {
containerClass: 'none'
},
};
```
4. **Reload the web interface**
After making these changes, restart your Jellyfin server and/or refresh the web interface (ctrl+F5, sometimes you need to clear the browsers temp files/cache (every time with firefox ;-()) to see the changes.
### Theme Settings
Each theme's JavaScript file contains additional settings to customize its behavior. Here are examples for the `autumn` and `snowflakes` themes:
**Autumn Theme Settings**
```javascript
const leaves = true; // Enable/disable leaves
const randomLeaves = true; // Enable random leaves
const randomLeavesMobile = false; // Enable random leaves on mobile devices
const enableDiffrentDuration = true; // Enable different animation duration for random leaves
const leafCount = 25; // Number of random extra leaves
```
**Snowflakes Theme Settings**
```javascript
const snowflakes = true; // Enable/disable snowflakes
const randomSnowflakes = true; // Enable random snowflakes
const randomSnowflakesMobile = false; // Enable random snowflakes on mobile devices
const enableColoredSnowflakes = true; // Enable colored snowflakes on mobile devices
const enableDiffrentDuration = true; // Enable different animation duration for random snowflakes
const snowflakeCount = 25; // Number of random extra snowflakes
```
### Usage
**Automatic Theme Selection**
By default, the theme is automatically selected based on the date. For example:
Snowfall: January
Hearts: February (Valentine's Day)
Halloween: October
Modify the determineCurrentTheme() function in seasonals.js to adjust date-based logic.
**Manual Theme Selection**
To use a fixed theme, set automateThemeSelection to false in the theme-configs.js file and specify a defaultTheme.
**Custom Themes**
1. Add your CSS and JavaScript files to the seasonals folder.
2. Extend the themeConfigs object with your theme details:
```javascript
myTheme: {
css: 'seasonals/my-theme.css',
js: 'seasonals/my-theme.js',
containerClass: 'my-theme-container',
}
```
### Additional Directory: Separate Single Seasonals
For users who prefer not to use the automatic seasonal theme selection, individual seasonals are available in the `separate single seasonals` folder. Each seasonal theme can be independently loaded and used without relying on the main automatic selection system.
but this requires to the modify of the `index.html` with adding the html in `add_to_index_html`.
To use a single seasonal theme, include its specific CSS and JS files in your `index.html` inside the `<body> </body>` tags provided by `add_to_index_html.html` in the sub-theme-folders as shown below:
```html
<div class="seasonalsname-container"></div>
<script src="separate single seasonals/snowflakes.js"></script>
<link rel="stylesheet" href="separate single seasonals/snowflakes.css">
</details>

View File

@@ -9,10 +9,7 @@ Bevor du baust, musst du die Versionsnummer in den folgenden Dateien aktualisier
1. **`Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj`** 1. **`Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj`**
Suche nach `<Version>...</Version>` und ändere die Nummer. Suche nach `<Version>...</Version>` und ändere die Nummer.
2. **`build.yaml`** 2. **`manifest.json`**
Ändere den Wert bei `version: "..."`.
3. **`manifest.json`**
Füge einen neuen Eintrag oben in die `versions`-Liste ein (oder bearbeite den vorhandenen, wenn es noch kein Release gab). Füge einen neuen Eintrag oben in die `versions`-Liste ein (oder bearbeite den vorhandenen, wenn es noch kein Release gab).
* `version`: Deine neue Nummer. * `version`: Deine neue Nummer.
* `changelog`: Was hat sich geändert? * `changelog`: Was hat sich geändert?

View File

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

View File

@@ -1,15 +0,0 @@
---
name: "Seasonals"
guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"
version: "1.2.0.0"
targetAbi: "10.11.0.0"
framework: "net9.0"
overview: "Seasonal effects for Jellyfin"
description: >
Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.
category: "General"
owner: "CodeDevMLH"
artifacts:
- "Jellyfin.Plugin.Seasonals.dll"
changelog: >
Added Advanced Configuration UI for customizing individual seasonal effects.

View File

@@ -1,34 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<JellyfinVersion>10.11.0</JellyfinVersion>
<TargetFramework Condition="$(JellyfinVersion.StartsWith('10.11'))">net9.0</TargetFramework>
<TargetFramework Condition="!$(JellyfinVersion.StartsWith('10.11'))">net8.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.Seasonals</RootNamespace>
<Nullable>enable</Nullable>
<!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode> -->
<!-- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> -->
<!-- <GenerateDocumentationFile>true</GenerateDocumentationFile> -->
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors>
<Version>1.1.0.0</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="$(JellyfinVersion)" />
<PackageReference Include="Jellyfin.Model" Version="$(JellyfinVersion)" />
</ItemGroup>
<ItemGroup>
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />
<None Remove="Web\**" />
<EmbeddedResource Include="Web\**" />
<None Include="..\README.md" />
<None Include="..\logo.png" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>

View File

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

View File

@@ -1,18 +0,0 @@
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Registers plugin services.
/// </summary>
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddTransient<IStartupFilter, ScriptInjectionStartupFilter>();
}
}

View File

@@ -1,102 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Middleware to inject the Seasonals script into the Jellyfin web interface.
/// </summary>
public class ScriptInjectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ScriptInjectionMiddleware> _logger;
private const string ScriptTag = "<script src=\"Seasonals/Resources/seasonals.js\"></script>";
private const string Marker = "</body>";
/// <summary>
/// Initializes a new instance of the <see cref="ScriptInjectionMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public ScriptInjectionMiddleware(RequestDelegate next, ILogger<ScriptInjectionMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Invokes the middleware.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task InvokeAsync(HttpContext context)
{
if (IsIndexRequest(context.Request.Path))
{
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
try
{
await _next(context);
context.Response.Body = originalBodyStream;
responseBody.Seek(0, SeekOrigin.Begin);
if (context.Response.StatusCode == 200 &&
context.Response.ContentType != null &&
context.Response.ContentType.Contains("text/html", StringComparison.OrdinalIgnoreCase))
{
using var reader = new StreamReader(responseBody);
var content = await reader.ReadToEndAsync();
if (!content.Contains(ScriptTag, StringComparison.Ordinal) && content.Contains(Marker, StringComparison.Ordinal))
{
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}", StringComparison.Ordinal);
var bytes = Encoding.UTF8.GetBytes(newContent);
context.Response.ContentLength = bytes.Length;
await context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
return;
}
}
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error injecting Seasonals script via middleware.");
// Ensure we try to write back the original response if something failed
if (responseBody.Length > 0 && context.Response.Body == originalBodyStream)
{
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
}
}
else
{
await _next(context);
}
}
private static bool IsIndexRequest(PathString path)
{
if (!path.HasValue)
{
return false;
}
var p = path.Value;
// Check for root, index.html, or web/index.html
return p.Equals("/", StringComparison.OrdinalIgnoreCase) ||
p.Equals("/index.html", StringComparison.OrdinalIgnoreCase) ||
p.EndsWith("/web/index.html", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -1,21 +0,0 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace Jellyfin.Plugin.Seasonals;
/// <summary>
/// Startup filter to register the ScriptInjectionMiddleware.
/// </summary>
public class ScriptInjectionStartupFilter : IStartupFilter
{
/// <inheritdoc />
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<ScriptInjectionMiddleware>();
next(builder);
};
}
}

163
git_cheat_sheet.md Normal file
View File

@@ -0,0 +1,163 @@
# 🚀 Git Cheat Sheet: Branches, Merge, Rebase & VS Code
## 🌳 Branches (Zweige)
* **`main`** → Stabiler Stand, Production-ready (Deployment).
* **`dev`** → Aktive Entwicklung, Sammelbecken für Features.
---
## 🛠 Zusammenführen von Änderungen
### Git Merge
Führt zwei Zweige zusammen. Git sucht den letzten gemeinsamen Basispunkt und erstellt einen neuen **Merge-Commit**.
```bash
git checkout dev
git merge main
```
| Details | |
|-----------|--------|
| Wann? | In Team-Branches, bei bereits gepushten Branches, wenn Stabilität wichtiger als eine saubere History ist. |
| Vorteile | Sicher, einfach nachvollziehbar, schreibt die History nicht um. |
| Nachteile | Die History kann bei vielen Merges unübersichtlich ("Spaghetti-Graph") werden. |
### Git Rebase
Setzt deine Commits neu auf die Spitze eines anderen Branches. Die Commit-IDs werden dabei neu geschrieben.
```bash
git stash # (optional) Änderungen parken.
git checkout dev
git fetch origin
git rebase origin/main
git stash pop # (optional) Änderungen zurückholen.
```
| Details | |
|-----------|--------|
| Wann? | In lokalen Feature-Branches (bevor sie geteilt werden), um die History sauber zu halten. |
| Vorteile | Erzeugt eine perfekt lineare, leicht lesbare History. |
| Nachteile | ⚠️ Gefährlich auf geteilten/öffentlichen Branches. Konflikte müssen ggf. für jeden einzelnen Commit gelöst werden. |
---
## **Git: dev zurücksetzen & „main ist Chef“**
Problem
Beim Arbeiten mit Git passiert oft Folgendes:
- `dev` hat Commits, die nicht in `main` sind
- diese Commits brauche ich nicht mehr
- beim `git rebase origin/main` will Git trotzdem mergen oder Konflikte lösen
⚠️ Wichtiges Missverständnis:
Das sind keine lokalen Änderungen, sondern Commits, die auf `dev` existieren.
`git reset --hard origin/dev` entfernt nur lokale, nicht gepushte Commits,
aber nicht Commits, die bereits auf `origin/dev` liegen.
Praktische Befehle & sichere Vorgehensweise
- Prüfen — welche Commits würden entfernt werden:
```bash
git fetch origin
git log --oneline origin/main..origin/dev
```
- Optional: Backup des aktuellen `origin/dev`, falls etwas schiefgeht:
```bash
git fetch origin
git branch backup/dev origin/dev
```
- Sicheres Zurücksetzen von `dev` auf `main` (lokal + remote):
```bash
git checkout dev
git fetch origin
git reset --hard origin/main
git push --force-with-lease origin dev
```
- Alternative (ohne lokalen Checkout): Push von `main` direkt nach `dev`:
```bash
git fetch origin
git push --force-with-lease origin origin/main:refs/heads/dev
```
- Warnung: Diese Aktionen schreiben die RemoteHistory um. Koordiniere dich mit
dem Team bevor du ein ForcePush ausführst. Verwende `--force-with-lease`
anstelle von `--force` für etwas mehr Sicherheit.
## 📦 Temporäres Speichern & Spezialbefehle
### Stash (Das "Regal")
Speichert uncommitted Changes temporär, um das Arbeitsverzeichnis sauber zu machen, ohne zu committen.
```bash
git stash # Änderungen "parken".
git stash pop # Änderungen zurückholen und Stash leeren.
```
**Wann?** Wenn du schnell den Branch wechseln musst, aber deine Arbeit noch nicht fertig ist.
### Cherry-pick
Kopiert einen ganz gezielten Commit von einem Branch in deinen aktuellen.
```bash
git cherry-pick <commit-hash>
```
**Wann?** Wenn ein Bugfix auf dem falschen Branch gelandet ist oder du nur eine einzige Funktion aus einem Feature-Branch brauchst.
---
## 🔄 Checkout & Switch
```bash
git checkout dev # oder git switch dev Wechselt zum Branch.
git checkout -f dev # Force Checkout: Wechselt den Branch und verwirft alle ungespeicherten lokalen Änderungen unwiderruflich! ⚠️
```
---
## 💻 VS Code Git-Optionen (UI)
VS Code bietet beim Branch-Wechsel oft drei intelligente Optionen an:
* **Migrate Changes ⭐**
* Nimmt deine aktuellen Änderungen einfach mit in den neuen Branch.
* (Intern: stash → switch → stash pop).
* **Stash & Checkout**
* Parkt deine Änderungen sicher im Stash und wechselt den Branch. Die Änderungen bleiben im Stash, bis du sie manuell wieder herausholst.
* **Force Checkout ⚠️**
* Wechselt den Branch und löscht deine aktuellen, ungespeicherten Änderungen. Nur nutzen, wenn die Arbeit weggeworfen werden kann.
---
## 🔄 Typischer Sync-Workflow
Um den Entwicklungs-Branch aktuell zu halten, nachdem dev in main gemerged wurde:
1. Auf dev entwickeln.
2. Merge dev → main für das Release.
3. Zurück auf dev wechseln:
```bash
git checkout dev
git merge main # (oder rebase), um den neuesten Stand vom Main wieder in Dev zu haben.
```
---
## 🧠 Merksätze
* **Merge** = Historien verbinden (Sicher & Dokumentiert).
* **Rebase** = Historie neu schreiben (Linear & Sauber).
* **Stash** = "Ich parke das mal kurz hier."
* **Migrate Changes** = Sicherer Branch-Wechsel mit "Gepäck".

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -1,21 +0,0 @@
[
{
"guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4",
"name": "Seasonals",
"description": "Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.",
"overview": "Seasonal effects for Jellyfin",
"owner": "CodeDevMLH",
"category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [
{
"version": "1.0.0.0",
"changelog": "Initial release",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.0.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be6d06a959b3e18e5058a6d8fb6d800c",
"timestamp": "2025-12-15T15:33:15Z"
}
]
}
]

View File

@@ -2,35 +2,107 @@
{ {
"guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4", "guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4",
"name": "Seasonals", "name": "Seasonals",
"description": "Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.", "description": "Adds atmospheric, animated seasonal effects like snow, falling leaves, hearts, fireworks and more. The plugin provides a settings page for configuration. If you do not have write permissions to the web folder, please also install the file-transformation plugin.",
"overview": "Seasonal effects for Jellyfin", "overview": "Atmospheric, configurable seasonal effects for the Jellyfin web interface",
"owner": "CodeDevMLH", "owner": "CodeDevMLH",
"category": "General", "category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [ "versions": [
{ {
"version": "1.2.0.0", "version": "1.7.1.1",
"changelog": "Bug fixing: Fix path, fix injection issue, added File Transformator as fallback if direct injection is blocked due to permissions.", "changelog": "- feat: add summer, spring and carnival themes",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.2.0.0/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.1/Jellyfin.Plugin.Seasonals.zip",
"checksum": "be2e93364396b6e0e02368d5a7db53bc", "checksum": "58a418e0070aab6f47b136fc305e2fc1",
"timestamp": "2025-12-17T15:32:08Z" "timestamp": "2026-02-19T18:00:57Z"
}, },
{ {
"version": "1.1.1.0", "version": "1.7.0.15",
"changelog": "Bug fixing: Added Advanced Configuration UI for customizing individual seasonal effects.", "changelog": "- feat: add customizable auto seasonal list via config page\n- feat: add new season theme 'resurrection' by Bioflash257",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.1.1.0/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.0.15/Jellyfin.Plugin.Seasonals.zip",
"checksum": "f1d7e2b24ad474904fa0eb1749e855b0", "checksum": "d1fc094710efe45ea8cc885bc6a826c4",
"timestamp": "2025-12-16T00:56:08Z" "timestamp": "2026-02-17T13:11:21Z"
},
{
"version": "1.6.3.0",
"changelog": "- fix path issue on subpath installations",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.6.3.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "e9a1fb6c91b8b48978efb43c72e462a0",
"timestamp": "2026-02-15T01:12:57Z"
},
{
"version": "1.5.1.0",
"changelog": "- fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.5.1.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "a66b5c24c1f4bfff14bba5e93576bb80",
"timestamp": "2026-01-28T22:40:49Z"
},
{
"version": "1.5.0.0",
"changelog": "- Refactor script injection logic",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.5.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "ba8d3c358df3e0546b99113b43f54fea",
"timestamp": "2026-01-08T23:31:52Z"
},
{
"version": "1.4.1.0",
"changelog": "- fix fireworks display issue on scroll",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.1.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "bd8d9c6af064de011a708ea85e9b08c0",
"timestamp": "2026-01-06T21:12:46Z"
},
{
"version": "1.4.0.0",
"changelog": "- settings linked directly in the main menu\n- renamed main plugin script\n- added enable/disable toggle for the entire plugin",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "205606075eec5f93d3da37efaecdeab5",
"timestamp": "2025-12-28T19:11:04Z"
},
{
"version": "1.3.4.0",
"changelog": "- some fixes for js loading\n- adapted config page descriptions",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.4.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "4869a9a0c08317d2cb0e0fc8f454cf2b",
"timestamp": "2025-12-24T02:02:15Z"
},
{
"version": "1.3.3.0",
"changelog": "- fixed: load config",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.3.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "9908c9357b2d8850e6349ce134bad2bd",
"timestamp": "2025-12-18T00:11:39Z"
},
{
"version": "1.3.0.0",
"changelog": "- fixed: image paths to ensure proper loading of resources.\n- fix: z-index issue to ensure seasonal effects appear above other UI elements.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "362fd94ab11f03e345a911a95d2d763b",
"timestamp": "2025-12-17T21:46:53Z"
},
{
"version": "1.2.0.0",
"changelog": "Advanced settings added: Users can now customize the intensity and speed of seasonal effects through the settings panel.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.2.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "5e18022c914c06072035dc0b4429e0fe",
"timestamp": "2025-12-17T20:59:37Z"
}, },
{ {
"version": "1.1.0.0", "version": "1.1.0.0",
"changelog": "Added Advanced Configuration UI for customizing individual seasonal effects.", "changelog": "Bug fixing: Fix path, fix injection issue, added File Transformator as fallback if direct injection is blocked due to permissions.",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.1.0.0/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.1.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "efc26cf0d09b313c52c089fc43df12ab", "checksum": "be2e93364396b6e0e02368d5a7db53bc",
"timestamp": "2025-12-16T00:22:10Z" "timestamp": "2025-12-17T15:32:08Z"
}, },
{ {
"version": "1.0.0.0", "version": "1.0.0.0",
@@ -41,5 +113,296 @@
"timestamp": "2025-12-15T15:33:15Z" "timestamp": "2025-12-15T15:33:15Z"
} }
] ]
},
{
"guid": "d7e11d57-819b-4bdd-a88d-53c5f5560225",
"name": "Media Bar Enhanced",
"description": "A feature-rich fork of the original Media Bar script by MakD that brings your home screen to life.\n\n-> 100% Configurable via Web UI: Manage all features, lists, and settings effortlessly through the plugin configuration page.\n\nKey Highlights:\n- Cinematic Video Backdrops: Supports local & YouTube trailers (incl. SponsorBlock)\n- Custom Content: Curate your slideshow with specific Collections, Playlists, or seasonal events\n\nAdditional Features:\n- Full-width immersive mode\n- Smart resolution handling (up to 4K)\n- Full keyboard navigation & playback control\n- Wait-for-trailer options\n- Customizable pagination & animations\n\nIf you do not have write permissions to the web folder, please also install the file-transformation plugin.",
"overview": "Transforms your Jellyfin home screen with an immersive, fully configurable media slideshow featuring video backdrops.",
"owner": "CodeDevMLH",
"category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [
{
"version": "1.4.0.12",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.12/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "26edee51b52dcee4ecf388aa376f3869",
"timestamp": "2026-02-04T18:07:40Z"
},
{
"version": "1.4.0.11",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.11/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "ca0b3270eba5871e7a23db6b45bc5048",
"timestamp": "2026-02-04T17:58:14Z"
},
{
"version": "1.4.0.10",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.10/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "9618570053f7acef445a034c1e2e044b",
"timestamp": "2026-02-04T17:51:25Z"
},
{
"version": "1.4.0.9",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.9/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "29b3ffb9caeab135df88b6313032fc50",
"timestamp": "2026-02-04T17:39:11Z"
},
{
"version": "1.4.0.8",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.8/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "ec343204a7cd2c1af4013e645bdddcd3",
"timestamp": "2026-02-04T17:27:22Z"
},
{
"version": "1.4.0.7",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "450e5977228d08b8451b6047e4a6be94",
"timestamp": "2026-02-04T17:09:28Z"
},
{
"version": "1.4.0.6",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.6/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "348ebf449ac77fd156e2afbd03e80fce",
"timestamp": "2026-02-04T16:40:21Z"
},
{
"version": "1.4.0.5",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.5/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "3ba68bae1c492767bddab2dee2540226",
"timestamp": "2026-02-04T16:23:14Z"
},
{
"version": "1.4.0.4",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "30a15dd883de7656c4480cfa932e9858",
"timestamp": "2026-02-04T16:17:15Z"
},
{
"version": "1.4.0.3",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "9abf21c095e1ae99cdbeb51edb08f370",
"timestamp": "2026-02-04T15:52:11Z"
},
{
"version": "1.4.0.2",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "6026fb8878a51f6dbe18aab1ac006df8",
"timestamp": "2026-02-04T15:45:39Z"
},
{
"version": "1.4.0.1",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "4068c03b1ab809906d64d4faed1c1b0e",
"timestamp": "2026-02-04T15:01:50Z"
},
{
"version": "1.4.0.0",
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "20faa2a703dbb46591f4bd09e6ab7ec3",
"timestamp": "2026-02-04T12:49:45Z"
},
{
"version": "1.3.0.3",
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "1d9e0a8342d46f84aed3f7bd1bee32d3",
"timestamp": "2026-02-04T01:41:35Z"
},
{
"version": "1.3.0.2",
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "22e79daa5f433ca09a3db4f8e37679b4",
"timestamp": "2026-02-04T01:27:55Z"
},
{
"version": "1.3.0.1",
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "5a4f555e29c733dabd51169f6ace56eb",
"timestamp": "2026-02-04T01:14:19Z"
},
{
"version": "1.3.0.0",
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "83c26ba8f7ad6e1a7fe73c7190f532f3",
"timestamp": "2026-02-04T00:07:15Z"
},
{
"version": "1.2.3.7",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 3.0.9 from original repo",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "fa1bf48cff159cc7dbf0aab48511a37c",
"timestamp": "2026-01-28T22:39:54Z"
},
{
"version": "1.2.3.6",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.6/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "da73bb490548c122906419d2762a2d00",
"timestamp": "2026-01-28T21:31:54Z"
},
{
"version": "1.2.3.5",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.5/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "b5efea79ec465522dad31e4ee5f1710c",
"timestamp": "2026-01-28T20:21:20Z"
},
{
"version": "1.2.3.4",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "4683f75e2df2590db663303bdd329ccd",
"timestamp": "2026-01-28T01:09:38Z"
},
{
"version": "1.2.3.3",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "3c18a84b2f59c86c130e91da83f980a2",
"timestamp": "2026-01-28T01:05:45Z"
},
{
"version": "1.2.3.2",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "4109a3ea10eb3145217b24ee8f8b37b5",
"timestamp": "2026-01-28T00:30:36Z"
},
{
"version": "1.2.3.1",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "e73029ac767e24d36742a27678758b6f",
"timestamp": "2026-01-28T00:17:28Z"
},
{
"version": "1.2.3.0",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "ae6f34bee76f9d7873964a71ca191bf3",
"timestamp": "2026-01-27T23:54:42Z"
},
{
"version": "1.2.2.0",
"changelog": "- Fixes issues with persistent slides-container visibility",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.2.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "3362f93815845c4e85b66b31bcd0f52c",
"timestamp": "2026-01-24T22:53:55Z"
},
{
"version": "1.2.1.0",
"changelog": "- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 3.0.8 from original repo",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "70defc1fb29a17ff4c9362bf7bdc53b5",
"timestamp": "2026-01-22T23:50:56Z"
},
{
"version": "1.2.0.0",
"changelog": "- Add video quality preference setting (Auto / 1080p / Highres)\n- Set preferred video quality on YouTube player based on setting",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "0b6379f68990026240d97fe8f77fbef1",
"timestamp": "2026-01-08T23:30:58Z"
},
{
"version": "1.1.2.0",
"changelog": "- Add method to resume video playback when slideshow is active",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.2.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "a0e8ff5e59b22a1bdedc916cd5e1c16a",
"timestamp": "2026-01-08T15:26:55Z"
},
{
"version": "1.1.1.0",
"changelog": "- Add method to pause all video playback when navigating away from home screen",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "09da95fc561b11191d23a5cfa30ea731",
"timestamp": "2026-01-08T14:54:57Z"
},
{
"version": "1.1.0.0",
"changelog": "- 'custom media IDs' setting is now enabled by default (no input --> random selection)\n- improve GUID handling in slideshow manager to handle seperator and description",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "32305d72b8d704acf8eef0c22277fee9",
"timestamp": "2026-01-08T02:15:50Z"
},
{
"version": "1.0.0.3",
"changelog": "fixes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "e6180c42836069029072e96ac4860c42",
"timestamp": "2026-01-06T23:26:29Z"
},
{
"version": "1.0.0.2",
"changelog": "fixes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "1041b403ec0193c2172a6fe15501afd3",
"timestamp": "2026-01-06T21:21:37Z"
},
{
"version": "1.0.0.1",
"changelog": "fixes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "f4e6194a9cc72fdda7436161c73832de",
"timestamp": "2026-01-06T21:18:33Z"
},
{
"version": "1.0.0.0",
"changelog": "Initial release",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "2ba7cc7f238f6aa7097628797935b903",
"timestamp": "2026-01-06T18:56:30Z"
}
]
} }
] ]