Compare commits
261 Commits
v1.6.8.0
...
9d1a268875
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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,23 +74,52 @@ 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 class AutumnOptions {
|
||||
public int LeafCount { get; set; } = 25;
|
||||
public bool EnableAutumn { get; set; } = true;
|
||||
public bool EnableRandomLeaves { get; set; } = true;
|
||||
@@ -74,95 +128,266 @@ public class AutumnOptions
|
||||
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 EnableRandomCarnival { get; set; } = true;
|
||||
public bool EnableRandomCarnivalMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableCarnivalSway { get; set; } = true;
|
||||
public int ObjectCount { get; set; } = 120;
|
||||
}
|
||||
|
||||
public class CherryBlossomOptions {
|
||||
public bool EnableCherryBlossom { get; set; } = true;
|
||||
public int PetalCount { get; set; } = 25;
|
||||
public bool EnableRandomCherryBlossom { get; set; } = true;
|
||||
public bool EnableRandomCherryBlossomMobile { get; set; } = false;
|
||||
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 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 FireworksOptions
|
||||
{
|
||||
public int ParticleCount { get; set; } = 50;
|
||||
public int LaunchInterval { get; set; } = 3200;
|
||||
public bool EnableFireworks { get; set; } = true;
|
||||
public bool ScrollFireworks { get; set; } = true;
|
||||
public int MinFireworks { get; set; } = 3;
|
||||
public int MaxFireworks { get; set; } = 6;
|
||||
}
|
||||
|
||||
public class HalloweenOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
public bool EnableHalloween { get; set; } = true;
|
||||
public bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class HeartsOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
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 class ChristmasOptions {
|
||||
public bool EnableChristmas { get; set; } = true;
|
||||
public bool EnableRandomChristmas { get; set; } = true;
|
||||
public bool EnableRandomChristmasMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 25;
|
||||
}
|
||||
|
||||
public class SantaOptions
|
||||
{
|
||||
public class EarthDayOptions {
|
||||
public bool EnableEarthDay { get; set; } = true;
|
||||
public int VineCount { get; set; } = 4;
|
||||
}
|
||||
|
||||
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 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 bool EnableDifferentDuration { get; set; } = true;
|
||||
public bool EnableSpiders { get; set; } = true;
|
||||
public bool EnableMice { get; set; } = true;
|
||||
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 int SymbolCount { get; set; } = 25;
|
||||
}
|
||||
|
||||
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 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 int 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 bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
public int SymbolCount { get; set; } = 12;
|
||||
}
|
||||
|
||||
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 bool EnableRandomSnowflakes { get; set; } = true;
|
||||
public bool EnableRandomSnowflakesMobile { get; set; } = false;
|
||||
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.8.0</Version>
|
||||
<Version>2.0.0.1</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 |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_96x96.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -1,161 +1,139 @@
|
||||
.autumn-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
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: 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;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
/* Class to disable rotation */
|
||||
.no-rotation {
|
||||
--rotate-start: 0deg !important;
|
||||
--rotate-end: 0deg !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaf-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@-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% {
|
||||
transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
||||
}
|
||||
50% {
|
||||
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;
|
||||
.autumn-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
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;
|
||||
animation-name: leaf-fall, leaf-shake;
|
||||
animation-duration: 7s, 4s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
/* Class to disable rotation */
|
||||
.no-rotation {
|
||||
--rotate-start: 0deg !important;
|
||||
--rotate-end: 0deg !important;
|
||||
}
|
||||
|
||||
|
||||
@keyframes leaf-fall {
|
||||
0% {
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
translate: 0 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes leaf-shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(var(--rotate-start, -20deg));
|
||||
}
|
||||
50% {
|
||||
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;
|
||||
@@ -1,12 +1,31 @@
|
||||
const config = window.SeasonalsPluginConfig?.Autumn || {};
|
||||
|
||||
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable leaves
|
||||
const leaves = config.EnableAutumn !== undefined ? config.EnableAutumn : true; // enable/disable autumn
|
||||
const randomLeaves = config.EnableRandomLeaves !== undefined ? config.EnableRandomLeaves : true; // enable random leaves
|
||||
const 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 randomLeavesMobile = config.EnableRandomLeavesMobile !== undefined ? config.EnableRandomLeavesMobile : false; // enable random leaves on mobile
|
||||
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 : 25; // count of random extra leaves
|
||||
|
||||
const images = [
|
||||
"../Seasonals/Resources/autumn_images/acorn1.png",
|
||||
"../Seasonals/Resources/autumn_images/acorn2.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf1.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf2.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf3.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf4.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf5.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf6.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf7.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf8.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf9.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf10.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf11.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf12.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf13.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf14.png",
|
||||
"../Seasonals/Resources/autumn_images/leaf15.png",
|
||||
];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -38,35 +57,13 @@ 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 addRandomLeaves(count) {
|
||||
const autumnContainer = document.querySelector('.autumn-container'); // get the leave container
|
||||
if (!autumnContainer) return; // exit if leave container is not found
|
||||
@@ -90,7 +87,7 @@ function addRandomLeaves(count) {
|
||||
// set random horizontal position, animation delay and size(uncomment lines to enable)
|
||||
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)
|
||||
const randomAnimationDelay2 = -(Math.random() * 4); // delay for shake+rotate (-4s to 0s)
|
||||
|
||||
// apply styles
|
||||
leaveDiv.style.left = `${randomLeft}%`;
|
||||
|
||||
155
Jellyfin.Plugin.Seasonals/Web/birthday.css
Normal file
@@ -0,0 +1,155 @@
|
||||
.birthday-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.birthday-symbol {
|
||||
will-change: opacity;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
animation: birthday-rise linear infinite forwards;
|
||||
opacity: 0.95;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.birthday-sway {
|
||||
will-change: transform;
|
||||
animation-name: birthday-sway;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
.birthday-inner {
|
||||
pointer-events: auto;
|
||||
cursor: crosshair;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* MARK: Balloon Size */
|
||||
.birthday-symbol img {
|
||||
width: 18vh;
|
||||
height: auto;
|
||||
max-width: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.birthday-confetti-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
will-change: transform;
|
||||
animation-name: birthday-confetti-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.birthday-confetti {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
will-change: transform;
|
||||
animation-name: birthday-flutter;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.birthday-confetti.circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.birthday-confetti.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.birthday-confetti.triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes birthday-rise {
|
||||
0% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0) rotate(var(--start-rot, 0deg)); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translate3d(var(--x-pos, 0vw), -20vh, 0) rotate(calc(var(--start-rot, 0deg) * -1)); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes birthday-confetti-fall {
|
||||
0% { transform: translate3d(var(--x-pos, 0vw), -10vh, 0); }
|
||||
100% { transform: translate3d(var(--x-pos, 0vw), 110vh, 0); }
|
||||
}
|
||||
|
||||
@keyframes birthday-sway {
|
||||
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
|
||||
100% { transform: translateX(var(--sway-amount, 50px)); }
|
||||
}
|
||||
|
||||
@keyframes birthday-flutter {
|
||||
0% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg); }
|
||||
100% { transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg)); }
|
||||
}
|
||||
|
||||
@keyframes birthday-pop {
|
||||
0% { transform: scale(1); opacity: 1; filter: brightness(1); }
|
||||
30% { transform: scale(1.3); opacity: 1; filter: brightness(1.5); }
|
||||
100% { transform: scale(0); opacity: 0; filter: brightness(2); }
|
||||
}
|
||||
|
||||
.birthday-burst-wrapper {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
will-change: transform, opacity;
|
||||
animation: birthday-burst-y 1.2s cubic-bezier(0.42, 0, 1, 1) forwards;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti {
|
||||
will-change: transform;
|
||||
animation: birthday-burst-x 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.birthday-burst-confetti.triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes birthday-burst-y {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(calc(var(--burst-y) + 150px));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes birthday-burst-x {
|
||||
0% {
|
||||
transform: translateX(0) rotate3d(var(--rx), var(--ry), var(--rz), 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(var(--burst-x) * 1.5)) rotate3d(var(--rx), var(--ry), var(--rz), var(--rot-dir));
|
||||
}
|
||||
}
|
||||
322
Jellyfin.Plugin.Seasonals/Web/birthday.js
Normal file
@@ -0,0 +1,322 @@
|
||||
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
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
190
Jellyfin.Plugin.Seasonals/Web/carnival.js
Normal file
@@ -0,0 +1,190 @@
|
||||
const config = window.SeasonalsPluginConfig?.Carnival || {};
|
||||
|
||||
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true; // enable/disable carnival
|
||||
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true; // enable random carnival
|
||||
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false; // enable random carnival 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 carnivalCount = config.ObjectCount !== undefined ? config.ObjectCount : 120; // Number of confetti pieces to spawn
|
||||
|
||||
const confettiColors = [
|
||||
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||
];
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// function to check and control the carnival animation
|
||||
function toggleCarnival() {
|
||||
const carnivalContainer = document.querySelector('.carnival-container');
|
||||
if (!carnivalContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide carnival if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
carnivalContainer.style.display = 'none'; // hide carnival
|
||||
if (!msgPrinted) {
|
||||
console.log('Carnival hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
carnivalContainer.style.display = 'block'; // show carnival
|
||||
if (msgPrinted) {
|
||||
console.log('Carnival visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleCarnival);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createConfettiPiece(container, isInitial = false) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('carnival-wrapper');
|
||||
|
||||
let swayWrapper = wrapper;
|
||||
|
||||
if (enableSway) {
|
||||
swayWrapper = document.createElement('div');
|
||||
swayWrapper.classList.add('carnival-sway-wrapper');
|
||||
wrapper.appendChild(swayWrapper);
|
||||
}
|
||||
|
||||
const confetti = document.createElement('div');
|
||||
confetti.classList.add('carnival-confetti');
|
||||
|
||||
// Random color
|
||||
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
|
||||
confetti.style.backgroundColor = color;
|
||||
|
||||
// Random shape
|
||||
const shape = Math.random();
|
||||
if (shape > 0.8) {
|
||||
confetti.classList.add('circle');
|
||||
} else if (shape > 0.6) {
|
||||
confetti.classList.add('square');
|
||||
} else if (shape > 0.4) {
|
||||
confetti.classList.add('triangle');
|
||||
} else {
|
||||
confetti.classList.add('rect');
|
||||
}
|
||||
|
||||
// Random position
|
||||
wrapper.style.left = `${Math.random() * 100}%`;
|
||||
|
||||
// MARK: CONFETTI SIZE (RECTANGLES)
|
||||
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
|
||||
const width = Math.random() * 3 + 4; // 4-7px
|
||||
const height = Math.random() * 5 + 8; // 8-13px
|
||||
confetti.style.width = `${width}px`;
|
||||
confetti.style.height = `${height}px`;
|
||||
} else if (confetti.classList.contains('circle') || confetti.classList.contains('square')) {
|
||||
// MARK: CONFETTI SIZE (CIRCLES/SQUARES)
|
||||
const size = Math.random() * 5 + 5; // 5-10px
|
||||
confetti.style.width = `${size}px`;
|
||||
confetti.style.height = `${size}px`;
|
||||
}
|
||||
|
||||
// MARK: CONFETTI FALLING SPEED (in seconds)
|
||||
const duration = Math.random() * 5 + 5;
|
||||
|
||||
let delay = 0;
|
||||
if (isInitial) {
|
||||
delay = -Math.random() * duration;
|
||||
} else {
|
||||
delay = Math.random() * 10;
|
||||
}
|
||||
|
||||
wrapper.style.animationDelay = `${delay}s`;
|
||||
wrapper.style.animationDuration = `${duration}s`;
|
||||
|
||||
if (enableSway) {
|
||||
// Random sway duration
|
||||
const swayDuration = Math.random() * 2 + 3; // 3-5s per cycle
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
swayWrapper.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
|
||||
// MARK: SWAY DISTANCE RANGE (in px)
|
||||
const swayAmount = Math.random() * 70 + 30; // 30-100px
|
||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||
}
|
||||
|
||||
// MARK: CONFETTI FLUTTER ROTATION SPEED
|
||||
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
|
||||
confetti.style.setProperty('--rx', Math.random().toFixed(2));
|
||||
confetti.style.setProperty('--ry', Math.random().toFixed(2));
|
||||
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||
|
||||
// Random direction for 3D rotation
|
||||
const rotDir = Math.random() > 0.5 ? 1 : -1;
|
||||
confetti.style.setProperty('--rot-dir', `${rotDir * 360}deg`);
|
||||
|
||||
if (enableSway) {
|
||||
swayWrapper.appendChild(confetti);
|
||||
wrapper.appendChild(swayWrapper);
|
||||
} else {
|
||||
wrapper.appendChild(confetti);
|
||||
}
|
||||
|
||||
// Respawn confetti when it hits the bottom
|
||||
wrapper.addEventListener('animationend', (e) => {
|
||||
if (e.animationName === 'carnival-fall') {
|
||||
wrapper.remove();
|
||||
createConfettiPiece(container, false); // respawn without initial huge delay
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
function addRandomCarnivalObjects(count) {
|
||||
const carnivalContainer = document.querySelector('.carnival-container');
|
||||
if (!carnivalContainer) return;
|
||||
|
||||
console.log('Adding random carnival confetti');
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
createConfettiPiece(carnivalContainer, true);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize standard carnival objects
|
||||
function initCarnivalObjects() {
|
||||
let container = document.querySelector('.carnival-container');
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "carnival-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Initial confetti
|
||||
for (let i = 0; i < 60; i++) {
|
||||
createConfettiPiece(container, true);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize carnival
|
||||
function initializeCarnival() {
|
||||
if (!carnival) return;
|
||||
initCarnivalObjects();
|
||||
toggleCarnival();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
|
||||
addRandomCarnivalObjects(carnivalCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeCarnival();
|
||||
60
Jellyfin.Plugin.Seasonals/Web/cherryblossom.css
Normal file
@@ -0,0 +1,60 @@
|
||||
.cherryblossom-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
/* Petals */
|
||||
.cherryblossom-petal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1005;
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
background-color: #ffc0cb;
|
||||
border-radius: 15px 0px 15px 0px;
|
||||
|
||||
will-change: transform;
|
||||
translate: 0 -10vh;
|
||||
animation-name: cherryblossom-fall, cherryblossom-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-duration: 10s, 3s;
|
||||
}
|
||||
|
||||
.cherryblossom-petal.lighter {
|
||||
background-color: #ffd1dc;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cherryblossom-petal.darker {
|
||||
background-color: #ffb7c5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cherryblossom-petal.type2 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 10px 0px 10px 5px;
|
||||
}
|
||||
|
||||
@keyframes cherryblossom-fall {
|
||||
0% { translate: 0 -10vh; }
|
||||
100% { translate: 0 110vh; }
|
||||
}
|
||||
|
||||
@keyframes cherryblossom-sway {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(30px) rotate(45deg);
|
||||
}
|
||||
}
|
||||
105
Jellyfin.Plugin.Seasonals/Web/cherryblossom.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const config = window.SeasonalsPluginConfig?.CherryBlossom || {};
|
||||
|
||||
const cherryBlossom = config.EnableCherryBlossom !== undefined ? config.EnableCherryBlossom : true; // enable/disable cherryblossom
|
||||
const petalCount = config.PetalCount !== undefined ? config.PetalCount : 25; // count of petal
|
||||
const randomCherryBlossom = config.EnableRandomCherryBlossom !== undefined ? config.EnableRandomCherryBlossom : true; // enable random cherryblossom
|
||||
const randomCherryBlossomMobile = config.EnableRandomCherryBlossomMobile !== undefined ? config.EnableRandomCherryBlossomMobile : false; // enable random cherryblossom 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 addRandomObjects() {
|
||||
const container = document.querySelector('.cherryblossom-container');
|
||||
if (!container) return;
|
||||
|
||||
for (let i = 0; i < petalCount; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initObjects() {
|
||||
let container = document.querySelector('.cherryblossom-container');
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "cherryblossom-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Initial batch
|
||||
for (let i = 0; i < 15; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCherryBlossom() {
|
||||
if (!cherryBlossom) return;
|
||||
initObjects();
|
||||
toggleCherryBlossom();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomCherryBlossom && (screenWidth > 768 || randomCherryBlossomMobile)) {
|
||||
addRandomObjects();
|
||||
}
|
||||
}
|
||||
|
||||
initializeCherryBlossom();
|
||||
@@ -1,138 +1,112 @@
|
||||
.christmas-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.christmas {
|
||||
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;
|
||||
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%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
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;
|
||||
.christmas-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.christmas {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
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;
|
||||
animation-name: christmas-fall, christmas-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes christmas-fall {
|
||||
0% {
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
translate: 0 110vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes christmas-shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
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,13 @@
|
||||
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 randomChristmas = config.EnableRandomChristmas !== undefined ? config.EnableRandomChristmas : true; // enable random christmas
|
||||
const randomChristmasMobile = config.EnableRandomChristmasMobile !== undefined ? config.EnableRandomChristmasMobile : false; // enable random christmas on mobile
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const christmasCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||
|
||||
// Array of christmas characters
|
||||
const christmasSymbols = ['❆', '🎁', '❄️', '🎁', '🎅', '🎊', '🎁', '🎉'];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -37,17 +39,12 @@ 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 addRandomChristmas(count) {
|
||||
const christmasContainer = document.querySelector('.christmas-container'); // get the christmas container
|
||||
if (!christmasContainer) return; // exit if christmas container is not found
|
||||
|
||||
38
Jellyfin.Plugin.Seasonals/Web/earthday.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.earthday-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 8vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.earthday-meadow {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: bottom;
|
||||
animation: grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes grow-meadow {
|
||||
0% { transform: translateY(100%); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 0.95; }
|
||||
}
|
||||
|
||||
.earthday-sway {
|
||||
will-change: transform;
|
||||
transform-origin: bottom center;
|
||||
animation: sway-grass 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes sway-grass {
|
||||
0% { transform: skewX(-2deg); }
|
||||
100% { transform: skewX(2deg); }
|
||||
}
|
||||
127
Jellyfin.Plugin.Seasonals/Web/earthday.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const config = window.SeasonalsPluginConfig?.EarthDay || {};
|
||||
|
||||
const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true; // 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";
|
||||
|
||||
rabbitImg.style.bottom = "-15px";
|
||||
rabbitImg.style.position = "absolute";
|
||||
|
||||
// CSS-Klassen hinzufügen
|
||||
rabbitImg.classList.add("hopping-rabbit");
|
||||
|
||||
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;
|
||||
|
||||
const completedLoops = Math.floor(elapsed / loopDurationMs);
|
||||
const timeInCurrentLoop = elapsed % loopDurationMs;
|
||||
|
||||
// calculate the vertical position (sinus curve)
|
||||
const y = Math.sin((progress / 500) * Math.PI) * hopHeight; // 500ms for one hop
|
||||
|
||||
// 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]];
|
||||
}
|
||||
|
||||
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`;
|
||||
createEasterGrassAndEggs(container);
|
||||
addHoppingRabbit(container);
|
||||
|
||||
// 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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,72 +51,175 @@
|
||||
|
||||
.halloween:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
animation-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
animation-delay: -1s, -1s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
animation-delay: -2s, -2s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-webkit-animation-delay: 4s, 2s;
|
||||
animation-delay: 4s, 2s
|
||||
animation-delay: -3s, -3s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(4) {
|
||||
left: 40%;
|
||||
-webkit-animation-delay: 2s, 2s;
|
||||
animation-delay: 2s, 2s
|
||||
animation-delay: -4s, -4s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(5) {
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
animation-delay: -5s, -5s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
animation-delay: -6s, -6s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
animation-delay: -7s, -7s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
animation-delay: -8s, -8s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
animation-delay: -9s, -9s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 2s, 0s;
|
||||
animation-delay: 2s, 0s
|
||||
animation-delay: -10s, -10s;
|
||||
}
|
||||
|
||||
.halloween:nth-of-type(11) {
|
||||
left: 65%;
|
||||
-webkit-animation-delay: 4s, 2.5s;
|
||||
animation-delay: 4s, 2.5s
|
||||
animation-delay: -11s, -11s;
|
||||
}
|
||||
|
||||
/* --- Fog Layer --- */
|
||||
.halloween-fog-layer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(to top, black, transparent);
|
||||
}
|
||||
.halloween-fog-blob {
|
||||
position: absolute;
|
||||
bottom: -10vh;
|
||||
width: 150vw;
|
||||
height: 50vh;
|
||||
background: radial-gradient(ellipse at center, rgba(120, 130, 140, 0.4) 0%, transparent 60%);
|
||||
border-radius: 50%;
|
||||
filter: blur(15px);
|
||||
}
|
||||
.halloween-fog-blob:nth-child(1) {
|
||||
will-change: transform;
|
||||
left: -20vw;
|
||||
animation: fog-float1 25s ease-in-out infinite alternate;
|
||||
}
|
||||
.halloween-fog-blob:nth-child(2) {
|
||||
will-change: transform;
|
||||
left: -50vw;
|
||||
background: radial-gradient(ellipse at center, rgba(100, 110, 120, 0.3) 0%, transparent 65%);
|
||||
animation: fog-float2 35s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes fog-float1 {
|
||||
0% { transform: translateX(0) scale(1); opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(20vw) scale(1.1); opacity: 0.6; }
|
||||
}
|
||||
@keyframes fog-float2 {
|
||||
0% { transform: translateX(0) scale(1.1); opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
100% { transform: translateX(30vw) scale(1); opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* --- Spiders --- */
|
||||
.halloween-spider-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1002;
|
||||
transform-origin: top;
|
||||
will-change: transform;
|
||||
pointer-events: auto;
|
||||
padding: 20px; /* Increase hit area */
|
||||
translate: 0 -50px;
|
||||
}
|
||||
|
||||
.halloween-thread {
|
||||
width: 30px; /* Wider hit area for mouse interaction */
|
||||
height: 100vh;
|
||||
margin-top: -100vh;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.halloween-thread::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, rgba(200, 200, 200, 0.1), rgba(200, 200, 200, 0.6));
|
||||
}
|
||||
.halloween-spider {
|
||||
will-change: transform;
|
||||
animation: spider-swing 3s ease-in-out infinite alternate;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
/* MARK: SPIDER SWAY CONFIGURATION */
|
||||
@keyframes wind-sway {
|
||||
0% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(2deg); }
|
||||
75% { transform: rotate(-2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes spider-drop {
|
||||
0% { transform: translateY(-50px); }
|
||||
30% { transform: translateY(var(--drop-height, 50vh)); }
|
||||
60% { transform: translateY(var(--drop-height, 50vh)); }
|
||||
100% { transform: translateY(-50px); }
|
||||
}
|
||||
@keyframes spider-swing {
|
||||
0% { transform: rotate(-10deg); }
|
||||
100% { transform: rotate(10deg); }
|
||||
}
|
||||
|
||||
/* Mice */
|
||||
.halloween-mouse {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
will-change: left;
|
||||
}
|
||||
@keyframes mouse-run-right {
|
||||
0% { left: -10vw; }
|
||||
100% { left: 110vw; }
|
||||
}
|
||||
@keyframes mouse-run-left {
|
||||
0% { left: 110vw; }
|
||||
100% { left: -10vw; }
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
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 randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable random symbols
|
||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile
|
||||
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
|
||||
|
||||
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 +42,34 @@ 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
|
||||
const halloweenContainer = document.querySelector('.halloween-container');
|
||||
if (!halloweenContainer) return;
|
||||
|
||||
console.log('Adding random 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)
|
||||
const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s)
|
||||
|
||||
// apply styles
|
||||
halloweenDiv.style.left = `${randomLeft}%`;
|
||||
@@ -87,13 +82,11 @@ 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');
|
||||
}
|
||||
|
||||
// create halloween objects
|
||||
function createHalloween() {
|
||||
const container = document.querySelector('.halloween-container') || document.createElement("div");
|
||||
|
||||
@@ -124,14 +117,144 @@ function createHalloween() {
|
||||
}
|
||||
}
|
||||
|
||||
// initialize halloween
|
||||
// create fog layer
|
||||
function createFog(container) {
|
||||
const fogContainer = document.createElement('div');
|
||||
fogContainer.className = 'halloween-fog-layer';
|
||||
|
||||
const fog1 = document.createElement('div');
|
||||
fog1.className = 'halloween-fog-blob';
|
||||
|
||||
const fog2 = document.createElement('div');
|
||||
fog2.className = 'halloween-fog-blob';
|
||||
|
||||
fogContainer.appendChild(fog1);
|
||||
fogContainer.appendChild(fog2);
|
||||
container.appendChild(fogContainer);
|
||||
}
|
||||
|
||||
// create dropping spiders
|
||||
function createSpider(container) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'halloween-spider-wrapper';
|
||||
|
||||
wrapper.innerHTML = `
|
||||
<div class="halloween-sway" style="display:flex; flex-direction:column; align-items:center; transform-origin: 50% -100vh;">
|
||||
<div class="halloween-thread"></div>
|
||||
<svg class="halloween-spider" viewBox="0 0 24 24" width="30" height="30">
|
||||
<circle cx="12" cy="12" r="6" fill="#1a1a1a"/>
|
||||
<!-- left legs -->
|
||||
<path d="M12 12 l-8 -4 M12 12 l-9 0 M12 12 l-8 4 M12 12 l-6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<!-- right legs -->
|
||||
<path d="M12 12 l8 -4 M12 12 l9 0 M12 12 l8 4 M12 12 l6 8" stroke="#1a1a1a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<circle cx="10" cy="14" r="1.5" fill="#ff3333"/>
|
||||
<circle cx="14" cy="14" r="1.5" fill="#ff3333"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
wrapper.style.left = `${10 + Math.random() * 80}%`;
|
||||
const dropHeight = 30 + Math.random() * 50; // 30vh to 80vh
|
||||
wrapper.style.setProperty('--drop-height', `${dropHeight}vh`);
|
||||
|
||||
const duration = Math.random() * 6 + 6; // 6-12s drop
|
||||
wrapper.style.animation = `spider-drop ${duration}s ease-in-out forwards`;
|
||||
|
||||
// Start the sway animation only after the drop completes (30% of total duration)
|
||||
const sway = wrapper.querySelector('.halloween-sway');
|
||||
sway.style.animation = `wind-sway 8s ease-in-out ${duration * 0.3}s infinite`;
|
||||
|
||||
// Spider retreat logic
|
||||
let isRetreating = false;
|
||||
wrapper.addEventListener('mouseenter', () => {
|
||||
if (isRetreating) return;
|
||||
isRetreating = true;
|
||||
// Retreat smoothly by pushing margin up
|
||||
wrapper.style.transition = 'margin-top 0.4s ease-in';
|
||||
wrapper.style.marginTop = '-100vh';
|
||||
|
||||
setTimeout(() => {
|
||||
wrapper.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
wrapper.addEventListener('animationend', () => {
|
||||
if (isRetreating) return;
|
||||
wrapper.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000 + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// create scurrying mice
|
||||
function createMouse(container) {
|
||||
const mouse = document.createElement('div');
|
||||
mouse.className = 'halloween-mouse';
|
||||
mouse.innerHTML = `
|
||||
<svg viewBox="0 0 30 15" width="40" height="20">
|
||||
<ellipse cx="15" cy="10" rx="10" ry="5" fill="#111"/>
|
||||
<circle cx="24" cy="10" r="4" fill="#111"/>
|
||||
<circle cx="24" cy="6" r="3" fill="#333"/>
|
||||
<path d="M 5 10 Q 0 10 0 2" stroke="#111" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
const duration = Math.random() * 3 + 2; // 2-5s run (fast)
|
||||
|
||||
if (direction === 'right') {
|
||||
mouse.style.animation = `mouse-run-right ${duration}s linear forwards`;
|
||||
mouse.style.transform = 'scaleX(1)';
|
||||
} else {
|
||||
mouse.style.animation = `mouse-run-left ${duration}s linear forwards`;
|
||||
mouse.style.transform = 'scaleX(-1)';
|
||||
}
|
||||
|
||||
mouse.style.bottom = `5px`; // Fixated bottom edge
|
||||
|
||||
mouse.addEventListener('animationend', () => {
|
||||
mouse.remove();
|
||||
if (document.body.contains(container)) {
|
||||
setTimeout(() => createMouse(container), Math.random() * 4000 + 2000);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(mouse);
|
||||
}
|
||||
|
||||
function initializeHalloween() {
|
||||
if (!halloween) return; // exit if halloween is disabled
|
||||
if (!halloween) return;
|
||||
createHalloween();
|
||||
toggleHalloween();
|
||||
|
||||
const container = document.querySelector('.halloween-container');
|
||||
|
||||
if (container) {
|
||||
createFog(container);
|
||||
|
||||
// Add a few spiders
|
||||
if (enableSpiders) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => createSpider(container), Math.random() * 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a few mice
|
||||
if (enableMice) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => createMouse(container), Math.random() * 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices
|
||||
if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) {
|
||||
addRandomSymbols(halloweenCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,72 +52,60 @@
|
||||
|
||||
.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,13 @@
|
||||
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 randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable random symbols
|
||||
const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile
|
||||
const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const heartsCount = config.SymbolCount !== undefined ? config.SymbolCount : 25; // count of symbol
|
||||
|
||||
// Array of hearts characters
|
||||
const heartSymbols = ['❤️', '💕', '💞', '💓', '💗', '💖'];
|
||||
|
||||
let msgPrinted = false; // flag to prevent multiple console messages
|
||||
|
||||
@@ -36,19 +39,13 @@ 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 addRandomSymbols(count) {
|
||||
const heartsContainer = document.querySelector('.hearts-container'); // get the hearts container
|
||||
if (!heartsContainer) return; // exit if hearts container is not found
|
||||
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/mario-running.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/mario.gif
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/mario_assets/toad.gif
Normal file
|
After Width: | Height: | Size: 40 KiB |
80
Jellyfin.Plugin.Seasonals/Web/marioday.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.marioday-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mario-wrapper {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: -100px;
|
||||
animation: mario-run 15s linear infinite;
|
||||
will-change: left, transform;
|
||||
}
|
||||
|
||||
.mario-runner {
|
||||
width: 64px;
|
||||
height: auto;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
display: block;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.mario-jump {
|
||||
will-change: transform;
|
||||
animation: jump-arc 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
/* 8-bit coin styling */
|
||||
.mario-coin {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #ffd700;
|
||||
border: 4px solid #b8860b;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 4px 4px 0 #fffbea, inset -4px -4px 0 #daa520;
|
||||
animation: pop-up-arc 2s forwards;
|
||||
}
|
||||
|
||||
.mario-coin::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10px;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
background: #daa520;
|
||||
translate: 0 6px;
|
||||
}
|
||||
|
||||
@keyframes mario-run {
|
||||
0% { left: -100px; transform: scaleX(1); }
|
||||
45% { left: 110vw; transform: scaleX(1); }
|
||||
50% { left: 110vw; transform: scaleX(-1); }
|
||||
95% { left: -100px; transform: scaleX(-1); }
|
||||
100% { left: -100px; transform: scaleX(1); }
|
||||
}
|
||||
|
||||
@keyframes pop-up-arc {
|
||||
0% { transform: translateY(0) rotateY(0deg); opacity: 0; animation-timing-function: ease-out; }
|
||||
20% { opacity: 1; }
|
||||
50% { transform: translateY(-30vh) rotateY(360deg); opacity: 1; animation-timing-function: ease-in; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(20vh) rotateY(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes jump-arc {
|
||||
0% { transform: translateY(0); }
|
||||
50% { transform: translateY(-25vh); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
91
Jellyfin.Plugin.Seasonals/Web/marioday.js
Normal file
@@ -0,0 +1,91 @@
|
||||
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);
|
||||
|
||||
// Periodically throw out an 8-bit coin
|
||||
const intervalId = setInterval(() => {
|
||||
if (!document.body.contains(container)) { clearInterval(intervalId); return; }
|
||||
if (container.style.display === 'none') return;
|
||||
const coin = document.createElement('div');
|
||||
coin.className = 'mario-coin';
|
||||
|
||||
// Grab Mario's current screen position to lock the coin's X coordinate
|
||||
const marioRect = wrapper.getBoundingClientRect();
|
||||
coin.style.left = `${marioRect.left + 16}px`;
|
||||
coin.style.bottom = '35px'; // bottom offset
|
||||
|
||||
container.appendChild(coin);
|
||||
setTimeout(() => coin.remove(), 2000);
|
||||
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
|
||||
function initializeMarioDay() {
|
||||
if (!marioday) return;
|
||||
|
||||
const container = document.querySelector('.marioday-container') || document.createElement("div");
|
||||
|
||||
if (!document.querySelector('.marioday-container')) {
|
||||
container.className = "marioday-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
createMarioDay(container);
|
||||
toggleMarioDay();
|
||||
}
|
||||
|
||||
initializeMarioDay();
|
||||
11
Jellyfin.Plugin.Seasonals/Web/matrix.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.matrix-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
contain: layout paint;
|
||||
}
|
||||
165
Jellyfin.Plugin.Seasonals/Web/matrix.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const config = window.SeasonalsPluginConfig?.Matrix || {};
|
||||
|
||||
const enabled = config.EnableMatrix !== undefined ? config.EnableMatrix : true; // 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; }
|
||||
}
|
||||
66
Jellyfin.Plugin.Seasonals/Web/oktoberfest.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const config = window.SeasonalsPluginConfig?.Oktoberfest || {};
|
||||
const oktoberfest = config.EnableOktoberfest !== undefined ? config.EnableOktoberfest : true; // enable/disable oktoberfest
|
||||
|
||||
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) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'oktoberfest-symbol';
|
||||
symbol.textContent = oktoberfestSymbols[Math.floor(Math.random() * oktoberfestSymbols.length)];
|
||||
symbol.style.left = `${Math.random() * 100}%`;
|
||||
symbol.style.animationDelay = `${Math.random() * 10}s, ${Math.random() * 5}s`;
|
||||
const duration1 = Math.random() * 5 + 8;
|
||||
const duration2 = Math.random() * 3 + 3;
|
||||
symbol.style.animationDuration = `${duration1}s, ${duration2}s`;
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeOktoberfest() {
|
||||
if (!oktoberfest) return;
|
||||
const container = document.querySelector('.oktoberfest-container') || document.createElement("div");
|
||||
if (!document.querySelector('.oktoberfest-container')) {
|
||||
container.className = "oktoberfest-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
createOktoberfest(container);
|
||||
}
|
||||
initializeOktoberfest();
|
||||
142
Jellyfin.Plugin.Seasonals/Web/olympia.css
Normal file
@@ -0,0 +1,142 @@
|
||||
.olympia-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.olympia-symbol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.2);
|
||||
z-index: 40;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
.olympia-flame {
|
||||
position: absolute;
|
||||
bottom: 0vh;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.olympia-ring-css {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.olympia-ring-css::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
translate: -50% -50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 5px solid #0081C8; /* Default blue ring */
|
||||
border-radius: 50%;
|
||||
}
|
||||
.olympia-ring-css[style*="--ring-color"]::before {
|
||||
border-color: var(--ring-color);
|
||||
}
|
||||
.olympia-symbol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.2);
|
||||
z-index: 40;
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
.olympia-inner {
|
||||
will-change: transform;
|
||||
display: inline-block;
|
||||
animation: olympia-sway linear infinite alternate;
|
||||
}
|
||||
|
||||
.olympia-symbol img {
|
||||
width: 6vh;
|
||||
height: auto;
|
||||
max-width: 60px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.olympia-confetti-wrapper {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
animation-name: olympia-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.olympia-confetti-sway {
|
||||
will-change: transform;
|
||||
animation-name: olympia-confetti-sway;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
@keyframes olympia-confetti-sway {
|
||||
0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); }
|
||||
100% { transform: translateX(var(--sway-amount, 50px)); }
|
||||
}
|
||||
|
||||
.olympia-confetti {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
will-change: transform;
|
||||
animation-name: olympia-confetti-flutter;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.olympia-confetti.circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.olympia-confetti.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.olympia-confetti.triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes olympia-fall {
|
||||
0% { transform: translateY(-10vh); }
|
||||
100% { transform: translateY(110vh); }
|
||||
}
|
||||
|
||||
@keyframes olympia-sway {
|
||||
0% { transform: rotate(-25deg) translateX(-20px); }
|
||||
100% { transform: rotate(25deg) translateX(20px); }
|
||||
}
|
||||
|
||||
@keyframes olympia-tumble-3d {
|
||||
0% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 0deg); }
|
||||
100% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 360deg); }
|
||||
}
|
||||
|
||||
@keyframes olympia-confetti-flutter {
|
||||
0% {
|
||||
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg));
|
||||
}
|
||||
}
|
||||
252
Jellyfin.Plugin.Seasonals/Web/olympia.js
Normal file
@@ -0,0 +1,252 @@
|
||||
const config = window.SeasonalsPluginConfig?.Olympia || {};
|
||||
|
||||
const olympia = config.EnableOlympia !== undefined ? config.EnableOlympia : true;
|
||||
const symbolCount = config.SymbolCount || 25;
|
||||
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleOlympia() {
|
||||
const container = document.querySelector('.olympia-container');
|
||||
if (!container) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
container.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('Olympia hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Olympia visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleOlympia);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createOlympia() {
|
||||
const container = document.querySelector('.olympia-container') || document.createElement('div');
|
||||
|
||||
if (!document.querySelector('.olympia-container')) {
|
||||
container.className = 'olympia-container';
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const standardCount = 15;
|
||||
const totalSymbols = symbolCount + standardCount;
|
||||
|
||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
||||
let finalCount = totalSymbols;
|
||||
|
||||
if (isMobile) {
|
||||
finalCount = enableRandomMobile ? totalSymbols : standardCount;
|
||||
}
|
||||
|
||||
const useRandomDuration = enableDifferentDuration !== false;
|
||||
|
||||
const activeItems = ['gold', 'silver', 'bronze', 'torch', 'rings_blue', 'rings_yellow', 'rings_black', 'rings_green', 'rings_red'];
|
||||
|
||||
for (let i = 0; i < finalCount; i++) {
|
||||
let symbol = document.createElement('div');
|
||||
|
||||
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
|
||||
const isRing = randomItem.startsWith('rings');
|
||||
const isMedal = ['gold', 'silver', 'bronze'].includes(randomItem);
|
||||
|
||||
symbol.className = `olympia-symbol olympia-${randomItem}`;
|
||||
|
||||
// Create inner div for sway/rotation
|
||||
let innerDiv = document.createElement('div');
|
||||
innerDiv.className = 'olympia-inner';
|
||||
let img = null;
|
||||
|
||||
if (isRing) {
|
||||
const ringColorMap = {
|
||||
'rings_blue': '#0081C8',
|
||||
'rings_yellow': '#FCB131',
|
||||
'rings_black': '#000000',
|
||||
'rings_green': '#00A651',
|
||||
'rings_red': '#EE334E'
|
||||
};
|
||||
let ringDiv = document.createElement('div');
|
||||
ringDiv.className = 'olympia-ring-css';
|
||||
ringDiv.style.setProperty('--ring-color', ringColorMap[randomItem]);
|
||||
innerDiv.appendChild(ringDiv);
|
||||
|
||||
// Add a 3D flip animation for rings and medals
|
||||
const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal';
|
||||
innerDiv.style.animation = `olympia-tumble-3d ${Math.random() * 4 + 4}s linear infinite ${spinReverse}`;
|
||||
|
||||
// Random 3D Rotation Axis for Tumbling
|
||||
innerDiv.style.setProperty('--rot-x', (Math.random() * 2 - 1).toFixed(2));
|
||||
innerDiv.style.setProperty('--rot-y', (Math.random() * 2 - 1).toFixed(2));
|
||||
innerDiv.style.setProperty('--rot-z', (Math.random() * 2 - 1).toFixed(2));
|
||||
} else {
|
||||
img = document.createElement('img');
|
||||
let imgName = randomItem;
|
||||
if (isMedal) {
|
||||
imgName = `${randomItem}_coin.gif`;
|
||||
} else {
|
||||
imgName = `${randomItem}.png`;
|
||||
}
|
||||
img.src = `../Seasonals/Resources/olympic_assets/${imgName}`;
|
||||
img.onerror = function() {
|
||||
symbol.remove();
|
||||
};
|
||||
innerDiv.appendChild(img);
|
||||
|
||||
if (isMedal) {
|
||||
innerDiv.style.animation = `olympia-flip-3d ${Math.random() * 4 + 3}s linear infinite`;
|
||||
} else {
|
||||
// Torch sways, medals flip
|
||||
const swayDur = Math.random() * 2 + 2; // 2 to 4s
|
||||
const swayDir = Math.random() > 0.5 ? 'normal' : 'reverse';
|
||||
innerDiv.style.animation = `olympia-sway ${swayDur}s ease-in-out infinite alternate ${swayDir}`;
|
||||
}
|
||||
}
|
||||
|
||||
symbol.appendChild(innerDiv);
|
||||
|
||||
const leftPos = Math.random() * 95;
|
||||
const delaySeconds = Math.random() * 10;
|
||||
|
||||
// Depth logic for medals and rings
|
||||
const depth = Math.random();
|
||||
const scale = 0.8 + depth * 0.4; // 0.8 to 1.2
|
||||
const zIndex = Math.floor(depth * 30) + 10;
|
||||
|
||||
if (img) {
|
||||
img.style.transform = `scale(${scale})`;
|
||||
} else {
|
||||
innerDiv.firstChild.style.transform = `scale(${scale})`;
|
||||
}
|
||||
symbol.style.zIndex = zIndex;
|
||||
|
||||
let durationSeconds = 8;
|
||||
if (useRandomDuration) {
|
||||
durationSeconds = (1 - depth) * 5 + 6 + Math.random() * 4;
|
||||
}
|
||||
|
||||
symbol.style.animation = `olympia-fall ${durationSeconds}s linear infinite`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
symbol.style.left = `${leftPos}vw`;
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
|
||||
// Olympic Torches (Fixed at bottom corners, symmetrically rotated inward)
|
||||
// Generate one random inward rotation (10 to 25 deg) for both to share
|
||||
const sharedTilt = Math.random() * 15 + 10;
|
||||
|
||||
const createTorch = (isLeft) => {
|
||||
const torch = document.createElement('div');
|
||||
torch.className = 'olympia-flame';
|
||||
|
||||
if (isLeft) {
|
||||
torch.style.left = '5vw';
|
||||
// Lean right, face normal
|
||||
torch.style.transform = `rotate(${sharedTilt}deg) scaleX(1)`;
|
||||
} else {
|
||||
torch.style.right = '5vw';
|
||||
// Lean left, mirror image
|
||||
torch.style.transform = `rotate(-${sharedTilt}deg) scaleX(-1)`;
|
||||
}
|
||||
|
||||
let torchImg = document.createElement('img');
|
||||
torchImg.src = `../Seasonals/Resources/olympic_assets/torch.gif`;
|
||||
torchImg.style.height = '25vh';
|
||||
torchImg.style.objectFit = 'contain';
|
||||
torchImg.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
};
|
||||
torch.appendChild(torchImg);
|
||||
container.appendChild(torch);
|
||||
};
|
||||
|
||||
createTorch(true);
|
||||
createTorch(false);
|
||||
|
||||
// Olympic Ring Colors (Carnival Config)
|
||||
const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E'];
|
||||
const confettiCount = isMobile ? 30 : 60;
|
||||
|
||||
for (let i = 0; i < confettiCount; i++) {
|
||||
let wrapper = document.createElement('div');
|
||||
wrapper.className = 'olympia-confetti-wrapper';
|
||||
|
||||
let leftPos = Math.random() * 100;
|
||||
wrapper.style.left = `${leftPos}vw`;
|
||||
|
||||
let fallDuration = Math.random() * 3 + 4; // 4 to 7 seconds to fall
|
||||
wrapper.style.animationDuration = `${fallDuration}s`;
|
||||
wrapper.style.animationDelay = `-${Math.random() * fallDuration}s`; // Negative delay so it distributes perfectly immediately
|
||||
|
||||
let swayWrapper = document.createElement('div');
|
||||
swayWrapper.className = 'olympia-confetti-sway';
|
||||
let swayDuration = Math.random() * 2 + 1.5; // 1.5s to 3.5s
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
let swayAmount = Math.random() * 30 + 30; // 30px to 60px
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount}px`);
|
||||
let initSwayDelay = Math.random() * swayDuration;
|
||||
swayWrapper.style.animationDelay = `-${initSwayDelay}s`;
|
||||
|
||||
let confetti = document.createElement('div');
|
||||
confetti.className = 'olympia-confetti';
|
||||
|
||||
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
|
||||
confetti.style.backgroundColor = color;
|
||||
|
||||
// Random shape
|
||||
const shape = Math.random();
|
||||
if (shape > 0.66) {
|
||||
confetti.classList.add('circle');
|
||||
const size = Math.random() * 5 + 5;
|
||||
confetti.style.width = `${size}px`;
|
||||
confetti.style.height = `${size}px`;
|
||||
} else if (shape > 0.33) {
|
||||
confetti.classList.add('rect');
|
||||
const width = Math.random() * 4 + 4;
|
||||
const height = Math.random() * 5 + 8;
|
||||
confetti.style.width = `${width}px`;
|
||||
confetti.style.height = `${height}px`;
|
||||
} else {
|
||||
confetti.classList.add('triangle');
|
||||
}
|
||||
|
||||
// Random 3D Rotation for flutter
|
||||
confetti.style.setProperty('--rx', Math.random().toFixed(2));
|
||||
confetti.style.setProperty('--ry', Math.random().toFixed(2));
|
||||
confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2));
|
||||
confetti.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`);
|
||||
let rotateDuration = Math.random() * 0.8 + 0.4;
|
||||
confetti.style.animationDuration = `${rotateDuration}s`;
|
||||
|
||||
swayWrapper.appendChild(confetti);
|
||||
wrapper.appendChild(swayWrapper);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeOlympia() {
|
||||
if (!olympia) return;
|
||||
createOlympia();
|
||||
toggleOlympia();
|
||||
}
|
||||
|
||||
initializeOlympia();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/bronze_coin.gif
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/gold_coin.gif
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/olympic_assets/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.innerWidth <= 768;
|
||||
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 enableRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable random symbols
|
||||
const enableRandomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different durations
|
||||
const symbolCount = config.SymbolCount !== undefined ? config.SymbolCount : 12; // count of symbols
|
||||
|
||||
let animationEnabled = true;
|
||||
let statusLogged = false;
|
||||
|
||||
const images = [
|
||||
'../Seasonals/Resources/resurrection_images/crosses.png',
|
||||
'../Seasonals/Resources/resurrection_images/palm-branch.png',
|
||||
'../Seasonals/Resources/resurrection_images/draped-cross.png',
|
||||
'../Seasonals/Resources/resurrection_images/empty-tomb.png',
|
||||
'../Seasonals/Resources/resurrection_images/he-is-risen.png',
|
||||
'../Seasonals/Resources/resurrection_images/crown-of-thorns.png',
|
||||
'../Seasonals/Resources/resurrection_images/risen-lord.png',
|
||||
'../Seasonals/Resources/resurrection_images/dove.png'
|
||||
];
|
||||
|
||||
function toggleResurrection() {
|
||||
const container = document.querySelector('.resurrection-container');
|
||||
if (!container) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
animationEnabled = !(videoPlayer || trailerPlayer || isDashboard || hasUserMenu);
|
||||
container.style.display = animationEnabled ? 'block' : 'none';
|
||||
|
||||
if (!animationEnabled && !statusLogged) {
|
||||
console.log('Resurrection hidden');
|
||||
statusLogged = true;
|
||||
} else if (animationEnabled && statusLogged) {
|
||||
console.log('Resurrection visible');
|
||||
statusLogged = false;
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleResurrection);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createSymbol(imageSrc, leftPercent, delaySeconds) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'resurrection-symbol';
|
||||
|
||||
const swayWrapper = document.createElement('div');
|
||||
swayWrapper.className = 'resurrection-sway-wrapper';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = imageSrc;
|
||||
img.alt = '';
|
||||
|
||||
symbol.style.left = `${leftPercent}%`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
const fallDuration = Math.random() * 7 + 7;
|
||||
const swayDuration = Math.random() * 4 + 2;
|
||||
symbol.style.animationDuration = `${fallDuration}s`;
|
||||
swayWrapper.style.animationDuration = `${swayDuration}s`;
|
||||
}
|
||||
|
||||
swayWrapper.style.animationDelay = `${Math.random() * 3}s`;
|
||||
|
||||
swayWrapper.appendChild(img);
|
||||
symbol.appendChild(swayWrapper);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function addSymbols(count) {
|
||||
const container = document.querySelector('.resurrection-container');
|
||||
if (!container || !enableRandomSymbols) return;
|
||||
|
||||
const isDesktop = window.innerWidth > 768;
|
||||
if (!isDesktop && !enableRandomSymbolsMobile) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const left = Math.random() * 100;
|
||||
const delay = Math.random() * 12;
|
||||
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||
}
|
||||
}
|
||||
|
||||
function initResurrection() {
|
||||
let container = document.querySelector('.resurrection-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'resurrection-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Place one of each of the 8 provided resurrection images first.
|
||||
images.forEach((imageSrc, index) => {
|
||||
const left = (index + 1) * (100 / (images.length + 1));
|
||||
const delay = Math.random() * 8;
|
||||
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||
});
|
||||
|
||||
const extraCount = Math.max(symbolCount - images.length, 0);
|
||||
addSymbols(extraCount);
|
||||
}
|
||||
|
||||
function initializeResurrection() {
|
||||
if (!enableResurrection) return;
|
||||
initResurrection();
|
||||
toggleResurrection();
|
||||
}
|
||||
|
||||
initializeResurrection();
|
||||
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/crosses.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 317 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/dove.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 382 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/empty-tomb.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 384 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/resurrection_images/risen-lord.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,293 +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();
|
||||
|
||||
// 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`;
|
||||
|
||||
// User UI Seasonal Settings
|
||||
|
||||
function getSavedSetting(key, defaultValue) {
|
||||
const value = localStorage.getItem(key);
|
||||
return value !== null ? value : defaultValue;
|
||||
}
|
||||
|
||||
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.innerHTML = '<img src="/Seasonals/Resources/logo_SW_24x24.png" style="width: 24px; height: 24px; vertical-align: middle;">';
|
||||
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();
|
||||
|
||||
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`;
|
||||
|
||||
popup.innerHTML = `
|
||||
// Popup HTML
|
||||
let html = `
|
||||
<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Seasonal Settings</h3>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 0.5em;">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
|
||||
@@ -298,77 +286,228 @@ function createSettingsPopup(anchorElement) {
|
||||
<div class="selectContainer" style="margin-bottom: 0.5em;">
|
||||
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
|
||||
<select id="seasonal-theme-select" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
|
||||
<option value="auto">Auto (Date Based)</option>
|
||||
<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
|
||||
|
||||
@@ -1,138 +1,112 @@
|
||||
.snowflakes {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
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: 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%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
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;
|
||||
.snowflakes {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
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;
|
||||
animation-name: snowflakes-fall, snowflakes-shake;
|
||||
animation-duration: 12s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes snowflakes-fall {
|
||||
0% {
|
||||
translate: 0 -10vh;
|
||||
}
|
||||
|
||||
100% {
|
||||
translate: 0 110vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
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%;
|
||||
@@ -1,12 +1,14 @@
|
||||
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 randomSnowflakes = config.EnableRandomSnowflakes !== undefined ? config.EnableRandomSnowflakes : true; // enable random snowflakes
|
||||
const randomSnowflakesMobile = config.EnableRandomSnowflakesMobile !== undefined ? config.EnableRandomSnowflakesMobile : false; // enable random 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
|
||||
|
||||
@@ -19,7 +21,7 @@ function toggleSnowflakes() {
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
|
||||
// hide snowflakes if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
snowflakeContainer.style.display = 'none'; // hide snowflakes
|
||||
@@ -38,12 +40,10 @@ 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) {
|
||||
@@ -52,9 +52,6 @@ function addRandomSnowflakes(count) {
|
||||
|
||||
console.log('Adding random snowflakes');
|
||||
|
||||
const snowflakeSymbols = ['❅', '❆']; // some snowflake symbols
|
||||
const snowflakeSymbolsMobile = ['❅', '❆', '❄']; // some snowflake symbols mobile version
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// create a new snowflake element
|
||||
const snowflake = document.createElement('div');
|
||||
@@ -70,7 +67,7 @@ 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 randomAnimationDelay2 = -(Math.random() * 5); // delay (-5s to 0s)
|
||||
|
||||
// apply styles
|
||||
snowflake.style.left = `${randomLeft}%`;
|
||||
|
||||
@@ -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
|
||||
|
||||
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();
|
||||