Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56298487f4 | ||
|
|
89fc1c38f0 | ||
|
|
4c168a5ec2 | ||
|
|
92d9e1a9ad | ||
|
|
007e55a612 | ||
|
|
20da9899e4 | ||
|
|
9b9cad1caa | ||
|
|
e8e3424cc9 | ||
|
|
0eeed99508 | ||
|
|
a0f261f597 | ||
|
|
35d92862aa | ||
|
|
693bb35aac | ||
|
|
1ddaab325e | ||
|
|
81facbdb00 | ||
|
|
34a58ac4bd | ||
|
|
2d8444701d | ||
|
|
66f5353659 | ||
|
|
b58264998a | ||
|
|
76c0bc5b3b | ||
|
|
1428db3e1e | ||
|
|
1f5f436e44 | ||
|
|
46f5c3648d | ||
|
|
555e2ab8be | ||
|
|
26eadfc0aa | ||
|
|
142f538939 | ||
|
|
b64e80fd60 | ||
|
|
fbf5fc7edf | ||
|
|
8defba4623 | ||
|
|
7f968ee050 | ||
|
|
dec5bbe39e | ||
|
|
63f3211cc4 | ||
|
|
4270235c78 | ||
|
|
76d8a67914 | ||
|
|
1a3caf5da6 | ||
|
|
3b3ef77e61 | ||
|
|
ba580b1b52 | ||
|
|
0a6284c716 | ||
|
|
f83e863664 | ||
|
|
747e8ed6bc | ||
|
|
30845442b2 | ||
|
|
bb83201736 | ||
|
|
457ae404ba | ||
|
|
b6d679f6ef | ||
|
|
3b88a1809d | ||
|
|
4614ce4a7a | ||
|
|
57840bb149 | ||
|
|
dd90a4630a | ||
|
|
b5d5e5706e | ||
|
|
a4b5cf5b6b | ||
|
|
353bda10df | ||
|
|
0e1b91d93c | ||
|
|
9363008d07 | ||
|
|
faec7d8941 | ||
|
|
7cc70854c4 | ||
|
|
9432f7aa86 | ||
|
|
4f7243bc74 | ||
|
|
ee724fedc8 | ||
|
|
a1dbd4eb12 | ||
|
|
236d8d9e70 | ||
|
|
6d55ae7524 | ||
|
|
99a0613893 | ||
|
|
61952a0af7 | ||
|
|
eca6ba96fb | ||
|
|
c2f0f01689 | ||
|
|
30d17baff4 | ||
|
|
96bb1a3744 | ||
|
|
772a0dae40 | ||
|
|
40c4454397 | ||
|
|
e5915e715a | ||
|
|
c171fc15f5 | ||
|
|
a749b1f98e | ||
|
|
6ccf6201b4 | ||
|
|
a69c741a39 | ||
|
|
d54b4f9b07 | ||
|
|
2cd427b6e9 | ||
|
|
55c1f8b191 | ||
|
|
fc3d6efd1c | ||
|
|
5ba5940e5f | ||
|
|
621b7da344 | ||
|
|
268ce5e307 | ||
|
|
412cc2d981 | ||
|
|
949df24bdb | ||
|
|
b987969200 | ||
|
|
3306bb703d | ||
|
|
6587a4e3d0 | ||
|
|
f794b71f44 | ||
|
|
34363c502a | ||
|
|
add2f7a551 | ||
|
|
1d7e9e27ec | ||
|
|
6459653328 | ||
|
|
9d738e6061 | ||
|
|
8f5a3650e6 | ||
|
|
229f9fe5ab | ||
|
|
0686129590 | ||
|
|
cb0392eb0d | ||
|
|
ed13e05b82 | ||
|
|
310fb4d496 | ||
|
|
78d25106db | ||
|
|
a328171a8a | ||
|
|
361559cbec | ||
|
|
e08bf66a53 | ||
|
|
d6ef81138d | ||
|
|
35f21e680a | ||
|
|
705fbaed9d | ||
|
|
9e52198ef7 | ||
|
|
b1943dfe17 | ||
|
|
c55e900c0f | ||
|
|
503e9addee | ||
|
|
d630fdd217 | ||
|
|
7e4a7c2a6e | ||
|
|
1716a771f3 | ||
|
|
36347cc4b0 | ||
|
|
7f94164e55 | ||
|
|
cbab7de546 | ||
|
|
d0de5cd021 | ||
|
|
16628e9902 | ||
|
|
72bfe0a14a | ||
|
|
6498ec4216 | ||
|
|
0d350fc76b | ||
|
|
2c6e4ce610 | ||
|
|
0c552774dc | ||
|
|
9ab605bb74 | ||
|
|
3d6cba0fe4 | ||
|
|
32e5e2b690 | ||
|
|
c967c1e308 | ||
|
|
ae28d5219b | ||
|
|
e4228f889e | ||
|
|
6d721c755e | ||
|
|
6948953778 | ||
|
|
8a50cef330 | ||
|
|
a0bf5370bd | ||
|
|
c5800b431d | ||
|
|
9a4056651d | ||
|
|
87382db78e | ||
|
|
5d9afa762f | ||
|
|
2f88587dab | ||
|
|
360a959b69 | ||
|
|
36fba545cf | ||
|
|
7faa2cc766 | ||
|
|
aa832e93aa | ||
|
|
86bbeb583d | ||
|
|
7a642b34b8 | ||
|
|
926b30b8ce | ||
|
|
5b672cef42 | ||
|
|
ceccbf4ded | ||
|
|
9cba2a0755 | ||
|
|
af036a9aa4 | ||
|
|
cfefd2d2d3 | ||
|
|
8e7299508b | ||
|
|
fc7aa36f41 | ||
|
|
fc9896048f | ||
|
|
572c4d9ace | ||
|
|
2572e085f6 | ||
|
|
8297f989fd | ||
|
|
636aaa2a4a | ||
|
|
5e70621e93 | ||
|
|
0b4434c51c | ||
|
|
dd6583c055 | ||
|
|
a0b0514159 | ||
|
|
e977c83e8f | ||
|
|
e281f5c579 | ||
|
|
e6b646e478 | ||
|
|
8d6bc12fa4 | ||
|
|
f036e748da | ||
|
|
0177a7caea | ||
|
|
c45e9f6156 | ||
|
|
8dc9b9f157 | ||
|
|
e73c6c14bb | ||
|
|
aa1c60f9ce | ||
|
|
d1e668bcff | ||
|
|
0454d43f32 | ||
|
|
61b3b51139 | ||
|
|
60d1a546a2 | ||
|
|
669933d270 | ||
|
|
053f0ccfa7 | ||
|
|
60e998fc7f | ||
|
|
3aa631198a | ||
|
|
b1876d655e | ||
|
|
9a9f89c1fc | ||
|
|
2fb41f6442 | ||
|
|
4ab949e6d7 | ||
|
|
7d1024c917 | ||
|
|
c2e3d55110 | ||
|
|
c6503a90bb | ||
|
|
b70787d5ec | ||
|
|
a9eb8113a6 | ||
|
|
aaf15d8934 | ||
|
|
ec298ebde0 | ||
|
|
bb108d0f49 | ||
| f271e1715d | |||
|
|
bd0e2779e5 | ||
|
|
53a1682868 | ||
|
|
a7df2fd832 | ||
|
|
c56cde860b | ||
|
|
59211e27c6 | ||
|
|
a2b1179353 | ||
|
|
c7f34ec92f | ||
|
|
4c011cf560 | ||
|
|
e5f78c711d | ||
|
|
98a536315b | ||
|
|
01343848e3 | ||
|
|
113e7dd0f7 | ||
|
|
1bc4176771 | ||
|
|
b091e5592d | ||
|
|
cb2d86340e | ||
|
|
57a92e94de | ||
|
|
30bc8bef39 | ||
|
|
2d237063a3 | ||
|
|
049a5075b5 | ||
|
|
cb2e9c4b07 | ||
|
|
5a39f85082 | ||
|
|
b863d201b9 | ||
|
|
78b50b41c2 | ||
|
|
0ba1545fd6 | ||
|
|
16c4e0f29b | ||
|
|
3581b8cbb2 | ||
|
|
7184c93f6f | ||
|
|
0969f0238f | ||
|
|
12f868d3f9 | ||
|
|
dc7be56807 | ||
|
|
cb60690e6b | ||
|
|
e0397bb2e8 | ||
|
|
822bcafd11 | ||
|
|
429d96c816 | ||
|
|
e948055f0f | ||
|
|
5129d46163 | ||
|
|
660f7142ef | ||
|
|
9f657588d8 | ||
|
|
0457f1a764 | ||
|
|
f196c6c296 | ||
|
|
2d2dcaee71 | ||
|
|
399b19f384 | ||
|
|
cba5bc8c95 | ||
|
|
0c97e2f4c4 | ||
|
|
631fbfb1cb | ||
|
|
5f14caeeb7 | ||
|
|
bb4d35fc00 | ||
|
|
a30729cf0b | ||
|
|
83bc5f6b1d | ||
|
|
8bcfaee22e | ||
|
|
6882922c5b | ||
|
|
35ea057110 | ||
|
|
2656606f8c | ||
|
|
024343f045 | ||
|
|
887e697a90 | ||
|
|
6e623668d5 | ||
|
|
17c60d2fb3 | ||
|
|
b4ce6a7d18 | ||
|
|
d4ec7cf31d | ||
|
|
7f01da0301 | ||
|
|
0e19769d79 | ||
|
|
0cda35f636 | ||
|
|
f8e30efe66 | ||
|
|
4160ac59ca | ||
|
|
62ebe8b293 | ||
|
|
fd891e84c1 | ||
|
|
de211d4567 | ||
|
|
dc292504b0 | ||
|
|
9cf4da92ac | ||
|
|
817b8786c8 | ||
|
|
fe7f1a30bb | ||
| 2d72c6e957 | |||
| e8c4d5a044 | |||
| 781c605e12 | |||
|
|
c712777d61 | ||
| 9755763e36 | |||
| 3d24c84c0f | |||
| 73b64c7302 | |||
| f42bba5dc3 | |||
| 8095196b1e | |||
| 41ef12d970 | |||
| c984464f78 | |||
|
|
29af8b8671 | ||
|
|
2df173de8e | ||
|
|
6ea83e777a | ||
|
|
96d847a5f8 | ||
|
|
2e6a1c9322 | ||
|
|
90e028eb3b | ||
|
|
0db833c4dc | ||
|
|
8c00a35a22 | ||
|
|
8d6d202ee9 | ||
|
|
d9a4f623a0 | ||
|
|
6d32293127 | ||
|
|
7e33cb8fb7 | ||
|
|
15d6f98427 | ||
|
|
3b1d0982e5 | ||
|
|
737e510a6c | ||
|
|
fbd77e56fd | ||
|
|
63dadb4ffa | ||
|
|
7167472f10 | ||
| 21b0382cfd | |||
| 496d274a56 | |||
| 3cce9dd3f1 | |||
| 9beb14d6b6 | |||
|
|
b311fe4e2a | ||
|
|
70eac57e4d |
45
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
name: '🏗️ Build Plugin'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '.gitea/**'
|
||||
- '.github/**'
|
||||
- 'jellyfin.ruleset'
|
||||
- '.gitignore'
|
||||
- '.editorconfig'
|
||||
- 'LICENSE'
|
||||
- 'logo.png'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "9.x"
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
run: |
|
||||
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish
|
||||
cd bin/Publish
|
||||
zip -r Jellyfin.Plugin.Seasonals.zip *
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: christopherHX/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: plugin-build-artifact
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
path: bin/Publish/Jellyfin.Plugin.Seasonals.zip
|
||||
45
.gitea/workflows/build_old.yaml.deprecated
Normal file
@@ -0,0 +1,45 @@
|
||||
name: '🏗️ Build Plugin'
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '.gitea/**'
|
||||
- '.github/**'
|
||||
- 'jellyfin.ruleset'
|
||||
- '.gitignore'
|
||||
- '.editorconfig'
|
||||
- 'LICENSE'
|
||||
- 'logo.png'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "9.x"
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
uses: oddstr13/jellyfin-plugin-repository-manager@v1.1.1
|
||||
id: jprm
|
||||
with:
|
||||
dotnet-target: "net9.0"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: christopherHX/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: plugin-build-artifact
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
path: ${{ steps.jprm.outputs.artifact }}
|
||||
216
.gitea/workflows/release_automation.yml
Normal file
@@ -0,0 +1,216 @@
|
||||
name: Auto Release Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.gitea/**'
|
||||
- '.github/**'
|
||||
- '*.md'
|
||||
- 'jellyfin.ruleset'
|
||||
- '.gitignore'
|
||||
- '.editorconfig'
|
||||
- 'LICENSE'
|
||||
- 'logo.png'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
|
||||
- name: Read Version from Manifest
|
||||
id: read_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.[0].versions[0].version' manifest.json)
|
||||
CHANGELOG=$(jq -r '.[0].versions[0].changelog' manifest.json)
|
||||
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' manifest.json)
|
||||
|
||||
echo "Detected Version: $VERSION"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "TARGET_ABI=$TARGET_ABI" >> $GITHUB_ENV
|
||||
|
||||
# Also export GUID for later use
|
||||
PLUGIN_GUID=$(jq -r '.[0].guid' manifest.json)
|
||||
echo "PLUGIN_GUID=$PLUGIN_GUID" >> $GITHUB_ENV
|
||||
|
||||
# Escape newlines in changelog for GITHUB_ENV
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
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
|
||||
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish /p:Version=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }}
|
||||
|
||||
cd bin/Publish
|
||||
zip -r Jellyfin.Plugin.Seasonals.zip *
|
||||
cd ../..
|
||||
|
||||
# Calculate hash
|
||||
HASH=$(md5sum bin/Publish/Jellyfin.Plugin.Seasonals.zip | awk '{ print $1 }')
|
||||
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Export variables for next steps
|
||||
echo "ZIP_HASH=$HASH" >> $GITHUB_ENV
|
||||
echo "BUILD_TIME=$TIME" >> $GITHUB_ENV
|
||||
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 }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
VERSION="${{ env.VERSION }}"
|
||||
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
|
||||
|
||||
echo "Updating manifest.json with:"
|
||||
echo "Hash: ${{ env.ZIP_HASH }}"
|
||||
echo "Time: ${{ env.BUILD_TIME }}"
|
||||
echo "Url: $DOWNLOAD_URL"
|
||||
|
||||
jq --arg hash "${{ env.ZIP_HASH }}" \
|
||||
--arg time "${{ env.BUILD_TIME }}" \
|
||||
--arg url "$DOWNLOAD_URL" \
|
||||
'.[0].versions[0].checksum = $hash | .[0].versions[0].timestamp = $time | .[0].versions[0].sourceUrl = $url' \
|
||||
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"
|
||||
body: ${{ env.CHANGELOG }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: ${{ env.ZIP_PATH }}
|
||||
name: "v${{ env.VERSION }}"
|
||||
tag_name: "v${{ env.VERSION }}"
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
# 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
|
||||
path: central-manifest
|
||||
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
|
||||
|
||||
REPO_OWNER="${{ github.repository_owner }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
|
||||
# 1. Get info from previous steps
|
||||
VERSION="${{ env.VERSION }}"
|
||||
HASH="${{ env.ZIP_HASH }}"
|
||||
TIME="${{ env.BUILD_TIME }}"
|
||||
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
|
||||
|
||||
# 2. Get info from env
|
||||
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
|
||||
CHANGELOG="${{ env.CHANGELOG }}"
|
||||
TARGET_ABI="${{ env.TARGET_ABI }}"
|
||||
|
||||
echo "Updating Central Manifest for Plugin GUID: $PLUGIN_GUID"
|
||||
|
||||
# 3. Update/Prepend entry in central manifest.json
|
||||
# Logic:
|
||||
# - If array is empty or new version != old version: PREPEND new entry
|
||||
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
|
||||
|
||||
jq --arg guid "$PLUGIN_GUID" \
|
||||
--arg hash "$HASH" \
|
||||
--arg time "$TIME" \
|
||||
--arg url "$DOWNLOAD_URL" \
|
||||
--arg ver "$VERSION" \
|
||||
--arg changelog "$CHANGELOG" \
|
||||
--arg abi "$TARGET_ABI" \
|
||||
'map(if .guid == $guid then
|
||||
if .versions[0].version != $ver then
|
||||
# New Version -> Prepend
|
||||
.versions = [{
|
||||
"version": $ver,
|
||||
"changelog": $changelog,
|
||||
"targetAbi": $abi,
|
||||
"sourceUrl": $url,
|
||||
"checksum": $hash,
|
||||
"timestamp": $time
|
||||
}] + .versions
|
||||
else
|
||||
# Same Version -> Update existing (overwrite top)
|
||||
.versions[0].changelog = $changelog |
|
||||
.versions[0].targetAbi = $abi |
|
||||
.versions[0].sourceUrl = $url |
|
||||
.versions[0].checksum = $hash |
|
||||
.versions[0].timestamp = $time
|
||||
end
|
||||
else . end)' \
|
||||
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"
|
||||
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
|
||||
|
||||
# Check if there are changes
|
||||
if [[ -n $(git status -s) ]]; then
|
||||
git add manifest.json
|
||||
git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
|
||||
git push
|
||||
else
|
||||
echo "No changes to central manifest."
|
||||
fi
|
||||
37
.github/workflows/build.yaml
vendored
@@ -3,16 +3,43 @@ name: '🏗️ Build Plugin'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '.gitea/**'
|
||||
- '.github/**'
|
||||
- 'jellyfin.ruleset'
|
||||
- '.gitignore'
|
||||
- '.editorconfig'
|
||||
- 'LICENSE'
|
||||
- 'logo.png'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "9.x"
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
run: |
|
||||
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish
|
||||
cd bin/Publish
|
||||
zip -r Jellyfin.Plugin.Seasonals.zip *
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: plugin-build-artifact
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
path: bin/Publish/Jellyfin.Plugin.Seasonals.zip
|
||||
20
.github/workflows/changelog.yaml
vendored
@@ -1,20 +0,0 @@
|
||||
name: '📝 Create/Update Release Draft & Release Bump PR'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- build.yaml
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
types:
|
||||
- update-prep-command
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/changelog.yaml@master
|
||||
with:
|
||||
repository-name: jellyfin/jellyfin-plugin-template
|
||||
secrets:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
13
.github/workflows/command-dispatch.yaml
vendored
@@ -1,13 +0,0 @@
|
||||
# Allows for the definition of PR and Issue /commands
|
||||
name: '📟 Slash Command Dispatcher'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-dispatch.yaml@master
|
||||
secrets:
|
||||
token: .
|
||||
16
.github/workflows/command-rebase.yaml
vendored
@@ -1,16 +0,0 @@
|
||||
name: '🔀 PR Rebase Command'
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types:
|
||||
- rebase-command
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-rebase.yaml@master
|
||||
with:
|
||||
rebase-head: ${{ github.event.client_payload.pull_request.head.label }}
|
||||
repository-full-name: ${{ github.event.client_payload.github.payload.repository.full_name }}
|
||||
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
|
||||
secrets:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
18
.github/workflows/publish.yaml
vendored
@@ -1,18 +0,0 @@
|
||||
name: '🚀 Publish Plugin'
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- released
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/publish.yaml@master
|
||||
with:
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
is-unstable: ${{ github.event.release.prerelease }}
|
||||
secrets:
|
||||
deploy-host: ${{ secrets.DEPLOY_HOST }}
|
||||
deploy-user: ${{ secrets.DEPLOY_USER }}
|
||||
deploy-key: ${{ secrets.DEPLOY_KEY }}
|
||||
184
.github/workflows/release_automation.yml
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
name: Auto Release Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- 'README.md'
|
||||
- 'jellyfin.ruleset'
|
||||
- '.gitignore'
|
||||
- '.editorconfig'
|
||||
- 'LICENSE'
|
||||
- 'logo.png'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
|
||||
- name: Read Version from Manifest
|
||||
id: read_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.[0].versions[0].version' manifest.json)
|
||||
CHANGELOG=$(jq -r '.[0].versions[0].changelog' manifest.json)
|
||||
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' manifest.json)
|
||||
|
||||
echo "Detected Version: $VERSION"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "TARGET_ABI=$TARGET_ABI" >> $GITHUB_ENV
|
||||
|
||||
# Also export GUID for later use
|
||||
PLUGIN_GUID=$(jq -r '.[0].guid' manifest.json)
|
||||
echo "PLUGIN_GUID=$PLUGIN_GUID" >> $GITHUB_ENV
|
||||
|
||||
# Escape newlines in changelog for GITHUB_ENV
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
echo "$CHANGELOG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and Zip
|
||||
shell: bash
|
||||
run: |
|
||||
# Inject version from manifest into the build
|
||||
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish /p:Version=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }}
|
||||
|
||||
cd bin/Publish
|
||||
zip -r Jellyfin.Plugin.Seasonals.zip *
|
||||
cd ../..
|
||||
|
||||
# Calculate hash
|
||||
HASH=$(md5sum bin/Publish/Jellyfin.Plugin.Seasonals.zip | awk '{ print $1 }')
|
||||
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Export variables for next steps
|
||||
echo "ZIP_HASH=$HASH" >> $GITHUB_ENV
|
||||
echo "BUILD_TIME=$TIME" >> $GITHUB_ENV
|
||||
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
|
||||
|
||||
- name: Update manifest.json
|
||||
shell: bash
|
||||
run: |
|
||||
REPO_OWNER="${{ github.repository_owner }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
VERSION="${{ env.VERSION }}"
|
||||
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
|
||||
|
||||
echo "Updating manifest.json with:"
|
||||
echo "Hash: ${{ env.ZIP_HASH }}"
|
||||
echo "Time: ${{ env.BUILD_TIME }}"
|
||||
echo "Url: $DOWNLOAD_URL"
|
||||
|
||||
jq --arg hash "${{ env.ZIP_HASH }}" \
|
||||
--arg time "${{ env.BUILD_TIME }}" \
|
||||
--arg url "$DOWNLOAD_URL" \
|
||||
'.[0].versions[0].checksum = $hash | .[0].versions[0].timestamp = $time | .[0].versions[0].sourceUrl = $url' \
|
||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||
|
||||
- name: Commit manifest.json
|
||||
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
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: "v${{ env.VERSION }}"
|
||||
name: "v${{ env.VERSION }}"
|
||||
# body: ${{ env.CHANGELOG }}
|
||||
files: ${{ env.ZIP_PATH }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
# Update Message in Remote Repository
|
||||
- name: Checkout Central Manifest Repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
|
||||
path: central-manifest
|
||||
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
||||
|
||||
- name: Update Central Manifest
|
||||
shell: bash
|
||||
run: |
|
||||
cd central-manifest
|
||||
|
||||
REPO_OWNER="${{ github.repository_owner }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
|
||||
# 1. Get info from previous steps
|
||||
VERSION="${{ env.VERSION }}"
|
||||
HASH="${{ env.ZIP_HASH }}"
|
||||
TIME="${{ env.BUILD_TIME }}"
|
||||
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
|
||||
|
||||
# 2. Get info from env
|
||||
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
|
||||
CHANGELOG="${{ env.CHANGELOG }}"
|
||||
TARGET_ABI="${{ env.TARGET_ABI }}"
|
||||
|
||||
echo "Updating Central Manifest for Plugin GUID: $PLUGIN_GUID"
|
||||
|
||||
# 3. Update/Prepend entry in central manifest.json
|
||||
# Logic:
|
||||
# - If array is empty or new version != old version: PREPEND new entry
|
||||
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
|
||||
|
||||
jq --arg guid "$PLUGIN_GUID" \
|
||||
--arg hash "$HASH" \
|
||||
--arg time "$TIME" \
|
||||
--arg url "$DOWNLOAD_URL" \
|
||||
--arg ver "$VERSION" \
|
||||
--arg changelog "$CHANGELOG" \
|
||||
--arg abi "$TARGET_ABI" \
|
||||
'map(if .guid == $guid then
|
||||
if .versions[0].version != $ver then
|
||||
# New Version -> Prepend
|
||||
.versions = [{
|
||||
"version": $ver,
|
||||
"changelog": $changelog,
|
||||
"targetAbi": $abi,
|
||||
"sourceUrl": $url,
|
||||
"checksum": $hash,
|
||||
"timestamp": $time
|
||||
}] + .versions
|
||||
else
|
||||
# Same Version -> Update existing (overwrite top)
|
||||
.versions[0].changelog = $changelog |
|
||||
.versions[0].targetAbi = $abi |
|
||||
.versions[0].sourceUrl = $url |
|
||||
.versions[0].checksum = $hash |
|
||||
.versions[0].timestamp = $time
|
||||
end
|
||||
else . end)' \
|
||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||
|
||||
- name: Commit and Push Central Manifest
|
||||
run: |
|
||||
cd central-manifest
|
||||
git config user.name "CodeDevMLH"
|
||||
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
|
||||
|
||||
# Check if there are changes
|
||||
if [[ -n $(git status -s) ]]; then
|
||||
git add manifest.json
|
||||
git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
|
||||
git push
|
||||
else
|
||||
echo "No changes to central manifest."
|
||||
fi
|
||||
20
.github/workflows/scan-codeql.yaml
vendored
@@ -1,20 +0,0 @@
|
||||
name: '🔬 Run CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
schedule:
|
||||
- cron: '24 2 * * 4'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master
|
||||
with:
|
||||
repository-name: jellyfin/jellyfin-plugin-template
|
||||
18
.github/workflows/test.yaml
vendored
@@ -1,18 +0,0 @@
|
||||
name: '🧪 Test Plugin'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call:
|
||||
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/test.yaml@master
|
||||
1
.gitignore
vendored
@@ -4,4 +4,5 @@ obj/
|
||||
.idea/
|
||||
artifacts
|
||||
|
||||
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! 🎉
|
||||
@@ -3,6 +3,8 @@ using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Jellyfin.Plugin.Seasonals;
|
||||
using Jellyfin.Plugin.Seasonals.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals.Api;
|
||||
|
||||
@@ -19,9 +21,9 @@ public class SeasonalsController : ControllerBase
|
||||
/// <returns>The configuration object.</returns>
|
||||
[HttpGet("Config")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<object> GetConfig()
|
||||
public ActionResult<PluginConfiguration> GetConfig()
|
||||
{
|
||||
return Plugin.Instance?.Configuration ?? new object();
|
||||
return SeasonalsPlugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,7 +40,7 @@ public class SeasonalsController : ControllerBase
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var assembly = typeof(SeasonalsPlugin).Assembly;
|
||||
// Convert path to resource name
|
||||
var resourcePath = path.Replace('/', '.').Replace('\\', '.');
|
||||
var resourceName = $"Jellyfin.Plugin.Seasonals.Web.{resourcePath}";
|
||||
@@ -60,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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
IsEnabled = true;
|
||||
SelectedSeason = "none";
|
||||
AutomateSeasonSelection = true;
|
||||
EnableClientSideToggle = true;
|
||||
|
||||
Autumn = new AutumnOptions();
|
||||
Snowflakes = new SnowflakesOptions();
|
||||
@@ -25,8 +27,17 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
Christmas = new ChristmasOptions();
|
||||
Santa = new SantaOptions();
|
||||
Easter = new EasterOptions();
|
||||
Resurrection = new ResurrectionOptions();
|
||||
Spring = new SpringOptions();
|
||||
Summer = new SummerOptions();
|
||||
Carnival = new CarnivalOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the plugin is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected season.
|
||||
/// </summary>
|
||||
@@ -37,6 +48,19 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
public bool AutomateSeasonSelection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable client-side toggle for users.
|
||||
/// </summary>
|
||||
public bool EnableClientSideToggle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the seasonal rules configuration as JSON.
|
||||
/// </summary>
|
||||
public string SeasonalRules { get; set; } = "[{\"Name\":\"New Year Fireworks\",\"StartDay\":28,\"StartMonth\":12,\"EndDay\":5,\"EndMonth\":1,\"Theme\":\"fireworks\"},{\"Name\":\"Carnival\",\"StartDay\":19,\"StartMonth\":2,\"EndDay\":28,\"EndMonth\":2,\"Theme\":\"carnival\"},{\"Name\":\"Valentine's Day\",\"StartDay\":10,\"StartMonth\":2,\"EndDay\":18,\"EndMonth\":2,\"Theme\":\"hearts\"},{\"Name\":\"Spring\",\"StartDay\":1,\"StartMonth\":3,\"EndDay\":31,\"EndMonth\":5,\"Theme\":\"spring\"},{\"Name\":\"Summer\",\"StartDay\":1,\"StartMonth\":6,\"EndDay\":31,\"EndMonth\":8,\"Theme\":\"summer\"},{\"Name\":\"Santa\",\"StartDay\":22,\"StartMonth\":12,\"EndDay\":27,\"EndMonth\":12,\"Theme\":\"santa\"},{\"Name\":\"Snowflakes (December)\",\"StartDay\":1,\"StartMonth\":12,\"EndDay\":31,\"EndMonth\":12,\"Theme\":\"snowflakes\"},{\"Name\":\"Snowfall (January)\",\"StartDay\":1,\"StartMonth\":1,\"EndDay\":31,\"EndMonth\":1,\"Theme\":\"snowfall\"},{\"Name\":\"Snowfall (February)\",\"StartDay\":1,\"StartMonth\":2,\"EndDay\":29,\"EndMonth\":2,\"Theme\":\"snowfall\"},{\"Name\":\"Easter\",\"StartDay\":25,\"StartMonth\":3,\"EndDay\":25,\"EndMonth\":4,\"Theme\":\"easter\"},{\"Name\":\"Halloween\",\"StartDay\":24,\"StartMonth\":10,\"EndDay\":5,\"EndMonth\":11,\"Theme\":\"halloween\"},{\"Name\":\"Autumn\",\"StartDay\":1,\"StartMonth\":9,\"EndDay\":30,\"EndMonth\":11,\"Theme\":\"autumn\"}]";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Seasonals options.
|
||||
/// </summary>
|
||||
public AutumnOptions Autumn { get; set; }
|
||||
public SnowflakesOptions Snowflakes { get; set; }
|
||||
public SnowfallOptions Snowfall { get; set; }
|
||||
@@ -47,6 +71,10 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
public ChristmasOptions Christmas { get; set; }
|
||||
public SantaOptions Santa { get; set; }
|
||||
public EasterOptions Easter { get; set; }
|
||||
public ResurrectionOptions Resurrection { get; set; }
|
||||
public SpringOptions Spring { get; set; }
|
||||
public SummerOptions Summer { get; set; }
|
||||
public CarnivalOptions Carnival { get; set; }
|
||||
}
|
||||
|
||||
public class AutumnOptions
|
||||
@@ -151,3 +179,43 @@ public class EasterOptions
|
||||
public int MinBunnyRestTime { get; set; } = 2000;
|
||||
public int MaxBunnyRestTime { get; set; } = 5000;
|
||||
}
|
||||
|
||||
public class ResurrectionOptions
|
||||
{
|
||||
public int SymbolCount { get; set; } = 12;
|
||||
public bool EnableResurrection { get; set; } = true;
|
||||
public bool EnableRandomSymbols { get; set; } = true;
|
||||
public bool EnableRandomSymbolsMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SpringOptions
|
||||
{
|
||||
public int PetalCount { get; set; } = 25;
|
||||
public int PollenCount { get; set; } = 15;
|
||||
public int LadybugCount { get; set; } = 5;
|
||||
public int SunbeamCount { get; set; } = 5;
|
||||
public bool EnableSpring { get; set; } = true;
|
||||
public bool EnableRandomSpring { get; set; } = true;
|
||||
public bool EnableRandomSpringMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class SummerOptions
|
||||
{
|
||||
public int BubbleCount { get; set; } = 20;
|
||||
public int DustCount { get; set; } = 50;
|
||||
public bool EnableSummer { get; set; } = true;
|
||||
public bool EnableRandomSummer { get; set; } = true;
|
||||
public bool EnableRandomSummerMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
public class CarnivalOptions
|
||||
{
|
||||
public int ObjectCount { get; set; } = 25;
|
||||
public bool EnableCarnival { get; set; } = true;
|
||||
public bool EnableRandomCarnival { get; set; } = true;
|
||||
public bool EnableRandomCarnivalMobile { get; set; } = false;
|
||||
public bool EnableDifferentDuration { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Template</title>
|
||||
<title>Seasonals Configuration</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer">
|
||||
<h2 class="sectionTitle">Seasonals</h2>
|
||||
<h2 class="sectionTitle">Seasonals Configuration</h2>
|
||||
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
|
||||
target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals">
|
||||
target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals">
|
||||
<i class="md-icon button-icon button-icon-left secondaryText"></i>
|
||||
<span>Help</span>
|
||||
<span>${Help}</span>
|
||||
</a>
|
||||
</div>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
<br>
|
||||
<form id="SeasonalsConfigForm">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="AutomateSeasonSelection" name="AutomateSeasonSelection" type="checkbox" is="emby-checkbox" />
|
||||
<span>Automate Season Selection</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Automatically select the season based on the date.</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="SelectedSeason">Selected Season</label>
|
||||
<select is="emby-select" id="SelectedSeason" name="SelectedSeason" class="emby-select-withcolor emby-select">
|
||||
<option value="none">None</option>
|
||||
<option value="snowflakes">Snowflakes</option>
|
||||
<option value="snowfall">Snowfall</option>
|
||||
<option value="snowstorm">Snowstorm</option>
|
||||
<option value="fireworks">Fireworks</option>
|
||||
<option value="halloween">Halloween</option>
|
||||
<option value="hearts">Hearts</option>
|
||||
<option value="christmas">Christmas</option>
|
||||
<option value="santa">Santa</option>
|
||||
<option value="autumn">Autumn</option>
|
||||
<option value="easter">Easter</option>
|
||||
<option value="summer" disabled>Summer (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="spring" disabled>Spring (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="oktoberfest" disabled>Oktoberfest (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="carnival" disabled>Carnival (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="championships" disabled>European/World Championships (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="patrick" disabled>St. Patrick's Day (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
<option value="pride" disabled>Pride (not implemented yet, no idea for a theme. Please commit ideas in a issue)</option>
|
||||
</select>
|
||||
<div class="fieldDescription">The season to display if automation is disabled.</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<details>
|
||||
<summary>Advanced Configuration</summary>
|
||||
<p>Configure specific settings for each seasonal theme below.</p>
|
||||
<p>All symbol count settings add this number in addition to the standard 12 symbols (if additional symbols is enabled).</p>
|
||||
<div style="margin-bottom: 1.5em;">
|
||||
<button class="seasonals-tab-button active" onclick="showSeasonalsTab('seasonals-basic', this)"
|
||||
style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid #00a4dc;">
|
||||
<h3>General Settings</h3>
|
||||
</button>
|
||||
<button class="seasonals-tab-button" onclick="showSeasonalsTab('seasonals-auto-selection', this)"
|
||||
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
|
||||
<h3>Auto Selection</h3>
|
||||
</button>
|
||||
<button class="seasonals-tab-button" onclick="showSeasonalsTab('seasonals-advanced', this)"
|
||||
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
|
||||
<h3>Advanced Settings</h3>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="SeasonalsConfigForm">
|
||||
<!-- BASIC Tab -->
|
||||
<div id="seasonals-basic" class="seasonals-tab-content">
|
||||
<h2 class="sectionTitle">Main Plugin Settings</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="SeasonalsIsEnabled" name="SeasonalsIsEnabled" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Seasonals</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="SeasonalsAutomateSeasonSelection" name="SeasonalsAutomateSeasonSelection" type="checkbox" is="emby-checkbox" />
|
||||
<span>Automate Season Selection</span>
|
||||
</label>
|
||||
<div class="fieldDescription">If enabled, the plugin will use the rules defined in the "Auto Selection" tab to assume the season. If no rule matches, it falls back to the "Standard Season" below.</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="SeasonalsSelectedSeason">Standard Season</label>
|
||||
<select is="emby-select" id="SeasonalsSelectedSeason" name="SeasonalsSelectedSeason" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
|
||||
<option value="none">None</option>
|
||||
<option value="snowflakes">Snowflakes</option>
|
||||
<option value="snowfall">Snowfall</option>
|
||||
<option value="snowstorm">Snowstorm</option>
|
||||
<option value="fireworks">Fireworks</option>
|
||||
<option value="halloween">Halloween</option>
|
||||
<option value="hearts">Hearts</option>
|
||||
<option value="christmas">Christmas</option>
|
||||
<option value="santa">Santa (flying santa & snowfall)</option>
|
||||
<option value="autumn">Autumn (falling leaves)</option>
|
||||
<option value="easter">Easter</option>
|
||||
<option value="resurrection">Resurrection</option>
|
||||
<option value="summer">Summer (Bubbles)</option>
|
||||
<option value="spring">Spring</option>
|
||||
<option value="carnival">Carnival (Confetti)</option>
|
||||
<option value="championships" disabled>European/World Championships (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="patrick" disabled>St. Patrick's Day (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="thanksgiving" disabled>Thanksgiving (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
<option value="pride" disabled>Pride (not implemented yet. Please commit ideas in a issue or PR)</option>
|
||||
</select>
|
||||
<div class="fieldDescription">The season to display if automation is disabled or no "Auto Selection" rule matches the current date.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="SeasonalsEnableClientSideToggle" name="SeasonalsEnableClientSideToggle" type="checkbox" is="emby-checkbox" />
|
||||
<span>Allow Client-Side Toggle</span>
|
||||
</label>
|
||||
<div class="fieldDescription">If enabled, users will see a seasonals icon in the header to toggle seasonals for their browser (device-specific).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Selection Tab -->
|
||||
<div id="seasonals-auto-selection" class="seasonals-tab-content" style="display: none;">
|
||||
<h2>Auto Selection Rules</h2>
|
||||
|
||||
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
|
||||
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
|
||||
<div>Define rules to automatically select a season based on the date. Rules are evaluated from top to bottom. The first matching rule wins.</div>
|
||||
</div>
|
||||
|
||||
<div id="seasonalRulesList">
|
||||
<!-- Rules will be injected here via JS -->
|
||||
</div>
|
||||
|
||||
<button is="emby-button" type="button" class="raised emby-button" onclick="SeasonalsConfigPage.addRule();"
|
||||
style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
|
||||
<i class="material-icons" style="font-size: 24px;">add</i>
|
||||
<span>Add Rule</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Tab -->
|
||||
<div id="seasonals-advanced" class="seasonals-tab-content" style="display: none;">
|
||||
<h2 class="sectionTitle">Configure specific settings for each seasonal theme</h2>
|
||||
<!-- <p>Configure specific settings for each seasonal theme below.</p> -->
|
||||
<p>All symbol count settings add this number in addition to the standard 12 symbols (if additional symbols is enabled).</p>
|
||||
<details>
|
||||
<summary>Autumn</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
@@ -485,8 +537,180 @@
|
||||
<div class="fieldDescription">Maximum time the bunny waits before appearing again.</div>
|
||||
</div>
|
||||
</details>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Resurrection</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableResurrection" name="EnableResurrection" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Resurrection Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Resurrection theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomResurrection" name="EnableRandomResurrection" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Symbols</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional symbols randomly distributed across the screen.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomResurrectionMobile" name="EnableRandomResurrectionMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Symbols on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional symbols randomly distributed across the screen on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="ResurrectionSymbolCount">Symbol Count</label>
|
||||
<input is="emby-input" type="number" id="ResurrectionSymbolCount" name="ResurrectionSymbolCount" />
|
||||
<div class="fieldDescription">Number of additional symbols (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationResurrection" name="EnableDifferentDurationResurrection" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Duration</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the movement speed.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Spring</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableSpring" name="EnableSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Spring Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Spring theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomSpring" name="EnableRandomSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Sakura Petals</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional Sakura petals falling across the screen.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomSpringMobile" name="EnableRandomSpringMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Sakura Petals on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional Sakura petals falling across the screen on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringPetalCount">Sakura Petal Count</label>
|
||||
<input is="emby-input" type="number" id="SpringPetalCount" name="SpringPetalCount" />
|
||||
<div class="fieldDescription">Number of additional Sakura petals (if enabled).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringPollenCount">Pollen Count</label>
|
||||
<input is="emby-input" type="number" id="SpringPollenCount" name="SpringPollenCount" />
|
||||
<div class="fieldDescription">Number of pollen particles (if enabled).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringLadybugCount">Ladybug Count</label>
|
||||
<input is="emby-input" type="number" id="SpringLadybugCount" name="SpringLadybugCount" />
|
||||
<div class="fieldDescription">Number of ladybugs.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SpringSunbeamCount">Sunbeam Count</label>
|
||||
<input is="emby-input" type="number" id="SpringSunbeamCount" name="SpringSunbeamCount" />
|
||||
<div class="fieldDescription">Number of sunbeams (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationSpring" name="EnableDifferentDurationSpring" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Falling Speed</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the falling speed of petals.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Summer</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableSummer" name="EnableSummer" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Summer Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Summer theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomSummer" name="EnableRandomSummer" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Bubbles and Dust</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional bubbles and dust particles rising across the screen.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomSummerMobile" name="EnableRandomSummerMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Bubbles and Dust on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional bubbles and dust particles rising across the screen on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SummerBubbleCount">Bubbles Count</label>
|
||||
<input is="emby-input" type="number" id="SummerBubbleCount" name="SummerBubbleCount" />
|
||||
<div class="fieldDescription">Number of bubbles (if enabled).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="SummerDustCount">Dust Count</label>
|
||||
<input is="emby-input" type="number" id="SummerDustCount" name="SummerDustCount" />
|
||||
<div class="fieldDescription">Number of dust particles (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationSummer" name="EnableDifferentDurationSummer" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Rising Speed</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the rising speed of bubbles.</div>
|
||||
</div>
|
||||
</details>
|
||||
<hr style="max-width: 800px; margin: 1em 0;">
|
||||
|
||||
<details>
|
||||
<summary>Carnival</summary>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableCarnival" name="EnableCarnival" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Carnival Seasonal</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Enable the Carnival theme in general (e.g. for automation).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomCarnival" name="EnableRandomCarnival" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Confetti</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional confetti falling and fluttering across the screen.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableRandomCarnivalMobile" name="EnableRandomCarnivalMobile" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Additional Random Confetti on Mobile</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Displays additional confetti falling and fluttering across the screen on mobile devices. Warning: High values may affect performance.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="CarnivalObjectCount">Confetti Count</label>
|
||||
<input is="emby-input" type="number" id="CarnivalObjectCount" name="CarnivalObjectCount" />
|
||||
<div class="fieldDescription">Number of additional confetti pieces (if enabled).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="EnableDifferentDurationCarnival" name="EnableDifferentDurationCarnival" type="checkbox" is="emby-checkbox" />
|
||||
<span>Enable Different Falling Speed</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Randomize the falling speed of confetti.</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
|
||||
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
|
||||
<div>
|
||||
@@ -506,18 +730,248 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var SeasonalsConfig = {
|
||||
pluginUniqueId: 'ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4'
|
||||
<style>
|
||||
/* Styles for the Seasonal Rules List (Auto Selection Tab) */
|
||||
.seasonal-rule {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.seasonal-rule-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.seasonal-rule-content {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.3fr 1.3fr 2fr;
|
||||
gap: 15px;
|
||||
align-items: end;
|
||||
}
|
||||
.rule-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.date-range-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.date-range-group > .inputContainer,
|
||||
.date-range-group > .selectContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.seasonal-rule-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles for the Tabs */
|
||||
.seasonals-tab-button.active {
|
||||
color: #fff !important;
|
||||
border-bottom: 2px solid #00a4dc !important;
|
||||
}
|
||||
|
||||
/* Disabled options in selects */
|
||||
select option:disabled {
|
||||
color: #a3a3a3 !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function showSeasonalsTab(tabId, btn) {
|
||||
document.querySelectorAll('.seasonals-tab-content').forEach(el => el.style.display = 'none');
|
||||
document.getElementById(tabId).style.display = 'block';
|
||||
|
||||
document.querySelectorAll('.seasonals-tab-button').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.style.color = '#ccc';
|
||||
b.style.borderBottom = '2px solid transparent';
|
||||
});
|
||||
|
||||
if (btn) {
|
||||
btn.classList.add('active');
|
||||
btn.style.color = '#fff';
|
||||
btn.style.borderBottom = '2px solid #00a4dc';
|
||||
}
|
||||
}
|
||||
|
||||
var SeasonalsConfigPage = {
|
||||
pluginUniqueId: 'ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4',
|
||||
|
||||
addRule: function(data = null) {
|
||||
var container = document.querySelector('#seasonalRulesList');
|
||||
var div = document.createElement('div');
|
||||
div.className = 'seasonal-rule';
|
||||
|
||||
var name = data ? (data.Name || data.name || 'New Rule') : 'New Rule';
|
||||
var startDay = data ? (data.StartDay !== undefined ? data.StartDay : (data.startDay !== undefined ? data.startDay : 1)) : 1;
|
||||
var startMonth = data ? (data.StartMonth !== undefined ? data.StartMonth : (data.startMonth !== undefined ? data.startMonth : 1)) : 1;
|
||||
var endDay = data ? (data.EndDay !== undefined ? data.EndDay : (data.endDay !== undefined ? data.endDay : 1)) : 1;
|
||||
var endMonth = data ? (data.EndMonth !== undefined ? data.EndMonth : (data.endMonth !== undefined ? data.endMonth : 1)) : 1;
|
||||
var theme = data ? (data.Theme || data.theme || 'none') : 'none';
|
||||
|
||||
var days = [];
|
||||
for (var i = 1; i <= 31; i++) days.push(i);
|
||||
|
||||
var months = [
|
||||
{ v: 1, n: 'Jan' }, { v: 2, n: 'Feb' }, { v: 3, n: 'Mar' }, { v: 4, n: 'Apr' },
|
||||
{ v: 5, n: 'May' }, { v: 6, n: 'Jun' }, { v: 7, n: 'Jul' }, { v: 8, n: 'Aug' },
|
||||
{ v: 9, n: 'Sep' }, { v: 10, n: 'Oct' }, { v: 11, n: 'Nov' }, { v: 12, n: 'Dec' }
|
||||
];
|
||||
|
||||
// Build select HTML via string concatenation to avoid Jellyfin's ${} localization processing
|
||||
function mkSelect(val, opts, cls) {
|
||||
var h = '<select class="emby-select emby-select-withcolor ' + cls + '" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">';
|
||||
opts.forEach(function(o) {
|
||||
var v = o.v || o;
|
||||
var n = o.n || o;
|
||||
h += '<option value="' + v + '" ' + (v == val ? 'selected' : '') + '>' + n + '</option>';
|
||||
});
|
||||
h += '</select>';
|
||||
return h;
|
||||
}
|
||||
|
||||
div.innerHTML =
|
||||
'<div class="seasonal-rule-header">' +
|
||||
' <div style="font-weight: bold; font-size: 1.1em;" class="rule-title"></div>' +
|
||||
' <div class="rule-actions">' +
|
||||
' <button type="button" is="paper-icon-button-light" class="btn-move-up" title="Move Up"><i class="material-icons">arrow_upward</i></button>' +
|
||||
' <button type="button" is="paper-icon-button-light" class="btn-move-down" title="Move Down"><i class="material-icons">arrow_downward</i></button>' +
|
||||
' <button type="button" is="paper-icon-button-light" class="btn-remove" title="Remove"><i class="material-icons">delete</i></button>' +
|
||||
' </div>' +
|
||||
'</div>' +
|
||||
'<div class="seasonal-rule-content">' +
|
||||
' <div class="inputContainer" style="margin:0;">' +
|
||||
' <label class="inputLabel">Name</label>' +
|
||||
' <input is="emby-input" class="rule-name" onchange="SeasonalsConfigPage.updateRuleTitles();" />' +
|
||||
' </div>' +
|
||||
' <div class="date-range-group">' +
|
||||
' <div class="selectContainer" style="margin:0; flex: 1;">' +
|
||||
' <label class="selectLabel">Start Day</label>' +
|
||||
mkSelect(startDay, days, 'rule-start-day') +
|
||||
' </div>' +
|
||||
' <div class="selectContainer" style="margin:0; flex: 1;">' +
|
||||
' <label class="selectLabel">Month</label>' +
|
||||
mkSelect(startMonth, months, 'rule-start-month') +
|
||||
' </div>' +
|
||||
' </div>' +
|
||||
' <div class="date-range-group">' +
|
||||
' <div class="selectContainer" style="margin:0; flex: 1;">' +
|
||||
' <label class="selectLabel">End Day</label>' +
|
||||
mkSelect(endDay, days, 'rule-end-day') +
|
||||
' </div>' +
|
||||
' <div class="selectContainer" style="margin:0; flex: 1;">' +
|
||||
' <label class="selectLabel">Month</label>' +
|
||||
mkSelect(endMonth, months, 'rule-end-month') +
|
||||
' </div>' +
|
||||
' </div>' +
|
||||
' <div class="selectContainer" style="margin:0;">' +
|
||||
' <label class="selectLabel">Theme</label>' +
|
||||
' <select class="emby-select emby-select-withcolor rule-theme" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">' +
|
||||
' <option value="none">None</option>' +
|
||||
' <option value="snowflakes">Snowflakes</option>' +
|
||||
' <option value="snowfall">Snowfall</option>' +
|
||||
' <option value="snowstorm">Snowstorm</option>' +
|
||||
' <option value="fireworks">Fireworks</option>' +
|
||||
' <option value="halloween">Halloween</option>' +
|
||||
' <option value="hearts">Hearts</option>' +
|
||||
' <option value="christmas">Christmas</option>' +
|
||||
' <option value="santa">Santa</option>' +
|
||||
' <option value="autumn">Autumn</option>' +
|
||||
' <option value="easter">Easter</option>' +
|
||||
' <option value="resurrection">Resurrection</option>' +
|
||||
' <option value="spring">Spring</option>' +
|
||||
' <option value="summer">Summer</option>' +
|
||||
' <option value="carnival">Carnival</option>' +
|
||||
' </select>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
|
||||
container.appendChild(div);
|
||||
|
||||
// Set values programmatically
|
||||
div.querySelector('.rule-name').value = name;
|
||||
div.querySelector('.rule-theme').value = theme;
|
||||
|
||||
// Bind events
|
||||
div.querySelector('.btn-remove').addEventListener('click', function() {
|
||||
div.remove();
|
||||
SeasonalsConfigPage.updateRuleTitles();
|
||||
});
|
||||
div.querySelector('.btn-move-up').addEventListener('click', function() {
|
||||
if (div.previousElementSibling) {
|
||||
container.insertBefore(div, div.previousElementSibling);
|
||||
SeasonalsConfigPage.updateRuleTitles();
|
||||
}
|
||||
});
|
||||
div.querySelector('.btn-move-down').addEventListener('click', function() {
|
||||
if (div.nextElementSibling) {
|
||||
container.insertBefore(div.nextElementSibling, div);
|
||||
SeasonalsConfigPage.updateRuleTitles();
|
||||
}
|
||||
});
|
||||
|
||||
this.updateRuleTitles();
|
||||
},
|
||||
|
||||
updateRuleTitles: function() {
|
||||
var rules = document.querySelectorAll('.seasonal-rule');
|
||||
rules.forEach((rule, index) => {
|
||||
var name = rule.querySelector('.rule-name').value;
|
||||
rule.querySelector('.rule-title').innerText = '#' + (index + 1) + ' ' + name;
|
||||
});
|
||||
},
|
||||
|
||||
renderRules: function(rules) {
|
||||
var container = document.querySelector('#seasonalRulesList');
|
||||
container.innerHTML = '';
|
||||
if (Array.isArray(rules)) {
|
||||
rules.forEach(rule => this.addRule(rule));
|
||||
}
|
||||
},
|
||||
|
||||
getRulesFromUI: function() {
|
||||
var rules = [];
|
||||
document.querySelectorAll('.seasonal-rule').forEach(div => {
|
||||
rules.push({
|
||||
Name: div.querySelector('.rule-name').value,
|
||||
StartDay: parseInt(div.querySelector('.rule-start-day').value),
|
||||
StartMonth: parseInt(div.querySelector('.rule-start-month').value),
|
||||
EndDay: parseInt(div.querySelector('.rule-end-day').value),
|
||||
EndMonth: parseInt(div.querySelector('.rule-end-month').value),
|
||||
Theme: div.querySelector('.rule-theme').value
|
||||
});
|
||||
});
|
||||
return rules;
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#SeasonalsConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
|
||||
document.querySelector('#SelectedSeason').value = config.SelectedSeason;
|
||||
document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection;
|
||||
|
||||
ApiClient.getPluginConfiguration(SeasonalsConfigPage.pluginUniqueId).then(function (config) {
|
||||
document.querySelector('#SeasonalsIsEnabled').checked = config.IsEnabled;
|
||||
document.querySelector('#SeasonalsSelectedSeason').value = config.SelectedSeason;
|
||||
document.querySelector('#SeasonalsAutomateSeasonSelection').checked = config.AutomateSeasonSelection;
|
||||
document.querySelector('#SeasonalsEnableClientSideToggle').checked = config.EnableClientSideToggle !== undefined ? config.EnableClientSideToggle : true;
|
||||
|
||||
// Load Rules
|
||||
try {
|
||||
var rules = JSON.parse(config.SeasonalRules || "[]");
|
||||
SeasonalsConfigPage.renderRules(rules);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse SeasonalRules", e);
|
||||
}
|
||||
|
||||
|
||||
// Advanced Config
|
||||
// Autumn
|
||||
document.querySelector('#EnableAutumn').checked = config.Autumn.EnableAutumn;
|
||||
@@ -602,105 +1056,174 @@
|
||||
document.querySelector('#MinBunnyRestTime').value = config.Easter.MinBunnyRestTime;
|
||||
document.querySelector('#MaxBunnyRestTime').value = config.Easter.MaxBunnyRestTime;
|
||||
|
||||
// Resurrection
|
||||
document.querySelector('#EnableResurrection').checked = config.Resurrection.EnableResurrection;
|
||||
document.querySelector('#ResurrectionSymbolCount').value = config.Resurrection.SymbolCount;
|
||||
document.querySelector('#EnableRandomResurrection').checked = config.Resurrection.EnableRandomSymbols;
|
||||
document.querySelector('#EnableRandomResurrectionMobile').checked = config.Resurrection.EnableRandomSymbolsMobile;
|
||||
document.querySelector('#EnableDifferentDurationResurrection').checked = config.Resurrection.EnableDifferentDuration;
|
||||
|
||||
// Spring
|
||||
document.querySelector('#EnableSpring').checked = config.Spring.EnableSpring;
|
||||
document.querySelector('#SpringPetalCount').value = config.Spring.PetalCount;
|
||||
document.querySelector('#SpringPollenCount').value = config.Spring.PollenCount;
|
||||
document.querySelector('#SpringLadybugCount').value = config.Spring.LadybugCount;
|
||||
document.querySelector('#SpringSunbeamCount').value = config.Spring.SunbeamCount;
|
||||
document.querySelector('#EnableRandomSpring').checked = config.Spring.EnableRandomSpring;
|
||||
document.querySelector('#EnableRandomSpringMobile').checked = config.Spring.EnableRandomSpringMobile;
|
||||
document.querySelector('#EnableDifferentDurationSpring').checked = config.Spring.EnableDifferentDuration;
|
||||
|
||||
// Summer
|
||||
document.querySelector('#EnableSummer').checked = config.Summer.EnableSummer;
|
||||
document.querySelector('#SummerBubbleCount').value = config.Summer.BubbleCount;
|
||||
document.querySelector('#SummerDustCount').value = config.Summer.DustCount;
|
||||
document.querySelector('#EnableRandomSummer').checked = config.Summer.EnableRandomSummer;
|
||||
document.querySelector('#EnableRandomSummerMobile').checked = config.Summer.EnableRandomSummerMobile;
|
||||
document.querySelector('#EnableDifferentDurationSummer').checked = config.Summer.EnableDifferentDuration;
|
||||
|
||||
// Carnival
|
||||
document.querySelector('#EnableCarnival').checked = config.Carnival.EnableCarnival;
|
||||
document.querySelector('#CarnivalObjectCount').value = config.Carnival.ObjectCount;
|
||||
document.querySelector('#EnableRandomCarnival').checked = config.Carnival.EnableRandomCarnival;
|
||||
document.querySelector('#EnableRandomCarnivalMobile').checked = config.Carnival.EnableRandomCarnivalMobile;
|
||||
document.querySelector('#EnableDifferentDurationCarnival').checked = config.Carnival.EnableDifferentDuration;
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#SeasonalsConfigForm')
|
||||
.addEventListener('submit', function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
|
||||
config.SelectedSeason = document.querySelector('#SelectedSeason').value;
|
||||
config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked;
|
||||
|
||||
// Advanced Config
|
||||
// Autumn
|
||||
config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
|
||||
config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
|
||||
config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
|
||||
config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
|
||||
config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
|
||||
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(SeasonalsConfigPage.pluginUniqueId).then(function (config) {
|
||||
config.IsEnabled = document.querySelector('#SeasonalsIsEnabled').checked;
|
||||
config.SelectedSeason = document.querySelector('#SeasonalsSelectedSeason').value;
|
||||
config.AutomateSeasonSelection = document.querySelector('#SeasonalsAutomateSeasonSelection').checked;
|
||||
config.EnableClientSideToggle = document.querySelector('#SeasonalsEnableClientSideToggle').checked;
|
||||
|
||||
// Snowflakes
|
||||
config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value);
|
||||
config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked;
|
||||
config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked;
|
||||
config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked;
|
||||
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked;
|
||||
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked;
|
||||
// Save Rules
|
||||
config.SeasonalRules = JSON.stringify(SeasonalsConfigPage.getRulesFromUI());
|
||||
|
||||
// Snowfall
|
||||
config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked;
|
||||
config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value);
|
||||
config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value);
|
||||
config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value);
|
||||
// Advanced Config
|
||||
// Autumn
|
||||
config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
|
||||
config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
|
||||
config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
|
||||
config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
|
||||
config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
|
||||
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
|
||||
|
||||
// Snowstorm
|
||||
config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked;
|
||||
config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value);
|
||||
config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value);
|
||||
config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value);
|
||||
config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
|
||||
config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
|
||||
// Snowflakes
|
||||
config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value);
|
||||
config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked;
|
||||
config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked;
|
||||
config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked;
|
||||
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked;
|
||||
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked;
|
||||
|
||||
// Fireworks
|
||||
config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked;
|
||||
config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value);
|
||||
config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value);
|
||||
config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked;
|
||||
config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value);
|
||||
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value);
|
||||
// Snowfall
|
||||
config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked;
|
||||
config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value);
|
||||
config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value);
|
||||
config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value);
|
||||
|
||||
// Halloween
|
||||
config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked;
|
||||
config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value);
|
||||
config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked;
|
||||
config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked;
|
||||
config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked;
|
||||
// Snowstorm
|
||||
config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked;
|
||||
config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value);
|
||||
config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value);
|
||||
config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value);
|
||||
config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
|
||||
config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
|
||||
|
||||
// Hearts
|
||||
config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked;
|
||||
config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value);
|
||||
config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked;
|
||||
config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked;
|
||||
config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked;
|
||||
// Fireworks
|
||||
config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked;
|
||||
config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value);
|
||||
config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value);
|
||||
config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked;
|
||||
config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value);
|
||||
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value);
|
||||
|
||||
// Christmas
|
||||
config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked;
|
||||
config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value);
|
||||
config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked;
|
||||
config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked;
|
||||
config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked;
|
||||
// Halloween
|
||||
config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked;
|
||||
config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value);
|
||||
config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked;
|
||||
config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked;
|
||||
config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked;
|
||||
|
||||
// Santa
|
||||
config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked;
|
||||
config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value);
|
||||
config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value);
|
||||
config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value);
|
||||
config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value);
|
||||
config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
|
||||
config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
|
||||
config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
|
||||
config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
|
||||
config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
|
||||
// Hearts
|
||||
config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked;
|
||||
config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value);
|
||||
config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked;
|
||||
config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked;
|
||||
config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked;
|
||||
|
||||
// Easter
|
||||
config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked;
|
||||
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value);
|
||||
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked;
|
||||
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked;
|
||||
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked;
|
||||
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked;
|
||||
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value);
|
||||
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value);
|
||||
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value);
|
||||
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value);
|
||||
// Christmas
|
||||
config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked;
|
||||
config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value);
|
||||
config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked;
|
||||
config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked;
|
||||
config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
// Santa
|
||||
config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked;
|
||||
config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value);
|
||||
config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value);
|
||||
config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value);
|
||||
config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value);
|
||||
config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
|
||||
config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
|
||||
config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
|
||||
config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
|
||||
config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
|
||||
|
||||
// Easter
|
||||
config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked;
|
||||
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value);
|
||||
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked;
|
||||
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked;
|
||||
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked;
|
||||
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked;
|
||||
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value);
|
||||
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value);
|
||||
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value);
|
||||
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value);
|
||||
|
||||
// Resurrection
|
||||
config.Resurrection.EnableResurrection = document.querySelector('#EnableResurrection').checked;
|
||||
config.Resurrection.SymbolCount = parseInt(document.querySelector('#ResurrectionSymbolCount').value);
|
||||
config.Resurrection.EnableRandomSymbols = document.querySelector('#EnableRandomResurrection').checked;
|
||||
config.Resurrection.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomResurrectionMobile').checked;
|
||||
config.Resurrection.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationResurrection').checked;
|
||||
|
||||
// Spring
|
||||
config.Spring.EnableSpring = document.querySelector('#EnableSpring').checked;
|
||||
config.Spring.PetalCount = parseInt(document.querySelector('#SpringPetalCount').value);
|
||||
config.Spring.PollenCount = parseInt(document.querySelector('#SpringPollenCount').value);
|
||||
config.Spring.LadybugCount = parseInt(document.querySelector('#SpringLadybugCount').value);
|
||||
config.Spring.SunbeamCount = parseInt(document.querySelector('#SpringSunbeamCount').value);
|
||||
config.Spring.EnableRandomSpring = document.querySelector('#EnableRandomSpring').checked;
|
||||
config.Spring.EnableRandomSpringMobile = document.querySelector('#EnableRandomSpringMobile').checked;
|
||||
config.Spring.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSpring').checked;
|
||||
|
||||
// Summer
|
||||
config.Summer.EnableSummer = document.querySelector('#EnableSummer').checked;
|
||||
config.Summer.BubbleCount = parseInt(document.querySelector('#SummerBubbleCount').value);
|
||||
config.Summer.DustCount = parseInt(document.querySelector('#SummerDustCount').value);
|
||||
config.Summer.EnableRandomSummer = document.querySelector('#EnableRandomSummer').checked;
|
||||
config.Summer.EnableRandomSummerMobile = document.querySelector('#EnableRandomSummerMobile').checked;
|
||||
config.Summer.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSummer').checked;
|
||||
|
||||
// Carnival
|
||||
config.Carnival.EnableCarnival = document.querySelector('#EnableCarnival').checked;
|
||||
config.Carnival.ObjectCount = parseInt(document.querySelector('#CarnivalObjectCount').value);
|
||||
config.Carnival.EnableRandomCarnival = document.querySelector('#EnableRandomCarnival').checked;
|
||||
config.Carnival.EnableRandomCarnivalMobile = document.querySelector('#EnableRandomCarnivalMobile').checked;
|
||||
config.Carnival.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationCarnival').checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(SeasonalsConfigPage.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
||||
49
Jellyfin.Plugin.Seasonals/Helpers/TransformationPatches.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Jellyfin.Plugin.Seasonals.Model;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals.Helpers
|
||||
{
|
||||
public static class TransformationPatches
|
||||
{
|
||||
public static string IndexHtml(PatchRequestPayload payload)
|
||||
{
|
||||
// Always return original content if something fails or is null
|
||||
string? originalContents = payload?.Contents;
|
||||
|
||||
if (string.IsNullOrEmpty(originalContents))
|
||||
{
|
||||
return originalContents ?? string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// Safety Check: If plugin is disabled, do nothing
|
||||
if (SeasonalsPlugin.Instance?.Configuration.IsEnabled != true)
|
||||
{
|
||||
return originalContents;
|
||||
}
|
||||
|
||||
// Use StringBuilder for efficient modification
|
||||
var builder = new System.Text.StringBuilder(originalContents);
|
||||
|
||||
// Inject Script if missing
|
||||
if (!originalContents.Contains("seasonals.js", StringComparison.Ordinal))
|
||||
{
|
||||
var scriptIndex = originalContents.LastIndexOf(ScriptInjector.Marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (scriptIndex != -1)
|
||||
{
|
||||
builder.Insert(scriptIndex, ScriptInjector.ScriptTag + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// On error, return original content to avoid breaking the UI
|
||||
return originalContents;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||
<Title>Jellyfin Seasonals Plugin</Title>
|
||||
<Authors>CodeDevMLH</Authors>
|
||||
<Version>1.3.3.0</Version>
|
||||
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl>
|
||||
<Version>1.7.1.0</Version>
|
||||
<RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -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" />
|
||||
|
||||
10
Jellyfin.Plugin.Seasonals/Model/PatchRequestPayload.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals.Model
|
||||
{
|
||||
public class PatchRequestPayload
|
||||
{
|
||||
[JsonPropertyName("contents")]
|
||||
public string? Contents { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Jellyfin.Plugin.Seasonals.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals;
|
||||
|
||||
/// <summary>
|
||||
/// The main plugin.
|
||||
/// </summary>
|
||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
private readonly ScriptInjector _scriptInjector;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILoggerFactory loggerFactory)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
_scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>());
|
||||
if (!_scriptInjector.Inject())
|
||||
{
|
||||
TryRegisterFallback(loggerFactory.CreateLogger("FileTransformationFallback"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Seasonals";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current plugin instance.
|
||||
/// </summary>
|
||||
public static Plugin? Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback method for FileTransformation plugin.
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload containing the file contents.</param>
|
||||
/// <returns>The modified file contents.</returns>
|
||||
public static string TransformIndexHtml(JObject payload)
|
||||
{
|
||||
// CRITICAL: Always return original content if something fails or is null
|
||||
string? originalContents = payload?["contents"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(originalContents))
|
||||
{
|
||||
return originalContents ?? string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var html = originalContents;
|
||||
const string inject = "<script src=\"/Seasonals/Resources/seasonals.js\"></script>\n<body";
|
||||
|
||||
if (!html.Contains("seasonals.js", StringComparison.Ordinal))
|
||||
{
|
||||
return html.Replace("<body", inject, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// On error, return original content to avoid breaking the UI
|
||||
return originalContents;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRegisterFallback(ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Find the FileTransformation assembly across all load contexts
|
||||
var assembly = AssemblyLoadContext.All
|
||||
.SelectMany(x => x.Assemblies)
|
||||
.FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false);
|
||||
|
||||
if (assembly == null)
|
||||
{
|
||||
logger.LogWarning("FileTransformation plugin not found. Fallback injection skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
var type = assembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
|
||||
if (type == null)
|
||||
{
|
||||
logger.LogWarning("Jellyfin.Plugin.FileTransformation.PluginInterface not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var method = type.GetMethod("RegisterTransformation");
|
||||
if (method == null)
|
||||
{
|
||||
logger.LogWarning("RegisterTransformation method not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create JObject payload directly using Newtonsoft.Json
|
||||
var payload = new JObject
|
||||
{
|
||||
{ "id", Id.ToString() },
|
||||
{ "fileNamePattern", "index.html" },
|
||||
{ "callbackAssembly", this.GetType().Assembly.FullName },
|
||||
{ "callbackClass", this.GetType().FullName },
|
||||
{ "callbackMethod", nameof(TransformIndexHtml) }
|
||||
};
|
||||
|
||||
// Invoke RegisterTransformation with the JObject payload
|
||||
method.Invoke(null, new object[] { payload });
|
||||
logger.LogInformation("Successfully registered fallback transformation via FileTransformation plugin.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error attempting to register fallback transformation.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new PluginPageInfo
|
||||
{
|
||||
Name = Name,
|
||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
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;
|
||||
|
||||
@@ -13,8 +18,8 @@ public class ScriptInjector
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<ScriptInjector> _logger;
|
||||
private const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
|
||||
private const string Marker = "</body>";
|
||||
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.
|
||||
@@ -30,53 +35,51 @@ public class ScriptInjector
|
||||
/// <summary>
|
||||
/// Injects the script tag into index.html if it's not already present.
|
||||
/// </summary>
|
||||
/// <returns>True if injection was successful or already present, false otherwise.</returns>
|
||||
public bool Inject()
|
||||
public void Inject()
|
||||
{
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
if (string.IsNullOrEmpty(webPath))
|
||||
{
|
||||
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped.");
|
||||
return false;
|
||||
_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.", indexPath);
|
||||
return false;
|
||||
_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, StringComparison.Ordinal))
|
||||
if (!content.Contains(ScriptTag))
|
||||
{
|
||||
_logger.LogInformation("Seasonals script already injected.");
|
||||
return true;
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
// Insert before the closing body tag
|
||||
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}", StringComparison.Ordinal);
|
||||
if (string.Equals(newContent, content, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
|
||||
return false;
|
||||
}
|
||||
|
||||
File.WriteAllText(indexPath, newContent);
|
||||
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
|
||||
return true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("Access was denied when attempting to inject script into index.html. Automatic injection failed. Please ensure the Jellyfin web directory is writable by the process, or manually add the script tag: {ScriptTag}", ScriptTag);
|
||||
return false;
|
||||
_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.");
|
||||
return false;
|
||||
_logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
|
||||
RegisterFileTransformation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +88,8 @@ public class ScriptInjector
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
UnregisterFileTransformation();
|
||||
|
||||
try
|
||||
{
|
||||
var webPath = GetWebPath();
|
||||
@@ -100,21 +105,18 @@ public class ScriptInjector
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(indexPath);
|
||||
if (!content.Contains(ScriptTag, StringComparison.Ordinal))
|
||||
if (content.Contains(ScriptTag))
|
||||
{
|
||||
return;
|
||||
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.");
|
||||
}
|
||||
|
||||
// Try to remove with newline first, then just the tag to ensure clean removal
|
||||
var newContent = content.Replace($"{ScriptTag}\n", "", StringComparison.Ordinal)
|
||||
.Replace(ScriptTag, "", StringComparison.Ordinal);
|
||||
|
||||
File.WriteAllText(indexPath, newContent);
|
||||
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("Permission denied when attempting to remove script from index.html.");
|
||||
_logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -132,4 +134,74 @@ public class ScriptInjector
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Jellyfin.Plugin.Seasonals/SeasonalsPlugin.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Jellyfin.Plugin.Seasonals.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Seasonals;
|
||||
|
||||
/// <summary>
|
||||
/// The main plugin.
|
||||
/// </summary>
|
||||
public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
private readonly ScriptInjector _scriptInjector;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public SeasonalsPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILoggerFactory loggerFactory)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
_loggerFactory = loggerFactory;
|
||||
_scriptInjector = new ScriptInjector(applicationPaths, loggerFactory.CreateLogger<ScriptInjector>());
|
||||
|
||||
if (Configuration.IsEnabled)
|
||||
{
|
||||
_scriptInjector.Inject();
|
||||
}
|
||||
else
|
||||
{
|
||||
_scriptInjector.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void UpdateConfiguration(BasePluginConfiguration configuration)
|
||||
{
|
||||
var oldConfig = Configuration;
|
||||
base.UpdateConfiguration(configuration);
|
||||
|
||||
if (Configuration.IsEnabled != oldConfig.IsEnabled)
|
||||
{
|
||||
if (Configuration.IsEnabled)
|
||||
{
|
||||
_scriptInjector.Inject();
|
||||
}
|
||||
else
|
||||
{
|
||||
_scriptInjector.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Seasonals";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Guid Id => Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current plugin instance.
|
||||
/// </summary>
|
||||
public static SeasonalsPlugin? Instance { get; private set; }
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new PluginPageInfo
|
||||
{
|
||||
Name = Name,
|
||||
EnableInMainMenu = true,
|
||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
1343
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW.svg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_192x192.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Jellyfin.Plugin.Seasonals/Web/assets/logo_SW_96x96.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -48,23 +48,23 @@ observer.observe(document.body, {
|
||||
|
||||
|
||||
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",
|
||||
"../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) {
|
||||
|
||||
101
Jellyfin.Plugin.Seasonals/Web/carnival.css
Normal file
@@ -0,0 +1,101 @@
|
||||
.carnival-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
perspective: 600px;
|
||||
}
|
||||
|
||||
.carnival-wrapper {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -20px;
|
||||
will-change: top;
|
||||
animation-name: carnival-fall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.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: #f0f;
|
||||
will-change: transform;
|
||||
animation-name: carnival-flutter;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: 2s;
|
||||
}
|
||||
|
||||
.carnival-confetti.circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.carnival-confetti.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.carnival-confetti.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent !important;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 10px solid;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: inherit;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
@keyframes carnival-fall {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
100% {
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@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(1, 1, 1, 0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate3d(1, 0.5, 0, 90deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate3d(0.5, 1, 0.5, 180deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate3d(0, 0.5, 1, 270deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(1, 1, 1, 360deg);
|
||||
}
|
||||
}
|
||||
174
Jellyfin.Plugin.Seasonals/Web/carnival.js
Normal file
@@ -0,0 +1,174 @@
|
||||
const config = window.SeasonalsPluginConfig?.Carnival || {};
|
||||
|
||||
const carnival = config.EnableCarnival !== undefined ? config.EnableCarnival : true;
|
||||
const randomCarnival = config.EnableRandomCarnival !== undefined ? config.EnableRandomCarnival : true;
|
||||
const randomCarnivalMobile = config.EnableRandomCarnivalMobile !== undefined ? config.EnableRandomCarnivalMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const enableSway = config.EnableCarnivalSway !== undefined ? config.EnableCarnivalSway : true;
|
||||
const carnivalCount = config.ObjectCount || 80;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
// function to check and control the carnival animation
|
||||
function toggleCarnival() {
|
||||
const carnivalContainer = document.querySelector('.carnival-container');
|
||||
if (!carnivalContainer) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
// hide carnival if video/trailer player is active or dashboard is visible
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
carnivalContainer.style.display = 'none'; // hide carnival
|
||||
if (!msgPrinted) {
|
||||
console.log('Carnival hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
carnivalContainer.style.display = 'block'; // show carnival
|
||||
if (msgPrinted) {
|
||||
console.log('Carnival visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// observe changes in the DOM
|
||||
const observer = new MutationObserver(toggleCarnival);
|
||||
|
||||
// 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)
|
||||
});
|
||||
|
||||
const confettiColors = [
|
||||
'#fce18a', '#ff726d', '#b48def', '#f4306d',
|
||||
'#36c5f0', '#2ccc5d', '#e9b31d', '#9b59b6',
|
||||
'#3498db', '#e74c3c', '#1abc9c', '#f1c40f'
|
||||
];
|
||||
|
||||
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}%`;
|
||||
|
||||
// Random dimensions
|
||||
if (!confetti.classList.contains('circle') && !confetti.classList.contains('square') && !confetti.classList.contains('triangle')) {
|
||||
const width = Math.random() * 5 + 4;
|
||||
const height = Math.random() * 6 + 8;
|
||||
confetti.style.width = `${width}px`;
|
||||
confetti.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
// Animation settings
|
||||
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`;
|
||||
|
||||
// Random sway amplitude (using CSS variable for dynamic keyframe)
|
||||
// Sway between 30px and 100px
|
||||
const swayAmount = Math.random() * 70 + 30;
|
||||
const direction = Math.random() > 0.5 ? 1 : -1;
|
||||
swayWrapper.style.setProperty('--sway-amount', `${swayAmount * direction}px`);
|
||||
}
|
||||
|
||||
// Flutter speed variation
|
||||
confetti.style.animationDuration = `${Math.random() * 2 + 1}s`;
|
||||
|
||||
if (enableSway) {
|
||||
swayWrapper.appendChild(confetti);
|
||||
wrapper.appendChild(swayWrapper);
|
||||
} else {
|
||||
wrapper.appendChild(confetti);
|
||||
}
|
||||
|
||||
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 < 30; i++) {
|
||||
createConfettiPiece(container, true);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize carnival
|
||||
function initializeCarnival() {
|
||||
if (!carnival) return; // exit if carnival is disabled
|
||||
initCarnivalObjects();
|
||||
toggleCarnival();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomCarnival && (screenWidth > 768 || randomCarnivalMobile)) {
|
||||
addRandomCarnivalObjects(carnivalCount);
|
||||
}
|
||||
}
|
||||
|
||||
initializeCarnival();
|
||||
@@ -61,20 +61,20 @@ observer.observe(document.body, {
|
||||
|
||||
|
||||
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",
|
||||
"../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";
|
||||
const rabbit = "../Seasonals/Resources/easter_images/easter-bunny.png";
|
||||
|
||||
function addRandomEaster(count) {
|
||||
const easterContainer = document.querySelector('.easter-container'); // get the leave container
|
||||
|
||||
@@ -42,6 +42,11 @@ function toggleFirework() {
|
||||
}
|
||||
} else {
|
||||
fireworksContainer.style.display = 'block'; // show fireworks
|
||||
|
||||
if (scrollFireworks) {
|
||||
fireworksContainer.style.height = `${document.documentElement.scrollHeight}px`;
|
||||
}
|
||||
|
||||
if (msgPrinted) {
|
||||
console.log('Fireworks visible');
|
||||
startFireworks();
|
||||
@@ -117,8 +122,8 @@ function launchFirework() {
|
||||
let startY, endY;
|
||||
if (scrollFireworks) {
|
||||
// Y-position considers scrolling
|
||||
startY = window.scrollY + window.innerHeight; // Bottom edge of the window plus the scroll offset
|
||||
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2 + window.scrollY; // Area around the middle, but also with scrolling
|
||||
startY = window.scrollY + window.innerHeight;
|
||||
endY = window.scrollY + (Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2);
|
||||
} else {
|
||||
startY = window.innerHeight; // Bottom edge of the window
|
||||
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2; // Area around the middle
|
||||
@@ -133,16 +138,29 @@ function launchFirework() {
|
||||
}, 1000); // or 1200
|
||||
}
|
||||
|
||||
// Start the firework routine
|
||||
// Start the firework routine
|
||||
function startFireworks() {
|
||||
const fireworkContainer = document.querySelector('.fireworks') || document.createElement("div");
|
||||
let fireworkContainer = document.querySelector('.fireworks');
|
||||
|
||||
if (!document.querySelector('.fireworks')) {
|
||||
if (!fireworkContainer) {
|
||||
fireworkContainer = document.createElement("div");
|
||||
fireworkContainer.className = "fireworks";
|
||||
fireworkContainer.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(fireworkContainer);
|
||||
}
|
||||
|
||||
|
||||
fireworkContainer.style.position = scrollFireworks ? 'absolute' : 'fixed';
|
||||
|
||||
if (scrollFireworks) {
|
||||
fireworkContainer.style.height = `${document.documentElement.scrollHeight}px`;
|
||||
fireworkContainer.style.width = '100%';
|
||||
fireworkContainer.style.top = '0';
|
||||
fireworkContainer.style.left = '0';
|
||||
} else {
|
||||
fireworkContainer.style.height = '100%';
|
||||
fireworkContainer.style.width = '100%';
|
||||
}
|
||||
|
||||
fireworksInterval = setInterval(() => {
|
||||
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
|
||||
for (let i = 0; i < randomCount; i++) {
|
||||
|
||||
@@ -46,9 +46,9 @@ observer.observe(document.body, {
|
||||
|
||||
|
||||
const images = [
|
||||
"/Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"/Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"/Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/ghost_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/bat_20x20.png",
|
||||
"../Seasonals/Resources/halloween_images/pumpkin_20x20.png",
|
||||
];
|
||||
|
||||
function addRandomSymbols(count) {
|
||||
|
||||
59
Jellyfin.Plugin.Seasonals/Web/resurrection.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.resurrection-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resurrection-symbol {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
top: -15%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
animation-name: resurrection-fall, resurrection-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
will-change: transform, top;
|
||||
}
|
||||
|
||||
.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% {
|
||||
top: -15%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 105%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resurrection-sway {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(65px);
|
||||
}
|
||||
}
|
||||
113
Jellyfin.Plugin.Seasonals/Web/resurrection.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const config = window.SeasonalsPluginConfig?.Resurrection || {};
|
||||
|
||||
const enableResurrection = config.EnableResurrection !== undefined ? config.EnableResurrection : true;
|
||||
const enableRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
|
||||
const enableRandomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const symbolCount = config.SymbolCount || 12;
|
||||
|
||||
let animationEnabled = true;
|
||||
let statusLogged = false;
|
||||
|
||||
const images = [
|
||||
'../Seasonals/Resources/resurrection_images/crosses.png',
|
||||
'../Seasonals/Resources/resurrection_images/palm-branch.png',
|
||||
'../Seasonals/Resources/resurrection_images/draped-cross.png',
|
||||
'../Seasonals/Resources/resurrection_images/empty-tomb.png',
|
||||
'../Seasonals/Resources/resurrection_images/he-is-risen.png',
|
||||
'../Seasonals/Resources/resurrection_images/crown-of-thorns.png',
|
||||
'../Seasonals/Resources/resurrection_images/risen-lord.png',
|
||||
'../Seasonals/Resources/resurrection_images/dove.png'
|
||||
];
|
||||
|
||||
function toggleResurrection() {
|
||||
const container = document.querySelector('.resurrection-container');
|
||||
if (!container) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
animationEnabled = !(videoPlayer || trailerPlayer || isDashboard || hasUserMenu);
|
||||
container.style.display = animationEnabled ? 'block' : 'none';
|
||||
|
||||
if (!animationEnabled && !statusLogged) {
|
||||
console.log('Resurrection hidden');
|
||||
statusLogged = true;
|
||||
} else if (animationEnabled && statusLogged) {
|
||||
console.log('Resurrection visible');
|
||||
statusLogged = false;
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleResurrection);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
function createSymbol(imageSrc, leftPercent, delaySeconds) {
|
||||
const symbol = document.createElement('div');
|
||||
symbol.className = 'resurrection-symbol';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = imageSrc;
|
||||
img.alt = '';
|
||||
|
||||
symbol.style.left = `${leftPercent}%`;
|
||||
symbol.style.animationDelay = `${delaySeconds}s, ${Math.random() * 3}s`;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
const fallDuration = Math.random() * 7 + 7;
|
||||
const swayDuration = Math.random() * 4 + 2;
|
||||
symbol.style.animationDuration = `${fallDuration}s, ${swayDuration}s`;
|
||||
}
|
||||
|
||||
symbol.appendChild(img);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function addSymbols(count) {
|
||||
const container = document.querySelector('.resurrection-container');
|
||||
if (!container || !enableRandomSymbols) return;
|
||||
|
||||
const isDesktop = window.innerWidth > 768;
|
||||
if (!isDesktop && !enableRandomSymbolsMobile) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const imageSrc = images[Math.floor(Math.random() * images.length)];
|
||||
const left = Math.random() * 100;
|
||||
const delay = Math.random() * 12;
|
||||
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||
}
|
||||
}
|
||||
|
||||
function initResurrection() {
|
||||
let container = document.querySelector('.resurrection-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'resurrection-container';
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Place one of each of the 8 provided resurrection images first.
|
||||
images.forEach((imageSrc, index) => {
|
||||
const left = (index + 1) * (100 / (images.length + 1));
|
||||
const delay = Math.random() * 8;
|
||||
container.appendChild(createSymbol(imageSrc, left, delay));
|
||||
});
|
||||
|
||||
const extraCount = Math.max(symbolCount - images.length, 0);
|
||||
addSymbols(extraCount);
|
||||
}
|
||||
|
||||
function initializeResurrection() {
|
||||
if (!enableResurrection) return;
|
||||
initResurrection();
|
||||
toggleResurrection();
|
||||
}
|
||||
|
||||
initializeResurrection();
|
||||
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 |
@@ -60,6 +60,7 @@ observer.observe(document.body, {
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
|
||||
function initializeCanvas() {
|
||||
if (document.getElementById('snowfallCanvas')) {
|
||||
@@ -78,8 +79,12 @@ function initializeCanvas() {
|
||||
container.appendChild(canvas);
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
// Initial resize
|
||||
resizeCanvas(container);
|
||||
window.addEventListener('resize', () => resizeCanvas(container));
|
||||
|
||||
// Initialize ResizeObserver
|
||||
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
|
||||
resizeObserver.observe(container);
|
||||
}
|
||||
|
||||
function removeCanvas() {
|
||||
@@ -96,15 +101,37 @@ function removeCanvas() {
|
||||
animationFrameIdSanta = null;
|
||||
console.log('Santa animation frame canceled');
|
||||
}
|
||||
|
||||
// Disconnect ResizeObserver
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
|
||||
console.log('Canvas removed');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(container) {
|
||||
if (!canvas) return;
|
||||
|
||||
const oldWidth = canvas.width;
|
||||
const oldHeight = canvas.height;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
|
||||
// Scale snowflakes positions if dimensions changed (to avoid clustering)
|
||||
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
|
||||
const scaleX = canvas.width / oldWidth;
|
||||
const scaleY = canvas.height / oldHeight;
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
flake.x *= scaleX;
|
||||
flake.y *= scaleY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createSnowflakes(container) {
|
||||
@@ -154,18 +181,18 @@ 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',
|
||||
'../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';
|
||||
const santaImage = '../Seasonals/Resources/santa_images/santa.gif';
|
||||
|
||||
|
||||
function createSantaElement() {
|
||||
@@ -210,7 +237,7 @@ function reloadSantaGif() {
|
||||
|
||||
function animateSanta() {
|
||||
const santa = document.querySelector('.santa');
|
||||
|
||||
|
||||
function startAnimation() {
|
||||
const santaHeight = santa.offsetHeight;
|
||||
if (santaHeight === 0) {
|
||||
|
||||
@@ -1,206 +1,402 @@
|
||||
// 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'
|
||||
},
|
||||
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
|
||||
|
||||
if (month === 1 && day >= 10 && day <= 18) return 'hearts'; // valentine's day february 10 - 18
|
||||
init(config) {
|
||||
if (this.initialized) return;
|
||||
this.config = config;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// load theme csss
|
||||
function loadThemeCSS(cssPath) {
|
||||
if (!cssPath) return;
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
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.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() {
|
||||
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.");
|
||||
}
|
||||
},
|
||||
|
||||
getSetting(key, defaultValue) {
|
||||
const value = localStorage.getItem(`seasonals-${key}`);
|
||||
return value !== null ? value : defaultValue;
|
||||
},
|
||||
|
||||
setSetting(key, value) {
|
||||
localStorage.setItem(`seasonals-${key}`, value);
|
||||
},
|
||||
|
||||
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';
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleSettingsPopup(button);
|
||||
});
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
injectSettingsIcon() {
|
||||
const observer = new MutationObserver((mutations, obs) => {
|
||||
const headerRight = document.querySelector('.headerRight');
|
||||
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
|
||||
const icon = this.createIcon();
|
||||
headerRight.prepend(icon);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
},
|
||||
|
||||
createPopup(anchorElement) {
|
||||
const existing = document.querySelector('.seasonal-settings-popup');
|
||||
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();
|
||||
|
||||
// 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`;
|
||||
|
||||
// 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" />
|
||||
<span class="checkboxLabel">Enable Seasonals</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer" style="margin-bottom: 0.5em;">
|
||||
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
|
||||
<select id="seasonal-theme-select" class="emby-select" style="width: 100%; padding: 0.5em; background-color: #333; border: 1px solid #444; color: #fff; border-radius: 4px;">
|
||||
<option value="auto">Server-Side</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
||||
popup.innerHTML = html;
|
||||
|
||||
// Populate Select Options
|
||||
const themeSelect = popup.querySelector('#seasonal-theme-select');
|
||||
Object.keys(ThemeConfigs).forEach(key => {
|
||||
if (key === 'none') return;
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
themeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set Initial Values
|
||||
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
|
||||
enabledCheckbox.checked = this.getSetting('enabled', 'true') === 'true';
|
||||
themeSelect.value = this.getSetting('theme', 'auto');
|
||||
|
||||
// Event Listeners
|
||||
enabledCheckbox.addEventListener('change', (e) => {
|
||||
this.setSetting('enabled', e.target.checked);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
themeSelect.addEventListener('change', (e) => {
|
||||
this.setSetting('theme', e.target.value);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
const closeHandler = (e) => {
|
||||
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener('click', closeHandler);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
},
|
||||
|
||||
toggleSettingsPopup(anchorElement) {
|
||||
const existing = document.querySelector('.seasonal-settings-popup');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
} else {
|
||||
this.createPopup(anchorElement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching Seasonals config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
let currentTheme;
|
||||
if (automateThemeSelection === false) {
|
||||
currentTheme = defaultTheme;
|
||||
} else {
|
||||
currentTheme = determineCurrentTheme();
|
||||
}
|
||||
const SeasonalsManager = {
|
||||
config: null,
|
||||
|
||||
console.log(`Selected theme: ${currentTheme}`);
|
||||
async init() {
|
||||
// Fetch Config
|
||||
try {
|
||||
const response = await fetch('../Seasonals/Config');
|
||||
if (response.ok) {
|
||||
this.config = await response.json();
|
||||
window.SeasonalsPluginConfig = this.config;
|
||||
console.log('Seasonals: Seasonals Config loaded:', this.config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Seasonals: Error fetching Seasonals config:', error);
|
||||
}
|
||||
|
||||
if (!currentTheme || currentTheme === 'none') {
|
||||
console.log('No theme selected.');
|
||||
removeSelf();
|
||||
return;
|
||||
}
|
||||
// Initialize Settings UI
|
||||
SeasonalSettingsManager.init(this.config);
|
||||
|
||||
const theme = themeConfigs[currentTheme];
|
||||
// User Preference Check
|
||||
const isEnabled = SeasonalSettingsManager.getSetting('enabled', 'true') === 'true';
|
||||
if (!isEnabled) {
|
||||
console.log('Seasonals: Disabled by user preference.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine Theme
|
||||
const themeName = this.selectTheme();
|
||||
console.log(`Seasonals: Selected theme: ${themeName}`);
|
||||
|
||||
if (!themeName || themeName === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply Theme
|
||||
this.applyTheme(themeName);
|
||||
},
|
||||
|
||||
selectTheme() {
|
||||
// Check local override
|
||||
const forcedTheme = SeasonalSettingsManager.getSetting('theme', 'auto');
|
||||
if (forcedTheme !== 'auto') {
|
||||
console.log(`Seasonals: User forced theme: ${forcedTheme}`);
|
||||
return forcedTheme;
|
||||
}
|
||||
|
||||
const automate = this.config ? this.config.AutomateSeasonSelection : true;
|
||||
const defaultTheme = this.config ? this.config.SelectedSeason : 'none';
|
||||
|
||||
if (!automate) {
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
return this.determineCurrentThemeDate();
|
||||
},
|
||||
|
||||
determineCurrentThemeDate() {
|
||||
var rules = [];
|
||||
try {
|
||||
rules = JSON.parse(this.config.SeasonalRules || "[]");
|
||||
} catch (e) {
|
||||
console.error("Seasonals: Error parsing SeasonalRules", e);
|
||||
}
|
||||
|
||||
if (rules.length === 0) {
|
||||
// Fallback to empty/none if no rules are defined (though default should exist)
|
||||
console.log("Seasonals: No auto-selection rules found.");
|
||||
return 'none';
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1; // 1-12
|
||||
const day = date.getDate(); // 1-31
|
||||
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
var rule = rules[i];
|
||||
if (this.isDateInRange(day, month, rule.StartDay, rule.StartMonth, rule.EndDay, rule.EndMonth)) {
|
||||
console.log(`Seasonals: Match found for rule "${rule.Name}" (${rule.Theme})`);
|
||||
return rule.Theme;
|
||||
}
|
||||
}
|
||||
|
||||
if (!theme) {
|
||||
console.error(`Theme "${currentTheme}" not found.`);
|
||||
return;
|
||||
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);
|
||||
}
|
||||
}
|
||||
updateThemeContainer(theme.containerClass);
|
||||
};
|
||||
|
||||
if (theme.css) loadThemeCSS(theme.css);
|
||||
if (theme.js) loadThemeJS(theme.js);
|
||||
|
||||
console.log(`Theme "${currentTheme}" loaded.`);
|
||||
|
||||
removeSelf();
|
||||
}
|
||||
|
||||
|
||||
initializeTheme();
|
||||
SeasonalsManager.init();
|
||||
|
||||
@@ -55,6 +55,7 @@ observer.observe(document.body, {
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
|
||||
function initializeCanvas() {
|
||||
if (document.getElementById('snowfallCanvas')) {
|
||||
@@ -73,8 +74,12 @@ function initializeCanvas() {
|
||||
container.appendChild(canvas);
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
// Initial resize
|
||||
resizeCanvas(container);
|
||||
window.addEventListener('resize', () => resizeCanvas(container));
|
||||
|
||||
// Initialize ResizeObserver
|
||||
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
|
||||
resizeObserver.observe(container);
|
||||
}
|
||||
|
||||
function removeCanvas() {
|
||||
@@ -86,15 +91,37 @@ function removeCanvas() {
|
||||
animationFrameId = null;
|
||||
console.log('Animation frame canceled');
|
||||
}
|
||||
|
||||
// Disconnect ResizeObserver
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
|
||||
console.log('Canvas removed');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(container) {
|
||||
if (!canvas) return;
|
||||
|
||||
const oldWidth = canvas.width;
|
||||
const oldHeight = canvas.height;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
|
||||
// Scale snowflakes positions if dimensions changed (to avoid clustering)
|
||||
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
|
||||
const scaleX = canvas.width / oldWidth;
|
||||
const scaleY = canvas.height / oldHeight;
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
flake.x *= scaleX;
|
||||
flake.y *= scaleY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createSnowflakes(container) {
|
||||
|
||||
@@ -57,6 +57,7 @@ observer.observe(document.body, {
|
||||
attributes: true // observe changes to attributes (e.g. class changes)
|
||||
});
|
||||
|
||||
let resizeObserver; // Observer for resize events
|
||||
|
||||
function initializeCanvas() {
|
||||
if (document.getElementById('snowfallCanvas')) {
|
||||
@@ -75,8 +76,12 @@ function initializeCanvas() {
|
||||
container.appendChild(canvas);
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
// Initial resize
|
||||
resizeCanvas(container);
|
||||
window.addEventListener('resize', () => resizeCanvas(container));
|
||||
|
||||
// Initialize ResizeObserver
|
||||
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
|
||||
resizeObserver.observe(container);
|
||||
}
|
||||
|
||||
function removeCanvas() {
|
||||
@@ -88,15 +93,37 @@ function removeCanvas() {
|
||||
animationFrameId = null;
|
||||
console.log('Animation frame canceled');
|
||||
}
|
||||
|
||||
// Disconnect ResizeObserver
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
|
||||
console.log('Canvas removed');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(container) {
|
||||
if (!canvas) return;
|
||||
|
||||
const oldWidth = canvas.width;
|
||||
const oldHeight = canvas.height;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
|
||||
// Scale snowflakes positions if dimensions changed (to avoid clustering)
|
||||
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
|
||||
const scaleX = canvas.width / oldWidth;
|
||||
const scaleY = canvas.height / oldHeight;
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
flake.x *= scaleX;
|
||||
flake.y *= scaleY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createSnowflakes(container) {
|
||||
|
||||
179
Jellyfin.Plugin.Seasonals/Web/spring.css
Normal file
@@ -0,0 +1,179 @@
|
||||
.spring-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Petals */
|
||||
.spring-petal {
|
||||
position: fixed;
|
||||
top: -20px;
|
||||
z-index: 1005;
|
||||
width: 15px;
|
||||
height: 10px;
|
||||
background-color: #ffc0cb;
|
||||
border-radius: 15px 0px 15px 0px;
|
||||
|
||||
will-change: transform, top;
|
||||
animation-name: spring-fall, spring-sway;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-duration: 10s, 3s;
|
||||
}
|
||||
|
||||
.spring-petal.lighter {
|
||||
background-color: #ffd1dc;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.spring-petal.darker {
|
||||
background-color: #ffb7c5;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.spring-petal.type2 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 10px 0px 10px 5px;
|
||||
}
|
||||
|
||||
/* Pollen */
|
||||
.spring-pollen {
|
||||
position: fixed;
|
||||
z-index: 14;
|
||||
background-color: #fffacd;
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
box-shadow: 0 0 4px rgba(255, 250, 205, 0.4);
|
||||
|
||||
will-change: transform;
|
||||
animation-name: spring-float;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/* Sunbeams */
|
||||
.spring-sunbeam {
|
||||
position: fixed;
|
||||
top: -50%;
|
||||
height: 200%;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 200, 0.08) 50%, rgba(255, 255, 255, 0));
|
||||
z-index: 5;
|
||||
transform-origin: top center;
|
||||
pointer-events: none;
|
||||
|
||||
animation-name: spring-beam-pulse;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/* Grass */
|
||||
.spring-grass-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
z-index: 1002;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spring-grass {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
border-radius: 100% 0 0 0;
|
||||
transform-origin: bottom center;
|
||||
background-color: #4caf50;
|
||||
|
||||
will-change: transform;
|
||||
animation-name: spring-grass-sway;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/* Ladybugs */
|
||||
.spring-ladybug {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 4px;
|
||||
background-color: #e74c3c; /* Red */
|
||||
border-radius: 3px 3px 0 0;
|
||||
z-index: 1003;
|
||||
|
||||
will-change: left, transform;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.spring-ladybug.right {
|
||||
animation-name: spring-bug-crawl-right;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.spring-ladybug.left {
|
||||
animation-name: spring-bug-crawl-left;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.spring-ladybug::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 1px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes spring-fall {
|
||||
0% { top: -10%; }
|
||||
100% { top: 100%; }
|
||||
}
|
||||
|
||||
@keyframes spring-sway {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(30px) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spring-float {
|
||||
0% { transform: translateX(0) translateY(0); }
|
||||
25% { transform: translateX(20px) translateY(-10px); }
|
||||
50% { transform: translateX(40px) translateY(0); }
|
||||
75% { transform: translateX(20px) translateY(10px); }
|
||||
100% { transform: translateX(0) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes spring-beam-pulse {
|
||||
0% { opacity: 0.3; transform: rotate(45deg) scaleX(1); }
|
||||
50% { opacity: 0.6; transform: rotate(45deg) scaleX(1.1); }
|
||||
100% { opacity: 0.3; transform: rotate(45deg) scaleX(1); }
|
||||
}
|
||||
|
||||
@keyframes spring-grass-sway {
|
||||
0% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(8deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes spring-bug-crawl-right {
|
||||
0% { left: -5%; }
|
||||
100% { left: 105%; }
|
||||
}
|
||||
|
||||
@keyframes spring-bug-crawl-left {
|
||||
0% { left: 105%; }
|
||||
100% { left: -5%; }
|
||||
}
|
||||
245
Jellyfin.Plugin.Seasonals/Web/spring.js
Normal file
@@ -0,0 +1,245 @@
|
||||
const config = window.SeasonalsPluginConfig?.Spring || {};
|
||||
|
||||
const spring = config.EnableSpring !== undefined ? config.EnableSpring : true;
|
||||
const petalCount = config.PetalCount || 25;
|
||||
const pollenCount = config.PollenCount || 15;
|
||||
const ladybugCountConfig = config.LadybugCount || 5;
|
||||
const sunbeamCount = config.SunbeamCount || 5;
|
||||
|
||||
const randomSpring = config.EnableRandomSpring !== undefined ? config.EnableRandomSpring : true;
|
||||
const randomSpringMobile = config.EnableRandomSpringMobile !== undefined ? config.EnableRandomSpringMobile : false;
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
|
||||
const enablePetals = config.EnableSpringPetals !== undefined ? config.EnableSpringPetals : true;
|
||||
const enableSunbeams = config.EnableSpringSunbeams !== undefined ? config.EnableSpringSunbeams : true;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleSpring() {
|
||||
const springContainer = document.querySelector('.spring-container');
|
||||
if (!springContainer) 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) {
|
||||
springContainer.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('Spring hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
springContainer.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Spring visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleSpring);
|
||||
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
||||
|
||||
function createPetal(container) {
|
||||
if (!enablePetals) return;
|
||||
|
||||
const petal = document.createElement('div');
|
||||
petal.classList.add('spring-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 createPollen(container) {
|
||||
const pollen = document.createElement('div');
|
||||
pollen.classList.add('spring-pollen');
|
||||
|
||||
const startY = Math.random() * 60 + 20;
|
||||
pollen.style.top = `${startY}%`;
|
||||
pollen.style.left = `${Math.random() * 100}%`;
|
||||
|
||||
const size = Math.random() * 3 + 1;
|
||||
pollen.style.width = `${size}px`;
|
||||
pollen.style.height = `${size}px`;
|
||||
|
||||
const duration = Math.random() * 20 + 20;
|
||||
pollen.style.animationDuration = `${duration}s`;
|
||||
pollen.style.animationDelay = `-${Math.random() * 20}s`;
|
||||
|
||||
container.appendChild(pollen);
|
||||
}
|
||||
|
||||
function createSunbeam(container) {
|
||||
if (!enableSunbeams) return;
|
||||
|
||||
const beam = document.createElement('div');
|
||||
beam.classList.add('spring-sunbeam');
|
||||
|
||||
const left = Math.random() * 100; // Spread across full width
|
||||
beam.style.left = `${left}%`;
|
||||
|
||||
// Thinner beams as requested
|
||||
const width = Math.random() * 20 + 10; // 10-30px wide
|
||||
beam.style.width = `${width}px`;
|
||||
|
||||
const rotate = Math.random() * 20 - 10 + 45;
|
||||
beam.style.transform = `rotate(${rotate}deg)`;
|
||||
|
||||
const duration = Math.random() * 10 + 10;
|
||||
beam.style.animationDuration = `${duration}s`;
|
||||
beam.style.animationDelay = `-${Math.random() * 10}s`;
|
||||
|
||||
container.appendChild(beam);
|
||||
}
|
||||
|
||||
function createGrass(container) {
|
||||
let grassContainer = container.querySelector('.spring-grass-container');
|
||||
if (!grassContainer) {
|
||||
grassContainer = document.createElement('div');
|
||||
grassContainer.className = 'spring-grass-container';
|
||||
container.appendChild(grassContainer);
|
||||
}
|
||||
|
||||
// More grass: 1 blade every 3px (was 15px)
|
||||
const bladeCount = window.innerWidth / 3;
|
||||
for (let i = 0; i < bladeCount; i++) {
|
||||
const blade = document.createElement('div');
|
||||
blade.classList.add('spring-grass');
|
||||
|
||||
const height = Math.random() * 40 + 20; // 20-60px height
|
||||
blade.style.height = `${height}px`;
|
||||
blade.style.left = `${i * 3 + Math.random() * 2}px`;
|
||||
|
||||
const duration = Math.random() * 2 + 3;
|
||||
blade.style.animationDuration = `${duration}s`;
|
||||
blade.style.animationDelay = `-${Math.random() * 5}s`;
|
||||
|
||||
const hue = 100 + Math.random() * 40;
|
||||
blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`;
|
||||
|
||||
grassContainer.appendChild(blade);
|
||||
}
|
||||
|
||||
// Add Ladybugs
|
||||
const bugs = ladybugCountConfig;
|
||||
for (let i = 0; i < bugs; i++) {
|
||||
createLadybug(grassContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function createLadybug(container) {
|
||||
const bug = document.createElement('div');
|
||||
bug.classList.add('spring-ladybug');
|
||||
|
||||
const direction = Math.random() > 0.5 ? 'right' : 'left';
|
||||
bug.classList.add(direction);
|
||||
|
||||
// Position lower (bottom of grass), but ensure visibility
|
||||
const bottomOffset = direction === 'right' ? Math.random() * 5 + 6 : Math.random() * 5 + 2;
|
||||
bug.style.bottom = `${bottomOffset}px`;
|
||||
|
||||
// Start position depends on direction
|
||||
if (direction === 'right') {
|
||||
bug.style.left = '-5%'; // Start off-screen left
|
||||
} else {
|
||||
bug.style.left = '105%'; // Start off-screen right
|
||||
}
|
||||
|
||||
const duration = Math.random() * 20 + 20; // Slow crawl
|
||||
bug.style.animationDuration = `${duration}s`;
|
||||
bug.style.animationDelay = `-${Math.random() * 20}s`;
|
||||
|
||||
container.appendChild(bug);
|
||||
}
|
||||
|
||||
function addRandomSpringObjects() {
|
||||
const container = document.querySelector('.spring-container');
|
||||
if (!container) return;
|
||||
|
||||
if (enablePetals) {
|
||||
for (let i = 0; i < petalCount; i++) {
|
||||
createPetal(container);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < pollenCount; i++) {
|
||||
createPollen(container);
|
||||
}
|
||||
}
|
||||
|
||||
function initSpringObjects() {
|
||||
let container = document.querySelector('.spring-container');
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "spring-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
createGrass(container);
|
||||
|
||||
if (enablePetals) {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const petal = document.createElement('div');
|
||||
petal.classList.add('spring-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 swayDuration = Math.random() * 2 + 2;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
petal.style.animationDuration = `${duration}s, ${swayDuration}s`;
|
||||
}
|
||||
petal.style.animationDelay = `-${Math.random() * 10}s, -${Math.random() * 3}s`;
|
||||
container.appendChild(petal);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableSunbeams) {
|
||||
// Initial sunbeams
|
||||
for (let i = 0; i < sunbeamCount; i++) {
|
||||
createSunbeam(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeSpring() {
|
||||
if (!spring) return;
|
||||
initSpringObjects();
|
||||
toggleSpring();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomSpring && (screenWidth > 768 || randomSpringMobile)) {
|
||||
addRandomSpringObjects();
|
||||
}
|
||||
}
|
||||
|
||||
initializeSpring();
|
||||
80
Jellyfin.Plugin.Seasonals/Web/summer.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.summer-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.summer-bubble {
|
||||
position: fixed;
|
||||
bottom: -50px;
|
||||
z-index: 15;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1) 60%, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
|
||||
|
||||
will-change: transform, bottom;
|
||||
animation-name: summer-rise, summer-wobble;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-duration: 10s, 3s;
|
||||
}
|
||||
|
||||
.summer-dust {
|
||||
position: fixed;
|
||||
bottom: -10px;
|
||||
z-index: 12;
|
||||
background-color: rgba(255, 223, 186, 0.6);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 5px rgba(255, 223, 186, 0.4);
|
||||
|
||||
will-change: transform, bottom;
|
||||
animation-name: summer-rise, summer-drift;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-duration: 20s, 5s;
|
||||
}
|
||||
|
||||
@keyframes summer-rise {
|
||||
0% {
|
||||
bottom: -10%;
|
||||
}
|
||||
100% {
|
||||
bottom: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes summer-wobble {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
33% {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
66% {
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes summer-drift {
|
||||
0% {
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(30px) translateY(-20px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
143
Jellyfin.Plugin.Seasonals/Web/summer.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const config = window.SeasonalsPluginConfig?.Summer || {};
|
||||
|
||||
const summer = config.EnableSummer !== undefined ? config.EnableSummer : true; // enable/disable summer
|
||||
const bubbleCount = config.BubbleCount || 20;
|
||||
const dustCount = config.DustCount || 50;
|
||||
const randomSummer = config.EnableRandomSummer !== undefined ? config.EnableRandomSummer : true; // enable random objects
|
||||
const randomSummerMobile = config.EnableRandomSummerMobile !== undefined ? config.EnableRandomSummerMobile : false; // enable random objects on mobile
|
||||
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different animation duration
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleSummer() {
|
||||
const summerContainer = document.querySelector('.summer-container');
|
||||
if (!summerContainer) 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) {
|
||||
summerContainer.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('Summer hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
summerContainer.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('Summer visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleSummer);
|
||||
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
||||
|
||||
function createBubble(container, isDust = false) {
|
||||
const bubble = document.createElement('div');
|
||||
|
||||
if (isDust) {
|
||||
bubble.classList.add('summer-dust');
|
||||
} else {
|
||||
bubble.classList.add('summer-bubble');
|
||||
}
|
||||
|
||||
// Random horizontal position
|
||||
const randomLeft = Math.random() * 100;
|
||||
bubble.style.left = `${randomLeft}%`;
|
||||
|
||||
// Random size
|
||||
if (!isDust) {
|
||||
const size = Math.random() * 20 + 10; // 10-30px bubbles
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
} else {
|
||||
const size = Math.random() * 3 + 1; // 1-4px dust
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
}
|
||||
|
||||
// Animation properties
|
||||
const duration = isDust ? (Math.random() * 20 + 10) : (Math.random() * 10 + 5); // Dust is slower
|
||||
const delay = Math.random() * 10;
|
||||
|
||||
if (enableDifferentDuration) {
|
||||
bubble.style.animationDuration = `${duration}s`;
|
||||
}
|
||||
bubble.style.animationDelay = `${delay}s`;
|
||||
|
||||
container.appendChild(bubble);
|
||||
}
|
||||
|
||||
function addRandomSummerObjects() {
|
||||
const container = document.querySelector('.summer-container');
|
||||
if (!container) return;
|
||||
|
||||
// Add bubbles
|
||||
for (let i = 0; i < bubbleCount; i++) {
|
||||
createBubble(container, false);
|
||||
}
|
||||
|
||||
// Add some dust particles (more of them, they are subtle)
|
||||
for (let i = 0; i < dustCount; i++) {
|
||||
createBubble(container, true);
|
||||
}
|
||||
}
|
||||
|
||||
function initSummerObjects() {
|
||||
let container = document.querySelector('.summer-container');
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "summer-container";
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Initial bubbles/dust
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const bubble = document.createElement('div');
|
||||
const isDust = Math.random() > 0.5;
|
||||
if (isDust) {
|
||||
bubble.classList.add('summer-dust');
|
||||
} else {
|
||||
bubble.classList.add('summer-bubble');
|
||||
}
|
||||
|
||||
const randomLeft = Math.random() * 100;
|
||||
bubble.style.left = `${randomLeft}%`;
|
||||
|
||||
if (!isDust) {
|
||||
const size = Math.random() * 20 + 10;
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
} else {
|
||||
const size = Math.random() * 3 + 1;
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
}
|
||||
|
||||
const duration = isDust ? (Math.random() * 20 + 10) : (Math.random() * 10 + 5);
|
||||
if (enableDifferentDuration) {
|
||||
bubble.style.animationDuration = `${duration}s`;
|
||||
}
|
||||
|
||||
bubble.style.animationDelay = `-${Math.random() * 10}s`;
|
||||
container.appendChild(bubble);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeSummer() {
|
||||
if (!summer) return;
|
||||
initSummerObjects();
|
||||
toggleSummer();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
if (randomSummer && (screenWidth > 768 || randomSummerMobile)) {
|
||||
addRandomSummerObjects();
|
||||
}
|
||||
}
|
||||
|
||||
initializeSummer();
|
||||
506
Jellyfin.Plugin.Seasonals/Web/test-site.html
Normal file
@@ -0,0 +1,506 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Seasonals Theme Tester</title>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background-color: #101010;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* ── Mock Jellyfin Header ── */
|
||||
.skinHeader {
|
||||
background-color: #181818;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.5em;
|
||||
border-bottom: 1px solid #282828;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.skinHeader-content { font-size: 1.1em; font-weight: 500; }
|
||||
|
||||
.headerRight {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.paper-icon-button-light {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.paper-icon-button-light:hover { background: rgba(255,255,255,0.1); }
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ── Control Panel ── */
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1px solid #333;
|
||||
padding: 1em 1.5em;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-panel label {
|
||||
font-size: 0.85em;
|
||||
color: #aaa;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.control-panel select,
|
||||
.control-panel input[type="text"] {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 0.5em 0.75em;
|
||||
font-size: 0.9em;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.control-panel select:focus,
|
||||
.control-panel input[type="text"]:focus {
|
||||
border-color: #00a4dc;
|
||||
}
|
||||
|
||||
.control-panel select { min-width: 160px; }
|
||||
.control-panel input[type="text"] { width: 160px; }
|
||||
|
||||
.control-panel button {
|
||||
background: #00a4dc;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.control-panel button:hover { background: #008bbd; }
|
||||
|
||||
.control-panel button.btn-secondary {
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.control-panel button.btn-secondary:hover { background: #444; }
|
||||
|
||||
.custom-fields {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.custom-fields.visible { display: flex; }
|
||||
|
||||
/* ── Mock Content ── */
|
||||
.mock-content {
|
||||
padding: 2em 1.5em 6em;
|
||||
}
|
||||
|
||||
.mock-content h2 {
|
||||
font-size: 1.4em;
|
||||
margin-bottom: 1em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mock-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.mock-card {
|
||||
background: #1a1a1a;
|
||||
border-radius: 8px;
|
||||
aspect-ratio: 2/3;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mock-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40%;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
position: fixed;
|
||||
bottom: 65px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 164, 220, 0.15);
|
||||
border-top: 1px solid rgba(0, 164, 220, 0.3);
|
||||
padding: 0.5em 1.5em;
|
||||
z-index: 199;
|
||||
font-size: 0.8em;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-bar a {
|
||||
color: #00a4dc;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Mock Header -->
|
||||
<div class="skinHeader">
|
||||
<div class="skinHeader-content">
|
||||
<span>Jellyfin</span>
|
||||
</div>
|
||||
<div class="headerRight">
|
||||
<button class="paper-icon-button-light" title="Search">
|
||||
<span class="material-icons">search</span>
|
||||
</button>
|
||||
<button class="paper-icon-button-light" title="User">
|
||||
<span class="material-icons">person</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seasonals Container (themes inject here) -->
|
||||
<div class="seasonals-container"></div>
|
||||
|
||||
<!-- Mock Library Content -->
|
||||
<div class="mock-content">
|
||||
<h2>My Media</h2>
|
||||
<div class="mock-grid">
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
<div class="mock-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Bar -->
|
||||
<div class="info-bar">
|
||||
📖 See <a href="../../CONTRIBUTING.md">CONTRIBUTING.md</a> for how to create your own seasonal theme
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="control-panel">
|
||||
<label for="theme-select">Theme:</label>
|
||||
<select id="theme-select">
|
||||
<option value="" selected disabled>— Select a theme —</option>
|
||||
<option value="snowfall">Snowfall</option>
|
||||
<option value="snowflakes">Snowflakes</option>
|
||||
<option value="snowstorm">Snowstorm</option>
|
||||
<option value="fireworks">Fireworks</option>
|
||||
<option value="halloween">Halloween</option>
|
||||
<option value="hearts">Hearts</option>
|
||||
<option value="christmas">Christmas</option>
|
||||
<option value="santa">Santa</option>
|
||||
<option value="autumn">Autumn</option>
|
||||
<option value="easter">Easter</option>
|
||||
<option value="resurrection">Resurrection</option>
|
||||
<option value="spring">Spring</option>
|
||||
<option value="summer">Summer (Bubbles)</option>
|
||||
<option value="carnival">Carnival (Confetti)</option>
|
||||
<option value="custom">⚙ Custom (Local Files)</option>
|
||||
</select>
|
||||
|
||||
<div id="custom-fields" class="custom-fields">
|
||||
<p>Javascript:</p>
|
||||
<input type="text" id="custom-js" placeholder="mytheme.js">
|
||||
<p>CSS:</p>
|
||||
<input type="text" id="custom-css" placeholder="mytheme.css">
|
||||
</div>
|
||||
|
||||
<button id="btn-load" onclick="loadTheme()">Load Theme</button>
|
||||
<button id="btn-clear" class="btn-secondary" onclick="clearTheme()">Clear & Reload</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Path Rewriter ──────────────────────────────────────────
|
||||
// Theme JS files reference images using the production path:
|
||||
// ../Seasonals/Resources/santa_images/gift1.png
|
||||
// When testing locally, the images live next to this HTML file:
|
||||
// ./santa_images/gift1.png
|
||||
// This observer intercepts all new <img> elements and rewrites
|
||||
// their src attribute so image-based themes (santa, halloween,
|
||||
// autumn, easter, resurrection) work out of the box.
|
||||
const PRODUCTION_PREFIX = '../Seasonals/Resources/';
|
||||
const LOCAL_PREFIX = './';
|
||||
|
||||
function rewritePath(src) {
|
||||
if (!src) return src;
|
||||
// Handle both full URLs and relative paths
|
||||
const idx = src.indexOf('Seasonals/Resources/');
|
||||
if (idx !== -1) {
|
||||
return LOCAL_PREFIX + src.substring(idx + 'Seasonals/Resources/'.length);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
function rewriteElement(el) {
|
||||
if (el.tagName === 'IMG' && el.src && el.src.includes('Seasonals/Resources/')) {
|
||||
const newSrc = rewritePath(el.src);
|
||||
console.log(`[Path Rewriter] ${el.src} → ${newSrc}`);
|
||||
el.src = newSrc;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for dynamically added images and rewrite their paths
|
||||
const pathRewriter = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== 1) continue; // skip non-elements
|
||||
rewriteElement(node);
|
||||
// Also check children (e.g. a div with img inside)
|
||||
if (node.querySelectorAll) {
|
||||
node.querySelectorAll('img').forEach(rewriteElement);
|
||||
}
|
||||
}
|
||||
// Also catch src attribute changes on existing elements
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
|
||||
rewriteElement(mutation.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pathRewriter.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['src']
|
||||
});
|
||||
|
||||
// ── Built-in theme map (local file paths for testing) ──
|
||||
const themes = {
|
||||
snowfall: { css: 'snowfall.css', js: 'snowfall.js', container: 'snowfall-container' },
|
||||
snowflakes: { css: 'snowflakes.css', js: 'snowflakes.js', container: 'snowflakes' },
|
||||
snowstorm: { css: 'snowstorm.css', js: 'snowstorm.js', container: 'snowstorm-container' },
|
||||
fireworks: { css: 'fireworks.css', js: 'fireworks.js', container: 'fireworks' },
|
||||
halloween: { css: 'halloween.css', js: 'halloween.js', container: 'halloween-container' },
|
||||
hearts: { css: 'hearts.css', js: 'hearts.js', container: 'hearts-container' },
|
||||
christmas: { css: 'christmas.css', js: 'christmas.js', container: 'christmas-container' },
|
||||
santa: { css: 'santa.css', js: 'santa.js', container: 'santa-container' },
|
||||
autumn: { css: 'autumn.css', js: 'autumn.js', container: 'autumn-container' },
|
||||
easter: { css: 'easter.css', js: 'easter.js', container: 'easter-container' },
|
||||
resurrection: { css: 'resurrection.css', js: 'resurrection.js', container: 'resurrection-container' },
|
||||
spring: { css: 'spring.css', js: 'spring.js', container: 'spring-container' },
|
||||
summer: { css: 'summer.css', js: 'summer.js', container: 'summer-container' },
|
||||
carnival: { css: 'carnival.css', js: 'carnival.js', container: 'carnival-container' },
|
||||
};
|
||||
|
||||
const select = document.getElementById('theme-select');
|
||||
const customFields = document.getElementById('custom-fields');
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
customFields.classList.toggle('visible', select.value === 'custom');
|
||||
});
|
||||
|
||||
function clearTheme() {
|
||||
// Remove injected CSS
|
||||
document.querySelectorAll('link[data-seasonal]').forEach(el => el.remove());
|
||||
// Remove injected JS
|
||||
document.querySelectorAll('script[data-seasonal]').forEach(el => el.remove());
|
||||
|
||||
// Reset the seasonals container
|
||||
const container = document.querySelector('.seasonals-container');
|
||||
container.className = 'seasonals-container';
|
||||
container.innerHTML = '';
|
||||
|
||||
// Remove any theme-created containers on body
|
||||
const knownContainers = [
|
||||
'.snowfall-container', '.snowflakes', '.snowstorm-container',
|
||||
'.fireworks', '.halloween-container', '.hearts-container',
|
||||
'.christmas-container', '.santa-container', '.autumn-container',
|
||||
'.christmas-container', '.santa-container', '.autumn-container',
|
||||
'.easter-container', '.resurrection-container', '.spring-container',
|
||||
'.summer-container', '.carnival-container'
|
||||
];
|
||||
knownContainers.forEach(sel => {
|
||||
document.querySelectorAll(sel).forEach(el => {
|
||||
if (!el.classList.contains('seasonals-container')) el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Remove any canvas elements left over
|
||||
document.querySelectorAll('#snowfallCanvas, #snowstormCanvas').forEach(el => el.remove());
|
||||
|
||||
// Remove rabbit element
|
||||
document.querySelectorAll('#rabbit, .hopping-rabbit').forEach(el => el.remove());
|
||||
|
||||
// Remove santa element
|
||||
document.querySelectorAll('.santa, .present').forEach(el => el.remove());
|
||||
|
||||
console.log('[Test Site] Theme cleared.');
|
||||
}
|
||||
|
||||
// Track active animation frames and observers for cleanup
|
||||
let activeAnimationFrames = [];
|
||||
let activeBlobUrls = [];
|
||||
|
||||
// Patch requestAnimationFrame and MutationObserver to track them
|
||||
const origRAF = window.requestAnimationFrame;
|
||||
const origCAF = window.cancelAnimationFrame;
|
||||
let trackingEnabled = false;
|
||||
|
||||
window.requestAnimationFrame = function(cb) {
|
||||
const id = origRAF.call(window, cb);
|
||||
if (trackingEnabled) activeAnimationFrames.push(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
window.cancelAnimationFrame = function(id) {
|
||||
origCAF.call(window, id);
|
||||
activeAnimationFrames = activeAnimationFrames.filter(f => f !== id);
|
||||
};
|
||||
|
||||
// Track MutationObservers created by themes
|
||||
let activeObservers = [];
|
||||
const OrigMO = window.MutationObserver;
|
||||
window.MutationObserver = class extends OrigMO {
|
||||
constructor(cb) {
|
||||
super(cb);
|
||||
if (trackingEnabled) activeObservers.push(this);
|
||||
}
|
||||
};
|
||||
|
||||
// Track intervals created by themes
|
||||
let activeIntervals = [];
|
||||
const origSetInterval = window.setInterval;
|
||||
const origClearInterval = window.clearInterval;
|
||||
window.setInterval = function(...args) {
|
||||
const id = origSetInterval.apply(window, args);
|
||||
if (trackingEnabled) activeIntervals.push(id);
|
||||
return id;
|
||||
};
|
||||
window.clearInterval = function(id) {
|
||||
origClearInterval.call(window, id);
|
||||
activeIntervals = activeIntervals.filter(i => i !== id);
|
||||
};
|
||||
|
||||
function loadTheme() {
|
||||
clearTheme();
|
||||
|
||||
// Cancel all tracked animation frames
|
||||
activeAnimationFrames.forEach(id => origCAF.call(window, id));
|
||||
activeAnimationFrames = [];
|
||||
|
||||
// Disconnect all tracked MutationObservers
|
||||
activeObservers.forEach(obs => obs.disconnect());
|
||||
activeObservers = [];
|
||||
|
||||
// Clear all tracked intervals
|
||||
activeIntervals.forEach(id => origClearInterval.call(window, id));
|
||||
activeIntervals = [];
|
||||
|
||||
// Revoke old blob URLs
|
||||
activeBlobUrls.forEach(url => URL.revokeObjectURL(url));
|
||||
activeBlobUrls = [];
|
||||
|
||||
const value = select.value;
|
||||
if (!value || value === '') return;
|
||||
|
||||
let cssFile, jsFile, containerClass;
|
||||
|
||||
if (value === 'custom') {
|
||||
cssFile = document.getElementById('custom-css').value.trim();
|
||||
jsFile = document.getElementById('custom-js').value.trim();
|
||||
containerClass = cssFile ? cssFile.replace('.css', '-container') : 'custom-container';
|
||||
} else {
|
||||
const theme = themes[value];
|
||||
cssFile = theme.css;
|
||||
jsFile = theme.js;
|
||||
containerClass = theme.container;
|
||||
}
|
||||
|
||||
// Update the seasonals-container class
|
||||
const container = document.querySelector('.seasonals-container');
|
||||
container.className = `seasonals-container ${containerClass}`;
|
||||
|
||||
// Inject CSS
|
||||
if (cssFile) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = cssFile;
|
||||
link.setAttribute('data-seasonal', 'true');
|
||||
link.onerror = () => console.error(`[Test Site] Failed to load CSS: ${cssFile}`);
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// Inject JS wrapped in IIFE to avoid const redeclaration errors
|
||||
if (jsFile) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const response = await fetch(jsFile);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const code = await response.text();
|
||||
|
||||
// Wrap in IIFE so each theme has its own scope
|
||||
const wrappedCode = `(function() {\n${code}\n})();`;
|
||||
const blob = new Blob([wrappedCode], { type: 'application/javascript' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
activeBlobUrls.push(blobUrl);
|
||||
|
||||
trackingEnabled = true;
|
||||
const script = document.createElement('script');
|
||||
script.src = blobUrl;
|
||||
script.setAttribute('data-seasonal', 'true');
|
||||
script.onerror = () => console.error(`[Test Site] Failed to execute JS: ${jsFile}`);
|
||||
document.body.appendChild(script);
|
||||
console.log(`[Test Site] Loaded theme: ${value} (${jsFile}) [IIFE-wrapped]`);
|
||||
} catch (err) {
|
||||
console.error(`[Test Site] Failed to load JS: ${jsFile}`, err);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,624 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v9.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v9.0": {
|
||||
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Controller": "10.11.0",
|
||||
"Jellyfin.Model": "10.11.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Jellyfin.Plugin.Seasonals.dll": {}
|
||||
}
|
||||
},
|
||||
"BitFaster.Caching/2.5.4": {
|
||||
"runtime": {
|
||||
"lib/net6.0/BitFaster.Caching.dll": {
|
||||
"assemblyVersion": "2.5.4.0",
|
||||
"fileVersion": "2.5.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Diacritics/4.0.17": {
|
||||
"runtime": {
|
||||
"lib/net9.0/Diacritics.dll": {
|
||||
"assemblyVersion": "4.0.17.0",
|
||||
"fileVersion": "4.0.17.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ICU4N/60.1.0-alpha.356": {
|
||||
"dependencies": {
|
||||
"J2N": "2.0.0",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/ICU4N.dll": {
|
||||
"assemblyVersion": "60.0.0.0",
|
||||
"fileVersion": "60.1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ICU4N.Transliterator/60.1.0-alpha.356": {
|
||||
"dependencies": {
|
||||
"ICU4N": "60.1.0-alpha.356"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/ICU4N.Transliterator.dll": {
|
||||
"assemblyVersion": "60.0.0.0",
|
||||
"fileVersion": "60.1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"J2N/2.0.0": {
|
||||
"runtime": {
|
||||
"lib/net6.0/J2N.dll": {
|
||||
"assemblyVersion": "2.0.0.0",
|
||||
"fileVersion": "2.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Common/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Model": "10.11.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/MediaBrowser.Common.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Controller/10.11.0": {
|
||||
"dependencies": {
|
||||
"BitFaster.Caching": "2.5.4",
|
||||
"Jellyfin.Common": "10.11.0",
|
||||
"Jellyfin.MediaEncoding.Keyframes": "10.11.0",
|
||||
"Jellyfin.Model": "10.11.0",
|
||||
"Jellyfin.Naming": "10.11.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.10",
|
||||
"System.Threading.Tasks.Dataflow": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/MediaBrowser.Controller.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Data/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Database.Implementations": "10.11.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.Data.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Database.Implementations/10.11.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Relational": "9.0.10",
|
||||
"Polly": "8.6.4"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.Database.Implementations.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Extensions/10.11.0": {
|
||||
"dependencies": {
|
||||
"Diacritics": "4.0.17",
|
||||
"ICU4N.Transliterator": "60.1.0-alpha.356"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.Extensions.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"NEbml": "1.1.0.5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.MediaEncoding.Keyframes.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Model/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Data": "10.11.0",
|
||||
"Jellyfin.Extensions": "10.11.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Text.Json": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/MediaBrowser.Model.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Naming/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Common": "10.11.0",
|
||||
"Jellyfin.Model": "10.11.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Emby.Naming.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.10",
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.10",
|
||||
"Microsoft.Extensions.Logging": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
|
||||
"assemblyVersion": "9.0.10.0",
|
||||
"fileVersion": "9.0.1025.47514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.10.0",
|
||||
"fileVersion": "9.0.1025.47514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {},
|
||||
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "9.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.10",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Logging": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
||||
"assemblyVersion": "9.0.10.0",
|
||||
"fileVersion": "9.0.1025.47514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Options": "9.0.10",
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Caching.Memory.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Configuration.Binder.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.10",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Options": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Logging.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Options.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives/9.0.10": {
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Primitives.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/1.1.0": {},
|
||||
"Microsoft.NETCore.Targets/1.1.0": {},
|
||||
"NEbml/1.1.0.5": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/NEbml.Core.dll": {
|
||||
"assemblyVersion": "1.1.0.5",
|
||||
"fileVersion": "1.1.0.5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Polly/8.6.4": {
|
||||
"dependencies": {
|
||||
"Polly.Core": "8.6.4"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/Polly.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.6.4.5033"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Polly.Core/8.6.4": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Polly.Core.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.6.4.5033"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Globalization/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Json/9.0.10": {},
|
||||
"System.Threading.Tasks.Dataflow/9.0.10": {}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"BitFaster.Caching/2.5.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ==",
|
||||
"path": "bitfaster.caching/2.5.4",
|
||||
"hashPath": "bitfaster.caching.2.5.4.nupkg.sha512"
|
||||
},
|
||||
"Diacritics/4.0.17": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-FmMvVQRsfon+x5P+dxz4mvV8wt45xr25EAOCkuo/Cjtc7lVYV5cZUSsNXwmKQpwO+TokIHpzxb8ENpqrm4yBlQ==",
|
||||
"path": "diacritics/4.0.17",
|
||||
"hashPath": "diacritics.4.0.17.nupkg.sha512"
|
||||
},
|
||||
"ICU4N/60.1.0-alpha.356": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-YMZtDnjcqWzziOKiE7w6Ma7Rl5vuFDxzOsUlHh1QyfghbNEIZQOLRs9MMfwCWAjX6n9UitrF6vLXy55Z5q+4Fg==",
|
||||
"path": "icu4n/60.1.0-alpha.356",
|
||||
"hashPath": "icu4n.60.1.0-alpha.356.nupkg.sha512"
|
||||
},
|
||||
"ICU4N.Transliterator/60.1.0-alpha.356": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-lFOSO6bbEtB6HkWMNDJAq+rFwVyi9g6xVc5O/2xHa6iZnV7wLVDqCbaQ4W4vIeBSQZAafqhxciaEkmAvSdzlCg==",
|
||||
"path": "icu4n.transliterator/60.1.0-alpha.356",
|
||||
"hashPath": "icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512"
|
||||
},
|
||||
"J2N/2.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-M5bwDajAARZiyqupU+rHQJnsVLxNBOHJ8vKYHd8LcLIb1FgLfzzcJvc31Qo5Xz/GEHFjDF9ScjKL/ks/zRTXuA==",
|
||||
"path": "j2n/2.0.0",
|
||||
"hashPath": "j2n.2.0.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Common/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-TitN7+qWFt2l0V5b+KTRt7ABDCvfZdvSC6qBG1uHS18Y80xrbrSCJ9O6BH/of310h6a4lxKlQjUtTPHCzeG2AA==",
|
||||
"path": "jellyfin.common/10.11.0",
|
||||
"hashPath": "jellyfin.common.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Controller/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-WV+PQy9AHdZLvYqUsNq6ZyQoxaiaEWLz0EwZGOiu8xSrepQLFope2U1VFHVCNbARwesg7s/B+9uB03eXDsQw9w==",
|
||||
"path": "jellyfin.controller/10.11.0",
|
||||
"hashPath": "jellyfin.controller.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Data/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-YEz7/85b98Rj14IJJIVqmzJsi69LDOKo4Ox+VHbh1vj3tkWomSPayzvG3kyU8I0yFMrd6+Ta55C20kZ2XC7vQg==",
|
||||
"path": "jellyfin.data/10.11.0",
|
||||
"hashPath": "jellyfin.data.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Database.Implementations/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-oLblVZzqF9zuLMdfqp8pbusSVQq6b40/RcHjGF1hxYozVNEi+UhiDX8aJipYBOrh33FFAofoQq468BvZixpPcw==",
|
||||
"path": "jellyfin.database.implementations/10.11.0",
|
||||
"hashPath": "jellyfin.database.implementations.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Extensions/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-1ufj+Rm0Bn6C990i2wwiT5UHPZfD65GOtJK6NcDU//DDMbuoGX1LQZxuCx+rhhRg1XdHPWzYASARYyNlFQa6cg==",
|
||||
"path": "jellyfin.extensions/10.11.0",
|
||||
"hashPath": "jellyfin.extensions.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-/OBcg4Qj825elOGNj5bNRfABKzfAf4qNQj0/d/DwhG/+V/wsKuxS0Pc/xOEagVVjXOnqGPZz/+k8D4UvnvMoHw==",
|
||||
"path": "jellyfin.mediaencoding.keyframes/10.11.0",
|
||||
"hashPath": "jellyfin.mediaencoding.keyframes.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Model/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-h+61RSXn4sk8fS6Zx9RkDyVnI5VnNbrsR2p8WcvybtNSW2pgU2uZ9pwEv2awD3ifX69weqYpQLMh91f6aidW2A==",
|
||||
"path": "jellyfin.model/10.11.0",
|
||||
"hashPath": "jellyfin.model.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Naming/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2++xSbhdFSb1J3XySjC6UU+uII6OdKc0DfkYx/E1oN7mSjoftyZR8eU045kVWBwsAxr+UcMI6t2DYfES2tJkRA==",
|
||||
"path": "jellyfin.naming/10.11.0",
|
||||
"hashPath": "jellyfin.naming.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-WjjxVyOTVs85V7SUe+lZjtGOEeVzF4RO8amrqL4adgbyThNq7vGCFzPw8buZj44gHeQYD5V/uZ/6XuqG9Jq+kA==",
|
||||
"path": "microsoft.entityframeworkcore/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-I3TWAs5Lbzmzu8S0T6qXhzBiO3CznYLrfE59W0npkqNHfWGH8CgA66LrHMWxWOXVTD4145QwYqiWNCdLwpJ1Ew==",
|
||||
"path": "microsoft.entityframeworkcore.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-mXNl0Gg3l3zGrClLCHepB+b7rYVuFfB9qQJwya0dUSHFuR1T0jMD8KxuNVyhQSfoWIepanhzjcG8TUNGXOcU0Q==",
|
||||
"path": "microsoft.entityframeworkcore.analyzers/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.analyzers.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-IJNrG5vdmFUvHR8FLLFg9AWpuE8qW1DTEN+fNLGbNTu6cnpZzzqU6+aknAGtTSAEVWosJ3BZ3TOO9wpifUvv3A==",
|
||||
"path": "microsoft.entityframeworkcore.relational/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.relational.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-cL6iTxgJ4u5zP3eFOdBiDAtmE/B2WKTRhyJfEne7n6qvHpsMwwIDxljs210mWSO1ucBy7lR1Lo7/7kjeZeLcqQ==",
|
||||
"path": "microsoft.extensions.caching.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.caching.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2iuzwIoCoqZJfH2VLk1xvlQS4PQDEuhj4dWiGb+Qpt1vHFHyffp497cTO6ucsV54W/h4JmM1vzDBv8pVAFazZg==",
|
||||
"path": "microsoft.extensions.caching.memory/9.0.10",
|
||||
"hashPath": "microsoft.extensions.caching.memory.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ad3JxmFj0uxuFa1CT6oxTCC1lQ0xeRuOvzBRFT/I/ofIXVOnNsH/v2GZkAJWhlpZqKUvSexQZzp3EEAB2CdtJg==",
|
||||
"path": "microsoft.extensions.configuration.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.configuration.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-D6Kng+9I+w1SQPxJybc6wzw9nnnyUQPutycjtI0svv1RHaWOpUk9PPlwIRfhhoQZ3yihejkEI2wNv/7VnVtkGA==",
|
||||
"path": "microsoft.extensions.configuration.binder/9.0.10",
|
||||
"hashPath": "microsoft.extensions.configuration.binder.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-iEtXCkNd5XhjNJAOb/wO4IhDRdLIE2CsPxZggZQWJ/q2+sa8dmEPC393nnsiqdH8/4KV8Xn25IzgKPR1UEQ0og==",
|
||||
"path": "microsoft.extensions.dependencyinjection/9.0.10",
|
||||
"hashPath": "microsoft.extensions.dependencyinjection.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-r9waLiOPe9ZF1PvzUT+RDoHvpMmY8MW+lb4lqjYGObwKpnyPMLI3odVvlmshwuZcdoHynsGWOrCPA0hxZ63lIA==",
|
||||
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Logging/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-UBXHqE9vyptVhaFnT1R7YJKCve7TqVI10yjjUZBNGMlW2lZ4c031Slt9hxsOzWCzlpPxxIFyf1Yk4a6Iubxx7w==",
|
||||
"path": "microsoft.extensions.logging/9.0.10",
|
||||
"hashPath": "microsoft.extensions.logging.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-MFUPv/nN1rAQ19w43smm6bbf0JDYN/1HEPHoiMYY50pvDMFpglzWAuoTavByDmZq7UuhjaxwrET3joU69ZHoHQ==",
|
||||
"path": "microsoft.extensions.logging.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.logging.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Options/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-zMNABt8eBv0B0XrWjFy9nZNgddavaOeq3ZdaD5IlHhRH65MrU7HM+Hd8GjWE3e2VDGFPZFfSAc6XVXC17f9fOA==",
|
||||
"path": "microsoft.extensions.options/9.0.10",
|
||||
"hashPath": "microsoft.extensions.options.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Primitives/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-3pl8D1O5ZwMpDkZAT2uXrhQ6NipkwEgDLMFuURiHTf72TvkoMP61QYH3Vk1yrzVHnHBdNZk3cQACz8Zc7YGNhQ==",
|
||||
"path": "microsoft.extensions.primitives/9.0.10",
|
||||
"hashPath": "microsoft.extensions.primitives.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/1.1.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
|
||||
"path": "microsoft.netcore.platforms/1.1.0",
|
||||
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Targets/1.1.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
|
||||
"path": "microsoft.netcore.targets/1.1.0",
|
||||
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
|
||||
},
|
||||
"NEbml/1.1.0.5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-svtqDc+hue9kbnqNN2KkK4om/hDrc7K127cNb5FIYfgKgzo+JNDPXNLp8NioCchHhBO3lxWd4Cp/iiZZ3aoUqg==",
|
||||
"path": "nebml/1.1.0.5",
|
||||
"hashPath": "nebml.1.1.0.5.nupkg.sha512"
|
||||
},
|
||||
"Polly/8.6.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-uuBsDoBw0oYrMe3uTWRjkT2sIkKh+ZZnnDrLb4Z+QANfeA4+7FJacx6E8CY5GAxXRoSgFrvUADEAQ7DPF6fGiw==",
|
||||
"path": "polly/8.6.4",
|
||||
"hashPath": "polly.8.6.4.nupkg.sha512"
|
||||
},
|
||||
"Polly.Core/8.6.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-4AWqYnQ2TME0E+Mzovt1Uu+VyvpR84ymUldMcPw7Mbj799Phaag14CKrMtlJGx5jsvYP+S3oR1QmysgmXoD5cw==",
|
||||
"path": "polly.core/8.6.4",
|
||||
"hashPath": "polly.core.8.6.4.nupkg.sha512"
|
||||
},
|
||||
"System.Globalization/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
|
||||
"path": "system.globalization/4.3.0",
|
||||
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"path": "system.runtime/4.3.0",
|
||||
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Text.Json/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-XM02ZBnzxk7Ti6l9YRy8Bp639wANqJzJzw4W4VYiCh+HXY9hBOWkGB4k89OLP/s/RxgM02P4a/mWcJTgFiLf1Q==",
|
||||
"path": "system.text.json/9.0.10",
|
||||
"hashPath": "system.text.json.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.Tasks.Dataflow/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-k1o6v6V3+4mznSnPnO0FBaRjiAPL1ouKPfPQH7hO9Z2SwJHt8E45F4wX5yQh1aeja1JHPYEungQedXibng654Q==",
|
||||
"path": "system.threading.tasks.dataflow/9.0.10",
|
||||
"hashPath": "system.threading.tasks.dataflow.9.0.10.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
193
README.md
@@ -13,20 +13,17 @@ This plugin is based on my manual mod (see the [legacy branch](https://github.co
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Overview](#overview)
|
||||
- [Ideas for additional seasonals](#ideas-for-additional-seasonals)
|
||||
- [Installation](#installation)
|
||||
- [Client Compatibility](#client-compatibility)
|
||||
- [Configuration](#configuration)
|
||||
- [Automatic Theme Selection](#automatic-theme-selection)
|
||||
- [Theme Settings](#theme-settings)
|
||||
- [Build Process](#build-process)
|
||||
- [Build the plugin by yourself](#build-the-plugin-by-yourself)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Effects Not Showing](#effects-not-showing)
|
||||
- [Docker Permission Issues](#docker-permission-issues)
|
||||
- [Contributing](#contributing)
|
||||
- [Legacy Manual Installation](#legacy-manual-installation)
|
||||
- [Installation](#installation-1)
|
||||
- [Usage](#usage)
|
||||
- [Additional Directory: Separate Single Seasonals](#additional-directory-separate-single-seasonals)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,39 +31,83 @@ This plugin is based on my manual mod (see the [legacy branch](https://github.co
|
||||
|
||||
- **Automatic Theme Selection**: Dynamically updates the theme based on the date (e.g., snowflakes in December, fireworks for new year's eve).
|
||||
- **Easy Integration**: No manual file editing required. The plugin injects everything automatically.
|
||||
- **Configuration UI**: Configure settings directly in the Jellyfin Dashboard (very basic for now, needs some work in the future).
|
||||
- **Configuration UI**: Configure settings directly in the Jellyfin Dashboard.
|
||||
<details>
|
||||
<summary>Have a look:</summary>
|
||||
<img width="852" height="782" alt="Admin-Settings" src="https://github.com/user-attachments/assets/03d04ea8-7dd9-418e-88f8-9ae2937c06bb" />
|
||||
</details>
|
||||
- **User Toggle**: Optionally allow users to enable/disable seasonal effects from their client.
|
||||
<details>
|
||||
<summary>Have a look:</summary>
|
||||
<img width="467" height="263" alt="Client-Settings" src="https://github.com/user-attachments/assets/a8dfc90a-16c8-409c-9133-4139f6527b0b" />
|
||||
</details>
|
||||
|
||||
## Overview
|
||||
Click on the following themes to expand them and see the theme in action:
|
||||
|
||||
<details>
|
||||
<summary>Easter</summary>
|
||||
|
||||
**Easter**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Autumn</summary>
|
||||
|
||||
**Autumn**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Santa</summary>
|
||||
|
||||
**Santa**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Christmas</summary>
|
||||
|
||||
**Christmas**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fireworks</summary>
|
||||
|
||||
**Fireworks**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Halloween</summary>
|
||||
|
||||
**Halloween**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Hearts</summary>
|
||||
|
||||
**Hearts**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Snowfall</summary>
|
||||
|
||||
**Snowfall**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Snowflakes</summary>
|
||||
|
||||
**Snowflakes**
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Snowstorm</summary>
|
||||
|
||||
**Snowstorm**
|
||||

|
||||
</details>
|
||||
|
||||
|
||||
## Ideas for additional seasonals
|
||||
If you have any (specific) ideas for additional seasonals, feel free to open an issue or create a pull request. See the [contributing guidelines](CONTRIBUTING.md) for more details.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -77,9 +118,9 @@ To install this plugin, you will first need to add the repository in Jellyfin.
|
||||
1. Open your Jellyfin Dashboard.
|
||||
2. Navigate to **Plugins** > **Manage Repositories**.
|
||||
3. Click the **+ New Repository** button to add a new repository.
|
||||
4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field:
|
||||
4. Enter a name for the repo and paste the following URL into the 'Repository URL' field:
|
||||
```bash
|
||||
https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/manifest.json
|
||||
https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-manifest/refs/heads/main/manifest.json
|
||||
```
|
||||
5. Click **Add**.
|
||||
6. Go to the **Available** tab at the top.
|
||||
@@ -135,7 +176,7 @@ If automatic selection is enabled, the following themes are applied based on the
|
||||
## Theme Settings
|
||||
Each theme contains additional settings to customize its behavior. Expand the advanced configuration section to configure each theme, adjust parameters like particle count, animation speed etc.
|
||||
|
||||
## Build Process
|
||||
## Build the plugin by yourself
|
||||
|
||||
If you want to build the plugin yourself:
|
||||
|
||||
@@ -202,117 +243,5 @@ volumes:
|
||||
## Contributing
|
||||
|
||||
Feel free to contribute to this project by creating pull requests or reporting issues.
|
||||
For detailed contribution guidelines, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
|
||||
---
|
||||
|
||||
## Legacy Manual Installation
|
||||
|
||||
<details>
|
||||
<summary>Click to expand instructions for the old manual installation method (without plugin)</summary>
|
||||
|
||||
### Installation
|
||||
|
||||
> [!TIP]
|
||||
> Take a look at [CodeDevMLH/Jellyfin-Mods-Automated-Script](https://github.com/CodeDevMLH/Jellyfin-Mods-Automated-Script)
|
||||
|
||||
1. **Add Seasonal Container to `index.html`**
|
||||
Edit the `index.html` file of your Jellyfin web instance. Add the following code inside the `<body>` tag:
|
||||
|
||||
```html
|
||||
<div class="seasonals-container"></div>
|
||||
<script src="seasonals/seasonals.js"></script>
|
||||
```
|
||||
2. **Deploy Files**
|
||||
Place the seasonals folder (including seasonals.js, CSS, and additional JavaScript files for each theme [this one](https://github.com/CodeDevMLH/Jellyfin-Seasonals/tree/legacy/seasonals)) inside the Jellyfin web server directory (labeld with "web").
|
||||
|
||||
3. **Configure Themes**
|
||||
Customize the theme-configs.js file to modify or add new themes. The default configuration is shown below:
|
||||
|
||||
```javascript
|
||||
const automateThemeSelection = true; // Set to false to disable automatic theme selection based on current date
|
||||
const defaultTheme = 'none'; // Default theme if automatic selection is off
|
||||
|
||||
const themeConfigs = {
|
||||
snowflakes: {
|
||||
css: 'seasonals/snowflakes.css',
|
||||
js: 'seasonals/snowflakes.js',
|
||||
containerClass: 'snowflakes'
|
||||
},
|
||||
snowfall: {
|
||||
css: 'seasonals/snowfall.css',
|
||||
js: 'seasonals/snowfall.js',
|
||||
containerClass: 'snowfall-container'
|
||||
},
|
||||
|
||||
// more configs...
|
||||
|
||||
none: {
|
||||
containerClass: 'none'
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
4. **Reload the web interface**
|
||||
After making these changes, restart your Jellyfin server and/or refresh the web interface (ctrl+F5, sometimes you need to clear the browsers temp files/cache (every time with firefox ;-()) to see the changes.
|
||||
|
||||
### Theme Settings
|
||||
Each theme's JavaScript file contains additional settings to customize its behavior. Here are examples for the `autumn` and `snowflakes` themes:
|
||||
|
||||
**Autumn Theme Settings**
|
||||
```javascript
|
||||
const leaves = true; // Enable/disable leaves
|
||||
const randomLeaves = true; // Enable random leaves
|
||||
const randomLeavesMobile = false; // Enable random leaves on mobile devices
|
||||
const enableDiffrentDuration = true; // Enable different animation duration for random leaves
|
||||
const leafCount = 25; // Number of random extra leaves
|
||||
```
|
||||
|
||||
**Snowflakes Theme Settings**
|
||||
```javascript
|
||||
const snowflakes = true; // Enable/disable snowflakes
|
||||
const randomSnowflakes = true; // Enable random snowflakes
|
||||
const randomSnowflakesMobile = false; // Enable random snowflakes on mobile devices
|
||||
const enableColoredSnowflakes = true; // Enable colored snowflakes on mobile devices
|
||||
const enableDiffrentDuration = true; // Enable different animation duration for random snowflakes
|
||||
const snowflakeCount = 25; // Number of random extra snowflakes
|
||||
```
|
||||
|
||||
### Usage
|
||||
**Automatic Theme Selection**
|
||||
By default, the theme is automatically selected based on the date. For example:
|
||||
|
||||
Snowfall: January
|
||||
Hearts: February (Valentine's Day)
|
||||
Halloween: October
|
||||
|
||||
Modify the determineCurrentTheme() function in seasonals.js to adjust date-based logic.
|
||||
|
||||
**Manual Theme Selection**
|
||||
To use a fixed theme, set automateThemeSelection to false in the theme-configs.js file and specify a defaultTheme.
|
||||
|
||||
**Custom Themes**
|
||||
1. Add your CSS and JavaScript files to the seasonals folder.
|
||||
|
||||
2. Extend the themeConfigs object with your theme details:
|
||||
```javascript
|
||||
myTheme: {
|
||||
css: 'seasonals/my-theme.css',
|
||||
js: 'seasonals/my-theme.js',
|
||||
containerClass: 'my-theme-container',
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Additional Directory: Separate Single Seasonals
|
||||
For users who prefer not to use the automatic seasonal theme selection, individual seasonals are available in the `separate single seasonals` folder. Each seasonal theme can be independently loaded and used without relying on the main automatic selection system.
|
||||
|
||||
but this requires to the modify of the `index.html` with adding the html in `add_to_index_html`.
|
||||
|
||||
To use a single seasonal theme, include its specific CSS and JS files in your `index.html` inside the `<body> </body>` tags provided by `add_to_index_html.html` in the sub-theme-folders as shown below:
|
||||
|
||||
```html
|
||||
<div class="seasonalsname-container"></div>
|
||||
<script src="separate single seasonals/snowflakes.js"></script>
|
||||
<link rel="stylesheet" href="separate single seasonals/snowflakes.css">
|
||||
|
||||
</details>
|
||||
|
||||
@@ -9,10 +9,7 @@ Bevor du baust, musst du die Versionsnummer in den folgenden Dateien aktualisier
|
||||
1. **`Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj`**
|
||||
Suche nach `<Version>...</Version>` und ändere die Nummer.
|
||||
|
||||
2. **`build.yaml`**
|
||||
Ändere den Wert bei `version: "..."`.
|
||||
|
||||
3. **`manifest.json`**
|
||||
2. **`manifest.json`**
|
||||
Füge einen neuen Eintrag oben in die `versions`-Liste ein (oder bearbeite den vorhandenen, wenn es noch kein Release gab).
|
||||
* `version`: Deine neue Nummer.
|
||||
* `changelog`: Was hat sich geändert?
|
||||
|
||||
@@ -1,640 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v9.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v9.0": {
|
||||
"Jellyfin.Plugin.Seasonals/1.3.3.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Controller": "10.11.0",
|
||||
"Jellyfin.Model": "10.11.0",
|
||||
"Newtonsoft.Json": "13.0.4"
|
||||
},
|
||||
"runtime": {
|
||||
"Jellyfin.Plugin.Seasonals.dll": {}
|
||||
}
|
||||
},
|
||||
"BitFaster.Caching/2.5.4": {
|
||||
"runtime": {
|
||||
"lib/net6.0/BitFaster.Caching.dll": {
|
||||
"assemblyVersion": "2.5.4.0",
|
||||
"fileVersion": "2.5.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Diacritics/4.0.17": {
|
||||
"runtime": {
|
||||
"lib/net9.0/Diacritics.dll": {
|
||||
"assemblyVersion": "4.0.17.0",
|
||||
"fileVersion": "4.0.17.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ICU4N/60.1.0-alpha.356": {
|
||||
"dependencies": {
|
||||
"J2N": "2.0.0",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/ICU4N.dll": {
|
||||
"assemblyVersion": "60.0.0.0",
|
||||
"fileVersion": "60.1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ICU4N.Transliterator/60.1.0-alpha.356": {
|
||||
"dependencies": {
|
||||
"ICU4N": "60.1.0-alpha.356"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/ICU4N.Transliterator.dll": {
|
||||
"assemblyVersion": "60.0.0.0",
|
||||
"fileVersion": "60.1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"J2N/2.0.0": {
|
||||
"runtime": {
|
||||
"lib/net6.0/J2N.dll": {
|
||||
"assemblyVersion": "2.0.0.0",
|
||||
"fileVersion": "2.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Common/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Model": "10.11.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/MediaBrowser.Common.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Controller/10.11.0": {
|
||||
"dependencies": {
|
||||
"BitFaster.Caching": "2.5.4",
|
||||
"Jellyfin.Common": "10.11.0",
|
||||
"Jellyfin.MediaEncoding.Keyframes": "10.11.0",
|
||||
"Jellyfin.Model": "10.11.0",
|
||||
"Jellyfin.Naming": "10.11.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Configuration.Binder": "9.0.10",
|
||||
"System.Threading.Tasks.Dataflow": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/MediaBrowser.Controller.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Data/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Database.Implementations": "10.11.0",
|
||||
"Microsoft.Extensions.Logging": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.Data.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Database.Implementations/10.11.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Relational": "9.0.10",
|
||||
"Polly": "8.6.4"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.Database.Implementations.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Extensions/10.11.0": {
|
||||
"dependencies": {
|
||||
"Diacritics": "4.0.17",
|
||||
"ICU4N.Transliterator": "60.1.0-alpha.356"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.Extensions.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"NEbml": "1.1.0.5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Jellyfin.MediaEncoding.Keyframes.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Model/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Data": "10.11.0",
|
||||
"Jellyfin.Extensions": "10.11.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"System.Globalization": "4.3.0",
|
||||
"System.Text.Json": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/MediaBrowser.Model.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jellyfin.Naming/10.11.0": {
|
||||
"dependencies": {
|
||||
"Jellyfin.Common": "10.11.0",
|
||||
"Jellyfin.Model": "10.11.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Emby.Naming.dll": {
|
||||
"assemblyVersion": "10.11.0.0",
|
||||
"fileVersion": "10.11.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.10",
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.10",
|
||||
"Microsoft.Extensions.Logging": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
|
||||
"assemblyVersion": "9.0.10.0",
|
||||
"fileVersion": "9.0.1025.47514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.10.0",
|
||||
"fileVersion": "9.0.1025.47514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {},
|
||||
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "9.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "9.0.10",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Logging": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
||||
"assemblyVersion": "9.0.10.0",
|
||||
"fileVersion": "9.0.1025.47514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Options": "9.0.10",
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Caching.Memory.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Configuration.Binder.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.10",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Options": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Logging.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options/9.0.10": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
|
||||
"Microsoft.Extensions.Primitives": "9.0.10"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Options.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives/9.0.10": {
|
||||
"runtime": {
|
||||
"lib/net9.0/Microsoft.Extensions.Primitives.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.1025.47515"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/1.1.0": {},
|
||||
"Microsoft.NETCore.Targets/1.1.0": {},
|
||||
"NEbml/1.1.0.5": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/NEbml.Core.dll": {
|
||||
"assemblyVersion": "1.1.0.5",
|
||||
"fileVersion": "1.1.0.5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"runtime": {
|
||||
"lib/net6.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.4.30916"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Polly/8.6.4": {
|
||||
"dependencies": {
|
||||
"Polly.Core": "8.6.4"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/Polly.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.6.4.5033"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Polly.Core/8.6.4": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Polly.Core.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.6.4.5033"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Globalization/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime/4.3.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Json/9.0.10": {},
|
||||
"System.Threading.Tasks.Dataflow/9.0.10": {}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Jellyfin.Plugin.Seasonals/1.3.3.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"BitFaster.Caching/2.5.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ==",
|
||||
"path": "bitfaster.caching/2.5.4",
|
||||
"hashPath": "bitfaster.caching.2.5.4.nupkg.sha512"
|
||||
},
|
||||
"Diacritics/4.0.17": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-FmMvVQRsfon+x5P+dxz4mvV8wt45xr25EAOCkuo/Cjtc7lVYV5cZUSsNXwmKQpwO+TokIHpzxb8ENpqrm4yBlQ==",
|
||||
"path": "diacritics/4.0.17",
|
||||
"hashPath": "diacritics.4.0.17.nupkg.sha512"
|
||||
},
|
||||
"ICU4N/60.1.0-alpha.356": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-YMZtDnjcqWzziOKiE7w6Ma7Rl5vuFDxzOsUlHh1QyfghbNEIZQOLRs9MMfwCWAjX6n9UitrF6vLXy55Z5q+4Fg==",
|
||||
"path": "icu4n/60.1.0-alpha.356",
|
||||
"hashPath": "icu4n.60.1.0-alpha.356.nupkg.sha512"
|
||||
},
|
||||
"ICU4N.Transliterator/60.1.0-alpha.356": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-lFOSO6bbEtB6HkWMNDJAq+rFwVyi9g6xVc5O/2xHa6iZnV7wLVDqCbaQ4W4vIeBSQZAafqhxciaEkmAvSdzlCg==",
|
||||
"path": "icu4n.transliterator/60.1.0-alpha.356",
|
||||
"hashPath": "icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512"
|
||||
},
|
||||
"J2N/2.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-M5bwDajAARZiyqupU+rHQJnsVLxNBOHJ8vKYHd8LcLIb1FgLfzzcJvc31Qo5Xz/GEHFjDF9ScjKL/ks/zRTXuA==",
|
||||
"path": "j2n/2.0.0",
|
||||
"hashPath": "j2n.2.0.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Common/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-TitN7+qWFt2l0V5b+KTRt7ABDCvfZdvSC6qBG1uHS18Y80xrbrSCJ9O6BH/of310h6a4lxKlQjUtTPHCzeG2AA==",
|
||||
"path": "jellyfin.common/10.11.0",
|
||||
"hashPath": "jellyfin.common.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Controller/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-WV+PQy9AHdZLvYqUsNq6ZyQoxaiaEWLz0EwZGOiu8xSrepQLFope2U1VFHVCNbARwesg7s/B+9uB03eXDsQw9w==",
|
||||
"path": "jellyfin.controller/10.11.0",
|
||||
"hashPath": "jellyfin.controller.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Data/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-YEz7/85b98Rj14IJJIVqmzJsi69LDOKo4Ox+VHbh1vj3tkWomSPayzvG3kyU8I0yFMrd6+Ta55C20kZ2XC7vQg==",
|
||||
"path": "jellyfin.data/10.11.0",
|
||||
"hashPath": "jellyfin.data.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Database.Implementations/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-oLblVZzqF9zuLMdfqp8pbusSVQq6b40/RcHjGF1hxYozVNEi+UhiDX8aJipYBOrh33FFAofoQq468BvZixpPcw==",
|
||||
"path": "jellyfin.database.implementations/10.11.0",
|
||||
"hashPath": "jellyfin.database.implementations.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Extensions/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-1ufj+Rm0Bn6C990i2wwiT5UHPZfD65GOtJK6NcDU//DDMbuoGX1LQZxuCx+rhhRg1XdHPWzYASARYyNlFQa6cg==",
|
||||
"path": "jellyfin.extensions/10.11.0",
|
||||
"hashPath": "jellyfin.extensions.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-/OBcg4Qj825elOGNj5bNRfABKzfAf4qNQj0/d/DwhG/+V/wsKuxS0Pc/xOEagVVjXOnqGPZz/+k8D4UvnvMoHw==",
|
||||
"path": "jellyfin.mediaencoding.keyframes/10.11.0",
|
||||
"hashPath": "jellyfin.mediaencoding.keyframes.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Model/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-h+61RSXn4sk8fS6Zx9RkDyVnI5VnNbrsR2p8WcvybtNSW2pgU2uZ9pwEv2awD3ifX69weqYpQLMh91f6aidW2A==",
|
||||
"path": "jellyfin.model/10.11.0",
|
||||
"hashPath": "jellyfin.model.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Jellyfin.Naming/10.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2++xSbhdFSb1J3XySjC6UU+uII6OdKc0DfkYx/E1oN7mSjoftyZR8eU045kVWBwsAxr+UcMI6t2DYfES2tJkRA==",
|
||||
"path": "jellyfin.naming/10.11.0",
|
||||
"hashPath": "jellyfin.naming.10.11.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-WjjxVyOTVs85V7SUe+lZjtGOEeVzF4RO8amrqL4adgbyThNq7vGCFzPw8buZj44gHeQYD5V/uZ/6XuqG9Jq+kA==",
|
||||
"path": "microsoft.entityframeworkcore/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-I3TWAs5Lbzmzu8S0T6qXhzBiO3CznYLrfE59W0npkqNHfWGH8CgA66LrHMWxWOXVTD4145QwYqiWNCdLwpJ1Ew==",
|
||||
"path": "microsoft.entityframeworkcore.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-mXNl0Gg3l3zGrClLCHepB+b7rYVuFfB9qQJwya0dUSHFuR1T0jMD8KxuNVyhQSfoWIepanhzjcG8TUNGXOcU0Q==",
|
||||
"path": "microsoft.entityframeworkcore.analyzers/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.analyzers.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-IJNrG5vdmFUvHR8FLLFg9AWpuE8qW1DTEN+fNLGbNTu6cnpZzzqU6+aknAGtTSAEVWosJ3BZ3TOO9wpifUvv3A==",
|
||||
"path": "microsoft.entityframeworkcore.relational/9.0.10",
|
||||
"hashPath": "microsoft.entityframeworkcore.relational.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-cL6iTxgJ4u5zP3eFOdBiDAtmE/B2WKTRhyJfEne7n6qvHpsMwwIDxljs210mWSO1ucBy7lR1Lo7/7kjeZeLcqQ==",
|
||||
"path": "microsoft.extensions.caching.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.caching.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2iuzwIoCoqZJfH2VLk1xvlQS4PQDEuhj4dWiGb+Qpt1vHFHyffp497cTO6ucsV54W/h4JmM1vzDBv8pVAFazZg==",
|
||||
"path": "microsoft.extensions.caching.memory/9.0.10",
|
||||
"hashPath": "microsoft.extensions.caching.memory.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ad3JxmFj0uxuFa1CT6oxTCC1lQ0xeRuOvzBRFT/I/ofIXVOnNsH/v2GZkAJWhlpZqKUvSexQZzp3EEAB2CdtJg==",
|
||||
"path": "microsoft.extensions.configuration.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.configuration.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-D6Kng+9I+w1SQPxJybc6wzw9nnnyUQPutycjtI0svv1RHaWOpUk9PPlwIRfhhoQZ3yihejkEI2wNv/7VnVtkGA==",
|
||||
"path": "microsoft.extensions.configuration.binder/9.0.10",
|
||||
"hashPath": "microsoft.extensions.configuration.binder.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-iEtXCkNd5XhjNJAOb/wO4IhDRdLIE2CsPxZggZQWJ/q2+sa8dmEPC393nnsiqdH8/4KV8Xn25IzgKPR1UEQ0og==",
|
||||
"path": "microsoft.extensions.dependencyinjection/9.0.10",
|
||||
"hashPath": "microsoft.extensions.dependencyinjection.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-r9waLiOPe9ZF1PvzUT+RDoHvpMmY8MW+lb4lqjYGObwKpnyPMLI3odVvlmshwuZcdoHynsGWOrCPA0hxZ63lIA==",
|
||||
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Logging/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-UBXHqE9vyptVhaFnT1R7YJKCve7TqVI10yjjUZBNGMlW2lZ4c031Slt9hxsOzWCzlpPxxIFyf1Yk4a6Iubxx7w==",
|
||||
"path": "microsoft.extensions.logging/9.0.10",
|
||||
"hashPath": "microsoft.extensions.logging.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-MFUPv/nN1rAQ19w43smm6bbf0JDYN/1HEPHoiMYY50pvDMFpglzWAuoTavByDmZq7UuhjaxwrET3joU69ZHoHQ==",
|
||||
"path": "microsoft.extensions.logging.abstractions/9.0.10",
|
||||
"hashPath": "microsoft.extensions.logging.abstractions.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Options/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-zMNABt8eBv0B0XrWjFy9nZNgddavaOeq3ZdaD5IlHhRH65MrU7HM+Hd8GjWE3e2VDGFPZFfSAc6XVXC17f9fOA==",
|
||||
"path": "microsoft.extensions.options/9.0.10",
|
||||
"hashPath": "microsoft.extensions.options.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Primitives/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-3pl8D1O5ZwMpDkZAT2uXrhQ6NipkwEgDLMFuURiHTf72TvkoMP61QYH3Vk1yrzVHnHBdNZk3cQACz8Zc7YGNhQ==",
|
||||
"path": "microsoft.extensions.primitives/9.0.10",
|
||||
"hashPath": "microsoft.extensions.primitives.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/1.1.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
|
||||
"path": "microsoft.netcore.platforms/1.1.0",
|
||||
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NETCore.Targets/1.1.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
|
||||
"path": "microsoft.netcore.targets/1.1.0",
|
||||
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
|
||||
},
|
||||
"NEbml/1.1.0.5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-svtqDc+hue9kbnqNN2KkK4om/hDrc7K127cNb5FIYfgKgzo+JNDPXNLp8NioCchHhBO3lxWd4Cp/iiZZ3aoUqg==",
|
||||
"path": "nebml/1.1.0.5",
|
||||
"hashPath": "nebml.1.1.0.5.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==",
|
||||
"path": "newtonsoft.json/13.0.4",
|
||||
"hashPath": "newtonsoft.json.13.0.4.nupkg.sha512"
|
||||
},
|
||||
"Polly/8.6.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-uuBsDoBw0oYrMe3uTWRjkT2sIkKh+ZZnnDrLb4Z+QANfeA4+7FJacx6E8CY5GAxXRoSgFrvUADEAQ7DPF6fGiw==",
|
||||
"path": "polly/8.6.4",
|
||||
"hashPath": "polly.8.6.4.nupkg.sha512"
|
||||
},
|
||||
"Polly.Core/8.6.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-4AWqYnQ2TME0E+Mzovt1Uu+VyvpR84ymUldMcPw7Mbj799Phaag14CKrMtlJGx5jsvYP+S3oR1QmysgmXoD5cw==",
|
||||
"path": "polly.core/8.6.4",
|
||||
"hashPath": "polly.core.8.6.4.nupkg.sha512"
|
||||
},
|
||||
"System.Globalization/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
|
||||
"path": "system.globalization/4.3.0",
|
||||
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime/4.3.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"path": "system.runtime/4.3.0",
|
||||
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
|
||||
},
|
||||
"System.Text.Json/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-XM02ZBnzxk7Ti6l9YRy8Bp639wANqJzJzw4W4VYiCh+HXY9hBOWkGB4k89OLP/s/RxgM02P4a/mWcJTgFiLf1Q==",
|
||||
"path": "system.text.json/9.0.10",
|
||||
"hashPath": "system.text.json.9.0.10.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.Tasks.Dataflow/9.0.10": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-k1o6v6V3+4mznSnPnO0FBaRjiAPL1ouKPfPQH7hO9Z2SwJHt8E45F4wX5yQh1aeja1JHPYEungQedXibng654Q==",
|
||||
"path": "system.threading.tasks.dataflow/9.0.10",
|
||||
"hashPath": "system.threading.tasks.dataflow.9.0.10.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
build.yaml
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: "Seasonals"
|
||||
guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"
|
||||
version: "1.3.3.0"
|
||||
targetAbi: "10.11.0.0"
|
||||
framework: "net9.0"
|
||||
overview: "Seasonal effects for Jellyfin"
|
||||
description: >
|
||||
Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.
|
||||
category: "General"
|
||||
owner: "CodeDevMLH"
|
||||
artifacts:
|
||||
- "Jellyfin.Plugin.Seasonals.dll"
|
||||
changelog: >
|
||||
Added Advanced Configuration UI for customizing individual seasonal effects.
|
||||
163
git_cheat_sheet.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 🚀 Git Cheat Sheet: Branches, Merge, Rebase & VS Code
|
||||
|
||||
## 🌳 Branches (Zweige)
|
||||
* **`main`** → Stabiler Stand, Production-ready (Deployment).
|
||||
* **`dev`** → Aktive Entwicklung, Sammelbecken für Features.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Zusammenführen von Änderungen
|
||||
|
||||
### Git Merge
|
||||
Führt zwei Zweige zusammen. Git sucht den letzten gemeinsamen Basispunkt und erstellt einen neuen **Merge-Commit**.
|
||||
|
||||
```bash
|
||||
git checkout dev
|
||||
git merge main
|
||||
```
|
||||
|
||||
| Details | |
|
||||
|-----------|--------|
|
||||
| Wann? | In Team-Branches, bei bereits gepushten Branches, wenn Stabilität wichtiger als eine saubere History ist. |
|
||||
| Vorteile | Sicher, einfach nachvollziehbar, schreibt die History nicht um. |
|
||||
| Nachteile | Die History kann bei vielen Merges unübersichtlich ("Spaghetti-Graph") werden. |
|
||||
|
||||
### Git Rebase
|
||||
Setzt deine Commits neu auf die Spitze eines anderen Branches. Die Commit-IDs werden dabei neu geschrieben.
|
||||
|
||||
```bash
|
||||
git stash # (optional) Änderungen parken.
|
||||
git checkout dev
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
git stash pop # (optional) Änderungen zurückholen.
|
||||
```
|
||||
|
||||
| Details | |
|
||||
|-----------|--------|
|
||||
| Wann? | In lokalen Feature-Branches (bevor sie geteilt werden), um die History sauber zu halten. |
|
||||
| Vorteile | Erzeugt eine perfekt lineare, leicht lesbare History. |
|
||||
| Nachteile | ⚠️ Gefährlich auf geteilten/öffentlichen Branches. Konflikte müssen ggf. für jeden einzelnen Commit gelöst werden. |
|
||||
|
||||
---
|
||||
|
||||
## **Git: dev zurücksetzen & „main ist Chef“**
|
||||
|
||||
Problem
|
||||
|
||||
Beim Arbeiten mit Git passiert oft Folgendes:
|
||||
|
||||
- `dev` hat Commits, die nicht in `main` sind
|
||||
- diese Commits brauche ich nicht mehr
|
||||
- beim `git rebase origin/main` will Git trotzdem mergen oder Konflikte lösen
|
||||
|
||||
⚠️ Wichtiges Missverständnis:
|
||||
|
||||
Das sind keine lokalen Änderungen, sondern Commits, die auf `dev` existieren.
|
||||
|
||||
`git reset --hard origin/dev` entfernt nur lokale, nicht gepushte Commits,
|
||||
aber nicht Commits, die bereits auf `origin/dev` liegen.
|
||||
|
||||
Praktische Befehle & sichere Vorgehensweise
|
||||
|
||||
- Prüfen — welche Commits würden entfernt werden:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git log --oneline origin/main..origin/dev
|
||||
```
|
||||
|
||||
- Optional: Backup des aktuellen `origin/dev`, falls etwas schiefgeht:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git branch backup/dev origin/dev
|
||||
```
|
||||
|
||||
- Sicheres Zurücksetzen von `dev` auf `main` (lokal + remote):
|
||||
|
||||
```bash
|
||||
git checkout dev
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
git push --force-with-lease origin dev
|
||||
```
|
||||
|
||||
- Alternative (ohne lokalen Checkout): Push von `main` direkt nach `dev`:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git push --force-with-lease origin origin/main:refs/heads/dev
|
||||
```
|
||||
|
||||
- Warnung: Diese Aktionen schreiben die Remote‑History um. Koordiniere dich mit
|
||||
dem Team bevor du ein Force‑Push ausführst. Verwende `--force-with-lease`
|
||||
anstelle von `--force` für etwas mehr Sicherheit.
|
||||
|
||||
|
||||
## 📦 Temporäres Speichern & Spezialbefehle
|
||||
|
||||
### Stash (Das "Regal")
|
||||
Speichert uncommitted Changes temporär, um das Arbeitsverzeichnis sauber zu machen, ohne zu committen.
|
||||
|
||||
```bash
|
||||
git stash # Änderungen "parken".
|
||||
git stash pop # Änderungen zurückholen und Stash leeren.
|
||||
```
|
||||
|
||||
**Wann?** Wenn du schnell den Branch wechseln musst, aber deine Arbeit noch nicht fertig ist.
|
||||
|
||||
### Cherry-pick
|
||||
Kopiert einen ganz gezielten Commit von einem Branch in deinen aktuellen.
|
||||
|
||||
```bash
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
**Wann?** Wenn ein Bugfix auf dem falschen Branch gelandet ist oder du nur eine einzige Funktion aus einem Feature-Branch brauchst.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Checkout & Switch
|
||||
|
||||
```bash
|
||||
git checkout dev # oder git switch dev – Wechselt zum Branch.
|
||||
git checkout -f dev # Force Checkout: Wechselt den Branch und verwirft alle ungespeicherten lokalen Änderungen unwiderruflich! ⚠️
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 VS Code Git-Optionen (UI)
|
||||
|
||||
VS Code bietet beim Branch-Wechsel oft drei intelligente Optionen an:
|
||||
|
||||
* **Migrate Changes ⭐**
|
||||
* Nimmt deine aktuellen Änderungen einfach mit in den neuen Branch.
|
||||
* (Intern: stash → switch → stash pop).
|
||||
* **Stash & Checkout**
|
||||
* Parkt deine Änderungen sicher im Stash und wechselt den Branch. Die Änderungen bleiben im Stash, bis du sie manuell wieder herausholst.
|
||||
* **Force Checkout ⚠️**
|
||||
* Wechselt den Branch und löscht deine aktuellen, ungespeicherten Änderungen. Nur nutzen, wenn die Arbeit weggeworfen werden kann.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Typischer Sync-Workflow
|
||||
|
||||
Um den Entwicklungs-Branch aktuell zu halten, nachdem dev in main gemerged wurde:
|
||||
|
||||
1. Auf dev entwickeln.
|
||||
2. Merge dev → main für das Release.
|
||||
3. Zurück auf dev wechseln:
|
||||
```bash
|
||||
git checkout dev
|
||||
git merge main # (oder rebase), um den neuesten Stand vom Main wieder in Dev zu haben.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Merksätze
|
||||
|
||||
* **Merge** = Historien verbinden (Sicher & Dokumentiert).
|
||||
* **Rebase** = Historie neu schreiben (Linear & Sauber).
|
||||
* **Stash** = "Ich parke das mal kurz hier."
|
||||
* **Migrate Changes** = Sicherer Branch-Wechsel mit "Gepäck".
|
||||
BIN
logo.png
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 253 KiB |
BIN
logos/Seasonals_logo_mod.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
357
manifest.json
@@ -2,19 +2,75 @@
|
||||
{
|
||||
"guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4",
|
||||
"name": "Seasonals",
|
||||
"description": "Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.",
|
||||
"overview": "Seasonal effects for Jellyfin",
|
||||
"description": "Adds atmospheric, animated seasonal effects like snow, falling leaves, hearts, fireworks and more. The plugin provides a settings page for configuration. If you do not have write permissions to the web folder, please also install the file-transformation plugin.",
|
||||
"overview": "Atmospheric, configurable seasonal effects for the Jellyfin web interface",
|
||||
"owner": "CodeDevMLH",
|
||||
"category": "General",
|
||||
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.7.1.0",
|
||||
"changelog": "- feat: add summer, spring and carnival themes",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.1.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "5f288972124771d1223484f75138f566",
|
||||
"timestamp": "2026-02-19T02:20:23Z"
|
||||
},
|
||||
{
|
||||
"version": "1.7.0.15",
|
||||
"changelog": "- feat: add customizable auto seasonal list via config page\n- feat: add new season theme 'resurrection' by Bioflash257",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.7.0.15/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "d1fc094710efe45ea8cc885bc6a826c4",
|
||||
"timestamp": "2026-02-17T13:11:21Z"
|
||||
},
|
||||
{
|
||||
"version": "1.6.3.0",
|
||||
"changelog": "- fix path issue on subpath installations",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.6.3.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "e9a1fb6c91b8b48978efb43c72e462a0",
|
||||
"timestamp": "2026-02-15T01:12:57Z"
|
||||
},
|
||||
{
|
||||
"version": "1.5.1.0",
|
||||
"changelog": "- fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.5.1.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "a66b5c24c1f4bfff14bba5e93576bb80",
|
||||
"timestamp": "2026-01-28T22:40:49Z"
|
||||
},
|
||||
{
|
||||
"version": "1.5.0.0",
|
||||
"changelog": "- Refactor script injection logic",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.5.0.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "ba8d3c358df3e0546b99113b43f54fea",
|
||||
"timestamp": "2026-01-08T23:31:52Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.1.0",
|
||||
"changelog": "- fix fireworks display issue on scroll",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.1.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "bd8d9c6af064de011a708ea85e9b08c0",
|
||||
"timestamp": "2026-01-06T21:12:46Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.0",
|
||||
"changelog": "- settings linked directly in the main menu\n- renamed main plugin script\n- added enable/disable toggle for the entire plugin",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.0.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "205606075eec5f93d3da37efaecdeab5",
|
||||
"timestamp": "2025-12-28T19:11:04Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.4.0",
|
||||
"changelog": "- some fixes for js loading\n- adapted config page descriptions",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.3.4.0/Jellyfin.Plugin.Seasonals.zip",
|
||||
"checksum": "4985ef202d93fa3287673daf4ab70360",
|
||||
"timestamp": "2025-12-24T01:41:26Z"
|
||||
"checksum": "4869a9a0c08317d2cb0e0fc8f454cf2b",
|
||||
"timestamp": "2025-12-24T02:02:15Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.3.0",
|
||||
@@ -57,5 +113,296 @@
|
||||
"timestamp": "2025-12-15T15:33:15Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "d7e11d57-819b-4bdd-a88d-53c5f5560225",
|
||||
"name": "Media Bar Enhanced",
|
||||
"description": "A feature-rich fork of the original Media Bar script by MakD that brings your home screen to life.\n\n-> 100% Configurable via Web UI: Manage all features, lists, and settings effortlessly through the plugin configuration page.\n\nKey Highlights:\n- Cinematic Video Backdrops: Supports local & YouTube trailers (incl. SponsorBlock)\n- Custom Content: Curate your slideshow with specific Collections, Playlists, or seasonal events\n\nAdditional Features:\n- Full-width immersive mode\n- Smart resolution handling (up to 4K)\n- Full keyboard navigation & playback control\n- Wait-for-trailer options\n- Customizable pagination & animations\n\nIf you do not have write permissions to the web folder, please also install the file-transformation plugin.",
|
||||
"overview": "Transforms your Jellyfin home screen with an immersive, fully configurable media slideshow featuring video backdrops.",
|
||||
"owner": "CodeDevMLH",
|
||||
"category": "General",
|
||||
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.4.0.12",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.12/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "26edee51b52dcee4ecf388aa376f3869",
|
||||
"timestamp": "2026-02-04T18:07:40Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.11",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.11/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "ca0b3270eba5871e7a23db6b45bc5048",
|
||||
"timestamp": "2026-02-04T17:58:14Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.10",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.10/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "9618570053f7acef445a034c1e2e044b",
|
||||
"timestamp": "2026-02-04T17:51:25Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.9",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.9/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "29b3ffb9caeab135df88b6313032fc50",
|
||||
"timestamp": "2026-02-04T17:39:11Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.8",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.8/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "ec343204a7cd2c1af4013e645bdddcd3",
|
||||
"timestamp": "2026-02-04T17:27:22Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.7",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "450e5977228d08b8451b6047e4a6be94",
|
||||
"timestamp": "2026-02-04T17:09:28Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.6",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.6/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "348ebf449ac77fd156e2afbd03e80fce",
|
||||
"timestamp": "2026-02-04T16:40:21Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.5",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.5/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "3ba68bae1c492767bddab2dee2540226",
|
||||
"timestamp": "2026-02-04T16:23:14Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.4",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "30a15dd883de7656c4480cfa932e9858",
|
||||
"timestamp": "2026-02-04T16:17:15Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.3",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "9abf21c095e1ae99cdbeb51edb08f370",
|
||||
"timestamp": "2026-02-04T15:52:11Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.2",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "6026fb8878a51f6dbe18aab1ac006df8",
|
||||
"timestamp": "2026-02-04T15:45:39Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.1",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "4068c03b1ab809906d64d4faed1c1b0e",
|
||||
"timestamp": "2026-02-04T15:01:50Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.0.0",
|
||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.4.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "20faa2a703dbb46591f4bd09e6ab7ec3",
|
||||
"timestamp": "2026-02-04T12:49:45Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.0.3",
|
||||
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "1d9e0a8342d46f84aed3f7bd1bee32d3",
|
||||
"timestamp": "2026-02-04T01:41:35Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.0.2",
|
||||
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "22e79daa5f433ca09a3db4f8e37679b4",
|
||||
"timestamp": "2026-02-04T01:27:55Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.0.1",
|
||||
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "5a4f555e29c733dabd51169f6ace56eb",
|
||||
"timestamp": "2026-02-04T01:14:19Z"
|
||||
},
|
||||
{
|
||||
"version": "1.3.0.0",
|
||||
"changelog": "- feat: Enhance custom media ID functionality with manual trailer override support",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.3.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "83c26ba8f7ad6e1a7fe73c7190f532f3",
|
||||
"timestamp": "2026-02-04T00:07:15Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.7",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 3.0.9 from original repo",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "fa1bf48cff159cc7dbf0aab48511a37c",
|
||||
"timestamp": "2026-01-28T22:39:54Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.6",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.6/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "da73bb490548c122906419d2762a2d00",
|
||||
"timestamp": "2026-01-28T21:31:54Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.5",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.5/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "b5efea79ec465522dad31e4ee5f1710c",
|
||||
"timestamp": "2026-01-28T20:21:20Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.4",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "4683f75e2df2590db663303bdd329ccd",
|
||||
"timestamp": "2026-01-28T01:09:38Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.3",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "3c18a84b2f59c86c130e91da83f980a2",
|
||||
"timestamp": "2026-01-28T01:05:45Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.2",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "4109a3ea10eb3145217b24ee8f8b37b5",
|
||||
"timestamp": "2026-01-28T00:30:36Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.1",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "e73029ac767e24d36742a27678758b6f",
|
||||
"timestamp": "2026-01-28T00:17:28Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.3.0",
|
||||
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "ae6f34bee76f9d7873964a71ca191bf3",
|
||||
"timestamp": "2026-01-27T23:54:42Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.2.0",
|
||||
"changelog": "- Fixes issues with persistent slides-container visibility",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.2.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "3362f93815845c4e85b66b31bcd0f52c",
|
||||
"timestamp": "2026-01-24T22:53:55Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.1.0",
|
||||
"changelog": "- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 3.0.8 from original repo",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "70defc1fb29a17ff4c9362bf7bdc53b5",
|
||||
"timestamp": "2026-01-22T23:50:56Z"
|
||||
},
|
||||
{
|
||||
"version": "1.2.0.0",
|
||||
"changelog": "- Add video quality preference setting (Auto / 1080p / Highres)\n- Set preferred video quality on YouTube player based on setting",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "0b6379f68990026240d97fe8f77fbef1",
|
||||
"timestamp": "2026-01-08T23:30:58Z"
|
||||
},
|
||||
{
|
||||
"version": "1.1.2.0",
|
||||
"changelog": "- Add method to resume video playback when slideshow is active",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.2.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "a0e8ff5e59b22a1bdedc916cd5e1c16a",
|
||||
"timestamp": "2026-01-08T15:26:55Z"
|
||||
},
|
||||
{
|
||||
"version": "1.1.1.0",
|
||||
"changelog": "- Add method to pause all video playback when navigating away from home screen",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "09da95fc561b11191d23a5cfa30ea731",
|
||||
"timestamp": "2026-01-08T14:54:57Z"
|
||||
},
|
||||
{
|
||||
"version": "1.1.0.0",
|
||||
"changelog": "- 'custom media IDs' setting is now enabled by default (no input --> random selection)\n- improve GUID handling in slideshow manager to handle seperator and description",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "32305d72b8d704acf8eef0c22277fee9",
|
||||
"timestamp": "2026-01-08T02:15:50Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.0.3",
|
||||
"changelog": "fixes",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "e6180c42836069029072e96ac4860c42",
|
||||
"timestamp": "2026-01-06T23:26:29Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.0.2",
|
||||
"changelog": "fixes",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "1041b403ec0193c2172a6fe15501afd3",
|
||||
"timestamp": "2026-01-06T21:21:37Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.0.1",
|
||||
"changelog": "fixes",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "f4e6194a9cc72fdda7436161c73832de",
|
||||
"timestamp": "2026-01-06T21:18:33Z"
|
||||
},
|
||||
{
|
||||
"version": "1.0.0.0",
|
||||
"changelog": "Initial release",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "2ba7cc7f238f6aa7097628797935b903",
|
||||
"timestamp": "2026-01-06T18:56:30Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||