Compare commits

..

318 Commits

Author SHA1 Message Date
CodeDevMLH
fdadc00a0c Update manifest.json for release v1.7.1.4 [skip ci] 2026-02-21 14:24:25 +00:00
CodeDevMLH
2ab88fd5ac Bump version to 1.7.1.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-21 15:23:35 +01:00
CodeDevMLH
9a41c0a2ce Adjust sunbeam and butterfly positioning for improved visuals 2026-02-21 15:23:29 +01:00
CodeDevMLH
816f58cf02 Update SpringOptions configuration and HTML for seasonal options
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-02-21 15:11:29 +01:00
CodeDevMLH
5be9a60eed Update manifest.json for release v1.7.1.3 [skip ci] 2026-02-21 13:50:20 +00:00
CodeDevMLH
133808105e Bump version to 1.7.1.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-21 14:49:26 +01:00
CodeDevMLH
c631aca44f Add Cherry Blossom seasonal options to configuration 2026-02-21 14:49:08 +01:00
CodeDevMLH
241450d132 Increase default counts for pollen and butterflies in SpringOptions configuration 2026-02-21 14:44:09 +01:00
CodeDevMLH
d50d71bde1 Update manifest.json for release v1.7.1.2 [skip ci] 2026-02-21 03:33:25 +00:00
CodeDevMLH
262dd98519 Bump version to 1.7.1.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m42s
2026-02-21 04:31:42 +01:00
CodeDevMLH
b45ec73a67 Enhance spring configuration options by adding counts for birds, butterflies, bees, and ladybugs; update UI labels and descriptions for clarity. 2026-02-21 04:31:22 +01:00
CodeDevMLH
4e8a37540f Refactor spring and carnival animations, enhance configuration options, and improve asset management 2026-02-21 04:31:15 +01:00
CodeDevMLH
cde5201991 Add Cherry Blossom option to theme selector and configuration 2026-02-21 04:31:04 +01:00
CodeDevMLH
b2420b8eb4 Add Rotkehlchen GIF asset for spring animations 2026-02-21 04:30:54 +01:00
CodeDevMLH
dacec7d03c Add new spring-themed GIF assets for animations 2026-02-21 04:30:47 +01:00
CodeDevMLH
65f8261fb7 Add Cherry Blossom feature with configuration options and animations [skip ci] 2026-02-20 01:09:23 +01:00
CodeDevMLH
78872e7f96 Remove petal and ladybug functionality from spring animation 2026-02-20 00:55:54 +01:00
CodeDevMLH
45c9a199c2 Update manifest.json for release v1.7.1.1 [skip ci] 2026-02-19 18:00:57 +00:00
CodeDevMLH
1df6fb37b1 now?
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-19 18:51:28 +01:00
CodeDevMLH
82a1e8a178 Refactor RemoveLegacyTags method to include modification tracking and update logging
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 12s
2026-02-19 18:36:08 +01:00
CodeDevMLH
22bf887d10 Bump version to 1.7.1.1
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 52s
2026-02-19 18:31:49 +01:00
CodeDevMLH
07600766cf Add legacy tag removal functionality to ScriptInjector 2026-02-19 18:31:10 +01:00
CodeDevMLH
56298487f4 Update manifest.json for release v1.7.1.0 [skip ci] 2026-02-19 02:20:24 +00:00
CodeDevMLH
89fc1c38f0 Update version to 1.7.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-19 03:19:31 +01:00
CodeDevMLH
4c168a5ec2 Add Spring, Summer, and Carnival options to plugin configuration and UI 2026-02-19 03:18:39 +01:00
CodeDevMLH
92d9e1a9ad Add spring, summer, and carnival themes to the theme selector 2026-02-19 03:18:32 +01:00
CodeDevMLH
007e55a612 Refactor summer object creation to use configurable bubble and dust counts 2026-02-19 03:18:26 +01:00
CodeDevMLH
20da9899e4 Fix typo in resurrection CSS class and animation names 2026-02-19 03:18:20 +01:00
CodeDevMLH
9b9cad1caa Add carnival animation effects with CSS and JavaScript implementation 2026-02-19 03:18:13 +01:00
CodeDevMLH
e8e3424cc9 Add spring animation effects with CSS and JavaScript implementation 2026-02-19 03:18:06 +01:00
CodeDevMLH
0eeed99508 Add summer animation effects with CSS and JavaScript implementation 2026-02-19 02:22:54 +01:00
CodeDevMLH
a0f261f597 Update CONTRIBUTING.md to enhance clarity on testing themes with test-site.html [skip ci] 2026-02-17 14:23:03 +01:00
CodeDevMLH
35d92862aa Update manifest.json for release v1.7.0.15 [skip ci] 2026-02-17 13:11:22 +00:00
CodeDevMLH
693bb35aac Bump version to 1.7.0.15 and update embedded resources in project files
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-17 14:10:26 +01:00
CodeDevMLH
1ddaab325e Update CONTRIBUTING.md for clarity and consistency in theme development guidelines 2026-02-17 14:10:19 +01:00
CodeDevMLH
81facbdb00 Update README.md to reference contributing guidelines 2026-02-17 14:10:13 +01:00
CodeDevMLH
34a58ac4bd Add test-site.html for seasonal theme testing interface 2026-02-17 14:10:07 +01:00
CodeDevMLH
2d8444701d Remove test-site.html and test-site-new.html from .gitignore 2026-02-17 14:09:58 +01:00
CodeDevMLH
66f5353659 Add input fields for custom JavaScript and CSS in the control panel 2026-02-17 13:47:43 +01:00
CodeDevMLH
b58264998a Enhance theme loading by tracking animation frames, MutationObservers, and intervals; wrap JS in IIFE for scope isolation
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 37s
2026-02-17 02:01:05 +01:00
CodeDevMLH
76c0bc5b3b Add CONTRIBUTING.md for theme development guidelines and update test site styles
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 36s
2026-02-17 01:58:58 +01:00
CodeDevMLH
1428db3e1e Update manifest.json for release v1.7.0.14 [skip ci] 2026-02-16 23:02:32 +00:00
CodeDevMLH
1f5f436e44 Bump version to 1.7.0.14
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-17 00:01:41 +01:00
CodeDevMLH
46f5c3648d Refactor seasonal rule HTML generation for improved maintainability 2026-02-17 00:01:21 +01:00
CodeDevMLH
555e2ab8be Update manifest.json for release v1.7.0.13 [skip ci] 2026-02-16 22:37:20 +00:00
CodeDevMLH
26eadfc0aa Bump version to 1.7.0.13
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-16 23:36:26 +01:00
CodeDevMLH
142f538939 Enhance select elements in date range group with consistent styling 2026-02-16 23:36:08 +01:00
CodeDevMLH
b64e80fd60 Update manifest.json for release v1.7.0.12 [skip ci] 2026-02-16 22:18:09 +00:00
CodeDevMLH
fbf5fc7edf Bump version to 1.7.0.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 23:17:17 +01:00
CodeDevMLH
8defba4623 Refactor date selection logic in SeasonalsConfigPage to use helper function for generating options 2026-02-16 23:17:02 +01:00
CodeDevMLH
7f968ee050 Update manifest.json for release v1.7.0.11 [skip ci] 2026-02-16 22:06:11 +00:00
CodeDevMLH
dec5bbe39e Bump version to 1.7.0.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 23:05:18 +01:00
CodeDevMLH
63f3211cc4 Refactor date selection options to use SeasonalsConfigPage methods for consistency 2026-02-16 23:05:06 +01:00
CodeDevMLH
4270235c78 Update manifest.json for release v1.7.0.10 [skip ci] 2026-02-16 22:02:53 +00:00
CodeDevMLH
76d8a67914 Bump version to 1.7.0.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 23:02:00 +01:00
CodeDevMLH
1a3caf5da6 Enhance configuration page: replace input fields with dropdowns for date selection and add utility functions for generating options 2026-02-16 23:01:46 +01:00
CodeDevMLH
3b3ef77e61 fix ui [skip ci] 2026-02-16 19:31:42 +01:00
CodeDevMLH
ba580b1b52 Update manifest.json for release v1.7.0.9 [skip ci] 2026-02-16 18:27:34 +00:00
CodeDevMLH
0a6284c716 Bump version to 1.7.0.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-16 19:26:43 +01:00
CodeDevMLH
f83e863664 .. 2026-02-16 19:26:26 +01:00
CodeDevMLH
747e8ed6bc Update manifest.json for release v1.7.0.8 [skip ci] 2026-02-16 18:01:43 +00:00
CodeDevMLH
30845442b2 Bump version to 1.7.0.8
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-16 19:00:51 +01:00
CodeDevMLH
bb83201736 .. 2026-02-16 18:59:28 +01:00
CodeDevMLH
457ae404ba Update manifest.json for release v1.7.0.7 [skip ci] 2026-02-16 17:49:49 +00:00
CodeDevMLH
b6d679f6ef Update version to 1.7.0.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-16 18:48:51 +01:00
CodeDevMLH
3b88a1809d small UI changes 2026-02-16 18:48:07 +01:00
CodeDevMLH
4614ce4a7a Update manifest.json for release v1.7.0.5 [skip ci] 2026-02-16 17:16:03 +00:00
CodeDevMLH
57840bb149 Bump version to 1.7.0.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m5s
2026-02-16 18:14:59 +01:00
CodeDevMLH
dd90a4630a Update layout of seasonal rules in configuration page for improved responsiveness 2026-02-16 18:14:45 +01:00
CodeDevMLH
b5d5e5706e Update manifest.json for release v1.7.0.4 [skip ci] 2026-02-16 16:31:00 +00:00
CodeDevMLH
a4b5cf5b6b Bump version to 1.7.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m3s
2026-02-16 17:29:57 +01:00
CodeDevMLH
353bda10df Enhance configuration page: add section titles and improve input handling for seasonal themes 2026-02-16 17:29:41 +01:00
CodeDevMLH
0e1b91d93c Update manifest.json for release v1.7.0.3 [skip ci] 2026-02-16 15:55:27 +00:00
CodeDevMLH
9363008d07 Bump version to 1.7.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 16:54:37 +01:00
CodeDevMLH
faec7d8941 Refactor configuration page: update button classes, form input names, and tab handling for consistency 2026-02-16 16:54:22 +01:00
CodeDevMLH
7cc70854c4 Update manifest.json for release v1.7.0.2 [skip ci] 2026-02-16 15:14:02 +00:00
CodeDevMLH
9432f7aa86 Bump version to 1.7.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-16 16:13:07 +01:00
CodeDevMLH
4f7243bc74 test 2026-02-16 16:12:52 +01:00
CodeDevMLH
ee724fedc8 Update manifest.json for release v1.7.0.1 [skip ci] 2026-02-16 14:41:02 +00:00
CodeDevMLH
a1dbd4eb12 Bump version to 1.7.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m1s
2026-02-16 15:40:03 +01:00
CodeDevMLH
236d8d9e70 fix tabs 2026-02-16 15:39:42 +01:00
CodeDevMLH
6d55ae7524 Update manifest.json for release v1.7.0.0 [skip ci] 2026-02-16 01:31:56 +00:00
CodeDevMLH
99a0613893 Update version to 1.7.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-16 02:31:06 +01:00
CodeDevMLH
61952a0af7 Add seasonal rules configuration and enhance settings UI 2026-02-16 02:28:47 +01:00
CodeDevMLH
eca6ba96fb Add resurrection theme and enhance seasonal rules parsing logic [skip ci] 2026-02-16 02:28:36 +01:00
CodeDevMLH
c2f0f01689 Add release existence check to automation workflow 2026-02-16 02:28:16 +01:00
CodeDevMLH
30d17baff4 add (komisches) neues PR theme 2026-02-16 02:28:03 +01:00
CodeDevMLH
96bb1a3744 Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 01:12:58 +00:00
CodeDevMLH
772a0dae40 rebuild
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-15 02:12:09 +01:00
CodeDevMLH
40c4454397 Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 01:08:22 +00:00
CodeDevMLH
e5915e715a fix path issue on subpath installations
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-15 02:07:30 +01:00
CodeDevMLH
c171fc15f5 Update changelog for version 1.6.13.5 to include additional improvements [skip ci] 2026-02-04 19:37:34 +01:00
CodeDevMLH
a749b1f98e Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 18:08:31 +00:00
CodeDevMLH
6ccf6201b4 Auto-Update MediaBar Enhanced to v1.4.0.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 18:07:46 +00:00
CodeDevMLH
a69c741a39 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:59:08 +00:00
CodeDevMLH
d54b4f9b07 Auto-Update MediaBar Enhanced to v1.4.0.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 17:58:20 +00:00
CodeDevMLH
2cd427b6e9 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:52:16 +00:00
CodeDevMLH
55c1f8b191 Auto-Update MediaBar Enhanced to v1.4.0.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-02-04 17:51:32 +00:00
CodeDevMLH
fc3d6efd1c Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:40:04 +00:00
CodeDevMLH
5ba5940e5f Auto-Update MediaBar Enhanced to v1.4.0.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 17:39:18 +00:00
CodeDevMLH
621b7da344 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:28:13 +00:00
CodeDevMLH
268ce5e307 Auto-Update MediaBar Enhanced to v1.4.0.8
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 17:27:28 +00:00
CodeDevMLH
412cc2d981 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 17:10:23 +00:00
CodeDevMLH
949df24bdb Auto-Update MediaBar Enhanced to v1.4.0.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 17:09:35 +00:00
CodeDevMLH
b987969200 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:41:16 +00:00
CodeDevMLH
3306bb703d Auto-Update MediaBar Enhanced to v1.4.0.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 16:40:28 +00:00
CodeDevMLH
6587a4e3d0 Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:24:07 +00:00
CodeDevMLH
f794b71f44 Auto-Update MediaBar Enhanced to v1.4.0.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 16:23:20 +00:00
CodeDevMLH
34363c502a Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:18:09 +00:00
CodeDevMLH
add2f7a551 Auto-Update MediaBar Enhanced to v1.4.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 16:17:22 +00:00
CodeDevMLH
1d7e9e27ec Update manifest.json for release v1.6.13.5 [skip ci] 2026-02-04 16:16:56 +00:00
CodeDevMLH
6459653328 Bump version to 1.6.13.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m0s
2026-02-04 17:15:56 +01:00
CodeDevMLH
9d738e6061 Disable dragging and pointer events for seasonal settings button image 2026-02-04 17:15:39 +01:00
CodeDevMLH
8f5a3650e6 Update button image size in SeasonalSettingsManager 2026-02-04 17:13:27 +01:00
CodeDevMLH
229f9fe5ab Update manifest.json for release v1.6.13.4 [skip ci] 2026-02-04 15:53:04 +00:00
CodeDevMLH
0686129590 Auto-Update MediaBar Enhanced to v1.4.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-04 15:52:17 +00:00
CodeDevMLH
cb0392eb0d Update manifest.json for release v1.6.13.4 [skip ci] 2026-02-04 15:51:34 +00:00
CodeDevMLH
ed13e05b82 Bump version to 1.6.13.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-04 16:50:44 +01:00
CodeDevMLH
310fb4d496 Rename SettingsManager to SeasonalSettingsManager and update related log messages 2026-02-04 16:50:26 +01:00
CodeDevMLH
78d25106db Update manifest.json for release v1.6.13.3 [skip ci] 2026-02-04 15:46:34 +00:00
CodeDevMLH
a328171a8a Auto-Update MediaBar Enhanced to v1.4.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 15:45:45 +00:00
CodeDevMLH
361559cbec Update manifest.json for release v1.6.13.3 [skip ci] 2026-02-04 15:22:39 +00:00
CodeDevMLH
e08bf66a53 Bump version to 1.6.13.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-04 16:21:48 +01:00
CodeDevMLH
d6ef81138d typo 2026-02-04 16:21:02 +01:00
CodeDevMLH
35f21e680a Update manifest.json for release v1.6.13.2 [skip ci] 2026-02-04 15:02:42 +00:00
CodeDevMLH
705fbaed9d Auto-Update MediaBar Enhanced to v1.4.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-04 15:01:55 +00:00
CodeDevMLH
9e52198ef7 Update manifest.json for release v1.6.13.2 [skip ci] 2026-02-04 15:01:13 +00:00
CodeDevMLH
b1943dfe17 Bump version to 1.6.13.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-04 16:00:20 +01:00
CodeDevMLH
c55e900c0f Enhance logging messages in SeasonalsManager for better clarity 2026-02-04 14:05:41 +01:00
CodeDevMLH
503e9addee Update manifest.json for release v1.6.13.1 [skip ci] 2026-02-04 12:50:35 +00:00
CodeDevMLH
d630fdd217 Auto-Update MediaBar Enhanced to v1.4.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-04 12:49:50 +00:00
CodeDevMLH
7e4a7c2a6e Update manifest.json for release v1.6.13.1 [skip ci] 2026-02-04 12:47:05 +00:00
CodeDevMLH
1716a771f3 Auto-Update MediaBar Enhanced to v1.4.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-02-04 12:46:21 +00:00
CodeDevMLH
36347cc4b0 Update manifest.json for release v1.6.13.1 [skip ci] 2026-02-04 12:39:14 +00:00
CodeDevMLH
7f94164e55 Update version to 1.6.13.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 13:38:23 +01:00
CodeDevMLH
cbab7de546 Refactor seasonals.js 2026-02-04 13:37:49 +01:00
CodeDevMLH
d0de5cd021 Update seasonals settings to use namespaced localStorage keys and enhance field description for client-side toggle 2026-02-04 12:40:50 +01:00
CodeDevMLH
16628e9902 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 01:42:27 +00:00
CodeDevMLH
72bfe0a14a Auto-Update MediaBar Enhanced to v1.3.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 01:41:42 +00:00
CodeDevMLH
6498ec4216 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 01:28:50 +00:00
CodeDevMLH
0d350fc76b Auto-Update MediaBar Enhanced to v1.3.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-04 01:28:02 +00:00
CodeDevMLH
2c6e4ce610 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 01:15:12 +00:00
CodeDevMLH
0c552774dc Auto-Update MediaBar Enhanced to v1.3.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 01:14:26 +00:00
CodeDevMLH
9ab605bb74 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-04 00:08:09 +00:00
CodeDevMLH
3d6cba0fe4 Auto-Update MediaBar Enhanced to v1.3.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-04 00:07:22 +00:00
CodeDevMLH
32e5e2b690 Update manifest.json for release v1.6.13.0 [skip ci] 2026-02-03 23:12:29 +00:00
CodeDevMLH
c967c1e308 Bump version to 1.6.13.0 and update changelog for new features and fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-04 00:11:38 +01:00
CodeDevMLH
ae28d5219b Enhance README.md with additional details and images for configuration UI and user toggle features 2026-02-03 23:47:58 +01:00
CodeDevMLH
e4228f889e Add guidance for safely resetting 'dev' branch to 'main' in Git [skip ci] 2026-02-03 23:22:33 +01:00
CodeDevMLH
6d721c755e Update README.md to enhance documentation and add new seasonal themes 2026-02-03 23:06:47 +01:00
CodeDevMLH
6948953778 Add optional stash commands to Git rebase instructions 2026-02-03 23:04:58 +01:00
CodeDevMLH
8a50cef330 Update Git rebase instructions to include fetching from origin [skip ci] 2026-02-03 22:37:20 +01:00
CodeDevMLH
a0bf5370bd Update manifest.json for release v1.6.12.0 [skip ci] 2026-02-03 21:28:05 +00:00
CodeDevMLH
c5800b431d Bump version to 1.6.12.0 and update disabled options in configuration page
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-03 22:27:15 +01:00
CodeDevMLH
9a4056651d Update manifest.json for release v1.6.11.0 [skip ci] 2026-02-03 21:10:34 +00:00
CodeDevMLH
87382db78e Bump version to 1.6.11.0 and update configuration styles in the settings page
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-03 22:09:43 +01:00
CodeDevMLH
5d9afa762f Update manifest.json for release v1.6.10.0 [skip ci] 2026-02-03 21:03:41 +00:00
CodeDevMLH
2f88587dab Bump version to 1.6.10.0 and update logo source in settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 22:02:50 +01:00
CodeDevMLH
360a959b69 rename logo [skip ci] 2026-02-03 22:02:01 +01:00
CodeDevMLH
36fba545cf Update manifest.json for release v1.6.9.0 [skip ci] 2026-02-03 21:01:40 +00:00
CodeDevMLH
7faa2cc766 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-02-03 22:00:43 +01:00
CodeDevMLH
aa832e93aa add logo [skip ci] 2026-02-03 22:00:32 +01:00
CodeDevMLH
86bbeb583d Update manifest.json for release v1.6.9.0 [skip ci] 2026-02-03 20:59:03 +00:00
CodeDevMLH
7a642b34b8 add assets [skip CI]
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 21:58:01 +01:00
CodeDevMLH
926b30b8ce Update manifest.json for release v1.6.9.0 [skip ci] 2026-02-03 20:30:56 +00:00
CodeDevMLH
5b672cef42 Bump version to 1.6.9.0; update logo asset path and manifest for new release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 21:30:03 +01:00
CodeDevMLH
ceccbf4ded Update manifest.json for release v1.6.8.0 [skip ci] 2026-02-03 20:24:42 +00:00
CodeDevMLH
9cba2a0755 Bump version to 1.6.8.0; update manifest and add new logo assets
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m6s
2026-02-03 21:23:36 +01:00
CodeDevMLH
af036a9aa4 Update manifest.json for release v1.6.7.0 [skip ci] 2026-02-03 19:59:06 +00:00
CodeDevMLH
cfefd2d2d3 Bump version to 1.6.7.0; update manifest and configuration files for improved seasonal settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 20:58:14 +01:00
CodeDevMLH
8e7299508b Update manifest.json for release v1.6.6.0 [skip ci] 2026-02-03 19:51:20 +00:00
CodeDevMLH
fc7aa36f41 Bump version to 1.6.6.0; update manifest and settings popup for improved theme selection
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 20:50:28 +01:00
CodeDevMLH
fc9896048f Update manifest.json for release v1.6.5.0 [skip ci] 2026-02-03 19:37:37 +00:00
CodeDevMLH
572c4d9ace Bump version to 1.6.5.0 and update manifest; add new select options in settings popup
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-03 20:36:45 +01:00
CodeDevMLH
2572e085f6 Update manifest.json for release v1.6.4.0 [skip ci] 2026-02-03 18:46:36 +00:00
CodeDevMLH
8297f989fd Bump version to 1.6.4.0 and update changelog for new features; modify select classes in config page and settings popup
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-03 19:45:38 +01:00
CodeDevMLH
636aaa2a4a Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-03 18:33:25 +00:00
CodeDevMLH
5e70621e93 Update version to 1.6.3.0, modify checkbox layout, and adjust popup styles
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 19:32:34 +01:00
CodeDevMLH
0b4434c51c Update manifest.json for release v1.6.2.0 [skip ci] 2026-02-03 18:18:35 +00:00
CodeDevMLH
dd6583c055 Bump version to 1.6.2.0 and update changelog for new features
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 19:17:44 +01:00
CodeDevMLH
a0b0514159 Update manifest.json for release v1.6.1.0 [skip ci] 2026-02-03 18:08:59 +00:00
CodeDevMLH
e977c83e8f Bump version to 1.6.1.0 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 19:08:07 +01:00
CodeDevMLH
e281f5c579 test other ui design 2026-02-03 19:08:02 +01:00
CodeDevMLH
e6b646e478 Update .gitignore to include test-site-new.html [skip CI] 2026-02-03 18:50:22 +01:00
CodeDevMLH
8d6bc12fa4 Update manifest.json for release v1.6.0.0 [skip ci] 2026-02-03 17:49:06 +00:00
CodeDevMLH
f036e748da Add client-side toggle option for seasonal settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-03 18:48:11 +01:00
CodeDevMLH
0177a7caea Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 22:40:50 +00:00
CodeDevMLH
c45e9f6156 Auto-Update MediaBar Enhanced to v1.2.3.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 22:40:00 +00:00
CodeDevMLH
8dc9b9f157 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 21:32:51 +00:00
CodeDevMLH
e73c6c14bb Auto-Update MediaBar Enhanced to v1.2.3.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 21:32:01 +00:00
CodeDevMLH
aa1c60f9ce Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 20:22:17 +00:00
CodeDevMLH
d1e668bcff Auto-Update MediaBar Enhanced to v1.2.3.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 20:21:27 +00:00
CodeDevMLH
0454d43f32 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 01:10:34 +00:00
CodeDevMLH
61b3b51139 Auto-Update MediaBar Enhanced to v1.2.3.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 01:09:45 +00:00
CodeDevMLH
60d1a546a2 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 01:06:40 +00:00
CodeDevMLH
669933d270 Auto-Update MediaBar Enhanced to v1.2.3.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 01:05:53 +00:00
CodeDevMLH
053f0ccfa7 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:46:16 +00:00
CodeDevMLH
60e998fc7f Auto-Update MediaBar Enhanced to v1.2.3.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 00:45:27 +00:00
CodeDevMLH
3aa631198a Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:31:33 +00:00
CodeDevMLH
b1876d655e Auto-Update MediaBar Enhanced to v1.2.3.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 00:30:43 +00:00
CodeDevMLH
9a9f89c1fc Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:18:24 +00:00
CodeDevMLH
2fb41f6442 Auto-Update MediaBar Enhanced to v1.2.3.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 00:17:35 +00:00
CodeDevMLH
4ab949e6d7 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-27 23:55:34 +00:00
CodeDevMLH
7d1024c917 Auto-Update MediaBar Enhanced to v1.2.3.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-27 23:54:49 +00:00
CodeDevMLH
c2e3d55110 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-24 22:54:52 +00:00
CodeDevMLH
c6503a90bb Auto-Update MediaBar Enhanced to v1.2.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-01-24 22:54:01 +00:00
CodeDevMLH
b70787d5ec Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-22 23:51:52 +00:00
CodeDevMLH
a9eb8113a6 Auto-Update MediaBar Enhanced to v1.2.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-22 23:51:03 +00:00
CodeDevMLH
aaf15d8934 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-21 23:12:11 +00:00
CodeDevMLH
ec298ebde0 bump version to 1.5.1.0 in project file
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-01-22 00:11:19 +01:00
CodeDevMLH
bb108d0f49 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-21 22:53:15 +00:00
MLH
f271e1715d Merge pull request 'fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes' (#1) from dev into main
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 57s
Reviewed-on: #1
2026-01-21 23:52:18 +01:00
CodeDevMLH
bd0e2779e5 fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes
All checks were successful
🏗️ Build Plugin / build (push) Successful in 53s
🏗️ Build Plugin / build (pull_request) Successful in 58s
2026-01-21 23:51:23 +01:00
CodeDevMLH
53a1682868 Enhance descriptions and overviews in manifest.json for Seasonals and Media Bar Enhanced plugins [skip ci] 2026-01-09 00:32:40 +01:00
CodeDevMLH
a7df2fd832 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 23:31:54 +00:00
CodeDevMLH
c56cde860b Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-08 23:31:04 +00:00
CodeDevMLH
59211e27c6 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 23:15:57 +00:00
CodeDevMLH
a2b1179353 Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-01-08 23:15:12 +00:00
CodeDevMLH
c7f34ec92f Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 22:16:58 +00:00
CodeDevMLH
4c011cf560 Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-01-08 22:16:11 +00:00
CodeDevMLH
e5f78c711d Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 15:27:46 +00:00
CodeDevMLH
98a536315b Auto-Update MediaBar Enhanced to v1.1.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 15:27:00 +00:00
CodeDevMLH
01343848e3 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 14:55:53 +00:00
CodeDevMLH
113e7dd0f7 Auto-Update MediaBar Enhanced to v1.1.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 14:55:04 +00:00
CodeDevMLH
1bc4176771 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 02:16:44 +00:00
CodeDevMLH
b091e5592d Auto-Update MediaBar Enhanced to v1.1.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 02:15:57 +00:00
CodeDevMLH
cb2d86340e Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:34:26 +00:00
CodeDevMLH
57a92e94de Remove build.yaml configuration file for Seasonals plugin
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 57s
2026-01-07 00:33:36 +01:00
CodeDevMLH
30bc8bef39 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:27:26 +00:00
CodeDevMLH
2d237063a3 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-01-06 23:26:36 +00:00
CodeDevMLH
049a5075b5 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:18:28 +00:00
CodeDevMLH
cb2e9c4b07 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:17:39 +00:00
CodeDevMLH
5a39f85082 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:13:12 +00:00
CodeDevMLH
b863d201b9 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:12:24 +00:00
CodeDevMLH
78b50b41c2 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:11:24 +00:00
CodeDevMLH
0ba1545fd6 test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-01-07 00:10:33 +01:00
CodeDevMLH
16c4e0f29b fix link 2026-01-07 00:09:41 +01:00
CodeDevMLH
3581b8cbb2 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:57:27 +00:00
CodeDevMLH
7184c93f6f Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-06 22:56:39 +00:00
CodeDevMLH
0969f0238f Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:45:32 +00:00
CodeDevMLH
12f868d3f9 test ..
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-06 23:44:42 +01:00
CodeDevMLH
dc7be56807 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2026-01-06 23:41:57 +01:00
CodeDevMLH
cb60690e6b fix log name 2026-01-06 23:41:55 +01:00
CodeDevMLH
e0397bb2e8 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:40:15 +00:00
CodeDevMLH
822bcafd11 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-06 22:39:29 +00:00
CodeDevMLH
429d96c816 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:37:52 +00:00
CodeDevMLH
e948055f0f test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:37:01 +01:00
CodeDevMLH
5129d46163 fix remote repo 2026-01-06 23:36:39 +01:00
CodeDevMLH
660f7142ef Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:33:07 +00:00
CodeDevMLH
9f657588d8 fix changelog
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-06 23:32:16 +01:00
CodeDevMLH
0457f1a764 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2026-01-06 23:30:37 +01:00
CodeDevMLH
f196c6c296 fix url 2026-01-06 23:30:36 +01:00
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
76 changed files with 6592 additions and 2058 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,56 @@ public class EasterOptions
public int MinBunnyRestTime { get; set; } = 2000; public int MinBunnyRestTime { get; set; } = 2000;
public int MaxBunnyRestTime { get; set; } = 5000; public int MaxBunnyRestTime { get; set; } = 5000;
} }
public class ResurrectionOptions
{
public int SymbolCount { get; set; } = 12;
public bool EnableResurrection { get; set; } = true;
public bool EnableRandomSymbols { get; set; } = true;
public bool EnableRandomSymbolsMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SpringOptions
{
public int PollenCount { get; set; } = 30;
public int SunbeamCount { get; set; } = 5;
public int BirdCount { get; set; } = 4;
public int ButterflyCount { get; set; } = 4;
public int BeeCount { get; set; } = 2;
public int LadybugCount { get; set; } = 2;
public bool EnableSpring { get; set; } = true;
public bool EnableSpringSunbeams { get; set; } = true;
public bool EnableRandomSpring { get; set; } = true;
public bool EnableRandomSpringMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class SummerOptions
{
public int BubbleCount { get; set; } = 20;
public int DustCount { get; set; } = 50;
public bool EnableSummer { get; set; } = true;
public bool EnableRandomSummer { get; set; } = true;
public bool EnableRandomSummerMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}
public class CarnivalOptions
{
public int ObjectCount { get; set; } = 25;
public bool EnableCarnival { get; set; } = true;
public bool EnableRandomCarnival { get; set; } = true;
public bool EnableRandomCarnivalMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
public bool EnableCarnivalSway { get; set; } = true;
}
public class CherryBlossomOptions
{
public int PetalCount { get; set; } = 25;
public bool EnableCherryBlossom { get; set; } = true;
public bool EnableRandomCherryBlossom { get; set; } = true;
public bool EnableRandomCherryBlossomMobile { get; set; } = false;
public bool EnableDifferentDuration { get; set; } = true;
}

View File

@@ -1,63 +1,118 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Template</title> <title>Seasonals Configuration</title>
</head> </head>
<body> <body>
<div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox"> <div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<div class="sectionTitleContainer"> <div class="sectionTitleContainer">
<h2 class="sectionTitle">Seasonals</h2> <h2 class="sectionTitle">Seasonals Configuration</h2>
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;" <a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals"> target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals">
<i class="md-icon button-icon button-icon-left secondaryText"></i> <i class="md-icon button-icon button-icon-left secondaryText"></i>
<span>Help</span> <span>${Help}</span>
</a> </a>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;"> <hr style="max-width: 800px; margin: 1em 0;">
<br>
<div style="margin-bottom: 1.5em;">
<button class="seasonals-tab-button active" onclick="showSeasonalsTab('seasonals-basic', this)"
style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid #00a4dc;">
<h3>General Settings</h3>
</button>
<button class="seasonals-tab-button" onclick="showSeasonalsTab('seasonals-auto-selection', this)"
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
<h3>Auto Selection</h3>
</button>
<button class="seasonals-tab-button" onclick="showSeasonalsTab('seasonals-advanced', this)"
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
<h3>Advanced Settings</h3>
</button>
</div>
<form id="SeasonalsConfigForm"> <form id="SeasonalsConfigForm">
<div class="checkboxContainer checkboxContainer-withDescription"> <!-- BASIC Tab -->
<label class="emby-checkbox-label"> <div id="seasonals-basic" class="seasonals-tab-content">
<input id="AutomateSeasonSelection" name="AutomateSeasonSelection" type="checkbox" is="emby-checkbox" /> <h2 class="sectionTitle">Main Plugin Settings</h2>
<span>Automate Season Selection</span> <div class="checkboxContainer checkboxContainer-withDescription">
</label> <label class="emby-checkbox-label">
<div class="fieldDescription">Automatically select the season based on the date.</div> <input id="SeasonalsIsEnabled" name="SeasonalsIsEnabled" type="checkbox" is="emby-checkbox" />
<span>Enable Seasonals</span>
</label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SeasonalsAutomateSeasonSelection" name="SeasonalsAutomateSeasonSelection" type="checkbox" is="emby-checkbox" />
<span>Automate Season Selection</span>
</label>
<div class="fieldDescription">If enabled, the plugin will use the rules defined in the "Auto Selection" tab to assume the season. If no rule matches, it falls back to the "Standard Season" below.</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="SeasonalsSelectedSeason">Standard Season</label>
<select is="emby-select" id="SeasonalsSelectedSeason" name="SeasonalsSelectedSeason" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="none">None</option>
<option value="snowflakes">Snowflakes</option>
<option value="snowfall">Snowfall</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 (flying santa & snowfall)</option>
<option value="autumn">Autumn (falling leaves)</option>
<option value="easter">Easter</option>
<option value="resurrection">Resurrection - by Bioflash257</option>
<option value="summer">Summer (Bubbles)</option>
<option value="spring">Spring</option>
<option value="carnival">Carnival (Confetti)</option>
<option value="championships" disabled>European/World Championships (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="patrick" disabled>St. Patrick's Day (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Pride (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Oscar Awards (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Eurovison Awards (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="pride" disabled>Sugar Feast (Eid al-Fitr, Ramadan) (not implemented yet. Please commit ideas in a issue or PR)</option>
</select>
<div class="fieldDescription">The season to display if automation is disabled or no "Auto Selection" rule matches the current date.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="SeasonalsEnableClientSideToggle" name="SeasonalsEnableClientSideToggle" type="checkbox" is="emby-checkbox" />
<span>Allow Client-Side Toggle</span>
</label>
<div class="fieldDescription">If enabled, users will see a seasonals icon in the header to toggle seasonals for their browser (device-specific).</div>
</div>
</div> </div>
<div class="selectContainer">
<label class="selectLabel" for="SelectedSeason">Selected Season</label>
<select is="emby-select" id="SelectedSeason" name="SelectedSeason" class="emby-select-withcolor emby-select">
<option value="none">None</option>
<option value="snowflakes">Snowflakes</option>
<option value="snowfall">Snowfall</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="summer" disabled>Summer (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="spring" disabled>Spring (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="oktoberfest" disabled>Oktoberfest (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="carnival" disabled>Carnival (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="championships" disabled>European/World Championships (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="patrick" disabled>St. Patrick's Day (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
<option value="pride" disabled>Pride (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
</select>
<div class="fieldDescription">The season to display if automation is disabled.</div>
</div>
<br>
<details> <!-- Auto Selection Tab -->
<summary>Advanced Configuration</summary> <div id="seasonals-auto-selection" class="seasonals-tab-content" style="display: none;">
<p>Configure specific settings for each seasonal theme below.</p> <h2>Auto Selection Rules</h2>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>Define rules to automatically select a season based on the date. Rules are evaluated from top to bottom. The first matching rule wins.</div>
</div>
<div id="seasonalRulesList">
<!-- Rules will be injected here via JS -->
</div>
<button is="emby-button" type="button" class="raised emby-button" onclick="SeasonalsConfigPage.addRule();"
style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
<i class="material-icons" style="font-size: 24px;">add</i>
<span>Add Rule</span>
</button>
</div>
<!-- Advanced Tab -->
<div id="seasonals-advanced" class="seasonals-tab-content" style="display: none;">
<h2 class="sectionTitle">Configure specific settings for each seasonal theme</h2>
<!-- <p>Configure specific settings for each seasonal theme below.</p> -->
<p>All symbol count settings add this number in addition to the standard 12 symbols (if additional symbols is enabled).</p> <p>All symbol count settings add this number in addition to the standard 12 symbols (if additional symbols is enabled).</p>
<details> <details>
<summary>Autumn</summary> <summary>Autumn</summary>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
@@ -485,7 +540,241 @@
<div class="fieldDescription">Maximum time the bunny waits before appearing again.</div> <div class="fieldDescription">Maximum time the bunny waits before appearing again.</div>
</div> </div>
</details> </details>
</details> <hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Resurrection</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableResurrection" name="EnableResurrection" type="checkbox" is="emby-checkbox" />
<span>Enable Resurrection Seasonal</span>
</label>
<div class="fieldDescription">Enable the Resurrection theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomResurrection" name="EnableRandomResurrection" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Symbols</span>
</label>
<div class="fieldDescription">Displays additional symbols randomly distributed across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomResurrectionMobile" name="EnableRandomResurrectionMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Symbols on Mobile</span>
</label>
<div class="fieldDescription">Displays additional symbols randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="ResurrectionSymbolCount">Symbol Count</label>
<input is="emby-input" type="number" id="ResurrectionSymbolCount" name="ResurrectionSymbolCount" />
<div class="fieldDescription">Number of additional symbols (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationResurrection" name="EnableDifferentDurationResurrection" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the movement speed.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Spring</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSpring" name="EnableSpring" type="checkbox" is="emby-checkbox" />
<span class="checkboxLabel">Enable Spring</span>
</label>
<div class="fieldDescription">Enables the Spring theme (grass, pollen).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomSpring" name="EnableRandomSpring" type="checkbox" is="emby-checkbox" />
<span>Enable Animation Assets</span>
</label>
<div class="fieldDescription">Enables animated spring assets (birds, butterflies, bees, etc.).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomSpringMobile" name="EnableRandomSpringMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Animation Assets on Mobile</span>
</label>
<div class="fieldDescription">Displays animated assets on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpringPollenCount">Pollen Count</label>
<input is="emby-input" type="number" id="SpringPollenCount" name="SpringPollenCount" />
<div class="fieldDescription">Number of pollen particles (if enabled).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpringSunbeamCount">Sunbeam Count</label>
<input is="emby-input" type="number" id="SpringSunbeamCount" name="SpringSunbeamCount" />
<div class="fieldDescription">Number of sunbeams (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSpringSunbeams" name="EnableSpringSunbeams" type="checkbox" is="emby-checkbox" />
<span>Enable Sunbeams</span>
</label>
<div class="fieldDescription">Display sunbeams at the top of the screen.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpringBirdCount">Bird Count</label>
<input is="emby-input" type="number" id="SpringBirdCount" name="SpringBirdCount" />
<div class="fieldDescription">Number of birds flying across the screen.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpringButterflyCount">Butterfly Count</label>
<input is="emby-input" type="number" id="SpringButterflyCount" name="SpringButterflyCount" />
<div class="fieldDescription">Number of butterflies.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpringBeeCount">Bee Count</label>
<input is="emby-input" type="number" id="SpringBeeCount" name="SpringBeeCount" />
<div class="fieldDescription">Number of bees.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SpringLadybugCount">Ladybug Count</label>
<input is="emby-input" type="number" id="SpringLadybugCount" name="SpringLadybugCount" />
<div class="fieldDescription">Number of ladybugs walking along the bottom.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationSpring" name="EnableDifferentDurationSpring" type="checkbox" is="emby-checkbox" />
<span>Enable Different Duration</span>
</label>
<div class="fieldDescription">Randomize the animations duration.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Summer</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableSummer" name="EnableSummer" type="checkbox" is="emby-checkbox" />
<span>Enable Summer Seasonal</span>
</label>
<div class="fieldDescription">Enable the Summer theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomSummer" name="EnableRandomSummer" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Bubbles and Dust</span>
</label>
<div class="fieldDescription">Displays additional bubbles and dust particles rising across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomSummerMobile" name="EnableRandomSummerMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Bubbles and Dust on Mobile</span>
</label>
<div class="fieldDescription">Displays additional bubbles and dust particles rising across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SummerBubbleCount">Bubbles Count</label>
<input is="emby-input" type="number" id="SummerBubbleCount" name="SummerBubbleCount" />
<div class="fieldDescription">Number of bubbles (if enabled).</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="SummerDustCount">Dust Count</label>
<input is="emby-input" type="number" id="SummerDustCount" name="SummerDustCount" />
<div class="fieldDescription">Number of dust particles (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationSummer" name="EnableDifferentDurationSummer" type="checkbox" is="emby-checkbox" />
<span>Enable Different Rising Speed</span>
</label>
<div class="fieldDescription">Randomize the rising speed of bubbles.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Carnival</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableCarnival" name="EnableCarnival" type="checkbox" is="emby-checkbox" />
<span>Enable Carnival Seasonal</span>
</label>
<div class="fieldDescription">Enable the Carnival theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomCarnival" name="EnableRandomCarnival" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Confetti</span>
</label>
<div class="fieldDescription">Displays additional confetti falling and fluttering across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomCarnivalMobile" name="EnableRandomCarnivalMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Confetti on Mobile</span>
</label>
<div class="fieldDescription">Displays additional confetti falling and fluttering across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="CarnivalObjectCount">Confetti Count</label>
<input is="emby-input" type="number" id="CarnivalObjectCount" name="CarnivalObjectCount" />
<div class="fieldDescription">Number of additional confetti pieces (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationCarnival" name="EnableDifferentDurationCarnival" type="checkbox" is="emby-checkbox" />
<span>Enable Different Falling Speed</span>
</label>
<div class="fieldDescription">Randomize the falling speed of confetti.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableCarnivalSway" name="EnableCarnivalSway" type="checkbox" is="emby-checkbox" />
<span>Enable Sway</span>
</label>
<div class="fieldDescription">Enable sway animation for carnival confetti.</div>
</div>
</details>
<hr style="max-width: 800px; margin: 1em 0;">
<details>
<summary>Cherry Blossom</summary>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableCherryBlossom" name="EnableCherryBlossom" type="checkbox" is="emby-checkbox" />
<span>Enable Cherry Blossom Seasonal</span>
</label>
<div class="fieldDescription">Enable the Cherry Blossom theme in general (e.g. for automation).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomCherryBlossom" name="EnableRandomCherryBlossom" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Cherry Blossoms</span>
</label>
<div class="fieldDescription">Displays additional cherry blossoms falling and fluttering across the screen.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableRandomCherryBlossomMobile" name="EnableRandomCherryBlossomMobile" type="checkbox" is="emby-checkbox" />
<span>Enable Additional Random Cherry Blossoms on Mobile</span>
</label>
<div class="fieldDescription">Displays additional cherry blossoms falling and fluttering across the screen on mobile devices. Warning: High values may affect performance.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="CherryBlossomPetalCount">Petal Count</label>
<input is="emby-input" type="number" id="CherryBlossomPetalCount" name="CherryBlossomPetalCount" />
<div class="fieldDescription">Number of additional cherry blossoms (if enabled).</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableDifferentDurationCherryBlossom" name="EnableDifferentDurationCherryBlossom" type="checkbox" is="emby-checkbox" />
<span>Enable Different Falling Speed</span>
</label>
<div class="fieldDescription">Randomize the falling speed of cherry blossoms.</div>
</div>
</details>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;"> <div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i> <i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
@@ -506,17 +795,248 @@
</form> </form>
</div> </div>
</div> </div>
<script type="text/javascript"> <style>
var SeasonalsConfig = { /* Styles for the Seasonal Rules List (Auto Selection Tab) */
pluginUniqueId: 'ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4' .seasonal-rule {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 10px;
border: 1px solid rgba(255,255,255,0.05);
}
.seasonal-rule-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.seasonal-rule-content {
display: grid;
grid-template-columns: 2fr 1.3fr 1.3fr 2fr;
gap: 15px;
align-items: end;
}
.rule-actions {
display: flex;
gap: 5px;
}
.date-range-group {
display: flex;
gap: 10px;
align-items: flex-end;
justify-content: space-between;
}
.date-range-group > .inputContainer,
.date-range-group > .selectContainer {
flex: 1;
}
@media (max-width: 800px) {
.seasonal-rule-content {
grid-template-columns: 1fr;
}
}
/* Styles for the Tabs */
.seasonals-tab-button.active {
color: #fff !important;
border-bottom: 2px solid #00a4dc !important;
}
/* Disabled options in selects */
select option:disabled {
color: #a3a3a3 !important;
}
</style>
<script>
function showSeasonalsTab(tabId, btn) {
document.querySelectorAll('.seasonals-tab-content').forEach(el => el.style.display = 'none');
document.getElementById(tabId).style.display = 'block';
document.querySelectorAll('.seasonals-tab-button').forEach(b => {
b.classList.remove('active');
b.style.color = '#ccc';
b.style.borderBottom = '2px solid transparent';
});
if (btn) {
btn.classList.add('active');
btn.style.color = '#fff';
btn.style.borderBottom = '2px solid #00a4dc';
}
}
var SeasonalsConfigPage = {
pluginUniqueId: 'ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4',
addRule: function(data = null) {
var container = document.querySelector('#seasonalRulesList');
var div = document.createElement('div');
div.className = 'seasonal-rule';
var name = data ? (data.Name || data.name || 'New Rule') : 'New Rule';
var startDay = data ? (data.StartDay !== undefined ? data.StartDay : (data.startDay !== undefined ? data.startDay : 1)) : 1;
var startMonth = data ? (data.StartMonth !== undefined ? data.StartMonth : (data.startMonth !== undefined ? data.startMonth : 1)) : 1;
var endDay = data ? (data.EndDay !== undefined ? data.EndDay : (data.endDay !== undefined ? data.endDay : 1)) : 1;
var endMonth = data ? (data.EndMonth !== undefined ? data.EndMonth : (data.endMonth !== undefined ? data.endMonth : 1)) : 1;
var theme = data ? (data.Theme || data.theme || 'none') : 'none';
var days = [];
for (var i = 1; i <= 31; i++) days.push(i);
var months = [
{ v: 1, n: 'Jan' }, { v: 2, n: 'Feb' }, { v: 3, n: 'Mar' }, { v: 4, n: 'Apr' },
{ v: 5, n: 'May' }, { v: 6, n: 'Jun' }, { v: 7, n: 'Jul' }, { v: 8, n: 'Aug' },
{ v: 9, n: 'Sep' }, { v: 10, n: 'Oct' }, { v: 11, n: 'Nov' }, { v: 12, n: 'Dec' }
];
// Build select HTML via string concatenation to avoid Jellyfin's ${} localization processing
function mkSelect(val, opts, cls) {
var h = '<select class="emby-select emby-select-withcolor ' + cls + '" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">';
opts.forEach(function(o) {
var v = o.v || o;
var n = o.n || o;
h += '<option value="' + v + '" ' + (v == val ? 'selected' : '') + '>' + n + '</option>';
});
h += '</select>';
return h;
}
div.innerHTML =
'<div class="seasonal-rule-header">' +
' <div style="font-weight: bold; font-size: 1.1em;" class="rule-title"></div>' +
' <div class="rule-actions">' +
' <button type="button" is="paper-icon-button-light" class="btn-move-up" title="Move Up"><i class="material-icons">arrow_upward</i></button>' +
' <button type="button" is="paper-icon-button-light" class="btn-move-down" title="Move Down"><i class="material-icons">arrow_downward</i></button>' +
' <button type="button" is="paper-icon-button-light" class="btn-remove" title="Remove"><i class="material-icons">delete</i></button>' +
' </div>' +
'</div>' +
'<div class="seasonal-rule-content">' +
' <div class="inputContainer" style="margin:0;">' +
' <label class="inputLabel">Name</label>' +
' <input is="emby-input" class="rule-name" onchange="SeasonalsConfigPage.updateRuleTitles();" />' +
' </div>' +
' <div class="date-range-group">' +
' <div class="selectContainer" style="margin:0; flex: 1;">' +
' <label class="selectLabel">Start Day</label>' +
mkSelect(startDay, days, 'rule-start-day') +
' </div>' +
' <div class="selectContainer" style="margin:0; flex: 1;">' +
' <label class="selectLabel">Month</label>' +
mkSelect(startMonth, months, 'rule-start-month') +
' </div>' +
' </div>' +
' <div class="date-range-group">' +
' <div class="selectContainer" style="margin:0; flex: 1;">' +
' <label class="selectLabel">End Day</label>' +
mkSelect(endDay, days, 'rule-end-day') +
' </div>' +
' <div class="selectContainer" style="margin:0; flex: 1;">' +
' <label class="selectLabel">Month</label>' +
mkSelect(endMonth, months, 'rule-end-month') +
' </div>' +
' </div>' +
' <div class="selectContainer" style="margin:0;">' +
' <label class="selectLabel">Theme</label>' +
' <select class="emby-select emby-select-withcolor rule-theme" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">' +
' <option value="none">None</option>' +
' <option value="snowflakes">Snowflakes</option>' +
' <option value="snowfall">Snowfall</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="cherryblossom">Cherry Blossom</option>' +
' <option value="summer">Summer</option>' +
' <option value="carnival">Carnival</option>' +
' </select>' +
' </div>' +
'</div>';
container.appendChild(div);
// Set values programmatically
div.querySelector('.rule-name').value = name;
div.querySelector('.rule-theme').value = theme;
// Bind events
div.querySelector('.btn-remove').addEventListener('click', function() {
div.remove();
SeasonalsConfigPage.updateRuleTitles();
});
div.querySelector('.btn-move-up').addEventListener('click', function() {
if (div.previousElementSibling) {
container.insertBefore(div, div.previousElementSibling);
SeasonalsConfigPage.updateRuleTitles();
}
});
div.querySelector('.btn-move-down').addEventListener('click', function() {
if (div.nextElementSibling) {
container.insertBefore(div.nextElementSibling, div);
SeasonalsConfigPage.updateRuleTitles();
}
});
this.updateRuleTitles();
},
updateRuleTitles: function() {
var rules = document.querySelectorAll('.seasonal-rule');
rules.forEach((rule, index) => {
var name = rule.querySelector('.rule-name').value;
rule.querySelector('.rule-title').innerText = '#' + (index + 1) + ' ' + name;
});
},
renderRules: function(rules) {
var container = document.querySelector('#seasonalRulesList');
container.innerHTML = '';
if (Array.isArray(rules)) {
rules.forEach(rule => this.addRule(rule));
}
},
getRulesFromUI: function() {
var rules = [];
document.querySelectorAll('.seasonal-rule').forEach(div => {
rules.push({
Name: div.querySelector('.rule-name').value,
StartDay: parseInt(div.querySelector('.rule-start-day').value),
StartMonth: parseInt(div.querySelector('.rule-start-month').value),
EndDay: parseInt(div.querySelector('.rule-end-day').value),
EndMonth: parseInt(div.querySelector('.rule-end-month').value),
Theme: div.querySelector('.rule-theme').value
});
});
return rules;
}
}; };
document.querySelector('#SeasonalsConfigPage') document.querySelector('#SeasonalsConfigPage')
.addEventListener('pageshow', function() { .addEventListener('pageshow', function() {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SeasonalsConfigPage.pluginUniqueId).then(function (config) {
document.querySelector('#SelectedSeason').value = config.SelectedSeason; document.querySelector('#SeasonalsIsEnabled').checked = config.IsEnabled;
document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection; document.querySelector('#SeasonalsSelectedSeason').value = config.SelectedSeason;
document.querySelector('#SeasonalsAutomateSeasonSelection').checked = config.AutomateSeasonSelection;
document.querySelector('#SeasonalsEnableClientSideToggle').checked = config.EnableClientSideToggle !== undefined ? config.EnableClientSideToggle : true;
// Load Rules
try {
var rules = JSON.parse(config.SeasonalRules || "[]");
SeasonalsConfigPage.renderRules(rules);
} catch (e) {
console.error("Failed to parse SeasonalRules", e);
}
// Advanced Config // Advanced Config
// Autumn // Autumn
@@ -602,105 +1122,196 @@
document.querySelector('#MinBunnyRestTime').value = config.Easter.MinBunnyRestTime; document.querySelector('#MinBunnyRestTime').value = config.Easter.MinBunnyRestTime;
document.querySelector('#MaxBunnyRestTime').value = config.Easter.MaxBunnyRestTime; document.querySelector('#MaxBunnyRestTime').value = config.Easter.MaxBunnyRestTime;
// Resurrection
document.querySelector('#EnableResurrection').checked = config.Resurrection.EnableResurrection;
document.querySelector('#ResurrectionSymbolCount').value = config.Resurrection.SymbolCount;
document.querySelector('#EnableRandomResurrection').checked = config.Resurrection.EnableRandomSymbols;
document.querySelector('#EnableRandomResurrectionMobile').checked = config.Resurrection.EnableRandomSymbolsMobile;
document.querySelector('#EnableDifferentDurationResurrection').checked = config.Resurrection.EnableDifferentDuration;
// Spring
document.querySelector('#EnableSpring').checked = config.Spring.EnableSpring;
document.querySelector('#EnableSpringSunbeams').checked = config.Spring.EnableSpringSunbeams !== undefined ? config.Spring.EnableSpringSunbeams : true;
document.querySelector('#SpringPollenCount').value = config.Spring.PollenCount;
document.querySelector('#SpringSunbeamCount').value = config.Spring.SunbeamCount;
document.querySelector('#SpringBirdCount').value = config.Spring.BirdCount !== undefined ? config.Spring.BirdCount : 3;
document.querySelector('#SpringButterflyCount').value = config.Spring.ButterflyCount !== undefined ? config.Spring.ButterflyCount : 2;
document.querySelector('#SpringBeeCount').value = config.Spring.BeeCount !== undefined ? config.Spring.BeeCount : 1;
document.querySelector('#SpringLadybugCount').value = config.Spring.LadybugCount !== undefined ? config.Spring.LadybugCount : 1;
document.querySelector('#EnableRandomSpring').checked = config.Spring.EnableRandomSpring;
document.querySelector('#EnableRandomSpringMobile').checked = config.Spring.EnableRandomSpringMobile;
document.querySelector('#EnableDifferentDurationSpring').checked = config.Spring.EnableDifferentDuration;
// Summer
document.querySelector('#EnableSummer').checked = config.Summer.EnableSummer;
document.querySelector('#SummerBubbleCount').value = config.Summer.BubbleCount;
document.querySelector('#SummerDustCount').value = config.Summer.DustCount;
document.querySelector('#EnableRandomSummer').checked = config.Summer.EnableRandomSummer;
document.querySelector('#EnableRandomSummerMobile').checked = config.Summer.EnableRandomSummerMobile;
document.querySelector('#EnableDifferentDurationSummer').checked = config.Summer.EnableDifferentDuration;
// Carnival
document.querySelector('#EnableCarnival').checked = config.Carnival.EnableCarnival;
document.querySelector('#EnableCarnivalSway').checked = config.Carnival.EnableCarnivalSway !== undefined ? config.Carnival.EnableCarnivalSway : true;
document.querySelector('#CarnivalObjectCount').value = config.Carnival.ObjectCount;
document.querySelector('#EnableRandomCarnival').checked = config.Carnival.EnableRandomCarnival;
document.querySelector('#EnableRandomCarnivalMobile').checked = config.Carnival.EnableRandomCarnivalMobile;
document.querySelector('#EnableDifferentDurationCarnival').checked = config.Carnival.EnableDifferentDuration;
// Cherry Blossom
document.querySelector('#EnableCherryBlossom').checked = config.CherryBlossom.EnableCherryBlossom;
document.querySelector('#CherryBlossomPetalCount').value = config.CherryBlossom.PetalCount;
document.querySelector('#EnableRandomCherryBlossom').checked = config.CherryBlossom.EnableRandomCherryBlossom;
document.querySelector('#EnableRandomCherryBlossomMobile').checked = config.CherryBlossom.EnableRandomCherryBlossomMobile;
document.querySelector('#EnableDifferentDurationCherryBlossom').checked = config.CherryBlossom.EnableDifferentDuration;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
}); });
document.querySelector('#SeasonalsConfigForm') document.querySelector('#SeasonalsConfigForm')
.addEventListener('submit', function(e) { .addEventListener('submit', function(e) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SeasonalsConfigPage.pluginUniqueId).then(function (config) {
config.SelectedSeason = document.querySelector('#SelectedSeason').value; config.IsEnabled = document.querySelector('#SeasonalsIsEnabled').checked;
config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked; config.SelectedSeason = document.querySelector('#SeasonalsSelectedSeason').value;
config.AutomateSeasonSelection = document.querySelector('#SeasonalsAutomateSeasonSelection').checked;
config.EnableClientSideToggle = document.querySelector('#SeasonalsEnableClientSideToggle').checked;
// Advanced Config // Save Rules
// Autumn config.SeasonalRules = JSON.stringify(SeasonalsConfigPage.getRulesFromUI());
config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
// Snowflakes // Advanced Config
config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value); // Autumn
config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked; config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked; config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked; config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked; config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked; config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
// Snowfall // Snowflakes
config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked; config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value);
config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value); config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked;
config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value); config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked;
config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value); config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked;
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked;
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked;
// Snowstorm // Snowfall
config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked; config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked;
config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value); config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value);
config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value); config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value);
config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value); config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value);
config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
// Fireworks // Snowstorm
config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked; config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked;
config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value); config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value);
config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value); config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value);
config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked; config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value);
config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value); config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value); config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
// Halloween // Fireworks
config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked; config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked;
config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value); config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value);
config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked; config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value);
config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked; config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked;
config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked; config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value);
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value);
// Hearts // Halloween
config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked; config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked;
config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value); config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value);
config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked; config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked;
config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked; config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked;
config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked; config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked;
// Christmas // Hearts
config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked; config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked;
config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value); config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value);
config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked; config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked;
config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked; config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked;
config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked; config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked;
// Santa // Christmas
config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked; config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked;
config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value); config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value);
config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value); config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked;
config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value); config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked;
config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value); config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked;
config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
// Easter // Santa
config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked; config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked;
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value); config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value);
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked; config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value);
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked; config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value);
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked; config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value);
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked; config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value); config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value); config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value); config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value); config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) { // Easter
Dashboard.processPluginConfigurationUpdateResult(result); config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked;
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value);
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked;
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked;
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked;
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked;
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value);
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value);
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value);
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value);
// Resurrection
config.Resurrection.EnableResurrection = document.querySelector('#EnableResurrection').checked;
config.Resurrection.SymbolCount = parseInt(document.querySelector('#ResurrectionSymbolCount').value);
config.Resurrection.EnableRandomSymbols = document.querySelector('#EnableRandomResurrection').checked;
config.Resurrection.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomResurrectionMobile').checked;
config.Resurrection.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationResurrection').checked;
// Spring
config.Spring.EnableSpring = document.querySelector('#EnableSpring').checked;
config.Spring.EnableSpringSunbeams = document.querySelector('#EnableSpringSunbeams').checked;
config.Spring.PollenCount = parseInt(document.querySelector('#SpringPollenCount').value);
config.Spring.SunbeamCount = parseInt(document.querySelector('#SpringSunbeamCount').value);
config.Spring.BirdCount = parseInt(document.querySelector('#SpringBirdCount').value);
config.Spring.ButterflyCount = parseInt(document.querySelector('#SpringButterflyCount').value);
config.Spring.BeeCount = parseInt(document.querySelector('#SpringBeeCount').value);
config.Spring.LadybugCount = parseInt(document.querySelector('#SpringLadybugCount').value);
config.Spring.EnableRandomSpring = document.querySelector('#EnableRandomSpring').checked;
config.Spring.EnableRandomSpringMobile = document.querySelector('#EnableRandomSpringMobile').checked;
config.Spring.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSpring').checked;
// Summer
config.Summer.EnableSummer = document.querySelector('#EnableSummer').checked;
config.Summer.BubbleCount = parseInt(document.querySelector('#SummerBubbleCount').value);
config.Summer.DustCount = parseInt(document.querySelector('#SummerDustCount').value);
config.Summer.EnableRandomSummer = document.querySelector('#EnableRandomSummer').checked;
config.Summer.EnableRandomSummerMobile = document.querySelector('#EnableRandomSummerMobile').checked;
config.Summer.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSummer').checked;
// Carnival
config.Carnival.EnableCarnival = document.querySelector('#EnableCarnival').checked;
config.Carnival.EnableCarnivalSway = document.querySelector('#EnableCarnivalSway').checked;
config.Carnival.ObjectCount = parseInt(document.querySelector('#CarnivalObjectCount').value);
config.Carnival.EnableRandomCarnival = document.querySelector('#EnableRandomCarnival').checked;
config.Carnival.EnableRandomCarnivalMobile = document.querySelector('#EnableRandomCarnivalMobile').checked;
config.Carnival.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationCarnival').checked;
// Cherry Blossom
config.CherryBlossom.EnableCherryBlossom = document.querySelector('#EnableCherryBlossom').checked;
config.CherryBlossom.PetalCount = parseInt(document.querySelector('#CherryBlossomPetalCount').value);
config.CherryBlossom.EnableRandomCherryBlossom = document.querySelector('#EnableRandomCherryBlossom').checked;
config.CherryBlossom.EnableRandomCherryBlossomMobile = document.querySelector('#EnableRandomCherryBlossomMobile').checked;
config.CherryBlossom.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationCherryBlossom').checked;
ApiClient.updatePluginConfiguration(SeasonalsConfigPage.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
}); });
});
e.preventDefault(); e.preventDefault();
return false; return false;

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.3.3.0</Version> <Version>1.7.1.4</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\" defer></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))
_logger.LogInformation("Successfully injected Seasonals script into index.html."); {
return true; 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) catch (UnauthorizedAccessException)
{ {
_logger.LogWarning("Access was denied when attempting to inject script into index.html. Automatic injection failed. Please ensure the Jellyfin web directory is writable by the process, or manually add the script tag: {ScriptTag}", ScriptTag); _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

@@ -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,85 @@
.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: 1;
animation-fill-mode: forwards;
}
.carnival-sway-wrapper {
will-change: transform;
animation-name: carnival-sway;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.carnival-confetti {
width: 8px;
height: 16px;
background-color: #f0f;
will-change: transform;
animation-name: carnival-flutter;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.carnival-confetti.circle {
width: 8px;
height: 8px;
border-radius: 50%;
}
.carnival-confetti.square {
width: 8px;
height: 8px;
}
.carnival-confetti.triangle {
width: 10px;
height: 10px;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
@keyframes carnival-fall {
0% {
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(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
}
100% {
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
}
}

View File

@@ -0,0 +1,197 @@
const config = window.SeasonalsPluginConfig?.Carnival || {};
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // Enable/disable carnival
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true; // Enable random carnival objects
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false; // Enable random carnival objects on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // Randomize falling and flutter speeds
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // Enable side-to-side sway animation
const carnivalCount = config.ObjectCount || 120; // Number of confetti pieces to spawn
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
// MARK: CONFETTI SIZE (RECTANGLES)
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
const width = Math.random() * 3 + 4; // 4-7px
const height = Math.random() * 5 + 8; // 8-13px
confetti.style.width = `${width}px`;
confetti.style.height = `${height}px`;
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
// MARK: CONFETTI SIZE (CIRCLES/SQUARES)
const size = Math.random() * 5 + 5; // 5-10px
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
}
// Animation settings
// MARK: CONFETTI FALLING SPEED (in seconds)
const duration = Math.random() * 5 + 5;
let delay = 0;
if (isInitial) {
delay = -Math.random() * duration;
} else {
delay = Math.random() * 10;
}
wrapper.style.animationDelay = `${delay}s`;
wrapper.style.animationDuration = `${duration}s`;
if (enableSway) {
// Random sway duration
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
swayWrapper.style.animationDuration = `${swayDuration}s`;
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
// Random sway amplitude (using CSS variable for dynamic keyframe)
// MARK: SWAY DISTANCE RANGE (in px)
const swayAmount = Math.random() * 70 + 30; // 30-100px
const direction = Math.random() > 0.5 ? 1 : -1;
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
}
// Flutter speed and random 3D rotation axis
// MARK: CONFETTI FLUTTER ROTATION SPEED
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
confetti.style.setProperty('--rx', Math.random().toFixed(2));
confetti.style.setProperty('--ry', Math.random().toFixed(2));
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
// Random direction for 3D rotation
const rotDir = Math.random() > 0.5 ? 1 : -1;
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
if (enableSway) {
swayWrapper.appendChild(confetti);
wrapper.appendChild(swayWrapper);
} else {
wrapper.appendChild(confetti);
}
// Respawn confetti when it hits the bottom
wrapper.addEventListener('animationend', (e) => {
if (e.animationName === 'carnival-fall') {
wrapper.remove();
createConfettiPiece(container, false); // respawn without initial huge delay
}
});
container.appendChild(wrapper);
}
function addRandomCarnivalObjects(count) {
const carnivalContainer = document.querySelector('.carnival-container');
if (!carnivalContainer) return;
console.log('Adding random carnival confetti');
for (let i = 0; i < count; i++) {
createConfettiPiece(carnivalContainer, true);
}
}
// initialize standard carnival objects
function initCarnivalObjects() {
let container = document.querySelector('.carnival-container');
if (!container) {
container = document.createElement("div");
container.className = "carnival-container";
container.setAttribute("aria-hidden", "true");
document.body.appendChild(container);
}
// Initial confetti
for (let i = 0; i < 60; i++) {
createConfettiPiece(container, true);
}
}
// initialize carnival
function initializeCarnival() {
if (!carnival) return; // exit if carnival is disabled
initCarnivalObjects();
toggleCarnival();
const screenWidth = window.innerWidth;
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
addRandomCarnivalObjects(carnivalCount);
}
}
initializeCarnival();

View File

@@ -0,0 +1,58 @@
.cherryblossom-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
/* Petals */
.cherryblossom-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: cherryblossom-fall, cherryblossom-sway;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-duration: 10s, 3s;
}
.cherryblossom-petal.lighter {
background-color: #ffd1dc;
opacity: 0.8;
}
.cherryblossom-petal.darker {
background-color: #ffb7c5;
opacity: 0.9;
}
.cherryblossom-petal.type2 {
width: 12px;
height: 12px;
border-radius: 10px 0px 10px 5px;
}
@keyframes cherryblossom-fall {
0% { top: -10%; }
100% { top: 100%; }
}
@keyframes cherryblossom-sway {
0%, 100% {
transform: translateX(0) rotate(0deg);
}
50% {
transform: translateX(30px) rotate(45deg);
}
}

View File

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

View File

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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,206 +1,413 @@
// theme-configs.js /*
* Seasonals Plugin (Client Side Manager Logic)
*/
// theme configurations const ThemeConfigs = {
const themeConfigs = {
snowflakes: { snowflakes: {
css: '/Seasonals/Resources/snowflakes.css', css: '../Seasonals/Resources/snowflakes.css',
js: '/Seasonals/Resources/snowflakes.js', js: '../Seasonals/Resources/snowflakes.js',
containerClass: 'snowflakes' containerClass: 'snowflakes'
}, },
snowfall: { snowfall: {
css: '/Seasonals/Resources/snowfall.css', css: '../Seasonals/Resources/snowfall.css',
js: '/Seasonals/Resources/snowfall.js', js: '../Seasonals/Resources/snowfall.js',
containerClass: 'snowfall-container' containerClass: 'snowfall-container'
}, },
snowstorm: { snowstorm: {
css: '/Seasonals/Resources/snowstorm.css', css: '../Seasonals/Resources/snowstorm.css',
js: '/Seasonals/Resources/snowstorm.js', js: '../Seasonals/Resources/snowstorm.js',
containerClass: 'snowstorm-container' containerClass: 'snowstorm-container'
}, },
fireworks: { fireworks: {
css: '/Seasonals/Resources/fireworks.css', css: '../Seasonals/Resources/fireworks.css',
js: '/Seasonals/Resources/fireworks.js', js: '../Seasonals/Resources/fireworks.js',
containerClass: 'fireworks' containerClass: 'fireworks'
}, },
halloween: { halloween: {
css: '/Seasonals/Resources/halloween.css', css: '../Seasonals/Resources/halloween.css',
js: '/Seasonals/Resources/halloween.js', js: '../Seasonals/Resources/halloween.js',
containerClass: 'halloween-container' containerClass: 'halloween-container'
}, },
hearts: { hearts: {
css: '/Seasonals/Resources/hearts.css', css: '../Seasonals/Resources/hearts.css',
js: '/Seasonals/Resources/hearts.js', js: '../Seasonals/Resources/hearts.js',
containerClass: 'hearts-container' containerClass: 'hearts-container'
}, },
christmas: { christmas: {
css: '/Seasonals/Resources/christmas.css', css: '../Seasonals/Resources/christmas.css',
js: '/Seasonals/Resources/christmas.js', js: '../Seasonals/Resources/christmas.js',
containerClass: 'christmas-container' containerClass: 'christmas-container'
}, },
santa: { santa: {
css: '/Seasonals/Resources/santa.css', css: '../Seasonals/Resources/santa.css',
js: '/Seasonals/Resources/santa.js', js: '../Seasonals/Resources/santa.js',
containerClass: 'santa-container' containerClass: 'santa-container'
}, },
autumn: { autumn: {
css: '/Seasonals/Resources/autumn.css', css: '../Seasonals/Resources/autumn.css',
js: '/Seasonals/Resources/autumn.js', js: '../Seasonals/Resources/autumn.js',
containerClass: 'autumn-container' containerClass: 'autumn-container'
}, },
easter: { easter: {
css: '/Seasonals/Resources/easter.css', css: '../Seasonals/Resources/easter.css',
js: '/Seasonals/Resources/easter.js', js: '../Seasonals/Resources/easter.js',
containerClass: 'easter-container' containerClass: 'easter-container'
}, },
resurrection: {
css: '../Seasonals/Resources/resurrection.css',
js: '../Seasonals/Resources/resurrection.js',
containerClass: 'resurrection-container'
},
summer: { summer: {
css: '/Seasonals/Resources/summer.css', css: '../Seasonals/Resources/summer.css',
js: '/Seasonals/Resources/summer.js', js: '../Seasonals/Resources/summer.js',
containerClass: 'summer-container' containerClass: 'summer-container'
}, },
spring: { spring: {
css: '/Seasonals/Resources/spring.css', css: '../Seasonals/Resources/spring.css',
js: '/Seasonals/Resources/spring.js', js: '../Seasonals/Resources/spring.js',
containerClass: 'spring-container' containerClass: 'spring-container'
}, },
carnival: {
css: '../Seasonals/Resources/carnival.css',
js: '../Seasonals/Resources/carnival.js',
containerClass: 'carnival-container'
},
cherryblossom: {
css: '../Seasonals/Resources/cherryblossom.css',
js: '../Seasonals/Resources/cherryblossom.js',
containerClass: 'cherryblossom-container'
},
none: { none: {
containerClass: 'none' containerClass: 'none'
}, },
}; };
// determine current theme based on the current month const SeasonalSettingsManager = {
function determineCurrentTheme() { initialized: false,
const date = new Date(); config: null,
const month = date.getMonth(); // 0-11
const day = date.getDate(); // 1-31
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5 init(config) {
if (this.initialized) return;
this.config = config;
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18 // Only inject settings if enabled on server by admin
if (this.config && this.config.EnableClientSideToggle !== false) {
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27 this.injectSettingsIcon();
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27 this.initialized = true;
console.log("Seasonals: Client-Side Settings Manager initialized.");
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
getSetting(key, defaultValue) {
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25 const value = localStorage.getItem(`seasonals-${key}`);
return value !== null ? value : defaultValue;
//NOT IMPLEMENTED YET },
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
setSetting(key, value) {
//NOT IMPLEMENTED YET localStorage.setItem(`seasonals-${key}`, value);
//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 createIcon() {
const button = document.createElement('button');
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november button.type = 'button';
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
return 'none'; // Fallback (nothing) 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;">';
// load theme csss button.style.verticalAlign = 'middle';
function loadThemeCSS(cssPath) {
if (!cssPath) return; button.addEventListener('click', (e) => {
e.stopPropagation();
const link = document.createElement('link'); this.toggleSettingsPopup(button);
link.rel = 'stylesheet'; });
link.href = cssPath;
return button;
link.onerror = () => { },
console.error(`Failed to load CSS: ${cssPath}`);
}; injectSettingsIcon() {
const observer = new MutationObserver((mutations, obs) => {
document.body.appendChild(link); const headerRight = document.querySelector('.headerRight');
console.log(`CSS file "${cssPath}" loaded.`); if (headerRight && !document.querySelector('.seasonal-settings-button')) {
} const icon = this.createIcon();
headerRight.prepend(icon);
// load theme js }
function loadThemeJS(jsPath) { });
if (!jsPath) return;
observer.observe(document.body, {
const script = document.createElement('script'); childList: true,
script.src = jsPath; subtree: true
script.defer = true; });
},
script.onerror = () => {
console.error(`Failed to load JS: ${jsPath}`); createPopup(anchorElement) {
}; const existing = document.querySelector('.seasonal-settings-popup');
if (existing) existing.remove();
document.body.appendChild(script);
console.log(`JS file "${jsPath}" loaded.`); const popup = document.createElement('div');
} popup.className = 'seasonal-settings-popup dialog';
// update theme container class name Object.assign(popup.style, {
function updateThemeContainer(containerClass) { position: 'fixed',
// Create container if it doesn't exist zIndex: '10000',
let container = document.querySelector('.seasonals-container'); backgroundColor: '#202020',
if (!container) { padding: '1em',
container = document.createElement('div'); borderRadius: '0.3em',
container.className = 'seasonals-container'; boxShadow: '0 0 20px rgba(0,0,0,0.5)',
document.body.appendChild(container); minWidth: '200px',
} color: '#fff',
maxWidth: '250px'
container.className = `seasonals-container ${containerClass}`; });
console.log(`Seasonals-Container class updated to "${containerClass}".`);
} const rect = anchorElement.getBoundingClientRect();
function removeSelf() { // Positioning logic
const script = document.currentScript; let rightPos = window.innerWidth - rect.right;
if (script) script.parentNode.removeChild(script); if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
console.log('External script removed:', script); popup.style.right = '1rem';
} popup.style.left = 'auto';
} else {
// initialize theme popup.style.right = `${rightPos}px`;
async function initializeTheme() { popup.style.left = 'auto';
let automateThemeSelection = true; }
let defaultTheme = 'none'; popup.style.top = `${rect.bottom + 10}px`;
try { // Popup HTML
const response = await fetch('/Seasonals/Config'); let html = `
if (response.ok) { <h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
const config = await response.json();
automateThemeSelection = config.AutomateSeasonSelection; <div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
defaultTheme = config.SelectedSeason; <label class="emby-checkbox-label">
window.SeasonalsPluginConfig = config; <input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
console.log('Seasonals Config loaded:', config); <span class="checkboxLabel">Enable Seasonals</span>
} else { </label>
console.error('Failed to fetch Seasonals config'); </div>
<div class="selectContainer" style="margin-bottom: 0.5em;">
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
<select id="seasonal-theme-select" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
<option value="auto">Server-Side</option>
</select>
</div>
`;
popup.innerHTML = html;
// Populate Select Options
const themeSelect = popup.querySelector('#seasonal-theme-select');
Object.keys(ThemeConfigs).forEach(key => {
if (key === 'none') return;
const option = document.createElement('option');
option.value = key;
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
themeSelect.appendChild(option);
});
// Set Initial Values
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
themeSelect.value = this.getSetting('theme', 'auto');
// Event Listeners
enabledCheckbox.addEventListener('change', (e) => {
this.setSetting('enabled', e.target.checked);
location.reload();
});
themeSelect.addEventListener('change', (e) => {
this.setSetting('theme', e.target.value);
location.reload();
});
// Close on outside click
const closeHandler = (e) => {
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
popup.remove();
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
document.body.appendChild(popup);
},
toggleSettingsPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) {
existing.remove();
} else {
this.createPopup(anchorElement);
} }
} catch (error) {
console.error('Error fetching Seasonals config:', error);
} }
};
let currentTheme; const SeasonalsManager = {
if (automateThemeSelection === false) { config: null,
currentTheme = defaultTheme;
} else { async init() {
currentTheme = determineCurrentTheme(); // Fetch Config
try {
const response = await fetch('../Seasonals/Config');
if (response.ok) {
this.config = await response.json();
window.SeasonalsPluginConfig = this.config;
if (this.config.IsEnabled === false) {
console.log('Seasonals: Plugin is disabled globally.');
return;
}
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 month = date.getMonth() + 1; // 1-12
const day = date.getDate(); // 1-31
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
return rule.Theme;
}
}
return 'none'; // No rule matched
},
isDateInRange: function(day, month, startDay, startMonth, endDay, endMonth) {
if (startMonth > endMonth) {
// Wrapping year (e.g. Dec to Jan)
return this.isDateAfterOrEqual(day, month, startDay, startMonth) ||
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
} else {
// Normal range
return this.isDateAfterOrEqual(day, month, startDay, startMonth) &&
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
}
},
isDateAfterOrEqual: function(day, month, targetDay, targetMonth) {
if (month > targetMonth) return true;
if (month === targetMonth && day >= targetDay) return true;
return false;
},
isDateBeforeOrEqual: function(day, month, targetDay, targetMonth) {
if (month < targetMonth) return true;
if (month === targetMonth && day <= targetDay) return true;
return false;
},
applyTheme(themeName) {
const theme = ThemeConfigs[themeName];
if (!theme) {
console.error(`Seasonals: Theme "${themeName}" not found.`);
return;
}
this.updateThemeContainer(theme.containerClass);
if (theme.css) this.loadResource('css', theme.css);
if (theme.js) this.loadResource('js', theme.js);
console.log(`Seasonals: Theme "${themeName}" applied.`);
},
updateThemeContainer(containerClass) {
let container = document.querySelector('.seasonals-container');
if (!container) {
container = document.createElement('div');
container.className = 'seasonals-container';
document.body.appendChild(container);
}
container.className = `seasonals-container ${containerClass}`;
},
// helper to resolve paths for local testing vs production
resolvePath(path) {
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return path.replace('/Seasonals/Resources/', './');
}
return path;
},
loadResource(type, path) {
if (!path) return;
if (type === 'css') {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = path;
// link.href = resolvePath(cssPath);
link.onerror = () => console.error(`Seasonals: Failed to load CSS: ${path}`);
document.body.appendChild(link);
} else if (type === 'js') {
const script = document.createElement('script');
script.src = path;
// script.src = resolvePath(jsPath);
script.defer = true;
script.onerror = () => console.error(`Seasonals: Failed to load JS: ${path}`);
document.body.appendChild(script);
}
} }
};
console.log(`Selected theme: ${currentTheme}`); SeasonalsManager.init();
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();
}
initializeTheme();

View File

@@ -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

@@ -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')) {
@@ -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,212 @@
.spring-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100vh;
pointer-events: none;
z-index: 1000;
}
/* 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;
opacity: 0;
}
/* Grass */
.spring-grass-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 80px;
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;
}
/* Birds */
.spring-bird {
position: static !important;
display: block;
z-index: 1001;
/* MARK: BIRD SIZE */
width: 80px;
height: auto;
will-change: transform;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
/* Butterflies */
.spring-butterfly {
position: static !important;
display: block;
z-index: 1001;
/* MARK: BUTTERFLY SIZE */
width: 40px;
height: auto;
will-change: transform;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
/* Bee */
.spring-bee {
position: static !important;
display: block;
z-index: 1001;
/* MARK: BEE SIZE */
width: 30px;
height: auto;
will-change: transform;
}
/* Ladybug */
.spring-ladybug-gif {
position: static !important;
display: block;
/* MARK: LADYBUG SIZE */
width: 30px;
height: auto;
will-change: transform;
animation-timing-function: linear;
animation-iteration-count: infinite;
--bug-rotation: -55deg;
}
.spring-ladybug-wrapper {
z-index: 1002;
}
/* Generic Wrappers */
.spring-anim-wrapper {
position: fixed;
z-index: 1001;
will-change: transform;
top: 0; left: 0;
}
.spring-mirror-wrapper {
width: 100%; height: 100%;
will-change: transform;
transform-origin: center;
}
@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; transform: rotate(var(--beam-rotation, 45deg)) scaleX(0.8); }
50% { opacity: 0.6; transform: rotate(var(--beam-rotation, 45deg)) scaleX(1.2); }
100% { opacity: 0; transform: rotate(var(--beam-rotation, 45deg)) scaleX(0.8); }
}
@keyframes spring-grass-sway {
0% { transform: rotate(0deg); }
50% { transform: rotate(8deg); }
100% { transform: rotate(0deg); }
}
/* Wrapper animations (Flight across screen) */
@keyframes spring-fly-right-wrapper {
0% { transform: translateX(-10vw); }
100% { transform: translateX(110vw); }
}
@keyframes spring-fly-left-wrapper {
0% { transform: translateX(110vw); }
100% { transform: translateX(-10vw); }
}
/* Vertical Drift for Sloped Flight */
@keyframes spring-vertical-drift {
0% { top: var(--start-y, 10%); }
100% { top: var(--end-y, 10%); }
}
/* Inner animations (Bobbing/Fluttering) */
@keyframes spring-bob {
0% { transform: translateY(0); }
50% { transform: translateY(-20px); }
100% { transform: translateY(0); }
}
@keyframes spring-flutter {
0% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-5px) rotate(5deg); }
50% { transform: translateY(0) rotate(0deg); }
75% { transform: translateY(5px) rotate(-5deg); }
100% { transform: translateY(0) rotate(0deg); }
}
/* Bee Buzz - Reduced Intensity */
@keyframes spring-buzz {
0% { transform: translate(0, 0); }
25% { transform: translate(2px, -2px); }
50% { transform: translate(0, 2px); }
75% { transform: translate(-2px, -2px); }
100% { transform: translate(0, 0); }
}
/* Ladybug Walk (Wrapper handles X) */
@keyframes spring-walk-right {
0% { transform: translateX(-10vw); }
100% { transform: translateX(110vw); }
}
@keyframes spring-walk-left {
0% { transform: translateX(110vw); }
100% { transform: translateX(-10vw); }
}
/* Ladybug Crawl (Inner Wobble) */
@keyframes spring-crawl {
0% { transform: translateY(0) rotate(var(--bug-rotation, 0deg)); }
25% { transform: translateY(-3px) rotate(calc(var(--bug-rotation, 0deg) + 8deg)); }
50% { transform: translateY(0) rotate(var(--bug-rotation, 0deg)); }
75% { transform: translateY(-3px) rotate(calc(var(--bug-rotation, 0deg) - 8deg)); }
100% { transform: translateY(0) rotate(var(--bug-rotation, 0deg)); }
}

View File

@@ -0,0 +1,396 @@
const config = window.SeasonalsPluginConfig?.Spring || {};
const spring = config.EnableSpring !== undefined ? config.EnableSpring : true; // Enable/disable spring
const pollenCount = config.PollenCount || 30; // Number of pollen particles
const sunbeamCount = config.SunbeamCount || 5; // Number of sunbeams
const enableSunbeams = config.EnableSpringSunbeams !== undefined ? config.EnableSpringSunbeams : true; // Enable/disable sunbeams
const birdCount = config.BirdCount !== undefined ? config.BirdCount : 3; // Number of birds
const butterflyCount = config.ButterflyCount !== undefined ? config.ButterflyCount : 4; // Number of butterflies
const beeCount = config.BeeCount !== undefined ? config.BeeCount : 2; // Number of bees
const ladybugCount = config.LadybugCount !== undefined ? config.LadybugCount : 2; // Number of ladybugs
const randomSpring = config.EnableRandomSpring !== undefined ? config.EnableRandomSpring : true; // Enable random spring objects
const birdImages = [
'../Seasonals/Resources/spring_assets/Bird_1.gif',
'../Seasonals/Resources/spring_assets/Bird_2.gif',
'../Seasonals/Resources/spring_assets/Bird_3.gif'
];
const butterflyImages = [
'../Seasonals/Resources/spring_assets/Butterfly_1.gif',
'../Seasonals/Resources/spring_assets/Butterfly_2.gif'
];
const beeImage = '../Seasonals/Resources/spring_assets/Bee.gif';
const ladybugImage = '../Seasonals/Resources/spring_assets/ladybug.gif';
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 createPollen(container) {
const pollen = document.createElement('div');
pollen.classList.add('spring-pollen');
// MARK: POLLEN START VERTICAL POSITION (in %)
const startY = Math.random() * 60 + 20;
pollen.style.top = `${startY}%`;
pollen.style.left = `${Math.random() * 100}%`;
// MARK: POLLEN SIZE
const size = Math.random() * 3 + 1; // 1-4px
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 spawnSunbeamGroup(container, count) {
if (!enableSunbeams) return;
const rotate = Math.random() * 30 - 15 + 45;
let beamsActive = count;
for (let i = 0; i < count; i++) {
const beam = document.createElement('div');
beam.classList.add('spring-sunbeam');
const left = Math.random() * 100;
beam.style.left = `${left}%`;
// MARK: SUNBEAM WIDTH (in px)
const width = Math.random() * 12 + 8; // 8-20px wide
beam.style.width = `${width}px`;
beam.style.setProperty('--beam-rotation', `${rotate}deg`);
const duration = Math.random() * 7 + 8; // 8-15s
beam.style.animation = `spring-beam-pulse ${duration}s ease-in-out forwards`;
beam.style.animationDelay = `${Math.random() * 3}s`;
beam.addEventListener('animationend', () => {
beam.remove();
beamsActive--;
if (beamsActive === 0) {
spawnSunbeamGroup(container, count);
}
});
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);
}
grassContainer.innerHTML = '';
const bladeCount = window.innerWidth / 3;
for (let i = 0; i < bladeCount; i++) {
const blade = document.createElement('div');
blade.classList.add('spring-grass');
// MARK: GRASS HEIGHT
const height = Math.random() * 40 + 20; // 20-60px height
blade.style.height = `${height}px`;
blade.style.left = `${i * 3 + Math.random() * 2}px`;
const duration = Math.random() * 2 + 3;
blade.style.animationDuration = `${duration}s`;
blade.style.animationDelay = `-${Math.random() * 5}s`;
const hue = 100 + Math.random() * 40;
blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`;
// Random z-index to interleave with Ladybug (1002)
// Values: 1001 (behind) or 1003 (front)
const z = Math.random() > 0.5 ? 1001 : 1003;
blade.style.zIndex = z;
grassContainer.appendChild(blade);
}
}
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 (enableSunbeams) {
spawnSunbeamGroup(container, sunbeamCount);
}
}
function initializeSpring() {
if (!spring) {
console.warn('Spring is disabled.');
return;
}
initSpringObjects();
toggleSpring();
const container = document.querySelector('.spring-container');
if (container) {
if (randomSpring) {
// Add Pollen
for (let i = 0; i < pollenCount; i++) {
createPollen(container);
}
// Add Birds
for (let i = 0; i < birdCount; i++) {
setTimeout(() => createBird(container), Math.random() * 1000); // 0-1s desync
}
// Add Butterflies
for (let i = 0; i < butterflyCount; i++) {
setTimeout(() => createButterfly(container), Math.random() * 1000); // 0-1s desync
}
// Add Bees
for (let i = 0; i < beeCount; i++) {
setTimeout(() => createBee(container), Math.random() * 1000); // 0-1s desync
}
// Add Ladybugs
for (let i = 0; i < ladybugCount; i++) {
setTimeout(() => createLadybugGif(container), Math.random() * 1000); // 0-1s desync
}
}
}
}
function createBird(container) {
const wrapper = document.createElement('div');
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-bird-wrapper');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
const bird = document.createElement('img');
bird.classList.add('spring-bird');
const randomSrc = birdImages[Math.floor(Math.random() * birdImages.length)];
bird.src = randomSrc;
const direction = Math.random() > 0.5 ? 'right' : 'left';
// MARK: BIRD SPEED (10-15s)
const duration = Math.random() * 5 + 10;
// MARK: BIRD HEIGHT RANGE (in %)
const startY = Math.random() * 55 + 5; // Start 5-60%
const endY = Math.random() * 55 + 5; // End 5-60%
wrapper.style.setProperty('--start-y', `${startY}%`);
wrapper.style.setProperty('--end-y', `${endY}%`);
if (direction === 'right') {
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards, spring-vertical-drift ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(-1)';
} else {
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards, spring-vertical-drift ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(1)';
}
wrapper.addEventListener('animationend', (e) => {
if (e.animationName.includes('fly-')) {
wrapper.remove();
createBird(container);
}
});
bird.style.animation = `spring-bob 2s ease-in-out infinite`;
wrapper.style.top = `${startY}%`;
mirror.appendChild(bird);
wrapper.appendChild(mirror);
container.appendChild(wrapper);
}
function createButterfly(container) {
const wrapper = document.createElement('div');
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-butterfly-wrapper');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
const butterfly = document.createElement('img');
butterfly.classList.add('spring-butterfly');
const randomSrc = butterflyImages[Math.floor(Math.random() * butterflyImages.length)];
butterfly.src = randomSrc;
const duration = Math.random() * 15 + 25;
const direction = Math.random() > 0.5 ? 'right' : 'left';
if (direction === 'right') {
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(1)';
} else {
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(-1)';
}
wrapper.addEventListener('animationend', (e) => {
if (e.animationName.includes('fly-')) {
wrapper.remove();
createButterfly(container);
}
});
// MARK: BUTTERFLY FLUTTER RHYTHM
butterfly.style.animation = `spring-flutter 3s ease-in-out infinite`;
butterfly.style.animationDelay = `-${Math.random() * 3}s`;
// MARK: BUTTERFLY HEIGHT (in %)
const top = Math.random() * 35 + 30; // 30-65%
wrapper.style.top = `${top}%`;
mirror.appendChild(butterfly);
wrapper.appendChild(mirror);
container.appendChild(wrapper);
}
function createBee(container) {
const wrapper = document.createElement('div');
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-bee-wrapper');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
const bee = document.createElement('img');
bee.classList.add('spring-bee');
bee.src = beeImage;
const duration = Math.random() * 10 + 15;
const direction = Math.random() > 0.5 ? 'right' : 'left';
if (direction === 'right') {
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(1)';
} else {
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(-1)';
}
wrapper.addEventListener('animationend', (e) => {
if (e.animationName.includes('fly-')) {
wrapper.remove();
createBee(container);
}
});
// MARK: BEE HEIGHT (in %)
const top = Math.random() * 60 + 20; // 20-80%
wrapper.style.top = `${top}%`;
mirror.appendChild(bee);
wrapper.appendChild(mirror);
container.appendChild(wrapper);
}
function createLadybugGif(container) {
const wrapper = document.createElement('div');
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-ladybug-wrapper');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
const bug = document.createElement('img');
bug.classList.add('spring-ladybug-gif');
bug.src = ladybugImage;
const direction = Math.random() > 0.5 ? 'right' : 'left';
const duration = Math.random() * 20 + 30;
if (direction === 'right') {
wrapper.style.animation = `spring-walk-right ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(1)';
} else {
wrapper.style.animation = `spring-walk-left ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(-1)';
}
wrapper.addEventListener('animationend', (e) => {
if (e.animationName.includes('walk-')) {
wrapper.remove();
createLadybugGif(container);
}
});
bug.style.animation = `spring-crawl 2s ease-in-out infinite`;
wrapper.style.top = 'auto';
wrapper.style.bottom = '5px';
mirror.appendChild(bug);
wrapper.appendChild(mirror);
container.appendChild(wrapper);
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const handleResize = debounce(() => {
const container = document.querySelector('.spring-container');
if (container) {
createGrass(container);
}
}, 250);
window.addEventListener('resize', handleResize);
initializeSpring();

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

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,147 @@
const config = window.SeasonalsPluginConfig?.Summer || {};
const summer = config.EnableSummer !== undefined ? config.EnableSummer : true; // Enable/disable summer theme
const bubbleCount = config.BubbleCount || 30; // Number of bubbles
const dustCount = config.DustCount || 50; // Number of dust particles
const randomSummer = config.EnableRandomSummer !== undefined ? config.EnableRandomSummer : true; // Enable random generating objects
const randomSummerMobile = config.EnableRandomSummerMobile !== undefined ? config.EnableRandomSummerMobile : false; // Enable random generating objects on mobile
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // Randomize animation duration of bubbles and dust
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) {
// MARK: BUBBLE SIZE
const size = Math.random() * 20 + 10; // 10-30px bubbles
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
} else {
// MARK: DUST SIZE
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
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) {
// MARK: BUBBLE SIZE
const size = Math.random() * 20 + 10;
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
} else {
// MARK: DUST SIZE
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,508 @@
<!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="cherryblossom">Cherryblossom</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' },
cherryblossom: { css: 'cherryblossom.css', js: 'cherryblossom.js', container: 'cherryblossom-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"
}
}
}

193
README.md
View File

@@ -13,20 +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)
- [Client Compatibility](#client-compatibility) - [Client Compatibility](#client-compatibility)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Automatic Theme Selection](#automatic-theme-selection) - [Automatic Theme Selection](#automatic-theme-selection)
- [Theme Settings](#theme-settings) - [Theme Settings](#theme-settings)
- [Build Process](#build-process) - [Build the plugin by yourself](#build-the-plugin-by-yourself)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Effects Not Showing](#effects-not-showing) - [Effects Not Showing](#effects-not-showing)
- [Docker Permission Issues](#docker-permission-issues) - [Docker Permission Issues](#docker-permission-issues)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Legacy Manual Installation](#legacy-manual-installation)
- [Installation](#installation-1)
- [Usage](#usage)
- [Additional Directory: Separate Single Seasonals](#additional-directory-separate-single-seasonals)
--- ---
@@ -34,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
@@ -77,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.
@@ -135,7 +176,7 @@ If automatic selection is enabled, the following themes are applied based on the
## Theme Settings ## 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. Each theme contains additional settings to customize its behavior. Expand the advanced configuration section to configure each theme, adjust parameters like particle count, animation speed etc.
## Build Process ## Build the plugin by yourself
If you want to build the plugin yourself: If you want to build the plugin yourself:
@@ -202,117 +243,5 @@ volumes:
## 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.3.3.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.3.3.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.3.3.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.

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

@@ -2,19 +2,75 @@
{ {
"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.7.1.4",
"changelog": "- feat: add summer, spring and carnival themes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.4/Jellyfin.Plugin.Seasonals.zip",
"checksum": "549d2d3d6451c828a5e1284a74d53ab2",
"timestamp": "2026-02-21T14:24:25Z"
},
{
"version": "1.7.0.15",
"changelog": "- feat: add customizable auto seasonal list via config page\n- feat: add new season theme 'resurrection' by Bioflash257",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.0.15/Jellyfin.Plugin.Seasonals.zip",
"checksum": "d1fc094710efe45ea8cc885bc6a826c4",
"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", "version": "1.3.4.0",
"changelog": "- some fixes for js loading\n- adapted config page descriptions", "changelog": "- some fixes for js loading\n- adapted config page descriptions",
"targetAbi": "10.11.0.0", "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", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.4.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "4985ef202d93fa3287673daf4ab70360", "checksum": "4869a9a0c08317d2cb0e0fc8f454cf2b",
"timestamp": "2025-12-24T01:41:26Z" "timestamp": "2025-12-24T02:02:15Z"
}, },
{ {
"version": "1.3.3.0", "version": "1.3.3.0",
@@ -57,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"
}
]
} }
] ]