Compare commits
291 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d9a474aae | ||
|
|
db5baa1fd7 | ||
|
|
72ad4ee1a4 | ||
|
|
bb6c7796d5 | ||
|
|
bd8088c52b | ||
|
|
3c1bd01373 | ||
|
|
669ac6d3da | ||
|
|
73f9be91ef | ||
|
|
f14785c54a | ||
|
|
296873f89e | ||
|
|
d6a9ff7176 | ||
|
|
ef15857533 | ||
|
|
19b21ba94f | ||
|
|
8f322fd6cf | ||
|
|
bdc7d2e325 | ||
|
|
8afe397c23 | ||
|
|
30c29d440f | ||
|
|
69adc64a44 | ||
|
|
b0fae10aa1 | ||
|
|
cee4dae769 | ||
|
|
f9aeeadccf | ||
|
|
fc35fcd3c4 | ||
|
|
6a83981e1d | ||
|
|
540d7f9baa | ||
|
|
a162b30bcd | ||
|
|
c6d04b9b3b | ||
|
|
1ceb9cef7f | ||
|
|
eb06a979f6 | ||
|
|
9b6d48a5fe | ||
|
|
e3ea4fa599 | ||
|
|
c5093073d0 | ||
|
|
85cabf29bb | ||
|
|
b008221cf4 | ||
|
|
2bbf13c044 | ||
|
|
082120b70b | ||
|
|
c66ccf970e | ||
|
|
861f431e50 | ||
|
|
be4313d776 | ||
|
|
9b8a563e43 | ||
|
|
8255683714 | ||
|
|
c24abcbd59 | ||
|
|
b17c2a6efe | ||
|
|
ad4fb7964b | ||
|
|
306b0c5e6e | ||
|
|
6cc344e0db | ||
|
|
3ea0709c77 | ||
|
|
b74c8ad2a1 | ||
|
|
8f0c2ac7df | ||
|
|
fa658c0057 | ||
|
|
de7e04c926 | ||
|
|
892be062d3 | ||
|
|
042d89f5b8 | ||
|
|
22709c38d1 | ||
|
|
22d40fb248 | ||
|
|
97dbc09daa | ||
|
|
df29e12699 | ||
|
|
6632cc81de | ||
|
|
437569ec1d | ||
|
|
5c0d8af5d8 | ||
|
|
5b98b442e5 | ||
|
|
e81ce3cab1 | ||
|
|
066ad6fc84 | ||
|
|
8baaa936e1 | ||
|
|
f9b4b3c25d | ||
|
|
f4f472e6ec | ||
|
|
e8effa7dfe | ||
|
|
ff2df0196a | ||
|
|
3e5da3dda2 | ||
|
|
509d198cd0 | ||
|
|
26eb40e282 | ||
|
|
08b2ae987e | ||
|
|
599518d627 | ||
|
|
23c5ab7e9d | ||
|
|
589a360729 | ||
|
|
5c10583601 | ||
|
|
20dcf08bda | ||
|
|
e4b3a132b1 | ||
|
|
63ec6d5e52 | ||
|
|
ec89f2d48d | ||
|
|
61b21de566 | ||
|
|
590f2c3606 | ||
|
|
fdadc00a0c | ||
|
|
2ab88fd5ac | ||
|
|
9a41c0a2ce | ||
|
|
816f58cf02 | ||
|
|
5be9a60eed | ||
|
|
133808105e | ||
|
|
c631aca44f | ||
|
|
241450d132 | ||
|
|
d50d71bde1 | ||
|
|
262dd98519 | ||
|
|
b45ec73a67 | ||
|
|
4e8a37540f | ||
|
|
cde5201991 | ||
|
|
b2420b8eb4 | ||
|
|
dacec7d03c | ||
|
|
65f8261fb7 | ||
|
|
78872e7f96 | ||
|
|
45c9a199c2 | ||
|
|
1df6fb37b1 | ||
|
|
82a1e8a178 | ||
|
|
22bf887d10 | ||
|
|
07600766cf | ||
|
|
56298487f4 | ||
|
|
89fc1c38f0 | ||
|
|
4c168a5ec2 | ||
|
|
92d9e1a9ad | ||
|
|
007e55a612 | ||
|
|
20da9899e4 | ||
|
|
9b9cad1caa | ||
|
|
e8e3424cc9 | ||
|
|
0eeed99508 | ||
|
|
a0f261f597 | ||
|
|
35d92862aa | ||
|
|
693bb35aac | ||
|
|
1ddaab325e | ||
|
|
81facbdb00 | ||
|
|
34a58ac4bd | ||
|
|
2d8444701d | ||
|
|
66f5353659 | ||
|
|
b58264998a | ||
|
|
76c0bc5b3b | ||
|
|
1428db3e1e | ||
|
|
1f5f436e44 | ||
|
|
46f5c3648d | ||
|
|
555e2ab8be | ||
|
|
26eadfc0aa | ||
|
|
142f538939 | ||
|
|
b64e80fd60 | ||
|
|
fbf5fc7edf | ||
|
|
8defba4623 | ||
|
|
7f968ee050 | ||
|
|
dec5bbe39e | ||
|
|
63f3211cc4 | ||
|
|
4270235c78 | ||
|
|
76d8a67914 | ||
|
|
1a3caf5da6 | ||
|
|
3b3ef77e61 | ||
|
|
ba580b1b52 | ||
|
|
0a6284c716 | ||
|
|
f83e863664 | ||
|
|
747e8ed6bc | ||
|
|
30845442b2 | ||
|
|
bb83201736 | ||
|
|
457ae404ba | ||
|
|
b6d679f6ef | ||
|
|
3b88a1809d | ||
|
|
4614ce4a7a | ||
|
|
57840bb149 | ||
|
|
dd90a4630a | ||
|
|
b5d5e5706e | ||
|
|
a4b5cf5b6b | ||
|
|
353bda10df | ||
|
|
0e1b91d93c | ||
|
|
9363008d07 | ||
|
|
faec7d8941 | ||
|
|
7cc70854c4 | ||
|
|
9432f7aa86 | ||
|
|
4f7243bc74 | ||
|
|
ee724fedc8 | ||
|
|
a1dbd4eb12 | ||
|
|
236d8d9e70 | ||
|
|
6d55ae7524 | ||
|
|
99a0613893 | ||
|
|
61952a0af7 | ||
|
|
eca6ba96fb | ||
|
|
c2f0f01689 | ||
|
|
30d17baff4 | ||
|
|
96bb1a3744 | ||
|
|
772a0dae40 | ||
|
|
40c4454397 | ||
|
|
e5915e715a | ||
|
|
c171fc15f5 | ||
|
|
a749b1f98e | ||
|
|
6ccf6201b4 | ||
|
|
a69c741a39 | ||
|
|
d54b4f9b07 | ||
|
|
2cd427b6e9 | ||
|
|
55c1f8b191 | ||
|
|
fc3d6efd1c | ||
|
|
5ba5940e5f | ||
|
|
621b7da344 | ||
|
|
268ce5e307 | ||
|
|
412cc2d981 | ||
|
|
949df24bdb | ||
|
|
b987969200 | ||
|
|
3306bb703d | ||
|
|
6587a4e3d0 | ||
|
|
f794b71f44 | ||
|
|
34363c502a | ||
|
|
add2f7a551 | ||
|
|
1d7e9e27ec | ||
|
|
6459653328 | ||
|
|
9d738e6061 | ||
|
|
8f5a3650e6 | ||
|
|
229f9fe5ab | ||
|
|
0686129590 | ||
|
|
cb0392eb0d | ||
|
|
ed13e05b82 | ||
|
|
310fb4d496 | ||
|
|
78d25106db | ||
|
|
a328171a8a | ||
|
|
361559cbec | ||
|
|
e08bf66a53 | ||
|
|
d6ef81138d | ||
|
|
35f21e680a | ||
|
|
705fbaed9d | ||
|
|
9e52198ef7 | ||
|
|
b1943dfe17 | ||
|
|
c55e900c0f | ||
|
|
503e9addee | ||
|
|
d630fdd217 | ||
|
|
7e4a7c2a6e | ||
|
|
1716a771f3 | ||
|
|
36347cc4b0 | ||
|
|
7f94164e55 | ||
|
|
cbab7de546 | ||
|
|
d0de5cd021 | ||
|
|
16628e9902 | ||
|
|
72bfe0a14a | ||
|
|
6498ec4216 | ||
|
|
0d350fc76b | ||
|
|
2c6e4ce610 | ||
|
|
0c552774dc | ||
|
|
9ab605bb74 | ||
|
|
3d6cba0fe4 | ||
|
|
32e5e2b690 | ||
|
|
c967c1e308 | ||
|
|
ae28d5219b | ||
|
|
e4228f889e | ||
|
|
6d721c755e | ||
|
|
6948953778 | ||
|
|
8a50cef330 | ||
|
|
a0bf5370bd | ||
|
|
c5800b431d | ||
|
|
9a4056651d | ||
|
|
87382db78e | ||
|
|
5d9afa762f | ||
|
|
2f88587dab | ||
|
|
360a959b69 | ||
|
|
36fba545cf | ||
|
|
7faa2cc766 | ||
|
|
aa832e93aa | ||
|
|
86bbeb583d | ||
|
|
7a642b34b8 | ||
|
|
926b30b8ce | ||
|
|
5b672cef42 | ||
|
|
ceccbf4ded | ||
|
|
9cba2a0755 | ||
|
|
af036a9aa4 | ||
|
|
cfefd2d2d3 | ||
|
|
8e7299508b | ||
|
|
fc7aa36f41 | ||
|
|
fc9896048f | ||
|
|
572c4d9ace | ||
|
|
2572e085f6 | ||
|
|
8297f989fd | ||
|
|
636aaa2a4a | ||
|
|
5e70621e93 | ||
|
|
0b4434c51c | ||
|
|
dd6583c055 | ||
|
|
a0b0514159 | ||
|
|
e977c83e8f | ||
|
|
e281f5c579 | ||
|
|
e6b646e478 | ||
|
|
8d6bc12fa4 | ||
|
|
f036e748da | ||
|
|
0177a7caea | ||
|
|
c45e9f6156 | ||
|
|
8dc9b9f157 | ||
|
|
e73c6c14bb | ||
|
|
aa1c60f9ce | ||
|
|
d1e668bcff | ||
|
|
0454d43f32 | ||
|
|
61b3b51139 | ||
|
|
60d1a546a2 | ||
|
|
669933d270 | ||
|
|
053f0ccfa7 | ||
|
|
60e998fc7f | ||
|
|
3aa631198a | ||
|
|
b1876d655e | ||
|
|
9a9f89c1fc | ||
|
|
2fb41f6442 | ||
|
|
4ab949e6d7 | ||
|
|
7d1024c917 | ||
|
|
c2e3d55110 | ||
|
|
c6503a90bb | ||
|
|
b70787d5ec | ||
|
|
a9eb8113a6 | ||
|
|
aaf15d8934 | ||
|
|
ec298ebde0 |
@@ -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"
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -4,5 +4,5 @@ obj/
|
|||||||
.idea/
|
.idea/
|
||||||
artifacts
|
artifacts
|
||||||
|
|
||||||
test-site.html
|
test-site-old.html
|
||||||
RELEASE_GUIDE.md
|
RELEASE_GUIDE.md
|
||||||
343
CONTRIBUTING.md
Normal file
@@ -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 **2–3 components** that live in `Jellyfin.Plugin.Seasonals/Web/`:
|
||||||
|
|
||||||
|
| Component | File(s) | Purpose |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **JavaScript** | `{themeName}.js` | Animation logic, DOM manipulation, element creation |
|
||||||
|
| **CSS** | `{themeName}.css` | Container styling, element appearance, keyframe animations |
|
||||||
|
| **Images** *(optional)* | `{themeName}_images/` | Image assets (PNGs, SVGs) used by the theme |
|
||||||
|
|
||||||
|
The orchestrator file `seasonals.js` manages theme loading at runtime. It reads the plugin configuration, determines which theme should be active, and dynamically injects the correct CSS and JS files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard Theme File Structure
|
||||||
|
|
||||||
|
Here is a complete file layout for a theme called `mytheme`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Jellyfin.Plugin.Seasonals/
|
||||||
|
└── Web/
|
||||||
|
├── mytheme.js # Animation/DOM logic
|
||||||
|
├── mytheme.css # Styles & animations
|
||||||
|
├── mytheme_images/ # (Optional) image assets
|
||||||
|
│ ├── sprite1.png
|
||||||
|
│ └── sprite2.png
|
||||||
|
└── seasonals.js # (Existing) Add your theme to ThemeConfigs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JavaScript File Pattern
|
||||||
|
|
||||||
|
Every theme JS file follows a **consistent skeleton**. Use this as your starting template:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Read Configuration
|
||||||
|
const config = window.SeasonalsPluginConfig?.MyTheme || {};
|
||||||
|
|
||||||
|
const enabled = config.EnableMyTheme !== undefined ? config.EnableMyTheme : true;
|
||||||
|
const elementCount = config.ElementCount || 25;
|
||||||
|
// ... add more config options as needed
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// 2. Toggle Function
|
||||||
|
// Hides the effect when a video player, trailer (in full width mode), dashboard, or user menu is active.
|
||||||
|
function toggleMyTheme() {
|
||||||
|
const container = document.querySelector('.mytheme-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('MyTheme hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('MyTheme visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. MutationObserver
|
||||||
|
// Watches the DOM for changes so the effect can auto-hide/show.
|
||||||
|
const observer = new MutationObserver(toggleMyTheme);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Element Creation
|
||||||
|
// Create and append your animated elements to the container.
|
||||||
|
function createElements() {
|
||||||
|
const container = document.querySelector('.mytheme-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.mytheme-container')) {
|
||||||
|
container.className = 'mytheme-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < elementCount; i++) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'mytheme-element';
|
||||||
|
|
||||||
|
// Set random position, delay, duration, etc.
|
||||||
|
el.style.left = `${Math.random() * 100}%`;
|
||||||
|
el.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 4}s`;
|
||||||
|
|
||||||
|
// If using images:
|
||||||
|
// const img = document.createElement('img');
|
||||||
|
// img.src = '../Seasonals/Resources/mytheme_images/sprite1.png';
|
||||||
|
// el.appendChild(img);
|
||||||
|
|
||||||
|
// If using text/emoji:
|
||||||
|
// el.textContent = '⭐';
|
||||||
|
|
||||||
|
container.appendChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Initialization
|
||||||
|
function initializeMyTheme() {
|
||||||
|
if (!enabled) return;
|
||||||
|
createElements();
|
||||||
|
toggleMyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeMyTheme();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
|
||||||
|
- **Always** read config from `window.SeasonalsPluginConfig?.{ThemeName}`.
|
||||||
|
- **Always** implement the toggle function with the same selectors (`.videoPlayerContainer`, `.youtubePlayerContainer`, `.dashboardDocument`, `#app-user-menu`, just use the above template).
|
||||||
|
- **Always** use `aria-hidden="true"` on the container for accessibility.
|
||||||
|
- Call your `initialize` function at the end of the file.
|
||||||
|
- For **canvas-based** themes (like `snowfall.js`), use a `<canvas>` element with `requestAnimationFrame` instead of CSS animations. Make sure to clean up with `cancelAnimationFrame` when hidden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSS File Pattern
|
||||||
|
|
||||||
|
Every theme CSS file follows this structure:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Container */
|
||||||
|
/* Full-screen overlay, transparent, non-interactive */
|
||||||
|
.mytheme-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none; /* IMPORTANT: don't block user interaction */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated Element */
|
||||||
|
.mytheme-element {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 15;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
/* Two animations: movement + secondary effect (shake, rotate, etc.) */
|
||||||
|
animation-name: mytheme-fall, mytheme-shake;
|
||||||
|
animation-duration: 10s, 3s;
|
||||||
|
animation-timing-function: linear, ease-in-out;
|
||||||
|
animation-iteration-count: infinite, infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyframes */
|
||||||
|
@keyframes mytheme-fall {
|
||||||
|
0% { top: -10%; }
|
||||||
|
100% { top: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mytheme-shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
50% { transform: translateX(80px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Staggered Delays for Base Elements */
|
||||||
|
/* Spread the initial 12 elements across the screen */
|
||||||
|
.mytheme-element:nth-of-type(1) { left: 10%; animation-delay: 1s, 1s; }
|
||||||
|
.mytheme-element:nth-of-type(2) { left: 20%; animation-delay: 6s, 0.5s; }
|
||||||
|
.mytheme-element:nth-of-type(3) { left: 30%; animation-delay: 4s, 2s; }
|
||||||
|
/* ... continue for each base element */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
|
||||||
|
- **Container** must be `position: fixed`, full-screen, with `pointer-events: none` and at least `z-index: 10`.
|
||||||
|
- **Elements** should use `position: fixed` with at least `z-index: 15`.
|
||||||
|
- Use **animations** (eg. primary movement + secondary effect for natural-looking motion).
|
||||||
|
- Include **`nth-of-type` rules** for the initial set of base elements to stagger them.
|
||||||
|
- Include **webkit prefixes** (`-webkit-animation-*`, `@-webkit-keyframes`) for broader compatibility (see existing themes for examples).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Assets (Optional)
|
||||||
|
|
||||||
|
If your theme uses images (e.g., leaves, ghosts, eggs):
|
||||||
|
|
||||||
|
1. Create a folder: `Jellyfin.Plugin.Seasonals/Web/{themeName}_images/`
|
||||||
|
2. Place your assets inside (PNG recommended, keep files small)
|
||||||
|
3. Reference them in JS using the production path:
|
||||||
|
```javascript
|
||||||
|
img.src = '../Seasonals/Resources/mytheme_images/sprite1.png';
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registering Your Theme
|
||||||
|
|
||||||
|
After creating your JS and CSS files, you need to register the theme in two places:
|
||||||
|
|
||||||
|
### 1. `seasonals.js` — Client-Side Registration
|
||||||
|
|
||||||
|
Add your theme to the `ThemeConfigs` object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ThemeConfigs = {
|
||||||
|
// ... existing themes ...
|
||||||
|
mytheme: {
|
||||||
|
css: '../Seasonals/Resources/mytheme.css',
|
||||||
|
js: '../Seasonals/Resources/mytheme.js',
|
||||||
|
containerClass: 'mytheme-container'
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `PluginConfiguration.cs` and `configPage.html` - Server-Side Registration
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The backend registration is handled by the plugin maintainers. You do **not** need to modify C# files for your theme submission. Just focus on the JS/CSS/images.
|
||||||
|
>
|
||||||
|
> However, if you'd like to include full backend integration, add your theme to the enum/configuration in `Configuration/PluginConfiguration.cs` and the selectors in `configPage.html`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Your Theme Locally
|
||||||
|
|
||||||
|
You can test your theme without a Jellyfin server by using the included test site.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. Navigate to the `Jellyfin.Plugin.Seasonals/Web/` directory
|
||||||
|
2. Open [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html) in your browser (just double-click the file) or vscode or what ever you use...
|
||||||
|
3. Use the **theme selector dropdown** to pick an existing theme or select **"Custom (Local Files)"** to test your own
|
||||||
|
4. When "Custom" is selected, enter your theme's JS and CSS filenames (e.g., `mytheme.js` and `mytheme.css` (must be in the same folder as [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html) for this to work))
|
||||||
|
5. Click **"Load Theme"** to apply. Click **"Clear & Reload"** to reset and try again
|
||||||
|
|
||||||
|
### What to Verify
|
||||||
|
|
||||||
|
- ✅ The effect is visible on the background
|
||||||
|
- ✅ The animation runs smoothly
|
||||||
|
- ✅ Elements are spread across the full viewport
|
||||||
|
- ✅ The mock header is **not blocked** by the effect (thanks to `pointer-events: none`)
|
||||||
|
- ✅ No theme related console errors appear (check DevTools → Console)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submitting Your Contribution
|
||||||
|
|
||||||
|
### Pull Request Checklist
|
||||||
|
|
||||||
|
- [ ] Created `{themeName}.js` following the [JS pattern](#javascript-file-pattern)
|
||||||
|
- [ ] Created `{themeName}.css` following the [CSS pattern](#css-file-pattern)
|
||||||
|
- [ ] (If applicable) Created `{themeName}_images/` with optimized assets
|
||||||
|
- [ ] Added theme to `ThemeConfigs` in `seasonals.js`
|
||||||
|
- [ ] Tested locally with [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html)
|
||||||
|
- [ ] No theme related console errors
|
||||||
|
- [ ] Effect has `pointer-events: none` (doesn't block the UI)
|
||||||
|
- [ ] Effect hides during video/trailer playback (toggle function implemented)
|
||||||
|
- [ ] (Optional) Included a screenshot or short recording of the effect to the readme
|
||||||
|
|
||||||
|
### PR Description Template
|
||||||
|
|
||||||
|
```
|
||||||
|
## New Seasonal Theme: {Theme Name}
|
||||||
|
|
||||||
|
**Description:** Brief description of the theme and what occasion/season it's for.
|
||||||
|
|
||||||
|
**Screenshot / Recording:**
|
||||||
|
[Attach a screenshot or GIF showcasing the theme in action]
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- Tested locally with test-site-new.html ✅
|
||||||
|
- No console errors ✅
|
||||||
|
- pointer-events: none verified ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitHub Issue Template for Theme Ideas
|
||||||
|
|
||||||
|
If you have an idea for a seasonal theme but don't want to implement it yourself, feel free to open an issue using the following template:
|
||||||
|
|
||||||
|
**Title:** `[Theme Idea] {Season/Holiday Name} Theme`
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```
|
||||||
|
## 🎨 Theme Idea: {Season/Holiday Name}
|
||||||
|
|
||||||
|
**Occasion/Season:** What time of year is this for?
|
||||||
|
|
||||||
|
**Description:** Describe the visual effect you have in mind.
|
||||||
|
|
||||||
|
**Visual References:** Links to images, GIFs, or videos that capture the aesthetic.
|
||||||
|
|
||||||
|
**Suggested Active Period:** e.g. "March 1 – March 17" for St. Patrick's Day
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have any questions about contributing, feel free to open an issue. Happy theming! 🎉
|
||||||
207
Injector_new.cs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using Jellyfin.Plugin.Seasonals.Helpers;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Seasonals;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the injection of the Seasonals script into the Jellyfin web interface.
|
||||||
|
/// </summary>
|
||||||
|
public class ScriptInjector
|
||||||
|
{
|
||||||
|
private readonly IApplicationPaths _appPaths;
|
||||||
|
private readonly ILogger<ScriptInjector> _logger;
|
||||||
|
public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
|
||||||
|
public const string Marker = "</body>";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ScriptInjector"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appPaths">The application paths.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
|
||||||
|
{
|
||||||
|
_appPaths = appPaths;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Injects the script tag into index.html if it's not already present.
|
||||||
|
/// </summary>
|
||||||
|
public void Inject()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var webPath = GetWebPath();
|
||||||
|
if (string.IsNullOrEmpty(webPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
|
||||||
|
RegisterFileTransformation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexPath = Path.Combine(webPath, "index.html");
|
||||||
|
if (!File.Exists(indexPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
|
||||||
|
RegisterFileTransformation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(indexPath);
|
||||||
|
if (!content.Contains(ScriptTag))
|
||||||
|
{
|
||||||
|
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
content = content.Insert(index, ScriptTag + Environment.NewLine);
|
||||||
|
File.WriteAllText(indexPath, content);
|
||||||
|
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Script already present in index.html. Or could not be injected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
|
||||||
|
RegisterFileTransformation();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
|
||||||
|
RegisterFileTransformation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the script tag from index.html.
|
||||||
|
/// </summary>
|
||||||
|
public void Remove()
|
||||||
|
{
|
||||||
|
UnregisterFileTransformation();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var webPath = GetWebPath();
|
||||||
|
if (string.IsNullOrEmpty(webPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexPath = Path.Combine(webPath, "index.html");
|
||||||
|
if (!File.Exists(indexPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(indexPath);
|
||||||
|
if (content.Contains(ScriptTag))
|
||||||
|
{
|
||||||
|
content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
|
||||||
|
File.WriteAllText(indexPath, content);
|
||||||
|
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
|
||||||
|
} else {
|
||||||
|
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error removing Seasonals script.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the path to the Jellyfin web interface directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The path to the web directory, or null if not found.</returns>
|
||||||
|
private string? GetWebPath()
|
||||||
|
{
|
||||||
|
// Use reflection to access WebPath property to ensure compatibility across different Jellyfin versions
|
||||||
|
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
|
||||||
|
return prop?.GetValue(_appPaths) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterFileTransformation()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Seasonals Fallback. Registering file transformations.");
|
||||||
|
|
||||||
|
List<JObject> payloads = new List<JObject>();
|
||||||
|
|
||||||
|
{
|
||||||
|
JObject payload = new JObject();
|
||||||
|
payload.Add("id", "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||||
|
payload.Add("fileNamePattern", "index.html");
|
||||||
|
payload.Add("callbackAssembly", GetType().Assembly.FullName);
|
||||||
|
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
|
||||||
|
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
|
||||||
|
|
||||||
|
payloads.Add(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assembly? fileTransformationAssembly =
|
||||||
|
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
|
||||||
|
x.FullName?.Contains(".FileTransformation") ?? false);
|
||||||
|
|
||||||
|
if (fileTransformationAssembly != null)
|
||||||
|
{
|
||||||
|
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||||
|
|
||||||
|
if (pluginInterfaceType != null)
|
||||||
|
{
|
||||||
|
foreach (JObject payload in payloads)
|
||||||
|
{
|
||||||
|
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
|
||||||
|
}
|
||||||
|
_logger.LogInformation("File transformations registered successfully.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback injection skipped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterFileTransformation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assembly? fileTransformationAssembly =
|
||||||
|
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
|
||||||
|
x.FullName?.Contains(".FileTransformation") ?? false);
|
||||||
|
|
||||||
|
if (fileTransformationAssembly != null)
|
||||||
|
{
|
||||||
|
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||||
|
|
||||||
|
if (pluginInterfaceType != null)
|
||||||
|
{
|
||||||
|
Guid id = Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||||
|
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
|
||||||
|
_logger.LogInformation("File transformation unregistered successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ public class SeasonalsController : ControllerBase
|
|||||||
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
|
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
|
||||||
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
|
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
|
||||||
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif";
|
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif";
|
||||||
|
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return "image/svg+xml";
|
||||||
return "application/octet-stream";
|
return "application/octet-stream";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
IsEnabled = true;
|
IsEnabled = true;
|
||||||
SelectedSeason = "none";
|
SelectedSeason = "none";
|
||||||
AutomateSeasonSelection = true;
|
AutomateSeasonSelection = true;
|
||||||
|
EnableClientSideToggle = true;
|
||||||
|
|
||||||
Autumn = new AutumnOptions();
|
Autumn = new AutumnOptions();
|
||||||
Snowflakes = new SnowflakesOptions();
|
Snowflakes = new SnowflakesOptions();
|
||||||
@@ -26,6 +27,31 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
Christmas = new ChristmasOptions();
|
Christmas = new ChristmasOptions();
|
||||||
Santa = new SantaOptions();
|
Santa = new SantaOptions();
|
||||||
Easter = new EasterOptions();
|
Easter = new EasterOptions();
|
||||||
|
Resurrection = new ResurrectionOptions();
|
||||||
|
Spring = new SpringOptions();
|
||||||
|
Summer = new SummerOptions();
|
||||||
|
CherryBlossom = new CherryBlossomOptions();
|
||||||
|
Carnival = new CarnivalOptions();
|
||||||
|
Matrix = new MatrixOptions();
|
||||||
|
Eurovision = new EurovisionOptions();
|
||||||
|
Storm = new StormOptions();
|
||||||
|
Pride = new PrideOptions();
|
||||||
|
EarthDay = new EarthDayOptions();
|
||||||
|
Rain = new RainOptions();
|
||||||
|
Frost = new FrostOptions();
|
||||||
|
FilmNoir = new FilmNoirOptions();
|
||||||
|
Oscar = new OscarOptions();
|
||||||
|
MarioDay = new MarioDayOptions();
|
||||||
|
StarWars = new StarWarsOptions();
|
||||||
|
Oktoberfest = new OktoberfestOptions();
|
||||||
|
Friday13 = new Friday13Options();
|
||||||
|
Eid = new EidOptions();
|
||||||
|
Spooky = new SpookyOptions();
|
||||||
|
Sports = new SportsOptions();
|
||||||
|
Olympia = new OlympiaOptions();
|
||||||
|
Space = new SpaceOptions();
|
||||||
|
Underwater = new UnderwaterOptions();
|
||||||
|
Birthday = new BirthdayOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,6 +69,19 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AutomateSeasonSelection { get; set; }
|
public bool AutomateSeasonSelection { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to enable client-side toggle for users.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableClientSideToggle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the seasonal rules configuration as JSON.
|
||||||
|
/// </summary>
|
||||||
|
public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"},{\"Name\":\"Cherry Blossom\",\"StartDay\":1,\"StartMonth\":4,\"EndDay\":30,\"EndMonth\":4,\"Theme\":\"cherryblossom\"}]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Seasonals options.
|
||||||
|
/// </summary>
|
||||||
public AutumnOptions Autumn { get; set; }
|
public AutumnOptions Autumn { get; set; }
|
||||||
public SnowflakesOptions Snowflakes { get; set; }
|
public SnowflakesOptions Snowflakes { get; set; }
|
||||||
public SnowfallOptions Snowfall { get; set; }
|
public SnowfallOptions Snowfall { get; set; }
|
||||||
@@ -53,6 +92,31 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
public ChristmasOptions Christmas { get; set; }
|
public ChristmasOptions Christmas { get; set; }
|
||||||
public SantaOptions Santa { get; set; }
|
public SantaOptions Santa { get; set; }
|
||||||
public EasterOptions Easter { get; set; }
|
public EasterOptions Easter { get; set; }
|
||||||
|
public ResurrectionOptions Resurrection { get; set; }
|
||||||
|
public SpringOptions Spring { get; set; }
|
||||||
|
public SummerOptions Summer { get; set; }
|
||||||
|
public CherryBlossomOptions CherryBlossom { get; set; }
|
||||||
|
public CarnivalOptions Carnival { get; set; }
|
||||||
|
public MatrixOptions Matrix { get; set; }
|
||||||
|
public EurovisionOptions Eurovision { get; set; }
|
||||||
|
public StormOptions Storm { get; set; }
|
||||||
|
public PrideOptions Pride { get; set; }
|
||||||
|
public EarthDayOptions EarthDay { get; set; }
|
||||||
|
public RainOptions Rain { get; set; }
|
||||||
|
public FrostOptions Frost { get; set; }
|
||||||
|
public FilmNoirOptions FilmNoir { get; set; }
|
||||||
|
public OscarOptions Oscar { get; set; }
|
||||||
|
public MarioDayOptions MarioDay { get; set; }
|
||||||
|
public StarWarsOptions StarWars { get; set; }
|
||||||
|
public OktoberfestOptions Oktoberfest { get; set; }
|
||||||
|
public Friday13Options Friday13 { get; set; }
|
||||||
|
public EidOptions Eid { get; set; }
|
||||||
|
public SpookyOptions Spooky { get; set; }
|
||||||
|
public SportsOptions Sports { get; set; }
|
||||||
|
public OlympiaOptions Olympia { get; set; }
|
||||||
|
public SpaceOptions Space { get; set; }
|
||||||
|
public UnderwaterOptions Underwater { get; set; }
|
||||||
|
public BirthdayOptions Birthday { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AutumnOptions
|
public class AutumnOptions
|
||||||
@@ -110,6 +174,8 @@ public class HalloweenOptions
|
|||||||
public bool EnableRandomSymbols { get; set; } = true;
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
public bool EnableDifferentDuration { get; set; } = true;
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableSpiders { get; set; } = true;
|
||||||
|
public bool EnableMice { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HeartsOptions
|
public class HeartsOptions
|
||||||
@@ -157,3 +223,223 @@ public class EasterOptions
|
|||||||
public int MinBunnyRestTime { get; set; } = 2000;
|
public int MinBunnyRestTime { get; set; } = 2000;
|
||||||
public int MaxBunnyRestTime { get; set; } = 5000;
|
public int MaxBunnyRestTime { get; set; } = 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ResurrectionOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 12;
|
||||||
|
public bool EnableResurrection { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpringOptions
|
||||||
|
{
|
||||||
|
public int PollenCount { get; set; } = 30;
|
||||||
|
public int SunbeamCount { get; set; } = 5;
|
||||||
|
public int BirdCount { get; set; } = 4;
|
||||||
|
public int ButterflyCount { get; set; } = 4;
|
||||||
|
public int BeeCount { get; set; } = 2;
|
||||||
|
public int LadybugCount { get; set; } = 2;
|
||||||
|
public bool EnableSpring { get; set; } = true;
|
||||||
|
public bool EnableSpringSunbeams { get; set; } = true;
|
||||||
|
public bool EnableRandomSpring { get; set; } = true;
|
||||||
|
public bool EnableRandomSpringMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SummerOptions
|
||||||
|
{
|
||||||
|
public int BubbleCount { get; set; } = 20;
|
||||||
|
public int DustCount { get; set; } = 50;
|
||||||
|
public bool EnableSummer { get; set; } = true;
|
||||||
|
public bool EnableRandomSummer { get; set; } = true;
|
||||||
|
public bool EnableRandomSummerMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CarnivalOptions
|
||||||
|
{
|
||||||
|
public int ObjectCount { get; set; } = 25;
|
||||||
|
public bool EnableCarnival { get; set; } = true;
|
||||||
|
public bool EnableRandomCarnival { get; set; } = true;
|
||||||
|
public bool EnableRandomCarnivalMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableCarnivalSway { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CherryBlossomOptions
|
||||||
|
{
|
||||||
|
public int PetalCount { get; set; } = 25;
|
||||||
|
public bool EnableCherryBlossom { get; set; } = true;
|
||||||
|
public bool EnableRandomCherryBlossom { get; set; } = true;
|
||||||
|
public bool EnableRandomCherryBlossomMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MatrixOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public bool EnableMatrix { get; set; } = true;
|
||||||
|
public bool EnableRandomMatrix { get; set; } = true;
|
||||||
|
public bool EnableRandomMatrixMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableMatrixBackground { get; set; } = false;
|
||||||
|
public string MatrixChars { get; set; } = "0123456789";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EurovisionOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public bool EnableEurovision { get; set; } = true;
|
||||||
|
public bool EnableRandomEurovision { get; set; } = true;
|
||||||
|
public bool EnableRandomEurovisionMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableColorfulNotes { get; set; } = true;
|
||||||
|
public string EurovisionColors { get; set; } = "#ff0026ff,#17a6ffff,#32d432ff,#FFD700,#f0821bff,#f826f8ff";
|
||||||
|
public int EurovisionGlowSize { get; set; } = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StormOptions
|
||||||
|
{
|
||||||
|
public int RaindropCount { get; set; } = 300;
|
||||||
|
public int RaindropCountMobile { get; set; } = 150;
|
||||||
|
public bool EnableStorm { get; set; } = true;
|
||||||
|
public bool EnableLightning { get; set; } = true;
|
||||||
|
public double RainSpeed { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PrideOptions
|
||||||
|
{
|
||||||
|
public bool EnablePride { get; set; } = true;
|
||||||
|
public int HeartCount { get; set; } = 20;
|
||||||
|
public int HeartSize { get; set; } = 2;
|
||||||
|
public bool ColorHeader { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EarthDayOptions
|
||||||
|
{
|
||||||
|
public bool EnableEarthDay { get; set; } = true;
|
||||||
|
public int VineCount { get; set; } = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RainOptions
|
||||||
|
{
|
||||||
|
public bool EnableRain { get; set; } = true;
|
||||||
|
public int RaindropCount { get; set; } = 300;
|
||||||
|
public int RaindropCountMobile { get; set; } = 150;
|
||||||
|
public double RainSpeed { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FrostOptions
|
||||||
|
{
|
||||||
|
public bool EnableFrost { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FilmNoirOptions
|
||||||
|
{
|
||||||
|
public bool EnableFilmNoir { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OscarOptions
|
||||||
|
{
|
||||||
|
public bool EnableOscar { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MarioDayOptions
|
||||||
|
{
|
||||||
|
public bool EnableMarioDay { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StarWarsOptions
|
||||||
|
{
|
||||||
|
public bool EnableStarWars { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OktoberfestOptions
|
||||||
|
{
|
||||||
|
public bool EnableOktoberfest { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Friday13Options
|
||||||
|
{
|
||||||
|
public bool EnableFriday13 { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EidOptions
|
||||||
|
{
|
||||||
|
public bool EnableEid { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpookyOptions
|
||||||
|
{
|
||||||
|
public bool EnableSpooky { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableSpookySway { get; set; } = true;
|
||||||
|
public int SpookySize { get; set; } = 20;
|
||||||
|
public int SpookyGlowSize { get; set; } = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SportsOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 5;
|
||||||
|
public bool EnableSports { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public string TurfColor { get; set; } = "#228b22";
|
||||||
|
public string SportsBalls { get; set; } = "football,basketball,tennis,volleyball";
|
||||||
|
public bool EnableTrophy { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OlympiaOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public bool EnableOlympia { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpaceOptions
|
||||||
|
{
|
||||||
|
public int PlanetCount { get; set; } = 12;
|
||||||
|
public int AstronautCount { get; set; } = 5;
|
||||||
|
public int SatelliteCount { get; set; } = 2;
|
||||||
|
public int IssCount { get; set; } = 1;
|
||||||
|
public int RocketCount { get; set; } = 1;
|
||||||
|
public bool EnableSpace { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UnderwaterOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 15;
|
||||||
|
public bool EnableUnderwater { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableLightRays { get; set; } = true;
|
||||||
|
public int SeaweedCount { get; set; } = 30;
|
||||||
|
public int CrabCount { get; set; } = 2;
|
||||||
|
public int StarfishCount { get; set; } = 2;
|
||||||
|
public int ShellCount { get; set; } = 2;
|
||||||
|
public int FishCount { get; set; } = 15;
|
||||||
|
public int SeahorseCount { get; set; } = 3;
|
||||||
|
public int JellyfishCount { get; set; } = 3;
|
||||||
|
public int TurtleCount { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BirthdayOptions
|
||||||
|
{
|
||||||
|
public int SymbolCount { get; set; } = 5;
|
||||||
|
public int ConfettiCount { get; set; } = 60;
|
||||||
|
public bool EnableBirthday { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbols { get; set; } = true;
|
||||||
|
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableGarland { get; set; } = true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||||
<Title>Jellyfin Seasonals Plugin</Title>
|
<Title>Jellyfin Seasonals Plugin</Title>
|
||||||
<Authors>CodeDevMLH</Authors>
|
<Authors>CodeDevMLH</Authors>
|
||||||
<Version>1.5.0.0</Version>
|
<Version>2.0.0.1</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" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class ScriptInjector
|
|||||||
{
|
{
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly ILogger<ScriptInjector> _logger;
|
private readonly ILogger<ScriptInjector> _logger;
|
||||||
public const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
|
public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
|
||||||
public const string Marker = "</body>";
|
public const string Marker = "</body>";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,6 +56,18 @@ public class ScriptInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
var content = File.ReadAllText(indexPath);
|
var content = File.ReadAllText(indexPath);
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Legacy Tags, remove in future versions
|
||||||
|
bool modified = false;
|
||||||
|
// Cleanup legacy tags first to avoid duplicates or conflicts
|
||||||
|
content = RemoveLegacyTags(content, ref modified);
|
||||||
|
if (modified)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Removed legacy tags from index.html.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!content.Contains(ScriptTag))
|
if (!content.Contains(ScriptTag))
|
||||||
{
|
{
|
||||||
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -113,6 +125,17 @@ public class ScriptInjector
|
|||||||
} else {
|
} else {
|
||||||
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Legacy Tags, remove in future versions
|
||||||
|
// Remove legacy tags
|
||||||
|
bool modified = false;
|
||||||
|
content = RemoveLegacyTags(content, ref modified);
|
||||||
|
if (modified)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Removed legacy tags from index.html.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
@@ -204,4 +227,21 @@ public class ScriptInjector
|
|||||||
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Legacy Tags, remove in future versions
|
||||||
|
/// <summary>
|
||||||
|
/// Removes legacy script tags from the content.
|
||||||
|
/// </summary>
|
||||||
|
private string RemoveLegacyTags(string content, ref bool modified)
|
||||||
|
{
|
||||||
|
// Legacy tags (used in versions prior to 1.6.3.0 where paths started with / instead of ../)
|
||||||
|
const string LegacyScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
|
||||||
|
|
||||||
|
if (content.Contains(LegacyScriptTag))
|
||||||
|
{
|
||||||
|
content = content.Replace(LegacyScriptTag + Environment.NewLine, "").Replace(LegacyScriptTag, "");
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1343
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW.svg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_192x192.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_96x96.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -1,161 +1,139 @@
|
|||||||
.autumn-container {
|
.autumn-container {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
contain: layout paint;
|
||||||
|
}
|
||||||
.leaf {
|
|
||||||
position: fixed;
|
.leaf {
|
||||||
z-index: 15;
|
position: fixed;
|
||||||
top: -10%;
|
z-index: 15;
|
||||||
font-size: 1em;
|
top: 0;
|
||||||
color: #fff;
|
will-change: transform;
|
||||||
font-family: Arial, sans-serif;
|
translate: 0 -10vh;
|
||||||
text-shadow: 0 0 5px #000;
|
font-size: 1em;
|
||||||
user-select: none;
|
color: #fff;
|
||||||
-webkit-user-select: none;
|
font-family: Arial, sans-serif;
|
||||||
cursor: default;
|
text-shadow: 0 0 5px #000;
|
||||||
-webkit-animation-name: leaf-fall, leaf-shake;
|
user-select: none;
|
||||||
-webkit-animation-duration: 7s, 4s;
|
cursor: default;
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
animation-name: leaf-fall, leaf-shake;
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
animation-duration: 7s, 4s;
|
||||||
-webkit-user-select: none;
|
animation-timing-function: linear, ease-in-out;
|
||||||
animation-name: leaf-fall, leaf-shake;
|
animation-iteration-count: infinite, infinite;
|
||||||
animation-duration: 7s, 4s;
|
}
|
||||||
animation-timing-function: linear, ease-in-out;
|
|
||||||
animation-iteration-count: infinite, infinite;
|
/* Class to disable rotation */
|
||||||
}
|
.no-rotation {
|
||||||
|
--rotate-start: 0deg !important;
|
||||||
/* Class to disable rotation */
|
--rotate-end: 0deg !important;
|
||||||
.no-rotation {
|
}
|
||||||
--rotate-start: 0deg !important;
|
|
||||||
--rotate-end: 0deg !important;
|
|
||||||
}
|
@keyframes leaf-fall {
|
||||||
|
0% {
|
||||||
@-webkit-keyframes leaf-fall {
|
translate: 0 -10vh;
|
||||||
0% {
|
}
|
||||||
top: -10%;
|
|
||||||
}
|
100% {
|
||||||
|
translate: 0 100vh;
|
||||||
100% {
|
}
|
||||||
top: 100%;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@keyframes leaf-shake {
|
||||||
@keyframes leaf-fall {
|
0%, 100% {
|
||||||
0% {
|
transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
||||||
top: -10%;
|
}
|
||||||
}
|
50% {
|
||||||
|
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
||||||
100% {
|
}
|
||||||
top: 100%;
|
}
|
||||||
}
|
|
||||||
}
|
.leaf:nth-of-type(0) {
|
||||||
|
left: 0%;
|
||||||
@-webkit-keyframes leaf-shake {
|
animation-delay: 0s, 0s;
|
||||||
0%, 100% {
|
--rotate-start: -25deg;
|
||||||
-webkit-transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
--rotate-end: 22deg;
|
||||||
}
|
}
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
.leaf:nth-of-type(1) {
|
||||||
}
|
left: 10%;
|
||||||
}
|
animation-delay: 1s, 0.5s;
|
||||||
|
--rotate-start: -32deg;
|
||||||
@keyframes leaf-shake {
|
--rotate-end: 35deg;
|
||||||
0%, 100% {
|
}
|
||||||
transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
|
||||||
}
|
.leaf:nth-of-type(2) {
|
||||||
50% {
|
left: 20%;
|
||||||
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
animation-delay: 6s, 1s;
|
||||||
}
|
--rotate-start: -28deg;
|
||||||
}
|
--rotate-end: 28deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(0) {
|
|
||||||
left: 0%;
|
.leaf:nth-of-type(3) {
|
||||||
animation-delay: 0s, 0s;
|
left: 30%;
|
||||||
--rotate-start: -25deg;
|
animation-delay: 4s, 1.5s;
|
||||||
--rotate-end: 22deg;
|
--rotate-start: -38deg;
|
||||||
}
|
--rotate-end: 32deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(1) {
|
|
||||||
left: 10%;
|
.leaf:nth-of-type(4) {
|
||||||
animation-delay: 1s, 0.5s;
|
left: 40%;
|
||||||
--rotate-start: -32deg;
|
animation-delay: 2s, 0.8s;
|
||||||
--rotate-end: 35deg;
|
--rotate-start: -22deg;
|
||||||
}
|
--rotate-end: 38deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(2) {
|
|
||||||
left: 20%;
|
.leaf:nth-of-type(5) {
|
||||||
animation-delay: 6s, 1s;
|
left: 50%;
|
||||||
--rotate-start: -28deg;
|
animation-delay: 8s, 2s;
|
||||||
--rotate-end: 28deg;
|
--rotate-start: -35deg;
|
||||||
}
|
--rotate-end: 25deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(3) {
|
|
||||||
left: 30%;
|
.leaf:nth-of-type(6) {
|
||||||
animation-delay: 4s, 1.5s;
|
left: 60%;
|
||||||
--rotate-start: -38deg;
|
animation-delay: 6s, 1.2s;
|
||||||
--rotate-end: 32deg;
|
--rotate-start: -40deg;
|
||||||
}
|
--rotate-end: 40deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(4) {
|
|
||||||
left: 40%;
|
.leaf:nth-of-type(7) {
|
||||||
animation-delay: 2s, 0.8s;
|
left: 70%;
|
||||||
--rotate-start: -22deg;
|
animation-delay: 2.5s, 0.3s;
|
||||||
--rotate-end: 38deg;
|
--rotate-start: -30deg;
|
||||||
}
|
--rotate-end: 30deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(5) {
|
|
||||||
left: 50%;
|
.leaf:nth-of-type(8) {
|
||||||
animation-delay: 8s, 2s;
|
left: 80%;
|
||||||
--rotate-start: -35deg;
|
animation-delay: 1s, 1.8s;
|
||||||
--rotate-end: 25deg;
|
--rotate-start: -26deg;
|
||||||
}
|
--rotate-end: 36deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(6) {
|
|
||||||
left: 60%;
|
.leaf:nth-of-type(9) {
|
||||||
animation-delay: 6s, 1.2s;
|
left: 90%;
|
||||||
--rotate-start: -40deg;
|
animation-delay: 3s, 0.7s;
|
||||||
--rotate-end: 40deg;
|
--rotate-start: -34deg;
|
||||||
}
|
--rotate-end: 24deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(7) {
|
|
||||||
left: 70%;
|
.leaf:nth-of-type(10) {
|
||||||
animation-delay: 2.5s, 0.3s;
|
left: 25%;
|
||||||
--rotate-start: -30deg;
|
animation-delay: 2s, 2.3s;
|
||||||
--rotate-end: 30deg;
|
--rotate-start: -29deg;
|
||||||
}
|
--rotate-end: 33deg;
|
||||||
|
}
|
||||||
.leaf:nth-of-type(8) {
|
|
||||||
left: 80%;
|
.leaf:nth-of-type(11) {
|
||||||
animation-delay: 1s, 1.8s;
|
left: 65%;
|
||||||
--rotate-start: -26deg;
|
animation-delay: 4s, 1.4s;
|
||||||
--rotate-end: 36deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(9) {
|
|
||||||
left: 90%;
|
|
||||||
animation-delay: 3s, 0.7s;
|
|
||||||
--rotate-start: -34deg;
|
|
||||||
--rotate-end: 24deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(10) {
|
|
||||||
left: 25%;
|
|
||||||
animation-delay: 2s, 2.3s;
|
|
||||||
--rotate-start: -29deg;
|
|
||||||
--rotate-end: 33deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(11) {
|
|
||||||
left: 65%;
|
|
||||||
animation-delay: 4s, 1.4s;
|
|
||||||
--rotate-start: -37deg;
|
|
||||||
--rotate-end: 27deg;
|
|
||||||
--rotate-start: -37deg;
|
--rotate-start: -37deg;
|
||||||
@@ -7,6 +7,25 @@ const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? co
|
|||||||
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
|
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
|
||||||
const leafCount = config.LeafCount || 25; // count of random extra leaves
|
const leafCount = config.LeafCount || 25; // count of random extra leaves
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
"../Seasonals/Resources/autumn_images/acorn1.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/acorn2.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf1.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf2.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf3.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf4.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf5.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf6.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf7.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf8.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf9.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf10.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf11.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf12.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf13.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf14.png",
|
||||||
|
"../Seasonals/Resources/autumn_images/leaf15.png",
|
||||||
|
];
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
@@ -38,35 +57,13 @@ function toggleAutumn() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleAutumn);
|
const observer = new MutationObserver(toggleAutumn);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const images = [
|
|
||||||
"/Seasonals/Resources/autumn_images/acorn1.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/acorn2.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf1.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf2.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf3.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf4.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf5.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf6.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf7.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf8.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf9.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf10.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf11.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf12.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf13.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf14.png",
|
|
||||||
"/Seasonals/Resources/autumn_images/leaf15.png",
|
|
||||||
];
|
|
||||||
|
|
||||||
function addRandomLeaves(count) {
|
function addRandomLeaves(count) {
|
||||||
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||||
if (!autumnContainer) return; // exit if leave container is not found
|
if (!autumnContainer) return; // exit if leave container is not found
|
||||||
@@ -90,7 +87,7 @@ function addRandomLeaves(count) {
|
|||||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||||
const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s)
|
const randomAnimationDelay = Math.random() * 12; // delay for fall (0s to 12s)
|
||||||
const randomAnimationDelay2 = Math.random() * 4; // delay for shake+rotate (0s to 4s)
|
const randomAnimationDelay2 = -(Math.random() * 4); // delay for shake+rotate (-4s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
leaveDiv.style.left = `${randomLeft}%`;
|
leaveDiv.style.left = `${randomLeft}%`;
|
||||||
|
|||||||
155
Jellyfin.Plugin.Seasonals/Web/birthday.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
.birthday-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: strict;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-symbol {
|
||||||
|
will-change: opacity;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: birthday-rise linear infinite forwards;
|
||||||
|
opacity: 0.95;
|
||||||
|
z-index: 40;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-sway {
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: birthday-sway;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-inner {
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: crosshair;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MARK: Balloon Size */
|
||||||
|
.birthday-symbol img {
|
||||||
|
width: 18vh;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-confetti-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 30;
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: birthday-confetti-fall;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-confetti {
|
||||||
|
width: 8px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: birthday-flutter;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-confetti.circle {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-confetti.square {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-confetti.triangle {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-rise {
|
||||||
|
0% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0) rotate(var(--start-rot, 0deg)); opacity: 0; }
|
||||||
|
10% { opacity: 1; }
|
||||||
|
90% { opacity: 1; }
|
||||||
|
100% { transform: translate3d(var(--x-pos, 0vw), -20vh, 0) rotate(calc(var(--start-rot, 0deg) * -1)); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-confetti-fall {
|
||||||
|
0% { transform: translate3d(var(--x-pos, 0vw), -10vh, 0); }
|
||||||
|
100% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-sway {
|
||||||
|
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
|
||||||
|
100% { transform: translateX(var(--sway-amount, 50px)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-flutter {
|
||||||
|
0% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg); }
|
||||||
|
100% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-pop {
|
||||||
|
0% { transform: scale(1); opacity: 1; filter: brightness(1); }
|
||||||
|
30% { transform: scale(1.3); opacity: 1; filter: brightness(1.5); }
|
||||||
|
100% { transform: scale(0); opacity: 0; filter: brightness(2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-burst-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
animation: birthday-burst-y 1.2s cubic-bezier(0.42, 0, 1, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-burst-confetti {
|
||||||
|
will-change: transform;
|
||||||
|
animation: birthday-burst-x 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-burst-confetti.circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthday-burst-confetti.triangle {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-burst-y {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(calc(var(--burst-y) + 150px));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes birthday-burst-x {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0) rotate3d(var(--rx), var(--ry), var(--rz), 0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(var(--burst-x) * 1.5)) rotate3d(var(--rx), var(--ry), var(--rz), var(--rot-dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
331
Jellyfin.Plugin.Seasonals/Web/birthday.js
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Birthday || {};
|
||||||
|
|
||||||
|
const birthday = config.EnableBirthday !== undefined ? config.EnableBirthday : true;
|
||||||
|
const symbolCount = config.SymbolCount || 5;
|
||||||
|
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||||
|
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||||
|
|
||||||
|
|
||||||
|
const birthdayImages = [
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_blue.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_green.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_lightblue.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_orange.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_pink.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_red.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_yellow.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_turquoise.gif',
|
||||||
|
'../Seasonals/Resources/birthday_assets/balloon_violet.gif'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const balloonColors = {
|
||||||
|
'balloon_blue': ['#3498db', '#2980b9', '#1f618d'],
|
||||||
|
'balloon_green': ['#2ecc71', '#27ae60', '#1e8449'],
|
||||||
|
'balloon_lightblue': ['#36c5f0', '#81ecec', '#00cec9'],
|
||||||
|
'balloon_orange': ['#e67e22', '#d35400', '#a04000'],
|
||||||
|
'balloon_pink': ['#ff726d', '#f4306d', '#e84393'],
|
||||||
|
'balloon_red': ['#e74c3c', '#c0392b', '#922b21'],
|
||||||
|
'balloon_yellow': ['#f1c40f', '#f39c12', '#b7950b'],
|
||||||
|
'balloon_turquoise': ['#36c5f0', '#81ecec', '#00cec9'],
|
||||||
|
'balloon_violet': ['#9b59b6', '#8e44ad', '#6c3483']
|
||||||
|
};
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleBirthday() {
|
||||||
|
const container = document.querySelector('.birthday-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Birthday hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Birthday visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleBirthday);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createBalloonPopConfetti(container, x, y, colors) {
|
||||||
|
const popConfettiColors = colors || [
|
||||||
|
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||||
|
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||||
|
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Spawn 15-20 particles
|
||||||
|
const particleCount = Math.floor(Math.random() * 5) + 15;
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'birthday-burst-wrapper';
|
||||||
|
wrapper.style.position = 'absolute';
|
||||||
|
wrapper.style.left = `${x}px`;
|
||||||
|
wrapper.style.top = `${y}px`;
|
||||||
|
wrapper.style.zIndex = '1000';
|
||||||
|
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.classList.add('birthday-burst-confetti');
|
||||||
|
|
||||||
|
// Random color
|
||||||
|
const color = popConfettiColors[Math.floor(Math.random() * popConfettiColors.length)];
|
||||||
|
particle.style.backgroundColor = color;
|
||||||
|
|
||||||
|
// Random shape
|
||||||
|
const shape = Math.random();
|
||||||
|
if (shape > 0.66) {
|
||||||
|
particle.classList.add('circle');
|
||||||
|
const size = Math.random() * 4 + 4; // 4-8px
|
||||||
|
particle.style.width = `${size}px`;
|
||||||
|
particle.style.height = `${size}px`;
|
||||||
|
} else if (shape > 0.33) {
|
||||||
|
particle.classList.add('rect');
|
||||||
|
const width = Math.random() * 3 + 3; // 3-6px
|
||||||
|
const height = Math.random() * 4 + 6; // 6-10px
|
||||||
|
particle.style.width = `${width}px`;
|
||||||
|
particle.style.height = `${height}px`;
|
||||||
|
} else {
|
||||||
|
particle.classList.add('triangle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random direction for explosion (circular)
|
||||||
|
const angle = Math.random() * 2 * Math.PI;
|
||||||
|
const distance = Math.random() * 60 + 20; // 20-80px burst radius
|
||||||
|
|
||||||
|
const xOffset = Math.cos(angle) * distance;
|
||||||
|
const yOffset = Math.sin(angle) * distance;
|
||||||
|
|
||||||
|
particle.style.setProperty('--burst-x', `${xOffset}px`);
|
||||||
|
wrapper.style.setProperty('--burst-y', `${yOffset}px`);
|
||||||
|
|
||||||
|
// Random rotation during fall
|
||||||
|
particle.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
|
||||||
|
particle.style.setProperty('--rx', Math.random().toFixed(2));
|
||||||
|
particle.style.setProperty('--ry', Math.random().toFixed(2));
|
||||||
|
particle.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||||
|
|
||||||
|
wrapper.appendChild(particle);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
// Remove particle after animation
|
||||||
|
setTimeout(() => wrapper.remove(), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBirthday() {
|
||||||
|
const container = document.querySelector('.birthday-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.birthday-container')) {
|
||||||
|
container.className = 'birthday-container';
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cake and Garland have been removed
|
||||||
|
|
||||||
|
const standardCount = 15;
|
||||||
|
const totalSymbols = symbolCount + standardCount;
|
||||||
|
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
let finalCount = totalSymbols;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
finalCount = enableRandomMobile ? totalSymbols : standardCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useRandomDuration = enableDifferentDuration !== false;
|
||||||
|
|
||||||
|
// Arrays moved to top of file
|
||||||
|
|
||||||
|
for (let i = 0; i < finalCount; i++) {
|
||||||
|
let symbol = document.createElement('div');
|
||||||
|
|
||||||
|
const randomImage = birthdayImages[Math.floor(Math.random() * birthdayImages.length)];
|
||||||
|
const randomItem = randomImage.split('/').pop().split('.')[0]; // Extracts "balloon_blue"
|
||||||
|
symbol.className = `birthday-symbol birthday-${randomItem}`;
|
||||||
|
|
||||||
|
// Create inner div for sway
|
||||||
|
let innerDiv = document.createElement('div');
|
||||||
|
innerDiv.className = 'birthday-inner';
|
||||||
|
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = randomImage;
|
||||||
|
img.onerror = function() {
|
||||||
|
symbol.remove(); // Remove element completely on error
|
||||||
|
};
|
||||||
|
innerDiv.appendChild(img);
|
||||||
|
|
||||||
|
// Sway wrapper
|
||||||
|
let swayWrapper = document.createElement('div');
|
||||||
|
swayWrapper.className = 'birthday-sway';
|
||||||
|
const swayDuration = Math.random() * 3 + 3; // 3-6s per cycle
|
||||||
|
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||||
|
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||||
|
const swayAmount = Math.random() * 60 + 20; // 20-80px
|
||||||
|
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||||
|
|
||||||
|
swayWrapper.appendChild(innerDiv);
|
||||||
|
symbol.appendChild(swayWrapper);
|
||||||
|
|
||||||
|
const leftPos = Math.random() * 95;
|
||||||
|
|
||||||
|
// Far away effect
|
||||||
|
const depth = Math.random();
|
||||||
|
// MARK: balloon size
|
||||||
|
const scale = 0.85 + depth * 0.3; // 0.85 to 1.15
|
||||||
|
const zIndex = Math.floor(depth * 30) + 10;
|
||||||
|
|
||||||
|
img.style.transform = `scale(${scale})`;
|
||||||
|
symbol.style.zIndex = zIndex;
|
||||||
|
|
||||||
|
let durationSeconds = 9;
|
||||||
|
if (useRandomDuration) {
|
||||||
|
// Far strings climb slower
|
||||||
|
durationSeconds = (1 - depth) * 6 + 7 + Math.random() * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative delay correctly scatters them initially across the screen vertically
|
||||||
|
// avoiding them all popping up at bottom edge together
|
||||||
|
const delaySeconds = -(Math.random() * durationSeconds);
|
||||||
|
|
||||||
|
const isBalloon = randomItem.startsWith('balloon');
|
||||||
|
|
||||||
|
if (isBalloon) {
|
||||||
|
// Sway animation is now handled natively by the GIF motion.
|
||||||
|
|
||||||
|
// Interaction to pop is handled visually by the GIF, but we can still remove it on hover
|
||||||
|
innerDiv.addEventListener('mouseenter', function(e) {
|
||||||
|
if (!this.classList.contains('popped')) {
|
||||||
|
this.classList.add('popped');
|
||||||
|
this.style.animation = 'birthday-pop 0.2s ease-out forwards';
|
||||||
|
this.style.pointerEvents = 'none'; // avoid re-triggering
|
||||||
|
|
||||||
|
// Create confetti burst at balloon's screen position
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
const cx = rect.left + rect.width / 2;
|
||||||
|
// explosion height
|
||||||
|
const cy = rect.top + rect.height * -0.05;
|
||||||
|
// Ensure the burst container is appended to the main document body or the birthday container
|
||||||
|
createBalloonPopConfetti(document.body, cx, cy, balloonColors[randomItem]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the balloon when it reappears at the bottom of the screen
|
||||||
|
symbol.addEventListener('animationiteration', function(e) {
|
||||||
|
// Ignore bubbling events from the inner sway animation
|
||||||
|
if (e.animationName === 'birthday-rise' || e.target === symbol) {
|
||||||
|
if (innerDiv.classList.contains('popped')) {
|
||||||
|
innerDiv.classList.remove('popped');
|
||||||
|
innerDiv.style.animation = '';
|
||||||
|
innerDiv.style.pointerEvents = 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const startRot = (Math.random() * 20) - 10; // -10 to +10 spread
|
||||||
|
symbol.style.setProperty('--start-rot', `${startRot}deg`);
|
||||||
|
symbol.style.setProperty('--x-pos', `${leftPos}vw`);
|
||||||
|
|
||||||
|
symbol.style.animationDuration = `${durationSeconds}s`;
|
||||||
|
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||||
|
|
||||||
|
container.appendChild(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Party Confetti
|
||||||
|
const baseConfettiCount = config.ConfettiCount !== undefined ? config.ConfettiCount : 60;
|
||||||
|
const confettiCount = isMobile ? Math.floor(baseConfettiCount / 2) : baseConfettiCount;
|
||||||
|
const allColors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
|
||||||
|
|
||||||
|
for (let i = 0; i < confettiCount; i++) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.classList.add('birthday-confetti-wrapper');
|
||||||
|
|
||||||
|
// Use carnival.js 3D advanced fluttering logic
|
||||||
|
let swayWrapper = document.createElement('div');
|
||||||
|
swayWrapper.classList.add('birthday-sway');
|
||||||
|
wrapper.appendChild(swayWrapper);
|
||||||
|
|
||||||
|
const confetti = document.createElement('div');
|
||||||
|
confetti.classList.add('birthday-confetti');
|
||||||
|
|
||||||
|
const color = allColors[Math.floor(Math.random() * allColors.length)];
|
||||||
|
confetti.style.backgroundColor = color;
|
||||||
|
|
||||||
|
// Shape assignments
|
||||||
|
const shape = Math.random();
|
||||||
|
if (shape > 0.8) confetti.classList.add('circle');
|
||||||
|
else if (shape > 0.6) confetti.classList.add('square');
|
||||||
|
else if (shape > 0.4) confetti.classList.add('triangle');
|
||||||
|
else confetti.classList.add('rect'); // default
|
||||||
|
|
||||||
|
// Sizing
|
||||||
|
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
|
||||||
|
const width = Math.random() * 3 + 4; // 4-7px
|
||||||
|
const height = Math.random() * 5 + 8; // 8-13px
|
||||||
|
confetti.style.width = `${width}px`;
|
||||||
|
confetti.style.height = `${height}px`;
|
||||||
|
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
|
||||||
|
const size = Math.random() * 5 + 5; // 5-10px
|
||||||
|
confetti.style.width = `${size}px`;
|
||||||
|
confetti.style.height = `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Math.random() * 5 + 5;
|
||||||
|
const delay = -Math.random() * duration; // Spawn fully integrated across screen width/height
|
||||||
|
|
||||||
|
wrapper.style.setProperty('--x-pos', `${Math.random() * 100}vw`);
|
||||||
|
wrapper.style.animationDelay = `${delay}s`;
|
||||||
|
wrapper.style.animationDuration = `${duration}s`;
|
||||||
|
|
||||||
|
// Sway handling
|
||||||
|
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
|
||||||
|
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||||
|
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||||
|
const swayAmount = Math.random() * 70 + 30; // 30-100px
|
||||||
|
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||||
|
|
||||||
|
// 3D Flutter Rotation
|
||||||
|
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
|
||||||
|
confetti.style.setProperty('--rx', Math.random().toFixed(2));
|
||||||
|
confetti.style.setProperty('--ry', Math.random().toFixed(2));
|
||||||
|
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||||
|
const rotDir = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
|
||||||
|
|
||||||
|
swayWrapper.appendChild(confetti);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Removed fallback logic */
|
||||||
|
|
||||||
|
function initializeBirthday() {
|
||||||
|
if (!birthday) return;
|
||||||
|
createBirthday();
|
||||||
|
toggleBirthday();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeBirthday();
|
||||||
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_blue.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_green.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_orange.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_pink.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_red.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_violet.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/birthday_assets/balloon_yellow.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
86
Jellyfin.Plugin.Seasonals/Web/carnival.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
.carnival-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
perspective: 600px;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carnival-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 15;
|
||||||
|
top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: carnival-fall;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carnival-sway-wrapper {
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: carnival-sway;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carnival-confetti {
|
||||||
|
width: 8px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: carnival-flutter;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carnival-confetti.circle {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carnival-confetti.square {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carnival-confetti.triangle {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carnival-fall {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, -10vh, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 110vh, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carnival-sway {
|
||||||
|
0% {
|
||||||
|
transform: translateX(calc(var(--sway-amount, 50px) * -1));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(var(--sway-amount, 50px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes carnival-flutter {
|
||||||
|
0% {
|
||||||
|
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
|
||||||
|
}
|
||||||
|
}
|
||||||
191
Jellyfin.Plugin.Seasonals/Web/carnival.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Carnival || {};
|
||||||
|
|
||||||
|
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // Enable/disable carnival
|
||||||
|
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true; // Enable random carnival objects
|
||||||
|
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false; // Enable random carnival objects on mobile
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // Randomize falling and flutter speeds
|
||||||
|
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // Enable side-to-side sway animation
|
||||||
|
const carnivalCount = config.ObjectCount || 120; // Number of confetti pieces to spawn
|
||||||
|
|
||||||
|
const confettiColors = [
|
||||||
|
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||||
|
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||||
|
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||||
|
];
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// function to check and control the carnival animation
|
||||||
|
function toggleCarnival() {
|
||||||
|
const carnivalContainer = document.querySelector('.carnival-container');
|
||||||
|
if (!carnivalContainer) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
// hide carnival if video/trailer player is active or dashboard is visible
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
carnivalContainer.style.display = 'none'; // hide carnival
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Carnival hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
carnivalContainer.style.display = 'block'; // show carnival
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Carnival visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// observe changes in the DOM
|
||||||
|
const observer = new MutationObserver(toggleCarnival);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createConfettiPiece(container, isInitial = false) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.classList.add('carnival-wrapper');
|
||||||
|
|
||||||
|
let swayWrapper = wrapper;
|
||||||
|
|
||||||
|
if (enableSway) {
|
||||||
|
swayWrapper = document.createElement('div');
|
||||||
|
swayWrapper.classList.add('carnival-sway-wrapper');
|
||||||
|
wrapper.appendChild(swayWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
const confetti = document.createElement('div');
|
||||||
|
confetti.classList.add('carnival-confetti');
|
||||||
|
|
||||||
|
// Random color
|
||||||
|
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
|
||||||
|
confetti.style.backgroundColor = color;
|
||||||
|
|
||||||
|
// Random shape
|
||||||
|
const shape = Math.random();
|
||||||
|
if (shape > 0.8) {
|
||||||
|
confetti.classList.add('circle');
|
||||||
|
} else if (shape > 0.6) {
|
||||||
|
confetti.classList.add('square');
|
||||||
|
} else if (shape > 0.4) {
|
||||||
|
confetti.classList.add('triangle');
|
||||||
|
} else {
|
||||||
|
confetti.classList.add('rect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random position
|
||||||
|
wrapper.style.left = `${Math.random() * 100}%`;
|
||||||
|
|
||||||
|
// MARK: CONFETTI SIZE (RECTANGLES)
|
||||||
|
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
|
||||||
|
const width = Math.random() * 3 + 4; // 4-7px
|
||||||
|
const height = Math.random() * 5 + 8; // 8-13px
|
||||||
|
confetti.style.width = `${width}px`;
|
||||||
|
confetti.style.height = `${height}px`;
|
||||||
|
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
|
||||||
|
// MARK: CONFETTI SIZE (CIRCLES/SQUARES)
|
||||||
|
const size = Math.random() * 5 + 5; // 5-10px
|
||||||
|
confetti.style.width = `${size}px`;
|
||||||
|
confetti.style.height = `${size}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: CONFETTI FALLING SPEED (in seconds)
|
||||||
|
const duration = Math.random() * 5 + 5;
|
||||||
|
|
||||||
|
let delay = 0;
|
||||||
|
if (isInitial) {
|
||||||
|
delay = -Math.random() * duration;
|
||||||
|
} else {
|
||||||
|
delay = Math.random() * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.style.animationDelay = `${delay}s`;
|
||||||
|
wrapper.style.animationDuration = `${duration}s`;
|
||||||
|
|
||||||
|
if (enableSway) {
|
||||||
|
// Random sway duration
|
||||||
|
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
|
||||||
|
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||||
|
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||||
|
|
||||||
|
// MARK: SWAY DISTANCE RANGE (in px)
|
||||||
|
const swayAmount = Math.random() * 70 + 30; // 30-100px
|
||||||
|
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: CONFETTI FLUTTER ROTATION SPEED
|
||||||
|
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
|
||||||
|
confetti.style.setProperty('--rx', Math.random().toFixed(2));
|
||||||
|
confetti.style.setProperty('--ry', Math.random().toFixed(2));
|
||||||
|
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||||
|
|
||||||
|
// Random direction for 3D rotation
|
||||||
|
const rotDir = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
|
||||||
|
|
||||||
|
if (enableSway) {
|
||||||
|
swayWrapper.appendChild(confetti);
|
||||||
|
wrapper.appendChild(swayWrapper);
|
||||||
|
} else {
|
||||||
|
wrapper.appendChild(confetti);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respawn confetti when it hits the bottom
|
||||||
|
wrapper.addEventListener('animationend', (e) => {
|
||||||
|
if (e.animationName === 'carnival-fall') {
|
||||||
|
wrapper.remove();
|
||||||
|
createConfettiPiece(container, false); // respawn without initial huge delay
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRandomCarnivalObjects(count) {
|
||||||
|
const carnivalContainer = document.querySelector('.carnival-container');
|
||||||
|
if (!carnivalContainer) return;
|
||||||
|
|
||||||
|
console.log('Adding random carnival confetti');
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
createConfettiPiece(carnivalContainer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize standard carnival objects
|
||||||
|
function initCarnivalObjects() {
|
||||||
|
let container = document.querySelector('.carnival-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.className = "carnival-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial confetti
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
createConfettiPiece(container, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize carnival
|
||||||
|
function initializeCarnival() {
|
||||||
|
if (!carnival) return;
|
||||||
|
initCarnivalObjects();
|
||||||
|
toggleCarnival();
|
||||||
|
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
|
||||||
|
addRandomCarnivalObjects(carnivalCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCarnival();
|
||||||
60
Jellyfin.Plugin.Seasonals/Web/cherryblossom.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
.cherryblossom-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Petals */
|
||||||
|
.cherryblossom-petal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1005;
|
||||||
|
width: 15px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #ffc0cb;
|
||||||
|
border-radius: 15px 0px 15px 0px;
|
||||||
|
|
||||||
|
will-change: transform;
|
||||||
|
translate: 0 -10vh;
|
||||||
|
animation-name: cherryblossom-fall, cherryblossom-sway;
|
||||||
|
animation-timing-function: linear, ease-in-out;
|
||||||
|
animation-iteration-count: infinite, infinite;
|
||||||
|
animation-duration: 10s, 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cherryblossom-petal.lighter {
|
||||||
|
background-color: #ffd1dc;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cherryblossom-petal.darker {
|
||||||
|
background-color: #ffb7c5;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cherryblossom-petal.type2 {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 10px 0px 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cherryblossom-fall {
|
||||||
|
0% { translate: 0 -10vh; }
|
||||||
|
100% { translate: 0 110vh; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cherryblossom-sway {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(30px) rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
Jellyfin.Plugin.Seasonals/Web/cherryblossom.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
|
||||||
|
|
||||||
|
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true;
|
||||||
|
const petalCount = config.PetalCount || 25;
|
||||||
|
const randomCherryBlossom = config.EnableRandomCherryBlossom !== undefined ? config.EnableRandomCherryBlossom : true;
|
||||||
|
const randomCherryBlossomMobile = config.EnableRandomCherryBlossomMobile !== undefined ? config.EnableRandomCherryBlossomMobile : false;
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleCherryBlossom() {
|
||||||
|
const container = document.querySelector('.cherryblossom-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('CherryBlossom hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('CherryBlossom visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleCherryBlossom);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createPetal(container) {
|
||||||
|
const petal = document.createElement('div');
|
||||||
|
petal.classList.add('cherryblossom-petal');
|
||||||
|
|
||||||
|
const type = Math.random() > 0.5 ? 'type1' : 'type2';
|
||||||
|
petal.classList.add(type);
|
||||||
|
|
||||||
|
const color = Math.random() > 0.7 ? 'darker' : 'lighter';
|
||||||
|
petal.classList.add(color);
|
||||||
|
|
||||||
|
const randomLeft = Math.random() * 100;
|
||||||
|
petal.style.left = `${randomLeft}%`;
|
||||||
|
|
||||||
|
const size = Math.random() * 0.5 + 0.5;
|
||||||
|
petal.style.transform = `scale(${size})`;
|
||||||
|
|
||||||
|
const duration = Math.random() * 5 + 8;
|
||||||
|
const delay = Math.random() * 10;
|
||||||
|
const swayDuration = Math.random() * 2 + 2;
|
||||||
|
|
||||||
|
if (enableDifferentDuration) {
|
||||||
|
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
|
||||||
|
}
|
||||||
|
petal.style.animationDelay = `${delay}s, ${Math.random() * 3}s`;
|
||||||
|
|
||||||
|
container.appendChild(petal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRandomObjects() {
|
||||||
|
const container = document.querySelector('.cherryblossom-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < petalCount; i++) {
|
||||||
|
createPetal(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initObjects() {
|
||||||
|
let container = document.querySelector('.cherryblossom-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.className = "cherryblossom-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial batch
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
createPetal(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCherryBlossom() {
|
||||||
|
if (!cherryBlossom) return;
|
||||||
|
initObjects();
|
||||||
|
toggleCherryBlossom();
|
||||||
|
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
if (randomCherryBlossom && (screenWidth > 768 || randomCherryBlossomMobile)) {
|
||||||
|
addRandomObjects();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCherryBlossom();
|
||||||
@@ -1,138 +1,112 @@
|
|||||||
.christmas-container {
|
.christmas-container {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
contain: layout paint;
|
||||||
|
}
|
||||||
.christmas {
|
|
||||||
position: fixed;
|
.christmas {
|
||||||
z-index: 15;
|
position: fixed;
|
||||||
top: -10%;
|
z-index: 15;
|
||||||
font-size: 1em;
|
top: 0;
|
||||||
color: #fff;
|
will-change: transform;
|
||||||
font-family: Arial, sans-serif;
|
translate: 0 -10vh;
|
||||||
text-shadow: 0 0 5px #000;
|
font-size: 1em;
|
||||||
user-select: none;
|
color: #fff;
|
||||||
cursor: default;
|
font-family: Arial, sans-serif;
|
||||||
-webkit-user-select: none;
|
text-shadow: 0 0 5px #000;
|
||||||
-webkit-animation-name: christmas-fall, christmas-shake;
|
user-select: none;
|
||||||
-webkit-animation-duration: 10s, 3s;
|
cursor: default;
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
animation-name: christmas-fall, christmas-shake;
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
animation-duration: 10s, 3s;
|
||||||
animation-name: christmas-fall, christmas-shake;
|
animation-timing-function: linear, ease-in-out;
|
||||||
animation-duration: 10s, 3s;
|
animation-iteration-count: infinite, infinite;
|
||||||
animation-timing-function: linear, ease-in-out;
|
}
|
||||||
animation-iteration-count: infinite, infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes christmas-fall {
|
@keyframes christmas-fall {
|
||||||
0% {
|
0% {
|
||||||
top: -10%;
|
translate: 0 -10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
top: 100%;
|
translate: 0 110vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes christmas-shake {
|
@keyframes christmas-shake {
|
||||||
|
0%,
|
||||||
0%,
|
100% {
|
||||||
100% {
|
transform: translateX(0);
|
||||||
-webkit-transform: translateX(0);
|
}
|
||||||
transform: translateX(0);
|
|
||||||
}
|
50% {
|
||||||
|
transform: translateX(80px);
|
||||||
50% {
|
}
|
||||||
-webkit-transform: translateX(80px);
|
}
|
||||||
transform: translateX(80px);
|
|
||||||
}
|
.christmas:nth-of-type(0) {
|
||||||
}
|
left: 0%;
|
||||||
|
animation-delay: 0s, 0s;
|
||||||
@keyframes christmas-fall {
|
}
|
||||||
0% {
|
|
||||||
top: -10%;
|
.christmas:nth-of-type(1) {
|
||||||
}
|
left: 10%;
|
||||||
|
animation-delay: 1s, 1s;
|
||||||
100% {
|
}
|
||||||
top: 100%;
|
|
||||||
}
|
.christmas:nth-of-type(2) {
|
||||||
}
|
left: 20%;
|
||||||
|
animation-delay: 6s, 0.5s;
|
||||||
@keyframes christmas-shake {
|
}
|
||||||
|
|
||||||
0%,
|
.christmas:nth-of-type(3) {
|
||||||
100% {
|
left: 30%;
|
||||||
transform: translateX(0);
|
animation-delay: 4s, 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
.christmas:nth-of-type(4) {
|
||||||
transform: translateX(80px);
|
left: 40%;
|
||||||
}
|
animation-delay: 2s, 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(0) {
|
.christmas:nth-of-type(5) {
|
||||||
left: 0%;
|
left: 50%;
|
||||||
animation-delay: 0s, 0s;
|
animation-delay: 8s, 3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(1) {
|
.christmas:nth-of-type(6) {
|
||||||
left: 10%;
|
left: 60%;
|
||||||
animation-delay: 1s, 1s;
|
animation-delay: 6s, 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(2) {
|
.christmas:nth-of-type(7) {
|
||||||
left: 20%;
|
left: 70%;
|
||||||
animation-delay: 6s, 0.5s;
|
animation-delay: 2.5s, 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(3) {
|
.christmas:nth-of-type(8) {
|
||||||
left: 30%;
|
left: 80%;
|
||||||
animation-delay: 4s, 2s;
|
animation-delay: 1s, 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(4) {
|
.christmas:nth-of-type(9) {
|
||||||
left: 40%;
|
left: 90%;
|
||||||
animation-delay: 2s, 2s;
|
animation-delay: 3s, 1.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(5) {
|
.christmas:nth-of-type(10) {
|
||||||
left: 50%;
|
left: 25%;
|
||||||
animation-delay: 8s, 3s;
|
animation-delay: 2s, 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(6) {
|
.christmas:nth-of-type(11) {
|
||||||
left: 60%;
|
left: 65%;
|
||||||
animation-delay: 6s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(7) {
|
|
||||||
left: 70%;
|
|
||||||
animation-delay: 2.5s, 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(8) {
|
|
||||||
left: 80%;
|
|
||||||
animation-delay: 1s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(9) {
|
|
||||||
left: 90%;
|
|
||||||
animation-delay: 3s, 1.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(10) {
|
|
||||||
left: 25%;
|
|
||||||
animation-delay: 2s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(11) {
|
|
||||||
left: 65%;
|
|
||||||
animation-delay: 4s, 2.5s;
|
|
||||||
animation-delay: 4s, 2.5s;
|
animation-delay: 4s, 2.5s;
|
||||||
@@ -6,6 +6,8 @@ const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ?
|
|||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols
|
||||||
const christmasCount = config.SymbolCount || 25; // count of random extra christmas
|
const christmasCount = config.SymbolCount || 25; // count of random extra christmas
|
||||||
|
|
||||||
|
// Array of christmas characters
|
||||||
|
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
@@ -37,17 +39,12 @@ function toggleChristmas() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleChristmas);
|
const observer = new MutationObserver(toggleChristmas);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Array of christmas characters
|
|
||||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
|
||||||
|
|
||||||
function addRandomChristmas(count) {
|
function addRandomChristmas(count) {
|
||||||
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
||||||
if (!christmasContainer) return; // exit if christmas container is not found
|
if (!christmasContainer) return; // exit if christmas container is not found
|
||||||
|
|||||||
38
Jellyfin.Plugin.Seasonals/Web/earthday.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.earthday-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 8vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earthday-meadow {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform-origin: bottom;
|
||||||
|
animation: grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes grow-meadow {
|
||||||
|
0% { transform: translateY(100%); opacity: 0; }
|
||||||
|
100% { transform: translateY(0); opacity: 0.95; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.earthday-sway {
|
||||||
|
will-change: transform;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
animation: sway-grass 4s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sway-grass {
|
||||||
|
0% { transform: skewX(-2deg); }
|
||||||
|
100% { transform: skewX(2deg); }
|
||||||
|
}
|
||||||
127
Jellyfin.Plugin.Seasonals/Web/earthday.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.EarthDay || {};
|
||||||
|
|
||||||
|
const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true;
|
||||||
|
const vineCount = config.VineCount || 4;
|
||||||
|
|
||||||
|
const flowerColors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// Toggle Function
|
||||||
|
function toggleEarthDay() {
|
||||||
|
const container = document.querySelector('.earthday-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('EarthDay hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('EarthDay visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleEarthDay);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function createElements() {
|
||||||
|
const container = document.querySelector('.earthday-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.earthday-container')) {
|
||||||
|
container.className = 'earthday-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = window.innerWidth;
|
||||||
|
// MARK: GRASS HEIGHT CONFIGURATION
|
||||||
|
// To prevent squishing, hSVG calculation MUST match the height in earthday.css exactly
|
||||||
|
// earthday.css uses 8vh, so here it is 0.08
|
||||||
|
const hSVG = Math.floor(window.innerHeight * 0.08) || 80;
|
||||||
|
let paths = '';
|
||||||
|
|
||||||
|
// Generate Grass
|
||||||
|
for (let i = 0; i < 400; i++) {
|
||||||
|
const x = Math.random() * w;
|
||||||
|
const h = hSVG * 0.2 + Math.random() * (hSVG * 0.8);
|
||||||
|
const cY = hSVG - h;
|
||||||
|
const bend = x + (Math.random() * 15 - 7.5); // curvature
|
||||||
|
const color = Math.random() > 0.5 ? '#2E8B57' : '#3CB371';
|
||||||
|
const width = 1 + Math.random() * 2;
|
||||||
|
paths += `<path d="M ${x} ${hSVG} Q ${bend} ${cY+hSVG*0.2} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Flowers
|
||||||
|
const flowerCount = Math.max(10, vineCount * 15);
|
||||||
|
for (let i = 0; i < flowerCount; i++) {
|
||||||
|
const x = 10 + Math.random() * (w - 20);
|
||||||
|
const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
|
||||||
|
const col = flowerColors[Math.floor(Math.random() * flowerColors.length)];
|
||||||
|
|
||||||
|
paths += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#006400" stroke-width="1.5" fill="none"/>`;
|
||||||
|
|
||||||
|
const r = 2 + Math.random() * 1.5;
|
||||||
|
paths += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||||
|
paths += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||||
|
paths += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||||
|
paths += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||||
|
paths += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgContent = `
|
||||||
|
<svg class="earthday-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g class="earthday-sway">
|
||||||
|
${paths}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML = svgContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Resize
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = debounce(() => {
|
||||||
|
const container = document.querySelector('.earthday-container');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
createElements();
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
function initializeEarthDay() {
|
||||||
|
if (!enabled) return;
|
||||||
|
createElements();
|
||||||
|
toggleEarthDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEarthDay();
|
||||||
@@ -1,160 +1,65 @@
|
|||||||
.easter-container {
|
.easter-container {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: hidden;
|
top: 0;
|
||||||
top: 0;
|
left: 0;
|
||||||
left: 0;
|
width: 100%;
|
||||||
width: 100%;
|
height: 100%;
|
||||||
height: 100%;
|
pointer-events: none;
|
||||||
pointer-events: none;
|
z-index: 10000;
|
||||||
z-index: 10;
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-grass-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 8vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-meadow-layer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easter-meadow {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sway */
|
||||||
|
.easter-sway {
|
||||||
|
will-change: transform;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
animation: easter-wind-sway 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes easter-wind-sway {
|
||||||
|
0% { transform: skewX(-3deg); }
|
||||||
|
100% { transform: skewX(5deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.hopping-rabbit {
|
.hopping-rabbit {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: 15;
|
bottom: -15px;
|
||||||
bottom: 10px;
|
left: 0;
|
||||||
width: 70px;
|
width: 160px;
|
||||||
overflow: hidden;
|
height: auto;
|
||||||
pointer-events: none;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.hopping-rabbit {
|
.hopping-rabbit {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.easter {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 15;
|
|
||||||
top: -10%;
|
|
||||||
font-size: 1em;
|
|
||||||
color: #fff;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
text-shadow: 0 0 5px #000;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
|
||||||
-webkit-animation-name: easter-fall, easter-shake;
|
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
|
||||||
animation-name: easter-fall, easter-shake;
|
|
||||||
animation-timing-function: linear, ease-in-out;
|
|
||||||
animation-iteration-count: infinite, infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter img {
|
|
||||||
z-index: 15;
|
|
||||||
height: auto;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes easter-fall {
|
|
||||||
0% {
|
|
||||||
top: -10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes easter-shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px);
|
|
||||||
transform: translateX(80px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes easter-fall {
|
|
||||||
0% {
|
|
||||||
top: -10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes easter-shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateX(80px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.easter:nth-of-type(0) {
|
|
||||||
left: 0%;
|
|
||||||
animation-delay: 0s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(1) {
|
|
||||||
left: 10%;
|
|
||||||
animation-delay: 1s, 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(2) {
|
|
||||||
left: 20%;
|
|
||||||
animation-delay: 6s, 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(3) {
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: 4s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(4) {
|
|
||||||
left: 40%;
|
|
||||||
animation-delay: 2s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(5) {
|
|
||||||
left: 50%;
|
|
||||||
animation-delay: 8s, 3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(6) {
|
|
||||||
left: 60%;
|
|
||||||
animation-delay: 6s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(7) {
|
|
||||||
left: 70%;
|
|
||||||
animation-delay: 2.5s, 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(8) {
|
|
||||||
left: 80%;
|
|
||||||
animation-delay: 1s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(9) {
|
|
||||||
left: 90%;
|
|
||||||
animation-delay: 3s, 1.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(10) {
|
|
||||||
left: 25%;
|
|
||||||
animation-delay: 2s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easter:nth-of-type(11) {
|
|
||||||
left: 65%;
|
|
||||||
animation-delay: 4s, 2.5s;
|
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,37 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Easter || {};
|
const config = window.SeasonalsPluginConfig?.Easter || {};
|
||||||
|
|
||||||
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
|
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true;
|
||||||
const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter
|
const enableBunny = config.EnableBunny !== undefined ? config.EnableBunny : true;
|
||||||
const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance)
|
/* MARK: Bunny movement config */
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter
|
const jumpDistanceVw = 5; // Distance in vw the bunny covers per jump
|
||||||
const easterEggCount = config.EggCount || 20; // count of random extra easter
|
const jumpDurationMs = 770; // Time in ms the bunny spends moving during a jump
|
||||||
|
const pauseDurationMs = 116.6666; // Time in ms the bunny pauses between jumps
|
||||||
|
|
||||||
const bunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable hopping bunny
|
const minBunnyRestTime = config.MinBunnyRestTime || 2000;
|
||||||
const bunnyDuration = config.BunnyDuration || 12000; // duration of the bunny animation in ms
|
const maxBunnyRestTime = config.MaxBunnyRestTime || 5000;
|
||||||
const hopHeight = config.HopHeight || 12; // height of the bunny hops in px
|
const eggCount = config.EggCount || 15;
|
||||||
const minBunnyRestTime = config.MinBunnyRestTime || 2000; // minimum time the bunny rests in ms
|
|
||||||
const maxBunnyRestTime = config.MaxBunnyRestTime || 5000; // maximum time the bunny rests in ms
|
|
||||||
|
|
||||||
|
const rabbit = "../Seasonals/Resources/easter_images/Osterhase.gif";
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
const easterEggImages = [
|
||||||
let animationFrameId;
|
"../Seasonals/Resources/easter_images/egg_1.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_2.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_3.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_4.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_5.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_6.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_7.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_8.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_9.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_10.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_11.png",
|
||||||
|
"../Seasonals/Resources/easter_images/egg_12.png",
|
||||||
|
"../Seasonals/Resources/easter_images/eggs.png"
|
||||||
|
];
|
||||||
|
|
||||||
// function to check and control the easter
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// Check visibility
|
||||||
function toggleEaster() {
|
function toggleEaster() {
|
||||||
const easterContainer = document.querySelector('.easter-container');
|
const easterContainer = document.querySelector('.easter-container');
|
||||||
if (!easterContainer) return;
|
if (!easterContainer) return;
|
||||||
@@ -26,21 +41,20 @@ function toggleEaster() {
|
|||||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
// hide easter if video/trailer player is active or dashboard is visible
|
|
||||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
easterContainer.style.display = 'none'; // hide easter
|
easterContainer.style.display = 'none';
|
||||||
if (animationFrameId) {
|
if (rabbitTimeout) {
|
||||||
cancelAnimationFrame(animationFrameId);
|
clearTimeout(rabbitTimeout);
|
||||||
animationFrameId = null;
|
isAnimating = false;
|
||||||
}
|
}
|
||||||
if (!msgPrinted) {
|
if (!msgPrinted) {
|
||||||
console.log('Easter hidden');
|
console.log('Easter hidden');
|
||||||
msgPrinted = true;
|
msgPrinted = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
easterContainer.style.display = 'block'; // show easter
|
easterContainer.style.display = 'block';
|
||||||
if (!animationFrameId) {
|
if (!isAnimating && enableBunny) {
|
||||||
animateRabbit(); // start animation
|
animateRabbit(document.querySelector('#rabbit'));
|
||||||
}
|
}
|
||||||
if (msgPrinted) {
|
if (msgPrinted) {
|
||||||
console.log('Easter visible');
|
console.log('Easter visible');
|
||||||
@@ -49,145 +63,204 @@ function toggleEaster() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// observe changes in the DOM
|
|
||||||
const observer = new MutationObserver(toggleEaster);
|
const observer = new MutationObserver(toggleEaster);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const images = [
|
function createEasterGrassAndEggs(container) {
|
||||||
"/Seasonals/Resources/easter_images/egg_1.png",
|
let grassContainer = container.querySelector('.easter-grass-container');
|
||||||
"/Seasonals/Resources/easter_images/egg_2.png",
|
if (!grassContainer) {
|
||||||
"/Seasonals/Resources/easter_images/egg_3.png",
|
grassContainer = document.createElement('div');
|
||||||
"/Seasonals/Resources/easter_images/egg_4.png",
|
grassContainer.className = 'easter-grass-container';
|
||||||
"/Seasonals/Resources/easter_images/egg_5.png",
|
container.appendChild(grassContainer);
|
||||||
"/Seasonals/Resources/easter_images/egg_6.png",
|
}
|
||||||
"/Seasonals/Resources/easter_images/egg_7.png",
|
|
||||||
"/Seasonals/Resources/easter_images/egg_8.png",
|
grassContainer.innerHTML = '';
|
||||||
"/Seasonals/Resources/easter_images/egg_9.png",
|
|
||||||
"/Seasonals/Resources/easter_images/egg_10.png",
|
let pathsBg = '';
|
||||||
"/Seasonals/Resources/easter_images/egg_11.png",
|
let pathsFg = '';
|
||||||
"/Seasonals/Resources/easter_images/egg_12.png",
|
const w = window.innerWidth;
|
||||||
];
|
const hSVG = 80; // Grass 80px high
|
||||||
const rabbit = "/Seasonals/Resources/easter_images/easter-bunny.png";
|
|
||||||
|
// Generate Grass
|
||||||
function addRandomEaster(count) {
|
const bladeCount = w / 5;
|
||||||
const easterContainer = document.querySelector('.easter-container'); // get the leave container
|
for (let i = 0; i < bladeCount; i++) {
|
||||||
if (!easterContainer) return; // exit if leave container is not found
|
const height = Math.random() * 40 + 20;
|
||||||
|
const x = i * 5 + Math.random() * 3;
|
||||||
console.log('Adding random easter eggs');
|
const hue = 80 + Math.random() * 40; // slightly more yellow-green for spring/easter
|
||||||
|
const color = `hsl(${hue}, 60%, 40%)`;
|
||||||
// Array of leave characters
|
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
|
||||||
for (let i = 0; i < count; i++) {
|
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
|
||||||
// create a new leave element
|
}
|
||||||
const eggDiv = document.createElement('div');
|
|
||||||
eggDiv.className = "easter";
|
for (let i = 0; i < 200; i++) {
|
||||||
|
const x = Math.random() * w;
|
||||||
// pick a random easter symbol
|
const h = 20 + Math.random() * 50;
|
||||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
const cY = hSVG - h;
|
||||||
const img = document.createElement("img");
|
const bend = x + (Math.random() * 40 - 20);
|
||||||
img.src = imageSrc;
|
const color = Math.random() > 0.5 ? '#4caf50' : '#8bc34a';
|
||||||
|
const width = 1 + Math.random() * 2;
|
||||||
eggDiv.appendChild(img);
|
const path = `<path d="M ${x} ${hSVG} Q ${bend} ${cY+20} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
|
||||||
|
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
|
||||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
}
|
||||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
|
||||||
const randomAnimationDelay = Math.random() * 12; // delay (0s to 12s)
|
// Generate Flowers
|
||||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
// apply styles
|
const x = 10 + Math.random() * (w - 20);
|
||||||
eggDiv.style.left = `${randomLeft}%`;
|
const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
|
||||||
eggDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
const col = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
|
||||||
// set random animation duration
|
let path = '';
|
||||||
if (enableDiffrentDuration) {
|
path += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#006400" stroke-width="1.5" fill="none"/>`;
|
||||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
|
||||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
const r = 2 + Math.random() * 1.5;
|
||||||
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
path += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||||
}
|
path += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
|
||||||
|
path += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||||
|
path += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
|
||||||
// add the leave to the container
|
path += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
|
||||||
easterContainer.appendChild(eggDiv);
|
|
||||||
|
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
|
||||||
|
}
|
||||||
|
|
||||||
|
grassContainer.innerHTML = `
|
||||||
|
<div class="easter-meadow-layer" style="z-index: 1001;">
|
||||||
|
<svg class="easter-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g class="easter-sway">
|
||||||
|
${pathsBg}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="easter-meadow-layer" style="z-index: 1003;">
|
||||||
|
<svg class="easter-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g class="easter-sway" style="animation-delay: -2s;">
|
||||||
|
${pathsFg}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add Easter Eggs
|
||||||
|
for (let i = 0; i < eggCount; i++) {
|
||||||
|
const x = 2 + Math.random() * 96;
|
||||||
|
const y = Math.random() * 18; // 0 to 18px off bottom
|
||||||
|
const imageSrc = easterEggImages[Math.floor(Math.random() * easterEggImages.length)];
|
||||||
|
|
||||||
|
const eggImg = document.createElement('img');
|
||||||
|
eggImg.src = imageSrc;
|
||||||
|
eggImg.style.position = 'absolute';
|
||||||
|
eggImg.style.left = `${x}vw`;
|
||||||
|
eggImg.style.bottom = `${y}px`;
|
||||||
|
eggImg.style.width = `${15 + Math.random() * 10}px`;
|
||||||
|
eggImg.style.height = 'auto';
|
||||||
|
eggImg.style.transform = `rotate(${Math.random() * 60 - 30}deg)`;
|
||||||
|
eggImg.style.zIndex = Math.random() > 0.5 ? '1000' : '1004'; // Between grass layers
|
||||||
|
|
||||||
|
grassContainer.appendChild(eggImg);
|
||||||
}
|
}
|
||||||
console.log('Random easter added');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHoppingRabbit() {
|
let rabbitTimeout;
|
||||||
if (!bunny) return; // Nur ausführen, wenn Easter aktiviert ist
|
let isAnimating = false;
|
||||||
|
|
||||||
const easterContainer = document.querySelector('.easter-container');
|
function addHoppingRabbit(container) {
|
||||||
if (!easterContainer) return;
|
if (!enableBunny) return;
|
||||||
|
|
||||||
// Hase erstellen
|
|
||||||
const rabbitImg = document.createElement("img");
|
const rabbitImg = document.createElement("img");
|
||||||
rabbitImg.id = "rabbit";
|
rabbitImg.id = "rabbit";
|
||||||
rabbitImg.src = rabbit; // Bildpfad aus der bestehenden Definition
|
rabbitImg.src = rabbit;
|
||||||
rabbitImg.alt = "Hoppelnder Osterhase";
|
rabbitImg.alt = "Hopping Easter Bunny";
|
||||||
|
rabbitImg.className = "hopping-rabbit";
|
||||||
|
|
||||||
|
rabbitImg.style.bottom = "-15px";
|
||||||
|
rabbitImg.style.position = "absolute";
|
||||||
|
|
||||||
// CSS-Klassen hinzufügen
|
container.appendChild(rabbitImg);
|
||||||
rabbitImg.classList.add("hopping-rabbit");
|
|
||||||
|
|
||||||
easterContainer.appendChild(rabbitImg);
|
|
||||||
|
|
||||||
rabbitImg.style.bottom = (hopHeight / 2 + 6) + "px";
|
|
||||||
|
|
||||||
animateRabbit(rabbitImg);
|
animateRabbit(rabbitImg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateRabbit(rabbitElement) {
|
function animateRabbit(rabbit) {
|
||||||
const rabbit = rabbitElement || document.querySelector('#rabbit');
|
if (!rabbit || isAnimating) return;
|
||||||
if (!rabbit) return;
|
isAnimating = true;
|
||||||
|
|
||||||
|
const startFromLeft = Math.random() >= 0.5;
|
||||||
|
const startX = startFromLeft ? -15 : 115;
|
||||||
|
let currentX = startX;
|
||||||
|
const endX = startFromLeft ? 115 : -15;
|
||||||
|
const direction = startFromLeft ? 1 : -1;
|
||||||
|
|
||||||
|
rabbit.style.transition = 'none';
|
||||||
|
const transformScale = startFromLeft ? 'scaleX(-1)' : '';
|
||||||
|
// Fix bounding box center-of-gravity shift when graphic is flipped
|
||||||
|
rabbit.style.transformOrigin = startFromLeft ? '59% 50%' : '50% 50%';
|
||||||
|
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
|
||||||
|
|
||||||
|
const loopDurationMs = jumpDurationMs + pauseDurationMs;
|
||||||
|
|
||||||
let startTime = null;
|
let startTime = null;
|
||||||
|
|
||||||
function animationStep(timestamp) {
|
function animationStep(timestamp) {
|
||||||
|
if (!document.querySelector('.easter-container') || rabbit.style.display === 'none') {
|
||||||
|
isAnimating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!startTime) {
|
if (!startTime) {
|
||||||
startTime = timestamp;
|
startTime = timestamp;
|
||||||
|
// resetting gif, appending a timestamp cache-buster forces the browser
|
||||||
// random start position and direction
|
// to reload and start the GIF strictly from the first frame.
|
||||||
const startFromLeft = Math.random() >= 0.5;
|
const currSrc = rabbit.src.split('?')[0];
|
||||||
rabbit.startX = startFromLeft ? -10 : 110;
|
rabbit.src = currSrc + '?t=' + Date.now();
|
||||||
rabbit.endX = startFromLeft ? 110 : -10;
|
|
||||||
rabbit.direction = startFromLeft ? 1 : -1;
|
|
||||||
|
|
||||||
// mirror the rabbit image if it starts from the right
|
|
||||||
rabbit.style.transform = startFromLeft ? '' : 'scaleX(-1)';
|
|
||||||
}
|
}
|
||||||
const progress = timestamp - startTime;
|
|
||||||
|
|
||||||
// calculate the horizontal position (linear interpolation)
|
const elapsed = timestamp - startTime;
|
||||||
const x = rabbit.startX + (progress / bunnyDuration) * (rabbit.endX - rabbit.startX);
|
|
||||||
|
const completedLoops = Math.floor(elapsed / loopDurationMs);
|
||||||
|
const timeInCurrentLoop = elapsed % loopDurationMs;
|
||||||
|
|
||||||
// calculate the vertical position (sinus curve)
|
// Determine if we are currently jumping or pausing
|
||||||
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
|
let currentLoopDistance = 0;
|
||||||
|
if (timeInCurrentLoop < jumpDurationMs) {
|
||||||
// set the new position
|
// We are in the jumping phase
|
||||||
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`;
|
currentLoopDistance = (timeInCurrentLoop / jumpDurationMs) * jumpDistanceVw;
|
||||||
|
|
||||||
if (progress < bunnyDuration) {
|
|
||||||
animationFrameId = requestAnimationFrame(animationStep);
|
|
||||||
} else {
|
} else {
|
||||||
// let the bunny rest for a while before hiding easter eggs again
|
// We are in the paused phase
|
||||||
const pauseDuration = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
|
currentLoopDistance = jumpDistanceVw;
|
||||||
setTimeout(() => {
|
|
||||||
startTime = null;
|
|
||||||
animationFrameId = requestAnimationFrame(animationStep);
|
|
||||||
}, pauseDuration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentX = startX + (completedLoops * jumpDistanceVw + currentLoopDistance) * direction;
|
||||||
|
|
||||||
|
// Update DOM without CSS transitions
|
||||||
|
rabbit.style.transform = `translateX(${currentX}vw) ${transformScale}`;
|
||||||
|
|
||||||
|
// Check if finished crossing
|
||||||
|
if ((direction === 1 && currentX >= endX) || (direction === -1 && currentX <= endX)) {
|
||||||
|
let restTime = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
|
||||||
|
|
||||||
|
isAnimating = false;
|
||||||
|
rabbitTimeout = setTimeout(() => {
|
||||||
|
if (!document.body.contains(rabbit)) return;
|
||||||
|
animateRabbit(document.querySelector('#rabbit'));
|
||||||
|
}, restTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rabbitTimeout = requestAnimationFrame(animationStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(animationStep);
|
// Start loop
|
||||||
|
rabbitTimeout = requestAnimationFrame(animationStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeEaster() {
|
||||||
|
if (!easter) return;
|
||||||
|
|
||||||
// initialize standard easter
|
|
||||||
function initEaster() {
|
|
||||||
const container = document.querySelector('.easter-container') || document.createElement("div");
|
const container = document.querySelector('.easter-container') || document.createElement("div");
|
||||||
|
|
||||||
if (!document.querySelector('.easter-container')) {
|
if (!document.querySelector('.easter-container')) {
|
||||||
@@ -196,48 +269,17 @@ function initEaster() {
|
|||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// shuffle the easter images
|
createEasterGrassAndEggs(container);
|
||||||
let currentIndex = images.length;
|
addHoppingRabbit(container);
|
||||||
let randomIndex;
|
|
||||||
while (currentIndex != 0) {
|
// Add resize listener to regenerate meadow
|
||||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
window.addEventListener('resize', () => {
|
||||||
currentIndex--;
|
if(document.querySelector('.easter-container')) {
|
||||||
[images[currentIndex], images[randomIndex]] = [images[randomIndex], images[currentIndex]];
|
createEasterGrassAndEggs(container);
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const eggDiv = document.createElement("div");
|
|
||||||
eggDiv.className = "easter";
|
|
||||||
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.src = images[i];
|
|
||||||
|
|
||||||
// set random animation duration
|
|
||||||
if (enableDiffrentDuration) {
|
|
||||||
const randomAnimationDuration = Math.random() * 10 + 6; // delay (6s to 10s)
|
|
||||||
const randomAnimationDuration2 = Math.random() * 5 + 2; // delay (2s to 5s)
|
|
||||||
eggDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
eggDiv.appendChild(img);
|
|
||||||
container.appendChild(eggDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
addHoppingRabbit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// initialize easter and add random easter after the DOM is loaded
|
|
||||||
function initializeEaster() {
|
|
||||||
if (!easter) return; // exit if easter are disabled
|
|
||||||
initEaster();
|
|
||||||
toggleEaster();
|
toggleEaster();
|
||||||
|
|
||||||
const screenWidth = window.innerWidth;
|
|
||||||
if (randomEaster && (screenWidth > 768 || randomEasterMobile)) { // add random easter only on larger screens, unless enabled for mobile devices
|
|
||||||
addRandomEaster(easterEggCount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
initializeEaster();
|
initializeEaster();
|
||||||
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/Osterhase.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
Jellyfin.Plugin.Seasonals/Web/easter_images/Osterhase_1.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB |
56
Jellyfin.Plugin.Seasonals/Web/eid.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.eid-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eid-symbol {
|
||||||
|
position: absolute;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eid-symbol.floating-star {
|
||||||
|
will-change: opacity;
|
||||||
|
opacity: 0;
|
||||||
|
animation: eid-twinkle 4s ease-in-out infinite;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern-rope {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: linear-gradient(to bottom, rgba(255, 215, 0, 0.1), rgba(255, 215, 0, 0.6));
|
||||||
|
transform-origin: top center;
|
||||||
|
animation: lantern-swing 4s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern-emoji {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 2.5em;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lantern-swing {
|
||||||
|
0% { transform: rotate(-8deg); }
|
||||||
|
100% { transform: rotate(8deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes eid-twinkle {
|
||||||
|
0% { transform: scale(0.8); opacity: 0; text-shadow: 0 0 5px gold; }
|
||||||
|
50% { transform: scale(1.2); opacity: 0.8; text-shadow: 0 0 20px gold; }
|
||||||
|
100% { transform: scale(0.8); opacity: 0; text-shadow: 0 0 5px gold; }
|
||||||
|
}
|
||||||
96
Jellyfin.Plugin.Seasonals/Web/eid.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Eid || {};
|
||||||
|
const eid = config.EnableEid !== undefined ? config.EnableEid : true;
|
||||||
|
|
||||||
|
const eidSymbols = ['🌙', '⭐', '✨'];
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleEid() {
|
||||||
|
const container = document.querySelector('.eid-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Eid hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Eid visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleEid);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function createEid(container) {
|
||||||
|
const starCount = 20;
|
||||||
|
const lanternCount = Math.floor(Math.random() * 3) + 4; // 4 to 6 lanterns
|
||||||
|
|
||||||
|
// Create evenly spaced lanterns
|
||||||
|
const segmentWidth = 100 / lanternCount;
|
||||||
|
for (let i = 0; i < lanternCount; i++) {
|
||||||
|
const symbol = document.createElement('div');
|
||||||
|
symbol.className = 'eid-symbol lantern-rope';
|
||||||
|
|
||||||
|
// Base position within segment, with slight random jitter
|
||||||
|
const baseLeft = (i * segmentWidth) + (segmentWidth * 0.2);
|
||||||
|
const jitter = Math.random() * (segmentWidth * 0.6);
|
||||||
|
symbol.style.left = `${baseLeft + jitter}%`;
|
||||||
|
|
||||||
|
symbol.style.animationDelay = `${Math.random() * -4}s`;
|
||||||
|
const ropeLen = 15 + Math.random() * 15; // 15vh to 30vh
|
||||||
|
symbol.style.height = `${ropeLen}vh`;
|
||||||
|
|
||||||
|
const lanternSpan = document.createElement('span');
|
||||||
|
lanternSpan.className = 'lantern-emoji';
|
||||||
|
lanternSpan.textContent = '🏮';
|
||||||
|
symbol.appendChild(lanternSpan);
|
||||||
|
|
||||||
|
container.appendChild(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create random floating stars
|
||||||
|
for (let i = 0; i < starCount; i++) {
|
||||||
|
const symbol = document.createElement('div');
|
||||||
|
symbol.className = 'eid-symbol floating-star';
|
||||||
|
symbol.textContent = eidSymbols[Math.floor(Math.random() * eidSymbols.length)];
|
||||||
|
symbol.style.left = `${Math.random() * 100}%`;
|
||||||
|
symbol.style.top = `${Math.random() * 100}%`;
|
||||||
|
symbol.style.animationDelay = `${Math.random() * -5}s`;
|
||||||
|
|
||||||
|
symbol.addEventListener('animationiteration', () => {
|
||||||
|
symbol.style.left = `${Math.random() * 90 + 5}%`;
|
||||||
|
symbol.style.top = `${Math.random() * 100}%`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeEid() {
|
||||||
|
if (!eid) return;
|
||||||
|
const container = document.querySelector('.eid-container') || document.createElement("div");
|
||||||
|
if (!document.querySelector('.eid-container')) {
|
||||||
|
container.className = "eid-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
createEid(container);
|
||||||
|
toggleEid();
|
||||||
|
}
|
||||||
|
initializeEid();
|
||||||
42
Jellyfin.Plugin.Seasonals/Web/eurovision.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.eurovision-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-note-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
animation: move-right linear infinite;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-note {
|
||||||
|
display: block;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
|
||||||
|
animation: sway ease-in-out infinite alternate;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal scroll from left to right */
|
||||||
|
@keyframes move-right {
|
||||||
|
0% { transform: translateX(-10vw); opacity: 0; }
|
||||||
|
10% { opacity: 1; }
|
||||||
|
90% { opacity: 1; }
|
||||||
|
100% { transform: translateX(110vw); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sine-wave style vertical bouncing for the note itself */
|
||||||
|
@keyframes sway {
|
||||||
|
0% { transform: translateY(-30px); }
|
||||||
|
100% { transform: translateY(30px); }
|
||||||
|
}
|
||||||
101
Jellyfin.Plugin.Seasonals/Web/eurovision.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Eurovision || {};
|
||||||
|
|
||||||
|
const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true;
|
||||||
|
const elementCount = config.SymbolCount || 25;
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||||
|
const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true;
|
||||||
|
const eurovisionColorsStr = config.EurovisionColors || '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff';
|
||||||
|
const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// Toggle Function
|
||||||
|
function toggleEurovision() {
|
||||||
|
const container = document.querySelector('.eurovision-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Eurovision hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Eurovision visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleEurovision);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createElements() {
|
||||||
|
const container = document.querySelector('.eurovision-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.eurovision-container')) {
|
||||||
|
container.className = 'eurovision-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notesSymbols = ['♪', '♫', '♬', '♭', '♮', '♯', '𝄞', '𝄢'];
|
||||||
|
const pColors = eurovisionColorsStr.split(',').map(s => s.trim()).filter(s => s);
|
||||||
|
|
||||||
|
for (let i = 0; i < elementCount; i++) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'music-note-wrapper';
|
||||||
|
|
||||||
|
const note = document.createElement('span');
|
||||||
|
note.className = 'music-note';
|
||||||
|
note.textContent = notesSymbols[Math.floor(Math.random() * notesSymbols.length)];
|
||||||
|
wrapper.appendChild(note);
|
||||||
|
|
||||||
|
wrapper.style.top = `${Math.random() * 90}vh`;
|
||||||
|
|
||||||
|
const minMoveDur = 10;
|
||||||
|
const maxMoveDur = 25;
|
||||||
|
const moveDur = enableDifferentDuration
|
||||||
|
? minMoveDur + Math.random() * (maxMoveDur - minMoveDur)
|
||||||
|
: (minMoveDur + maxMoveDur) / 2;
|
||||||
|
wrapper.style.animationDuration = `${moveDur}s`;
|
||||||
|
wrapper.style.animationDelay = `${Math.random() * 15}s`;
|
||||||
|
|
||||||
|
const minSwayDur = 1;
|
||||||
|
const maxSwayDur = 3;
|
||||||
|
const swayDur = minSwayDur + Math.random() * (maxSwayDur - minSwayDur);
|
||||||
|
note.style.animationDuration = `${swayDur}s`;
|
||||||
|
note.style.animationDelay = `${Math.random() * 2}s`;
|
||||||
|
|
||||||
|
note.style.fontSize = `${Math.random() * 1.5 + 1.5}rem`;
|
||||||
|
|
||||||
|
if (enableColorfulNotes && pColors.length > 0) {
|
||||||
|
note.style.color = pColors[Math.floor(Math.random() * pColors.length)];
|
||||||
|
note.style.textShadow = `0 0 ${glowSize}px ${note.style.color}`;
|
||||||
|
} else {
|
||||||
|
note.style.color = `rgba(255, 255, 255, 0.9)`;
|
||||||
|
note.style.textShadow = `0 0 ${glowSize}px rgba(255, 255, 255, 0.6)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeEurovision() {
|
||||||
|
if (!enabled) return;
|
||||||
|
createElements();
|
||||||
|
toggleEurovision();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEurovision();
|
||||||
89
Jellyfin.Plugin.Seasonals/Web/filmnoir.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.filmnoir-tint {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
background-color: #8c7355;
|
||||||
|
mix-blend-mode: color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filmnoir-effects {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Film grain */
|
||||||
|
.filmnoir-grain {
|
||||||
|
will-change: transform, opacity;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" stitchTiles="stitch"/></filter><rect width="200" height="200" filter="url(%23n)" opacity="0.4"/></svg>');
|
||||||
|
animation: grain-dance 0.2s steps(4) infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
opacity: 0.3;
|
||||||
|
translate: 0 -50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vignette */
|
||||||
|
.filmnoir-vignette {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(circle at center, transparent 50%, rgba(0,0,0,0.8) 120%);
|
||||||
|
box-shadow: inset 0 0 100px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Occasional flicker and scratch */
|
||||||
|
.filmnoir-scratches {
|
||||||
|
will-change: opacity;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to right, transparent 50%, rgba(255,255,255,0.2) 51%, transparent 52%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: scratch 4s infinite linear, flicker 6s infinite alternate;
|
||||||
|
opacity: 0.2;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes grain-dance {
|
||||||
|
0% { transform: translate(0,0); }
|
||||||
|
25% { transform: translate(-5%,-5%); }
|
||||||
|
50% { transform: translate(-10%,5%); }
|
||||||
|
75% { transform: translate(5%,-10%); }
|
||||||
|
100% { transform: translate(0,0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scratch {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
10% { background-position: 200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0% { opacity: 0.2; }
|
||||||
|
5% { opacity: 0.1; }
|
||||||
|
10% { opacity: 0.3; }
|
||||||
|
15% { opacity: 0.2; }
|
||||||
|
50% { opacity: 0.15; }
|
||||||
|
55% { opacity: 0.25; }
|
||||||
|
100% { opacity: 0.2; }
|
||||||
|
}
|
||||||
79
Jellyfin.Plugin.Seasonals/Web/filmnoir.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.FilmNoir || {};
|
||||||
|
const filmnoir = config.EnableFilmNoir !== undefined ? config.EnableFilmNoir : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleFilmNoir() {
|
||||||
|
const tint = document.querySelector('.filmnoir-tint');
|
||||||
|
const effects = document.querySelector('.filmnoir-effects');
|
||||||
|
if (!tint || !effects) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
tint.style.display = 'none';
|
||||||
|
effects.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('FilmNoir hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tint.style.display = 'block';
|
||||||
|
effects.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('FilmNoir visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleFilmNoir);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function createFilmNoir() {
|
||||||
|
if (!document.querySelector('.filmnoir-tint')) {
|
||||||
|
const tint = document.createElement('div');
|
||||||
|
tint.className = 'filmnoir-tint';
|
||||||
|
tint.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let effects = document.querySelector('.filmnoir-effects');
|
||||||
|
if (!effects) {
|
||||||
|
effects = document.createElement('div');
|
||||||
|
effects.className = 'filmnoir-effects';
|
||||||
|
effects.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(effects);
|
||||||
|
|
||||||
|
const vignette = document.createElement('div');
|
||||||
|
vignette.className = 'filmnoir-vignette';
|
||||||
|
|
||||||
|
const grain = document.createElement('div');
|
||||||
|
grain.className = 'filmnoir-grain';
|
||||||
|
|
||||||
|
const scratches = document.createElement('div');
|
||||||
|
scratches.className = 'filmnoir-scratches';
|
||||||
|
|
||||||
|
effects.appendChild(grain);
|
||||||
|
effects.appendChild(scratches);
|
||||||
|
effects.appendChild(vignette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initializeFilmNoir() {
|
||||||
|
if (!filmnoir) return;
|
||||||
|
|
||||||
|
createFilmNoir();
|
||||||
|
toggleFilmNoir();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeFilmNoir();
|
||||||
@@ -7,12 +7,14 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rocket-trail {
|
.rocket-trail {
|
||||||
|
will-change: transform;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--trailX);
|
left: var(--trailX);
|
||||||
top: var(--trailStartY);
|
top: 0;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
|
|
||||||
/* activate the following for rocket trail */
|
/* activate the following for rocket trail */
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
box-shadow: 0 0 8px 2px white;*/
|
box-shadow: 0 0 8px 2px white;*/
|
||||||
|
|
||||||
animation: rocket-trail-animation 1s linear forwards;
|
animation: rocket-trail-animation 1s linear forwards;
|
||||||
|
translate: 0 var(--trailStartY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rocket-trail-animation {
|
@keyframes rocket-trail-animation {
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.firework {
|
.firework {
|
||||||
|
will-change: transform;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ const observer = new MutationObserver(toggleFirework);
|
|||||||
|
|
||||||
// start observation
|
// start observation
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -162,6 +162,7 @@ function startFireworks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fireworksInterval = setInterval(() => {
|
fireworksInterval = setInterval(() => {
|
||||||
|
if (!document.body.contains(fireworkContainer)) { clearInterval(fireworksInterval); return; }
|
||||||
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
|
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
|
||||||
for (let i = 0; i < randomCount; i++) {
|
for (let i = 0; i < randomCount; i++) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
34
Jellyfin.Plugin.Seasonals/Web/friday13.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
.friday13-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friday13-cat {
|
||||||
|
position: absolute;
|
||||||
|
width: 150px; /* MARK: Cat size */
|
||||||
|
height: auto;
|
||||||
|
user-select: none;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cat-walk-right {
|
||||||
|
0% { left: -10vw; transform: scaleX(-1); opacity: 1; }
|
||||||
|
99% { left: 110vw; transform: scaleX(-1); opacity: 1; }
|
||||||
|
100% { opacity: 0; transform: scaleX(-1); left: 110vw; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cat-walk-left {
|
||||||
|
0% { left: 110vw; transform: scaleX(1); opacity: 1; }
|
||||||
|
99% { left: -10vw; transform: scaleX(1); opacity: 1; }
|
||||||
|
100% { opacity: 0; transform: scaleX(1); left: -10vw; }
|
||||||
|
}
|
||||||
|
|
||||||
83
Jellyfin.Plugin.Seasonals/Web/friday13.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Friday13 || {};
|
||||||
|
const friday13 = config.EnableFriday13 !== undefined ? config.EnableFriday13 : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleFriday13() {
|
||||||
|
const container = document.querySelector('.friday13-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Friday13 hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Friday13 visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleFriday13);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createFriday13(container) {
|
||||||
|
function spawnCat() {
|
||||||
|
// MARK: Height of the cat from bottom
|
||||||
|
const catBottomPosition = "-15px";
|
||||||
|
// MARK: Time it takes for the cat to cross the screen
|
||||||
|
const catWalkDurationSeconds = 20;
|
||||||
|
|
||||||
|
const cat = document.createElement('img');
|
||||||
|
cat.className = 'friday13-cat';
|
||||||
|
cat.src = '../Seasonals/Resources/friday_assets/black-cat.gif';
|
||||||
|
cat.style.bottom = catBottomPosition;
|
||||||
|
|
||||||
|
// Either walk left to right or right to left
|
||||||
|
const dir = Math.random() > 0.5 ? 'right' : 'left';
|
||||||
|
cat.style.animationName = `cat-walk-${dir}`;
|
||||||
|
cat.style.animationDuration = `${catWalkDurationSeconds}s`;
|
||||||
|
cat.style.animationIterationCount = `1`; // play once and remove
|
||||||
|
cat.style.animationFillMode = `forwards`;
|
||||||
|
|
||||||
|
container.appendChild(cat);
|
||||||
|
|
||||||
|
// Remove and respawn
|
||||||
|
setTimeout(() => {
|
||||||
|
if (cat.parentNode) {
|
||||||
|
cat.parentNode.removeChild(cat);
|
||||||
|
}
|
||||||
|
// Respawn with random delay between 5 to 25 seconds
|
||||||
|
setTimeout(() => { if (document.body.contains(container)) spawnCat(); }, Math.random() * 20000 + 5000);
|
||||||
|
}, (catWalkDurationSeconds * 1000) + 500); // Wait for duration + 500ms safety margin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial spawn with random delay
|
||||||
|
setTimeout(() => { if (document.body.contains(container)) spawnCat(); }, Math.random() * 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFriday13() {
|
||||||
|
if (!friday13) return;
|
||||||
|
const container = document.querySelector('.friday13-container') || document.createElement("div");
|
||||||
|
if (!document.querySelector('.friday13-container')) {
|
||||||
|
container.className = "friday13-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
createFriday13(container);
|
||||||
|
toggleFriday13();
|
||||||
|
}
|
||||||
|
initializeFriday13();
|
||||||
BIN
Jellyfin.Plugin.Seasonals/Web/friday_assets/black-cat.gif
Normal file
|
After Width: | Height: | Size: 88 KiB |
71
Jellyfin.Plugin.Seasonals/Web/frost.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
.frost-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: strict;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frost-layer {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(ellipse at center, transparent 60%, rgba(180, 220, 255, 0.4) 100%);
|
||||||
|
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
|
||||||
|
|
||||||
|
filter: url('#frost-filter');
|
||||||
|
|
||||||
|
animation: frost-creep 4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frost-crystals {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -5%;
|
||||||
|
width: 110%;
|
||||||
|
height: 110%;
|
||||||
|
background-image:
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><circle cx="10" cy="10" r="1.5" fill="rgba(255,255,255,0.2)"/><circle cx="40" cy="30" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="20" cy="50" r="2" fill="rgba(255,255,255,0.1)"/><path d="M50 10 L51 15 L56 16 L51 17 L50 22 L49 17 L44 16 L49 15 Z" fill="rgba(255,255,255,0.2)"/></svg>'),
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="25" cy="5" r="1.5" fill="rgba(255,255,255,0.1)"/><path d="M20 20 L21 23 L24 24 L21 25 L20 28 L19 25 L16 24 L19 23 Z" fill="rgba(255,255,255,0.15)"/></svg>'),
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><circle cx="15" cy="15" r="1" fill="rgba(255,255,255,0.2)"/><circle cx="5" cy="5" r="0.8" fill="rgba(255,255,255,0.1)"/></svg>');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 110px 110px, 60px 60px, 30px 30px;
|
||||||
|
background-position: 0 0, 15px 15px, 5px 10px;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
|
||||||
|
animation: frost-shimmer 6s infinite alternate ease-in-out;
|
||||||
|
translate: 0 -5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes frost-creep {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
box-shadow: inset 0 0 10px rgba(200, 230, 255, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes frost-shimmer {
|
||||||
|
0% {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
Jellyfin.Plugin.Seasonals/Web/frost.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Frost || {};
|
||||||
|
|
||||||
|
const frost = config.EnableFrost !== undefined ? config.EnableFrost : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleFrost() {
|
||||||
|
const container = document.querySelector('.frost-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Frost hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Frost visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleFrost);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createFrost(container) {
|
||||||
|
const frostLayer = document.createElement('div');
|
||||||
|
frostLayer.className = 'frost-layer';
|
||||||
|
|
||||||
|
const frostCrystals = document.createElement('div');
|
||||||
|
frostCrystals.className = 'frost-crystals';
|
||||||
|
|
||||||
|
// An SVG filter to make things look "frozen"/distorted around the edges
|
||||||
|
const svgFilter = document.createElement('div');
|
||||||
|
svgFilter.innerHTML = `
|
||||||
|
<svg style="display:none;">
|
||||||
|
<filter id="frost-filter">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="3" result="noise" />
|
||||||
|
<feDisplacementMap in="SourceGraphic" in2="noise" scale="5" xChannelSelector="R" yChannelSelector="G" />
|
||||||
|
</filter>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
frostLayer.appendChild(frostCrystals);
|
||||||
|
container.appendChild(frostLayer);
|
||||||
|
container.appendChild(svgFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFrost() {
|
||||||
|
if (!frost) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.frost-container') || document.createElement("div");
|
||||||
|
|
||||||
|
if (!document.querySelector('.frost-container')) {
|
||||||
|
container.className = "frost-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
createFrost(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeFrost();
|
||||||
@@ -8,23 +8,16 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween {
|
.halloween {
|
||||||
|
will-change: transform;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: -10%;
|
bottom: -10%;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
-webkit-animation-name: halloween-fall, halloween-shake;
|
|
||||||
-webkit-animation-duration: 10s, 3s;
|
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
|
||||||
-webkit-animation-play-state: running, running;
|
|
||||||
animation-name: halloween-fall, halloween-shake;
|
animation-name: halloween-fall, halloween-shake;
|
||||||
animation-duration: 10s, 3s;
|
animation-duration: 10s, 3s;
|
||||||
animation-timing-function: linear, ease-in-out;
|
animation-timing-function: linear, ease-in-out;
|
||||||
@@ -32,37 +25,15 @@
|
|||||||
animation-play-state: running, running
|
animation-play-state: running, running
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes halloween-fall {
|
|
||||||
0% {
|
|
||||||
bottom: -10%
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
bottom: 100%
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes halloween-shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
transform: translateX(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px);
|
|
||||||
transform: translateX(80px)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes halloween-fall {
|
@keyframes halloween-fall {
|
||||||
0% {
|
0% {
|
||||||
bottom: -10%
|
bottom: -10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
bottom: 100%
|
bottom: 110%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,72 +51,175 @@
|
|||||||
|
|
||||||
.halloween:nth-of-type(0) {
|
.halloween:nth-of-type(0) {
|
||||||
left: 1%;
|
left: 1%;
|
||||||
-webkit-animation-delay: 0s, 0s;
|
animation-delay: 0s, 0s;
|
||||||
animation-delay: 0s, 0s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(1) {
|
.halloween:nth-of-type(1) {
|
||||||
left: 10%;
|
left: 10%;
|
||||||
-webkit-animation-delay: 1s, 1s;
|
animation-delay: -1s, -1s;
|
||||||
animation-delay: 1s, 1s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(2) {
|
.halloween:nth-of-type(2) {
|
||||||
left: 20%;
|
left: 20%;
|
||||||
-webkit-animation-delay: 6s, .5s;
|
animation-delay: -2s, -2s;
|
||||||
animation-delay: 6s, .5s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(3) {
|
.halloween:nth-of-type(3) {
|
||||||
left: 30%;
|
left: 30%;
|
||||||
-webkit-animation-delay: 4s, 2s;
|
animation-delay: -3s, -3s;
|
||||||
animation-delay: 4s, 2s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(4) {
|
.halloween:nth-of-type(4) {
|
||||||
left: 40%;
|
left: 40%;
|
||||||
-webkit-animation-delay: 2s, 2s;
|
animation-delay: -4s, -4s;
|
||||||
animation-delay: 2s, 2s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(5) {
|
.halloween:nth-of-type(5) {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
-webkit-animation-delay: 8s, 3s;
|
animation-delay: -5s, -5s;
|
||||||
animation-delay: 8s, 3s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(6) {
|
.halloween:nth-of-type(6) {
|
||||||
left: 60%;
|
left: 60%;
|
||||||
-webkit-animation-delay: 6s, 2s;
|
animation-delay: -6s, -6s;
|
||||||
animation-delay: 6s, 2s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(7) {
|
.halloween:nth-of-type(7) {
|
||||||
left: 70%;
|
left: 70%;
|
||||||
-webkit-animation-delay: 2.5s, 1s;
|
animation-delay: -7s, -7s;
|
||||||
animation-delay: 2.5s, 1s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(8) {
|
.halloween:nth-of-type(8) {
|
||||||
left: 80%;
|
left: 80%;
|
||||||
-webkit-animation-delay: 1s, 0s;
|
animation-delay: -8s, -8s;
|
||||||
animation-delay: 1s, 0s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(9) {
|
.halloween:nth-of-type(9) {
|
||||||
left: 90%;
|
left: 90%;
|
||||||
-webkit-animation-delay: 3s, 1.5s;
|
animation-delay: -9s, -9s;
|
||||||
animation-delay: 3s, 1.5s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(10) {
|
.halloween:nth-of-type(10) {
|
||||||
left: 25%;
|
left: 25%;
|
||||||
-webkit-animation-delay: 2s, 0s;
|
animation-delay: -10s, -10s;
|
||||||
animation-delay: 2s, 0s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(11) {
|
.halloween:nth-of-type(11) {
|
||||||
left: 65%;
|
left: 65%;
|
||||||
-webkit-animation-delay: 4s, 2.5s;
|
animation-delay: -11s, -11s;
|
||||||
animation-delay: 4s, 2.5s
|
}
|
||||||
|
|
||||||
|
/* --- Fog Layer --- */
|
||||||
|
.halloween-fog-layer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 40vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to top, black, transparent);
|
||||||
|
}
|
||||||
|
.halloween-fog-blob {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10vh;
|
||||||
|
width: 150vw;
|
||||||
|
height: 50vh;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(120, 130, 140, 0.4) 0%, transparent 60%);
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(15px);
|
||||||
|
}
|
||||||
|
.halloween-fog-blob:nth-child(1) {
|
||||||
|
will-change: transform;
|
||||||
|
left: -20vw;
|
||||||
|
animation: fog-float1 25s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
.halloween-fog-blob:nth-child(2) {
|
||||||
|
will-change: transform;
|
||||||
|
left: -50vw;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(100, 110, 120, 0.3) 0%, transparent 65%);
|
||||||
|
animation: fog-float2 35s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes fog-float1 {
|
||||||
|
0% { transform: translateX(0) scale(1); opacity: 0.8; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
100% { transform: translateX(20vw) scale(1.1); opacity: 0.6; }
|
||||||
|
}
|
||||||
|
@keyframes fog-float2 {
|
||||||
|
0% { transform: translateX(0) scale(1.1); opacity: 0.7; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
100% { transform: translateX(30vw) scale(1); opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Spiders --- */
|
||||||
|
.halloween-spider-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1002;
|
||||||
|
transform-origin: top;
|
||||||
|
will-change: transform;
|
||||||
|
pointer-events: auto;
|
||||||
|
padding: 20px; /* Increase hit area */
|
||||||
|
translate: 0 -50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.halloween-thread {
|
||||||
|
width: 30px; /* Wider hit area for mouse interaction */
|
||||||
|
height: 100vh;
|
||||||
|
margin-top: -100vh;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.halloween-thread::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to bottom, rgba(200, 200, 200, 0.1), rgba(200, 200, 200, 0.6));
|
||||||
|
}
|
||||||
|
.halloween-spider {
|
||||||
|
will-change: transform;
|
||||||
|
animation: spider-swing 3s ease-in-out infinite alternate;
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MARK: SPIDER SWAY CONFIGURATION */
|
||||||
|
@keyframes wind-sway {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(2deg); }
|
||||||
|
75% { transform: rotate(-2deg); }
|
||||||
|
100% { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spider-drop {
|
||||||
|
0% { transform: translateY(-50px); }
|
||||||
|
30% { transform: translateY(var(--drop-height, 50vh)); }
|
||||||
|
60% { transform: translateY(var(--drop-height, 50vh)); }
|
||||||
|
100% { transform: translateY(-50px); }
|
||||||
|
}
|
||||||
|
@keyframes spider-swing {
|
||||||
|
0% { transform: rotate(-10deg); }
|
||||||
|
100% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mice */
|
||||||
|
.halloween-mouse {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: left;
|
||||||
|
}
|
||||||
|
@keyframes mouse-run-right {
|
||||||
|
0% { left: -10vw; }
|
||||||
|
100% { left: 110vw; }
|
||||||
|
}
|
||||||
|
@keyframes mouse-run-left {
|
||||||
|
0% { left: 110vw; }
|
||||||
|
100% { left: -10vw; }
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,16 @@ const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween
|
|||||||
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
||||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
|
||||||
|
const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true;
|
||||||
|
const enableMice = config.EnableMice !== undefined ? config.EnableMice : true;
|
||||||
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
|
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
"../Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||||
|
"../Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||||
|
"../Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||||
|
];
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
// function to check and control the halloween
|
// function to check and control the halloween
|
||||||
@@ -36,21 +44,13 @@ function toggleHalloween() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleHalloween);
|
const observer = new MutationObserver(toggleHalloween);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const images = [
|
|
||||||
"/Seasonals/Resources/halloween_images/ghost_20x20.png",
|
|
||||||
"/Seasonals/Resources/halloween_images/bat_20x20.png",
|
|
||||||
"/Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
|
||||||
];
|
|
||||||
|
|
||||||
function addRandomSymbols(count) {
|
function addRandomSymbols(count) {
|
||||||
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
|
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
|
||||||
if (!halloweenContainer) return; // exit if halloween container is not found
|
if (!halloweenContainer) return; // exit if halloween container is not found
|
||||||
@@ -74,7 +74,7 @@ function addRandomSymbols(count) {
|
|||||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||||
const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s)
|
const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s)
|
||||||
const randomAnimationDelay2 = Math.random() * 3; // delay (0s to 3s)
|
const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
halloweenDiv.style.left = `${randomLeft}%`;
|
halloweenDiv.style.left = `${randomLeft}%`;
|
||||||
@@ -124,12 +124,143 @@ function createHalloween() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create fog layer
|
||||||
|
function createFog(container) {
|
||||||
|
const fogContainer = document.createElement('div');
|
||||||
|
fogContainer.className = 'halloween-fog-layer';
|
||||||
|
|
||||||
|
const fog1 = document.createElement('div');
|
||||||
|
fog1.className = 'halloween-fog-blob';
|
||||||
|
|
||||||
|
const fog2 = document.createElement('div');
|
||||||
|
fog2.className = 'halloween-fog-blob';
|
||||||
|
|
||||||
|
fogContainer.appendChild(fog1);
|
||||||
|
fogContainer.appendChild(fog2);
|
||||||
|
container.appendChild(fogContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create dropping spiders
|
||||||
|
function createSpider(container) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'halloween-spider-wrapper';
|
||||||
|
|
||||||
|
wrapper.innerHTML = `
|
||||||
|
<div class="halloween-sway" style="display:flex; flex-direction:column; align-items:center; transform-origin: 50% -100vh;">
|
||||||
|
<div class="halloween-thread"></div>
|
||||||
|
<svg class="halloween-spider" viewBox="0 0 24 24" width="30" height="30">
|
||||||
|
<circle cx="12" cy="12" r="6" fill="#1a1a1a"/>
|
||||||
|
<!-- left legs -->
|
||||||
|
<path d="M12 12 l-8 -4 M12 12 l-9 0 M12 12 l-8 4 M12 12 l-6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<!-- right legs -->
|
||||||
|
<path d="M12 12 l8 -4 M12 12 l9 0 M12 12 l8 4 M12 12 l6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="10" cy="14" r="1.5" fill="#ff3333"/>
|
||||||
|
<circle cx="14" cy="14" r="1.5" fill="#ff3333"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
wrapper.style.left = `${10 + Math.random() * 80}%`;
|
||||||
|
const dropHeight = 30 + Math.random() * 50; // 30vh to 80vh
|
||||||
|
wrapper.style.setProperty('--drop-height', `${dropHeight}vh`);
|
||||||
|
|
||||||
|
const duration = Math.random() * 6 + 6; // 6-12s drop
|
||||||
|
wrapper.style.animation = `spider-drop ${duration}s ease-in-out forwards`;
|
||||||
|
|
||||||
|
// Start the sway animation only after the drop completes (30% of total duration)
|
||||||
|
const sway = wrapper.querySelector('.halloween-sway');
|
||||||
|
sway.style.animation = `wind-sway 8s ease-in-out ${duration * 0.3}s infinite`;
|
||||||
|
|
||||||
|
// Spider retreat logic
|
||||||
|
let isRetreating = false;
|
||||||
|
wrapper.addEventListener('mouseenter', () => {
|
||||||
|
if (isRetreating) return;
|
||||||
|
isRetreating = true;
|
||||||
|
// Retreat smoothly by pushing margin up
|
||||||
|
wrapper.style.transition = 'margin-top 0.4s ease-in';
|
||||||
|
wrapper.style.marginTop = '-100vh';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.remove();
|
||||||
|
if (document.body.contains(container)) {
|
||||||
|
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.addEventListener('animationend', () => {
|
||||||
|
if (isRetreating) return;
|
||||||
|
wrapper.remove();
|
||||||
|
if (document.body.contains(container)) {
|
||||||
|
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create scurrying mice
|
||||||
|
function createMouse(container) {
|
||||||
|
const mouse = document.createElement('div');
|
||||||
|
mouse.className = 'halloween-mouse';
|
||||||
|
mouse.innerHTML = `
|
||||||
|
<svg viewBox="0 0 30 15" width="40" height="20">
|
||||||
|
<ellipse cx="15" cy="10" rx="10" ry="5" fill="#111"/>
|
||||||
|
<circle cx="24" cy="10" r="4" fill="#111"/>
|
||||||
|
<circle cx="24" cy="6" r="3" fill="#333"/>
|
||||||
|
<path d="M 5 10 Q 0 10 0 2" stroke="#111" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||||
|
const duration = Math.random() * 3 + 2; // 2-5s run (fast)
|
||||||
|
|
||||||
|
if (direction === 'right') {
|
||||||
|
mouse.style.animation = `mouse-run-right ${duration}s linear forwards`;
|
||||||
|
mouse.style.transform = 'scaleX(1)';
|
||||||
|
} else {
|
||||||
|
mouse.style.animation = `mouse-run-left ${duration}s linear forwards`;
|
||||||
|
mouse.style.transform = 'scaleX(-1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse.style.bottom = `5px`; // Fixated bottom edge
|
||||||
|
|
||||||
|
mouse.addEventListener('animationend', () => {
|
||||||
|
mouse.remove();
|
||||||
|
if (document.body.contains(container)) {
|
||||||
|
setTimeout(() => createMouse(container), Math.random() * 4000 + 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(mouse);
|
||||||
|
}
|
||||||
|
|
||||||
// initialize halloween
|
// initialize halloween
|
||||||
function initializeHalloween() {
|
function initializeHalloween() {
|
||||||
if (!halloween) return; // exit if halloween is disabled
|
if (!halloween) return; // exit if halloween is disabled
|
||||||
createHalloween();
|
createHalloween();
|
||||||
toggleHalloween();
|
toggleHalloween();
|
||||||
|
|
||||||
|
const container = document.querySelector('.halloween-container');
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
createFog(container);
|
||||||
|
|
||||||
|
// Add a few spiders
|
||||||
|
if (enableSpiders) {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
setTimeout(() => createSpider(container), Math.random() * 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a few mice
|
||||||
|
if (enableMice) {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
setTimeout(() => createMouse(container), Math.random() * 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
||||||
addRandomSymbols(halloweenCount);
|
addRandomSymbols(halloweenCount);
|
||||||
|
|||||||
@@ -8,59 +8,33 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart {
|
.heart {
|
||||||
|
will-change: transform;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: -10%;
|
bottom: -10%;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
-webkit-animation-name: heart-fall, heart-shake;
|
|
||||||
-webkit-animation-duration: 14s, 5s;
|
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
|
||||||
animation-name: heart-fall, heart-shake;
|
animation-name: heart-fall, heart-shake;
|
||||||
animation-duration: 14s, 5s;
|
animation-duration: 14s, 5s;
|
||||||
animation-timing-function: linear, ease-in-out;
|
animation-timing-function: linear, ease-in-out;
|
||||||
animation-iteration-count: infinite, infinite;
|
animation-iteration-count: infinite, infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes heart-fall {
|
|
||||||
0% {
|
|
||||||
bottom: -10%
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
bottom: 100%
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes heart-shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
transform: translateX(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px);
|
|
||||||
transform: translateX(80px)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes heart-fall {
|
@keyframes heart-fall {
|
||||||
0% {
|
0% {
|
||||||
bottom: -10%
|
bottom: -10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
bottom: 100%
|
bottom: 110%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,72 +52,60 @@
|
|||||||
|
|
||||||
.heart:nth-of-type(0) {
|
.heart:nth-of-type(0) {
|
||||||
left: 1%;
|
left: 1%;
|
||||||
-webkit-animation-delay: 0s, 0s;
|
|
||||||
animation-delay: 0s, 0s
|
animation-delay: 0s, 0s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(1) {
|
.heart:nth-of-type(1) {
|
||||||
left: 10%;
|
left: 10%;
|
||||||
-webkit-animation-delay: 1s, 1s;
|
|
||||||
animation-delay: 1s, 1s
|
animation-delay: 1s, 1s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(2) {
|
.heart:nth-of-type(2) {
|
||||||
left: 20%;
|
left: 20%;
|
||||||
-webkit-animation-delay: 6s, .5s;
|
|
||||||
animation-delay: 6s, .5s
|
animation-delay: 6s, .5s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(3) {
|
.heart:nth-of-type(3) {
|
||||||
left: 30%;
|
left: 30%;
|
||||||
-webkit-animation-delay: 4s, 2s;
|
|
||||||
animation-delay: 4s, 2s
|
animation-delay: 4s, 2s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(4) {
|
.heart:nth-of-type(4) {
|
||||||
left: 40%;
|
left: 40%;
|
||||||
-webkit-animation-delay: 2s, 2s;
|
|
||||||
animation-delay: 2s, 2s
|
animation-delay: 2s, 2s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(5) {
|
.heart:nth-of-type(5) {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
-webkit-animation-delay: 8s, 3s;
|
|
||||||
animation-delay: 8s, 3s
|
animation-delay: 8s, 3s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(6) {
|
.heart:nth-of-type(6) {
|
||||||
left: 60%;
|
left: 60%;
|
||||||
-webkit-animation-delay: 6s, 2s;
|
|
||||||
animation-delay: 6s, 2s
|
animation-delay: 6s, 2s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(7) {
|
.heart:nth-of-type(7) {
|
||||||
left: 70%;
|
left: 70%;
|
||||||
-webkit-animation-delay: 2.5s, 1s;
|
|
||||||
animation-delay: 2.5s, 1s
|
animation-delay: 2.5s, 1s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(8) {
|
.heart:nth-of-type(8) {
|
||||||
left: 80%;
|
left: 80%;
|
||||||
-webkit-animation-delay: 1s, 0s;
|
|
||||||
animation-delay: 1s, 0s
|
animation-delay: 1s, 0s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(9) {
|
.heart:nth-of-type(9) {
|
||||||
left: 90%;
|
left: 90%;
|
||||||
-webkit-animation-delay: 3s, 1.5s;
|
|
||||||
animation-delay: 3s, 1.5s
|
animation-delay: 3s, 1.5s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(10) {
|
.heart:nth-of-type(10) {
|
||||||
left: 25%;
|
left: 25%;
|
||||||
-webkit-animation-delay: 2s, 0s;
|
|
||||||
animation-delay: 2s, 0s
|
animation-delay: 2s, 0s
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(11) {
|
.heart:nth-of-type(11) {
|
||||||
left: 65%;
|
left: 65%;
|
||||||
-webkit-animation-delay: 4s, 2.5s;
|
|
||||||
animation-delay: 4s, 2.5s
|
animation-delay: 4s, 2.5s
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@ const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? con
|
|||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols
|
||||||
const heartsCount = config.SymbolCount || 25; // count of random extra symbols
|
const heartsCount = config.SymbolCount || 25; // count of random extra symbols
|
||||||
|
|
||||||
|
// Array of hearts characters
|
||||||
|
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
// function to check and control the hearts
|
// function to check and control the hearts
|
||||||
@@ -36,19 +39,13 @@ function toggleHearts() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleHearts);
|
const observer = new MutationObserver(toggleHearts);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Array of hearts characters
|
|
||||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
|
||||||
|
|
||||||
|
|
||||||
function addRandomSymbols(count) {
|
function addRandomSymbols(count) {
|
||||||
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
||||||
if (!heartsContainer) return; // exit if hearts container is not found
|
if (!heartsContainer) return; // exit if hearts container is not found
|
||||||
|
|||||||
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/mario-running.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/mario.gif
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/toad.gif
Normal file
|
After Width: | Height: | Size: 40 KiB |
80
Jellyfin.Plugin.Seasonals/Web/marioday.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
.marioday-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000;
|
||||||
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mario-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
left: -100px;
|
||||||
|
animation: mario-run 15s linear infinite;
|
||||||
|
will-change: left, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mario-runner {
|
||||||
|
width: 64px;
|
||||||
|
height: auto;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
display: block;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mario-jump {
|
||||||
|
will-change: transform;
|
||||||
|
animation: jump-arc 0.8s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8-bit coin styling */
|
||||||
|
.mario-coin {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: #ffd700;
|
||||||
|
border: 4px solid #b8860b;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: inset 4px 4px 0 #fffbea, inset -4px -4px 0 #daa520;
|
||||||
|
animation: pop-up-arc 2s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mario-coin::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 10px;
|
||||||
|
width: 4px;
|
||||||
|
height: 12px;
|
||||||
|
background: #daa520;
|
||||||
|
translate: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mario-run {
|
||||||
|
0% { left: -100px; transform: scaleX(1); }
|
||||||
|
45% { left: 110vw; transform: scaleX(1); }
|
||||||
|
50% { left: 110vw; transform: scaleX(-1); }
|
||||||
|
95% { left: -100px; transform: scaleX(-1); }
|
||||||
|
100% { left: -100px; transform: scaleX(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pop-up-arc {
|
||||||
|
0% { transform: translateY(0) rotateY(0deg); opacity: 0; animation-timing-function: ease-out; }
|
||||||
|
20% { opacity: 1; }
|
||||||
|
50% { transform: translateY(-30vh) rotateY(360deg); opacity: 1; animation-timing-function: ease-in; }
|
||||||
|
90% { opacity: 1; }
|
||||||
|
100% { transform: translateY(20vh) rotateY(720deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes jump-arc {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-25vh); }
|
||||||
|
100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
87
Jellyfin.Plugin.Seasonals/Web/marioday.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.MarioDay || {};
|
||||||
|
const marioday = config.EnableMarioDay !== undefined ? config.EnableMarioDay : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleMarioDay() {
|
||||||
|
const container = document.querySelector('.marioday-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('MarioDay hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('MarioDay visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleMarioDay);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function createMarioDay(container) {
|
||||||
|
// MARK: Mario's running speed across the screen
|
||||||
|
const marioSpeedSeconds = 18;
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'mario-wrapper';
|
||||||
|
wrapper.style.animationDuration = `${marioSpeedSeconds}s`;
|
||||||
|
|
||||||
|
const mario = document.createElement('img');
|
||||||
|
mario.className = 'mario-runner';
|
||||||
|
mario.src = '../Seasonals/Resources/mario_assets/mario.gif';
|
||||||
|
|
||||||
|
wrapper.appendChild(mario);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
// Periodically throw out an 8-bit coin
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
|
||||||
|
if (container.style.display === 'none') return;
|
||||||
|
const coin = document.createElement('div');
|
||||||
|
coin.className = 'mario-coin';
|
||||||
|
|
||||||
|
// Grab Mario's current screen position to lock the coin's X coordinate
|
||||||
|
const marioRect = wrapper.getBoundingClientRect();
|
||||||
|
coin.style.left = `${marioRect.left + 16}px`;
|
||||||
|
coin.style.bottom = '35px'; // bottom offset
|
||||||
|
|
||||||
|
container.appendChild(coin);
|
||||||
|
setTimeout(() => coin.remove(), 2000);
|
||||||
|
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function initializeMarioDay() {
|
||||||
|
if (!marioday) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.marioday-container') || document.createElement("div");
|
||||||
|
|
||||||
|
if (!document.querySelector('.marioday-container')) {
|
||||||
|
container.className = "marioday-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
createMarioDay(container);
|
||||||
|
toggleMarioDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeMarioDay();
|
||||||
11
Jellyfin.Plugin.Seasonals/Web/matrix.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.matrix-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
165
Jellyfin.Plugin.Seasonals/Web/matrix.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Matrix || {};
|
||||||
|
|
||||||
|
const enabled = config.EnableMatrix !== undefined ? config.EnableMatrix : true;
|
||||||
|
const maxTrails = config.SymbolCount || 25;
|
||||||
|
const backgroundMode = config.EnableMatrixBackground !== undefined ? config.EnableMatrixBackground : false;
|
||||||
|
const matrixChars = config.MatrixChars || '0123456789';
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
let isHidden = false;
|
||||||
|
|
||||||
|
// Toggle Function
|
||||||
|
function toggleMatrix() {
|
||||||
|
const container = document.querySelector('.matrix-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
if (!isHidden) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
isHidden = true;
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Matrix hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isHidden) {
|
||||||
|
container.style.display = 'block';
|
||||||
|
isHidden = false;
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Matrix visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleMatrix);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createElements() {
|
||||||
|
const container = document.querySelector('.matrix-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.matrix-container')) {
|
||||||
|
container.className = 'matrix-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
if (backgroundMode) container.style.zIndex = '5';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
container.appendChild(canvas);
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
// const chars = '0123456789πABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||||
|
const chars = matrixChars.split('');
|
||||||
|
const fontSize = 18;
|
||||||
|
|
||||||
|
class Trail {
|
||||||
|
constructor() {
|
||||||
|
this.reset();
|
||||||
|
this.y = Math.random() * -100; // Allow initial staggered start
|
||||||
|
}
|
||||||
|
reset() {
|
||||||
|
const cols = Math.floor(canvas.width / fontSize);
|
||||||
|
this.x = Math.floor(Math.random() * cols);
|
||||||
|
this.y = -Math.round(Math.random() * 20);
|
||||||
|
this.speed = 0.5 + Math.random() * 0.5;
|
||||||
|
this.len = 10 + Math.floor(Math.random() * 20);
|
||||||
|
this.chars = [];
|
||||||
|
for(let i=0; i<this.len; i++) {
|
||||||
|
this.chars.push(chars[Math.floor(Math.random() * chars.length)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update() {
|
||||||
|
const oldY = Math.floor(this.y);
|
||||||
|
this.y += this.speed;
|
||||||
|
const newY = Math.floor(this.y);
|
||||||
|
|
||||||
|
// If crossed a full vertical unit, push a new character and pop the old one to preserve screen positions
|
||||||
|
if (newY > oldY) {
|
||||||
|
this.chars.unshift(chars[Math.floor(Math.random() * chars.length)]);
|
||||||
|
this.chars.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Randomly mutate some characters (heads mutate faster)
|
||||||
|
for (let i = 0; i < this.len; i++) {
|
||||||
|
const chance = i < 3 ? 0.90 : 0.98;
|
||||||
|
if (Math.random() > chance) {
|
||||||
|
this.chars[i] = chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.y - this.len > Math.ceil(canvas.height / fontSize)) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw(ctx) {
|
||||||
|
const headY = Math.floor(this.y);
|
||||||
|
for (let i = 0; i < this.len; i++) {
|
||||||
|
const charY = headY - i;
|
||||||
|
if (charY < 0 || charY * fontSize > canvas.height + fontSize) continue;
|
||||||
|
|
||||||
|
const ratio = i / this.len;
|
||||||
|
const alpha = 1 - ratio;
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
|
||||||
|
ctx.shadowBlur = 8;
|
||||||
|
ctx.shadowColor = '#0F0';
|
||||||
|
} else if (i === 1) {
|
||||||
|
ctx.fillStyle = `rgba(150, 255, 150, ${alpha})`;
|
||||||
|
ctx.shadowBlur = 4;
|
||||||
|
ctx.shadowColor = '#0F0';
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = `rgba(0, 255, 0, ${alpha * 0.8})`;
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(this.chars[i], this.x * fontSize + fontSize/2, charY * fontSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trails = [];
|
||||||
|
for(let i=0; i<maxTrails; i++) trails.push(new Trail());
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
if (!document.body.contains(container)) { clearInterval(window.matrixInterval); return; }
|
||||||
|
if (isHidden) return; // Pause drawing when hidden
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.font = 'bold ' + fontSize + 'px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
for (const t of trails) {
|
||||||
|
t.update();
|
||||||
|
t.draw(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.matrixInterval) clearInterval(window.matrixInterval);
|
||||||
|
window.matrixInterval = setInterval(loop, 50);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeMatrix() {
|
||||||
|
if (!enabled) return;
|
||||||
|
createElements();
|
||||||
|
toggleMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeMatrix();
|
||||||
35
Jellyfin.Plugin.Seasonals/Web/oktoberfest.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
.oktoberfest-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oktoberfest-symbol {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
font-size: 2.2em;
|
||||||
|
user-select: none;
|
||||||
|
animation-name: oktoberfest-fall, oktoberfest-sway;
|
||||||
|
animation-timing-function: linear, ease-in-out;
|
||||||
|
animation-iteration-count: infinite, infinite;
|
||||||
|
translate: 0 -10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes oktoberfest-fall {
|
||||||
|
0% { transform: translateY(0); opacity: 0; }
|
||||||
|
10% { opacity: 1; }
|
||||||
|
100% { transform: translateY(120vh); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes oktoberfest-sway {
|
||||||
|
0%, 100% { margin-left: 0; }
|
||||||
|
50% { margin-left: 50px; }
|
||||||
|
}
|
||||||
67
Jellyfin.Plugin.Seasonals/Web/oktoberfest.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Oktoberfest || {};
|
||||||
|
const oktoberfest = config.EnableOktoberfest !== undefined ? config.EnableOktoberfest : true;
|
||||||
|
|
||||||
|
const oktoberfestSymbols = ['🥨', '🍺', '🍻', '🥨', '🥨'];
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// function to check and control the oktoberfest
|
||||||
|
function toggleOktoberfest() {
|
||||||
|
const oktoberfestContainer = document.querySelector('.oktoberfest-container');
|
||||||
|
if (!oktoberfestContainer) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
// hide oktoberfest if video/trailer player is active or dashboard is visible
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
oktoberfestContainer.style.display = 'none'; // hide oktoberfest
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Oktoberfest hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oktoberfestContainer.style.display = 'block'; // show oktoberfest
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Oktoberfest visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// observe changes in the DOM
|
||||||
|
const observer = new MutationObserver(toggleOktoberfest);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createOktoberfest(container) {
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const symbol = document.createElement('div');
|
||||||
|
symbol.className = 'oktoberfest-symbol';
|
||||||
|
symbol.textContent = oktoberfestSymbols[Math.floor(Math.random() * oktoberfestSymbols.length)];
|
||||||
|
symbol.style.left = `${Math.random() * 100}%`;
|
||||||
|
symbol.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 5}s`;
|
||||||
|
const duration1 = Math.random() * 5 + 8;
|
||||||
|
const duration2 = Math.random() * 3 + 3;
|
||||||
|
symbol.style.animationDuration = `${duration1}s, ${duration2}s`;
|
||||||
|
|
||||||
|
container.appendChild(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeOktoberfest() {
|
||||||
|
if (!oktoberfest) return;
|
||||||
|
const container = document.querySelector('.oktoberfest-container') || document.createElement("div");
|
||||||
|
if (!document.querySelector('.oktoberfest-container')) {
|
||||||
|
container.className = "oktoberfest-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
createOktoberfest(container);
|
||||||
|
}
|
||||||
|
initializeOktoberfest();
|
||||||
142
Jellyfin.Plugin.Seasonals/Web/olympia.css
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
.olympia-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-symbol {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
opacity: 0.95;
|
||||||
|
text-shadow: 0 0 10px rgba(255,255,255,0.2);
|
||||||
|
z-index: 40;
|
||||||
|
translate: 0 -10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-flame {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0vh;
|
||||||
|
z-index: 50;
|
||||||
|
pointer-events: none;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-ring-css {
|
||||||
|
position: relative;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.olympia-ring-css::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
translate: -50% -50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 5px solid #0081C8; /* Default blue ring */
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.olympia-ring-css[style*="--ring-color"]::before {
|
||||||
|
border-color: var(--ring-color);
|
||||||
|
}
|
||||||
|
.olympia-symbol {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
opacity: 0.95;
|
||||||
|
text-shadow: 0 0 10px rgba(255,255,255,0.2);
|
||||||
|
z-index: 40;
|
||||||
|
translate: 0 -10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-inner {
|
||||||
|
will-change: transform;
|
||||||
|
display: inline-block;
|
||||||
|
animation: olympia-sway linear infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-symbol img {
|
||||||
|
width: 6vh;
|
||||||
|
height: auto;
|
||||||
|
max-width: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-confetti-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 15;
|
||||||
|
top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: olympia-fall;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-confetti-sway {
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: olympia-confetti-sway;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes olympia-confetti-sway {
|
||||||
|
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
|
||||||
|
100% { transform: translateX(var(--sway-amount, 50px)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-confetti {
|
||||||
|
width: 8px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: olympia-confetti-flutter;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-confetti.circle {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-confetti.square {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.olympia-confetti.triangle {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes olympia-fall {
|
||||||
|
0% { transform: translateY(-10vh); }
|
||||||
|
100% { transform: translateY(110vh); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes olympia-sway {
|
||||||
|
0% { transform: rotate(-25deg) translateX(-20px); }
|
||||||
|
100% { transform: rotate(25deg) translateX(20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes olympia-tumble-3d {
|
||||||
|
0% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 0deg); }
|
||||||
|
100% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes olympia-confetti-flutter {
|
||||||
|
0% {
|
||||||
|
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
|
||||||
|
}
|
||||||
|
}
|
||||||
252
Jellyfin.Plugin.Seasonals/Web/olympia.js
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Olympia || {};
|
||||||
|
|
||||||
|
const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true;
|
||||||
|
const symbolCount = config.SymbolCount || 25;
|
||||||
|
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||||
|
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleOlympia() {
|
||||||
|
const container = document.querySelector('.olympia-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Olympia hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Olympia visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleOlympia);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createOlympia() {
|
||||||
|
const container = document.querySelector('.olympia-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.olympia-container')) {
|
||||||
|
container.className = 'olympia-container';
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const standardCount = 15;
|
||||||
|
const totalSymbols = symbolCount + standardCount;
|
||||||
|
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
let finalCount = totalSymbols;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
finalCount = enableRandomMobile ? totalSymbols : standardCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useRandomDuration = enableDifferentDuration !== false;
|
||||||
|
|
||||||
|
const activeItems = ['gold', 'silver', 'bronze', 'torch', 'rings_blue', 'rings_yellow', 'rings_black', 'rings_green', 'rings_red'];
|
||||||
|
|
||||||
|
for (let i = 0; i < finalCount; i++) {
|
||||||
|
let symbol = document.createElement('div');
|
||||||
|
|
||||||
|
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
|
||||||
|
const isRing = randomItem.startsWith('rings');
|
||||||
|
const isMedal = ['gold', 'silver', 'bronze'].includes(randomItem);
|
||||||
|
|
||||||
|
symbol.className = `olympia-symbol olympia-${randomItem}`;
|
||||||
|
|
||||||
|
// Create inner div for sway/rotation
|
||||||
|
let innerDiv = document.createElement('div');
|
||||||
|
innerDiv.className = 'olympia-inner';
|
||||||
|
let img = null;
|
||||||
|
|
||||||
|
if (isRing) {
|
||||||
|
const ringColorMap = {
|
||||||
|
'rings_blue': '#0081C8',
|
||||||
|
'rings_yellow': '#FCB131',
|
||||||
|
'rings_black': '#000000',
|
||||||
|
'rings_green': '#00A651',
|
||||||
|
'rings_red': '#EE334E'
|
||||||
|
};
|
||||||
|
let ringDiv = document.createElement('div');
|
||||||
|
ringDiv.className = 'olympia-ring-css';
|
||||||
|
ringDiv.style.setProperty('--ring-color', ringColorMap[randomItem]);
|
||||||
|
innerDiv.appendChild(ringDiv);
|
||||||
|
|
||||||
|
// Add a 3D flip animation for rings and medals
|
||||||
|
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
|
||||||
|
innerDiv.style.animation = `olympia-tumble-3d ${Math.random() * 4 + 4}s linear infinite ${spinReverse}`;
|
||||||
|
|
||||||
|
// Random 3D Rotation Axis for Tumbling
|
||||||
|
innerDiv.style.setProperty('--rot-x', (Math.random() * 2 - 1).toFixed(2));
|
||||||
|
innerDiv.style.setProperty('--rot-y', (Math.random() * 2 - 1).toFixed(2));
|
||||||
|
innerDiv.style.setProperty('--rot-z', (Math.random() * 2 - 1).toFixed(2));
|
||||||
|
} else {
|
||||||
|
img = document.createElement('img');
|
||||||
|
let imgName = randomItem;
|
||||||
|
if (isMedal) {
|
||||||
|
imgName = `${randomItem}_coin.gif`;
|
||||||
|
} else {
|
||||||
|
imgName = `${randomItem}.png`;
|
||||||
|
}
|
||||||
|
img.src = `../Seasonals/Resources/olympic_assets/${imgName}`;
|
||||||
|
img.onerror = function() {
|
||||||
|
symbol.remove();
|
||||||
|
};
|
||||||
|
innerDiv.appendChild(img);
|
||||||
|
|
||||||
|
if (isMedal) {
|
||||||
|
innerDiv.style.animation = `olympia-flip-3d ${Math.random() * 4 + 3}s linear infinite`;
|
||||||
|
} else {
|
||||||
|
// Torch sways, medals flip
|
||||||
|
const swayDur = Math.random() * 2 + 2; // 2 to 4s
|
||||||
|
const swayDir = Math.random() > 0.5 ? 'normal' : 'reverse';
|
||||||
|
innerDiv.style.animation = `olympia-sway ${swayDur}s ease-in-out infinite alternate ${swayDir}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol.appendChild(innerDiv);
|
||||||
|
|
||||||
|
const leftPos = Math.random() * 95;
|
||||||
|
const delaySeconds = Math.random() * 10;
|
||||||
|
|
||||||
|
// Depth logic for medals and rings
|
||||||
|
const depth = Math.random();
|
||||||
|
const scale = 0.8 + depth * 0.4; // 0.8 to 1.2
|
||||||
|
const zIndex = Math.floor(depth * 30) + 10;
|
||||||
|
|
||||||
|
if (img) {
|
||||||
|
img.style.transform = `scale(${scale})`;
|
||||||
|
} else {
|
||||||
|
innerDiv.firstChild.style.transform = `scale(${scale})`;
|
||||||
|
}
|
||||||
|
symbol.style.zIndex = zIndex;
|
||||||
|
|
||||||
|
let durationSeconds = 8;
|
||||||
|
if (useRandomDuration) {
|
||||||
|
durationSeconds = (1 - depth) * 5 + 6 + Math.random() * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol.style.animation = `olympia-fall ${durationSeconds}s linear infinite`;
|
||||||
|
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||||
|
symbol.style.left = `${leftPos}vw`;
|
||||||
|
|
||||||
|
container.appendChild(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Olympic Torches (Fixed at bottom corners, symmetrically rotated inward)
|
||||||
|
// Generate one random inward rotation (10 to 25 deg) for both to share
|
||||||
|
const sharedTilt = Math.random() * 15 + 10;
|
||||||
|
|
||||||
|
const createTorch = (isLeft) => {
|
||||||
|
const torch = document.createElement('div');
|
||||||
|
torch.className = 'olympia-flame';
|
||||||
|
|
||||||
|
if (isLeft) {
|
||||||
|
torch.style.left = '5vw';
|
||||||
|
// Lean right, face normal
|
||||||
|
torch.style.transform = `rotate(${sharedTilt}deg) scaleX(1)`;
|
||||||
|
} else {
|
||||||
|
torch.style.right = '5vw';
|
||||||
|
// Lean left, mirror image
|
||||||
|
torch.style.transform = `rotate(-${sharedTilt}deg) scaleX(-1)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let torchImg = document.createElement('img');
|
||||||
|
torchImg.src = `../Seasonals/Resources/olympic_assets/torch.gif`;
|
||||||
|
torchImg.style.height = '25vh';
|
||||||
|
torchImg.style.objectFit = 'contain';
|
||||||
|
torchImg.onerror = function() {
|
||||||
|
this.style.display = 'none';
|
||||||
|
};
|
||||||
|
torch.appendChild(torchImg);
|
||||||
|
container.appendChild(torch);
|
||||||
|
};
|
||||||
|
|
||||||
|
createTorch(true);
|
||||||
|
createTorch(false);
|
||||||
|
|
||||||
|
// Olympic Ring Colors (Carnival Config)
|
||||||
|
const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E'];
|
||||||
|
const confettiCount = isMobile ? 30 : 60;
|
||||||
|
|
||||||
|
for (let i = 0; i < confettiCount; i++) {
|
||||||
|
let wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'olympia-confetti-wrapper';
|
||||||
|
|
||||||
|
let leftPos = Math.random() * 100;
|
||||||
|
wrapper.style.left = `${leftPos}vw`;
|
||||||
|
|
||||||
|
let fallDuration = Math.random() * 3 + 4; // 4 to 7 seconds to fall
|
||||||
|
wrapper.style.animationDuration = `${fallDuration}s`;
|
||||||
|
wrapper.style.animationDelay = `-${Math.random() * fallDuration}s`; // Negative delay so it distributes perfectly immediately
|
||||||
|
|
||||||
|
let swayWrapper = document.createElement('div');
|
||||||
|
swayWrapper.className = 'olympia-confetti-sway';
|
||||||
|
let swayDuration = Math.random() * 2 + 1.5; // 1.5s to 3.5s
|
||||||
|
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||||
|
let swayAmount = Math.random() * 30 + 30; // 30px to 60px
|
||||||
|
swayWrapper.style.setProperty('--sway-amount', `${swayAmount}px`);
|
||||||
|
let initSwayDelay = Math.random() * swayDuration;
|
||||||
|
swayWrapper.style.animationDelay = `-${initSwayDelay}s`;
|
||||||
|
|
||||||
|
let confetti = document.createElement('div');
|
||||||
|
confetti.className = 'olympia-confetti';
|
||||||
|
|
||||||
|
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
|
||||||
|
confetti.style.backgroundColor = color;
|
||||||
|
|
||||||
|
// Random shape
|
||||||
|
const shape = Math.random();
|
||||||
|
if (shape > 0.66) {
|
||||||
|
confetti.classList.add('circle');
|
||||||
|
const size = Math.random() * 5 + 5;
|
||||||
|
confetti.style.width = `${size}px`;
|
||||||
|
confetti.style.height = `${size}px`;
|
||||||
|
} else if (shape > 0.33) {
|
||||||
|
confetti.classList.add('rect');
|
||||||
|
const width = Math.random() * 4 + 4;
|
||||||
|
const height = Math.random() * 5 + 8;
|
||||||
|
confetti.style.width = `${width}px`;
|
||||||
|
confetti.style.height = `${height}px`;
|
||||||
|
} else {
|
||||||
|
confetti.classList.add('triangle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random 3D Rotation for flutter
|
||||||
|
confetti.style.setProperty('--rx', Math.random().toFixed(2));
|
||||||
|
confetti.style.setProperty('--ry', Math.random().toFixed(2));
|
||||||
|
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||||
|
confetti.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
|
||||||
|
let rotateDuration = Math.random() * 0.8 + 0.4;
|
||||||
|
confetti.style.animationDuration = `${rotateDuration}s`;
|
||||||
|
|
||||||
|
swayWrapper.appendChild(confetti);
|
||||||
|
wrapper.appendChild(swayWrapper);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeOlympia() {
|
||||||
|
if (!olympia) return;
|
||||||
|
createOlympia();
|
||||||
|
toggleOlympia();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeOlympia();
|
||||||
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/bronze_coin.gif
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/gold_coin.gif
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/ring_black.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/ring_blue.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/ring_green.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/ring_red.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/ring_yellow.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/silver_coin.gif
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/torch.gif
Normal file
|
After Width: | Height: | Size: 123 KiB |
70
Jellyfin.Plugin.Seasonals/Web/oscar.css
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
.oscar-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
contain: strict;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oscar-carpet {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 15vh;
|
||||||
|
background: linear-gradient(to top, rgba(139, 0, 0, 0.8) 0%, transparent 100%);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oscar-spotlights {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oscar-spotlight {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
/* MARK: SPOTLIGHT WIDTH CONFIGURATION */
|
||||||
|
/* To adjust bottom width (spread), change 'width' property (e.g., 20vw for narrow, 40vw for wide). */
|
||||||
|
/* To adjust top width (origin), modify first two percentages in 'clip-path' (e.g., 48% 0, 52% 0 for a very thin start). */
|
||||||
|
width: 30vw;
|
||||||
|
height: 120vh;
|
||||||
|
background: linear-gradient(to bottom, rgba(255, 215, 0, 0.4) 0%, transparent 80%);
|
||||||
|
clip-path: polygon(45% 0, 55% 0, 100% 100%, 0 100%);
|
||||||
|
transform-origin: top center;
|
||||||
|
animation: spotlight-sweep 12s infinite alternate ease-in-out;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
translate: 0 -10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oscar-flash {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 50px 30px rgba(255, 255, 255, 0.8), 0 0 100px 50px rgba(255, 255, 255, 0.5);
|
||||||
|
animation: flash-pop 0.2s cubic-bezier(0.1, 0.8, 0.1, 1);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spotlight-sweep {
|
||||||
|
0% { transform: rotate(-30deg); }
|
||||||
|
100% { transform: rotate(30deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash-pop {
|
||||||
|
0% { transform: scale(0.5); opacity: 1; }
|
||||||
|
50% { transform: scale(2); opacity: 1; }
|
||||||
|
100% { transform: scale(3); opacity: 0; }
|
||||||
|
}
|
||||||
94
Jellyfin.Plugin.Seasonals/Web/oscar.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Oscar || {};
|
||||||
|
const oscar = config.EnableOscar !== undefined ? config.EnableOscar : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleOscar() {
|
||||||
|
const container = document.querySelector('.oscar-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Oscar hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Oscar visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleOscar);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function createOscar(container) {
|
||||||
|
// Red carpet floor
|
||||||
|
const carpet = document.createElement('div');
|
||||||
|
carpet.className = 'oscar-carpet';
|
||||||
|
|
||||||
|
// Spotlights
|
||||||
|
const spotlights = document.createElement('div');
|
||||||
|
spotlights.className = 'oscar-spotlights';
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const spot = document.createElement('div');
|
||||||
|
spot.className = 'oscar-spotlight';
|
||||||
|
spot.style.animationDelay = `-${Math.random() * 8}s`;
|
||||||
|
spot.style.left = `${20 + (i * 30)}%`;
|
||||||
|
spot.style.top = `${-5 - Math.random() * 15}vh`; // randomize top origin
|
||||||
|
spotlights.appendChild(spot);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(carpet);
|
||||||
|
container.appendChild(spotlights);
|
||||||
|
|
||||||
|
function flashLoop() {
|
||||||
|
if (!document.body.contains(container)) return; // Kill the loop if container is removed
|
||||||
|
if (container.style.display === 'none') {
|
||||||
|
setTimeout(flashLoop, 1000); // Check again later if hidden
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const flash = document.createElement('div');
|
||||||
|
flash.className = 'oscar-flash';
|
||||||
|
flash.style.left = `${Math.random() * 100}%`;
|
||||||
|
flash.style.top = `${Math.random() * 100}%`;
|
||||||
|
container.appendChild(flash);
|
||||||
|
setTimeout(() => flash.remove(), 200);
|
||||||
|
|
||||||
|
// Randomize next flash between 200ms and 1500ms
|
||||||
|
const nextDelay = Math.random() * 1300 + 200;
|
||||||
|
setTimeout(flashLoop, nextDelay);
|
||||||
|
}
|
||||||
|
flashLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeOscar() {
|
||||||
|
if (!oscar) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.oscar-container') || document.createElement("div");
|
||||||
|
|
||||||
|
if (!document.querySelector('.oscar-container')) {
|
||||||
|
container.className = "oscar-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
createOscar(container);
|
||||||
|
toggleOscar();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeOscar();
|
||||||
33
Jellyfin.Plugin.Seasonals/Web/pride.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.pride-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pride-heart {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -50px;
|
||||||
|
animation: pride-rise ease-in infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes pride-rise {
|
||||||
|
0% { transform: translateY(0) scale(0.8); opacity: 0; }
|
||||||
|
10% { opacity: 1; }
|
||||||
|
90% { opacity: 1; }
|
||||||
|
100% { transform: translateY(-115vh) scale(1.2); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Coloring the Jellyfin Header */
|
||||||
|
body.pride-active .skinHeader,
|
||||||
|
body.pride-active .skinHeader-withBackground {
|
||||||
|
background: linear-gradient(90deg, #E40303, #FF8C00, #FFED00, #008026, #24408E, #732982) !important;
|
||||||
|
}
|
||||||
84
Jellyfin.Plugin.Seasonals/Web/pride.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Pride || {};
|
||||||
|
|
||||||
|
const enabled = config.EnablePride !== undefined ? config.EnablePride : true;
|
||||||
|
const elementCount = config.HeartCount || 20;
|
||||||
|
const heartSize = config.HeartSize || 1.5;
|
||||||
|
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function togglePride() {
|
||||||
|
const container = document.querySelector('.pride-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Pride hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Pride visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(togglePride);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createElements() {
|
||||||
|
const container = document.querySelector('.pride-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.pride-container')) {
|
||||||
|
container.className = 'pride-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorHeader) {
|
||||||
|
document.body.classList.add('pride-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupObserver = new MutationObserver(() => {
|
||||||
|
if (!document.querySelector('.pride-container')) {
|
||||||
|
document.body.classList.remove('pride-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cleanupObserver.observe(document.body, { childList: true });
|
||||||
|
|
||||||
|
const heartEmojis = ['❤️', '🧡', '💛', '💚', '💙', '💜'];
|
||||||
|
|
||||||
|
for (let i = 0; i < elementCount; i++) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'pride-heart';
|
||||||
|
|
||||||
|
el.innerText = heartEmojis[Math.floor(Math.random() * heartEmojis.length)];
|
||||||
|
el.style.fontSize = `${heartSize}rem`;
|
||||||
|
el.style.left = `${Math.random() * 100}vw`;
|
||||||
|
el.style.animationDuration = `${5 + Math.random() * 5}s`;
|
||||||
|
el.style.animationDelay = `${Math.random() * 5}s`;
|
||||||
|
el.style.marginLeft = `${(Math.random() - 0.5) * 100}px`;
|
||||||
|
|
||||||
|
container.appendChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializePride() {
|
||||||
|
if (!enabled) return;
|
||||||
|
createElements();
|
||||||
|
togglePride();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializePride();
|
||||||
26
Jellyfin.Plugin.Seasonals/Web/rain.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.rain-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raindrop-pure {
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
|
||||||
|
transform-origin: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pure-rain {
|
||||||
|
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
|
||||||
|
5% { opacity: 1; }
|
||||||
|
95% { opacity: 1; }
|
||||||
|
100% { transform: translateY(180vh) translateX(-60vh) rotate(20deg); opacity: 0; }
|
||||||
|
}
|
||||||
73
Jellyfin.Plugin.Seasonals/Web/rain.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Rain || {};
|
||||||
|
|
||||||
|
const enabled = config.EnableRain !== undefined ? config.EnableRain : true;
|
||||||
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
|
||||||
|
const rainSpeed = config.RainSpeed || 1.0;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
// Toggle Function
|
||||||
|
function toggleRain() {
|
||||||
|
const container = document.querySelector('.rain-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Rain hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Rain visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleRain);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createElements() {
|
||||||
|
const container = document.querySelector('.rain-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.rain-container')) {
|
||||||
|
container.className = 'rain-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < elementCount; i++) {
|
||||||
|
const drop = document.createElement('div');
|
||||||
|
drop.className = 'raindrop-pure';
|
||||||
|
|
||||||
|
drop.style.left = `${Math.random() * 140}vw`;
|
||||||
|
drop.style.top = `${-20 - Math.random() * 50}vh`;
|
||||||
|
|
||||||
|
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
|
||||||
|
drop.style.animation = `pure-rain ${duration}s linear infinite`;
|
||||||
|
drop.style.animationDelay = `${Math.random() * 2}s`;
|
||||||
|
drop.style.opacity = Math.random() * 0.5 + 0.3;
|
||||||
|
|
||||||
|
container.appendChild(drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeRain() {
|
||||||
|
if (!enabled) return;
|
||||||
|
createElements();
|
||||||
|
toggleRain();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeRain();
|
||||||
66
Jellyfin.Plugin.Seasonals/Web/resurrection.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
.resurrection-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resurrection-symbol {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 15;
|
||||||
|
top: 0;
|
||||||
|
translate: 0 -15vh;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
animation-name: resurrection-fall;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resurrection-sway-wrapper {
|
||||||
|
will-change: transform;
|
||||||
|
animation-name: resurrection-sway;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resurrection-symbol img {
|
||||||
|
z-index: 15;
|
||||||
|
height: auto;
|
||||||
|
width: 56px;
|
||||||
|
opacity: 0.95;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255, 215, 130, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.resurrection-symbol img {
|
||||||
|
width: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes resurrection-fall {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, -15vh, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 105vh, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes resurrection-sway {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(65px);
|
||||||
|
}
|
||||||
120
Jellyfin.Plugin.Seasonals/Web/resurrection.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Resurrection || {};
|
||||||
|
|
||||||
|
const enableResurrection = config.EnableResurrection !== undefined ? config.EnableResurrection : true;
|
||||||
|
const enableRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||||
|
const enableRandomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||||
|
const symbolCount = config.SymbolCount || 12;
|
||||||
|
|
||||||
|
let animationEnabled = true;
|
||||||
|
let statusLogged = false;
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
'../Seasonals/Resources/resurrection_images/crosses.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/palm-branch.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/draped-cross.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/empty-tomb.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/he-is-risen.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/crown-of-thorns.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/risen-lord.png',
|
||||||
|
'../Seasonals/Resources/resurrection_images/dove.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
function toggleResurrection() {
|
||||||
|
const container = document.querySelector('.resurrection-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
|
animationEnabled = !(videoPlayer || trailerPlayer || isDashboard || hasUserMenu);
|
||||||
|
container.style.display = animationEnabled ? 'block' : 'none';
|
||||||
|
|
||||||
|
if (!animationEnabled && !statusLogged) {
|
||||||
|
console.log('Resurrection hidden');
|
||||||
|
statusLogged = true;
|
||||||
|
} else if (animationEnabled && statusLogged) {
|
||||||
|
console.log('Resurrection visible');
|
||||||
|
statusLogged = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleResurrection);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createSymbol(imageSrc, leftPercent, delaySeconds) {
|
||||||
|
const symbol = document.createElement('div');
|
||||||
|
symbol.className = 'resurrection-symbol';
|
||||||
|
|
||||||
|
const swayWrapper = document.createElement('div');
|
||||||
|
swayWrapper.className = 'resurrection-sway-wrapper';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = imageSrc;
|
||||||
|
img.alt = '';
|
||||||
|
|
||||||
|
symbol.style.left = `${leftPercent}%`;
|
||||||
|
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||||
|
|
||||||
|
if (enableDifferentDuration) {
|
||||||
|
const fallDuration = Math.random() * 7 + 7;
|
||||||
|
const swayDuration = Math.random() * 4 + 2;
|
||||||
|
symbol.style.animationDuration = `${fallDuration}s`;
|
||||||
|
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
swayWrapper.style.animationDelay = `${Math.random() * 3}s`;
|
||||||
|
|
||||||
|
swayWrapper.appendChild(img);
|
||||||
|
symbol.appendChild(swayWrapper);
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSymbols(count) {
|
||||||
|
const container = document.querySelector('.resurrection-container');
|
||||||
|
if (!container || !enableRandomSymbols) return;
|
||||||
|
|
||||||
|
const isDesktop = window.innerWidth > 768;
|
||||||
|
if (!isDesktop && !enableRandomSymbolsMobile) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||||
|
const left = Math.random() * 100;
|
||||||
|
const delay = Math.random() * 12;
|
||||||
|
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initResurrection() {
|
||||||
|
let container = document.querySelector('.resurrection-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'resurrection-container';
|
||||||
|
container.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place one of each of the 8 provided resurrection images first.
|
||||||
|
images.forEach((imageSrc, index) => {
|
||||||
|
const left = (index + 1) * (100 / (images.length + 1));
|
||||||
|
const delay = Math.random() * 8;
|
||||||
|
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraCount = Math.max(symbolCount - images.length, 0);
|
||||||
|
addSymbols(extraCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeResurrection() {
|
||||||
|
if (!enableResurrection) return;
|
||||||
|
initResurrection();
|
||||||
|
toggleResurrection();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeResurrection();
|
||||||
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/crosses.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 317 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/dove.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 382 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/empty-tomb.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 384 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/risen-lord.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
@@ -11,6 +11,21 @@ const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa res
|
|||||||
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
|
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
|
||||||
const minPresentFallSpeed = config.MinPresentFallSpeed || 2; // minimum speed of falling presents in seconds
|
const minPresentFallSpeed = config.MinPresentFallSpeed || 2; // minimum speed of falling presents in seconds
|
||||||
|
|
||||||
|
// credits: flaticon.com
|
||||||
|
const presentImages = [
|
||||||
|
'../Seasonals/Resources/santa_images/gift1.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift2.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift3.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift4.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift5.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift6.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift7.png',
|
||||||
|
'../Seasonals/Resources/santa_images/gift8.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
|
||||||
|
const santaImage = '../Seasonals/Resources/santa_images/santa.gif';
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
let isMobile = false; // flag to detect mobile devices
|
let isMobile = false; // flag to detect mobile devices
|
||||||
let canvas, ctx; // canvas and context for drawing snowflakes
|
let canvas, ctx; // canvas and context for drawing snowflakes
|
||||||
@@ -52,12 +67,10 @@ function toggleSnowfall() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleSnowfall);
|
const observer = new MutationObserver(toggleSnowfall);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
let resizeObserver; // Observer for resize events
|
let resizeObserver; // Observer for resize events
|
||||||
@@ -179,22 +192,6 @@ function updateSnowflakes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// credits: flaticon.com
|
|
||||||
const presentImages = [
|
|
||||||
'/Seasonals/Resources/santa_images/gift1.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift2.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift3.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift4.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift5.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift6.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift7.png',
|
|
||||||
'/Seasonals/Resources/santa_images/gift8.png',
|
|
||||||
];
|
|
||||||
|
|
||||||
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
|
|
||||||
const santaImage = '/Seasonals/Resources/santa_images/santa.gif';
|
|
||||||
|
|
||||||
|
|
||||||
function createSantaElement() {
|
function createSantaElement() {
|
||||||
const santa = document.createElement('img');
|
const santa = document.createElement('img');
|
||||||
santa.src = santaImage;
|
santa.src = santaImage;
|
||||||
@@ -241,7 +238,7 @@ function animateSanta() {
|
|||||||
function startAnimation() {
|
function startAnimation() {
|
||||||
const santaHeight = santa.offsetHeight;
|
const santaHeight = santa.offsetHeight;
|
||||||
if (santaHeight === 0) {
|
if (santaHeight === 0) {
|
||||||
setTimeout(startAnimation, 100);
|
setTimeout(() => { if (document.body.contains(santa)) startAnimation(); }, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// console.log('Santa height: ', santaHeight);
|
// console.log('Santa height: ', santaHeight);
|
||||||
@@ -286,7 +283,7 @@ function animateSanta() {
|
|||||||
animationFrameIdSanta = requestAnimationFrame(move);
|
animationFrameIdSanta = requestAnimationFrame(move);
|
||||||
} else {
|
} else {
|
||||||
const pause = Math.random() * ((maxSantaRestTime - minSantaRestTime) * 1000) + minSantaRestTime * 1000;
|
const pause = Math.random() * ((maxSantaRestTime - minSantaRestTime) * 1000) + minSantaRestTime * 1000;
|
||||||
setTimeout(animateSanta, pause);
|
setTimeout(() => { if (document.body.contains(santa)) animateSanta(); }, pause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,206 +1,513 @@
|
|||||||
// theme-configs.js
|
/*
|
||||||
|
* Seasonals Plugin (Client Side Manager Logic)
|
||||||
|
*/
|
||||||
|
|
||||||
// theme configurations
|
const ThemeConfigs = {
|
||||||
const themeConfigs = {
|
|
||||||
snowflakes: {
|
snowflakes: {
|
||||||
css: '/Seasonals/Resources/snowflakes.css',
|
css: '../Seasonals/Resources/snowflakes.css',
|
||||||
js: '/Seasonals/Resources/snowflakes.js',
|
js: '../Seasonals/Resources/snowflakes.js',
|
||||||
containerClass: 'snowflakes'
|
containerClass: 'snowflakes'
|
||||||
},
|
},
|
||||||
snowfall: {
|
snowfall: {
|
||||||
css: '/Seasonals/Resources/snowfall.css',
|
css: '../Seasonals/Resources/snowfall.css',
|
||||||
js: '/Seasonals/Resources/snowfall.js',
|
js: '../Seasonals/Resources/snowfall.js',
|
||||||
containerClass: 'snowfall-container'
|
containerClass: 'snowfall-container'
|
||||||
},
|
},
|
||||||
snowstorm: {
|
snowstorm: {
|
||||||
css: '/Seasonals/Resources/snowstorm.css',
|
css: '../Seasonals/Resources/snowstorm.css',
|
||||||
js: '/Seasonals/Resources/snowstorm.js',
|
js: '../Seasonals/Resources/snowstorm.js',
|
||||||
containerClass: 'snowstorm-container'
|
containerClass: 'snowstorm-container'
|
||||||
},
|
},
|
||||||
fireworks: {
|
fireworks: {
|
||||||
css: '/Seasonals/Resources/fireworks.css',
|
css: '../Seasonals/Resources/fireworks.css',
|
||||||
js: '/Seasonals/Resources/fireworks.js',
|
js: '../Seasonals/Resources/fireworks.js',
|
||||||
containerClass: 'fireworks'
|
containerClass: 'fireworks'
|
||||||
},
|
},
|
||||||
halloween: {
|
halloween: {
|
||||||
css: '/Seasonals/Resources/halloween.css',
|
css: '../Seasonals/Resources/halloween.css',
|
||||||
js: '/Seasonals/Resources/halloween.js',
|
js: '../Seasonals/Resources/halloween.js',
|
||||||
containerClass: 'halloween-container'
|
containerClass: 'halloween-container'
|
||||||
},
|
},
|
||||||
hearts: {
|
hearts: {
|
||||||
css: '/Seasonals/Resources/hearts.css',
|
css: '../Seasonals/Resources/hearts.css',
|
||||||
js: '/Seasonals/Resources/hearts.js',
|
js: '../Seasonals/Resources/hearts.js',
|
||||||
containerClass: 'hearts-container'
|
containerClass: 'hearts-container'
|
||||||
},
|
},
|
||||||
christmas: {
|
christmas: {
|
||||||
css: '/Seasonals/Resources/christmas.css',
|
css: '../Seasonals/Resources/christmas.css',
|
||||||
js: '/Seasonals/Resources/christmas.js',
|
js: '../Seasonals/Resources/christmas.js',
|
||||||
containerClass: 'christmas-container'
|
containerClass: 'christmas-container'
|
||||||
},
|
},
|
||||||
santa: {
|
santa: {
|
||||||
css: '/Seasonals/Resources/santa.css',
|
css: '../Seasonals/Resources/santa.css',
|
||||||
js: '/Seasonals/Resources/santa.js',
|
js: '../Seasonals/Resources/santa.js',
|
||||||
containerClass: 'santa-container'
|
containerClass: 'santa-container'
|
||||||
},
|
},
|
||||||
autumn: {
|
autumn: {
|
||||||
css: '/Seasonals/Resources/autumn.css',
|
css: '../Seasonals/Resources/autumn.css',
|
||||||
js: '/Seasonals/Resources/autumn.js',
|
js: '../Seasonals/Resources/autumn.js',
|
||||||
containerClass: 'autumn-container'
|
containerClass: 'autumn-container'
|
||||||
},
|
},
|
||||||
easter: {
|
easter: {
|
||||||
css: '/Seasonals/Resources/easter.css',
|
css: '../Seasonals/Resources/easter.css',
|
||||||
js: '/Seasonals/Resources/easter.js',
|
js: '../Seasonals/Resources/easter.js',
|
||||||
containerClass: 'easter-container'
|
containerClass: 'easter-container'
|
||||||
},
|
},
|
||||||
|
resurrection: {
|
||||||
|
css: '../Seasonals/Resources/resurrection.css',
|
||||||
|
js: '../Seasonals/Resources/resurrection.js',
|
||||||
|
containerClass: 'resurrection-container'
|
||||||
|
},
|
||||||
summer: {
|
summer: {
|
||||||
css: '/Seasonals/Resources/summer.css',
|
css: '../Seasonals/Resources/summer.css',
|
||||||
js: '/Seasonals/Resources/summer.js',
|
js: '../Seasonals/Resources/summer.js',
|
||||||
containerClass: 'summer-container'
|
containerClass: 'summer-container'
|
||||||
},
|
},
|
||||||
spring: {
|
spring: {
|
||||||
css: '/Seasonals/Resources/spring.css',
|
css: '../Seasonals/Resources/spring.css',
|
||||||
js: '/Seasonals/Resources/spring.js',
|
js: '../Seasonals/Resources/spring.js',
|
||||||
containerClass: 'spring-container'
|
containerClass: 'spring-container'
|
||||||
},
|
},
|
||||||
|
carnival: {
|
||||||
|
css: '../Seasonals/Resources/carnival.css',
|
||||||
|
js: '../Seasonals/Resources/carnival.js',
|
||||||
|
containerClass: 'carnival-container'
|
||||||
|
},
|
||||||
|
cherryblossom: {
|
||||||
|
css: '../Seasonals/Resources/cherryblossom.css',
|
||||||
|
js: '../Seasonals/Resources/cherryblossom.js',
|
||||||
|
containerClass: 'cherryblossom-container'
|
||||||
|
},
|
||||||
|
matrix: {
|
||||||
|
css: '../Seasonals/Resources/matrix.css',
|
||||||
|
js: '../Seasonals/Resources/matrix.js',
|
||||||
|
containerClass: 'matrix-container'
|
||||||
|
},
|
||||||
|
eurovision: {
|
||||||
|
css: '../Seasonals/Resources/eurovision.css',
|
||||||
|
js: '../Seasonals/Resources/eurovision.js',
|
||||||
|
containerClass: 'eurovision-container'
|
||||||
|
},
|
||||||
|
storm: {
|
||||||
|
css: '../Seasonals/Resources/storm.css',
|
||||||
|
js: '../Seasonals/Resources/storm.js',
|
||||||
|
containerClass: 'storm-container'
|
||||||
|
},
|
||||||
|
pride: {
|
||||||
|
css: '../Seasonals/Resources/pride.css',
|
||||||
|
js: '../Seasonals/Resources/pride.js',
|
||||||
|
containerClass: 'pride-container'
|
||||||
|
},
|
||||||
|
rain: {
|
||||||
|
css: '../Seasonals/Resources/rain.css',
|
||||||
|
js: '../Seasonals/Resources/rain.js',
|
||||||
|
containerClass: 'rain-container'
|
||||||
|
},
|
||||||
|
earthday: {
|
||||||
|
css: '../Seasonals/Resources/earthday.css',
|
||||||
|
js: '../Seasonals/Resources/earthday.js',
|
||||||
|
containerClass: 'earthday-container'
|
||||||
|
},
|
||||||
|
frost: {
|
||||||
|
css: '../Seasonals/Resources/frost.css',
|
||||||
|
js: '../Seasonals/Resources/frost.js',
|
||||||
|
containerClass: 'frost-container'
|
||||||
|
},
|
||||||
|
filmnoir: {
|
||||||
|
css: '../Seasonals/Resources/filmnoir.css',
|
||||||
|
js: '../Seasonals/Resources/filmnoir.js',
|
||||||
|
containerClass: 'filmnoir-container'
|
||||||
|
},
|
||||||
|
oscar: {
|
||||||
|
css: '../Seasonals/Resources/oscar.css',
|
||||||
|
js: '../Seasonals/Resources/oscar.js',
|
||||||
|
containerClass: 'oscar-container'
|
||||||
|
},
|
||||||
|
marioday: {
|
||||||
|
css: '../Seasonals/Resources/marioday.css',
|
||||||
|
js: '../Seasonals/Resources/marioday.js',
|
||||||
|
containerClass: 'marioday-container'
|
||||||
|
},
|
||||||
|
starwars: {
|
||||||
|
css: '../Seasonals/Resources/starwars.css',
|
||||||
|
js: '../Seasonals/Resources/starwars.js',
|
||||||
|
containerClass: 'starwars-container'
|
||||||
|
},
|
||||||
|
oktoberfest: {
|
||||||
|
css: '../Seasonals/Resources/oktoberfest.css',
|
||||||
|
js: '../Seasonals/Resources/oktoberfest.js',
|
||||||
|
containerClass: 'oktoberfest-container'
|
||||||
|
},
|
||||||
|
friday13: {
|
||||||
|
css: '../Seasonals/Resources/friday13.css',
|
||||||
|
js: '../Seasonals/Resources/friday13.js',
|
||||||
|
containerClass: 'friday13-container'
|
||||||
|
},
|
||||||
|
eid: {
|
||||||
|
css: '../Seasonals/Resources/eid.css',
|
||||||
|
js: '../Seasonals/Resources/eid.js',
|
||||||
|
containerClass: 'eid-container'
|
||||||
|
},
|
||||||
|
spooky: {
|
||||||
|
css: '../Seasonals/Resources/spooky.css',
|
||||||
|
js: '../Seasonals/Resources/spooky.js',
|
||||||
|
containerClass: 'spooky-container'
|
||||||
|
},
|
||||||
|
sports: {
|
||||||
|
css: '../Seasonals/Resources/sports.css',
|
||||||
|
js: '../Seasonals/Resources/sports.js',
|
||||||
|
containerClass: 'sports-container'
|
||||||
|
},
|
||||||
|
olympia: {
|
||||||
|
css: '../Seasonals/Resources/olympia.css',
|
||||||
|
js: '../Seasonals/Resources/olympia.js',
|
||||||
|
containerClass: 'olympia-container'
|
||||||
|
},
|
||||||
|
space: {
|
||||||
|
css: '../Seasonals/Resources/space.css',
|
||||||
|
js: '../Seasonals/Resources/space.js',
|
||||||
|
containerClass: 'space-container'
|
||||||
|
},
|
||||||
|
underwater: {
|
||||||
|
css: '../Seasonals/Resources/underwater.css',
|
||||||
|
js: '../Seasonals/Resources/underwater.js',
|
||||||
|
containerClass: 'underwater-container'
|
||||||
|
},
|
||||||
|
birthday: {
|
||||||
|
css: '../Seasonals/Resources/birthday.css',
|
||||||
|
js: '../Seasonals/Resources/birthday.js',
|
||||||
|
containerClass: 'birthday-container'
|
||||||
|
},
|
||||||
none: {
|
none: {
|
||||||
containerClass: 'none'
|
containerClass: 'none'
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// determine current theme based on the current month
|
const SeasonalSettingsManager = {
|
||||||
function determineCurrentTheme() {
|
initialized: false,
|
||||||
const date = new Date();
|
config: null,
|
||||||
const month = date.getMonth(); // 0-11
|
|
||||||
const day = date.getDate(); // 1-31
|
|
||||||
|
|
||||||
if ((month === 11 && day >= 28) || (month === 0 && day <= 5)) return 'fireworks'; //new year fireworks december 28 - january 5
|
init(config) {
|
||||||
|
if (this.initialized) return;
|
||||||
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18
|
this.config = config;
|
||||||
|
|
||||||
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27
|
// Only inject settings if enabled on server by admin
|
||||||
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27
|
if (this.config && this.config.EnableClientSideToggle !== false) {
|
||||||
|
this.injectSettingsIcon();
|
||||||
if (month === 11) return 'snowflakes'; // snowflakes december
|
this.initialized = true;
|
||||||
if (month === 0 || month === 1) return 'snowfall'; // snow january, february
|
console.log("Seasonals: Client-Side Settings Manager initialized.");
|
||||||
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february
|
}
|
||||||
|
},
|
||||||
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25
|
|
||||||
|
getSetting(key, defaultValue) {
|
||||||
//NOT IMPLEMENTED YET
|
const value = localStorage.getItem(`seasonals-${key}`);
|
||||||
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
|
return value !== null ? value : defaultValue;
|
||||||
|
},
|
||||||
//NOT IMPLEMENTED YET
|
|
||||||
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
|
setSetting(key, value) {
|
||||||
|
localStorage.setItem(`seasonals-${key}`, value);
|
||||||
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
|
},
|
||||||
|
|
||||||
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
|
createIcon() {
|
||||||
|
const button = document.createElement('button');
|
||||||
return 'none'; // Fallback (nothing)
|
button.type = 'button';
|
||||||
}
|
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
|
||||||
|
button.title = 'Seasonal Settings';
|
||||||
// load theme csss
|
// button.innerHTML = '<span class="material-icons">ac_unit</span>';
|
||||||
function loadThemeCSS(cssPath) {
|
button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
||||||
if (!cssPath) return;
|
button.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
const link = document.createElement('link');
|
button.addEventListener('click', (e) => {
|
||||||
link.rel = 'stylesheet';
|
e.stopPropagation();
|
||||||
link.href = cssPath;
|
this.toggleSettingsPopup(button);
|
||||||
|
});
|
||||||
link.onerror = () => {
|
|
||||||
console.error(`Failed to load CSS: ${cssPath}`);
|
return button;
|
||||||
};
|
},
|
||||||
|
|
||||||
document.body.appendChild(link);
|
injectSettingsIcon() {
|
||||||
console.log(`CSS file "${cssPath}" loaded.`);
|
const observer = new MutationObserver((mutations, obs) => {
|
||||||
}
|
const headerRight = document.querySelector('.headerRight');
|
||||||
|
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
|
||||||
// load theme js
|
const icon = this.createIcon();
|
||||||
function loadThemeJS(jsPath) {
|
headerRight.prepend(icon);
|
||||||
if (!jsPath) return;
|
}
|
||||||
|
});
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = jsPath;
|
observer.observe(document.body, {
|
||||||
script.defer = true;
|
childList: true,
|
||||||
|
subtree: true
|
||||||
script.onerror = () => {
|
});
|
||||||
console.error(`Failed to load JS: ${jsPath}`);
|
},
|
||||||
};
|
|
||||||
|
createPopup(anchorElement) {
|
||||||
document.body.appendChild(script);
|
const existing = document.querySelector('.seasonal-settings-popup');
|
||||||
console.log(`JS file "${jsPath}" loaded.`);
|
if (existing) existing.remove();
|
||||||
}
|
|
||||||
|
const popup = document.createElement('div');
|
||||||
// update theme container class name
|
popup.className = 'seasonal-settings-popup dialog';
|
||||||
function updateThemeContainer(containerClass) {
|
|
||||||
// Create container if it doesn't exist
|
Object.assign(popup.style, {
|
||||||
let container = document.querySelector('.seasonals-container');
|
position: 'fixed',
|
||||||
if (!container) {
|
zIndex: '10000',
|
||||||
container = document.createElement('div');
|
backgroundColor: '#202020',
|
||||||
container.className = 'seasonals-container';
|
padding: '1em',
|
||||||
document.body.appendChild(container);
|
borderRadius: '0.3em',
|
||||||
}
|
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
|
||||||
|
minWidth: '200px',
|
||||||
container.className = `seasonals-container ${containerClass}`;
|
color: '#fff',
|
||||||
console.log(`Seasonals-Container class updated to "${containerClass}".`);
|
maxWidth: '250px'
|
||||||
}
|
});
|
||||||
|
|
||||||
function removeSelf() {
|
const rect = anchorElement.getBoundingClientRect();
|
||||||
const script = document.currentScript;
|
|
||||||
if (script) script.parentNode.removeChild(script);
|
// Positioning logic
|
||||||
console.log('External script removed:', script);
|
let rightPos = window.innerWidth - rect.right;
|
||||||
}
|
if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
|
||||||
|
popup.style.right = '1rem';
|
||||||
// initialize theme
|
popup.style.left = 'auto';
|
||||||
async function initializeTheme() {
|
} else {
|
||||||
let automateThemeSelection = true;
|
popup.style.right = `${rightPos}px`;
|
||||||
let defaultTheme = 'none';
|
popup.style.left = 'auto';
|
||||||
|
}
|
||||||
try {
|
popup.style.top = `${rect.bottom + 10}px`;
|
||||||
const response = await fetch('/Seasonals/Config');
|
|
||||||
if (response.ok) {
|
// Popup HTML
|
||||||
const config = await response.json();
|
let html = `
|
||||||
automateThemeSelection = config.AutomateSeasonSelection;
|
<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
|
||||||
defaultTheme = config.SelectedSeason;
|
|
||||||
window.SeasonalsPluginConfig = config;
|
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
|
||||||
console.log('Seasonals Config loaded:', config);
|
<label class="emby-checkbox-label">
|
||||||
} else {
|
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
|
||||||
console.error('Failed to fetch Seasonals config');
|
<span class="checkboxLabel">Enable Seasonals</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selectContainer" style="margin-bottom: 0.5em;">
|
||||||
|
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
|
||||||
|
<select id="seasonal-theme-select" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
|
||||||
|
<option value="auto">Server-Side</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
popup.innerHTML = html;
|
||||||
|
|
||||||
|
// Populate Select Options
|
||||||
|
const themeSelect = popup.querySelector('#seasonal-theme-select');
|
||||||
|
Object.keys(ThemeConfigs).forEach(key => {
|
||||||
|
if (key === 'none') return;
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = key;
|
||||||
|
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
|
themeSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set Initial Values
|
||||||
|
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
|
||||||
|
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
|
||||||
|
themeSelect.value = this.getSetting('theme', 'auto');
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
enabledCheckbox.addEventListener('change', (e) => {
|
||||||
|
this.setSetting('enabled', e.target.checked);
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
themeSelect.addEventListener('change', (e) => {
|
||||||
|
this.setSetting('theme', e.target.value);
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
const closeHandler = (e) => {
|
||||||
|
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
||||||
|
popup.remove();
|
||||||
|
document.removeEventListener('click', closeHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
||||||
|
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSettingsPopup(anchorElement) {
|
||||||
|
const existing = document.querySelector('.seasonal-settings-popup');
|
||||||
|
if (existing) {
|
||||||
|
existing.remove();
|
||||||
|
} else {
|
||||||
|
this.createPopup(anchorElement);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching Seasonals config:', error);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let currentTheme;
|
const SeasonalsManager = {
|
||||||
if (automateThemeSelection === false) {
|
config: null,
|
||||||
currentTheme = defaultTheme;
|
|
||||||
} else {
|
|
||||||
currentTheme = determineCurrentTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Selected theme: ${currentTheme}`);
|
async init() {
|
||||||
|
// Fetch Config
|
||||||
|
try {
|
||||||
|
const response = await fetch('../Seasonals/Config');
|
||||||
|
if (response.ok) {
|
||||||
|
this.config = await response.json();
|
||||||
|
window.SeasonalsPluginConfig = this.config;
|
||||||
|
|
||||||
|
if (this.config.IsEnabled === false) {
|
||||||
|
console.log('Seasonals: Plugin is disabled globally.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentTheme || currentTheme === 'none') {
|
console.log('Seasonals: Seasonals Config loaded:', this.config);
|
||||||
console.log('No theme selected.');
|
}
|
||||||
removeSelf();
|
} catch (error) {
|
||||||
return;
|
console.error('Seasonals: Error fetching Seasonals config:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = themeConfigs[currentTheme];
|
// Initialize Settings UI
|
||||||
|
SeasonalSettingsManager.init(this.config);
|
||||||
|
|
||||||
|
// User Preference Check
|
||||||
|
const isEnabled = SeasonalSettingsManager.getSetting('enabled', 'true') === 'true';
|
||||||
|
if (!isEnabled) {
|
||||||
|
console.log('Seasonals: Disabled by user preference.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine Theme
|
||||||
|
const themeName = this.selectTheme();
|
||||||
|
console.log(`Seasonals: Selected theme: ${themeName}`);
|
||||||
|
|
||||||
|
if (!themeName || themeName === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Theme
|
||||||
|
this.applyTheme(themeName);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectTheme() {
|
||||||
|
// Check local override
|
||||||
|
const forcedTheme = SeasonalSettingsManager.getSetting('theme', 'auto');
|
||||||
|
if (forcedTheme !== 'auto') {
|
||||||
|
console.log(`Seasonals: User forced theme: ${forcedTheme}`);
|
||||||
|
return forcedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const automate = this.config ? this.config.AutomateSeasonSelection : true;
|
||||||
|
const defaultTheme = this.config ? this.config.SelectedSeason : 'none';
|
||||||
|
|
||||||
|
if (!automate) {
|
||||||
|
return defaultTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.determineCurrentThemeDate();
|
||||||
|
},
|
||||||
|
|
||||||
|
determineCurrentThemeDate() {
|
||||||
|
var rules = [];
|
||||||
|
try {
|
||||||
|
rules = JSON.parse(this.config.SeasonalRules || "[]");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Seasonals: Error parsing SeasonalRules", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length === 0) {
|
||||||
|
// Fallback to empty/none if no rules are defined (though default should exist)
|
||||||
|
console.log("Seasonals: No auto-selection rules found.");
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const month = date.getMonth() + 1; // 1-12
|
||||||
|
const day = date.getDate(); // 1-31
|
||||||
|
|
||||||
|
for (var i = 0; i < rules.length; i++) {
|
||||||
|
var rule = rules[i];
|
||||||
|
if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
|
||||||
|
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
|
||||||
|
return rule.Theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!theme) {
|
return 'none'; // No rule matched
|
||||||
console.error(`Theme "${currentTheme}" not found.`);
|
},
|
||||||
return;
|
|
||||||
|
isDateInRange: function(day, month, startDay, startMonth, endDay, endMonth) {
|
||||||
|
if (startMonth > endMonth) {
|
||||||
|
// Wrapping year (e.g. Dec to Jan)
|
||||||
|
return this.isDateAfterOrEqual(day, month, startDay, startMonth) ||
|
||||||
|
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
|
||||||
|
} else {
|
||||||
|
// Normal range
|
||||||
|
return this.isDateAfterOrEqual(day, month, startDay, startMonth) &&
|
||||||
|
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDateAfterOrEqual: function(day, month, targetDay, targetMonth) {
|
||||||
|
if (month > targetMonth) return true;
|
||||||
|
if (month === targetMonth && day >= targetDay) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
isDateBeforeOrEqual: function(day, month, targetDay, targetMonth) {
|
||||||
|
if (month < targetMonth) return true;
|
||||||
|
if (month === targetMonth && day <= targetDay) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyTheme(themeName) {
|
||||||
|
const theme = ThemeConfigs[themeName];
|
||||||
|
if (!theme) {
|
||||||
|
console.error(`Seasonals: Theme "${themeName}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateThemeContainer(theme.containerClass);
|
||||||
|
|
||||||
|
if (theme.css) this.loadResource('css', theme.css);
|
||||||
|
if (theme.js) this.loadResource('js', theme.js);
|
||||||
|
|
||||||
|
console.log(`Seasonals: Theme "${themeName}" applied.`);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateThemeContainer(containerClass) {
|
||||||
|
let container = document.querySelector('.seasonals-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'seasonals-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
container.className = `seasonals-container ${containerClass}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// helper to resolve paths for local testing vs production
|
||||||
|
resolvePath(path) {
|
||||||
|
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
|
return path.replace('/Seasonals/Resources/', './');
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadResource(type, path) {
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
if (type === 'css') {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = path;
|
||||||
|
// link.href = resolvePath(cssPath);
|
||||||
|
link.onerror = () => console.error(`Seasonals: Failed to load CSS: ${path}`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
} else if (type === 'js') {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = path;
|
||||||
|
// script.src = resolvePath(jsPath);
|
||||||
|
script.defer = true;
|
||||||
|
script.onerror = () => console.error(`Seasonals: Failed to load JS: ${path}`);
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateThemeContainer(theme.containerClass);
|
};
|
||||||
|
|
||||||
if (theme.css) loadThemeCSS(theme.css);
|
SeasonalsManager.init();
|
||||||
if (theme.js) loadThemeJS(theme.js);
|
|
||||||
|
|
||||||
console.log(`Theme "${currentTheme}" loaded.`);
|
|
||||||
|
|
||||||
removeSelf();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
initializeTheme();
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
#snowfallCanvas {
|
#snowfallCanvas {
|
||||||
|
|||||||
@@ -47,12 +47,10 @@ function toggleSnowfall() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleSnowfall);
|
const observer = new MutationObserver(toggleSnowfall);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
let resizeObserver; // Observer for resize events
|
let resizeObserver; // Observer for resize events
|
||||||
|
|||||||
@@ -1,138 +1,112 @@
|
|||||||
.snowflakes {
|
.snowflakes {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
contain: layout paint;
|
||||||
|
}
|
||||||
.snowflake {
|
|
||||||
position: fixed;
|
.snowflake {
|
||||||
z-index: 15;
|
position: fixed;
|
||||||
top: -10%;
|
z-index: 15;
|
||||||
font-size: 1em;
|
top: 0;
|
||||||
color: #fff;
|
will-change: transform;
|
||||||
font-family: Arial, sans-serif;
|
translate: 0 -10vh;
|
||||||
text-shadow: 0 0 5px #000;
|
font-size: 1em;
|
||||||
user-select: none;
|
color: #fff;
|
||||||
-webkit-user-select: none;
|
font-family: Arial, sans-serif;
|
||||||
cursor: default;
|
text-shadow: 0 0 5px #000;
|
||||||
-webkit-animation-name: heart-fall, heart-shake;
|
user-select: none;
|
||||||
-webkit-animation-duration: 12s, 3s;
|
cursor: default;
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
animation-name: snowflakes-fall, snowflakes-shake;
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
animation-duration: 12s, 3s;
|
||||||
animation-name: snowflakes-fall, snowflakes-shake;
|
animation-timing-function: linear, ease-in-out;
|
||||||
animation-duration: 12s, 3s;
|
animation-iteration-count: infinite, infinite;
|
||||||
animation-timing-function: linear, ease-in-out;
|
}
|
||||||
animation-iteration-count: infinite, infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes snowflakes-fall {
|
@keyframes snowflakes-fall {
|
||||||
0% {
|
0% {
|
||||||
top: -10%;
|
translate: 0 -10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
top: 100%;
|
translate: 0 110vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes snowflakes-shake {
|
@keyframes snowflakes-shake {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
-webkit-transform: translateX(0);
|
transform: translateX(0);
|
||||||
transform: translateX(0);
|
}
|
||||||
}
|
|
||||||
|
50% {
|
||||||
50% {
|
transform: translateX(80px);
|
||||||
-webkit-transform: translateX(80px);
|
}
|
||||||
transform: translateX(80px);
|
}
|
||||||
}
|
|
||||||
}
|
.snowflake:nth-of-type(0) {
|
||||||
|
left: 0%;
|
||||||
@keyframes snowflakes-fall {
|
animation-delay: 0s, 0s;
|
||||||
0% {
|
}
|
||||||
top: -10%;
|
|
||||||
}
|
.snowflake:nth-of-type(1) {
|
||||||
|
left: 10%;
|
||||||
100% {
|
animation-delay: 1s, 1s;
|
||||||
top: 100%;
|
}
|
||||||
}
|
|
||||||
}
|
.snowflake:nth-of-type(2) {
|
||||||
|
left: 20%;
|
||||||
@keyframes snowflakes-shake {
|
animation-delay: 6s, 0.5s;
|
||||||
|
}
|
||||||
0%,
|
|
||||||
100% {
|
.snowflake:nth-of-type(3) {
|
||||||
transform: translateX(0);
|
left: 30%;
|
||||||
}
|
animation-delay: 4s, 2s;
|
||||||
|
}
|
||||||
50% {
|
|
||||||
transform: translateX(80px);
|
.snowflake:nth-of-type(4) {
|
||||||
}
|
left: 40%;
|
||||||
}
|
animation-delay: 2s, 2s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(0) {
|
|
||||||
left: 0%;
|
.snowflake:nth-of-type(5) {
|
||||||
animation-delay: 0s, 0s;
|
left: 50%;
|
||||||
}
|
animation-delay: 8s, 3s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(1) {
|
|
||||||
left: 10%;
|
.snowflake:nth-of-type(6) {
|
||||||
animation-delay: 1s, 1s;
|
left: 60%;
|
||||||
}
|
animation-delay: 6s, 2s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(2) {
|
|
||||||
left: 20%;
|
.snowflake:nth-of-type(7) {
|
||||||
animation-delay: 6s, 0.5s;
|
left: 70%;
|
||||||
}
|
animation-delay: 2.5s, 1s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(3) {
|
|
||||||
left: 30%;
|
.snowflake:nth-of-type(8) {
|
||||||
animation-delay: 4s, 2s;
|
left: 80%;
|
||||||
}
|
animation-delay: 1s, 0s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(4) {
|
|
||||||
left: 40%;
|
.snowflake:nth-of-type(9) {
|
||||||
animation-delay: 2s, 2s;
|
left: 90%;
|
||||||
}
|
animation-delay: 3s, 1.5s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(5) {
|
|
||||||
left: 50%;
|
.snowflake:nth-of-type(10) {
|
||||||
animation-delay: 8s, 3s;
|
left: 25%;
|
||||||
}
|
animation-delay: 2s, 0s;
|
||||||
|
}
|
||||||
.snowflake:nth-of-type(6) {
|
|
||||||
left: 60%;
|
.snowflake:nth-of-type(11) {
|
||||||
animation-delay: 6s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(7) {
|
|
||||||
left: 70%;
|
|
||||||
animation-delay: 2.5s, 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(8) {
|
|
||||||
left: 80%;
|
|
||||||
animation-delay: 1s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(9) {
|
|
||||||
left: 90%;
|
|
||||||
animation-delay: 3s, 1.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(10) {
|
|
||||||
left: 25%;
|
|
||||||
animation-delay: 2s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(11) {
|
|
||||||
left: 65%;
|
|
||||||
animation-delay: 4s, 2.5s;
|
|
||||||
left: 65%;
|
left: 65%;
|
||||||
@@ -7,6 +7,8 @@ const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? c
|
|||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
||||||
const snowflakeCount = config.SnowflakeCount || 25; // count of random extra snowflakes
|
const snowflakeCount = config.SnowflakeCount || 25; // count of random extra snowflakes
|
||||||
|
|
||||||
|
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||||
|
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ function toggleSnowflakes() {
|
|||||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||||
|
|
||||||
// hide snowflakes if video/trailer player is active or dashboard is visible
|
// hide snowflakes if video/trailer player is active or dashboard is visible
|
||||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||||
snowflakeContainer.style.display = 'none'; // hide snowflakes
|
snowflakeContainer.style.display = 'none'; // hide snowflakes
|
||||||
@@ -38,12 +40,10 @@ function toggleSnowflakes() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleSnowflakes);
|
const observer = new MutationObserver(toggleSnowflakes);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
function addRandomSnowflakes(count) {
|
function addRandomSnowflakes(count) {
|
||||||
@@ -52,9 +52,6 @@ function addRandomSnowflakes(count) {
|
|||||||
|
|
||||||
console.log('Adding random snowflakes');
|
console.log('Adding random snowflakes');
|
||||||
|
|
||||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
|
||||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
// create a new snowflake element
|
// create a new snowflake element
|
||||||
const snowflake = document.createElement('div');
|
const snowflake = document.createElement('div');
|
||||||
@@ -70,7 +67,7 @@ function addRandomSnowflakes(count) {
|
|||||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||||
const randomAnimationDelay = Math.random() * 8; // delay (0s to 8s)
|
const randomAnimationDelay = Math.random() * 8; // delay (0s to 8s)
|
||||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
snowflake.style.left = `${randomLeft}%`;
|
snowflake.style.left = `${randomLeft}%`;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
#snowfallCanvas {
|
#snowfallCanvas {
|
||||||
|
|||||||