Compare commits

...

84 Commits

Author SHA1 Message Date
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
26 changed files with 1666 additions and 260 deletions

View File

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

3
.gitignore vendored
View File

@@ -4,6 +4,5 @@ obj/
.idea/ .idea/
artifacts artifacts
test-site.html test-site-old.html
test-site-new.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` 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` 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`
- [ ] 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! 🎉

View File

@@ -27,6 +27,7 @@ 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();
} }
/// <summary> /// <summary>
@@ -49,6 +50,11 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public bool EnableClientSideToggle { get; set; } 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\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"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> /// <summary>
/// Gets or sets the Seasonals options. /// Gets or sets the Seasonals options.
/// </summary> /// </summary>
@@ -62,6 +68,7 @@ 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 class AutumnOptions public class AutumnOptions
@@ -166,3 +173,12 @@ 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;
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -6,15 +6,10 @@
</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">
<style>
select option:disabled {
color: #a3a3a3 !important;
}
</style>
<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>
@@ -22,62 +17,100 @@
</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="IsEnabled" name="IsEnabled" type="checkbox" is="emby-checkbox" /> <h2 class="sectionTitle">Main Plugin Settings</h2>
<span>Enable Seasonals</span> <div class="checkboxContainer checkboxContainer-withDescription">
</label> <label class="emby-checkbox-label">
<div class="fieldDescription">Enable or disable the entire plugin functionality.</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</option>
<option value="autumn">Autumn</option>
<option value="easter">Easter</option>
<option value="resurrection">Resurrection</option>
<option value="summer" disabled>Summer (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="spring" disabled>Spring (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="oktoberfest" disabled>Oktoberfest (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="carnival" disabled>Carnival (not implemented yet. Please commit ideas in a issue or PR)</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>
</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="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AutomateSeasonSelection" name="AutomateSeasonSelection" type="checkbox" is="emby-checkbox" />
<span>Automate Season Selection</span>
</label>
<div class="fieldDescription">Automatically select the season based on the date.</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="SelectedSeason">Selected Season</label>
<select id="SelectedSeason" name="SelectedSeason" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
<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. Please commit ideas in a issue or PR)</option>
<option value="spring" disabled>Spring (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="oktoberfest" disabled>Oktoberfest (not implemented yet. Please commit ideas in a issue or PR)</option>
<option value="carnival" disabled>Carnival (not implemented yet. Please commit ideas in a issue or PR)</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>
</select>
<div class="fieldDescription">The season to display if automation is disabled.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableClientSideToggle" name="EnableClientSideToggle" 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>
<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">
@@ -505,7 +538,45 @@
<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>
</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>
@@ -526,19 +597,244 @@
</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>' +
' </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('#IsEnabled').checked = config.IsEnabled; document.querySelector('#SeasonalsIsEnabled').checked = config.IsEnabled;
document.querySelector('#SelectedSeason').value = config.SelectedSeason; document.querySelector('#SeasonalsSelectedSeason').value = config.SelectedSeason;
document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection; document.querySelector('#SeasonalsAutomateSeasonSelection').checked = config.AutomateSeasonSelection;
document.querySelector('#EnableClientSideToggle').checked = config.EnableClientSideToggle !== undefined ? config.EnableClientSideToggle : true; 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
@@ -624,6 +920,13 @@
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;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
}); });
@@ -631,12 +934,14 @@
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.IsEnabled = document.querySelector('#IsEnabled').checked; config.IsEnabled = document.querySelector('#SeasonalsIsEnabled').checked;
config.SelectedSeason = document.querySelector('#SelectedSeason').value; config.SelectedSeason = document.querySelector('#SeasonalsSelectedSeason').value;
config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked; config.AutomateSeasonSelection = document.querySelector('#SeasonalsAutomateSeasonSelection').checked;
config.EnableClientSideToggle = document.querySelector('#EnableClientSideToggle').checked; config.EnableClientSideToggle = document.querySelector('#SeasonalsEnableClientSideToggle').checked;
// Save Rules
config.SeasonalRules = JSON.stringify(SeasonalsConfigPage.getRulesFromUI());
// Advanced Config // Advanced Config
// Autumn // Autumn
@@ -722,10 +1027,17 @@
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value); config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value);
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value); config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value);
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) { // 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;
ApiClient.updatePluginConfiguration(SeasonalsConfigPage.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result); Dashboard.processPluginConfigurationUpdateResult(result);
});
}); });
});
e.preventDefault(); e.preventDefault();
return false; return false;

View File

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

View File

@@ -18,7 +18,7 @@ public class ScriptInjector
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>"; public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
public const string Marker = "</body>"; public const string Marker = "</body>";
/// <summary> /// <summary>

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

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

@@ -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 @@
.ressurection-container {
display: block;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.ressurection-symbol {
position: fixed;
z-index: 15;
top: -15%;
user-select: none;
-webkit-user-select: none;
cursor: default;
animation-name: ressurection-fall, ressurection-sway;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
will-change: transform, top;
}
.ressurection-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) {
.ressurection-symbol img {
width: 42px;
}
}
@keyframes ressurection-fall {
0% {
top: -15%;
}
100% {
top: 105%;
}
}
@keyframes ressurection-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

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

@@ -4,63 +4,68 @@
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'
}, },
none: { none: {
@@ -99,8 +104,7 @@ const SeasonalSettingsManager = {
button.className = 'paper-icon-button-light headerButton seasonal-settings-button'; button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
button.title = 'Seasonal Settings'; button.title = 'Seasonal Settings';
// button.innerHTML = '<span class="material-icons">ac_unit</span>'; // button.innerHTML = '<span class="material-icons">ac_unit</span>';
// button.innerHTML = '<img src="/Seasonals/Resources/assets/logo_SW.svg" style="width: 52px; height: 24px; vertical-align: middle;">'; button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
button.innerHTML = '<img src="/Seasonals/Resources/assets/logo_SW.svg" style="width: 24px; height: 24px; vertical-align: middle;">';
button.style.verticalAlign = 'middle'; button.style.verticalAlign = 'middle';
button.addEventListener('click', (e) => { button.addEventListener('click', (e) => {
@@ -233,7 +237,7 @@ const SeasonalsManager = {
async init() { async init() {
// Fetch Config // Fetch Config
try { try {
const response = await fetch('/Seasonals/Config'); const response = await fetch('../Seasonals/Config');
if (response.ok) { if (response.ok) {
this.config = await response.json(); this.config = await response.json();
window.SeasonalsPluginConfig = this.config; window.SeasonalsPluginConfig = this.config;
@@ -284,34 +288,56 @@ const SeasonalsManager = {
}, },
determineCurrentThemeDate() { determineCurrentThemeDate() {
var rules = [];
try {
rules = JSON.parse(this.config.SeasonalRules || "[]");
} catch (e) {
console.error("Seasonals: Error parsing SeasonalRules", e);
}
if (rules.length === 0) {
// Fallback to empty/none if no rules are defined (though default should exist)
console.log("Seasonals: No auto-selection rules found.");
return 'none';
}
const date = new Date(); const date = new Date();
const month = date.getMonth(); // 0-11 const month = date.getMonth() + 1; // 1-12
const day = date.getDate(); // 1-31 const day = date.getDate(); // 1-31
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5 for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
return rule.Theme;
}
}
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18 return 'none'; // No rule matched
},
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // santa december 22 - 27 isDateInRange: function(day, month, startDay, startMonth, endDay, endMonth) {
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27 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);
}
},
if (month === 11) return 'snowflakes'; // snowflakes december isDateAfterOrEqual: function(day, month, targetDay, targetMonth) {
if (month === 0 || month === 1) return 'snowfall'; // snow january, february if (month > targetMonth) return true;
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february if (month === targetMonth && day >= targetDay) return true;
return false;
},
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25 isDateBeforeOrEqual: function(day, month, targetDay, targetMonth) {
if (month < targetMonth) return true;
//NOT IMPLEMENTED YET if (month === targetMonth && day <= targetDay) return true;
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may return false;
//NOT IMPLEMENTED YET
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
return 'none'; // Fallback (no theme)
}, },
applyTheme(themeName) { applyTheme(themeName) {

View File

@@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Seasonals Test Display</title>
<link rel="stylesheet" href="snowfall.css">
<style>
body {
background-color: black;
color: white;
margin: 0;
font-family: sans-serif;
}
/* Mock Jellyfin Header */
.skinHeader {
background-color: #101010;
height: 60px;
display: flex;
align-items: center;
padding: 0 1em;
border-bottom: 1px solid #333;
}
.headerRight {
margin-left: auto;
display: flex;
align-items: center;
}
.paper-icon-button-light {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 10px;
}
.material-icons {
font-family: 'Material Icons';
/* Placeholder if not loaded */
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
</style>
<!-- Load Material Icons for the snowflake -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<!-- Mock Header Structure -->
<div class="skinHeader">
<div class="skinHeader-content">
<span>Jellyfin Mock Header</span>
</div>
<div class="headerRight">
<button class="paper-icon-button-light">
<span class="material-icons">person</span>
</button>
</div>
</div>
<div class="seasonals-container"></div>
<!-- Load the main script -->
<script src="seasonals.js"></script>
</body>
</html>

View File

@@ -0,0 +1,498 @@
<!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="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' },
};
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',
'.easter-container', '.resurrection-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

@@ -107,7 +107,7 @@ Click on the following themes to expand them and see the theme in action:
## Ideas for additional seasonals ## Ideas for additional seasonals
If you have any (specific) ideas for additional seasonals, feel free to open an issue or create a pull request. 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
@@ -243,4 +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.

View File

@@ -9,12 +9,20 @@
"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.6.13.4", "version": "1.7.0.15",
"changelog": "- Refactor seasonals.js", "changelog": "- feat: add customizable auto seasonal list via config page\n- feat: add new season theme 'resurrection' by Bioflash257",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.6.13.4/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.0.15/Jellyfin.Plugin.Seasonals.zip",
"checksum": "8e46e1ba0cfc0d2501be33c03effb9e3", "checksum": "d1fc094710efe45ea8cc885bc6a826c4",
"timestamp": "2026-02-04T15:51:33Z" "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", "version": "1.5.1.0",
@@ -107,6 +115,86 @@
"category": "General", "category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "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", "version": "1.4.0.2",
"changelog": "- feat: Add client-side settings feature for selected media bar settings", "changelog": "- feat: Add client-side settings feature for selected media bar settings",