Compare commits
276 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc2c5eb973 | ||
|
|
8c02a07b88 | ||
|
|
7da6549bf9 | ||
|
|
2e6c1534b1 | ||
|
|
0a301564ac | ||
|
|
85e69a0b34 | ||
|
|
5adaf202ae | ||
|
|
99ac46a384 | ||
|
|
3a2750388b | ||
|
|
33e89ec16b | ||
|
|
9adbe92e7c | ||
|
|
103d63f1b1 | ||
|
|
49bad2e880 | ||
|
|
04616c2ac4 | ||
|
|
3b73dd1728 | ||
|
|
d5df90a6ae | ||
|
|
494e475f42 | ||
|
|
4703ba48ed | ||
|
|
93d5686b77 | ||
|
|
71d07aa0f3 | ||
|
|
c43f031617 | ||
|
|
89ce903e8a | ||
|
|
cbf5d73629 | ||
|
|
8be17dae74 | ||
|
|
76006dc162 | ||
|
|
492acb4052 | ||
|
|
68dc9efa4d | ||
|
|
7b15ed46c1 | ||
|
|
8019ba760f | ||
|
|
9d1a268875 | ||
|
|
9e5feafd64 | ||
|
|
5283a69bb8 | ||
|
|
a4b2d2edd5 | ||
|
|
d0634e4487 | ||
|
|
79c4f988f2 | ||
|
|
cee1fa6736 | ||
|
|
8510674d58 | ||
|
|
5ee724201b | ||
|
|
b85c038df0 | ||
|
|
3d4e04ab0f | ||
|
|
b1d1ce79e6 | ||
|
|
0b7b506b8d | ||
|
|
f3ea84cc80 | ||
|
|
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 |
@@ -51,7 +51,31 @@ jobs:
|
|||||||
echo "$CHANGELOG" >> $GITHUB_ENV
|
echo "$CHANGELOG" >> $GITHUB_ENV
|
||||||
echo "EOF" >> $GITHUB_ENV
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check if Release Already Exists
|
||||||
|
id: check_release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
REPO_OWNER="${{ github.repository_owner }}"
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
VERSION="${{ env.VERSION }}"
|
||||||
|
TAG="v$VERSION"
|
||||||
|
SERVER_URL="https://git.mahom03-spacecloud.de"
|
||||||
|
|
||||||
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")
|
||||||
|
|
||||||
|
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||||
|
echo "Release $TAG already exists. Skipping release-related steps."
|
||||||
|
echo "release_exists=true" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "$HTTP_STATUS" -eq 404 ]; then
|
||||||
|
echo "No existing release for $TAG. Continuing."
|
||||||
|
echo "release_exists=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "Unexpected response when checking release: $HTTP_STATUS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and Zip
|
- name: Build and Zip
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Inject version from manifest into the build
|
# Inject version from manifest into the build
|
||||||
@@ -71,6 +95,7 @@ jobs:
|
|||||||
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
|
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Update manifest.json
|
- name: Update manifest.json
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
REPO_OWNER="${{ github.repository_owner }}"
|
REPO_OWNER="${{ github.repository_owner }}"
|
||||||
@@ -90,12 +115,14 @@ jobs:
|
|||||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||||
|
|
||||||
- name: Commit manifest.json
|
- name: Commit manifest.json
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
uses: stefanzweifel/git-auto-commit-action@v7
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
with:
|
with:
|
||||||
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
|
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
|
||||||
file_pattern: manifest.json
|
file_pattern: manifest.json
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
uses: akkuman/gitea-release-action@v1
|
uses: akkuman/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
server_url: "https://git.mahom03-spacecloud.de"
|
server_url: "https://git.mahom03-spacecloud.de"
|
||||||
@@ -109,6 +136,7 @@ jobs:
|
|||||||
|
|
||||||
# Update Message in Remote Repository
|
# Update Message in Remote Repository
|
||||||
- name: Checkout Central Manifest Repo
|
- name: Checkout Central Manifest Repo
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
|
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
|
||||||
@@ -116,6 +144,7 @@ jobs:
|
|||||||
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
||||||
|
|
||||||
- name: Update Central Manifest
|
- name: Update Central Manifest
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd central-manifest
|
cd central-manifest
|
||||||
@@ -171,6 +200,7 @@ jobs:
|
|||||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||||
|
|
||||||
- name: Commit and Push Central Manifest
|
- name: Commit and Push Central Manifest
|
||||||
|
if: steps.check_release.outputs.release_exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
cd central-manifest
|
cd central-manifest
|
||||||
git config user.name "CodeDevMLH"
|
git config user.name "CodeDevMLH"
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -4,6 +4,5 @@ obj/
|
|||||||
.idea/
|
.idea/
|
||||||
artifacts
|
artifacts
|
||||||
|
|
||||||
test-site.html
|
test-site-old.html
|
||||||
test-site-new.html
|
|
||||||
RELEASE_GUIDE.md
|
RELEASE_GUIDE.md
|
||||||
343
CONTRIBUTING.md
Normal file
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,15 +18,40 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
EnableClientSideToggle = true;
|
EnableClientSideToggle = true;
|
||||||
|
|
||||||
Autumn = new AutumnOptions();
|
Autumn = new AutumnOptions();
|
||||||
Snowflakes = new SnowflakesOptions();
|
Birthday = new BirthdayOptions();
|
||||||
Snowfall = new SnowfallOptions();
|
Carnival = new CarnivalOptions();
|
||||||
Snowstorm = new SnowstormOptions();
|
CherryBlossom = new CherryBlossomOptions();
|
||||||
|
Christmas = new ChristmasOptions();
|
||||||
|
EarthDay = new EarthDayOptions();
|
||||||
|
Easter = new EasterOptions();
|
||||||
|
Eid = new EidOptions();
|
||||||
|
Eurovision = new EurovisionOptions();
|
||||||
|
FilmNoir = new FilmNoirOptions();
|
||||||
Fireworks = new FireworksOptions();
|
Fireworks = new FireworksOptions();
|
||||||
|
Friday13 = new Friday13Options();
|
||||||
|
Frost = new FrostOptions();
|
||||||
Halloween = new HalloweenOptions();
|
Halloween = new HalloweenOptions();
|
||||||
Hearts = new HeartsOptions();
|
Hearts = new HeartsOptions();
|
||||||
Christmas = new ChristmasOptions();
|
MarioDay = new MarioDayOptions();
|
||||||
|
Matrix = new MatrixOptions();
|
||||||
|
Oktoberfest = new OktoberfestOptions();
|
||||||
|
Olympia = new OlympiaOptions();
|
||||||
|
Oscar = new OscarOptions();
|
||||||
|
Rain = new RainOptions();
|
||||||
|
Pride = new PrideOptions();
|
||||||
|
Resurrection = new ResurrectionOptions();
|
||||||
Santa = new SantaOptions();
|
Santa = new SantaOptions();
|
||||||
Easter = new EasterOptions();
|
Snowfall = new SnowfallOptions();
|
||||||
|
Snowflakes = new SnowflakesOptions();
|
||||||
|
Snowstorm = new SnowstormOptions();
|
||||||
|
Space = new SpaceOptions();
|
||||||
|
Spooky = new SpookyOptions();
|
||||||
|
Sports = new SportsOptions();
|
||||||
|
Spring = new SpringOptions();
|
||||||
|
StarWars = new StarWarsOptions();
|
||||||
|
Storm = new StormOptions();
|
||||||
|
Summer = new SummerOptions();
|
||||||
|
Underwater = new UnderwaterOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -49,120 +74,315 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableClientSideToggle { get; set; }
|
public bool EnableClientSideToggle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the seasonal rules configuration as JSON.
|
||||||
|
/// </summary>
|
||||||
|
public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"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>
|
/// <summary>
|
||||||
/// Gets or sets the Seasonals options.
|
/// Gets or sets the Seasonals options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AutumnOptions Autumn { get; set; }
|
public AutumnOptions Autumn { get; set; }
|
||||||
public SnowflakesOptions Snowflakes { get; set; }
|
public BirthdayOptions Birthday { get; set; }
|
||||||
public SnowfallOptions Snowfall { get; set; }
|
public CarnivalOptions Carnival { get; set; }
|
||||||
public SnowstormOptions Snowstorm { get; set; }
|
public CherryBlossomOptions CherryBlossom { get; set; }
|
||||||
|
public ChristmasOptions Christmas { get; set; }
|
||||||
|
public EarthDayOptions EarthDay { get; set; }
|
||||||
|
public EasterOptions Easter { get; set; }
|
||||||
|
public EidOptions Eid { get; set; }
|
||||||
|
public EurovisionOptions Eurovision { get; set; }
|
||||||
|
public FilmNoirOptions FilmNoir { get; set; }
|
||||||
public FireworksOptions Fireworks { get; set; }
|
public FireworksOptions Fireworks { get; set; }
|
||||||
|
public Friday13Options Friday13 { get; set; }
|
||||||
|
public FrostOptions Frost { get; set; }
|
||||||
public HalloweenOptions Halloween { get; set; }
|
public HalloweenOptions Halloween { get; set; }
|
||||||
public HeartsOptions Hearts { get; set; }
|
public HeartsOptions Hearts { get; set; }
|
||||||
public ChristmasOptions Christmas { get; set; }
|
public MarioDayOptions MarioDay { get; set; }
|
||||||
|
public MatrixOptions Matrix { get; set; }
|
||||||
|
public OktoberfestOptions Oktoberfest { get; set; }
|
||||||
|
public OlympiaOptions Olympia { get; set; }
|
||||||
|
public OscarOptions Oscar { get; set; }
|
||||||
|
public PrideOptions Pride { get; set; }
|
||||||
|
public RainOptions Rain { get; set; }
|
||||||
|
public ResurrectionOptions Resurrection { get; set; }
|
||||||
public SantaOptions Santa { get; set; }
|
public SantaOptions Santa { get; set; }
|
||||||
public EasterOptions Easter { get; set; }
|
public SnowfallOptions Snowfall { get; set; }
|
||||||
|
public SnowflakesOptions Snowflakes { get; set; }
|
||||||
|
public SnowstormOptions Snowstorm { get; set; }
|
||||||
|
public SpaceOptions Space { get; set; }
|
||||||
|
public SpookyOptions Spooky { get; set; }
|
||||||
|
public SportsOptions Sports { get; set; }
|
||||||
|
public SpringOptions Spring { get; set; }
|
||||||
|
public StarWarsOptions StarWars { get; set; }
|
||||||
|
public StormOptions Storm { get; set; }
|
||||||
|
public SummerOptions Summer { get; set; }
|
||||||
|
public UnderwaterOptions Underwater { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AutumnOptions
|
public class AutumnOptions {
|
||||||
{
|
|
||||||
public int LeafCount { get; set; } = 25;
|
|
||||||
public bool EnableAutumn { get; set; } = true;
|
public bool EnableAutumn { get; set; } = true;
|
||||||
public bool EnableRandomLeaves { get; set; } = true;
|
public int LeafCount { get; set; } = 35;
|
||||||
public bool EnableRandomLeavesMobile { get; set; } = false;
|
public int LeafCountMobile { get; set; } = 10;
|
||||||
public bool EnableDifferentDuration { get; set; } = true;
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
public bool EnableRotation { get; set; } = false;
|
public bool EnableRotation { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnowflakesOptions
|
public class BirthdayOptions {
|
||||||
{
|
public bool EnableBirthday { get; set; } = true;
|
||||||
public int SnowflakeCount { get; set; } = 25;
|
public int SymbolCount { get; set; } = 12;
|
||||||
public bool EnableSnowflakes { get; set; } = true;
|
public int SymbolCountMobile { get; set; } = 5;
|
||||||
public bool EnableRandomSnowflakes { get; set; } = true;
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
public bool EnableRandomSnowflakesMobile { get; set; } = false;
|
public int ConfettiCount { get; set; } = 60;
|
||||||
public bool EnableColoredSnowflakes { get; set; } = true;
|
}
|
||||||
|
|
||||||
|
public class CarnivalOptions {
|
||||||
|
public bool EnableCarnival { get; set; } = true;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableCarnivalSway { get; set; } = true;
|
||||||
|
public int ObjectCount { get; set; } = 120;
|
||||||
|
public int ObjectCountMobile { get; set; } = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CherryBlossomOptions {
|
||||||
|
public bool EnableCherryBlossom { get; set; } = true;
|
||||||
|
public int PetalCount { get; set; } = 25;
|
||||||
|
public int PetalCountMobile { get; set; } = 15;
|
||||||
public bool EnableDifferentDuration { get; set; } = true;
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnowfallOptions
|
public class ChristmasOptions {
|
||||||
{
|
public bool EnableChristmas { get; set; } = true;
|
||||||
public int SnowflakesCount { get; set; } = 500;
|
public int SymbolCount { get; set; } = 25;
|
||||||
public int SnowflakesCountMobile { get; set; } = 250;
|
public int SymbolCountMobile { get; set; } = 10;
|
||||||
public double Speed { get; set; } = 3;
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
public bool EnableSnowfall { get; set; } = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnowstormOptions
|
public class EarthDayOptions {
|
||||||
{
|
public bool EnableEarthDay { get; set; } = true;
|
||||||
public int SnowflakesCount { get; set; } = 500;
|
public int VineCount { get; set; } = 4;
|
||||||
public int SnowflakesCountMobile { get; set; } = 250;
|
|
||||||
public double Speed { get; set; } = 6;
|
|
||||||
public bool EnableSnowstorm { get; set; } = true;
|
|
||||||
public double HorizontalWind { get; set; } = 4;
|
|
||||||
public double VerticalVariation { get; set; } = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FireworksOptions
|
public class EasterOptions {
|
||||||
{
|
public bool EnableEaster { get; set; } = true;
|
||||||
public int ParticleCount { get; set; } = 50;
|
public bool EnableBunny { get; set; } = true;
|
||||||
public int LaunchInterval { get; set; } = 3200;
|
public int MinBunnyRestTime { get; set; } = 2000;
|
||||||
|
public int MaxBunnyRestTime { get; set; } = 5000;
|
||||||
|
public int EggCount { get; set; } = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EidOptions {
|
||||||
|
public bool EnableEid { get; set; } = true;
|
||||||
|
public int LanternCount { get; set; } = 8;
|
||||||
|
public int LanternCountMobile { get; set; } = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EurovisionOptions {
|
||||||
|
public bool EnableEurovision { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
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; } = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FilmNoirOptions {
|
||||||
|
public bool EnableFilmNoir { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FireworksOptions {
|
||||||
public bool EnableFireworks { get; set; } = true;
|
public bool EnableFireworks { get; set; } = true;
|
||||||
public bool ScrollFireworks { get; set; } = true;
|
public bool ScrollFireworks { get; set; } = true;
|
||||||
|
public int ParticleCount { get; set; } = 50;
|
||||||
public int MinFireworks { get; set; } = 3;
|
public int MinFireworks { get; set; } = 3;
|
||||||
public int MaxFireworks { get; set; } = 6;
|
public int MaxFireworks { get; set; } = 6;
|
||||||
|
public int LaunchInterval { get; set; } = 3200;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HalloweenOptions
|
public class Friday13Options {
|
||||||
{
|
public bool EnableFriday13 { get; set; } = true;
|
||||||
public int SymbolCount { get; set; } = 25;
|
}
|
||||||
|
|
||||||
|
public class FrostOptions {
|
||||||
|
public bool EnableFrost { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HalloweenOptions {
|
||||||
public bool EnableHalloween { get; set; } = true;
|
public bool EnableHalloween { get; set; } = true;
|
||||||
public bool EnableRandomSymbols { get; set; } = true;
|
public int SymbolCount { get; set; } = 25;
|
||||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
public int SymbolCountMobile { get; set; } = 10;
|
||||||
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 {
|
||||||
{
|
|
||||||
public int SymbolCount { get; set; } = 25;
|
|
||||||
public bool EnableHearts { get; set; } = true;
|
public bool EnableHearts { get; set; } = true;
|
||||||
public bool EnableRandomSymbols { get; set; } = true;
|
|
||||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
|
||||||
public bool EnableDifferentDuration { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ChristmasOptions
|
|
||||||
{
|
|
||||||
public int SymbolCount { get; set; } = 25;
|
public int SymbolCount { get; set; } = 25;
|
||||||
public bool EnableChristmas { get; set; } = true;
|
public int SymbolCountMobile { get; set; } = 10;
|
||||||
public bool EnableRandomChristmas { get; set; } = true;
|
|
||||||
public bool EnableRandomChristmasMobile { get; set; } = false;
|
|
||||||
public bool EnableDifferentDuration { get; set; } = true;
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SantaOptions
|
public class MarioDayOptions {
|
||||||
{
|
public bool EnableMarioDay { get; set; } = true;
|
||||||
|
public bool LetMarioJump { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MatrixOptions {
|
||||||
|
public bool EnableMatrix { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public bool EnableMatrixBackground { get; set; } = false;
|
||||||
|
public string MatrixChars { get; set; } = "0123456789";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OktoberfestOptions {
|
||||||
|
public bool EnableOktoberfest { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public int SymbolCountMobile { get; set; } = 10;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OlympiaOptions {
|
||||||
|
public bool EnableOlympia { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 25;
|
||||||
|
public int SymbolCountMobile { get; set; } = 10;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OscarOptions {
|
||||||
|
public bool EnableOscar { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PrideOptions {
|
||||||
|
public bool EnablePride { get; set; } = true;
|
||||||
|
public int HeartCount { get; set; } = 20;
|
||||||
|
public double HeartSize { get; set; } = 1.5;
|
||||||
|
public bool ColorHeader { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResurrectionOptions {
|
||||||
|
public bool EnableResurrection { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 12;
|
||||||
|
public int SymbolCountMobile { get; set; } = 5;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SantaOptions {
|
||||||
|
public bool EnableSanta { get; set; } = true;
|
||||||
public int SnowflakesCount { get; set; } = 500;
|
public int SnowflakesCount { get; set; } = 500;
|
||||||
public int SnowflakesCountMobile { get; set; } = 250;
|
public int SnowflakesCountMobile { get; set; } = 250;
|
||||||
|
public double SnowFallSpeed { get; set; } = 3;
|
||||||
public double SantaSpeed { get; set; } = 10;
|
public double SantaSpeed { get; set; } = 10;
|
||||||
public double SantaSpeedMobile { get; set; } = 8;
|
public double SantaSpeedMobile { get; set; } = 8;
|
||||||
public bool EnableSanta { get; set; } = true;
|
|
||||||
public double SnowFallSpeed { get; set; } = 3;
|
|
||||||
public double MaxSantaRestTime { get; set; } = 8;
|
public double MaxSantaRestTime { get; set; } = 8;
|
||||||
public double MinSantaRestTime { get; set; } = 3;
|
public double MinSantaRestTime { get; set; } = 3;
|
||||||
public double MaxPresentFallSpeed { get; set; } = 5;
|
public double MaxPresentFallSpeed { get; set; } = 5;
|
||||||
public double MinPresentFallSpeed { get; set; } = 2;
|
public double MinPresentFallSpeed { get; set; } = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EasterOptions
|
public class SnowfallOptions {
|
||||||
{
|
public bool EnableSnowfall { get; set; } = true;
|
||||||
public int EggCount { get; set; } = 20;
|
public int SnowflakesCount { get; set; } = 500;
|
||||||
public bool EnableEaster { get; set; } = true;
|
public int SnowflakesCountMobile { get; set; } = 250;
|
||||||
public bool EnableRandomEaster { get; set; } = true;
|
public double Speed { get; set; } = 3;
|
||||||
public bool EnableRandomEasterMobile { get; set; } = false;
|
}
|
||||||
public bool EnableDifferentDuration { get; set; } = true;
|
|
||||||
public bool EnableBunny { get; set; } = true;
|
public class SnowflakesOptions {
|
||||||
public int BunnyDuration { get; set; } = 12000;
|
public bool EnableSnowflakes { get; set; } = true;
|
||||||
public int HopHeight { get; set; } = 12;
|
public int SnowflakeCount { get; set; } = 25;
|
||||||
public int MinBunnyRestTime { get; set; } = 2000;
|
public int SnowflakeCountMobile { get; set; } = 10;
|
||||||
public int MaxBunnyRestTime { get; set; } = 5000;
|
public bool EnableColoredSnowflakes { get; set; } = true;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnowstormOptions {
|
||||||
|
public bool EnableSnowstorm { get; set; } = true;
|
||||||
|
public int SnowflakesCount { get; set; } = 500;
|
||||||
|
public int SnowflakesCountMobile { get; set; } = 250;
|
||||||
|
public double Speed { get; set; } = 6;
|
||||||
|
public double HorizontalWind { get; set; } = 4;
|
||||||
|
public double VerticalVariation { get; set; } = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpaceOptions {
|
||||||
|
public bool EnableSpace { get; set; } = true;
|
||||||
|
public int PlanetCount { get; set; } = 6;
|
||||||
|
public int AstronautCount { get; set; } = 1;
|
||||||
|
public int SatelliteCount { get; set; } = 4;
|
||||||
|
public int IssCount { get; set; } = 1;
|
||||||
|
public int RocketCount { get; set; } = 1;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public int SymbolCountMobile { get; set; } = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bool EnableSports { get; set; } = true;
|
||||||
|
public int SymbolCount { get; set; } = 5;
|
||||||
|
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 string ConfettiColors { get; set; } = "#000000,#FF0000,#FFCC00";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpringOptions {
|
||||||
|
public bool EnableSpring { get; set; } = true;
|
||||||
|
public int PollenCount { get; set; } = 30;
|
||||||
|
public bool EnableSpringSunbeams { get; set; } = true;
|
||||||
|
public int SunbeamCount { get; set; } = 5;
|
||||||
|
public int BirdCount { get; set; } = 3;
|
||||||
|
public int ButterflyCount { get; set; } = 4;
|
||||||
|
public int BeeCount { get; set; } = 2;
|
||||||
|
public int LadybugCount { get; set; } = 2;
|
||||||
|
public int SymbolCountMobile { get; set; } = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StarWarsOptions {
|
||||||
|
public bool EnableStarWars { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StormOptions {
|
||||||
|
public bool EnableStorm { get; set; } = true;
|
||||||
|
public int RaindropCount { get; set; } = 300;
|
||||||
|
public int RaindropCountMobile { get; set; } = 150;
|
||||||
|
public bool EnableLightning { get; set; } = true;
|
||||||
|
public double RainSpeed { get; set; } = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SummerOptions {
|
||||||
|
public bool EnableSummer { get; set; } = true;
|
||||||
|
public int BubbleCount { get; set; } = 30;
|
||||||
|
public int DustCount { get; set; } = 50;
|
||||||
|
public int SymbolCountMobile { get; set; } = 2;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UnderwaterOptions {
|
||||||
|
public bool EnableUnderwater { get; set; } = true;
|
||||||
|
public int SymbolCountMobile { get; set; } = 2;
|
||||||
|
public bool EnableDifferentDuration { get; set; } = true;
|
||||||
|
public bool EnableLightRays { get; set; } = true;
|
||||||
|
public int SeaweedCount { get; set; } = 50;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||||
<Title>Jellyfin Seasonals Plugin</Title>
|
<Title>Jellyfin Seasonals Plugin</Title>
|
||||||
<Authors>CodeDevMLH</Authors>
|
<Authors>CodeDevMLH</Authors>
|
||||||
<Version>1.6.12.0</Version>
|
<Version>2.0.0.6</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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,24 +8,21 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaf {
|
.leaf {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
top: -10%;
|
top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
translate: 0 -10vh;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-shadow: 0 0 5px #000;
|
text-shadow: 0 0 5px #000;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
-webkit-animation-name: leaf-fall, leaf-shake;
|
|
||||||
-webkit-animation-duration: 7s, 4s;
|
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
animation-name: leaf-fall, leaf-shake;
|
animation-name: leaf-fall, leaf-shake;
|
||||||
animation-duration: 7s, 4s;
|
animation-duration: 7s, 4s;
|
||||||
animation-timing-function: linear, ease-in-out;
|
animation-timing-function: linear, ease-in-out;
|
||||||
@@ -38,34 +35,17 @@
|
|||||||
--rotate-end: 0deg !important;
|
--rotate-end: 0deg !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes leaf-fall {
|
|
||||||
0% {
|
|
||||||
top: -10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes leaf-fall {
|
@keyframes leaf-fall {
|
||||||
0% {
|
0% {
|
||||||
top: -10%;
|
translate: 0 -10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
top: 100%;
|
translate: 0 100vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes leaf-shake {
|
|
||||||
0%, 100% {
|
|
||||||
-webkit-transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes leaf-shake {
|
@keyframes leaf-shake {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
@@ -75,87 +55,3 @@
|
|||||||
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
transform: translateX(80px) rotate(var(--rotate-end, 20deg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaf:nth-of-type(0) {
|
|
||||||
left: 0%;
|
|
||||||
animation-delay: 0s, 0s;
|
|
||||||
--rotate-start: -25deg;
|
|
||||||
--rotate-end: 22deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(1) {
|
|
||||||
left: 10%;
|
|
||||||
animation-delay: 1s, 0.5s;
|
|
||||||
--rotate-start: -32deg;
|
|
||||||
--rotate-end: 35deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(2) {
|
|
||||||
left: 20%;
|
|
||||||
animation-delay: 6s, 1s;
|
|
||||||
--rotate-start: -28deg;
|
|
||||||
--rotate-end: 28deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(3) {
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: 4s, 1.5s;
|
|
||||||
--rotate-start: -38deg;
|
|
||||||
--rotate-end: 32deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(4) {
|
|
||||||
left: 40%;
|
|
||||||
animation-delay: 2s, 0.8s;
|
|
||||||
--rotate-start: -22deg;
|
|
||||||
--rotate-end: 38deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(5) {
|
|
||||||
left: 50%;
|
|
||||||
animation-delay: 8s, 2s;
|
|
||||||
--rotate-start: -35deg;
|
|
||||||
--rotate-end: 25deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(6) {
|
|
||||||
left: 60%;
|
|
||||||
animation-delay: 6s, 1.2s;
|
|
||||||
--rotate-start: -40deg;
|
|
||||||
--rotate-end: 40deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(7) {
|
|
||||||
left: 70%;
|
|
||||||
animation-delay: 2.5s, 0.3s;
|
|
||||||
--rotate-start: -30deg;
|
|
||||||
--rotate-end: 30deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf:nth-of-type(8) {
|
|
||||||
left: 80%;
|
|
||||||
animation-delay: 1s, 1.8s;
|
|
||||||
--rotate-start: -26deg;
|
|
||||||
--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;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,30 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Autumn || {};
|
const config = window.SeasonalsPluginConfig?.Autumn || {};
|
||||||
|
|
||||||
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable leaves
|
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable autumn
|
||||||
const randomLeaves = config.EnableRandomLeaves !== undefined ? config.EnableRandomLeaves : true; // enable random leaves
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
const randomLeavesMobile = config.EnableRandomLeavesMobile !== undefined ? config.EnableRandomLeavesMobile : false; // enable random leaves on mobile devices (Warning: High values may affect performance)
|
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable rotation
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random leaves
|
const leafCount = config.LeafCount !== undefined ? config.LeafCount : 35; // count of random extra leaves
|
||||||
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
|
const leafCountMobile = config.LeafCountMobile !== undefined ? config.LeafCountMobile : 10; // count of random extra leaves on mobile
|
||||||
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,40 +56,23 @@ 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 = [
|
function initLeaves(count) {
|
||||||
"/Seasonals/Resources/autumn_images/acorn1.png",
|
let autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||||
"/Seasonals/Resources/autumn_images/acorn2.png",
|
if (!autumnContainer) {
|
||||||
"/Seasonals/Resources/autumn_images/leaf1.png",
|
autumnContainer = document.createElement("div");
|
||||||
"/Seasonals/Resources/autumn_images/leaf2.png",
|
autumnContainer.className = "autumn-container";
|
||||||
"/Seasonals/Resources/autumn_images/leaf3.png",
|
autumnContainer.setAttribute("aria-hidden", "true");
|
||||||
"/Seasonals/Resources/autumn_images/leaf4.png",
|
document.body.appendChild(autumnContainer);
|
||||||
"/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) {
|
console.log('Adding leaves');
|
||||||
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
|
||||||
if (!autumnContainer) return; // exit if leave container is not found
|
|
||||||
|
|
||||||
console.log('Adding random leaves');
|
|
||||||
|
|
||||||
// Array of leave characters
|
// Array of leave characters
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
@@ -90,7 +91,9 @@ 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)
|
// Display directly symbols on full screen (below) or let it build up (above)
|
||||||
|
// const randomAnimationDelay = -(Math.random() * 16); // delay for fall (-16s to 0s)
|
||||||
|
const randomAnimationDelay2 = -(Math.random() * 4); // delay for shake+rotate (-4s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
leaveDiv.style.left = `${randomLeft}%`;
|
leaveDiv.style.left = `${randomLeft}%`;
|
||||||
@@ -118,60 +121,18 @@ function addRandomLeaves(count) {
|
|||||||
// add the leave to the container
|
// add the leave to the container
|
||||||
autumnContainer.appendChild(leaveDiv);
|
autumnContainer.appendChild(leaveDiv);
|
||||||
}
|
}
|
||||||
console.log('Random leaves added');
|
console.log('Leaves added');
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize standard leaves
|
// initialize leaves
|
||||||
function initLeaves() {
|
|
||||||
const container = document.querySelector('.autumn-container') || document.createElement("div");
|
|
||||||
|
|
||||||
if (!document.querySelector('.autumn-container')) {
|
|
||||||
container.className = "autumn-container";
|
|
||||||
container.setAttribute("aria-hidden", "true");
|
|
||||||
document.body.appendChild(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const leafDiv = document.createElement("div");
|
|
||||||
leafDiv.className = enableRotation ? "leaf" : "leaf no-rotation";
|
|
||||||
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.src = images[Math.floor(Math.random() * images.length)];
|
|
||||||
|
|
||||||
// set random animation duration
|
|
||||||
if (enableDiffrentDuration) {
|
|
||||||
const randomAnimationDuration = Math.random() * 10 + 6; // fall duration (6s to 16s)
|
|
||||||
const randomAnimationDuration2 = Math.random() * 3 + 2; // shake+rotate duration (2s to 5s)
|
|
||||||
leafDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set random rotation angles for standard leaves too (only if rotation is enabled)
|
|
||||||
if (enableRotation) {
|
|
||||||
const randomRotateStart = -(Math.random() * 40 + 20); // -20deg to -60deg
|
|
||||||
const randomRotateEnd = Math.random() * 40 + 20; // 20deg to 60deg
|
|
||||||
leafDiv.style.setProperty('--rotate-start', `${randomRotateStart}deg`);
|
|
||||||
leafDiv.style.setProperty('--rotate-end', `${randomRotateEnd}deg`);
|
|
||||||
} else {
|
|
||||||
// No rotation - set to 0 degrees
|
|
||||||
leafDiv.style.setProperty('--rotate-start', '0deg');
|
|
||||||
leafDiv.style.setProperty('--rotate-end', '0deg');
|
|
||||||
}
|
|
||||||
|
|
||||||
leafDiv.appendChild(img);
|
|
||||||
container.appendChild(leafDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize leaves and add random leaves
|
|
||||||
function initializeLeaves() {
|
function initializeLeaves() {
|
||||||
if (!leaves) return; // exit if leaves are disabled
|
if (!leaves) return; // exit if leaves are disabled
|
||||||
initLeaves();
|
|
||||||
toggleAutumn();
|
|
||||||
|
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
if (randomLeaves && (screenWidth > 768 || randomLeavesMobile)) { // add random leaves only on larger screens, unless enabled for mobile devices
|
const count = !isMobile ? leafCount : leafCountMobile;
|
||||||
addRandomLeaves(leafCount);
|
|
||||||
}
|
initLeaves(count);
|
||||||
|
toggleAutumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeLeaves();
|
initializeLeaves();
|
||||||
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
326
Jellyfin.Plugin.Seasonals/Web/birthday.js
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Birthday || {};
|
||||||
|
|
||||||
|
const birthday = config.EnableBirthday !== undefined ? config.EnableBirthday : true; // enable/disable birthday symbols
|
||||||
|
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 15; // count of balloons
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the symbols
|
||||||
|
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 5; // count of mobile balloons
|
||||||
|
const baseConfettiCount = config.ConfettiCount !== undefined ? config.ConfettiCount : 60; // count of confetti
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base ballon image: https://www.flaticon.com/de/kostenloses-icon/ballon_1512470
|
||||||
|
* modified by CodeDevMLH
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
let finalCount = isMobile ? symbolCountMobile : symbolCount;
|
||||||
|
|
||||||
|
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 confettiCount = 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
177
Jellyfin.Plugin.Seasonals/Web/carnival.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Carnival || {};
|
||||||
|
|
||||||
|
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // enable/disable carnival
|
||||||
|
const carnivalCount = config.ObjectCount !== undefined ? config.ObjectCount : 120; // Number of confetti pieces to spawn
|
||||||
|
const carnivalCountMobile = config.ObjectCountMobile !== undefined ? config.ObjectCountMobile : 60; // Number of confetti pieces to spawn on mobile
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true; // enable/disable carnivalsway
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize standard carnival objects
|
||||||
|
function initCarnivalObjects(count) {
|
||||||
|
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 < count; i++) {
|
||||||
|
createConfettiPiece(container, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize carnival
|
||||||
|
function initializeCarnival() {
|
||||||
|
if (!carnival) return;
|
||||||
|
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const count = !isMobile ? carnivalCount : carnivalCountMobile;
|
||||||
|
|
||||||
|
initCarnivalObjects(count);
|
||||||
|
toggleCarnival();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Jellyfin.Plugin.Seasonals/Web/cherryblossom.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
|
||||||
|
|
||||||
|
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true; // enable/disable cherryblossom
|
||||||
|
const petalCount = config.PetalCount !== undefined ? config.PetalCount : 25; // count of petal
|
||||||
|
const petalCountMobile = config.PetalCountMobile !== undefined ? config.PetalCountMobile : 10; // count of petal on mobile
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
|
||||||
|
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 initObjects(count) {
|
||||||
|
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 < count; i++) {
|
||||||
|
createPetal(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCherryBlossom() {
|
||||||
|
if (!cherryBlossom) return;
|
||||||
|
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const count = !isMobile ? petalCount : petalCountMobile;
|
||||||
|
|
||||||
|
initObjects(count);
|
||||||
|
toggleCherryBlossom();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCherryBlossom();
|
||||||
@@ -8,65 +8,40 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas {
|
.christmas {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
top: -10%;
|
top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
translate: 0 -10vh;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-shadow: 0 0 5px #000;
|
text-shadow: 0 0 5px #000;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
-webkit-user-select: none;
|
|
||||||
-webkit-animation-name: christmas-fall, christmas-shake;
|
|
||||||
-webkit-animation-duration: 10s, 3s;
|
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
|
||||||
animation-name: christmas-fall, christmas-shake;
|
animation-name: christmas-fall, christmas-shake;
|
||||||
animation-duration: 10s, 3s;
|
animation-duration: 10s, 3s;
|
||||||
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 christmas-fall {
|
|
||||||
0% {
|
|
||||||
top: -10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes christmas-shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px);
|
|
||||||
transform: translateX(80px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes christmas-fall {
|
@keyframes christmas-fall {
|
||||||
0% {
|
0% {
|
||||||
top: -10%;
|
translate: 0 -10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
top: 100%;
|
translate: 0 110vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes christmas-shake {
|
@keyframes christmas-shake {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
@@ -76,63 +51,3 @@
|
|||||||
transform: translateX(80px);
|
transform: translateX(80px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.christmas:nth-of-type(0) {
|
|
||||||
left: 0%;
|
|
||||||
animation-delay: 0s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(1) {
|
|
||||||
left: 10%;
|
|
||||||
animation-delay: 1s, 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(2) {
|
|
||||||
left: 20%;
|
|
||||||
animation-delay: 6s, 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(3) {
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: 4s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(4) {
|
|
||||||
left: 40%;
|
|
||||||
animation-delay: 2s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(5) {
|
|
||||||
left: 50%;
|
|
||||||
animation-delay: 8s, 3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.christmas:nth-of-type(6) {
|
|
||||||
left: 60%;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Christmas || {};
|
const config = window.SeasonalsPluginConfig?.Christmas || {};
|
||||||
|
|
||||||
const christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
|
const christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
|
||||||
const randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random Christmas
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ? config.EnableRandomChristmasMobile : false; // enable random Christmas on mobile devices (Warning: High values may affect performance)
|
const christmasCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols
|
const christmasCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
|
||||||
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,22 +38,22 @@ 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
|
function initChristmas(count) {
|
||||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
let christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
||||||
|
if (!christmasContainer) {
|
||||||
|
christmasContainer = document.createElement("div");
|
||||||
|
christmasContainer.className = "christmas-container";
|
||||||
|
christmasContainer.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(christmasContainer);
|
||||||
|
}
|
||||||
|
|
||||||
function addRandomChristmas(count) {
|
console.log('Adding christmas');
|
||||||
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
|
||||||
if (!christmasContainer) return; // exit if christmas container is not found
|
|
||||||
|
|
||||||
console.log('Adding random christmas');
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
// create a new christmas element
|
// create a new christmas element
|
||||||
@@ -64,8 +65,8 @@ function addRandomChristmas(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 + 8; // delay (8s to 12s)
|
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
|
||||||
const randomAnimationDelay2 = Math.random() * 5 + 3; // delay (0s to 5s)
|
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
christmasDiv.style.left = `${randomLeft}%`;
|
christmasDiv.style.left = `${randomLeft}%`;
|
||||||
@@ -81,46 +82,18 @@ function addRandomChristmas(count) {
|
|||||||
// add the christmas to the container
|
// add the christmas to the container
|
||||||
christmasContainer.appendChild(christmasDiv);
|
christmasContainer.appendChild(christmasDiv);
|
||||||
}
|
}
|
||||||
console.log('Random christmas added');
|
console.log('Christmas added');
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize standard christmas
|
// initialize christmas
|
||||||
function initChristmas() {
|
|
||||||
const christmasContainer = document.querySelector('.christmas-container') || document.createElement("div");
|
|
||||||
|
|
||||||
if (!document.querySelector('.christmas-container')) {
|
|
||||||
christmasContainer.className = "christmas-container";
|
|
||||||
christmasContainer.setAttribute("aria-hidden", "true");
|
|
||||||
document.body.appendChild(christmasContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the 12 standard christmas
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const christmasDiv = document.createElement('div');
|
|
||||||
christmasDiv.className = 'christmas';
|
|
||||||
christmasDiv.textContent = christmasSymbols[Math.floor(Math.random() * christmasSymbols.length)];
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
christmasDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
christmasContainer.appendChild(christmasDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize christmas and add random christmas symbols
|
|
||||||
function initializeChristmas() {
|
function initializeChristmas() {
|
||||||
if (!christmas) return; // exit if christmas is disabled
|
if (!christmas) return; // exit if christmas is disabled
|
||||||
initChristmas();
|
|
||||||
toggleChristmas();
|
|
||||||
|
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
if (randomChristmas && (screenWidth > 768 || randomChristmasMobile)) { // add random christmas only on larger screens, unless enabled for mobile devices
|
const count = !isMobile ? christmasCount : christmasCountMobile;
|
||||||
addRandomChristmas(christmasCount);
|
|
||||||
}
|
initChristmas(count);
|
||||||
|
toggleChristmas();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeChristmas();
|
initializeChristmas();
|
||||||
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; // enable/disable earthday
|
||||||
|
const vineCount = config.VineCount !== undefined ? config.VineCount : 4; // count of vine
|
||||||
|
|
||||||
|
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: 10;
|
z-index: 10000;
|
||||||
|
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,38 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Easter || {};
|
const config = window.SeasonalsPluginConfig?.Easter || {};
|
||||||
|
|
||||||
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
|
const easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
|
||||||
const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter
|
const enableBunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable bunny
|
||||||
const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance)
|
const minBunnyRestTime = config.MinBunnyRestTime !== undefined ? config.MinBunnyRestTime : 2000; // timing parameter
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter
|
const maxBunnyRestTime = config.MaxBunnyRestTime !== undefined ? config.MaxBunnyRestTime : 5000; // timing parameter
|
||||||
const easterEggCount = config.EggCount || 20; // count of random extra easter
|
const eggCount = config.EggCount !== undefined ? config.EggCount : 15; // count of egg
|
||||||
|
|
||||||
const bunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable hopping bunny
|
/* MARK: Bunny movement config */
|
||||||
const bunnyDuration = config.BunnyDuration || 12000; // duration of the bunny animation in ms
|
const jumpDistanceVw = 5; // Distance in vw the bunny covers per jump
|
||||||
const hopHeight = config.HopHeight || 12; // height of the bunny hops in px
|
const jumpDurationMs = 770; // Time in ms the bunny spends moving during a jump
|
||||||
const minBunnyRestTime = config.MinBunnyRestTime || 2000; // minimum time the bunny rests in ms
|
const pauseDurationMs = 116.6666; // Time in ms the bunny pauses between jumps
|
||||||
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
|
// Credit: https://flaticon.com
|
||||||
let animationFrameId;
|
const easterEggImages = [
|
||||||
|
"../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 +42,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 +64,201 @@ function toggleEaster() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// observe changes in the DOM
|
|
||||||
const observer = new MutationObserver(toggleEaster);
|
const observer = new MutationObserver(toggleEaster);
|
||||||
|
|
||||||
// start observation
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true, // observe adding/removing of child elements
|
childList: true,
|
||||||
subtree: true, // observe all levels of the DOM tree
|
subtree: true,
|
||||||
attributes: true // observe changes to attributes (e.g. class changes)
|
attributes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const images = [
|
function createEasterGrassAndEggs(container) {
|
||||||
"/Seasonals/Resources/easter_images/egg_1.png",
|
let grassContainer = container.querySelector('.easter-grass-container');
|
||||||
"/Seasonals/Resources/easter_images/egg_2.png",
|
if (!grassContainer) {
|
||||||
"/Seasonals/Resources/easter_images/egg_3.png",
|
grassContainer = document.createElement('div');
|
||||||
"/Seasonals/Resources/easter_images/egg_4.png",
|
grassContainer.className = 'easter-grass-container';
|
||||||
"/Seasonals/Resources/easter_images/egg_5.png",
|
container.appendChild(grassContainer);
|
||||||
"/Seasonals/Resources/easter_images/egg_6.png",
|
|
||||||
"/Seasonals/Resources/easter_images/egg_7.png",
|
|
||||||
"/Seasonals/Resources/easter_images/egg_8.png",
|
|
||||||
"/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",
|
|
||||||
];
|
|
||||||
const rabbit = "/Seasonals/Resources/easter_images/easter-bunny.png";
|
|
||||||
|
|
||||||
function addRandomEaster(count) {
|
|
||||||
const easterContainer = document.querySelector('.easter-container'); // get the leave container
|
|
||||||
if (!easterContainer) return; // exit if leave container is not found
|
|
||||||
|
|
||||||
console.log('Adding random easter eggs');
|
|
||||||
|
|
||||||
// Array of leave characters
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
// create a new leave element
|
|
||||||
const eggDiv = document.createElement('div');
|
|
||||||
eggDiv.className = "easter";
|
|
||||||
|
|
||||||
// pick a random easter symbol
|
|
||||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.src = imageSrc;
|
|
||||||
|
|
||||||
eggDiv.appendChild(img);
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
|
||||||
|
|
||||||
// apply styles
|
|
||||||
eggDiv.style.left = `${randomLeft}%`;
|
|
||||||
eggDiv.style.animationDelay = `${randomAnimationDelay}s, ${randomAnimationDelay2}s`;
|
|
||||||
|
|
||||||
// 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`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grassContainer.innerHTML = '';
|
||||||
|
|
||||||
// add the leave to the container
|
let pathsBg = '';
|
||||||
easterContainer.appendChild(eggDiv);
|
let pathsFg = '';
|
||||||
|
const w = window.innerWidth;
|
||||||
|
const hSVG = 80; // Grass 80px high
|
||||||
|
|
||||||
|
// Generate Grass
|
||||||
|
const bladeCount = w / 5;
|
||||||
|
for (let i = 0; i < bladeCount; i++) {
|
||||||
|
const height = Math.random() * 40 + 20;
|
||||||
|
const x = i * 5 + Math.random() * 3;
|
||||||
|
const hue = 80 + Math.random() * 40; // slightly more yellow-green for spring/easter
|
||||||
|
const color = `hsl(${hue}, 60%, 40%)`;
|
||||||
|
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
|
||||||
|
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
const x = Math.random() * w;
|
||||||
|
const h = 20 + Math.random() * 50;
|
||||||
|
const cY = hSVG - h;
|
||||||
|
const bend = x + (Math.random() * 40 - 20);
|
||||||
|
const color = Math.random() > 0.5 ? '#4caf50' : '#8bc34a';
|
||||||
|
const width = 1 + Math.random() * 2;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Flowers
|
||||||
|
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const x = 10 + Math.random() * (w - 20);
|
||||||
|
const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
|
||||||
|
const col = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
|
||||||
|
let path = '';
|
||||||
|
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 r = 2 + Math.random() * 1.5;
|
||||||
|
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}"/>`;
|
||||||
|
path += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
// CSS-Klassen hinzufügen
|
rabbitImg.style.bottom = "-15px";
|
||||||
rabbitImg.classList.add("hopping-rabbit");
|
rabbitImg.style.position = "absolute";
|
||||||
|
|
||||||
easterContainer.appendChild(rabbitImg);
|
container.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)' : '';
|
||||||
|
// Set 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;
|
||||||
|
const currSrc = rabbit.src.split('?')[0];
|
||||||
// random start position and direction
|
rabbit.src = currSrc + '?t=' + Date.now();
|
||||||
const startFromLeft = Math.random() >= 0.5;
|
|
||||||
rabbit.startX = startFromLeft ? -10 : 110;
|
|
||||||
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);
|
|
||||||
|
|
||||||
// calculate the vertical position (sinus curve)
|
const completedLoops = Math.floor(elapsed / loopDurationMs);
|
||||||
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
|
const timeInCurrentLoop = elapsed % loopDurationMs;
|
||||||
|
|
||||||
// set the new position
|
// Determine if we are currently jumping or pausing
|
||||||
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`;
|
let currentLoopDistance = 0;
|
||||||
|
if (timeInCurrentLoop < jumpDurationMs) {
|
||||||
if (progress < bunnyDuration) {
|
// We are in the jumping phase
|
||||||
animationFrameId = requestAnimationFrame(animationStep);
|
currentLoopDistance = (timeInCurrentLoop / jumpDurationMs) * jumpDistanceVw;
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(animationStep);
|
currentX = startX + (completedLoops * jumpDistanceVw + currentLoopDistance) * direction;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +267,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; }
|
||||||
|
}
|
||||||
100
Jellyfin.Plugin.Seasonals/Web/eid.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Eid || {};
|
||||||
|
const eid = config.EnableEid !== undefined ? config.EnableEid : true; // enable/disable eid
|
||||||
|
const lanternCount = config.LanternCount !== undefined ? config.LanternCount : 8; // count of lantern
|
||||||
|
const lanternCountMobile = config.LanternCountMobile !== undefined ? config.LanternCountMobile : 3; // count of lantern on mobile
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
let activeLanternCount = isMobile ? lanternCountMobile : lanternCount;
|
||||||
|
|
||||||
|
// Create evenly spaced lanterns
|
||||||
|
const segmentWidth = 100 / activeLanternCount;
|
||||||
|
for (let i = 0; i < activeLanternCount; 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); }
|
||||||
|
}
|
||||||
100
Jellyfin.Plugin.Seasonals/Web/eurovision.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Eurovision || {};
|
||||||
|
|
||||||
|
const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true; // enable/disable eurovision
|
||||||
|
const elementCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of notes
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true; // enable/disable colorful notes
|
||||||
|
const eurovisionColorsStr = config.EurovisionColors !== undefined ? config.EurovisionColors : '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff'; // colors to use
|
||||||
|
const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2; // size of eurovision glow
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
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; // enable/disable filmnoir
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ const config = window.SeasonalsPluginConfig?.Fireworks || {};
|
|||||||
|
|
||||||
const fireworks = config.EnableFireworks !== undefined ? config.EnableFireworks : true; // enable/disable fireworks
|
const fireworks = config.EnableFireworks !== undefined ? config.EnableFireworks : true; // enable/disable fireworks
|
||||||
const scrollFireworks = config.ScrollFireworks !== undefined ? config.ScrollFireworks : true; // enable fireworks to scroll with page content
|
const scrollFireworks = config.ScrollFireworks !== undefined ? config.ScrollFireworks : true; // enable fireworks to scroll with page content
|
||||||
const particlesPerFirework = config.ParticleCount || 50; // count of particles per firework (Warning: High values may affect performance)
|
const particlesPerFirework = config.ParticleCount !== undefined ? config.ParticleCount : 50; // count of particles per firework
|
||||||
const minFireworks = config.MinFireworks || 3; // minimum number of simultaneous fireworks
|
const minFireworks = config.MinFireworks !== undefined ? config.MinFireworks : 3; // minimum number of simultaneous fireworks
|
||||||
const maxFireworks = config.MaxFireworks || 6; // maximum number of simultaneous fireworks
|
const maxFireworks = config.MaxFireworks !== undefined ? config.MaxFireworks : 6; // maximum number of simultaneous fireworks
|
||||||
const intervalOfFireworks = config.LaunchInterval || 3200; // interval for the fireworks in milliseconds
|
const intervalOfFireworks = config.LaunchInterval !== undefined ? config.LaunchInterval : 3200; // interval for the fireworks in milliseconds
|
||||||
|
|
||||||
// array of color palettes for the fireworks
|
// array of color palettes for the fireworks
|
||||||
const colorPalettes = [
|
const colorPalettes = [
|
||||||
@@ -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; // enable/disable friday13
|
||||||
|
|
||||||
|
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; // enable/disable frost
|
||||||
|
|
||||||
|
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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,74 +49,117 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(0) {
|
/* --- Fog Layer --- */
|
||||||
left: 1%;
|
.halloween-fog-layer {
|
||||||
-webkit-animation-delay: 0s, 0s;
|
position: absolute;
|
||||||
animation-delay: 0s, 0s
|
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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(1) {
|
/* --- Spiders --- */
|
||||||
left: 10%;
|
.halloween-spider-wrapper {
|
||||||
-webkit-animation-delay: 1s, 1s;
|
position: absolute;
|
||||||
animation-delay: 1s, 1s
|
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:nth-of-type(2) {
|
.halloween-thread {
|
||||||
left: 20%;
|
width: 30px; /* Wider hit area for mouse interaction */
|
||||||
-webkit-animation-delay: 6s, .5s;
|
height: 100vh;
|
||||||
animation-delay: 6s, .5s
|
margin-top: -100vh;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.halloween-thread::after {
|
||||||
.halloween:nth-of-type(3) {
|
content: '';
|
||||||
left: 30%;
|
position: absolute;
|
||||||
-webkit-animation-delay: 4s, 2s;
|
|
||||||
animation-delay: 4s, 2s
|
|
||||||
}
|
|
||||||
|
|
||||||
.halloween:nth-of-type(4) {
|
|
||||||
left: 40%;
|
|
||||||
-webkit-animation-delay: 2s, 2s;
|
|
||||||
animation-delay: 2s, 2s
|
|
||||||
}
|
|
||||||
|
|
||||||
.halloween:nth-of-type(5) {
|
|
||||||
left: 50%;
|
left: 50%;
|
||||||
-webkit-animation-delay: 8s, 3s;
|
transform: translateX(-50%);
|
||||||
animation-delay: 8s, 3s
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(6) {
|
/* MARK: SPIDER SWAY CONFIGURATION */
|
||||||
left: 60%;
|
@keyframes wind-sway {
|
||||||
-webkit-animation-delay: 6s, 2s;
|
0% { transform: rotate(0deg); }
|
||||||
animation-delay: 6s, 2s
|
25% { transform: rotate(2deg); }
|
||||||
|
75% { transform: rotate(-2deg); }
|
||||||
|
100% { transform: rotate(0deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(7) {
|
@keyframes spider-drop {
|
||||||
left: 70%;
|
0% { transform: translateY(-50px); }
|
||||||
-webkit-animation-delay: 2.5s, 1s;
|
30% { transform: translateY(var(--drop-height, 50vh)); }
|
||||||
animation-delay: 2.5s, 1s
|
60% { transform: translateY(var(--drop-height, 50vh)); }
|
||||||
|
100% { transform: translateY(-50px); }
|
||||||
|
}
|
||||||
|
@keyframes spider-swing {
|
||||||
|
0% { transform: rotate(-10deg); }
|
||||||
|
100% { transform: rotate(10deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.halloween:nth-of-type(8) {
|
/* Mice */
|
||||||
left: 80%;
|
.halloween-mouse {
|
||||||
-webkit-animation-delay: 1s, 0s;
|
position: absolute;
|
||||||
animation-delay: 1s, 0s
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: left;
|
||||||
}
|
}
|
||||||
|
@keyframes mouse-run-right {
|
||||||
.halloween:nth-of-type(9) {
|
0% { left: -10vw; }
|
||||||
left: 90%;
|
100% { left: 110vw; }
|
||||||
-webkit-animation-delay: 3s, 1.5s;
|
|
||||||
animation-delay: 3s, 1.5s
|
|
||||||
}
|
}
|
||||||
|
@keyframes mouse-run-left {
|
||||||
.halloween:nth-of-type(10) {
|
0% { left: 110vw; }
|
||||||
left: 25%;
|
100% { left: -10vw; }
|
||||||
-webkit-animation-delay: 2s, 0s;
|
|
||||||
animation-delay: 2s, 0s
|
|
||||||
}
|
|
||||||
|
|
||||||
.halloween:nth-of-type(11) {
|
|
||||||
left: 65%;
|
|
||||||
-webkit-animation-delay: 4s, 2.5s;
|
|
||||||
animation-delay: 4s, 2.5s
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Halloween || {};
|
const config = window.SeasonalsPluginConfig?.Halloween || {};
|
||||||
|
|
||||||
const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween : true; // enable/disable halloween
|
const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween : true; // enable/disable halloween
|
||||||
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true; // enable/disable spiders
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
|
const enableMice = config.EnableMice !== undefined ? config.EnableMice : true; // enable/disable mice
|
||||||
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
|
const halloweenCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
|
||||||
|
const halloweenCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbols on mobile
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
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;
|
||||||
|
|
||||||
// function to check and control the halloween
|
// function to check and control the halloween
|
||||||
function toggleHalloween() {
|
function toggleHalloween() {
|
||||||
@@ -34,47 +41,40 @@ function toggleHalloween() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = [
|
function initHalloween(count) {
|
||||||
"/Seasonals/Resources/halloween_images/ghost_20x20.png",
|
let halloweenContainer = document.querySelector('.halloween-container');
|
||||||
"/Seasonals/Resources/halloween_images/bat_20x20.png",
|
if (!halloweenContainer) {
|
||||||
"/Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
halloweenContainer = document.createElement("div");
|
||||||
];
|
halloweenContainer.className = "halloween-container";
|
||||||
|
halloweenContainer.setAttribute("aria-hidden", "true");
|
||||||
function addRandomSymbols(count) {
|
document.body.appendChild(halloweenContainer);
|
||||||
const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container
|
}
|
||||||
if (!halloweenContainer) return; // exit if halloween container is not found
|
|
||||||
|
|
||||||
console.log('Adding random halloween symbols');
|
|
||||||
|
|
||||||
|
console.log('Adding halloween symbols');
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
// create a new halloween elements
|
|
||||||
const halloweenDiv = document.createElement("div");
|
const halloweenDiv = document.createElement("div");
|
||||||
halloweenDiv.className = "halloween";
|
halloweenDiv.className = "halloween";
|
||||||
|
|
||||||
// pick a random halloween symbol
|
|
||||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = imageSrc;
|
img.src = imageSrc;
|
||||||
|
|
||||||
halloweenDiv.appendChild(img);
|
halloweenDiv.appendChild(img);
|
||||||
|
|
||||||
|
|
||||||
// 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)
|
// Display directly symbols on full screen (below) or let it build up (above)
|
||||||
|
// const randomAnimationDelay = -(Math.random() * 10); // delay (-10s to 0s)
|
||||||
|
const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
halloweenDiv.style.left = `${randomLeft}%`;
|
halloweenDiv.style.left = `${randomLeft}%`;
|
||||||
@@ -87,52 +87,149 @@ function addRandomSymbols(count) {
|
|||||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the halloween to the container
|
|
||||||
halloweenContainer.appendChild(halloweenDiv);
|
halloweenContainer.appendChild(halloweenDiv);
|
||||||
}
|
}
|
||||||
console.log('Random halloween symbols added');
|
console.log('Halloween symbols added');
|
||||||
}
|
}
|
||||||
|
|
||||||
// create halloween objects
|
// create fog layer
|
||||||
function createHalloween() {
|
function createFog(container) {
|
||||||
const container = document.querySelector('.halloween-container') || document.createElement("div");
|
const fogContainer = document.createElement('div');
|
||||||
|
fogContainer.className = 'halloween-fog-layer';
|
||||||
|
|
||||||
if (!document.querySelector('.halloween-container')) {
|
const fog1 = document.createElement('div');
|
||||||
container.className = "halloween-container";
|
fog1.className = 'halloween-fog-blob';
|
||||||
container.setAttribute("aria-hidden", "true");
|
|
||||||
document.body.appendChild(container);
|
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);
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
images.forEach(imageSrc => {
|
|
||||||
const halloweenDiv = document.createElement("div");
|
|
||||||
halloweenDiv.className = "halloween";
|
|
||||||
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.src = imageSrc;
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
halloweenDiv.appendChild(img);
|
|
||||||
container.appendChild(halloweenDiv);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
|
||||||
function initializeHalloween() {
|
function initializeHalloween() {
|
||||||
if (!halloween) return; // exit if halloween is disabled
|
if (!halloween) return;
|
||||||
createHalloween();
|
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const count = !isMobile ? halloweenCount : halloweenCountMobile;
|
||||||
|
|
||||||
|
initHalloween(count);
|
||||||
toggleHalloween();
|
toggleHalloween();
|
||||||
|
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const container = document.querySelector('.halloween-container');
|
||||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
|
||||||
addRandomSymbols(halloweenCount);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,75 +49,3 @@
|
|||||||
transform: translateX(80px)
|
transform: translateX(80px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart:nth-of-type(0) {
|
|
||||||
left: 1%;
|
|
||||||
-webkit-animation-delay: 0s, 0s;
|
|
||||||
animation-delay: 0s, 0s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(1) {
|
|
||||||
left: 10%;
|
|
||||||
-webkit-animation-delay: 1s, 1s;
|
|
||||||
animation-delay: 1s, 1s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(2) {
|
|
||||||
left: 20%;
|
|
||||||
-webkit-animation-delay: 6s, .5s;
|
|
||||||
animation-delay: 6s, .5s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(3) {
|
|
||||||
left: 30%;
|
|
||||||
-webkit-animation-delay: 4s, 2s;
|
|
||||||
animation-delay: 4s, 2s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(4) {
|
|
||||||
left: 40%;
|
|
||||||
-webkit-animation-delay: 2s, 2s;
|
|
||||||
animation-delay: 2s, 2s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(5) {
|
|
||||||
left: 50%;
|
|
||||||
-webkit-animation-delay: 8s, 3s;
|
|
||||||
animation-delay: 8s, 3s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(6) {
|
|
||||||
left: 60%;
|
|
||||||
-webkit-animation-delay: 6s, 2s;
|
|
||||||
animation-delay: 6s, 2s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(7) {
|
|
||||||
left: 70%;
|
|
||||||
-webkit-animation-delay: 2.5s, 1s;
|
|
||||||
animation-delay: 2.5s, 1s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(8) {
|
|
||||||
left: 80%;
|
|
||||||
-webkit-animation-delay: 1s, 0s;
|
|
||||||
animation-delay: 1s, 0s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(9) {
|
|
||||||
left: 90%;
|
|
||||||
-webkit-animation-delay: 3s, 1.5s;
|
|
||||||
animation-delay: 3s, 1.5s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(10) {
|
|
||||||
left: 25%;
|
|
||||||
-webkit-animation-delay: 2s, 0s;
|
|
||||||
animation-delay: 2s, 0s
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart:nth-of-type(11) {
|
|
||||||
left: 65%;
|
|
||||||
-webkit-animation-delay: 4s, 2.5s;
|
|
||||||
animation-delay: 4s, 2.5s
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Hearts || {};
|
const config = window.SeasonalsPluginConfig?.Hearts || {};
|
||||||
|
|
||||||
const hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
|
const hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
|
||||||
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
const heartsCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols
|
const heartsCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
|
||||||
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
|
||||||
|
|
||||||
@@ -36,24 +38,23 @@ 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
|
function initHearts(count) {
|
||||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
let heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
||||||
|
if (!heartsContainer) {
|
||||||
|
heartsContainer = document.createElement("div");
|
||||||
|
heartsContainer.className = "hearts-container";
|
||||||
|
heartsContainer.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(heartsContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Adding heart symbols');
|
||||||
function addRandomSymbols(count) {
|
|
||||||
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
|
||||||
if (!heartsContainer) return; // exit if hearts container is not found
|
|
||||||
|
|
||||||
console.log('Adding random heart symbols');
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
// create a new hearts elements
|
// create a new hearts elements
|
||||||
@@ -66,8 +67,8 @@ 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() * 14; // delay (0s to 14s)
|
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
|
||||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||||
|
|
||||||
// apply styles
|
// apply styles
|
||||||
heartsDiv.style.left = `${randomLeft}%`;
|
heartsDiv.style.left = `${randomLeft}%`;
|
||||||
@@ -83,46 +84,18 @@ function addRandomSymbols(count) {
|
|||||||
// add the hearts to the container
|
// add the hearts to the container
|
||||||
heartsContainer.appendChild(heartsDiv);
|
heartsContainer.appendChild(heartsDiv);
|
||||||
}
|
}
|
||||||
console.log('Random hearts symbols added');
|
console.log('Heart symbols added');
|
||||||
}
|
}
|
||||||
|
|
||||||
// create hearts objects
|
|
||||||
function createHearts() {
|
|
||||||
const container = document.querySelector('.hearts-container') || document.createElement("div");
|
|
||||||
|
|
||||||
if (!document.querySelector('.hearts-container')) {
|
|
||||||
container.className = "hearts-container";
|
|
||||||
container.setAttribute("aria-hidden", "true");
|
|
||||||
document.body.appendChild(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const heartsDiv = document.createElement("div");
|
|
||||||
heartsDiv.className = "heart";
|
|
||||||
heartsDiv.textContent = heartSymbols[i % heartSymbols.length];
|
|
||||||
|
|
||||||
// set random animation duration
|
|
||||||
if (enableDiffrentDuration) {
|
|
||||||
const randomAnimationDuration = Math.random() * 16 + 12; // delay (12s to 16s)
|
|
||||||
const randomAnimationDuration2 = Math.random() * 7 + 3; // delay (3s to 7s)
|
|
||||||
heartsDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(heartsDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// initialize hearts
|
// initialize hearts
|
||||||
function initializeHearts() {
|
function initializeHearts() {
|
||||||
if (!hearts) return; // exit if hearts is disabled
|
if (!hearts) return; // exit if hearts is disabled
|
||||||
createHearts();
|
|
||||||
toggleHearts();
|
|
||||||
|
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random heartss only on larger screens, unless enabled for mobile devices
|
const count = !isMobile ? heartsCount : heartsCountMobile;
|
||||||
addRandomSymbols(heartsCount);
|
|
||||||
}
|
initHearts(count);
|
||||||
|
toggleHearts();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeHearts();
|
initializeHearts();
|
||||||
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); }
|
||||||
|
}
|
||||||
109
Jellyfin.Plugin.Seasonals/Web/marioday.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.MarioDay || {};
|
||||||
|
const marioday = config.EnableMarioDay !== undefined ? config.EnableMarioDay : true; // enable/disable marioday
|
||||||
|
const letMarioJump = config.LetMarioJump !== undefined ? config.LetMarioJump : true; // optionally let mario jump occasionally
|
||||||
|
|
||||||
|
// Credit: https://gifs.alphacoders.com/gifs/view/2585
|
||||||
|
const marioImage = '../Seasonals/Resources/mario_assets/mario.gif';
|
||||||
|
|
||||||
|
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 = marioImage;
|
||||||
|
|
||||||
|
wrapper.appendChild(mario);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
let jumpCount = 0;
|
||||||
|
let maxJumpsForThisRun = Math.floor(Math.random() * 3); // 0, 1, or 2
|
||||||
|
|
||||||
|
const resetJumpInterval = setInterval(() => {
|
||||||
|
if (!document.body.contains(container)) { clearInterval(resetJumpInterval); return; }
|
||||||
|
jumpCount = 0;
|
||||||
|
maxJumpsForThisRun = Math.floor(Math.random() * 3); // Randomize jumps for the next pass
|
||||||
|
}, (marioSpeedSeconds / 2) * 1000);
|
||||||
|
|
||||||
|
// 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 marioRect = wrapper.getBoundingClientRect();
|
||||||
|
if (marioRect.left < 0 || marioRect.right > window.innerWidth) return;
|
||||||
|
|
||||||
|
if (letMarioJump && !mario.classList.contains('mario-jump') && jumpCount < maxJumpsForThisRun) {
|
||||||
|
mario.classList.add('mario-jump');
|
||||||
|
jumpCount++;
|
||||||
|
setTimeout(() => mario.classList.remove('mario-jump'), 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coin = document.createElement('div');
|
||||||
|
coin.className = 'mario-coin';
|
||||||
|
|
||||||
|
// Grab Mario's current screen position to lock the coin's X coordinate
|
||||||
|
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; // enable/disable matrix
|
||||||
|
const maxTrails = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of max trails on screen
|
||||||
|
const backgroundMode = config.EnableMatrixBackground !== undefined ? config.EnableMatrixBackground : false; // enable/disable matrix as background
|
||||||
|
const matrixChars = config.MatrixChars !== undefined ? config.MatrixChars : '0123456789'; // characters to use in the matrix rain, default is '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; }
|
||||||
|
}
|
||||||
75
Jellyfin.Plugin.Seasonals/Web/oktoberfest.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Oktoberfest || {};
|
||||||
|
const oktoberfest = config.EnableOktoberfest !== undefined ? config.EnableOktoberfest : true; // enable/disable oktoberfest
|
||||||
|
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbols
|
||||||
|
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbols on mobile
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleOktoberfest);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createOktoberfest(container, count) {
|
||||||
|
for (let i = 0; i < count; 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;
|
||||||
|
if (enableDifferentDuration) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const count = !isMobile ? symbolCount : symbolCountMobile;
|
||||||
|
|
||||||
|
createOktoberfest(container, count);
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
263
Jellyfin.Plugin.Seasonals/Web/olympia.js
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Olympia || {};
|
||||||
|
|
||||||
|
const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true; // enable/disable olympia theme
|
||||||
|
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of floating symbols
|
||||||
|
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of floating symbols on mobile
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
|
||||||
|
// Olympic Ring Colors (Carnival Config)
|
||||||
|
const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E'];
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const confettiCount = isMobile ? 30 : 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credits:
|
||||||
|
* https://lottiefiles.com/free-animation/gold-coin-5Spp5kJbLP
|
||||||
|
* https://lottiefiles.com/free-animation/silver-coin-SIgIP59fII
|
||||||
|
* https://lottiefiles.com/free-animation/bronze-coin-wWVCJMsOUq
|
||||||
|
*/
|
||||||
|
const olympicMedals = [
|
||||||
|
"../Seasonals/Resources/olympic_assets/gold_coin.gif",
|
||||||
|
"../Seasonals/Resources/olympic_assets/silver_coin.gif",
|
||||||
|
"../Seasonals/Resources/olympic_assets/bronze_coin.gif"
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credits:
|
||||||
|
* https://www.flaticon.com/de/kostenloses-icon/fackel_4683293
|
||||||
|
* merged with:
|
||||||
|
* https://lottiefiles.com/free-animation/abstract-flames-lottie-json-animation-oSb0IFoBrj
|
||||||
|
*/
|
||||||
|
const olympicTorch = "../Seasonals/Resources/olympic_assets/torch.gif";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
let finalCount = isMobile ? symbolCountMobile : symbolCount;
|
||||||
|
|
||||||
|
const useRandomDuration = enableDifferentDuration !== false;
|
||||||
|
|
||||||
|
const olympicRings = ['ring_blue.css', 'ring_yellow.css', 'ring_black.css', 'ring_green.css', 'ring_red.css'];
|
||||||
|
const activeItems = [...olympicMedals, ...olympicRings];
|
||||||
|
|
||||||
|
for (let i = 0; i < finalCount; i++) {
|
||||||
|
let symbol = document.createElement('div');
|
||||||
|
|
||||||
|
const randomImgUrl = activeItems[Math.floor(Math.random() * activeItems.length)];
|
||||||
|
const isRing = randomImgUrl.includes('ring_');
|
||||||
|
const isMedal = randomImgUrl.includes('_coin');
|
||||||
|
|
||||||
|
symbol.className = `olympia-symbol`;
|
||||||
|
|
||||||
|
// Create inner div for sway/rotation
|
||||||
|
let innerDiv = document.createElement('div');
|
||||||
|
innerDiv.className = 'olympia-inner';
|
||||||
|
let img = null;
|
||||||
|
|
||||||
|
if (isRing) {
|
||||||
|
const colorName = randomImgUrl.split('ring_')[1].split('.')[0];
|
||||||
|
const ringColorMap = {
|
||||||
|
'blue': '#0081C8',
|
||||||
|
'yellow': '#FCB131',
|
||||||
|
'black': '#000000',
|
||||||
|
'green': '#00A651',
|
||||||
|
'red': '#EE334E'
|
||||||
|
};
|
||||||
|
let ringDiv = document.createElement('div');
|
||||||
|
ringDiv.className = 'olympia-ring-css';
|
||||||
|
ringDiv.style.setProperty('--ring-color', ringColorMap[colorName]);
|
||||||
|
innerDiv.appendChild(ringDiv);
|
||||||
|
|
||||||
|
// Add a 3D flip animation for rings
|
||||||
|
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');
|
||||||
|
img.src = randomImgUrl;
|
||||||
|
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);
|
||||||
|
|
||||||
|
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/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; // enable/disable oscar
|
||||||
|
|
||||||
|
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; // enable/disable pride
|
||||||
|
const elementCount = config.HeartCount !== undefined ? config.HeartCount : 20; // count of heart
|
||||||
|
const heartSize = config.HeartSize !== undefined ? config.HeartSize : 1.5; // size of hearts
|
||||||
|
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true; // optionally color the header with pride colors
|
||||||
|
|
||||||
|
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; // enable/disable rain
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300); // count of raindrops
|
||||||
|
const rainSpeed = config.RainSpeed !== undefined ? config.RainSpeed : 1.0; // speed of rain
|
||||||
|
|
||||||
|
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; // enable/disable resurrection
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 12; // count of symbols
|
||||||
|
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 5; // count of symbols on mobile
|
||||||
|
|
||||||
|
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) 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(count) {
|
||||||
|
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(count - images.length, 0);
|
||||||
|
addSymbols(extraCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeResurrection() {
|
||||||
|
if (!enableResurrection) return;
|
||||||
|
|
||||||
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
const count = !isMobile ? symbolCount : symbolCountMobile;
|
||||||
|
|
||||||
|
initResurrection(count);
|
||||||
|
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 |
@@ -1,15 +1,30 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Santa || {};
|
const config = window.SeasonalsPluginConfig?.Santa || {};
|
||||||
|
|
||||||
const santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
|
const santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
|
||||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||||
const snowFallSpeed = config.SnowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
|
const snowFallSpeed = config.SnowFallSpeed !== undefined ? config.SnowFallSpeed : 3; // speed of snowfall
|
||||||
const santaSpeed = config.SantaSpeed || 10; // speed of santa in seconds (recommended values: 5-15)
|
const santaSpeed = config.SantaSpeed !== undefined ? config.SantaSpeed : 10; // speed of santa in seconds
|
||||||
const santaSpeedMobile = config.SantaSpeedMobile || 8; // speed of santa on mobile devices in seconds
|
const santaSpeedMobile = config.SantaSpeedMobile !== undefined ? config.SantaSpeedMobile : 8; // speed of santa on mobile devices in seconds
|
||||||
const maxSantaRestTime = config.MaxSantaRestTime || 8; // maximum time santa rests in seconds
|
const maxSantaRestTime = config.MaxSantaRestTime !== undefined ? config.MaxSantaRestTime : 8; // maximum time santa rests in seconds
|
||||||
const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa rests in seconds
|
const minSantaRestTime = config.MinSantaRestTime !== undefined ? config.MinSantaRestTime : 3; // minimum time santa rests in seconds
|
||||||
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
|
const maxPresentFallSpeed = config.MaxPresentFallSpeed !== undefined ? 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 !== undefined ? 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
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,8 +310,8 @@ function initializeSanta() {
|
|||||||
}
|
}
|
||||||
const container = document.querySelector('.santa-container');
|
const container = document.querySelector('.santa-container');
|
||||||
if (container) {
|
if (container) {
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
|
||||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||||
isMobile = true;
|
isMobile = true;
|
||||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||||
snowflakesCount = snowflakesCountMobile;
|
snowflakesCount = snowflakesCountMobile;
|
||||||
|
|||||||
@@ -1,261 +1,246 @@
|
|||||||
// 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;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18
|
// Only inject settings if enabled on server by admin
|
||||||
|
if (this.config && this.config.EnableClientSideToggle !== false) {
|
||||||
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27
|
this.injectSettingsIcon();
|
||||||
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27
|
this.initialized = true;
|
||||||
|
console.log("Seasonals: Client-Side Settings Manager initialized.");
|
||||||
if (month === 11) return 'snowflakes'; // snowflakes december
|
|
||||||
if (month === 0 || month === 1) return 'snowfall'; // snow january, february
|
|
||||||
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february
|
|
||||||
|
|
||||||
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25
|
|
||||||
|
|
||||||
//NOT IMPLEMENTED YET
|
|
||||||
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
|
|
||||||
|
|
||||||
//NOT IMPLEMENTED YET
|
|
||||||
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
|
|
||||||
|
|
||||||
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
|
|
||||||
|
|
||||||
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
|
|
||||||
|
|
||||||
return 'none'; // Fallback (nothing)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper to resolve paths for local testing vs production
|
|
||||||
function 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;
|
},
|
||||||
}
|
|
||||||
|
|
||||||
// load theme csss
|
getSetting(key, defaultValue) {
|
||||||
function loadThemeCSS(cssPath) {
|
const value = localStorage.getItem(`seasonals-${key}`);
|
||||||
if (!cssPath) return;
|
|
||||||
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
// link.href = resolvePath(cssPath);
|
|
||||||
link.href = cssPath;
|
|
||||||
|
|
||||||
link.onerror = () => {
|
|
||||||
console.error(`Failed to load CSS: ${cssPath}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
console.log(`CSS file "${cssPath}" loaded.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load theme js
|
|
||||||
function loadThemeJS(jsPath) {
|
|
||||||
if (!jsPath) return;
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = jsPath;
|
|
||||||
// script.src = resolvePath(jsPath);
|
|
||||||
script.defer = true;
|
|
||||||
|
|
||||||
script.onerror = () => {
|
|
||||||
console.error(`Failed to load JS: ${jsPath}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.body.appendChild(script);
|
|
||||||
console.log(`JS file "${jsPath}" loaded.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update theme container class name
|
|
||||||
function updateThemeContainer(containerClass) {
|
|
||||||
// Create container if it doesn't exist
|
|
||||||
let container = document.querySelector('.seasonals-container');
|
|
||||||
if (!container) {
|
|
||||||
container = document.createElement('div');
|
|
||||||
container.className = 'seasonals-container';
|
|
||||||
document.body.appendChild(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
container.className = `seasonals-container ${containerClass}`;
|
|
||||||
console.log(`Seasonals-Container class updated to "${containerClass}".`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// function removeSelf() {
|
|
||||||
// const script = document.currentScript;
|
|
||||||
// if (script) script.parentNode.removeChild(script);
|
|
||||||
// console.log('External script removed:', script);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// initialize theme
|
|
||||||
async function initializeTheme() {
|
|
||||||
|
|
||||||
// Check local user preference
|
|
||||||
const isEnabled = getSavedSetting('seasonals-enabled', 'true') === 'true';
|
|
||||||
if (!isEnabled) {
|
|
||||||
console.log('Seasonals disabled by user preference.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const forcedTheme = getSavedSetting('seasonals-theme', 'auto');
|
|
||||||
|
|
||||||
let automateThemeSelection = true;
|
|
||||||
let defaultTheme = 'none';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/Seasonals/Config');
|
|
||||||
if (response.ok) {
|
|
||||||
const config = await response.json();
|
|
||||||
automateThemeSelection = config.AutomateSeasonSelection;
|
|
||||||
defaultTheme = config.SelectedSeason;
|
|
||||||
window.SeasonalsPluginConfig = config;
|
|
||||||
console.log('Seasonals Config loaded:', config);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to fetch Seasonals config');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching Seasonals config:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentTheme;
|
|
||||||
|
|
||||||
if (forcedTheme !== 'auto') {
|
|
||||||
currentTheme = forcedTheme;
|
|
||||||
console.log(`User forced theme: ${currentTheme}`);
|
|
||||||
} else if (automateThemeSelection === false) {
|
|
||||||
currentTheme = defaultTheme;
|
|
||||||
} else {
|
|
||||||
currentTheme = determineCurrentTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Selected theme: ${currentTheme}`);
|
|
||||||
|
|
||||||
if (!currentTheme || currentTheme === 'none') {
|
|
||||||
console.log('No theme selected.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = themeConfigs[currentTheme];
|
|
||||||
|
|
||||||
if (!theme) {
|
|
||||||
console.error(`Theme "${currentTheme}" not found.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateThemeContainer(theme.containerClass);
|
|
||||||
|
|
||||||
if (theme.css) loadThemeCSS(theme.css);
|
|
||||||
if (theme.js) loadThemeJS(theme.js);
|
|
||||||
|
|
||||||
console.log(`Theme "${currentTheme}" loaded.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
initializeTheme();
|
|
||||||
|
|
||||||
|
|
||||||
// User UI Seasonal Settings
|
|
||||||
|
|
||||||
function getSavedSetting(key, defaultValue) {
|
|
||||||
const value = localStorage.getItem(key);
|
|
||||||
return value !== null ? value : defaultValue;
|
return value !== null ? value : defaultValue;
|
||||||
}
|
},
|
||||||
|
|
||||||
function setSavedSetting(key, value) {
|
setSetting(key, value) {
|
||||||
localStorage.setItem(key, value);
|
localStorage.setItem(`seasonals-${key}`, value);
|
||||||
}
|
},
|
||||||
|
|
||||||
function createSettingsIcon() {
|
createIcon() {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
|
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
|
||||||
button.title = 'Seasonal Settings';
|
button.title = 'Seasonal Settings';
|
||||||
// button.innerHTML = '<span class="material-icons">ac_unit</span>';
|
// button.innerHTML = '<span class="material-icons">ac_unit</span>';
|
||||||
button.innerHTML = '<img src="/Seasonals/Resources/assets/logo_SW.svg" style="width: 24px; height: 24px; vertical-align: middle;">';
|
button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
||||||
button.style.verticalAlign = 'middle';
|
button.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
button.addEventListener('click', (e) => {
|
button.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleSettingsPopup(button);
|
this.toggleSettingsPopup(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
},
|
||||||
|
|
||||||
function createSettingsPopup(anchorElement) {
|
injectSettingsIcon() {
|
||||||
|
const observer = new MutationObserver((mutations, obs) => {
|
||||||
|
const headerRight = document.querySelector('.headerRight');
|
||||||
|
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
|
||||||
|
const icon = this.createIcon();
|
||||||
|
headerRight.prepend(icon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createPopup(anchorElement) {
|
||||||
const existing = document.querySelector('.seasonal-settings-popup');
|
const existing = document.querySelector('.seasonal-settings-popup');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
@@ -276,6 +261,7 @@ function createSettingsPopup(anchorElement) {
|
|||||||
|
|
||||||
const rect = anchorElement.getBoundingClientRect();
|
const rect = anchorElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Positioning logic
|
||||||
let rightPos = window.innerWidth - rect.right;
|
let rightPos = window.innerWidth - rect.right;
|
||||||
if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
|
if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
|
||||||
popup.style.right = '1rem';
|
popup.style.right = '1rem';
|
||||||
@@ -284,10 +270,12 @@ function createSettingsPopup(anchorElement) {
|
|||||||
popup.style.right = `${rightPos}px`;
|
popup.style.right = `${rightPos}px`;
|
||||||
popup.style.left = 'auto';
|
popup.style.left = 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.style.top = `${rect.bottom + 10}px`;
|
popup.style.top = `${rect.bottom + 10}px`;
|
||||||
|
|
||||||
popup.innerHTML = `
|
// Popup HTML
|
||||||
|
let html = `
|
||||||
|
<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
|
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
|
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
|
||||||
@@ -298,38 +286,40 @@ function createSettingsPopup(anchorElement) {
|
|||||||
<div class="selectContainer" style="margin-bottom: 0.5em;">
|
<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>
|
<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;">
|
<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">Auto (Date Based)</option>
|
<option value="auto">Server-Side</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
popup.innerHTML = html;
|
||||||
|
|
||||||
// Populate Select Options
|
// Populate Select Options
|
||||||
const themeSelect = popup.querySelector('#seasonal-theme-select');
|
const themeSelect = popup.querySelector('#seasonal-theme-select');
|
||||||
Object.keys(themeConfigs).forEach(key => {
|
Object.keys(ThemeConfigs).forEach(key => {
|
||||||
if (key === 'none') return;
|
if (key === 'none') return;
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = key;
|
option.value = key;
|
||||||
// Capitalize first letter
|
|
||||||
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set Initial Values
|
// Set Initial Values
|
||||||
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
|
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
|
||||||
enabledCheckbox.checked = getSavedSetting('seasonals-enabled', 'true') === 'true';
|
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
|
||||||
|
themeSelect.value = this.getSetting('theme', 'auto');
|
||||||
themeSelect.value = getSavedSetting('seasonals-theme', 'auto');
|
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
enabledCheckbox.addEventListener('change', (e) => {
|
enabledCheckbox.addEventListener('change', (e) => {
|
||||||
setSavedSetting('seasonals-enabled', e.target.checked);
|
this.setSetting('enabled', e.target.checked);
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
themeSelect.addEventListener('change', (e) => {
|
themeSelect.addEventListener('change', (e) => {
|
||||||
setSavedSetting('seasonals-theme', e.target.value);
|
this.setSetting('theme', e.target.value);
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
const closeHandler = (e) => {
|
const closeHandler = (e) => {
|
||||||
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
||||||
popup.remove();
|
popup.remove();
|
||||||
@@ -339,36 +329,185 @@ function createSettingsPopup(anchorElement) {
|
|||||||
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
||||||
|
|
||||||
document.body.appendChild(popup);
|
document.body.appendChild(popup);
|
||||||
}
|
},
|
||||||
|
|
||||||
function toggleSettingsPopup(anchorElement) {
|
toggleSettingsPopup(anchorElement) {
|
||||||
const existing = document.querySelector('.seasonal-settings-popup');
|
const existing = document.querySelector('.seasonal-settings-popup');
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.remove();
|
existing.remove();
|
||||||
} else {
|
} else {
|
||||||
createSettingsPopup(anchorElement);
|
this.createPopup(anchorElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function injectSettingsIcon() {
|
const SeasonalsManager = {
|
||||||
const observer = new MutationObserver((mutations, obs) => {
|
config: null,
|
||||||
// Check if admin has enabled this feature
|
|
||||||
if (window.SeasonalsPluginConfig && window.SeasonalsPluginConfig.EnableClientSideToggle === false) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerRight = document.querySelector('.headerRight');
|
console.log('Seasonals: Seasonals Config loaded:', this.config);
|
||||||
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
|
}
|
||||||
const icon = createSettingsIcon();
|
} catch (error) {
|
||||||
headerRight.prepend(icon);
|
console.error('Seasonals: Error fetching Seasonals config:', error);
|
||||||
// obs.disconnect();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
// Initialize Settings UI
|
||||||
childList: true,
|
SeasonalSettingsManager.init(this.config);
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
injectSettingsIcon();
|
// User Preference Check
|
||||||
|
const isEnabled = SeasonalSettingsManager.getSetting('enabled', 'true') === 'true';
|
||||||
|
if (!isEnabled) {
|
||||||
|
console.log('Seasonals: Disabled by user preference.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine Theme
|
||||||
|
const themeName = this.selectTheme();
|
||||||
|
console.log(`Seasonals: Selected theme: ${themeName}`);
|
||||||
|
|
||||||
|
if (!themeName || themeName === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Theme
|
||||||
|
this.applyTheme(themeName);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectTheme() {
|
||||||
|
// Check local override
|
||||||
|
const forcedTheme = SeasonalSettingsManager.getSetting('theme', 'auto');
|
||||||
|
if (forcedTheme !== 'auto') {
|
||||||
|
console.log(`Seasonals: User forced theme: ${forcedTheme}`);
|
||||||
|
return forcedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const automate = this.config ? this.config.AutomateSeasonSelection : true;
|
||||||
|
const defaultTheme = this.config ? this.config.SelectedSeason : 'none';
|
||||||
|
|
||||||
|
if (!automate) {
|
||||||
|
return defaultTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.determineCurrentThemeDate();
|
||||||
|
},
|
||||||
|
|
||||||
|
determineCurrentThemeDate() {
|
||||||
|
var rules = [];
|
||||||
|
try {
|
||||||
|
rules = JSON.parse(this.config.SeasonalRules || "[]");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Seasonals: Error parsing SeasonalRules", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length === 0) {
|
||||||
|
// Fallback to empty/none if no rules are defined (though default should exist)
|
||||||
|
console.log("Seasonals: No auto-selection rules found.");
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const month = date.getMonth() + 1; // 1-12
|
||||||
|
const day = date.getDate(); // 1-31
|
||||||
|
|
||||||
|
for (var i = 0; i < rules.length; i++) {
|
||||||
|
var rule = rules[i];
|
||||||
|
if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
|
||||||
|
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
|
||||||
|
return rule.Theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'none'; // No rule matched
|
||||||
|
},
|
||||||
|
|
||||||
|
isDateInRange: function(day, month, startDay, startMonth, endDay, endMonth) {
|
||||||
|
if (startMonth > endMonth) {
|
||||||
|
// Wrapping year (e.g. Dec to Jan)
|
||||||
|
return this.isDateAfterOrEqual(day, month, startDay, startMonth) ||
|
||||||
|
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
|
||||||
|
} else {
|
||||||
|
// Normal range
|
||||||
|
return this.isDateAfterOrEqual(day, month, startDay, startMonth) &&
|
||||||
|
this.isDateBeforeOrEqual(day, month, endDay, endMonth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDateAfterOrEqual: function(day, month, targetDay, targetMonth) {
|
||||||
|
if (month > targetMonth) return true;
|
||||||
|
if (month === targetMonth && day >= targetDay) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
isDateBeforeOrEqual: function(day, month, targetDay, targetMonth) {
|
||||||
|
if (month < targetMonth) return true;
|
||||||
|
if (month === targetMonth && day <= targetDay) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyTheme(themeName) {
|
||||||
|
const theme = ThemeConfigs[themeName];
|
||||||
|
if (!theme) {
|
||||||
|
console.error(`Seasonals: Theme "${themeName}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateThemeContainer(theme.containerClass);
|
||||||
|
|
||||||
|
if (theme.css) this.loadResource('css', theme.css);
|
||||||
|
if (theme.js) this.loadResource('js', theme.js);
|
||||||
|
|
||||||
|
console.log(`Seasonals: Theme "${themeName}" applied.`);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateThemeContainer(containerClass) {
|
||||||
|
let container = document.querySelector('.seasonals-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'seasonals-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
container.className = `seasonals-container ${containerClass}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// helper to resolve paths for local testing vs production
|
||||||
|
resolvePath(path) {
|
||||||
|
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
|
return path.replace('/Seasonals/Resources/', './');
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadResource(type, path) {
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
if (type === 'css') {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = path;
|
||||||
|
// link.href = resolvePath(cssPath);
|
||||||
|
link.onerror = () => console.error(`Seasonals: Failed to load CSS: ${path}`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
} else if (type === 'js') {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = path;
|
||||||
|
// script.src = resolvePath(jsPath);
|
||||||
|
script.defer = true;
|
||||||
|
script.onerror = () => console.error(`Seasonals: Failed to load JS: ${path}`);
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SeasonalsManager.init();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
#snowfallCanvas {
|
#snowfallCanvas {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Snowfall || {};
|
const config = window.SeasonalsPluginConfig?.Snowfall || {};
|
||||||
|
|
||||||
const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
|
const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
|
||||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||||
const snowFallSpeed = config.Speed || 3; // speed of snowfall (recommended values: 0-5)
|
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 3; // speed of snowfall
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -183,8 +181,8 @@ function initializeSnowfall() {
|
|||||||
}
|
}
|
||||||
const container = document.querySelector('.snowfall-container');
|
const container = document.querySelector('.snowfall-container');
|
||||||
if (container) {
|
if (container) {
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
|
||||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||||
snowflakesCount = snowflakesCountMobile;
|
snowflakesCount = snowflakesCountMobile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,60 +8,36 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snowflake {
|
.snowflake {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
top: -10%;
|
top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
translate: 0 -10vh;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-shadow: 0 0 5px #000;
|
text-shadow: 0 0 5px #000;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
-webkit-animation-name: heart-fall, heart-shake;
|
|
||||||
-webkit-animation-duration: 12s, 3s;
|
|
||||||
-webkit-animation-timing-function: linear, ease-in-out;
|
|
||||||
-webkit-animation-iteration-count: infinite, infinite;
|
|
||||||
animation-name: snowflakes-fall, snowflakes-shake;
|
animation-name: snowflakes-fall, snowflakes-shake;
|
||||||
animation-duration: 12s, 3s;
|
animation-duration: 12s, 3s;
|
||||||
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 snowflakes-fall {
|
|
||||||
0% {
|
|
||||||
top: -10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes snowflakes-shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateX(0);
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
-webkit-transform: translateX(80px);
|
|
||||||
transform: translateX(80px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes snowflakes-fall {
|
@keyframes snowflakes-fall {
|
||||||
0% {
|
0% {
|
||||||
top: -10%;
|
translate: 0 -10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
top: 100%;
|
translate: 0 110vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,63 +52,3 @@
|
|||||||
transform: translateX(80px);
|
transform: translateX(80px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.snowflake:nth-of-type(0) {
|
|
||||||
left: 0%;
|
|
||||||
animation-delay: 0s, 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(1) {
|
|
||||||
left: 10%;
|
|
||||||
animation-delay: 1s, 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(2) {
|
|
||||||
left: 20%;
|
|
||||||
animation-delay: 6s, 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(3) {
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: 4s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(4) {
|
|
||||||
left: 40%;
|
|
||||||
animation-delay: 2s, 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(5) {
|
|
||||||
left: 50%;
|
|
||||||
animation-delay: 8s, 3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-of-type(6) {
|
|
||||||
left: 60%;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Snowflakes || {};
|
const config = window.SeasonalsPluginConfig?.Snowflakes || {};
|
||||||
|
|
||||||
const snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
|
const snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
|
||||||
const randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random Snowflakes
|
const snowflakeCount = config.SnowflakeCount !== undefined ? config.SnowflakeCount : 25; // count of snowflakes
|
||||||
const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices
|
const snowflakeCountMobile = config.SnowflakeCountMobile !== undefined ? config.SnowflakeCountMobile : 10; // count of snowflakes on mobile
|
||||||
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable colored snowflakes
|
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable/disable colored snowflakes
|
||||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
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
|
||||||
|
|
||||||
@@ -38,22 +39,22 @@ 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 initSnowflakes(count) {
|
||||||
const snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
let snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
||||||
if (!snowflakeContainer) return; // exit if snowflake container is not found
|
if (!snowflakeContainer) {
|
||||||
|
snowflakeContainer = document.createElement("div");
|
||||||
|
snowflakeContainer.className = "snowflakes";
|
||||||
|
snowflakeContainer.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(snowflakeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Adding random snowflakes');
|
console.log('Adding 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
|
||||||
@@ -69,8 +70,8 @@ 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() * 14); // delay (-14s to 0s)
|
||||||
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}%`;
|
||||||
@@ -86,49 +87,18 @@ function addRandomSnowflakes(count) {
|
|||||||
// add the snowflake to the container
|
// add the snowflake to the container
|
||||||
snowflakeContainer.appendChild(snowflake);
|
snowflakeContainer.appendChild(snowflake);
|
||||||
}
|
}
|
||||||
console.log('Random snowflakes added');
|
console.log('Snowflakes added');
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize standard snowflakes
|
// initialize snowflakes
|
||||||
function initSnowflakes() {
|
|
||||||
const snowflakesContainer = document.querySelector('.snowflakes') || document.createElement("div");
|
|
||||||
|
|
||||||
if (!document.querySelector('.snowflakes')) {
|
|
||||||
snowflakesContainer.className = "snowflakes";
|
|
||||||
snowflakesContainer.setAttribute("aria-hidden", "true");
|
|
||||||
document.body.appendChild(snowflakesContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array of snowflake characters
|
|
||||||
const snowflakeSymbols = ['❅', '❆'];
|
|
||||||
|
|
||||||
// create the 12 standard snowflakes
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const snowflake = document.createElement('div');
|
|
||||||
snowflake.className = 'snowflake';
|
|
||||||
snowflake.textContent = snowflakeSymbols[i % 2]; // change between ❅ and ❆
|
|
||||||
|
|
||||||
// set random animation duration
|
|
||||||
if (enableDiffrentDuration) {
|
|
||||||
const randomAnimationDuration = Math.random() * 14 + 10; // delay (10s to 14s)
|
|
||||||
const randomAnimationDuration2 = Math.random() * 5 + 3; // delay (3s to 5s)
|
|
||||||
snowflake.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
snowflakesContainer.appendChild(snowflake);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize snowflakes and add random snowflakes
|
|
||||||
function initializeSnowflakes() {
|
function initializeSnowflakes() {
|
||||||
if (!snowflakes) return; // exit if snowflakes are disabled
|
if (!snowflakes) return; // exit if snowflakes are disabled
|
||||||
initSnowflakes();
|
|
||||||
toggleSnowflakes();
|
|
||||||
|
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
if (randomSnowflakes && (screenWidth > 768 || randomSnowflakesMobile)) { // add random snowflakes only on larger screens, unless enabled for mobile devices
|
const count = !isMobile ? snowflakeCount : snowflakeCountMobile;
|
||||||
addRandomSnowflakes(snowflakeCount);
|
|
||||||
}
|
initSnowflakes(count);
|
||||||
|
toggleSnowflakes();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeSnowflakes();
|
initializeSnowflakes();
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
contain: layout paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
#snowfallCanvas {
|
#snowfallCanvas {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const config = window.SeasonalsPluginConfig?.Snowstorm || {};
|
const config = window.SeasonalsPluginConfig?.Snowstorm || {};
|
||||||
|
|
||||||
const snowstorm = config.enableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
|
const snowstorm = config.EnableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
|
||||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||||
const snowFallSpeed = config.Speed || 6; // speed of snowfall (recommended values: 4-8)
|
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 6; // speed of snowstorm
|
||||||
const horizontalWind = config.HorizontalWind || 4; // horizontal wind speed (recommended value: 4)
|
const horizontalWind = config.HorizontalWind !== undefined ? config.HorizontalWind : 4; // horizontal wind strength
|
||||||
const verticalVariation = config.VerticalVariation || 2; // vertical variation (recommended value: 2)
|
const verticalVariation = config.VerticalVariation !== undefined ? config.VerticalVariation : 2; // vertical variation
|
||||||
|
|
||||||
let msgPrinted = false; // flag to prevent multiple console messages
|
let msgPrinted = false; // flag to prevent multiple console messages
|
||||||
|
|
||||||
@@ -49,12 +49,10 @@ function toggleSnowstorm() {
|
|||||||
|
|
||||||
// observe changes in the DOM
|
// observe changes in the DOM
|
||||||
const observer = new MutationObserver(toggleSnowstorm);
|
const observer = new MutationObserver(toggleSnowstorm);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -186,8 +184,8 @@ function initializeSnowstorm() {
|
|||||||
}
|
}
|
||||||
const container = document.querySelector('.snowstorm-container');
|
const container = document.querySelector('.snowstorm-container');
|
||||||
if (container) {
|
if (container) {
|
||||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||||
snowflakesCount = snowflakesCountMobile;
|
snowflakesCount = snowflakesCountMobile;
|
||||||
}
|
}
|
||||||
|
|||||||
101
Jellyfin.Plugin.Seasonals/Web/space.css
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
.space-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-bg-glow {
|
||||||
|
will-change: transform;
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100vw; height: 100vh;
|
||||||
|
background: radial-gradient(circle at 70% 30%, rgba(138, 43, 226, 0.15), transparent 60%),
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(65, 105, 225, 0.15), transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
animation: space-nebula-pulse 10s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes space-nebula-pulse {
|
||||||
|
0% { opacity: 0.6; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-starfield {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100vw; height: 100vh;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-shooting-star {
|
||||||
|
will-change: opacity;
|
||||||
|
position: absolute;
|
||||||
|
width: 250px;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: space-shoot 25s linear infinite;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes space-shoot {
|
||||||
|
0% { transform: rotate(var(--shoot-angle)) translateX(0); opacity: 0; }
|
||||||
|
5% { opacity: 1; }
|
||||||
|
35% { opacity: 1; }
|
||||||
|
40% { transform: rotate(var(--shoot-angle)) translateX(var(--shoot-distance)); opacity: 0; }
|
||||||
|
100% { transform: rotate(var(--shoot-angle)) translateX(var(--shoot-distance)); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-symbol {
|
||||||
|
position: absolute;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-symbol img {
|
||||||
|
will-change: transform;
|
||||||
|
width: 6vh;
|
||||||
|
height: auto;
|
||||||
|
max-width: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: space-slow-spin var(--rot-dur, 20s) linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific elements scaling */
|
||||||
|
.space-planet img { width: 8vh; max-width: 80px; }
|
||||||
|
.space-astronaut img { width: 10vh; max-width: 100px; }
|
||||||
|
.space-satellite img { width: 12vh; max-width: 120px; }
|
||||||
|
.space-iss img { width: 25vh; max-width: 180px; }
|
||||||
|
.space-rocket img { width: 12vh; max-width: 120px; }
|
||||||
|
|
||||||
|
@keyframes space-drift-right {
|
||||||
|
0% { transform: translateX(-10vw) translateY(0) scaleX(-1); }
|
||||||
|
50% { transform: translateX(60vw) translateY(-30vh) scaleX(-1); }
|
||||||
|
100% { transform: translateX(140vw) translateY(0) scaleX(-1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes space-drift-left {
|
||||||
|
0% { transform: translateX(10vw) translateY(0); }
|
||||||
|
50% { transform: translateX(-60vw) translateY(30vh); }
|
||||||
|
100% { transform: translateX(-140vw) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes space-slow-spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes space-star-drift {
|
||||||
|
from { transform: translateY(0); }
|
||||||
|
to { transform: translateY(-100vh); }
|
||||||
|
}
|
||||||
285
Jellyfin.Plugin.Seasonals/Web/space.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Space || {};
|
||||||
|
|
||||||
|
const space = config.EnableSpace !== undefined ? config.EnableSpace : true; // enable/disable space
|
||||||
|
const planetCountConf = config.PlanetCount !== undefined ? config.PlanetCount : 6; // count of planets
|
||||||
|
const astronautCountConf = config.AstronautCount !== undefined ? config.AstronautCount : 1; // count of astronaut
|
||||||
|
const satelliteCountConf = config.SatelliteCount !== undefined ? config.SatelliteCount : 4; // count of satellite
|
||||||
|
const issCountConf = config.IssCount !== undefined ? config.IssCount : 1; // count of iss
|
||||||
|
const rocketCountConf = config.RocketCount !== undefined ? config.RocketCount : 1; // count of rocket/space shuttle
|
||||||
|
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||||
|
const symbolCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 2; // Devisor to reduce number of objects on mobile
|
||||||
|
|
||||||
|
// Credit: https://lottiefiles.com/free-animation/astronaut-63lcWG4Xnh
|
||||||
|
const astronautImages = [
|
||||||
|
"../Seasonals/Resources/space_assets/astronaut_1.gif"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Credits: https://flaticon.com
|
||||||
|
const planetImages = [
|
||||||
|
"../Seasonals/Resources/space_assets/planet_1.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_2.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_3.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_4.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_5.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_6.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_7.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_8.png",
|
||||||
|
"../Seasonals/Resources/space_assets/planet_9.png"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Credits: https://lottiefiles.com/free-animation/s-satellite-vfnNE8AALo
|
||||||
|
const satelliteImages = [
|
||||||
|
"../Seasonals/Resources/space_assets/Satellite_1.gif",
|
||||||
|
"../Seasonals/Resources/space_assets/Satellite_2.gif"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Credit: https://pixabay.com/de/illustrations/raumstation-raum-struktur-8023777/
|
||||||
|
const issImage = "../Seasonals/Resources/space_assets/iss.png";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credits:
|
||||||
|
* https://lottiefiles.com/free-animation/rocket-MYUQ3UFq3k
|
||||||
|
* https://pixabay.com/de/vectors/space-shuttle-atlantis-nasa-156012/
|
||||||
|
*/
|
||||||
|
const rocketImages = [
|
||||||
|
"../Seasonals/Resources/space_assets/rocket.gif",
|
||||||
|
"../Seasonals/Resources/space_assets/space-shuttle.png"
|
||||||
|
]
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleSpace() {
|
||||||
|
const container = document.querySelector('.space-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('Space hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Space visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleSpace);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createSpace() {
|
||||||
|
const container = document.querySelector('.space-container') || document.createElement('div');
|
||||||
|
|
||||||
|
if (!document.querySelector('.space-container')) {
|
||||||
|
container.className = 'space-container';
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// const standardPlanetCount = 4;
|
||||||
|
// const standardAstronautCount = 1;
|
||||||
|
// const standardSatelliteCount = 2;
|
||||||
|
// const standardIssCount = 1;
|
||||||
|
// const standardRocketCount = 1;
|
||||||
|
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||||
|
let divisor = isMobile ? Math.max(1, symbolCountMobile) : 1;
|
||||||
|
let pCount = Math.floor(planetCountConf / divisor);
|
||||||
|
let aCount = Math.floor(astronautCountConf / divisor);
|
||||||
|
let sCount = Math.floor(satelliteCountConf / divisor);
|
||||||
|
let iCount = Math.floor(issCountConf / divisor);
|
||||||
|
let rCount = Math.floor(rocketCountConf / divisor);
|
||||||
|
|
||||||
|
// Add Nebula Glow
|
||||||
|
const bgGlow = document.createElement('div');
|
||||||
|
bgGlow.className = 'space-bg-glow';
|
||||||
|
container.appendChild(bgGlow);
|
||||||
|
|
||||||
|
// Add CSS Starfield
|
||||||
|
const starfield = document.createElement('div');
|
||||||
|
starfield.className = 'space-starfield';
|
||||||
|
let boxShadows1 = [];
|
||||||
|
let boxShadows2 = [];
|
||||||
|
let boxShadows3 = [];
|
||||||
|
|
||||||
|
// Generate random stars for parallax starfield using CSS % / vw sizes for responsiveness
|
||||||
|
for (let i = 0; i < 150; i++) {
|
||||||
|
let x = (Math.random() * 100).toFixed(2);
|
||||||
|
let y = (Math.random() * 100).toFixed(2);
|
||||||
|
boxShadows1.push(`${x}vw ${y}vh #FFF`);
|
||||||
|
boxShadows1.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
let x = (Math.random() * 100).toFixed(2);
|
||||||
|
let y = (Math.random() * 100).toFixed(2);
|
||||||
|
boxShadows2.push(`${x}vw ${y}vh #FFF`);
|
||||||
|
boxShadows2.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
let x = (Math.random() * 100).toFixed(2);
|
||||||
|
let y = (Math.random() * 100).toFixed(2);
|
||||||
|
boxShadows3.push(`${x}vw ${y}vh #FFF`);
|
||||||
|
boxShadows3.push(`${x}vw ${(parseFloat(y) + 100).toFixed(2)}vh #FFF`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const starLayer1 = document.createElement('div');
|
||||||
|
starLayer1.style.width = '1px'; starLayer1.style.height = '1px';
|
||||||
|
starLayer1.style.background = 'transparent';
|
||||||
|
starLayer1.style.boxShadow = boxShadows1.join(", ");
|
||||||
|
starLayer1.style.animation = 'space-star-drift 200s linear infinite';
|
||||||
|
starfield.appendChild(starLayer1);
|
||||||
|
|
||||||
|
const starLayer2 = document.createElement('div');
|
||||||
|
starLayer2.style.width = '2px'; starLayer2.style.height = '2px';
|
||||||
|
starLayer2.style.background = 'transparent';
|
||||||
|
starLayer2.style.boxShadow = boxShadows2.join(", ");
|
||||||
|
starLayer2.style.animation = 'space-star-drift 150s linear infinite';
|
||||||
|
starfield.appendChild(starLayer2);
|
||||||
|
|
||||||
|
const starLayer3 = document.createElement('div');
|
||||||
|
starLayer3.style.width = '3px'; starLayer3.style.height = '3px';
|
||||||
|
starLayer3.style.background = 'transparent';
|
||||||
|
starLayer3.style.boxShadow = boxShadows3.join(", ");
|
||||||
|
starLayer3.style.animation = 'space-star-drift 100s linear infinite';
|
||||||
|
starfield.appendChild(starLayer3);
|
||||||
|
|
||||||
|
container.appendChild(starfield);
|
||||||
|
|
||||||
|
// Shooting stars
|
||||||
|
const shootingStarCount = isMobile ? 1 : 2; // Less frequent
|
||||||
|
for (let i = 0; i < shootingStarCount; i++) {
|
||||||
|
const streak = document.createElement('div');
|
||||||
|
streak.className = 'space-shooting-star';
|
||||||
|
// Pick a random tail direction and fall direction to match
|
||||||
|
const isFromLeft = Math.random() > 0.5;
|
||||||
|
// Direction angle: random between 15deg-75deg (left) or 105deg-165deg (right)
|
||||||
|
// so they don't always fall in the exact same quadrant trajectory
|
||||||
|
let angle = isFromLeft
|
||||||
|
? Math.random() * 60 + 15
|
||||||
|
: Math.random() * 60 + 105;
|
||||||
|
|
||||||
|
streak.style.setProperty('--shoot-angle', `${angle}deg`);
|
||||||
|
|
||||||
|
const topStart = Math.random() * 50;
|
||||||
|
streak.style.left = isFromLeft ? '-20vw' : '120vw';
|
||||||
|
streak.style.top = `${topStart}vh`;
|
||||||
|
|
||||||
|
// Travel 200 viewport widths exactly along the rotated angle
|
||||||
|
streak.style.setProperty('--shoot-distance', '200vw');
|
||||||
|
|
||||||
|
streak.style.animationDelay = `${Math.random() * 20}s`;
|
||||||
|
|
||||||
|
// MARK: Shooting Star Speed
|
||||||
|
const flightCycleDuration = Math.random() * 10 + 15; // 15-25s
|
||||||
|
streak.style.animationDuration = `${flightCycleDuration}s`;
|
||||||
|
|
||||||
|
container.appendChild(streak);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useRandomDuration = enableDifferentDuration !== false;
|
||||||
|
|
||||||
|
function createSpaceItem(imageArr, cCount, addedClass) {
|
||||||
|
for (let i = 0; i < cCount; i++) {
|
||||||
|
let symbol = document.createElement('div');
|
||||||
|
|
||||||
|
const randomImage = imageArr[Math.floor(Math.random() * imageArr.length)];
|
||||||
|
symbol.className = `space-symbol ${addedClass}`;
|
||||||
|
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = randomImage;
|
||||||
|
img.onerror = function() {
|
||||||
|
this.style.display = 'none';
|
||||||
|
};
|
||||||
|
symbol.appendChild(img);
|
||||||
|
|
||||||
|
const topPos = Math.random() * 90; // 0 to 90vh
|
||||||
|
|
||||||
|
// Zero gravity sizes / speeds
|
||||||
|
const depth = Math.random();
|
||||||
|
// Make background elements (depth close to 0) much smaller than foreground
|
||||||
|
const distanceScale = 0.15 + (depth * 0.85); // 0.15 to 1.0
|
||||||
|
|
||||||
|
symbol.style.zIndex = Math.floor(depth * 30) + 20;
|
||||||
|
|
||||||
|
let durationSeconds = 30; // Very slow
|
||||||
|
if (useRandomDuration) {
|
||||||
|
durationSeconds = (1 - depth) * 40 + 30 + Math.random() * 10 - 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Randomly pick direction: left-to-right OR right-to-left
|
||||||
|
const goRight = Math.random() > 0.5;
|
||||||
|
const baseTransformScale = goRight ? 'scaleX(-1)' : 'scaleX(1)';
|
||||||
|
|
||||||
|
if (goRight) {
|
||||||
|
symbol.style.animationName = 'space-drift-right';
|
||||||
|
symbol.style.left = '-20vw';
|
||||||
|
symbol.style.right = 'auto';
|
||||||
|
} else {
|
||||||
|
symbol.style.animationName = 'space-drift-left';
|
||||||
|
symbol.style.right = '-20vw';
|
||||||
|
symbol.style.left = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol.style.top = `${topPos}vh`;
|
||||||
|
symbol.style.animationDuration = `${durationSeconds}s`;
|
||||||
|
|
||||||
|
// Negative delay correctly scatters them initially across the screen
|
||||||
|
// so they don't all appear to spawn from the edge at the start
|
||||||
|
const delaySeconds = -(Math.random() * durationSeconds);
|
||||||
|
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||||
|
|
||||||
|
// Slow rotation inside inner div
|
||||||
|
const rotationDiv = document.createElement('div');
|
||||||
|
const rotDur = Math.random() * 20 + 20; // 20-40s spin
|
||||||
|
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
|
||||||
|
rotationDiv.style.animation = `space-slow-spin ${rotDur}s linear infinite ${spinReverse}`;
|
||||||
|
|
||||||
|
// Apply final static scaling and facing to inner image directly
|
||||||
|
img.style.transform = `scale(${distanceScale}) ${baseTransformScale}`;
|
||||||
|
|
||||||
|
rotationDiv.appendChild(img);
|
||||||
|
symbol.appendChild(rotationDiv);
|
||||||
|
|
||||||
|
// Swap to a random image from the pool every time it completes an orbit (disappears)
|
||||||
|
if (imageArr.length > 1) {
|
||||||
|
// The animation delay pushes the initial cycle, so we use setInterval matched to duration
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
|
||||||
|
// Update only if currently out of bounds to avoid popping
|
||||||
|
const rect = symbol.getBoundingClientRect();
|
||||||
|
if (rect.right < 0 || rect.left > window.innerWidth) {
|
||||||
|
img.src = imageArr[Math.floor(Math.random() * imageArr.length)];
|
||||||
|
}
|
||||||
|
}, 2000); // Check occasionally if it's off screen
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSpaceItem(planetImages, pCount, 'space-planet');
|
||||||
|
createSpaceItem(astronautImages, aCount, 'space-astronaut');
|
||||||
|
createSpaceItem(satelliteImages, sCount, 'space-satellite');
|
||||||
|
createSpaceItem([issImage], iCount, 'space-iss');
|
||||||
|
createSpaceItem(rocketImages, rCount, 'space-rocket');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeSpace() {
|
||||||
|
if (!space) return;
|
||||||
|
createSpace();
|
||||||
|
toggleSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSpace();
|
||||||
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/Satellite_1.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/Satellite_2.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/astronaut_1.gif
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/iss.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_1.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/space_assets/planet_2.png
Normal file
|
After Width: | Height: | Size: 27 KiB |