Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a0bf5370bd | ||
|
|
c5800b431d | ||
|
|
9a4056651d | ||
|
|
87382db78e | ||
|
|
5d9afa762f | ||
|
|
2f88587dab | ||
|
|
360a959b69 | ||
|
|
36fba545cf | ||
|
|
7faa2cc766 | ||
|
|
aa832e93aa | ||
|
|
86bbeb583d | ||
|
|
7a642b34b8 | ||
|
|
926b30b8ce | ||
|
|
5b672cef42 | ||
|
|
ceccbf4ded | ||
|
|
9cba2a0755 | ||
|
|
af036a9aa4 | ||
|
|
cfefd2d2d3 | ||
|
|
8e7299508b | ||
|
|
fc7aa36f41 | ||
|
|
fc9896048f | ||
|
|
572c4d9ace |
@@ -51,7 +51,31 @@ jobs:
|
||||
echo "$CHANGELOG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if Release Already Exists
|
||||
id: check_release
|
||||
shell: bash
|
||||
run: |
|
||||
REPO_OWNER="${{ github.repository_owner }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
VERSION="${{ env.VERSION }}"
|
||||
TAG="v$VERSION"
|
||||
SERVER_URL="https://git.mahom03-spacecloud.de"
|
||||
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")
|
||||
|
||||
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||
echo "Release $TAG already exists. Skipping release-related steps."
|
||||
echo "release_exists=true" >> $GITHUB_OUTPUT
|
||||
elif [ "$HTTP_STATUS" -eq 404 ]; then
|
||||
echo "No existing release for $TAG. Continuing."
|
||||
echo "release_exists=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Unexpected response when checking release: $HTTP_STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and Zip
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
# Inject version from manifest into the build
|
||||
@@ -71,6 +95,7 @@ jobs:
|
||||
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
|
||||
|
||||
- name: Update manifest.json
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
REPO_OWNER="${{ github.repository_owner }}"
|
||||
@@ -90,12 +115,14 @@ jobs:
|
||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||
|
||||
- name: Commit manifest.json
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
uses: stefanzweifel/git-auto-commit-action@v7
|
||||
with:
|
||||
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
|
||||
file_pattern: manifest.json
|
||||
|
||||
- name: Create Release
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
server_url: "https://git.mahom03-spacecloud.de"
|
||||
@@ -109,6 +136,7 @@ jobs:
|
||||
|
||||
# Update Message in Remote Repository
|
||||
- name: Checkout Central Manifest Repo
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
|
||||
@@ -116,6 +144,7 @@ jobs:
|
||||
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
||||
|
||||
- name: Update Central Manifest
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
cd central-manifest
|
||||
@@ -171,6 +200,7 @@ jobs:
|
||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||
|
||||
- name: Commit and Push Central Manifest
|
||||
if: steps.check_release.outputs.release_exists == 'false'
|
||||
run: |
|
||||
cd central-manifest
|
||||
git config user.name "CodeDevMLH"
|
||||
|
||||
3
.gitignore
vendored
@@ -4,6 +4,5 @@ obj/
|
||||
.idea/
|
||||
artifacts
|
||||
|
||||
test-site.html
|
||||
test-site-new.html
|
||||
test-site-old.html
|
||||
RELEASE_GUIDE.md
|
||||
343
CONTRIBUTING.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Contributing to Jellyfin Seasonals Plugin
|
||||
|
||||
Thank you for your interest in contributing seasonal themes to the Jellyfin Seasonals Plugin! This guide explains how seasonal themes are structured, how to create your own, and how to test them locally before submitting a pull request.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Contributing to Jellyfin Seasonals Plugin](#contributing-to-jellyfin-seasonals-plugin)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Theme Architecture Overview](#theme-architecture-overview)
|
||||
- [Standard Theme File Structure](#standard-theme-file-structure)
|
||||
- [JavaScript File Pattern](#javascript-file-pattern)
|
||||
- [Key Rules](#key-rules)
|
||||
- [CSS File Pattern](#css-file-pattern)
|
||||
- [Key Rules](#key-rules-1)
|
||||
- [Image Assets (Optional)](#image-assets-optional)
|
||||
- [Registering Your Theme](#registering-your-theme)
|
||||
- [1. `seasonals.js` — Client-Side Registration](#1-seasonalsjs--client-side-registration)
|
||||
- [2. `PluginConfiguration.cs` and `configPage.html` - Server-Side Registration](#2-pluginconfigurationcs-and-configpagehtml---server-side-registration)
|
||||
- [Testing Your Theme Locally](#testing-your-theme-locally)
|
||||
- [Steps](#steps)
|
||||
- [What to Verify](#what-to-verify)
|
||||
- [Submitting Your Contribution](#submitting-your-contribution)
|
||||
- [Pull Request Checklist](#pull-request-checklist)
|
||||
- [PR Description Template](#pr-description-template)
|
||||
- [GitHub Issue Template for Theme Ideas](#github-issue-template-for-theme-ideas)
|
||||
- [Questions?](#questions)
|
||||
|
||||
---
|
||||
|
||||
## Theme Architecture Overview
|
||||
|
||||
Each seasonal theme consists of **2–3 components** that live in `Jellyfin.Plugin.Seasonals/Web/`:
|
||||
|
||||
| Component | File(s) | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| **JavaScript** | `{themeName}.js` | Animation logic, DOM manipulation, element creation |
|
||||
| **CSS** | `{themeName}.css` | Container styling, element appearance, keyframe animations |
|
||||
| **Images** *(optional)* | `{themeName}_images/` | Image assets (PNGs, SVGs) used by the theme |
|
||||
|
||||
The orchestrator file `seasonals.js` manages theme loading at runtime. It reads the plugin configuration, determines which theme should be active, and dynamically injects the correct CSS and JS files.
|
||||
|
||||
---
|
||||
|
||||
## Standard Theme File Structure
|
||||
|
||||
Here is a complete file layout for a theme called `mytheme`:
|
||||
|
||||
```
|
||||
Jellyfin.Plugin.Seasonals/
|
||||
└── Web/
|
||||
├── mytheme.js # Animation/DOM logic
|
||||
├── mytheme.css # Styles & animations
|
||||
├── mytheme_images/ # (Optional) image assets
|
||||
│ ├── sprite1.png
|
||||
│ └── sprite2.png
|
||||
└── seasonals.js # (Existing) Add your theme to ThemeConfigs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript File Pattern
|
||||
|
||||
Every theme JS file follows a **consistent skeleton**. Use this as your starting template:
|
||||
|
||||
```javascript
|
||||
// 1. Read Configuration
|
||||
const config = window.SeasonalsPluginConfig?.MyTheme || {};
|
||||
|
||||
const enabled = config.EnableMyTheme !== undefined ? config.EnableMyTheme : true;
|
||||
const elementCount = config.ElementCount || 25;
|
||||
// ... add more config options as needed
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// 2. Toggle Function
|
||||
// Hides the effect when a video player, trailer (in full width mode), dashboard, or user menu is active.
|
||||
function toggleMyTheme() {
|
||||
const container = document.querySelector('.mytheme-container');
|
||||
if (!container) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
container.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('MyTheme hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('MyTheme visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. MutationObserver
|
||||
// Watches the DOM for changes so the effect can auto-hide/show.
|
||||
const observer = new MutationObserver(toggleMyTheme);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// 4. Element Creation
|
||||
// Create and append your animated elements to the container.
|
||||
function createElements() {
|
||||
const container = document.querySelector('.mytheme-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.mytheme-container')) {
|
||||
container.className = 'mytheme-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
for (let i = 0; i < elementCount; i++) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'mytheme-element';
|
||||
|
||||
// Set random position, delay, duration, etc.
|
||||
el.style.left = `${Math.random() * 100}%`;
|
||||
el.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 4}s`;
|
||||
|
||||
// If using images:
|
||||
// const img = document.createElement('img');
|
||||
// img.src = '../Seasonals/Resources/mytheme_images/sprite1.png';
|
||||
// el.appendChild(img);
|
||||
|
||||
// If using text/emoji:
|
||||
// el.textContent = '⭐';
|
||||
|
||||
container.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Initialization
|
||||
function initializeMyTheme() {
|
||||
if (!enabled) return;
|
||||
createElements();
|
||||
toggleMyTheme();
|
||||
}
|
||||
|
||||
initializeMyTheme();
|
||||
```
|
||||
|
||||
### Key Rules
|
||||
|
||||
- **Always** read config from `window.SeasonalsPluginConfig?.{ThemeName}`.
|
||||
- **Always** implement the toggle function with the same selectors (`.videoPlayerContainer`, `.youtubePlayerContainer`, `.dashboardDocument`, `#app-user-menu`, just use the above template).
|
||||
- **Always** use `aria-hidden="true"` on the container for accessibility.
|
||||
- Call your `initialize` function at the end of the file.
|
||||
- For **canvas-based** themes (like `snowfall.js`), use a `<canvas>` element with `requestAnimationFrame` instead of CSS animations. Make sure to clean up with `cancelAnimationFrame` when hidden.
|
||||
|
||||
---
|
||||
|
||||
## CSS File Pattern
|
||||
|
||||
Every theme CSS file follows this structure:
|
||||
|
||||
```css
|
||||
/* Container */
|
||||
/* Full-screen overlay, transparent, non-interactive */
|
||||
.mytheme-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none; /* IMPORTANT: don't block user interaction */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Animated Element */
|
||||
.mytheme-element {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
|
||||
/* Two animations: movement + secondary effect (shake, rotate, etc.) */
|
||||
animation-name: mytheme-fall, mytheme-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
/* Keyframes */
|
||||
@keyframes mytheme-fall {
|
||||
0% { top: -10%; }
|
||||
100% { top: 100%; }
|
||||
}
|
||||
|
||||
@keyframes mytheme-shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
50% { transform: translateX(80px); }
|
||||
}
|
||||
|
||||
/* Staggered Delays for Base Elements */
|
||||
/* Spread the initial 12 elements across the screen */
|
||||
.mytheme-element:nth-of-type(1) { left: 10%; animation-delay: 1s, 1s; }
|
||||
.mytheme-element:nth-of-type(2) { left: 20%; animation-delay: 6s, 0.5s; }
|
||||
.mytheme-element:nth-of-type(3) { left: 30%; animation-delay: 4s, 2s; }
|
||||
/* ... continue for each base element */
|
||||
```
|
||||
|
||||
### Key Rules
|
||||
|
||||
- **Container** must be `position: fixed`, full-screen, with `pointer-events: none` and at least `z-index: 10`.
|
||||
- **Elements** should use `position: fixed` with at least `z-index: 15`.
|
||||
- Use **animations** (eg. primary movement + secondary effect for natural-looking motion).
|
||||
- Include **`nth-of-type` rules** for the initial set of base elements to stagger them.
|
||||
- Include **webkit prefixes** (`-webkit-animation-*`, `@-webkit-keyframes`) for broader compatibility (see existing themes for examples).
|
||||
|
||||
---
|
||||
|
||||
## Image Assets (Optional)
|
||||
|
||||
If your theme uses images (e.g., leaves, ghosts, eggs):
|
||||
|
||||
1. Create a folder: `Jellyfin.Plugin.Seasonals/Web/{themeName}_images/`
|
||||
2. Place your assets inside (PNG recommended, keep files small)
|
||||
3. Reference them in JS using the production path:
|
||||
```javascript
|
||||
img.src = '../Seasonals/Resources/mytheme_images/sprite1.png';
|
||||
```
|
||||
---
|
||||
|
||||
## Registering Your Theme
|
||||
|
||||
After creating your JS and CSS files, you need to register the theme in two places:
|
||||
|
||||
### 1. `seasonals.js` — Client-Side Registration
|
||||
|
||||
Add your theme to the `ThemeConfigs` object:
|
||||
|
||||
```javascript
|
||||
const ThemeConfigs = {
|
||||
// ... existing themes ...
|
||||
mytheme: {
|
||||
css: '../Seasonals/Resources/mytheme.css',
|
||||
js: '../Seasonals/Resources/mytheme.js',
|
||||
containerClass: 'mytheme-container'
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 2. `PluginConfiguration.cs` and `configPage.html` - Server-Side Registration
|
||||
|
||||
> [!NOTE]
|
||||
> The backend registration is handled by the plugin maintainers. You do **not** need to modify C# files for your theme submission. Just focus on the JS/CSS/images.
|
||||
>
|
||||
> However, if you'd like to include full backend integration, add your theme to the enum/configuration in `Configuration/PluginConfiguration.cs` and the selectors in `configPage.html`.
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Theme Locally
|
||||
|
||||
You can test your theme without a Jellyfin server by using the included test site.
|
||||
|
||||
### Steps
|
||||
|
||||
1. Navigate to the `Jellyfin.Plugin.Seasonals/Web/` directory
|
||||
2. Open [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html) in your browser (just double-click the file) or vscode or what ever you use...
|
||||
3. Use the **theme selector dropdown** to pick an existing theme or select **"Custom (Local Files)"** to test your own
|
||||
4. When "Custom" is selected, enter your theme's JS and CSS filenames (e.g., `mytheme.js` and `mytheme.css` (must be in the same folder as [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html) for this to work))
|
||||
5. Click **"Load Theme"** to apply. Click **"Clear & Reload"** to reset and try again
|
||||
|
||||
### What to Verify
|
||||
|
||||
- ✅ The effect is visible on the background
|
||||
- ✅ The animation runs smoothly
|
||||
- ✅ Elements are spread across the full viewport
|
||||
- ✅ The mock header is **not blocked** by the effect (thanks to `pointer-events: none`)
|
||||
- ✅ No theme related console errors appear (check DevTools → Console)
|
||||
|
||||
---
|
||||
|
||||
## Submitting Your Contribution
|
||||
|
||||
### Pull Request Checklist
|
||||
|
||||
- [ ] Created `{themeName}.js` following the [JS pattern](#javascript-file-pattern)
|
||||
- [ ] Created `{themeName}.css` following the [CSS pattern](#css-file-pattern)
|
||||
- [ ] (If applicable) Created `{themeName}_images/` with optimized assets
|
||||
- [ ] Added theme to `ThemeConfigs` in `seasonals.js`
|
||||
- [ ] Tested locally with [`test-site.html`](./Jellyfin.Plugin.Seasonals/Web/test-site.html)
|
||||
- [ ] No theme related console errors
|
||||
- [ ] Effect has `pointer-events: none` (doesn't block the UI)
|
||||
- [ ] Effect hides during video/trailer playback (toggle function implemented)
|
||||
- [ ] (Optional) Included a screenshot or short recording of the effect to the readme
|
||||
|
||||
### PR Description Template
|
||||
|
||||
```
|
||||
## New Seasonal Theme: {Theme Name}
|
||||
|
||||
**Description:** Brief description of the theme and what occasion/season it's for.
|
||||
|
||||
**Screenshot / Recording:**
|
||||
[Attach a screenshot or GIF showcasing the theme in action]
|
||||
|
||||
**Testing:**
|
||||
- Tested locally with test-site-new.html ✅
|
||||
- No console errors ✅
|
||||
- pointer-events: none verified ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GitHub Issue Template for Theme Ideas
|
||||
|
||||
If you have an idea for a seasonal theme but don't want to implement it yourself, feel free to open an issue using the following template:
|
||||
|
||||
**Title:** `[Theme Idea] {Season/Holiday Name} Theme`
|
||||
|
||||
**Body:**
|
||||
```
|
||||
## 🎨 Theme Idea: {Season/Holiday Name}
|
||||
|
||||
**Occasion/Season:** What time of year is this for?
|
||||
|
||||
**Description:** Describe the visual effect you have in mind.
|
||||
|
||||
**Visual References:** Links to images, GIFs, or videos that capture the aesthetic.
|
||||
|
||||
**Suggested Active Period:** e.g. "March 1 – March 17" for St. Patrick's Day
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have any questions about contributing, feel free to open an issue. Happy theming! 🎉
|
||||
207
Injector_new.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Jellyfin.Plugin.Seasonals.Helpers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the injection of the Seasonals script into the Jellyfin web interface.
|
||||
/// </summary>
|
||||
public class ScriptInjector
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<ScriptInjector> _logger;
|
||||
public const string ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
|
||||
public const string Marker = "</body>";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScriptInjector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects the script tag into index.html if it's not already present.
|
||||
/// </summary>
|
||||
public void Inject()
|
||||
{
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath))
|
||||
{
|
||||
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
|
||||
RegisterFileTransformation();
|
||||
return;
|
||||
}
|
||||
|
||||
var indexPath = Path.Combine(webPath, "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
_logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
|
||||
RegisterFileTransformation();
|
||||
return;
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (!content.Contains(ScriptTag))
|
||||
{
|
||||
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
content = content.Insert(index, ScriptTag + Environment.NewLine);
|
||||
File.WriteAllText(indexPath, content);
|
||||
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Script already present in index.html. Or could not be injected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
|
||||
RegisterFileTransformation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
|
||||
RegisterFileTransformation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the script tag from index.html.
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
UnregisterFileTransformation();
|
||||
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var indexPath = Path.Combine(webPath, "index.html");
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (content.Contains(ScriptTag))
|
||||
{
|
||||
content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
|
||||
File.WriteAllText(indexPath, content);
|
||||
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
|
||||
} else {
|
||||
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error removing Seasonals script.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the path to the Jellyfin web interface directory.
|
||||
/// </summary>
|
||||
/// <returns>The path to the web directory, or null if not found.</returns>
|
||||
private string? GetWebPath()
|
||||
{
|
||||
// Use reflection to access WebPath property to ensure compatibility across different Jellyfin versions
|
||||
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
|
||||
return prop?.GetValue(_appPaths) as string;
|
||||
}
|
||||
|
||||
private void RegisterFileTransformation()
|
||||
{
|
||||
_logger.LogInformation("Seasonals Fallback. Registering file transformations.");
|
||||
|
||||
List<JObject> payloads = new List<JObject>();
|
||||
|
||||
{
|
||||
JObject payload = new JObject();
|
||||
payload.Add("id", "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
payload.Add("fileNamePattern", "index.html");
|
||||
payload.Add("callbackAssembly", GetType().Assembly.FullName);
|
||||
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
|
||||
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
|
||||
|
||||
payloads.Add(payload);
|
||||
}
|
||||
|
||||
Assembly? fileTransformationAssembly =
|
||||
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
|
||||
x.FullName?.Contains(".FileTransformation") ?? false);
|
||||
|
||||
if (fileTransformationAssembly != null)
|
||||
{
|
||||
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||
|
||||
if (pluginInterfaceType != null)
|
||||
{
|
||||
foreach (JObject payload in payloads)
|
||||
{
|
||||
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
|
||||
}
|
||||
_logger.LogInformation("File transformations registered successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback injection skipped.");
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterFileTransformation()
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly? fileTransformationAssembly =
|
||||
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
|
||||
x.FullName?.Contains(".FileTransformation") ?? false);
|
||||
|
||||
if (fileTransformationAssembly != null)
|
||||
{
|
||||
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||
|
||||
if (pluginInterfaceType != null)
|
||||
{
|
||||
Guid id = Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
|
||||
_logger.LogInformation("File transformation unregistered successfully.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public class SeasonalsController : ControllerBase
|
||||
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) return "image/png";
|
||||
if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)) return "image/jpeg";
|
||||
if (path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) return "image/gif";
|
||||
if (path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) return "image/svg+xml";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,40 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
EnableClientSideToggle = true;
|
||||
|
||||
Autumn = new AutumnOptions();
|
||||
Snowflakes = new SnowflakesOptions();
|
||||
Snowfall = new SnowfallOptions();
|
||||
Snowstorm = new SnowstormOptions();
|
||||
Birthday = new BirthdayOptions();
|
||||
Carnival = new CarnivalOptions();
|
||||
CherryBlossom = new CherryBlossomOptions();
|
||||
Christmas = new ChristmasOptions();
|
||||
EarthDay = new EarthDayOptions();
|
||||
Easter = new EasterOptions();
|
||||
Eid = new EidOptions();
|
||||
Eurovision = new EurovisionOptions();
|
||||
FilmNoir = new FilmNoirOptions();
|
||||
Fireworks = new FireworksOptions();
|
||||
Friday13 = new Friday13Options();
|
||||
Frost = new FrostOptions();
|
||||
Halloween = new HalloweenOptions();
|
||||
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();
|
||||
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>
|
||||
@@ -49,120 +74,315 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
public bool EnableClientSideToggle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the seasonal rules configuration as JSON.
|
||||
/// </summary>
|
||||
public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"},{\"Name\":\"Cherry Blossom\",\"StartDay\":1,\"StartMonth\":4,\"EndDay\":30,\"EndMonth\":4,\"Theme\":\"cherryblossom\"}]";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Seasonals options.
|
||||
/// </summary>
|
||||
public AutumnOptions Autumn { get; set; }
|
||||
public SnowflakesOptions Snowflakes { get; set; }
|
||||
public SnowfallOptions Snowfall { get; set; }
|
||||
public SnowstormOptions Snowstorm { get; set; }
|
||||
public BirthdayOptions Birthday { get; set; }
|
||||
public CarnivalOptions Carnival { 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 Friday13Options Friday13 { get; set; }
|
||||
public FrostOptions Frost { get; set; }
|
||||
public HalloweenOptions Halloween { 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 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 int LeafCount { get; set; } = 25;
|
||||
public class AutumnOptions {
|
||||
public bool EnableAutumn { get; set; } = true;
|
||||
public bool EnableRandomLeaves { get; set; } = true;
|
||||
public bool EnableRandomLeavesMobile { get; set; } = false;
|
||||
public int LeafCount { get; set; } = 35;
|
||||
public int LeafCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableRotation { get; set; } = false;
|
||||
}
|
||||
|
||||
public class SnowflakesOptions
|
||||
{
|
||||
public int SnowflakeCount { get; set; } = 25;
|
||||
public bool EnableSnowflakes { get; set; } = true;
|
||||
public bool EnableRandomSnowflakes { get; set; } = true;
|
||||
public bool EnableRandomSnowflakesMobile { get; set; } = false;
|
||||
public bool EnableColoredSnowflakes { get; set; } = true;
|
||||
public class BirthdayOptions {
|
||||
public bool EnableBirthday { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 12;
|
||||
public int SymbolCountMobile { get; set; } = 5;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public int ConfettiCount { get; set; } = 60;
|
||||
}
|
||||
|
||||
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 class SnowfallOptions
|
||||
{
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double Speed { get; set; } = 3;
|
||||
public bool EnableSnowfall { get; set; } = true;
|
||||
public class ChristmasOptions {
|
||||
public bool EnableChristmas { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SnowstormOptions
|
||||
{
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
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 EarthDayOptions {
|
||||
public bool EnableEarthDay { get; set; } = true;
|
||||
public int VineCount { get; set; } = 4;
|
||||
}
|
||||
|
||||
public class FireworksOptions
|
||||
{
|
||||
public int ParticleCount { get; set; } = 50;
|
||||
public int LaunchInterval { get; set; } = 3200;
|
||||
public class EasterOptions {
|
||||
public bool EnableEaster { get; set; } = true;
|
||||
public bool EnableBunny { get; set; } = true;
|
||||
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 ScrollFireworks { get; set; } = true;
|
||||
public int ParticleCount { get; set; } = 50;
|
||||
public int MinFireworks { get; set; } = 3;
|
||||
public int MaxFireworks { get; set; } = 6;
|
||||
public int LaunchInterval { get; set; } = 3200;
|
||||
}
|
||||
|
||||
public class HalloweenOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public class Friday13Options {
|
||||
public bool EnableFriday13 { get; set; } = true;
|
||||
}
|
||||
|
||||
public class FrostOptions {
|
||||
public bool EnableFrost { get; set; } = true;
|
||||
}
|
||||
|
||||
public class HalloweenOptions {
|
||||
public bool EnableHalloween { get; set; } = true;
|
||||
public bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableSpiders { get; set; } = true;
|
||||
public bool EnableMice { get; set; } = true;
|
||||
}
|
||||
|
||||
public class HeartsOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public class HeartsOptions {
|
||||
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 bool EnableChristmas { get; set; } = true;
|
||||
public bool EnableRandomChristmas { get; set; } = true;
|
||||
public bool EnableRandomChristmasMobile { get; set; } = false;
|
||||
public int SymbolCountMobile { get; set; } = 10;
|
||||
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 SnowflakesCountMobile { get; set; } = 250;
|
||||
public double SnowFallSpeed { get; set; } = 3;
|
||||
public double SantaSpeed { get; set; } = 10;
|
||||
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 MinSantaRestTime { get; set; } = 3;
|
||||
public double MaxPresentFallSpeed { get; set; } = 5;
|
||||
public double MinPresentFallSpeed { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class EasterOptions
|
||||
{
|
||||
public int EggCount { get; set; } = 20;
|
||||
public bool EnableEaster { get; set; } = true;
|
||||
public bool EnableRandomEaster { get; set; } = true;
|
||||
public bool EnableRandomEasterMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableBunny { get; set; } = true;
|
||||
public int BunnyDuration { get; set; } = 12000;
|
||||
public int HopHeight { get; set; } = 12;
|
||||
public int MinBunnyRestTime { get; set; } = 2000;
|
||||
public int MaxBunnyRestTime { get; set; } = 5000;
|
||||
public class SnowfallOptions {
|
||||
public bool EnableSnowfall { get; set; } = true;
|
||||
public int SnowflakesCount { get; set; } = 500;
|
||||
public int SnowflakesCountMobile { get; set; } = 250;
|
||||
public double Speed { get; set; } = 3;
|
||||
}
|
||||
|
||||
public class SnowflakesOptions {
|
||||
public bool EnableSnowflakes { get; set; } = true;
|
||||
public int SnowflakeCount { get; set; } = 25;
|
||||
public int SnowflakeCountMobile { get; set; } = 10;
|
||||
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> -->
|
||||
<Title>Jellyfin Seasonals Plugin</Title>
|
||||
<Authors>CodeDevMLH</Authors>
|
||||
<Version>1.6.4.0</Version>
|
||||
<Version>2.0.0.5</Version>
|
||||
<RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<None Remove="Configuration\configPage.html" />
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
<None Remove="Web\**" />
|
||||
<EmbeddedResource Include="Web\**" />
|
||||
<EmbeddedResource Include="Web\**" Exclude="Web\test-site.html" />
|
||||
|
||||
<None Include="..\README.md" />
|
||||
<None Include="..\logo.png" CopyToOutputDirectory="Always" />
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 ScriptTag = "<script src=\"../Seasonals/Resources/seasonals.js\" defer></script>";
|
||||
public const string Marker = "</body>";
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +56,18 @@ public class ScriptInjector
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -113,6 +125,17 @@ public class ScriptInjector
|
||||
} else {
|
||||
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
|
||||
}
|
||||
|
||||
// MARK: Legacy Tags, remove in future versions
|
||||
// Remove legacy tags
|
||||
bool modified = false;
|
||||
content = RemoveLegacyTags(content, ref modified);
|
||||
if (modified)
|
||||
{
|
||||
_logger.LogInformation("Removed legacy tags from index.html.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
@@ -204,4 +227,21 @@ public class ScriptInjector
|
||||
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Legacy Tags, remove in future versions
|
||||
/// <summary>
|
||||
/// Removes legacy script tags from the content.
|
||||
/// </summary>
|
||||
private string RemoveLegacyTags(string content, ref bool modified)
|
||||
{
|
||||
// Legacy tags (used in versions prior to 1.6.3.0 where paths started with / instead of ../)
|
||||
const string LegacyScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
|
||||
|
||||
if (content.Contains(LegacyScriptTag))
|
||||
{
|
||||
content = content.Replace(LegacyScriptTag + Environment.NewLine, "").Replace(LegacyScriptTag, "");
|
||||
modified = true;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
1343
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW.svg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_192x192.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_96x96.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -8,24 +8,21 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
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: 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-duration: 7s, 4s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
@@ -38,34 +35,17 @@
|
||||
--rotate-end: 0deg !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
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 {
|
||||
0%, 100% {
|
||||
@@ -75,87 +55,3 @@
|
||||
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 leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable leaves
|
||||
const randomLeaves = config.EnableRandomLeaves !== undefined ? config.EnableRandomLeaves : true; // enable random leaves
|
||||
const randomLeavesMobile = config.EnableRandomLeavesMobile !== undefined ? config.EnableRandomLeavesMobile : false; // enable random leaves on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random leaves
|
||||
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable leaf rotation
|
||||
const leafCount = config.LeafCount || 25; // count of random extra leaves
|
||||
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable autumn
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableRotation = config.EnableRotation !== undefined ? config.EnableRotation : false; // enable/disable rotation
|
||||
const leafCount = config.LeafCount !== undefined ? config.LeafCount : 35; // count of random extra leaves
|
||||
const leafCountMobile = config.LeafCountMobile !== undefined ? config.LeafCountMobile : 10; // count of random extra leaves on mobile
|
||||
|
||||
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
|
||||
|
||||
@@ -38,40 +56,23 @@ function toggleAutumn() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleAutumn);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"/Seasonals/Resources/autumn_images/acorn1.png",
|
||||
"/Seasonals/Resources/autumn_images/acorn2.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf1.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf2.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf3.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf4.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf5.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf6.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf7.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf8.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf9.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf10.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf11.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf12.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf13.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf14.png",
|
||||
"/Seasonals/Resources/autumn_images/leaf15.png",
|
||||
];
|
||||
function initLeaves(count) {
|
||||
let autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||
if (!autumnContainer) {
|
||||
autumnContainer = document.createElement("div");
|
||||
autumnContainer.className = "autumn-container";
|
||||
autumnContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(autumnContainer);
|
||||
}
|
||||
|
||||
function addRandomLeaves(count) {
|
||||
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');
|
||||
console.log('Adding leaves');
|
||||
|
||||
// Array of leave characters
|
||||
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)
|
||||
const randomLeft = Math.random() * 100; // position (0% to 100%)
|
||||
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
|
||||
leaveDiv.style.left = `${randomLeft}%`;
|
||||
@@ -118,60 +121,18 @@ function addRandomLeaves(count) {
|
||||
// add the leave to the container
|
||||
autumnContainer.appendChild(leaveDiv);
|
||||
}
|
||||
console.log('Random leaves added');
|
||||
console.log('Leaves added');
|
||||
}
|
||||
|
||||
// initialize standard 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
|
||||
// initialize leaves
|
||||
function initializeLeaves() {
|
||||
if (!leaves) return; // exit if leaves are disabled
|
||||
initLeaves();
|
||||
toggleAutumn();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomLeaves && (screenWidth > 768 || randomLeavesMobile)) { // add random leaves only on larger screens, unless enabled for mobile devices
|
||||
addRandomLeaves(leafCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? leafCount : leafCountMobile;
|
||||
|
||||
initLeaves(count);
|
||||
toggleAutumn();
|
||||
}
|
||||
|
||||
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%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.christmas {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
user-select: none;
|
||||
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-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
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 {
|
||||
0% {
|
||||
top: -10%;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
translate: 0 110vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
@@ -76,63 +51,3 @@
|
||||
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 christmas = config.EnableChristmas !== undefined ? config.EnableChristmas : true; // enable/disable christmas
|
||||
const randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random Christmas
|
||||
const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ? config.EnableRandomChristmasMobile : false; // enable random Christmas on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random Christmas symbols
|
||||
const christmasCount = config.SymbolCount || 25; // count of random extra christmas
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const christmasCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||
const christmasCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
|
||||
|
||||
// Array of christmas characters
|
||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -37,22 +38,22 @@ function toggleChristmas() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleChristmas);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
// Array of christmas characters
|
||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||
function initChristmas(count) {
|
||||
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) {
|
||||
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');
|
||||
console.log('Adding christmas');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new christmas element
|
||||
@@ -64,8 +65,8 @@ function addRandomChristmas(count) {
|
||||
|
||||
// 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 + 8; // delay (8s to 12s)
|
||||
const randomAnimationDelay2 = Math.random() * 5 + 3; // delay (0s to 5s)
|
||||
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
christmasDiv.style.left = `${randomLeft}%`;
|
||||
@@ -81,46 +82,18 @@ function addRandomChristmas(count) {
|
||||
// add the christmas to the container
|
||||
christmasContainer.appendChild(christmasDiv);
|
||||
}
|
||||
console.log('Random christmas added');
|
||||
console.log('Christmas added');
|
||||
}
|
||||
|
||||
// initialize standard 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
|
||||
// initialize christmas
|
||||
function initializeChristmas() {
|
||||
if (!christmas) return; // exit if christmas is disabled
|
||||
initChristmas();
|
||||
toggleChristmas();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomChristmas && (screenWidth > 768 || randomChristmasMobile)) { // add random christmas only on larger screens, unless enabled for mobile devices
|
||||
addRandomChristmas(christmasCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? christmasCount : christmasCountMobile;
|
||||
|
||||
initChristmas(count);
|
||||
toggleChristmas();
|
||||
}
|
||||
|
||||
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 {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
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 {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
bottom: 10px;
|
||||
width: 70px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 0;
|
||||
width: 160px;
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hopping-rabbit {
|
||||
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 easter = config.EnableEaster !== undefined ? config.EnableEaster : true; // enable/disable easter
|
||||
const randomEaster = config.EnableRandomEaster !== undefined ? config.EnableRandomEaster : true; // enable random easter
|
||||
const randomEasterMobile = config.EnableRandomEasterMobile !== undefined ? config.EnableRandomEasterMobile : false; // enable random easter on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random easter
|
||||
const easterEggCount = config.EggCount || 20; // count of random extra easter
|
||||
const enableBunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable bunny
|
||||
const minBunnyRestTime = config.MinBunnyRestTime !== undefined ? config.MinBunnyRestTime : 2000; // timing parameter
|
||||
const maxBunnyRestTime = config.MaxBunnyRestTime !== undefined ? config.MaxBunnyRestTime : 5000; // timing parameter
|
||||
const eggCount = config.EggCount !== undefined ? config.EggCount : 15; // count of egg
|
||||
|
||||
const bunny = config.EnableBunny !== undefined ? config.EnableBunny : true; // enable/disable hopping bunny
|
||||
const bunnyDuration = config.BunnyDuration || 12000; // duration of the bunny animation in ms
|
||||
const hopHeight = config.HopHeight || 12; // height of the bunny hops in px
|
||||
const minBunnyRestTime = config.MinBunnyRestTime || 2000; // minimum time the bunny rests in ms
|
||||
const maxBunnyRestTime = config.MaxBunnyRestTime || 5000; // maximum time the bunny rests in ms
|
||||
/* MARK: Bunny movement config */
|
||||
const jumpDistanceVw = 5; // Distance in vw the bunny covers per jump
|
||||
const jumpDurationMs = 770; // Time in ms the bunny spends moving during a jump
|
||||
const pauseDurationMs = 116.6666; // Time in ms the bunny pauses between jumps
|
||||
|
||||
const rabbit = "../Seasonals/Resources/easter_images/Osterhase.gif";
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
let animationFrameId;
|
||||
// Credit: https://flaticon.com
|
||||
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() {
|
||||
const easterContainer = document.querySelector('.easter-container');
|
||||
if (!easterContainer) return;
|
||||
@@ -26,21 +42,20 @@ function toggleEaster() {
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide easter if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
easterContainer.style.display = 'none'; // hide easter
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
easterContainer.style.display = 'none';
|
||||
if (rabbitTimeout) {
|
||||
clearTimeout(rabbitTimeout);
|
||||
isAnimating = false;
|
||||
}
|
||||
if (!msgPrinted) {
|
||||
console.log('Easter hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
easterContainer.style.display = 'block'; // show easter
|
||||
if (!animationFrameId) {
|
||||
animateRabbit(); // start animation
|
||||
easterContainer.style.display = 'block';
|
||||
if (!isAnimating && enableBunny) {
|
||||
animateRabbit(document.querySelector('#rabbit'));
|
||||
}
|
||||
if (msgPrinted) {
|
||||
console.log('Easter visible');
|
||||
@@ -49,145 +64,201 @@ function toggleEaster() {
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleEaster);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"/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",
|
||||
];
|
||||
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`;
|
||||
}
|
||||
|
||||
|
||||
// add the leave to the container
|
||||
easterContainer.appendChild(eggDiv);
|
||||
function createEasterGrassAndEggs(container) {
|
||||
let grassContainer = container.querySelector('.easter-grass-container');
|
||||
if (!grassContainer) {
|
||||
grassContainer = document.createElement('div');
|
||||
grassContainer.className = 'easter-grass-container';
|
||||
container.appendChild(grassContainer);
|
||||
}
|
||||
|
||||
grassContainer.innerHTML = '';
|
||||
|
||||
let pathsBg = '';
|
||||
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() {
|
||||
if (!bunny) return; // Nur ausführen, wenn Easter aktiviert ist
|
||||
let rabbitTimeout;
|
||||
let isAnimating = false;
|
||||
|
||||
const easterContainer = document.querySelector('.easter-container');
|
||||
if (!easterContainer) return;
|
||||
function addHoppingRabbit(container) {
|
||||
if (!enableBunny) return;
|
||||
|
||||
// Hase erstellen
|
||||
const rabbitImg = document.createElement("img");
|
||||
rabbitImg.id = "rabbit";
|
||||
rabbitImg.src = rabbit; // Bildpfad aus der bestehenden Definition
|
||||
rabbitImg.alt = "Hoppelnder Osterhase";
|
||||
rabbitImg.src = rabbit;
|
||||
rabbitImg.alt = "Hopping Easter Bunny";
|
||||
rabbitImg.className = "hopping-rabbit";
|
||||
|
||||
// CSS-Klassen hinzufügen
|
||||
rabbitImg.classList.add("hopping-rabbit");
|
||||
rabbitImg.style.bottom = "-15px";
|
||||
rabbitImg.style.position = "absolute";
|
||||
|
||||
easterContainer.appendChild(rabbitImg);
|
||||
|
||||
rabbitImg.style.bottom = (hopHeight / 2 + 6) + "px";
|
||||
container.appendChild(rabbitImg);
|
||||
|
||||
animateRabbit(rabbitImg);
|
||||
}
|
||||
|
||||
function animateRabbit(rabbitElement) {
|
||||
const rabbit = rabbitElement || document.querySelector('#rabbit');
|
||||
if (!rabbit) return;
|
||||
function animateRabbit(rabbit) {
|
||||
if (!rabbit || isAnimating) 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;
|
||||
|
||||
function animationStep(timestamp) {
|
||||
if (!document.querySelector('.easter-container') || rabbit.style.display === 'none') {
|
||||
isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTime) {
|
||||
startTime = timestamp;
|
||||
|
||||
// random start position and direction
|
||||
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 currSrc = rabbit.src.split('?')[0];
|
||||
rabbit.src = currSrc + '?t=' + Date.now();
|
||||
}
|
||||
const progress = timestamp - startTime;
|
||||
|
||||
// calculate the horizontal position (linear interpolation)
|
||||
const x = rabbit.startX + (progress / bunnyDuration) * (rabbit.endX - rabbit.startX);
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
// calculate the vertical position (sinus curve)
|
||||
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
|
||||
const completedLoops = Math.floor(elapsed / loopDurationMs);
|
||||
const timeInCurrentLoop = elapsed % loopDurationMs;
|
||||
|
||||
// set the new position
|
||||
rabbit.style.transform = `translate(${x}vw, ${y}px) scaleX(${rabbit.direction})`;
|
||||
|
||||
if (progress < bunnyDuration) {
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
// Determine if we are currently jumping or pausing
|
||||
let currentLoopDistance = 0;
|
||||
if (timeInCurrentLoop < jumpDurationMs) {
|
||||
// We are in the jumping phase
|
||||
currentLoopDistance = (timeInCurrentLoop / jumpDurationMs) * jumpDistanceVw;
|
||||
} else {
|
||||
// let the bunny rest for a while before hiding easter eggs again
|
||||
const pauseDuration = Math.random() * (maxBunnyRestTime - minBunnyRestTime) + minBunnyRestTime;
|
||||
setTimeout(() => {
|
||||
startTime = null;
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
}, pauseDuration);
|
||||
// We are in the paused phase
|
||||
currentLoopDistance = jumpDistanceVw;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationStep);
|
||||
// Start loop
|
||||
rabbitTimeout = requestAnimationFrame(animationStep);
|
||||
}
|
||||
|
||||
function initializeEaster() {
|
||||
if (!easter) return;
|
||||
|
||||
// initialize standard easter
|
||||
function initEaster() {
|
||||
const container = document.querySelector('.easter-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.easter-container')) {
|
||||
@@ -196,48 +267,17 @@ function initEaster() {
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// shuffle the easter images
|
||||
let currentIndex = images.length;
|
||||
let randomIndex;
|
||||
while (currentIndex != 0) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[images[currentIndex], images[randomIndex]] = [images[randomIndex], images[currentIndex]];
|
||||
}
|
||||
createEasterGrassAndEggs(container);
|
||||
addHoppingRabbit(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`;
|
||||
// Add resize listener to regenerate meadow
|
||||
window.addEventListener('resize', () => {
|
||||
if(document.querySelector('.easter-container')) {
|
||||
createEasterGrassAndEggs(container);
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomEaster && (screenWidth > 768 || randomEasterMobile)) { // add random easter only on larger screens, unless enabled for mobile devices
|
||||
addRandomEaster(easterEggCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.rocket-trail {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
left: var(--trailX);
|
||||
top: var(--trailStartY);
|
||||
top: 0;
|
||||
width: 4px;
|
||||
|
||||
/* activate the following for rocket trail */
|
||||
@@ -27,6 +29,7 @@
|
||||
box-shadow: 0 0 8px 2px white;*/
|
||||
|
||||
animation: rocket-trail-animation 1s linear forwards;
|
||||
translate: 0 var(--trailStartY);
|
||||
}
|
||||
|
||||
@keyframes rocket-trail-animation {
|
||||
@@ -55,6 +58,7 @@
|
||||
}
|
||||
|
||||
.firework {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
@@ -2,10 +2,10 @@ const config = window.SeasonalsPluginConfig?.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 particlesPerFirework = config.ParticleCount || 50; // count of particles per firework (Warning: High values may affect performance)
|
||||
const minFireworks = config.MinFireworks || 3; // minimum number of simultaneous fireworks
|
||||
const maxFireworks = config.MaxFireworks || 6; // maximum number of simultaneous fireworks
|
||||
const intervalOfFireworks = config.LaunchInterval || 3200; // interval for the fireworks in milliseconds
|
||||
const particlesPerFirework = config.ParticleCount !== undefined ? config.ParticleCount : 50; // count of particles per firework
|
||||
const minFireworks = config.MinFireworks !== undefined ? config.MinFireworks : 3; // minimum number of simultaneous fireworks
|
||||
const maxFireworks = config.MaxFireworks !== undefined ? config.MaxFireworks : 6; // maximum number of simultaneous fireworks
|
||||
const intervalOfFireworks = config.LaunchInterval !== undefined ? config.LaunchInterval : 3200; // interval for the fireworks in milliseconds
|
||||
|
||||
// array of color palettes for the fireworks
|
||||
const colorPalettes = [
|
||||
@@ -60,9 +60,9 @@ const observer = new MutationObserver(toggleFirework);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ function startFireworks() {
|
||||
}
|
||||
|
||||
fireworksInterval = setInterval(() => {
|
||||
if (!document.body.contains(fireworkContainer)) { clearInterval(fireworksInterval); return; }
|
||||
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
|
||||
for (let i = 0; i < randomCount; i++) {
|
||||
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%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.halloween {
|
||||
will-change: transform;
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
z-index: 15;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
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-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
@@ -32,37 +25,15 @@
|
||||
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 {
|
||||
0% {
|
||||
bottom: -10%
|
||||
bottom: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
bottom: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,74 +49,117 @@
|
||||
}
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
/* --- Fog Layer --- */
|
||||
.halloween-fog-layer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(to top, black, transparent);
|
||||
}
|
||||
.halloween-fog-blob {
|
||||
position: absolute;
|
||||
bottom: -10vh;
|
||||
width: 150vw;
|
||||
height: 50vh;
|
||||
background: radial-gradient(ellipse at center, rgba(120, 130, 140, 0.4) 0%, transparent 60%);
|
||||
border-radius: 50%;
|
||||
filter: blur(15px);
|
||||
}
|
||||
.halloween-fog-blob:nth-child(1) {
|
||||
will-change: transform;
|
||||
left: -20vw;
|
||||
animation: fog-float1 25s ease-in-out infinite alternate;
|
||||
}
|
||||
.halloween-fog-blob:nth-child(2) {
|
||||
will-change: transform;
|
||||
left: -50vw;
|
||||
background: radial-gradient(ellipse at center, rgba(100, 110, 120, 0.3) 0%, transparent 65%);
|
||||
animation: fog-float2 35s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes fog-float1 {
|
||||
0% { transform: translateX(0) scale(1); opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(20vw) scale(1.1); opacity: 0.6; }
|
||||
}
|
||||
@keyframes fog-float2 {
|
||||
0% { transform: translateX(0) scale(1.1); opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(30vw) scale(1); opacity: 0.5; }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
/* --- Spiders --- */
|
||||
.halloween-spider-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1002;
|
||||
transform-origin: top;
|
||||
will-change: transform;
|
||||
pointer-events: auto;
|
||||
padding: 20px; /* Increase hit area */
|
||||
translate: 0 -50px;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
.halloween-thread {
|
||||
width: 30px; /* Wider hit area for mouse interaction */
|
||||
height: 100vh;
|
||||
margin-top: -100vh;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-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) {
|
||||
.halloween-thread::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, rgba(200, 200, 200, 0.1), rgba(200, 200, 200, 0.6));
|
||||
}
|
||||
.halloween-spider {
|
||||
will-change: transform;
|
||||
animation: spider-swing 3s ease-in-out infinite alternate;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
/* MARK: SPIDER SWAY CONFIGURATION */
|
||||
@keyframes wind-sway {
|
||||
0% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(2deg); }
|
||||
75% { transform: rotate(-2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
@keyframes spider-drop {
|
||||
0% { transform: translateY(-50px); }
|
||||
30% { transform: translateY(var(--drop-height, 50vh)); }
|
||||
60% { transform: translateY(var(--drop-height, 50vh)); }
|
||||
100% { transform: translateY(-50px); }
|
||||
}
|
||||
@keyframes spider-swing {
|
||||
0% { transform: rotate(-10deg); }
|
||||
100% { transform: rotate(10deg); }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
/* Mice */
|
||||
.halloween-mouse {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
will-change: left;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
@keyframes mouse-run-right {
|
||||
0% { left: -10vw; }
|
||||
100% { left: 110vw; }
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-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
|
||||
@keyframes mouse-run-left {
|
||||
0% { left: 110vw; }
|
||||
100% { left: -10vw; }
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
const config = window.SeasonalsPluginConfig?.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 randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols
|
||||
const halloweenCount = config.SymbolCount || 25; // count of random extra symbols
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true; // enable/disable spiders
|
||||
const enableMice = config.EnableMice !== undefined ? config.EnableMice : true; // enable/disable mice
|
||||
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 toggleHalloween() {
|
||||
@@ -34,47 +41,40 @@ function toggleHalloween() {
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleHalloween);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
const images = [
|
||||
"/Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"/Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"/Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
];
|
||||
|
||||
function addRandomSymbols(count) {
|
||||
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');
|
||||
function initHalloween(count) {
|
||||
let halloweenContainer = document.querySelector('.halloween-container');
|
||||
if (!halloweenContainer) {
|
||||
halloweenContainer = document.createElement("div");
|
||||
halloweenContainer.className = "halloween-container";
|
||||
halloweenContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(halloweenContainer);
|
||||
}
|
||||
|
||||
console.log('Adding halloween symbols');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new halloween elements
|
||||
const halloweenDiv = document.createElement("div");
|
||||
halloweenDiv.className = "halloween";
|
||||
|
||||
// pick a random halloween symbol
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
|
||||
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 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
|
||||
halloweenDiv.style.left = `${randomLeft}%`;
|
||||
@@ -87,52 +87,149 @@ function addRandomSymbols(count) {
|
||||
halloweenDiv.style.animationDuration = `${randomAnimationDuration}s, ${randomAnimationDuration2}s`;
|
||||
}
|
||||
|
||||
// add the halloween to the container
|
||||
halloweenContainer.appendChild(halloweenDiv);
|
||||
}
|
||||
console.log('Random halloween symbols added');
|
||||
console.log('Halloween symbols added');
|
||||
}
|
||||
|
||||
// create halloween objects
|
||||
function createHalloween() {
|
||||
const container = document.querySelector('.halloween-container') || document.createElement("div");
|
||||
// create fog layer
|
||||
function createFog(container) {
|
||||
const fogContainer = document.createElement('div');
|
||||
fogContainer.className = 'halloween-fog-layer';
|
||||
|
||||
if (!document.querySelector('.halloween-container')) {
|
||||
container.className = "halloween-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
const fog1 = document.createElement('div');
|
||||
fog1.className = 'halloween-fog-blob';
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
images.forEach(imageSrc => {
|
||||
const halloweenDiv = document.createElement("div");
|
||||
halloweenDiv.className = "halloween";
|
||||
const fog2 = document.createElement('div');
|
||||
fog2.className = 'halloween-fog-blob';
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = imageSrc;
|
||||
fogContainer.appendChild(fog1);
|
||||
fogContainer.appendChild(fog2);
|
||||
container.appendChild(fogContainer);
|
||||
}
|
||||
|
||||
// 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`;
|
||||
}
|
||||
// create dropping spiders
|
||||
function createSpider(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'halloween-spider-wrapper';
|
||||
|
||||
halloweenDiv.appendChild(img);
|
||||
container.appendChild(halloweenDiv);
|
||||
wrapper.innerHTML = `
|
||||
<div class="halloween-sway" style="display:flex; flex-direction:column; align-items:center; transform-origin: 50% -100vh;">
|
||||
<div class="halloween-thread"></div>
|
||||
<svg class="halloween-spider" viewBox="0 0 24 24" width="30" height="30">
|
||||
<circle cx="12" cy="12" r="6" fill="#1a1a1a"/>
|
||||
<!-- left legs -->
|
||||
<path d="M12 12 l-8 -4 M12 12 l-9 0 M12 12 l-8 4 M12 12 l-6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<!-- right legs -->
|
||||
<path d="M12 12 l8 -4 M12 12 l9 0 M12 12 l8 4 M12 12 l6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<circle cx="10" cy="14" r="1.5" fill="#ff3333"/>
|
||||
<circle cx="14" cy="14" r="1.5" fill="#ff3333"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
wrapper.style.left = `${10 + Math.random() * 80}%`;
|
||||
const dropHeight = 30 + Math.random() * 50; // 30vh to 80vh
|
||||
wrapper.style.setProperty('--drop-height', `${dropHeight}vh`);
|
||||
|
||||
const duration = Math.random() * 6 + 6; // 6-12s drop
|
||||
wrapper.style.animation = `spider-drop ${duration}s ease-in-out forwards`;
|
||||
|
||||
// Start the sway animation only after the drop completes (30% of total duration)
|
||||
const sway = wrapper.querySelector('.halloween-sway');
|
||||
sway.style.animation = `wind-sway 8s ease-in-out ${duration * 0.3}s infinite`;
|
||||
|
||||
// Spider retreat logic
|
||||
let isRetreating = false;
|
||||
wrapper.addEventListener('mouseenter', () => {
|
||||
if (isRetreating) return;
|
||||
isRetreating = true;
|
||||
// Retreat smoothly by pushing margin up
|
||||
wrapper.style.transition = 'margin-top 0.4s ease-in';
|
||||
wrapper.style.marginTop = '-100vh';
|
||||
|
||||
setTimeout(() => {
|
||||
wrapper.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.addEventListener('animationend', () => {
|
||||
if (isRetreating) return;
|
||||
wrapper.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// create scurrying mice
|
||||
function createMouse(container) {
|
||||
const mouse = document.createElement('div');
|
||||
mouse.className = 'halloween-mouse';
|
||||
mouse.innerHTML = `
|
||||
<svg viewBox="0 0 30 15" width="40" height="20">
|
||||
<ellipse cx="15" cy="10" rx="10" ry="5" fill="#111"/>
|
||||
<circle cx="24" cy="10" r="4" fill="#111"/>
|
||||
<circle cx="24" cy="6" r="3" fill="#333"/>
|
||||
<path d="M 5 10 Q 0 10 0 2" stroke="#111" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
const duration = Math.random() * 3 + 2; // 2-5s run (fast)
|
||||
|
||||
if (direction === 'right') {
|
||||
mouse.style.animation = `mouse-run-right ${duration}s linear forwards`;
|
||||
mouse.style.transform = 'scaleX(1)';
|
||||
} else {
|
||||
mouse.style.animation = `mouse-run-left ${duration}s linear forwards`;
|
||||
mouse.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
mouse.style.bottom = `5px`; // Fixated bottom edge
|
||||
|
||||
mouse.addEventListener('animationend', () => {
|
||||
mouse.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createMouse(container), Math.random() * 4000 + 2000);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(mouse);
|
||||
}
|
||||
|
||||
// initialize halloween
|
||||
function initializeHalloween() {
|
||||
if (!halloween) return; // exit if halloween is disabled
|
||||
createHalloween();
|
||||
if (!halloween) return;
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? halloweenCount : halloweenCountMobile;
|
||||
|
||||
initHalloween(count);
|
||||
toggleHalloween();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
||||
addRandomSymbols(halloweenCount);
|
||||
const container = document.querySelector('.halloween-container');
|
||||
|
||||
if (container) {
|
||||
createFog(container);
|
||||
|
||||
// Add a few spiders
|
||||
if (enableSpiders) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a few mice
|
||||
if (enableMice) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => createMouse(container), Math.random() * 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,59 +8,33 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.heart {
|
||||
will-change: transform;
|
||||
position: fixed;
|
||||
bottom: -10%;
|
||||
z-index: 15;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
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-duration: 14s, 5s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
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 {
|
||||
0% {
|
||||
bottom: -10%
|
||||
bottom: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 100%
|
||||
bottom: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,75 +49,3 @@
|
||||
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 hearts = config.EnableHearts !== undefined ? config.EnableHearts : true; // enable/disable hearts
|
||||
const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols
|
||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance)
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration for random symbols
|
||||
const heartsCount = config.SymbolCount || 25; // count of random extra symbols
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const heartsCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||
const heartsCountMobile = config.SymbolCountMobile !== undefined ? config.SymbolCountMobile : 10; // count of symbol on mobile
|
||||
|
||||
// Array of hearts characters
|
||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -36,24 +38,23 @@ function toggleHearts() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleHearts);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
// Array of hearts characters
|
||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||
function initHearts(count) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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');
|
||||
console.log('Adding heart symbols');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new hearts elements
|
||||
@@ -66,8 +67,8 @@ function addRandomSymbols(count) {
|
||||
|
||||
// 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() * 14; // delay (0s to 14s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
const randomAnimationDelay = -(Math.random() * 16); // delay (-16s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
heartsDiv.style.left = `${randomLeft}%`;
|
||||
@@ -83,46 +84,18 @@ function addRandomSymbols(count) {
|
||||
// add the hearts to the container
|
||||
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
|
||||
function initializeHearts() {
|
||||
if (!hearts) return; // exit if hearts is disabled
|
||||
createHearts();
|
||||
toggleHearts();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random heartss only on larger screens, unless enabled for mobile devices
|
||||
addRandomSymbols(heartsCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? heartsCount : heartsCountMobile;
|
||||
|
||||
initHearts(count);
|
||||
toggleHearts();
|
||||
}
|
||||
|
||||
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 santaIsFlying = config.EnableSanta !== undefined ? config.EnableSanta : true; // enable/disable santa
|
||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
||||
const snowFallSpeed = config.SnowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
|
||||
const santaSpeed = config.SantaSpeed || 10; // speed of santa in seconds (recommended values: 5-15)
|
||||
const santaSpeedMobile = config.SantaSpeedMobile || 8; // speed of santa on mobile devices in seconds
|
||||
const maxSantaRestTime = config.MaxSantaRestTime || 8; // maximum time santa rests in seconds
|
||||
const minSantaRestTime = config.MinSantaRestTime || 3; // minimum time santa rests in seconds
|
||||
const maxPresentFallSpeed = config.MaxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
|
||||
const minPresentFallSpeed = config.MinPresentFallSpeed || 2; // minimum speed of falling presents in seconds
|
||||
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||
const snowFallSpeed = config.SnowFallSpeed !== undefined ? config.SnowFallSpeed : 3; // speed of snowfall
|
||||
const santaSpeed = config.SantaSpeed !== undefined ? config.SantaSpeed : 10; // speed of santa in seconds
|
||||
const santaSpeedMobile = config.SantaSpeedMobile !== undefined ? config.SantaSpeedMobile : 8; // speed of santa on mobile devices in seconds
|
||||
const maxSantaRestTime = config.MaxSantaRestTime !== undefined ? config.MaxSantaRestTime : 8; // maximum time santa rests in seconds
|
||||
const minSantaRestTime = config.MinSantaRestTime !== undefined ? config.MinSantaRestTime : 3; // minimum time santa rests in seconds
|
||||
const maxPresentFallSpeed = config.MaxPresentFallSpeed !== undefined ? config.MaxPresentFallSpeed : 5; // maximum 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 isMobile = false; // flag to detect mobile devices
|
||||
@@ -52,12 +67,10 @@ function toggleSnowfall() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowfall);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
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() {
|
||||
const santa = document.createElement('img');
|
||||
santa.src = santaImage;
|
||||
@@ -241,7 +238,7 @@ function animateSanta() {
|
||||
function startAnimation() {
|
||||
const santaHeight = santa.offsetHeight;
|
||||
if (santaHeight === 0) {
|
||||
setTimeout(startAnimation, 100);
|
||||
setTimeout(() => { if (document.body.contains(santa)) startAnimation(); }, 100);
|
||||
return;
|
||||
}
|
||||
// console.log('Santa height: ', santaHeight);
|
||||
@@ -286,7 +283,7 @@ function animateSanta() {
|
||||
animationFrameIdSanta = requestAnimationFrame(move);
|
||||
} else {
|
||||
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');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
|
||||
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||
isMobile = true;
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
|
||||
@@ -1,283 +1,281 @@
|
||||
// theme-configs.js
|
||||
/*
|
||||
* Seasonals Plugin (Client Side Manager Logic)
|
||||
*/
|
||||
|
||||
// theme configurations
|
||||
const themeConfigs = {
|
||||
const ThemeConfigs = {
|
||||
snowflakes: {
|
||||
css: '/Seasonals/Resources/snowflakes.css',
|
||||
js: '/Seasonals/Resources/snowflakes.js',
|
||||
css: '../Seasonals/Resources/snowflakes.css',
|
||||
js: '../Seasonals/Resources/snowflakes.js',
|
||||
containerClass: 'snowflakes'
|
||||
},
|
||||
snowfall: {
|
||||
css: '/Seasonals/Resources/snowfall.css',
|
||||
js: '/Seasonals/Resources/snowfall.js',
|
||||
css: '../Seasonals/Resources/snowfall.css',
|
||||
js: '../Seasonals/Resources/snowfall.js',
|
||||
containerClass: 'snowfall-container'
|
||||
},
|
||||
snowstorm: {
|
||||
css: '/Seasonals/Resources/snowstorm.css',
|
||||
js: '/Seasonals/Resources/snowstorm.js',
|
||||
css: '../Seasonals/Resources/snowstorm.css',
|
||||
js: '../Seasonals/Resources/snowstorm.js',
|
||||
containerClass: 'snowstorm-container'
|
||||
},
|
||||
fireworks: {
|
||||
css: '/Seasonals/Resources/fireworks.css',
|
||||
js: '/Seasonals/Resources/fireworks.js',
|
||||
css: '../Seasonals/Resources/fireworks.css',
|
||||
js: '../Seasonals/Resources/fireworks.js',
|
||||
containerClass: 'fireworks'
|
||||
},
|
||||
halloween: {
|
||||
css: '/Seasonals/Resources/halloween.css',
|
||||
js: '/Seasonals/Resources/halloween.js',
|
||||
css: '../Seasonals/Resources/halloween.css',
|
||||
js: '../Seasonals/Resources/halloween.js',
|
||||
containerClass: 'halloween-container'
|
||||
},
|
||||
hearts: {
|
||||
css: '/Seasonals/Resources/hearts.css',
|
||||
js: '/Seasonals/Resources/hearts.js',
|
||||
css: '../Seasonals/Resources/hearts.css',
|
||||
js: '../Seasonals/Resources/hearts.js',
|
||||
containerClass: 'hearts-container'
|
||||
},
|
||||
christmas: {
|
||||
css: '/Seasonals/Resources/christmas.css',
|
||||
js: '/Seasonals/Resources/christmas.js',
|
||||
css: '../Seasonals/Resources/christmas.css',
|
||||
js: '../Seasonals/Resources/christmas.js',
|
||||
containerClass: 'christmas-container'
|
||||
},
|
||||
santa: {
|
||||
css: '/Seasonals/Resources/santa.css',
|
||||
js: '/Seasonals/Resources/santa.js',
|
||||
css: '../Seasonals/Resources/santa.css',
|
||||
js: '../Seasonals/Resources/santa.js',
|
||||
containerClass: 'santa-container'
|
||||
},
|
||||
autumn: {
|
||||
css: '/Seasonals/Resources/autumn.css',
|
||||
js: '/Seasonals/Resources/autumn.js',
|
||||
css: '../Seasonals/Resources/autumn.css',
|
||||
js: '../Seasonals/Resources/autumn.js',
|
||||
containerClass: 'autumn-container'
|
||||
},
|
||||
easter: {
|
||||
css: '/Seasonals/Resources/easter.css',
|
||||
js: '/Seasonals/Resources/easter.js',
|
||||
css: '../Seasonals/Resources/easter.css',
|
||||
js: '../Seasonals/Resources/easter.js',
|
||||
containerClass: 'easter-container'
|
||||
},
|
||||
resurrection: {
|
||||
css: '../Seasonals/Resources/resurrection.css',
|
||||
js: '../Seasonals/Resources/resurrection.js',
|
||||
containerClass: 'resurrection-container'
|
||||
},
|
||||
summer: {
|
||||
css: '/Seasonals/Resources/summer.css',
|
||||
js: '/Seasonals/Resources/summer.js',
|
||||
css: '../Seasonals/Resources/summer.css',
|
||||
js: '../Seasonals/Resources/summer.js',
|
||||
containerClass: 'summer-container'
|
||||
},
|
||||
spring: {
|
||||
css: '/Seasonals/Resources/spring.css',
|
||||
js: '/Seasonals/Resources/spring.js',
|
||||
css: '../Seasonals/Resources/spring.css',
|
||||
js: '../Seasonals/Resources/spring.js',
|
||||
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: {
|
||||
containerClass: 'none'
|
||||
},
|
||||
};
|
||||
|
||||
// determine current theme based on the current month
|
||||
function determineCurrentTheme() {
|
||||
const date = new Date();
|
||||
const month = date.getMonth(); // 0-11
|
||||
const day = date.getDate(); // 1-31
|
||||
const SeasonalSettingsManager = {
|
||||
initialized: false,
|
||||
config: null,
|
||||
|
||||
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
|
||||
|
||||
if (month === 11 && day >= 22 && day <= 27) return 'santa'; // christmas december 22 - 27
|
||||
// if (month === 11 && day >= 22 && day <= 27) return 'christmas'; // christmas december 22 - 27
|
||||
|
||||
if (month === 11) return 'snowflakes'; // snowflakes december
|
||||
if (month === 0 || month === 1) return 'snowfall'; // snow january, february
|
||||
// if (month === 0 || month === 1) return 'snowstorm'; // snow january, february
|
||||
|
||||
if ((month === 2 && day >= 25) || (month === 3 && day <= 25)) return 'easter'; // easter march 25 - april 25
|
||||
|
||||
//NOT IMPLEMENTED YET
|
||||
//if (month >= 2 && month <= 4) return 'spring'; // spring march, april, may
|
||||
|
||||
//NOT IMPLEMENTED YET
|
||||
//if (month >= 5 && month <= 7) return 'summer'; // summer june, july, august
|
||||
|
||||
if ((month === 9 && day >= 24) || (month === 10 && day <= 5)) return 'halloween'; // halloween october 24 - november 5
|
||||
|
||||
if (month >= 8 && month <= 10) return 'autumn'; // autumn september, october, november
|
||||
|
||||
return 'none'; // Fallback (nothing)
|
||||
}
|
||||
|
||||
// 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
|
||||
function loadThemeCSS(cssPath) {
|
||||
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');
|
||||
// Only inject settings if enabled on server by admin
|
||||
if (this.config && this.config.EnableClientSideToggle !== false) {
|
||||
this.injectSettingsIcon();
|
||||
this.initialized = true;
|
||||
console.log("Seasonals: Client-Side Settings Manager initialized.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching Seasonals config:', error);
|
||||
}
|
||||
},
|
||||
|
||||
let currentTheme;
|
||||
getSetting(key, defaultValue) {
|
||||
const value = localStorage.getItem(`seasonals-${key}`);
|
||||
return value !== null ? value : defaultValue;
|
||||
},
|
||||
|
||||
if (forcedTheme !== 'auto') {
|
||||
currentTheme = forcedTheme;
|
||||
console.log(`User forced theme: ${currentTheme}`);
|
||||
} else if (automateThemeSelection === false) {
|
||||
currentTheme = defaultTheme;
|
||||
} else {
|
||||
currentTheme = determineCurrentTheme();
|
||||
}
|
||||
setSetting(key, value) {
|
||||
localStorage.setItem(`seasonals-${key}`, value);
|
||||
},
|
||||
|
||||
console.log(`Selected theme: ${currentTheme}`);
|
||||
createIcon() {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
|
||||
button.title = 'Seasonal Settings';
|
||||
// button.innerHTML = '<span class="material-icons">ac_unit</span>';
|
||||
button.innerHTML = '<img src="../Seasonals/Resources/assets/logo_SW.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
||||
button.style.verticalAlign = 'middle';
|
||||
|
||||
if (!currentTheme || currentTheme === 'none') {
|
||||
console.log('No theme selected.');
|
||||
return;
|
||||
}
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleSettingsPopup(button);
|
||||
});
|
||||
|
||||
const theme = themeConfigs[currentTheme];
|
||||
return button;
|
||||
},
|
||||
|
||||
if (!theme) {
|
||||
console.error(`Theme "${currentTheme}" not found.`);
|
||||
return;
|
||||
}
|
||||
updateThemeContainer(theme.containerClass);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
if (theme.css) loadThemeCSS(theme.css);
|
||||
if (theme.js) loadThemeJS(theme.js);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
},
|
||||
|
||||
console.log(`Theme "${currentTheme}" loaded.`);
|
||||
}
|
||||
createPopup(anchorElement) {
|
||||
const existing = document.querySelector('.seasonal-settings-popup');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'seasonal-settings-popup dialog';
|
||||
|
||||
initializeTheme();
|
||||
Object.assign(popup.style, {
|
||||
position: 'fixed',
|
||||
zIndex: '10000',
|
||||
backgroundColor: '#202020',
|
||||
padding: '1em',
|
||||
borderRadius: '0.3em',
|
||||
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
|
||||
minWidth: '200px',
|
||||
color: '#fff',
|
||||
maxWidth: '250px'
|
||||
});
|
||||
|
||||
const rect = anchorElement.getBoundingClientRect();
|
||||
|
||||
// User UI Seasonal Settings
|
||||
// Positioning logic
|
||||
let rightPos = window.innerWidth - rect.right;
|
||||
if (window.innerWidth < 450 || (window.innerWidth - rightPos) < 260) {
|
||||
popup.style.right = '1rem';
|
||||
popup.style.left = 'auto';
|
||||
} else {
|
||||
popup.style.right = `${rightPos}px`;
|
||||
popup.style.left = 'auto';
|
||||
}
|
||||
popup.style.top = `${rect.bottom + 10}px`;
|
||||
|
||||
function getSavedSetting(key, defaultValue) {
|
||||
const value = localStorage.getItem(key);
|
||||
return value !== null ? value : defaultValue;
|
||||
}
|
||||
// Popup HTML
|
||||
let html = `
|
||||
<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
|
||||
|
||||
function setSavedSetting(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function createSettingsIcon() {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
|
||||
button.title = 'Seasonal Settings';
|
||||
button.innerHTML = '<span class="material-icons">ac_unit</span>';
|
||||
button.style.verticalAlign = 'middle';
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleSettingsPopup(button);
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
function createSettingsPopup(anchorElement) {
|
||||
const existing = document.querySelector('.seasonal-settings-popup');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'seasonal-settings-popup dialog';
|
||||
|
||||
Object.assign(popup.style, {
|
||||
position: 'fixed',
|
||||
zIndex: '10000',
|
||||
backgroundColor: '#202020',
|
||||
padding: '1em',
|
||||
borderRadius: '0.3em',
|
||||
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
|
||||
minWidth: '200px',
|
||||
color: '#fff',
|
||||
maxWidth: '250px'
|
||||
});
|
||||
|
||||
const rect = anchorElement.getBoundingClientRect();
|
||||
popup.style.top = `${rect.bottom + 10}px`;
|
||||
popup.style.right = `${window.innerWidth - rect.right}px`;
|
||||
|
||||
popup.innerHTML = `
|
||||
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
|
||||
@@ -285,80 +283,231 @@ function createSettingsPopup(anchorElement) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<div class="selectContainer" style="margin-bottom: 0.5em;">
|
||||
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
|
||||
<select is="emby-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>
|
||||
<select id="seasonal-theme-select" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
|
||||
<option value="auto">Server-Side</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
// Populate Select Options
|
||||
const themeSelect = popup.querySelector('#seasonal-theme-select');
|
||||
Object.keys(themeConfigs).forEach(key => {
|
||||
if (key === 'none') return;
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
// Capitalize first letter
|
||||
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
themeSelect.appendChild(option);
|
||||
});
|
||||
popup.innerHTML = html;
|
||||
|
||||
// Set Initial Values
|
||||
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
|
||||
enabledCheckbox.checked = getSavedSetting('seasonals-enabled', 'true') === 'true';
|
||||
// Populate Select Options
|
||||
const themeSelect = popup.querySelector('#seasonal-theme-select');
|
||||
Object.keys(ThemeConfigs).forEach(key => {
|
||||
if (key === 'none') return;
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
themeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
themeSelect.value = getSavedSetting('seasonals-theme', 'auto');
|
||||
// Set Initial Values
|
||||
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
|
||||
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
|
||||
themeSelect.value = this.getSetting('theme', 'auto');
|
||||
|
||||
enabledCheckbox.addEventListener('change', (e) => {
|
||||
setSavedSetting('seasonals-enabled', e.target.checked);
|
||||
location.reload();
|
||||
});
|
||||
// Event Listeners
|
||||
enabledCheckbox.addEventListener('change', (e) => {
|
||||
this.setSetting('enabled', e.target.checked);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
themeSelect.addEventListener('change', (e) => {
|
||||
setSavedSetting('seasonals-theme', e.target.value);
|
||||
location.reload();
|
||||
});
|
||||
themeSelect.addEventListener('change', (e) => {
|
||||
this.setSetting('theme', e.target.value);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
const closeHandler = (e) => {
|
||||
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener('click', closeHandler);
|
||||
// Close on outside click
|
||||
const closeHandler = (e) => {
|
||||
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener('click', closeHandler);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
},
|
||||
|
||||
toggleSettingsPopup(anchorElement) {
|
||||
const existing = document.querySelector('.seasonal-settings-popup');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
} else {
|
||||
this.createPopup(anchorElement);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
}
|
||||
|
||||
function toggleSettingsPopup(anchorElement) {
|
||||
const existing = document.querySelector('.seasonal-settings-popup');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
} else {
|
||||
createSettingsPopup(anchorElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function injectSettingsIcon() {
|
||||
const observer = new MutationObserver((mutations, obs) => {
|
||||
// Check if admin has enabled this feature
|
||||
if (window.SeasonalsPluginConfig && window.SeasonalsPluginConfig.EnableClientSideToggle === false) {
|
||||
const SeasonalsManager = {
|
||||
config: null,
|
||||
|
||||
async init() {
|
||||
// Fetch Config
|
||||
try {
|
||||
const response = await fetch('../Seasonals/Config');
|
||||
if (response.ok) {
|
||||
this.config = await response.json();
|
||||
window.SeasonalsPluginConfig = this.config;
|
||||
|
||||
if (this.config.IsEnabled === false) {
|
||||
console.log('Seasonals: Plugin is disabled globally.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Seasonals: Seasonals Config loaded:', this.config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Seasonals: Error fetching Seasonals config:', error);
|
||||
}
|
||||
|
||||
// Initialize Settings UI
|
||||
SeasonalSettingsManager.init(this.config);
|
||||
|
||||
// User Preference Check
|
||||
const isEnabled = SeasonalSettingsManager.getSetting('enabled', 'true') === 'true';
|
||||
if (!isEnabled) {
|
||||
console.log('Seasonals: Disabled by user preference.');
|
||||
return;
|
||||
}
|
||||
|
||||
const headerRight = document.querySelector('.headerRight');
|
||||
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
|
||||
const icon = createSettingsIcon();
|
||||
headerRight.prepend(icon);
|
||||
// obs.disconnect();
|
||||
// Determine Theme
|
||||
const themeName = this.selectTheme();
|
||||
console.log(`Seasonals: Selected theme: ${themeName}`);
|
||||
|
||||
if (!themeName || themeName === 'none') {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
// Apply Theme
|
||||
this.applyTheme(themeName);
|
||||
},
|
||||
|
||||
injectSettingsIcon();
|
||||
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;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const config = window.SeasonalsPluginConfig?.Snowfall || {};
|
||||
|
||||
const snowfall = config.EnableSnowfall !== undefined ? config.EnableSnowfall : true; // enable/disable snowfall
|
||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
||||
const snowFallSpeed = config.Speed || 3; // speed of snowfall (recommended values: 0-5)
|
||||
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 3; // speed of snowfall
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -47,12 +47,10 @@ function toggleSnowfall() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowfall);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
@@ -183,8 +181,8 @@ function initializeSnowfall() {
|
||||
}
|
||||
const container = document.querySelector('.snowfall-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // check if mobile device
|
||||
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
snowflakesCount = snowflakesCountMobile;
|
||||
}
|
||||
|
||||
@@ -8,60 +8,36 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -10%;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
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: 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-duration: 12s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
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 {
|
||||
0% {
|
||||
top: -10%;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
translate: 0 110vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,63 +52,3 @@
|
||||
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 snowflakes = config.EnableSnowflakes !== undefined ? config.EnableSnowflakes : true; // enable/disable snowflakes
|
||||
const randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random Snowflakes
|
||||
const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random Snowflakes on mobile devices
|
||||
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable colored snowflakes
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
||||
const snowflakeCount = config.SnowflakeCount || 25; // count of random extra snowflakes
|
||||
const snowflakeCount = config.SnowflakeCount !== undefined ? config.SnowflakeCount : 25; // count of snowflakes
|
||||
const snowflakeCountMobile = config.SnowflakeCountMobile !== undefined ? config.SnowflakeCountMobile : 10; // count of snowflakes on mobile
|
||||
const enableColoredSnowflakes = config.EnableColoredSnowflakes !== undefined ? config.EnableColoredSnowflakes : true; // enable/disable colored snowflakes
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
|
||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -38,22 +39,22 @@ function toggleSnowflakes() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowflakes);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function addRandomSnowflakes(count) {
|
||||
const snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
||||
if (!snowflakeContainer) return; // exit if snowflake container is not found
|
||||
function initSnowflakes(count) {
|
||||
let snowflakeContainer = document.querySelector('.snowflakes'); // get the snowflake container
|
||||
if (!snowflakeContainer) {
|
||||
snowflakeContainer = document.createElement("div");
|
||||
snowflakeContainer.className = "snowflakes";
|
||||
snowflakeContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(snowflakeContainer);
|
||||
}
|
||||
|
||||
console.log('Adding random snowflakes');
|
||||
|
||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||
console.log('Adding snowflakes');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new snowflake element
|
||||
@@ -69,8 +70,8 @@ function addRandomSnowflakes(count) {
|
||||
|
||||
// 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() * 8; // delay (0s to 8s)
|
||||
const randomAnimationDelay2 = Math.random() * 5; // delay (0s to 5s)
|
||||
const randomAnimationDelay = -(Math.random() * 14); // delay (-14s to 0s)
|
||||
const randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
snowflake.style.left = `${randomLeft}%`;
|
||||
@@ -86,49 +87,18 @@ function addRandomSnowflakes(count) {
|
||||
// add the snowflake to the container
|
||||
snowflakeContainer.appendChild(snowflake);
|
||||
}
|
||||
console.log('Random snowflakes added');
|
||||
console.log('Snowflakes added');
|
||||
}
|
||||
|
||||
// initialize standard 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
|
||||
// initialize snowflakes
|
||||
function initializeSnowflakes() {
|
||||
if (!snowflakes) return; // exit if snowflakes are disabled
|
||||
initSnowflakes();
|
||||
toggleSnowflakes();
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSnowflakes && (screenWidth > 768 || randomSnowflakesMobile)) { // add random snowflakes only on larger screens, unless enabled for mobile devices
|
||||
addRandomSnowflakes(snowflakeCount);
|
||||
}
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
const count = !isMobile ? snowflakeCount : snowflakeCountMobile;
|
||||
|
||||
initSnowflakes(count);
|
||||
toggleSnowflakes();
|
||||
}
|
||||
|
||||
initializeSnowflakes();
|
||||
@@ -8,6 +8,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
#snowfallCanvas {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const config = window.SeasonalsPluginConfig?.Snowstorm || {};
|
||||
|
||||
const snowstorm = config.enableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
|
||||
let snowflakesCount = config.SnowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
|
||||
const snowFallSpeed = config.Speed || 6; // speed of snowfall (recommended values: 4-8)
|
||||
const horizontalWind = config.HorizontalWind || 4; // horizontal wind speed (recommended value: 4)
|
||||
const verticalVariation = config.VerticalVariation || 2; // vertical variation (recommended value: 2)
|
||||
const snowstorm = config.EnableSnowstorm !== undefined ? config.EnableSnowstorm : true; // enable/disable snowstorm
|
||||
let snowflakesCount = config.SnowflakesCount !== undefined ? config.SnowflakesCount : 500; // count of snowflakes
|
||||
const snowflakesCountMobile = config.SnowflakesCountMobile !== undefined ? config.SnowflakesCountMobile : 250; // count of snowflakes on mobile
|
||||
const snowFallSpeed = config.Speed !== undefined ? config.Speed : 6; // speed of snowstorm
|
||||
const horizontalWind = config.HorizontalWind !== undefined ? config.HorizontalWind : 4; // horizontal wind strength
|
||||
const verticalVariation = config.VerticalVariation !== undefined ? config.VerticalVariation : 2; // vertical variation
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -49,12 +49,10 @@ function toggleSnowstorm() {
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleSnowstorm);
|
||||
|
||||
// start observation
|
||||
observer.observe(document.body, {
|
||||
childList: true, // observe adding/removing of child elements
|
||||
subtree: true, // observe all levels of the DOM tree
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
@@ -186,8 +184,8 @@ function initializeSnowstorm() {
|
||||
}
|
||||
const container = document.querySelector('.snowstorm-container');
|
||||
if (container) {
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
if (isMobile) { // lower count of snowflakes on mobile devices
|
||||
console.log('Mobile device detected. Reducing snowflakes count.');
|
||||
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 |