Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29a365b690 | ||
|
|
0ee0a65309 | ||
|
|
152d22b709 | ||
|
|
216dddad94 | ||
|
|
a6de148ca1 | ||
|
|
1f9378d74d | ||
|
|
cc025779dc | ||
|
|
3d10fd59b5 | ||
|
|
bf261eba96 | ||
|
|
e3116c30cf | ||
|
|
bb39c91d32 | ||
|
|
4e0c74614a | ||
|
|
b61bf92437 | ||
|
|
dfbd6ce964 | ||
|
|
f1cbcad177 | ||
|
|
feedd5d95f | ||
|
|
87d82cca15 | ||
|
|
a70746e095 | ||
|
|
f32283e0bf | ||
|
|
8f3985f307 | ||
|
|
0b2817ecff | ||
|
|
84faee2db4 | ||
|
|
3efa07ec51 | ||
|
|
af603e8803 | ||
|
|
fe63414e4b | ||
|
|
614c86083f | ||
|
|
17b9e8921e | ||
|
|
5075226ba8 | ||
|
|
443fb1008b | ||
|
|
8f12140aad | ||
|
|
1469712bb5 | ||
|
|
76ff03ac17 | ||
|
|
25b1ba5f2b | ||
|
|
eed3ca1860 | ||
|
|
0f14577f5d | ||
|
|
9999d6a633 | ||
|
|
a935fd7d5d | ||
|
|
60293e81e1 | ||
|
|
f54e55fe04 | ||
|
|
b3c42e2100 | ||
|
|
20ddbb32c7 | ||
|
|
14a5075e22 | ||
|
|
accb316a81 | ||
|
|
51c5cdf5bf | ||
|
|
306eff757b | ||
|
|
b8f28a5735 | ||
|
|
0932d9611d | ||
|
|
5e616db0ae | ||
|
|
538b0f2110 | ||
|
|
e717c07c54 | ||
|
|
0fa2d01ca3 | ||
|
|
5299b2a9d5 | ||
|
|
4f244988b9 | ||
|
|
5635a8f05e | ||
|
|
c901da4b0c | ||
|
|
c45cd0281f | ||
|
|
0c3e74829a | ||
|
|
e6b769f099 | ||
|
|
77371f7b98 | ||
|
|
988b800b6d | ||
|
|
4c6514ba9f | ||
|
|
6910eba7d6 | ||
|
|
3585b47b6c | ||
|
|
8170abdc94 | ||
|
|
535c0e17bf | ||
|
|
df1cee0eeb | ||
|
|
16cc56030f | ||
|
|
13ecbac96e | ||
|
|
8bca6f9052 | ||
|
|
217db2a66d | ||
|
|
5fd7bcb8b6 | ||
|
|
0b0e41a9f9 | ||
|
|
370db55714 | ||
|
|
e5d4800ef1 | ||
|
|
b910e92364 | ||
|
|
82283b1faf | ||
|
|
1588b1a6b2 | ||
|
|
de19466341 | ||
|
|
15054e314c | ||
|
|
3877f96b09 | ||
|
|
abca7cb3b6 | ||
|
|
998a0cfc68 | ||
|
|
63be8214d0 | ||
|
|
99411afffd | ||
|
|
9510ae6ba7 | ||
|
|
2030538acc | ||
|
|
463e9ef424 | ||
|
|
dbe9ce97d2 | ||
|
|
45c780c018 | ||
|
|
9de268caa7 | ||
|
|
a8298461f9 | ||
|
|
aa369e5c7b | ||
|
|
f62dc853be | ||
|
|
d0a7f5da8c | ||
|
|
1f140fd3c1 | ||
|
|
18474d4537 | ||
|
|
839424a960 | ||
|
|
8050aa8e76 | ||
|
|
0c173b9685 | ||
|
|
90ecc7c0ef | ||
|
|
c80d3726a6 | ||
|
|
a8e63c935f | ||
|
|
d931a5d589 | ||
|
|
629f13ab58 | ||
|
|
6a91784401 | ||
|
|
3f8777db9f | ||
|
|
4ec337a893 | ||
|
|
7366f2ca30 | ||
|
|
faabf13870 | ||
|
|
3e584fa419 | ||
|
|
13889c3c68 | ||
|
|
7af9eb5004 | ||
|
|
1e57d8acef | ||
|
|
5964d2cb34 | ||
|
|
6f3fd23879 | ||
|
|
47949e9820 | ||
|
|
5fe8da8347 | ||
|
|
a8842f8415 | ||
|
|
3ecaf5fae6 | ||
|
|
9d13d61e77 | ||
|
|
0e441b751b | ||
|
|
4f14c00636 | ||
|
|
2dbb4dcfea |
@@ -186,79 +186,79 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Update Message in Seasonals Repository
|
# Update Message in Seasonals Repository
|
||||||
- name: Checkout Seasonal Manifest Repo
|
# - name: Checkout Seasonal Manifest Repo
|
||||||
uses: actions/checkout@v6
|
# uses: actions/checkout@v6
|
||||||
with:
|
# with:
|
||||||
repository: ${{ github.repository_owner }}/Jellyfin-Seasonals-Plugin
|
# repository: ${{ github.repository_owner }}/Jellyfin-Seasonals-Plugin
|
||||||
path: seasonal-manifest
|
# path: seasonal-manifest
|
||||||
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
# token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
||||||
|
|
||||||
- name: Update Seasonal Manifest
|
# - name: Update Seasonal Manifest
|
||||||
shell: bash
|
# shell: bash
|
||||||
run: |
|
# run: |
|
||||||
cd seasonal-manifest
|
# cd seasonal-manifest
|
||||||
|
|
||||||
REPO_OWNER="${{ github.repository_owner }}"
|
# REPO_OWNER="${{ github.repository_owner }}"
|
||||||
REPO_NAME="${{ github.event.repository.name }}"
|
# REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
# 1. Get info from previous steps
|
# # 1. Get info from previous steps
|
||||||
VERSION="${{ env.VERSION }}"
|
# VERSION="${{ env.VERSION }}"
|
||||||
HASH="${{ env.ZIP_HASH }}"
|
# HASH="${{ env.ZIP_HASH }}"
|
||||||
TIME="${{ env.BUILD_TIME }}"
|
# TIME="${{ env.BUILD_TIME }}"
|
||||||
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.MediaBarEnhanced.zip"
|
# DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.MediaBarEnhanced.zip"
|
||||||
|
|
||||||
# 2. Get info from env
|
# # 2. Get info from env
|
||||||
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
|
# PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
|
||||||
CHANGELOG="${{ env.CHANGELOG }}"
|
# CHANGELOG="${{ env.CHANGELOG }}"
|
||||||
TARGET_ABI="${{ env.TARGET_ABI }}"
|
# TARGET_ABI="${{ env.TARGET_ABI }}"
|
||||||
|
|
||||||
echo "Updating Seasonal Manifest for Plugin GUID: $PLUGIN_GUID"
|
# echo "Updating Seasonal Manifest for Plugin GUID: $PLUGIN_GUID"
|
||||||
|
|
||||||
# 3. Update/Prepend entry in seasonal manifest.json
|
# # 3. Update/Prepend entry in seasonal manifest.json
|
||||||
# Logic:
|
# # Logic:
|
||||||
# - If array is empty or new version != old version: PREPEND new entry
|
# # - If array is empty or new version != old version: PREPEND new entry
|
||||||
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
|
# # - If new version == old version: OVERWRITE (update) existing entry (re-release)
|
||||||
|
|
||||||
jq --arg guid "$PLUGIN_GUID" \
|
# jq --arg guid "$PLUGIN_GUID" \
|
||||||
--arg hash "$HASH" \
|
# --arg hash "$HASH" \
|
||||||
--arg time "$TIME" \
|
# --arg time "$TIME" \
|
||||||
--arg url "$DOWNLOAD_URL" \
|
# --arg url "$DOWNLOAD_URL" \
|
||||||
--arg ver "$VERSION" \
|
# --arg ver "$VERSION" \
|
||||||
--arg changelog "$CHANGELOG" \
|
# --arg changelog "$CHANGELOG" \
|
||||||
--arg abi "$TARGET_ABI" \
|
# --arg abi "$TARGET_ABI" \
|
||||||
'map(if .guid == $guid then
|
# 'map(if .guid == $guid then
|
||||||
if .versions[0].version != $ver then
|
# if .versions[0].version != $ver then
|
||||||
# New Version -> Prepend
|
# # New Version -> Prepend
|
||||||
.versions = [{
|
# .versions = [{
|
||||||
"version": $ver,
|
# "version": $ver,
|
||||||
"changelog": $changelog,
|
# "changelog": $changelog,
|
||||||
"targetAbi": $abi,
|
# "targetAbi": $abi,
|
||||||
"sourceUrl": $url,
|
# "sourceUrl": $url,
|
||||||
"checksum": $hash,
|
# "checksum": $hash,
|
||||||
"timestamp": $time
|
# "timestamp": $time
|
||||||
}] + .versions
|
# }] + .versions
|
||||||
else
|
# else
|
||||||
# Same Version -> Update existing (overwrite top)
|
# # Same Version -> Update existing (overwrite top)
|
||||||
.versions[0].changelog = $changelog |
|
# .versions[0].changelog = $changelog |
|
||||||
.versions[0].targetAbi = $abi |
|
# .versions[0].targetAbi = $abi |
|
||||||
.versions[0].sourceUrl = $url |
|
# .versions[0].sourceUrl = $url |
|
||||||
.versions[0].checksum = $hash |
|
# .versions[0].checksum = $hash |
|
||||||
.versions[0].timestamp = $time
|
# .versions[0].timestamp = $time
|
||||||
end
|
# end
|
||||||
else . end)' \
|
# else . end)' \
|
||||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
# manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||||
|
|
||||||
- name: Commit and Push Seasonal Manifest
|
# - name: Commit and Push Seasonal Manifest
|
||||||
run: |
|
# run: |
|
||||||
cd seasonal-manifest
|
# cd seasonal-manifest
|
||||||
git config user.name "CodeDevMLH"
|
# git config user.name "CodeDevMLH"
|
||||||
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
|
# git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
|
||||||
|
|
||||||
# Check if there are changes
|
# # Check if there are changes
|
||||||
if [[ -n $(git status -s) ]]; then
|
# if [[ -n $(git status -s) ]]; then
|
||||||
git add manifest.json
|
# git add manifest.json
|
||||||
git commit -m "Auto-Update MediaBar Enhanced to v${{ env.VERSION }}"
|
# git commit -m "Auto-Update MediaBar Enhanced to v${{ env.VERSION }}"
|
||||||
git push
|
# git push
|
||||||
else
|
# else
|
||||||
echo "No changes to seasonal manifest."
|
# echo "No changes to seasonal manifest."
|
||||||
fi
|
# fi
|
||||||
134
.github/workflows/release_automation.yml
vendored
134
.github/workflows/release_automation.yml
vendored
@@ -222,79 +222,79 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Update Message in Seasonals Repository
|
# Update Message in Seasonals Repository
|
||||||
- name: Checkout Seasonal Manifest Repo
|
# - name: Checkout Seasonal Manifest Repo
|
||||||
uses: actions/checkout@v6
|
# uses: actions/checkout@v6
|
||||||
with:
|
# with:
|
||||||
repository: ${{ github.repository_owner }}/Jellyfin-Seasonals
|
# repository: ${{ github.repository_owner }}/Jellyfin-Seasonals
|
||||||
path: seasonal-manifest
|
# path: seasonal-manifest
|
||||||
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
# token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
|
||||||
|
|
||||||
- name: Update Seasonal Manifest
|
# - name: Update Seasonal Manifest
|
||||||
shell: bash
|
# shell: bash
|
||||||
run: |
|
# run: |
|
||||||
cd seasonal-manifest
|
# cd seasonal-manifest
|
||||||
|
|
||||||
REPO_OWNER="${{ github.repository_owner }}"
|
# REPO_OWNER="${{ github.repository_owner }}"
|
||||||
REPO_NAME="${{ github.event.repository.name }}"
|
# REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
# 1. Get info from previous steps
|
# # 1. Get info from previous steps
|
||||||
VERSION="${{ env.VERSION }}"
|
# VERSION="${{ env.VERSION }}"
|
||||||
HASH="${{ env.ZIP_HASH }}"
|
# HASH="${{ env.ZIP_HASH }}"
|
||||||
TIME="${{ env.BUILD_TIME }}"
|
# TIME="${{ env.BUILD_TIME }}"
|
||||||
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.MediaBarEnhanced.zip"
|
# DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.MediaBarEnhanced.zip"
|
||||||
|
|
||||||
# 2. Get info from env
|
# # 2. Get info from env
|
||||||
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
|
# PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
|
||||||
CHANGELOG="${{ env.CHANGELOG }}"
|
# CHANGELOG="${{ env.CHANGELOG }}"
|
||||||
TARGET_ABI="${{ env.TARGET_ABI }}"
|
# TARGET_ABI="${{ env.TARGET_ABI }}"
|
||||||
|
|
||||||
echo "Updating Seasonal Manifest for Plugin GUID: $PLUGIN_GUID"
|
# echo "Updating Seasonal Manifest for Plugin GUID: $PLUGIN_GUID"
|
||||||
|
|
||||||
# 3. Update/Prepend entry in seasonal manifest.json
|
# # 3. Update/Prepend entry in seasonal manifest.json
|
||||||
# Logic:
|
# # Logic:
|
||||||
# - If array is empty or new version != old version: PREPEND new entry
|
# # - If array is empty or new version != old version: PREPEND new entry
|
||||||
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
|
# # - If new version == old version: OVERWRITE (update) existing entry (re-release)
|
||||||
|
|
||||||
jq --arg guid "$PLUGIN_GUID" \
|
# jq --arg guid "$PLUGIN_GUID" \
|
||||||
--arg hash "$HASH" \
|
# --arg hash "$HASH" \
|
||||||
--arg time "$TIME" \
|
# --arg time "$TIME" \
|
||||||
--arg url "$DOWNLOAD_URL" \
|
# --arg url "$DOWNLOAD_URL" \
|
||||||
--arg ver "$VERSION" \
|
# --arg ver "$VERSION" \
|
||||||
--arg changelog "$CHANGELOG" \
|
# --arg changelog "$CHANGELOG" \
|
||||||
--arg abi "$TARGET_ABI" \
|
# --arg abi "$TARGET_ABI" \
|
||||||
'map(if .guid == $guid then
|
# 'map(if .guid == $guid then
|
||||||
if .versions[0].version != $ver then
|
# if .versions[0].version != $ver then
|
||||||
# New Version -> Prepend
|
# # New Version -> Prepend
|
||||||
.versions = [{
|
# .versions = [{
|
||||||
"version": $ver,
|
# "version": $ver,
|
||||||
"changelog": $changelog,
|
# "changelog": $changelog,
|
||||||
"targetAbi": $abi,
|
# "targetAbi": $abi,
|
||||||
"sourceUrl": $url,
|
# "sourceUrl": $url,
|
||||||
"checksum": $hash,
|
# "checksum": $hash,
|
||||||
"timestamp": $time
|
# "timestamp": $time
|
||||||
}] + .versions
|
# }] + .versions
|
||||||
else
|
# else
|
||||||
# Same Version -> Update existing (overwrite top)
|
# # Same Version -> Update existing (overwrite top)
|
||||||
.versions[0].changelog = $changelog |
|
# .versions[0].changelog = $changelog |
|
||||||
.versions[0].targetAbi = $abi |
|
# .versions[0].targetAbi = $abi |
|
||||||
.versions[0].sourceUrl = $url |
|
# .versions[0].sourceUrl = $url |
|
||||||
.versions[0].checksum = $hash |
|
# .versions[0].checksum = $hash |
|
||||||
.versions[0].timestamp = $time
|
# .versions[0].timestamp = $time
|
||||||
end
|
# end
|
||||||
else . end)' \
|
# else . end)' \
|
||||||
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
# manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
|
||||||
|
|
||||||
- name: Commit and Push Seasonal Manifest
|
# - name: Commit and Push Seasonal Manifest
|
||||||
run: |
|
# run: |
|
||||||
cd seasonal-manifest
|
# cd seasonal-manifest
|
||||||
git config user.name "CodeDevMLH"
|
# git config user.name "CodeDevMLH"
|
||||||
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
|
# git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
|
||||||
|
|
||||||
# Check if there are changes
|
# # Check if there are changes
|
||||||
if [[ -n $(git status -s) ]]; then
|
# if [[ -n $(git status -s) ]]; then
|
||||||
git add manifest.json
|
# git add manifest.json
|
||||||
git commit -m "Auto-Update MediaBar Enhanced to v${{ env.VERSION }}"
|
# git commit -m "Auto-Update MediaBar Enhanced to v${{ env.VERSION }}"
|
||||||
git push
|
# git push
|
||||||
else
|
# else
|
||||||
echo "No changes to seasonal manifest."
|
# echo "No changes to seasonal manifest."
|
||||||
fi
|
# fi
|
||||||
@@ -21,6 +21,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
|
|||||||
public bool SlideAnimationEnabled { get; set; } = true;
|
public bool SlideAnimationEnabled { get; set; } = true;
|
||||||
public bool EnableVideoBackdrop { get; set; } = true;
|
public bool EnableVideoBackdrop { get; set; } = true;
|
||||||
public bool UseSponsorBlock { get; set; } = true;
|
public bool UseSponsorBlock { get; set; } = true;
|
||||||
|
public bool PreferLocalTrailers { get; set; } = false;
|
||||||
public bool WaitForTrailerToEnd { get; set; } = true;
|
public bool WaitForTrailerToEnd { get; set; } = true;
|
||||||
public bool StartMuted { get; set; } = true;
|
public bool StartMuted { get; set; } = true;
|
||||||
public bool FullWidthVideo { get; set; } = true;
|
public bool FullWidthVideo { get; set; } = true;
|
||||||
@@ -35,5 +36,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
|
|||||||
public bool EnableSeasonalContent { get; set; } = false;
|
public bool EnableSeasonalContent { get; set; } = false;
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
public bool EnableClientSideSettings { get; set; } = false;
|
public bool EnableClientSideSettings { get; set; } = false;
|
||||||
|
public string SortBy { get; set; } = "Random";
|
||||||
|
public string SortOrder { get; set; } = "Ascending";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,19 @@
|
|||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" id="EnableVideoBackdrop"
|
<input is="emby-checkbox" type="checkbox" id="EnableVideoBackdrop"
|
||||||
name="EnableVideoBackdrop" />
|
name="EnableVideoBackdrop" />
|
||||||
<span>Enable Video Backdrops</span>
|
<span>Enable Trailer Backdrops</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription">Show video trailers as background if available.<br>Adds a
|
<div class="fieldDescription">Show trailers as background if available.<br>Adds a
|
||||||
mute/unmute and pause/play button to control the video in the right top corner.</div>
|
mute/unmute and pause/play button to control the video in the right top corner.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalTrailersContainer">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers"
|
||||||
|
name="PreferLocalTrailers" />
|
||||||
|
<span>Prefer Local Trailers</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">If enabled, local trailers will be preferred over remote (YouTube) trailers.</div>
|
||||||
|
</div>
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
|
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
|
||||||
@@ -69,7 +77,7 @@
|
|||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
|
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
|
||||||
name="EnableMobileVideo" />
|
name="EnableMobileVideo" />
|
||||||
<span>Enable Mobile Video</span>
|
<span>Enable Trailer On Mobile</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription">Allow video playback on mobile devices.</div>
|
<div class="fieldDescription">Allow video playback on mobile devices.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +88,7 @@
|
|||||||
<span>Show Trailer Button</span>
|
<span>Show Trailer Button</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if
|
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if
|
||||||
trailer is not set as backdrop.</div>
|
trailer is not set as backdrop or if no trailer is available.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,9 +123,15 @@
|
|||||||
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow.
|
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow.
|
||||||
You can separate them by new line or comma.
|
You can separate them by new line or comma.
|
||||||
<br><br>
|
<br><br>
|
||||||
<b>Manual Trailer Override:</b> You can specify a YouTube URL for an item by adding it in
|
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
|
||||||
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [https://youtu.be/...] DESCRIPTION</code>
|
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [Method] DESCRIPTION</code>.
|
||||||
<br><br>
|
<br>
|
||||||
|
Methods:
|
||||||
|
<ul>
|
||||||
|
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
|
||||||
|
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
You can also add a description after the ID using any separator like space, pipe
|
You can also add a description after the ID using any separator like space, pipe
|
||||||
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
|
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
|
||||||
<br><br>
|
<br><br>
|
||||||
@@ -153,8 +167,17 @@
|
|||||||
name="SlideAnimationEnabled" />
|
name="SlideAnimationEnabled" />
|
||||||
<span>Enable Slide Animations</span>
|
<span>Enable Slide Animations</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription">Enable the zooming-in effect when a new slide is
|
<div class="fieldDescription">Enable the zooming-in effect on background images when a new slide is
|
||||||
shown. Attention: This may cause performance issues on weaker client hardware.</div>
|
shown (does not affect trailer backdrops). Attention: This may cause performance issues on weaker client hardware.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableClientSideSettings"
|
||||||
|
name="EnableClientSideSettings" />
|
||||||
|
<span>Enable Client-Side Settings</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to
|
||||||
|
override settings (like disabling the bar or trailer backdrops) locally on their device.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label>
|
<label>
|
||||||
@@ -165,8 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="selectContainer">
|
<div class="selectContainer">
|
||||||
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
|
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
|
||||||
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality"
|
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select">
|
||||||
class="emby-select-withcolor emby-select">
|
|
||||||
<option value="Auto">Auto (Smart)</option>
|
<option value="Auto">Auto (Smart)</option>
|
||||||
<option value="Maximum">Maximum (4K+)</option>
|
<option value="Maximum">Maximum (4K+)</option>
|
||||||
<option value="1080p">1080p</option>
|
<option value="1080p">1080p</option>
|
||||||
@@ -221,15 +243,6 @@
|
|||||||
Space (pause), M (mute/unmute)) for
|
Space (pause), M (mute/unmute)) for
|
||||||
the slideshow.</div>
|
the slideshow.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label>
|
|
||||||
<input is="emby-checkbox" type="checkbox" id="EnableClientSideSettings"
|
|
||||||
name="EnableClientSideSettings" />
|
|
||||||
<span>Enable Client-Side Settings</span>
|
|
||||||
</label>
|
|
||||||
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to
|
|
||||||
override settings (like disabling the bar or video backdrops) locally on their device.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="sectionTitle">Time Settings</h2>
|
<h2 class="sectionTitle">Time Settings</h2>
|
||||||
<p>Leave a setting blank to use the default value.</p>
|
<p>Leave a setting blank to use the default value.</p>
|
||||||
@@ -274,6 +287,33 @@
|
|||||||
mobile).</div>
|
mobile).</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sectionTitle">Content Sorting</h2>
|
||||||
|
<div class="selectContainer">
|
||||||
|
<label class="selectLabel" for="SortBy">Sort By</label>
|
||||||
|
<select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select">
|
||||||
|
<option value="Random">Random</option>
|
||||||
|
<option value="Original">Original (Custom List Order)</option>
|
||||||
|
<option value="PremiereDate">Premiere Date</option>
|
||||||
|
<option value="ProductionYear">Production Year</option>
|
||||||
|
<option value="CriticRating">Critic Rating</option>
|
||||||
|
<option value="CommunityRating">Community Rating</option>
|
||||||
|
<option value="Name">Name</option>
|
||||||
|
<option value="Runtime">Runtime</option>
|
||||||
|
</select>
|
||||||
|
<div class="fieldDescription">Sort items by the selected criteria.</div>
|
||||||
|
</div>
|
||||||
|
<div class="selectContainer">
|
||||||
|
<label class="selectLabel" for="SortOrder">Sort Order</label>
|
||||||
|
<select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select">
|
||||||
|
<option value="Ascending">Ascending</option>
|
||||||
|
<option value="Descending">Descending</option>
|
||||||
|
</select>
|
||||||
|
<div class="fieldDescription">Sort items in Ascending or Descending order.</div>
|
||||||
|
</div>
|
||||||
|
<div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;">
|
||||||
|
<b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original' preserves Custom List order.
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="sectionTitle">Content Limits</h2>
|
<h2 class="sectionTitle">Content Limits</h2>
|
||||||
<p>Leave a setting blank to use the default value.</p>
|
<p>Leave a setting blank to use the default value.</p>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
@@ -297,7 +337,7 @@
|
|||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
|
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
|
||||||
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
|
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
|
||||||
<div class="fieldDescription">Number of images to preload.</div>
|
<div class="fieldDescription">Number of slides to preload.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
|
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
|
||||||
@@ -370,7 +410,8 @@
|
|||||||
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
||||||
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
||||||
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
||||||
'EnableSeasonalContent', 'EnableClientSideSettings'
|
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
|
||||||
|
'PreferLocalTrailers'
|
||||||
];
|
];
|
||||||
|
|
||||||
keys.forEach(function (key) {
|
keys.forEach(function (key) {
|
||||||
@@ -404,6 +445,23 @@
|
|||||||
updateDesc();
|
updateDesc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Prefer Local Trailers visibility
|
||||||
|
var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop');
|
||||||
|
var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer');
|
||||||
|
|
||||||
|
function updatePreferLocalVisibility() {
|
||||||
|
if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) {
|
||||||
|
if (preferLocalContainer) preferLocalContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
if (preferLocalContainer) preferLocalContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableVideoBackdropCheckbox) {
|
||||||
|
enableVideoBackdropCheckbox.addEventListener('change', updatePreferLocalVisibility);
|
||||||
|
updatePreferLocalVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -419,7 +477,8 @@
|
|||||||
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
||||||
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
||||||
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
||||||
'EnableSeasonalContent', 'EnableClientSideSettings'
|
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
|
||||||
|
'PreferLocalTrailers'
|
||||||
];
|
];
|
||||||
|
|
||||||
keys.forEach(function (key) {
|
keys.forEach(function (key) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||||
<Title>Jellyfin Media Bar Enhanced Plugin</Title>
|
<Title>Jellyfin Media Bar Enhanced Plugin</Title>
|
||||||
<Authors>CodeDevMLH</Authors>
|
<Authors>CodeDevMLH</Authors>
|
||||||
<Version>1.4.0.6</Version>
|
<Version>1.6.1.1</Version>
|
||||||
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
|
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="100%"
|
||||||
|
viewBox="0 0 661 614"
|
||||||
|
enable-background="new 0 0 661 614"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs6" /><sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="1.3925081"
|
||||||
|
inkscape:cx="330.33918"
|
||||||
|
inkscape:cy="307"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1057"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" />
|
||||||
|
|
||||||
|
<path
|
||||||
|
fill="#FFFFFF"
|
||||||
|
opacity="1.000000"
|
||||||
|
stroke="none"
|
||||||
|
d=" M327.000000,536.000000 C342.496460,536.000000 357.498688,536.258484 372.487610,535.922241 C385.664062,535.626770 398.866150,535.102905 411.983582,533.888245 C428.681671,532.341858 445.408508,530.610413 461.935608,527.840759 C481.796478,524.512451 501.467499,520.042786 521.199707,515.965698 C523.529053,515.484375 526.041382,514.803894 527.927551,513.457397 C534.700745,508.622009 542.319885,508.932159 550.045410,508.959991 C578.189880,509.061340 606.334900,509.000000 634.740234,509.000000 C637.186890,520.472595 639.957642,531.779480 636.933289,543.351501 C634.903503,551.117737 628.718933,555.934631 622.355957,559.773132 C608.164856,568.333984 592.050842,571.987366 576.187866,575.886841 C559.302490,580.037537 542.257751,583.610168 525.170837,586.844788 C514.799194,588.808228 504.204498,589.571716 493.731018,591.025574 C485.218262,592.207336 476.759888,593.799255 468.234467,594.866150 C460.604767,595.821045 452.927185,596.476501 445.251709,596.965637 C427.840851,598.075317 410.418152,598.998474 393.000580,600.003540 C387.415436,600.325806 381.835144,600.855774 376.246216,600.966370 C355.166992,601.383362 334.085815,601.853882 313.004028,601.930115 C300.255890,601.976196 287.500092,601.463989 274.757141,600.963013 C262.081879,600.464600 249.388809,600.007324 236.757446,598.910950 C221.974625,597.627930 207.229050,595.838806 192.505737,593.967224 C175.589020,591.816711 158.624741,589.840515 141.845245,586.863647 C122.976410,583.516113 104.227318,579.411011 85.549820,575.099731 C74.786339,572.615234 64.115028,569.507751 53.686581,565.866333 C45.305603,562.939758 36.992039,559.384155 30.888775,552.377869 C23.703732,544.129883 24.449305,534.207397 25.353891,524.421143 C25.793415,519.666260 27.276276,515.007690 28.352476,510.000000 C31.826927,510.000000 35.304031,510.000000 38.781136,510.000000 C64.774979,510.000000 90.768852,509.977386 116.762634,510.017090 C122.732361,510.026184 128.391159,510.172974 134.351212,512.686401 C143.412094,516.507568 153.202728,518.885010 162.889816,520.866455 C176.838547,523.719727 190.930084,525.975830 205.039062,527.917175 C219.893982,529.961182 234.816391,531.620972 249.755615,532.913391 C262.386749,534.006165 275.078796,534.468628 287.753540,534.962585 C300.662903,535.465698 313.584015,535.666687 327.000000,536.000000 z"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
fill="#FFFFFF"
|
||||||
|
opacity="1.000000"
|
||||||
|
stroke="none"
|
||||||
|
d=" M606.999634,140.000000 C616.128296,140.706787 625.146667,140.066467 633.298218,142.423615 C646.862671,146.345963 654.950256,156.523895 657.932922,170.401443 C658.647095,173.724014 658.955444,177.196487 658.957458,180.600327 C659.010925,271.765411 659.009338,362.930511 658.991943,454.095612 C658.988525,471.780304 647.686096,487.160645 629.451294,490.947906 C626.281982,491.606171 622.988159,491.957306 619.751709,491.958099 C427.255005,492.004913 234.758301,491.987091 42.261608,492.021698 C24.730577,492.024841 8.072522,479.153046 5.027515,461.896179 C4.492155,458.862152 4.043763,455.761963 4.042027,452.692047 C3.990298,361.193634 3.871316,269.694855 4.104546,178.197021 C4.153368,159.043365 16.296051,146.063629 33.331520,141.092468 C36.402164,140.196411 39.775433,140.042984 43.009106,140.042160 C230.839310,139.994934 418.669525,140.000000 606.999634,140.000000 M359.500000,157.000000 C289.517120,157.000000 219.534225,157.000000 149.551346,157.000000 C114.893280,157.000000 80.235199,156.980255 45.577160,157.011627 C34.607761,157.021545 24.557083,164.368423 22.403088,174.518036 C21.314177,179.648972 21.059866,185.035080 21.055122,190.306381 C20.977442,276.618225 21.132351,362.930573 20.868862,449.241699 C20.825279,463.518036 31.508949,474.175995 45.628529,474.158508 C236.414246,473.922333 427.200470,473.908447 617.986023,474.208954 C630.511597,474.228729 642.140198,463.677826 642.108398,450.146484 C641.898560,360.835785 642.000916,271.524384 641.999146,182.213196 C641.998840,166.797821 632.262817,157.001709 616.936340,157.001236 C531.457581,156.998657 445.978790,157.000000 359.500000,157.000000 z"
|
||||||
|
id="path3" />
|
||||||
|
<path
|
||||||
|
fill="#FFFFFF"
|
||||||
|
opacity="1.000000"
|
||||||
|
stroke="none"
|
||||||
|
d=" M410.281616,93.782341 C415.291992,100.787689 420.140594,107.470741 424.713318,114.337517 C425.884766,116.096649 426.184479,118.436256 426.884674,120.509186 C424.913452,121.006126 422.942780,121.934616 420.970886,121.937248 C363.000671,122.014679 305.030334,122.011337 247.060028,121.969559 C243.859818,121.967247 240.659912,121.524078 236.054047,121.181580 C238.077652,116.962379 239.199081,113.266785 241.360825,110.339432 C254.448532,92.616531 267.699677,75.012611 281.049347,57.485538 C288.331970,47.924042 295.607147,38.325375 303.457062,29.236992 C310.346344,21.260773 318.726776,14.498528 329.750397,14.243196 C337.574341,14.061975 345.864899,15.756383 351.433380,22.105671 C364.247803,36.716980 376.755737,51.604027 389.124115,66.596909 C396.351105,75.357414 403.069855,84.537186 410.281616,93.782341 z"
|
||||||
|
id="path4" />
|
||||||
|
<path
|
||||||
|
fill="#B9B9B9"
|
||||||
|
opacity="1.000000"
|
||||||
|
stroke="none"
|
||||||
|
d=" M360.000000,157.000000 C445.978790,157.000000 531.457581,156.998657 616.936340,157.001236 C632.262817,157.001709 641.998840,166.797821 641.999146,182.213196 C642.000916,271.524384 641.898560,360.835785 642.108398,450.146484 C642.140198,463.677826 630.511597,474.228729 617.986023,474.208954 C427.200470,473.908447 236.414246,473.922333 45.628529,474.158508 C31.508949,474.175995 20.825279,463.518036 20.868862,449.241699 C21.132351,362.930573 20.977442,276.618225 21.055122,190.306381 C21.059866,185.035080 21.314177,179.648972 22.403088,174.518036 C24.557083,164.368423 34.607761,157.021545 45.577160,157.011627 C80.235199,156.980255 114.893280,157.000000 149.551346,157.000000 C219.534225,157.000000 289.517120,157.000000 360.000000,157.000000 M265.000000,302.500000 C265.000000,329.638916 264.986816,356.777832 265.030060,383.916656 C265.032898,385.700653 265.134369,387.610443 265.756378,389.246338 C268.897522,397.507599 276.467865,400.490845 284.746582,395.464508 C301.954895,385.016602 318.697327,373.801392 335.631592,362.902100 C353.537262,351.377533 371.500580,339.941193 389.328918,328.298248 C400.978180,320.690613 400.993195,311.320953 389.382721,303.731628 C365.885529,288.372528 342.291901,273.160889 318.730255,257.900482 C307.365387,250.539673 296.161346,242.908707 284.556580,235.946579 C273.636658,229.395309 265.002625,234.531113 265.000702,247.055817 C264.997925,265.203888 265.000000,283.351929 265.000000,302.500000 z"
|
||||||
|
id="path5" />
|
||||||
|
<path
|
||||||
|
fill="#FFFFFF"
|
||||||
|
opacity="1.000000"
|
||||||
|
stroke="none"
|
||||||
|
d=" M265.000000,302.000000 C265.000000,283.351929 264.997925,265.203888 265.000702,247.055817 C265.002625,234.531113 273.636658,229.395309 284.556580,235.946579 C296.161346,242.908707 307.365387,250.539673 318.730255,257.900482 C342.291901,273.160889 365.885529,288.372528 389.382721,303.731628 C400.993195,311.320953 400.978180,320.690613 389.328918,328.298248 C371.500580,339.941193 353.537262,351.377533 335.631592,362.902100 C318.697327,373.801392 301.954895,385.016602 284.746582,395.464508 C276.467865,400.490845 268.897522,397.507599 265.756378,389.246338 C265.134369,387.610443 265.032898,385.700653 265.030060,383.916656 C264.986816,356.777832 265.000000,329.638916 265.000000,302.000000 z"
|
||||||
|
id="path6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.3 KiB |
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Jellyfin Slideshow by M0RPH3US v3.0.9
|
* Jellyfin Slideshow by M0RPH3US v4.0.1
|
||||||
* Modified by CodeDevMLH v1.1.0.0
|
* Modified by CodeDevMLH
|
||||||
*
|
*
|
||||||
* New features:
|
* New features:
|
||||||
* - optional Trailer background video support
|
* - optional Trailer background video support
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
* - option to disable loading screen
|
* - option to disable loading screen
|
||||||
* - option to put collection (boxsets) IDs into the slideshow to display their items
|
* - option to put collection (boxsets) IDs into the slideshow to display their items
|
||||||
* - option to enable client-side settings (allow users to override settings locally on their device)
|
* - option to enable client-side settings (allow users to override settings locally on their device)
|
||||||
|
* - option to enable seasonal content (only show items that are relevant to the current season/holiday)
|
||||||
|
* - option to prefer local trailers (from the media item) over online sources
|
||||||
|
* - options to sort the content by various criteria (PremiereDate, ProductionYear, Random, Original order, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap);
|
@import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap);
|
||||||
@@ -992,3 +995,14 @@
|
|||||||
.dots-container .slide-counter {
|
.dots-container .slide-counter {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-tv .video-backdrop {
|
||||||
|
mask-image: linear-gradient(to top,
|
||||||
|
#fff0 2%,
|
||||||
|
rgb(0 0 0 / 0.5) 6%,
|
||||||
|
#000000 8%);
|
||||||
|
-webkit-mask-image: linear-gradient(to top,
|
||||||
|
#fff0 2%,
|
||||||
|
rgb(0 0 0 / 0.5) 6%,
|
||||||
|
#000000 8%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Jellyfin Slideshow by M0RPH3US v3.0.9
|
* Jellyfin Slideshow by M0RPH3US v4.0.1
|
||||||
* Modified by CodeDevMLH v1.1.0.0
|
* Modified by CodeDevMLH
|
||||||
*
|
*
|
||||||
* New features:
|
* New features:
|
||||||
* - optional Trailer background video support
|
* - optional Trailer background video support
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
* - option to disable loading screen
|
* - option to disable loading screen
|
||||||
* - option to put collection (boxsets) IDs into the slideshow to display their items
|
* - option to put collection (boxsets) IDs into the slideshow to display their items
|
||||||
* - option to enable client-side settings (allow users to override settings locally on their device)
|
* - option to enable client-side settings (allow users to override settings locally on their device)
|
||||||
|
* - option to enable seasonal content (only show items that are relevant to the current season/holiday)
|
||||||
|
* - option to prefer local trailers (from the media item) over online sources
|
||||||
|
* - options to sort the content by various criteria (PremiereDate, ProductionYear, Random, Original order, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//Core Module Configuration
|
//Core Module Configuration
|
||||||
@@ -38,6 +41,7 @@ const CONFIG = {
|
|||||||
slideAnimationEnabled: true,
|
slideAnimationEnabled: true,
|
||||||
enableVideoBackdrop: true,
|
enableVideoBackdrop: true,
|
||||||
useSponsorBlock: true,
|
useSponsorBlock: true,
|
||||||
|
preferLocalTrailers: false,
|
||||||
waitForTrailerToEnd: true,
|
waitForTrailerToEnd: true,
|
||||||
startMuted: true,
|
startMuted: true,
|
||||||
fullWidthVideo: true,
|
fullWidthVideo: true,
|
||||||
@@ -51,6 +55,8 @@ const CONFIG = {
|
|||||||
customMediaIds: "",
|
customMediaIds: "",
|
||||||
enableLoadingScreen: true,
|
enableLoadingScreen: true,
|
||||||
enableClientSideSettings: false,
|
enableClientSideSettings: false,
|
||||||
|
sortBy: "Random",
|
||||||
|
sortOrder: "Ascending",
|
||||||
};
|
};
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
@@ -81,6 +87,8 @@ const STATE = {
|
|||||||
sponsorBlockInterval: null,
|
sponsorBlockInterval: null,
|
||||||
isMuted: CONFIG.startMuted,
|
isMuted: CONFIG.startMuted,
|
||||||
customTrailerUrls: {},
|
customTrailerUrls: {},
|
||||||
|
ytPromise: null,
|
||||||
|
autoplayTimeouts: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -459,6 +467,66 @@ waitForApiClientAndInitialize();
|
|||||||
* Utility functions for slide creation and management
|
* Utility functions for slide creation and management
|
||||||
*/
|
*/
|
||||||
const SlideUtils = {
|
const SlideUtils = {
|
||||||
|
/**
|
||||||
|
* Sorts items based on configuration
|
||||||
|
* @param {Array<Object>} items - Array of item objects
|
||||||
|
* @param {string} sortBy - Sort criteria
|
||||||
|
* @param {string} sortOrder - Sort order 'Ascending' or 'Descending'
|
||||||
|
* @returns {Array<Object>} Sorted array of items
|
||||||
|
*/
|
||||||
|
sortItems(items, sortBy, sortOrder) {
|
||||||
|
if (sortBy === 'Random' || sortBy === 'Original') {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleCompare = (a, b) => {
|
||||||
|
if (a < b) return -1;
|
||||||
|
if (a > b) return 1;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorted = [...items].sort((a, b) => {
|
||||||
|
let valA, valB;
|
||||||
|
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'PremiereDate':
|
||||||
|
valA = new Date(a.PremiereDate).getTime();
|
||||||
|
valB = new Date(b.PremiereDate).getTime();
|
||||||
|
break;
|
||||||
|
case 'ProductionYear':
|
||||||
|
valA = a.ProductionYear || 0;
|
||||||
|
valB = b.ProductionYear || 0;
|
||||||
|
break;
|
||||||
|
case 'CriticRating':
|
||||||
|
valA = a.CriticRating || 0;
|
||||||
|
valB = b.CriticRating || 0;
|
||||||
|
break;
|
||||||
|
case 'CommunityRating':
|
||||||
|
valA = a.CommunityRating || 0;
|
||||||
|
valB = b.CommunityRating || 0;
|
||||||
|
break;
|
||||||
|
case 'Runtime':
|
||||||
|
valA = a.RunTimeTicks || 0;
|
||||||
|
valB = b.RunTimeTicks || 0;
|
||||||
|
break;
|
||||||
|
case 'Name':
|
||||||
|
valA = (a.Name || '').toLowerCase();
|
||||||
|
valB = (b.Name || '').toLowerCase();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleCompare(valA, valB);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sortOrder === 'Descending') {
|
||||||
|
sorted.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffles array elements randomly
|
* Shuffles array elements randomly
|
||||||
* @param {Array} array - Array to shuffle
|
* @param {Array} array - Array to shuffle
|
||||||
@@ -541,7 +609,10 @@ const SlideUtils = {
|
|||||||
getOrCreateSlidesContainer() {
|
getOrCreateSlidesContainer() {
|
||||||
let container = document.getElementById("slides-container");
|
let container = document.getElementById("slides-container");
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = this.createElement("div", { id: "slides-container" });
|
container = this.createElement("div", {
|
||||||
|
id: "slides-container",
|
||||||
|
className: "noautofocus"
|
||||||
|
});
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
}
|
}
|
||||||
return container;
|
return container;
|
||||||
@@ -582,23 +653,25 @@ const SlideUtils = {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
loadYouTubeIframeAPI() {
|
loadYouTubeIframeAPI() {
|
||||||
return new Promise((resolve) => {
|
if (STATE.slideshow.ytPromise) return STATE.slideshow.ytPromise;
|
||||||
|
|
||||||
|
STATE.slideshow.ytPromise = new Promise((resolve) => {
|
||||||
if (window.YT && window.YT.Player) {
|
if (window.YT && window.YT.Player) {
|
||||||
resolve();
|
resolve(window.YT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = document.createElement('script');
|
window.onYouTubeIframeAPIReady = () => resolve(window.YT);
|
||||||
tag.src = "https://www.youtube.com/iframe_api";
|
|
||||||
const firstScriptTag = document.getElementsByTagName('script')[0];
|
|
||||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
|
||||||
|
|
||||||
const previousOnYouTubeIframeAPIReady = window.onYouTubeIframeAPIReady;
|
if (!document.querySelector('script[src*="youtube.com/iframe_api"]')) {
|
||||||
window.onYouTubeIframeAPIReady = () => {
|
const tag = document.createElement('script');
|
||||||
if (previousOnYouTubeIframeAPIReady) previousOnYouTubeIframeAPIReady();
|
tag.src = "https://www.youtube.com/iframe_api";
|
||||||
resolve();
|
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||||
};
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return STATE.slideshow.ytPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -667,7 +740,11 @@ const SlideUtils = {
|
|||||||
autoplay: 1,
|
autoplay: 1,
|
||||||
controls: 1,
|
controls: 1,
|
||||||
iv_load_policy: 3,
|
iv_load_policy: 3,
|
||||||
rel: 0
|
rel: 0,
|
||||||
|
playsinline: 1,
|
||||||
|
origin: window.location.origin,
|
||||||
|
widget_referrer: window.location.href,
|
||||||
|
enablejsapi: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -957,8 +1034,8 @@ const ApiUtils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${STATE.jellyfinData.serverAddress}/Items/${itemId}`,
|
// `${STATE.jellyfinData.serverAddress}/Items/${itemId}`,
|
||||||
// `${STATE.jellyfinData.serverAddress}/Users/${STATE.jellyfinData.userId}/Items/${itemId}?Fields=Overview,RemoteTrailers,Genres,CommunityRating,CriticRating,OfficialRating,PremiereDate,RunTimeTicks,ProductionYear,MediaSources`,
|
`${STATE.jellyfinData.serverAddress}/Items/${itemId}?Fields=Overview,RemoteTrailers,Genres,CommunityRating,CriticRating,OfficialRating,PremiereDate,ProductionYear,MediaSources,RunTimeTicks,LocalTrailerCount`,
|
||||||
{
|
{
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
}
|
}
|
||||||
@@ -1030,8 +1107,16 @@ const ApiUtils = {
|
|||||||
|
|
||||||
console.log("Fetching random items from server...");
|
console.log("Fetching random items from server...");
|
||||||
|
|
||||||
|
let sortParams = `sortBy=${CONFIG.sortBy}`;
|
||||||
|
|
||||||
|
if (CONFIG.sortBy === 'Random' || CONFIG.sortBy === 'Original') {
|
||||||
|
sortParams = 'sortBy=Random';
|
||||||
|
} else {
|
||||||
|
sortParams += `&sortOrder=${CONFIG.sortOrder}`;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${STATE.jellyfinData.serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&sortBy=Random&isPlayed=False&enableUserData=true&Limit=${CONFIG.maxItems}&fields=Id`,
|
`${STATE.jellyfinData.serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}&isPlayed=False&enableUserData=true&Limit=${CONFIG.maxItems}&fields=Id`,
|
||||||
{
|
{
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
}
|
}
|
||||||
@@ -1250,6 +1335,42 @@ const ApiUtils = {
|
|||||||
console.error(`Error fetching collection items for ${collectionId}:`, error);
|
console.error(`Error fetching collection items for ${collectionId}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the first local trailer for an item
|
||||||
|
* @param {string} itemId - Item ID
|
||||||
|
* @returns {Promise<Object|null>} Trailer data object {id, url} or null
|
||||||
|
*/
|
||||||
|
async fetchLocalTrailer(itemId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${STATE.jellyfinData.serverAddress}/Users/${STATE.jellyfinData.userId}/Items/${itemId}/LocalTrailers`,
|
||||||
|
{
|
||||||
|
headers: this.getAuthHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailers = await response.json();
|
||||||
|
if (trailers && trailers.length > 0) {
|
||||||
|
const trailer = trailers[0];
|
||||||
|
const mediaSourceId = trailer.MediaSources && trailer.MediaSources[0] ? trailer.MediaSources[0].Id : trailer.Id;
|
||||||
|
|
||||||
|
// Return object with ID and URL
|
||||||
|
return {
|
||||||
|
id: trailer.Id,
|
||||||
|
url: `${STATE.jellyfinData.serverAddress}/Videos/${trailer.Id}/stream.mp4?Static=true&mediaSourceId=${mediaSourceId}&api_key=${STATE.jellyfinData.accessToken}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching local trailer for ${itemId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1306,6 +1427,24 @@ class SlideTimer {
|
|||||||
*/
|
*/
|
||||||
const VisibilityObserver = {
|
const VisibilityObserver = {
|
||||||
updateVisibility() {
|
updateVisibility() {
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
|
||||||
|
// If a full screen video player is active, hide slideshow and stop playback
|
||||||
|
if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) {
|
||||||
|
const container = document.getElementById("slides-container");
|
||||||
|
if (container) {
|
||||||
|
container.style.display = "none";
|
||||||
|
container.style.visibility = "hidden";
|
||||||
|
container.style.pointerEvents = "none";
|
||||||
|
}
|
||||||
|
if (STATE.slideshow.slideInterval) {
|
||||||
|
STATE.slideshow.slideInterval.stop();
|
||||||
|
}
|
||||||
|
SlideshowManager.stopAllPlayback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const activeTab = document.querySelector(".emby-tab-button-active");
|
const activeTab = document.querySelector(".emby-tab-button-active");
|
||||||
const container = document.getElementById("slides-container");
|
const container = document.getElementById("slides-container");
|
||||||
|
|
||||||
@@ -1432,12 +1571,37 @@ const SlideCreator = {
|
|||||||
let isVideo = false;
|
let isVideo = false;
|
||||||
let trailerUrl = null;
|
let trailerUrl = null;
|
||||||
|
|
||||||
// 1. Check for Remote Trailers (YouTube)
|
// 1. Check for Remote/Local Trailers
|
||||||
// Priority: Custom Config URL > Metadata RemoteTrailer
|
// Priority: Custom Config URL > (PreferLocal -> Local) > Metadata RemoteTrailer
|
||||||
|
|
||||||
|
// 1a. Custom URL override
|
||||||
if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) {
|
if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) {
|
||||||
trailerUrl = STATE.slideshow.customTrailerUrls[itemId];
|
const customValue = STATE.slideshow.customTrailerUrls[itemId];
|
||||||
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
|
|
||||||
} else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) {
|
// Check if the custom value is a Jellyfin Item ID (GUID)
|
||||||
|
const guidMatch = customValue.match(/^([0-9a-f]{32})$/i);
|
||||||
|
|
||||||
|
if (guidMatch) {
|
||||||
|
const videoId = guidMatch[1];
|
||||||
|
console.log(`Using custom local video ID for ${itemId}: ${videoId}`);
|
||||||
|
|
||||||
|
trailerUrl = {
|
||||||
|
id: videoId,
|
||||||
|
url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?Static=true&api_key=${STATE.jellyfinData.accessToken}`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Assume it's a standard URL (YouTube, etc.)
|
||||||
|
trailerUrl = customValue;
|
||||||
|
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 1b. Check Local Trailer if preferred
|
||||||
|
else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) {
|
||||||
|
trailerUrl = item.localTrailerUrl;
|
||||||
|
console.log(`Using local trailer for ${itemId}: ${trailerUrl}`);
|
||||||
|
}
|
||||||
|
// 1c. Fallback to Remote Trailer
|
||||||
|
else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) {
|
||||||
trailerUrl = item.RemoteTrailers[0].Url;
|
trailerUrl = item.RemoteTrailers[0].Url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1454,12 +1618,17 @@ const SlideCreator = {
|
|||||||
let videoId = null;
|
let videoId = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(trailerUrl);
|
let urlToCheck = trailerUrl;
|
||||||
if (urlObj.hostname.includes('youtube.com') || urlObj.hostname.includes('youtu.be')) {
|
if (typeof trailerUrl === 'object' && trailerUrl.url) {
|
||||||
|
urlToCheck = trailerUrl.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlObjChecked = new URL(urlToCheck);
|
||||||
|
if (urlObjChecked.hostname.includes('youtube.com') || urlObjChecked.hostname.includes('youtu.be')) {
|
||||||
isYoutube = true;
|
isYoutube = true;
|
||||||
videoId = urlObj.searchParams.get('v');
|
videoId = urlObjChecked.searchParams.get('v');
|
||||||
if (!videoId && urlObj.hostname.includes('youtu.be')) {
|
if (!videoId && urlObjChecked.hostname.includes('youtu.be')) {
|
||||||
videoId = urlObj.pathname.substring(1);
|
videoId = urlObjChecked.pathname.substring(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1488,7 +1657,11 @@ const SlideCreator = {
|
|||||||
fs: 0,
|
fs: 0,
|
||||||
iv_load_policy: 3,
|
iv_load_policy: 3,
|
||||||
rel: 0,
|
rel: 0,
|
||||||
loop: 0
|
loop: 0,
|
||||||
|
playsinline: 1,
|
||||||
|
origin: window.location.origin,
|
||||||
|
widget_referrer: window.location.href,
|
||||||
|
enablejsapi: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine video quality
|
// Determine video quality
|
||||||
@@ -1539,12 +1712,32 @@ const SlideCreator = {
|
|||||||
event.target.setPlaybackQuality(quality);
|
event.target.setPlaybackQuality(quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only play if this is the active slide
|
// Only play if this is the active slide AND the slideshow is visible
|
||||||
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
|
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
|
||||||
if (slide && slide.classList.contains('active')) {
|
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
|
||||||
|
|
||||||
|
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
|
||||||
|
const currentIndex = STATE.slideshow.currentSlideIndex;
|
||||||
|
const currentItemId = STATE.slideshow.itemIds[currentIndex];
|
||||||
|
if (currentItemId !== itemId) {
|
||||||
|
console.log(`Slide ${itemId} is no longer active (current: ${currentItemId}), aborting playback.`);
|
||||||
|
event.target.mute(); // Mute just in case
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.target.playVideo();
|
event.target.playVideo();
|
||||||
|
|
||||||
// Check if it actually started playing after a short delay (handling autoplay blocks)
|
// Check if it actually started playing after a short delay (handling autoplay blocks)
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
|
// Re-check conditions before processing fallback
|
||||||
|
const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
|
||||||
|
if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) {
|
||||||
|
console.log(`Navigation detected during autoplay check for ${itemId}, stopping video.`);
|
||||||
|
try {
|
||||||
|
event.target.stopVideo();
|
||||||
|
} catch (e) { console.warn("Error stopping video in timeout:", e); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.target.getPlayerState() !== YT.PlayerState.PLAYING &&
|
if (event.target.getPlayerState() !== YT.PlayerState.PLAYING &&
|
||||||
event.target.getPlayerState() !== YT.PlayerState.BUFFERING) {
|
event.target.getPlayerState() !== YT.PlayerState.BUFFERING) {
|
||||||
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
|
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
|
||||||
@@ -1553,6 +1746,9 @@ const SlideCreator = {
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
if (!STATE.slideshow.autoplayTimeouts) STATE.slideshow.autoplayTimeouts = [];
|
||||||
|
STATE.slideshow.autoplayTimeouts.push(timeoutId);
|
||||||
|
|
||||||
// Pause slideshow timer when video starts if configured
|
// Pause slideshow timer when video starts if configured
|
||||||
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
|
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
|
||||||
STATE.slideshow.slideInterval.stop();
|
STATE.slideshow.slideInterval.stop();
|
||||||
@@ -1585,11 +1781,11 @@ const SlideCreator = {
|
|||||||
|
|
||||||
const videoAttributes = {
|
const videoAttributes = {
|
||||||
className: "backdrop video-backdrop",
|
className: "backdrop video-backdrop",
|
||||||
src: trailerUrl,
|
src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl),
|
||||||
autoplay: false,
|
autoplay: false,
|
||||||
preload: "auto",
|
preload: "auto",
|
||||||
loop: false,
|
loop: false,
|
||||||
style: "object-fit: cover; width: 100%; height: 100%; pointer-events: none;"
|
style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (STATE.slideshow.isMuted) {
|
if (STATE.slideshow.isMuted) {
|
||||||
@@ -1876,11 +2072,20 @@ const SlideCreator = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a trailer button
|
* Creates a trailer button
|
||||||
* @param {string} url - Trailer URL
|
* @param {string|Object} trailerInfo - Trailer URL string or object {id, url}
|
||||||
* @returns {HTMLElement} Trailer button element
|
* @returns {HTMLElement} Trailer button element
|
||||||
*/
|
*/
|
||||||
createTrailerButton(url) {
|
createTrailerButton(trailerInfo) {
|
||||||
const trailerText = LocalizationUtils.getLocalizedString('Trailer', 'Trailer');
|
const trailerText = LocalizationUtils.getLocalizedString('Trailer', 'Trailer');
|
||||||
|
|
||||||
|
let url = trailerInfo;
|
||||||
|
let localTrailerId = null;
|
||||||
|
|
||||||
|
if (typeof trailerInfo === 'object' && trailerInfo !== null) {
|
||||||
|
url = trailerInfo.url;
|
||||||
|
localTrailerId = trailerInfo.id;
|
||||||
|
}
|
||||||
|
|
||||||
return SlideUtils.createElement("button", {
|
return SlideUtils.createElement("button", {
|
||||||
className: "detailButton trailer-button",
|
className: "detailButton trailer-button",
|
||||||
innerHTML: `<span class="material-icons">movie</span> <span class="trailer-text">${trailerText}</span>`,
|
innerHTML: `<span class="material-icons">movie</span> <span class="trailer-text">${trailerText}</span>`,
|
||||||
@@ -1888,7 +2093,13 @@ const SlideCreator = {
|
|||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
SlideUtils.openVideoModal(url);
|
|
||||||
|
if (localTrailerId) {
|
||||||
|
// Play local trailer using native player
|
||||||
|
ApiUtils.playItem(localTrailerId);
|
||||||
|
} else {
|
||||||
|
SlideUtils.openVideoModal(url);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1931,6 +2142,11 @@ const SlideCreator = {
|
|||||||
|
|
||||||
const item = await ApiUtils.fetchItemDetails(itemId);
|
const item = await ApiUtils.fetchItemDetails(itemId);
|
||||||
|
|
||||||
|
// Pre-fetch local trailer URL if needed
|
||||||
|
if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0) {
|
||||||
|
item.localTrailerUrl = await ApiUtils.fetchLocalTrailer(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
const slideElement = this.createSlideElement(
|
const slideElement = this.createSlideElement(
|
||||||
item,
|
item,
|
||||||
item.Type === "Movie" ? "Movie" : "TV Show"
|
item.Type === "Movie" ? "Movie" : "TV Show"
|
||||||
@@ -2028,6 +2244,16 @@ const SlideshowManager = {
|
|||||||
let previousVisibleSlide;
|
let previousVisibleSlide;
|
||||||
try {
|
try {
|
||||||
const container = SlideUtils.getOrCreateSlidesContainer();
|
const container = SlideUtils.getOrCreateSlidesContainer();
|
||||||
|
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
let focusSelector = null;
|
||||||
|
if (container.contains(activeElement)) {
|
||||||
|
if (activeElement.classList.contains('play-button')) focusSelector = '.play-button';
|
||||||
|
else if (activeElement.classList.contains('detail-button')) focusSelector = '.detail-button';
|
||||||
|
else if (activeElement.classList.contains('favorite-button')) focusSelector = '.favorite-button';
|
||||||
|
else if (activeElement.classList.contains('trailer-button')) focusSelector = '.trailer-button';
|
||||||
|
}
|
||||||
|
|
||||||
const totalItems = STATE.slideshow.totalItems;
|
const totalItems = STATE.slideshow.totalItems;
|
||||||
|
|
||||||
index = Math.max(0, Math.min(index, totalItems - 1));
|
index = Math.max(0, Math.min(index, totalItems - 1));
|
||||||
@@ -2056,6 +2282,15 @@ const SlideshowManager = {
|
|||||||
|
|
||||||
currentSlide.classList.add("active");
|
currentSlide.classList.add("active");
|
||||||
|
|
||||||
|
if (focusSelector) {
|
||||||
|
const target = currentSlide.querySelector(focusSelector);
|
||||||
|
if (target) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
target.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Manage Video Playback: Stop others, Play current
|
// Manage Video Playback: Stop others, Play current
|
||||||
|
|
||||||
// 1. Pause all other YouTube players
|
// 1. Pause all other YouTube players
|
||||||
@@ -2063,8 +2298,14 @@ const SlideshowManager = {
|
|||||||
Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
|
Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
|
||||||
if (id !== currentItemId) {
|
if (id !== currentItemId) {
|
||||||
const p = STATE.slideshow.videoPlayers[id];
|
const p = STATE.slideshow.videoPlayers[id];
|
||||||
if (p && typeof p.pauseVideo === 'function') {
|
if (p) {
|
||||||
p.pauseVideo();
|
try {
|
||||||
|
if (typeof p.pauseVideo === 'function') {
|
||||||
|
p.pauseVideo();
|
||||||
|
} else if (p.tagName === 'VIDEO') {
|
||||||
|
p.pause();
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn("Error pausing player", id, e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2072,8 +2313,11 @@ const SlideshowManager = {
|
|||||||
|
|
||||||
// 2. Pause all other HTML5 videos e.g. local trailers
|
// 2. Pause all other HTML5 videos e.g. local trailers
|
||||||
document.querySelectorAll('video').forEach(video => {
|
document.querySelectorAll('video').forEach(video => {
|
||||||
if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) {
|
const slideParent = video.closest('.slide');
|
||||||
video.pause();
|
if (slideParent && slideParent.dataset.itemId !== currentItemId) {
|
||||||
|
try {
|
||||||
|
video.pause();
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2277,7 +2521,14 @@ const SlideshowManager = {
|
|||||||
const index = STATE.slideshow.itemIds.indexOf(itemId);
|
const index = STATE.slideshow.itemIds.indexOf(itemId);
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
const distance = Math.abs(index - currentIndex);
|
const totalItems = STATE.slideshow.itemIds.length;
|
||||||
|
|
||||||
|
// Calculate wrapped distance
|
||||||
|
let distance = Math.abs(index - currentIndex);
|
||||||
|
if (totalItems > keepRange * 2) {
|
||||||
|
distance = Math.min(distance, totalItems - distance);
|
||||||
|
}
|
||||||
|
|
||||||
if (distance > keepRange) {
|
if (distance > keepRange) {
|
||||||
// Destroy video player if exists
|
// Destroy video player if exists
|
||||||
if (STATE.slideshow.videoPlayers[itemId]) {
|
if (STATE.slideshow.videoPlayers[itemId]) {
|
||||||
@@ -2423,15 +2674,26 @@ const SlideshowManager = {
|
|||||||
* Used when navigating away from the home screen
|
* Used when navigating away from the home screen
|
||||||
*/
|
*/
|
||||||
stopAllPlayback() {
|
stopAllPlayback() {
|
||||||
// 1. Pause all YouTube players
|
// Clear any pending autoplay timeouts
|
||||||
|
if (STATE.slideshow.autoplayTimeouts) {
|
||||||
|
STATE.slideshow.autoplayTimeouts.forEach(id => clearTimeout(id));
|
||||||
|
STATE.slideshow.autoplayTimeouts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Stop all YouTube players
|
||||||
if (STATE.slideshow.videoPlayers) {
|
if (STATE.slideshow.videoPlayers) {
|
||||||
Object.values(STATE.slideshow.videoPlayers).forEach(player => {
|
Object.values(STATE.slideshow.videoPlayers).forEach(player => {
|
||||||
try {
|
try {
|
||||||
if (player && typeof player.pauseVideo === 'function') {
|
if (player && typeof player.stopVideo === 'function') {
|
||||||
|
player.stopVideo();
|
||||||
|
if (typeof player.clearVideo === 'function') {
|
||||||
|
player.clearVideo();
|
||||||
|
}
|
||||||
|
} else if (player && typeof player.pauseVideo === 'function') {
|
||||||
player.pauseVideo();
|
player.pauseVideo();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Error pausing YouTube player:", e);
|
console.warn("Error pausing/stopping YouTube player:", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2530,48 +2792,80 @@ const SlideshowManager = {
|
|||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
const container = document.getElementById("slides-container");
|
const container = document.getElementById("slides-container");
|
||||||
// Allow interaction if container is visible, even if not strictly focused
|
|
||||||
if (!container || container.style.display === "none") {
|
if (!container || container.style.display === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
|
const isTvDevice = window.browser && window.browser.tv;
|
||||||
|
const isTvLayout = window.layoutManager && window.layoutManager.tv;
|
||||||
|
const hasTvClass = document.documentElement.classList.contains('layout-tv') || document.body.classList.contains('layout-tv');
|
||||||
|
const isTvMode = isTvDevice || isTvLayout || hasTvClass;
|
||||||
|
|
||||||
|
// Check Focus State
|
||||||
|
const isBodyFocused = activeElement === document.body;
|
||||||
|
const hasDirectFocus = container.contains(activeElement) || activeElement === container;
|
||||||
|
|
||||||
|
// Determine if we should handle navigation keys (Arrows, Space, M)
|
||||||
|
// TV Mode: Strict focus required (must be on slideshow)
|
||||||
|
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused)
|
||||||
|
const canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused);
|
||||||
|
|
||||||
|
// Check for Input Fields (always ignore typing)
|
||||||
|
const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
|
||||||
|
if (isInputElement) return;
|
||||||
|
|
||||||
|
// Check active video players (ignore if video is playing/overlay is open)
|
||||||
|
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||||
|
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||||
|
const isVideoOpen = (videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'));
|
||||||
|
if (isVideoOpen) return;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
if (focusElement && focusElement.classList.contains("detail-button")) {
|
if (canControlSlideshow) {
|
||||||
focusElement.previousElementSibling.focus();
|
|
||||||
} else {
|
|
||||||
SlideshowManager.nextSlide();
|
SlideshowManager.nextSlide();
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
if (focusElement && focusElement.classList.contains("play-button")) {
|
if (canControlSlideshow) {
|
||||||
focusElement.nextElementSibling.focus();
|
|
||||||
} else {
|
|
||||||
SlideshowManager.prevSlide();
|
SlideshowManager.prevSlide();
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case " ": // Space bar
|
case " ": // Space bar
|
||||||
this.togglePause();
|
if (canControlSlideshow) {
|
||||||
e.preventDefault();
|
this.togglePause();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "m": // Mute toggle
|
case "m": // Mute toggle
|
||||||
case "M":
|
case "M":
|
||||||
this.toggleMute();
|
if (canControlSlideshow) {
|
||||||
e.preventDefault();
|
this.toggleMute();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "Enter":
|
case "Enter":
|
||||||
if (focusElement) {
|
// Enter always requires direct focus on the slideshow to avoid conflicts
|
||||||
focusElement.click();
|
if (hasDirectFocus) {
|
||||||
|
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
|
||||||
|
if (currentItemId) {
|
||||||
|
if (window.Emby && window.Emby.Page) {
|
||||||
|
Emby.Page.show(
|
||||||
|
`/details?id=${currentItemId}&serverId=${STATE.jellyfinData.serverId}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.location.href = `#/details?id=${currentItemId}&serverId=${STATE.jellyfinData.serverId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2763,9 +3057,27 @@ const SlideshowManager = {
|
|||||||
if (itemIds.length === 0) {
|
if (itemIds.length === 0) {
|
||||||
console.log("No custom list found, fetching random items from server...");
|
console.log("No custom list found, fetching random items from server...");
|
||||||
itemIds = await ApiUtils.fetchItemIdsFromServer();
|
itemIds = await ApiUtils.fetchItemIdsFromServer();
|
||||||
}
|
|
||||||
|
|
||||||
itemIds = SlideUtils.shuffleArray(itemIds);
|
if (CONFIG.sortBy === 'Random') {
|
||||||
|
itemIds = SlideUtils.shuffleArray(itemIds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Custom IDs
|
||||||
|
if (CONFIG.sortBy === 'Random') {
|
||||||
|
itemIds = SlideUtils.shuffleArray(itemIds);
|
||||||
|
} else if (CONFIG.sortBy !== 'Original') {
|
||||||
|
// Client-side sort required...
|
||||||
|
console.log(`Sorting ${itemIds.length} custom items by ${CONFIG.sortBy} ${CONFIG.sortOrder}`);
|
||||||
|
const itemsWithDetails = [];
|
||||||
|
for (const id of itemIds) {
|
||||||
|
const item = await ApiUtils.fetchItemDetails(id);
|
||||||
|
if (item) itemsWithDetails.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedItems = SlideUtils.sortItems(itemsWithDetails, CONFIG.sortBy, CONFIG.sortOrder);
|
||||||
|
itemIds = sortedItems.map(i => i.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
STATE.slideshow.itemIds = itemIds;
|
STATE.slideshow.itemIds = itemIds;
|
||||||
STATE.slideshow.totalItems = itemIds.length;
|
STATE.slideshow.totalItems = itemIds.length;
|
||||||
@@ -2962,10 +3274,13 @@ const MediaBarEnhancedSettingsManager = {
|
|||||||
button.className = 'paper-icon-button-light headerButton media-bar-settings-button';
|
button.className = 'paper-icon-button-light headerButton media-bar-settings-button';
|
||||||
button.title = 'Media Bar Settings';
|
button.title = 'Media Bar Settings';
|
||||||
// button.innerHTML = '<span class="material-icons">tune</span>';
|
// button.innerHTML = '<span class="material-icons">tune</span>';
|
||||||
|
|
||||||
// button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW.svg" style="width: 24px; height: 24px; vertical-align: middle;">';
|
// button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW.svg" style="width: 24px; height: 24px; vertical-align: middle;">';
|
||||||
// currently not optimal, as it's egg-shaped due to the svg format... but if it's square, it's very small...
|
// currently not optimal, as it's egg-shaped due to the svg format... but if it's square, it's very small...
|
||||||
// button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW.svg" draggable="false" style="width: 52px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
// button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW.svg" draggable="false" style="width: 52px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
||||||
button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW_SHORT.svg" draggable="false" style="width: 41px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
// button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW_SHORT.svg" draggable="false" style="width: 41px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
||||||
|
button.innerHTML = '<img src="/MediaBarEnhanced/Resources/assets/logo_SW_MINIMAL.svg" draggable="false" style="width: 24px; height: 24px; vertical-align: middle; pointer-events: none;">';
|
||||||
|
|
||||||
button.style.verticalAlign = 'middle';
|
button.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
button.addEventListener('click', (e) => {
|
button.addEventListener('click', (e) => {
|
||||||
@@ -3023,12 +3338,12 @@ const MediaBarEnhancedSettingsManager = {
|
|||||||
popup.style.top = `${rect.bottom + 10}px`;
|
popup.style.top = `${rect.bottom + 10}px`;
|
||||||
|
|
||||||
const settings = [
|
const settings = [
|
||||||
{ key: 'enabled', label: 'Enable Media Bar', description: 'Toggle the entire media bar visibility.', default: true },
|
{ key: 'enabled', label: 'Enable Media Bar Enhanced', description: 'Toggle the entire media bar visibility.', default: true },
|
||||||
{ key: 'videoBackdrops', label: 'Enable Video Backdrops', description: 'Play trailers as background videos.', default: CONFIG.enableVideoBackdrop },
|
{ key: 'videoBackdrops', label: 'Enable Trailer Backdrops', description: 'Play trailers as background videos.', default: CONFIG.enableVideoBackdrop },
|
||||||
{ key: 'trailerButton', label: 'Show Trailer Button', description: 'Show button to play trailers in popup on non Video backdrops.', default: CONFIG.showTrailerButton },
|
{ key: 'trailerButton', label: 'Show Trailer Button', description: 'Show button to play trailer in popup (only backdrops without trailer)', default: CONFIG.showTrailerButton },
|
||||||
{ key: 'mobileVideo', label: 'Enable Mobile Video', description: 'Allow video backdrops on mobile devices.', default: CONFIG.enableMobileVideo },
|
{ key: 'mobileVideo', label: 'Enable Trailer On Mobile', description: 'Allow trailer backdrops on mobile devices.', default: CONFIG.enableMobileVideo },
|
||||||
{ key: 'waitForTrailer', label: 'Wait For Trailer To End', description: 'Wait for the trailer to finish before changing slides.', default: CONFIG.waitForTrailerToEnd },
|
{ key: 'waitForTrailer', label: 'Wait For Trailer To End', description: 'Wait for the trailer to finish before changing slides.', default: CONFIG.waitForTrailerToEnd },
|
||||||
{ key: 'slideAnimations', label: 'Enable Animations', description: 'Enable transition animations between slides.', default: CONFIG.slideAnimationEnabled },
|
{ key: 'slideAnimations', label: 'Enable Animations', description: 'Enable zooming-in effect (only on background images)', default: CONFIG.slideAnimationEnabled },
|
||||||
];
|
];
|
||||||
|
|
||||||
let html = '<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Media Bar Settings</h3>';
|
let html = '<h3 style="margin-top:0; margin-bottom:1em; border-bottom:1px solid #444; padding-bottom:0.5em;">Media Bar Settings</h3>';
|
||||||
@@ -3046,11 +3361,14 @@ const MediaBarEnhancedSettingsManager = {
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reload button
|
// Buttons Container
|
||||||
html += `
|
html += `
|
||||||
<div style="margin-top:1em; text-align:right;">
|
<div style="margin-top:1em; display:flex; justify-content:flex-end; align-items:center; gap:1.5em;">
|
||||||
|
<button is="emby-button" type="button" class="raised button-cancel emby-button" id="mb-settings-reset" title="Reset to Server Defaults">
|
||||||
|
<span>Load Server Defaults</span>
|
||||||
|
</button>
|
||||||
<button is="emby-button" type="button" class="raised button-submit emby-button" id="mb-settings-save">
|
<button is="emby-button" type="button" class="raised button-submit emby-button" id="mb-settings-save">
|
||||||
<span>Reload</span>
|
<span>Save & Reload</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -3065,10 +3383,23 @@ const MediaBarEnhancedSettingsManager = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reload Handler
|
||||||
popup.querySelector('#mb-settings-save').addEventListener('click', () => {
|
popup.querySelector('#mb-settings-save').addEventListener('click', () => {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset Handler
|
||||||
|
popup.querySelector('#mb-settings-reset').addEventListener('click', () => {
|
||||||
|
if (confirm("Reset all local Media Bar settings to server defaults?")) {
|
||||||
|
Object.keys(localStorage).forEach(key => {
|
||||||
|
if (key.startsWith('mediaBarEnhanced-')) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const closeHandler = (e) => {
|
const closeHandler = (e) => {
|
||||||
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
|
||||||
popup.remove();
|
popup.remove();
|
||||||
@@ -3090,6 +3421,84 @@ const MediaBarEnhancedSettingsManager = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize page visibility handling to pause when tab is inactive
|
||||||
|
*/
|
||||||
|
const initPageVisibilityHandler = () => {
|
||||||
|
let wasVideoPlayingBeforeHide = false;
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
console.log("Tab inactive - pausing slideshow and videos");
|
||||||
|
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
|
||||||
|
|
||||||
|
if (STATE.slideshow.slideInterval) {
|
||||||
|
STATE.slideshow.slideInterval.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause active video if playing
|
||||||
|
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
|
||||||
|
if (currentItemId) {
|
||||||
|
// YouTube
|
||||||
|
if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
|
||||||
|
const player = STATE.slideshow.videoPlayers[currentItemId];
|
||||||
|
if (typeof player.pauseVideo === "function") {
|
||||||
|
try {
|
||||||
|
player.pauseVideo();
|
||||||
|
STATE.slideshow.isVideoPlaying = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error pausing video on tab hide:", e);
|
||||||
|
}
|
||||||
|
} else if (player.tagName === 'VIDEO') { // HTML5 Video
|
||||||
|
player.pause();
|
||||||
|
STATE.slideshow.isVideoPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Tab active - resuming slideshow");
|
||||||
|
if (!STATE.slideshow.isPaused) {
|
||||||
|
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
|
||||||
|
|
||||||
|
if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
|
||||||
|
const player = STATE.slideshow.videoPlayers[currentItemId];
|
||||||
|
|
||||||
|
// YouTube
|
||||||
|
if (typeof player.playVideo === "function") {
|
||||||
|
try {
|
||||||
|
player.playVideo();
|
||||||
|
STATE.slideshow.isVideoPlaying = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error resuming video on tab show:", e);
|
||||||
|
if (STATE.slideshow.slideInterval) {
|
||||||
|
STATE.slideshow.slideInterval.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (player.tagName === 'VIDEO') { // HTML5 Video
|
||||||
|
try {
|
||||||
|
player.play().catch(e => console.warn("Error resuming HTML5 video:", e));
|
||||||
|
STATE.slideshow.isVideoPlaying = true;
|
||||||
|
} catch(e) { console.warn(e); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No video was playing, just restart interval
|
||||||
|
const activeSlide = document.querySelector('.slide.active');
|
||||||
|
const hasVideo = activeSlide && activeSlide.querySelector('.video-backdrop');
|
||||||
|
|
||||||
|
if (CONFIG.waitForTrailerToEnd && hasVideo) {
|
||||||
|
// Don't restart interval if waiting for trailer
|
||||||
|
} else {
|
||||||
|
if (STATE.slideshow.slideInterval) {
|
||||||
|
STATE.slideshow.slideInterval.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasVideoPlayingBeforeHide = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the slideshow
|
* Initialize the slideshow
|
||||||
*/
|
*/
|
||||||
@@ -3203,6 +3612,8 @@ const slidesInit = async () => {
|
|||||||
|
|
||||||
SlideshowManager.initKeyboardEvents();
|
SlideshowManager.initKeyboardEvents();
|
||||||
|
|
||||||
|
initPageVisibilityHandler();
|
||||||
|
|
||||||
VisibilityObserver.init();
|
VisibilityObserver.init();
|
||||||
|
|
||||||
console.log("✅ Enhanced Jellyfin Slideshow initialized successfully");
|
console.log("✅ Enhanced Jellyfin Slideshow initialized successfully");
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -82,11 +82,21 @@ This plugin builds upon the original Media Bar with new capabilities and improve
|
|||||||
* **Smarter Playback**:
|
* **Smarter Playback**:
|
||||||
* Option to wait for the trailer to end before advancing the slide.
|
* Option to wait for the trailer to end before advancing the slide.
|
||||||
* Mute/Unmute controls
|
* Mute/Unmute controls
|
||||||
|
* **Override Trailers**: Manually specify a custom trailer URL for any item via the Custom Media IDs list
|
||||||
* **Customization**:
|
* **Customization**:
|
||||||
* **Custom Media IDs**: Manually specify which items (Movies, Series, Collections/Boxsets) to display. Easily configurable via the plugin settings
|
* **Custom Media IDs**: Manually specify which items (Movies, Series, Collections/Boxsets) to display. Easily configurable via the plugin settings
|
||||||
* **Seasonal Content Mode**: Define date-based lists for holidays and seasons (e.g., Halloween, Christmas)
|
* **Seasonal Content Mode**: Define date-based lists for holidays and seasons (e.g., Halloween, Christmas)
|
||||||
* Pagination dots turn into a counter (e.g., 1/20) if the limit is exceeded
|
* Pagination dots turn into a counter (e.g., 1/20) if the limit is exceeded
|
||||||
|
<details>
|
||||||
|
<summary>Have a look:</summary>
|
||||||
|
<img width="167" height="142" alt="PagDots_Number" src="https://github.com/user-attachments/assets/6a0a5040-cf13-4d9c-ae96-f50ec249c3f1" />
|
||||||
|
</details>
|
||||||
* Option to disable the loading screen
|
* Option to disable the loading screen
|
||||||
|
* Client Settings: Optionally allow users to set selected media bar settings from their client.
|
||||||
|
<details>
|
||||||
|
<summary>Have a look:</summary>
|
||||||
|
<img width="513" height="575" alt="Client-Settings" src="https://github.com/user-attachments/assets/3e29a84f-f8ea-4b7b-b561-80493cb1535b" />
|
||||||
|
</details>
|
||||||
|
|
||||||
### Core Features
|
### Core Features
|
||||||
* **Immersive Slideshow**: Rotates through your media library
|
* **Immersive Slideshow**: Rotates through your media library
|
||||||
@@ -118,13 +128,14 @@ Because this plugin relies on injecting JavaScript and CSS into the web interfac
|
|||||||
|
|
||||||
| Client Platform | Status | Notes |
|
| Client Platform | Status | Notes |
|
||||||
| :--- | :---: | :--- |
|
| :--- | :---: | :--- |
|
||||||
| **Web Browsers** (Chrome, Firefox, Edge, etc.) | ✅ | Fully supported. |
|
| **Web Browsers** (Firefox, Chrome etc.) | ✅ | Direct JS injection |
|
||||||
| **Jellyfin Media Player** (Windows/Linux/macOS) | ✅ | Fully supported. |
|
| **Jellyfin Media Player** (Windows/Linux/macOS) | ✅ | Uses jellyfin web |
|
||||||
| **Android App** | ✅ | Works (Web wrapper). |
|
| **Android App** | ✅ | Uses a web wrapper |
|
||||||
| **iOS App** | ✅ | Works (Web wrapper). |
|
| **iOS App** | ✅ | Uses a web wrapper |
|
||||||
| **Android TV / Fire TV** | ❌ | **Not supported** (Native UI). |
|
| **Android TV / Fire TV** | ❌ | **Not supported.** Uses a native Java/Kotlin UI. |
|
||||||
| **Roku** | ❌ | **Not supported** (Native UI). |
|
| **Roku** | ❌ | **Not supported.** Uses a native UI. |
|
||||||
| **Swiftfin** | ❌ | **Not supported** (Native UI). |
|
| **Swiftfin** (iOS/tvOS) | ❌ | **Not supported.** Uses a native Swift UI. |
|
||||||
|
| **Kodi** (via Jellyfin Addon) | ❌ | **Not supported.** Uses Kodi's native skinning engine. |
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,36 @@
|
|||||||
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
|
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"version": "1.4.0.6",
|
"version": "1.6.1.1",
|
||||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
"changelog": "- fix tv mode issue",
|
||||||
"targetAbi": "10.11.0.0",
|
"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",
|
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||||
"checksum": "348ebf449ac77fd156e2afbd03e80fce",
|
"checksum": "74483dae57374e338f933768c2c79862",
|
||||||
"timestamp": "2026-02-04T16:40:21Z"
|
"timestamp": "2026-02-11T18:57:35Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.6.0.2",
|
||||||
|
"changelog": "- add local trailer support on trailer button\nfix: iOS/MacOS playback issue?",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||||
|
"checksum": "cdd0208f8cc4f4b04f50e7138e508370",
|
||||||
|
"timestamp": "2026-02-10T22:07:33Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.1.3",
|
||||||
|
"changelog": "- fix: iOS/MacOS playback issue?",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.5.1.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||||
|
"checksum": "9d9dbed453673d4b78acf2adaaaee126",
|
||||||
|
"timestamp": "2026-02-10T20:12:59Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.0.28",
|
||||||
|
"changelog": "- fix: Keyboard controls in TV mode\n- Add sorting options for content\n- Add local trailer support\n- fix performance issue\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 4.0.1 from upstream repo",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.5.0.28/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||||
|
"checksum": "0261ff27be18d48cefa5078706954240",
|
||||||
|
"timestamp": "2026-02-10T00:35:41Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "1.3.0.3",
|
"version": "1.3.0.3",
|
||||||
|
|||||||
32
test_scripts/fetch_random_item_browser_console.js
Normal file
32
test_scripts/fetch_random_item_browser_console.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
(async () => {
|
||||||
|
const apiClient = window.ApiClient;
|
||||||
|
if (!apiClient) { console.error("Logged in?"); return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch 1 random item ID
|
||||||
|
const rnd = await apiClient.getItems(apiClient.getCurrentUserId(), { SortBy: "Random", Limit: 1, Recursive: true, IncludeItemTypes: "Movie,Series" });
|
||||||
|
if (rnd.Items.length > 0) {
|
||||||
|
const id = rnd.Items[0].Id;
|
||||||
|
console.log("Random Item ID:", id);
|
||||||
|
|
||||||
|
// Fetch Default Details
|
||||||
|
const defd = await apiClient.getItem(apiClient.getCurrentUserId(), id);
|
||||||
|
console.log("Default Fields:", defd);
|
||||||
|
|
||||||
|
// Fetch ALL Known Fields manually
|
||||||
|
const allFields = "Chapters,People,MediaStreams,UserData,RecursiveItemCount,DateCreated,MediaSources,ProductionYear,Studios,Genres,Tags,RemoteTrailers,ProviderIds,Overview,CommunityRating,CriticRating,OfficialRating,PremiereDate,RunTimeTicks";
|
||||||
|
const full = await res.json();
|
||||||
|
console.log("Full Details:", full);
|
||||||
|
|
||||||
|
// Helper to download JSON
|
||||||
|
const blob = new Blob([JSON.stringify(full, null, 2)], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `jellyfin-item-${id}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log("Downloaded JSON file.");
|
||||||
|
} else { console.warn("No items."); }
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
})();
|
||||||
32
test_scripts/fetch_random_item_browser_console_2.js
Normal file
32
test_scripts/fetch_random_item_browser_console_2.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
(async () => {
|
||||||
|
// 1. Get Auth Data from the active client
|
||||||
|
const apiClient = window.ApiClient;
|
||||||
|
if (!apiClient) {
|
||||||
|
console.error("ApiClient not found. Are you logged in?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Fetching random item...");
|
||||||
|
|
||||||
|
// 2. Fetch 1 random item
|
||||||
|
// const result = await apiClient.getItems(apiClient.getCurrentUserId(), { SortBy: "Random", Limit: 1, Recursive: true, IncludeItemTypes: "Movie,Series" });
|
||||||
|
const result = await apiClient.getItems(apiClient.getCurrentUserId(), {
|
||||||
|
SortBy: "Random",
|
||||||
|
Limit: 1,
|
||||||
|
Recursive: true,
|
||||||
|
IncludeItemTypes: "Movie,Series", // Optional: filter types
|
||||||
|
Fields: "Overview,RemoteTrailers,Genres,CommunityRating,CriticRating,OfficialRating,PremiereDate,RunTimeTicks,ProductionYear,MediaSources" // Request ALL fields
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.Items.length > 0) {
|
||||||
|
const item = result.Items[0];
|
||||||
|
console.log("Random Item Found:", item.Name);
|
||||||
|
console.dir(item); // Prints the full interactive object
|
||||||
|
} else {
|
||||||
|
console.warn("No items found.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching item:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
28
test_scripts/fetch_specific_items.js
Normal file
28
test_scripts/fetch_specific_items.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
(async () => {
|
||||||
|
const apiClient = window.ApiClient;
|
||||||
|
if (!apiClient) {
|
||||||
|
console.error("ApiClient nicht gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Die ID des Items, das du abrufen möchtest
|
||||||
|
const itemId = "DEINE_ITEM_ID_HIER";
|
||||||
|
const userId = apiClient.getCurrentUserId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Rufe Details für Item ${itemId} ab...`);
|
||||||
|
|
||||||
|
// Nutze getItem() statt getItems()
|
||||||
|
// Parameter: userId, itemId
|
||||||
|
const item = await apiClient.getItem(userId, itemId);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
console.log("Item Details gefunden:", item.Name);
|
||||||
|
console.dir(item); // Zeigt alle Metadaten (Genres, Pfade, ProviderIds, etc.)
|
||||||
|
} else {
|
||||||
|
console.warn("Item konnte nicht gefunden werden.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Abrufen des Items:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
462
without_list_txt/configPage.html
Normal file
462
without_list_txt/configPage.html
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Media Bar Enhanced Configuration</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="MediaBarEnhancedConfigurationPage" data-role="page" class="page type-interior pluginConfigurationPage"
|
||||||
|
data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-textarea">
|
||||||
|
<div data-role="content">
|
||||||
|
<div class="content-primary">
|
||||||
|
<div class="sectionTitleContainer">
|
||||||
|
<h2 class="sectionTitle">Media Bar Enhanced</h2>
|
||||||
|
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
|
||||||
|
target="_blank" href="https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced">
|
||||||
|
<i class="md-icon button-icon button-icon-left secondaryText"></i>
|
||||||
|
<span>${Help}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<hr style="max-width: 800px; margin: 1em 0;">
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.5em;">
|
||||||
|
<button class="jellyfin-tab-button active" onclick="showTab('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="jellyfin-tab-button" onclick="showTab('custom', 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>Custom Content</h3>
|
||||||
|
</button>
|
||||||
|
<button class="jellyfin-tab-button" onclick="showTab('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="mediaBarEnhancedConfigForm">
|
||||||
|
|
||||||
|
<!-- BASIC TAB -->
|
||||||
|
<div id="basic" class="tab-content">
|
||||||
|
<h2 class="sectionTitle">Main Plugin Settings</h2>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="IsEnabled" name="IsEnabled" />
|
||||||
|
<span>Enable Media Bar Enhanced Plugin</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableVideoBackdrop"
|
||||||
|
name="EnableVideoBackdrop" />
|
||||||
|
<span>Enable Trailer Backdrops</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Show trailers as background if available.<br>Adds a
|
||||||
|
mute/unmute and pause/play button to control the video in the right top corner.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
|
||||||
|
name="WaitForTrailerToEnd" />
|
||||||
|
<span>Wait For Trailer To End</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Delay slide transition until trailer finishes.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
|
||||||
|
name="EnableMobileVideo" />
|
||||||
|
<span>Enable Trailer On Mobile</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Allow video playback on mobile devices.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="ShowTrailerButton"
|
||||||
|
name="ShowTrailerButton" />
|
||||||
|
<span>Show Trailer Button</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if
|
||||||
|
trailer is not set as backdrop or if no trailer is available.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CUSTOM CONTENT TAB -->
|
||||||
|
<div id="custom" class="tab-content" style="display:none;">
|
||||||
|
<h2 class="sectionTitle">Custom Media IDs</h2>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableCustomMediaIds"
|
||||||
|
name="EnableCustomMediaIds" />
|
||||||
|
<span>Enable Custom Media IDs</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">If enabled, the slideshow will try to show the items listed
|
||||||
|
below. If the list is empty, default behavior (random items) is used. Disable this
|
||||||
|
to temporarily ignore your custom list without deleting it.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent"
|
||||||
|
name="EnableSeasonalContent" />
|
||||||
|
<span>Enable Seasonal Content Mode</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Enable this to define time-based lists in the field below.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Media/Collection/Playlist
|
||||||
|
IDs
|
||||||
|
(Newline or Comma separated)</label>
|
||||||
|
<textarea is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds"
|
||||||
|
style="width: 100%; height: 150px; font-family: monospace;"></textarea>
|
||||||
|
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow.
|
||||||
|
You can separate them by new line or comma.
|
||||||
|
<br><br>
|
||||||
|
<b>Manual Trailer Override:</b> You can specify a YouTube URL for an item by adding it in
|
||||||
|
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [https://youtu.be/...] DESCRIPTION</code>
|
||||||
|
<br><br>
|
||||||
|
You can also add a description after the ID using any separator like space, pipe
|
||||||
|
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
|
||||||
|
<br><br>
|
||||||
|
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use
|
||||||
|
the pipe (|) separator.
|
||||||
|
<br>
|
||||||
|
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).</div>
|
||||||
|
<div class="fieldDescription" id="seasonalMediaIdsDesc" style="display: none;">
|
||||||
|
<b>Seasonal Mode Enabled:</b> Define lines with date ranges (Format: DD.MM-DD.MM |
|
||||||
|
<i>name</i> | <i>IDs</i>).<br>
|
||||||
|
Example:<br>
|
||||||
|
<code>20.10-31.10 | Halloween | ID1, ID2 [https://youtu.be/...]</code><br>
|
||||||
|
<code>01.12-26.12 | Christmas | ID3, ID4</code><br>
|
||||||
|
<i>Only lines matching the current date will be used. If no line matches, it will try to
|
||||||
|
use random items.</i>
|
||||||
|
</div>
|
||||||
|
<p>You can find the IDs of your items in the URL of the item page in the web interface.<br>
|
||||||
|
Example:
|
||||||
|
<code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br>
|
||||||
|
You can also insert a name of a collection or playlist to fetch the IDs of all items in
|
||||||
|
it (will take the first hit.<br><b>Note:</b> there is currently no feedback if the name
|
||||||
|
resolution succeeded, you will have to look if the bar displays the correct items.).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ADVANCED TAB -->
|
||||||
|
<div id="advanced" class="tab-content" style="display:none;">
|
||||||
|
<h2 class="sectionTitle">Features</h2>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="SlideAnimationEnabled"
|
||||||
|
name="SlideAnimationEnabled" />
|
||||||
|
<span>Enable Slide Animations</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Enable the zooming-in effect on background images when a new slide is
|
||||||
|
shown (does not affect trailer backdrops). Attention: This may cause performance issues on weaker client hardware.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableClientSideSettings"
|
||||||
|
name="EnableClientSideSettings" />
|
||||||
|
<span>Enable Client-Side Settings</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to
|
||||||
|
override settings (like disabling the bar or trailer backdrops) locally on their device.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" />
|
||||||
|
<span>Use SponsorBlock</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers.</div>
|
||||||
|
</div>
|
||||||
|
<div class="selectContainer">
|
||||||
|
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
|
||||||
|
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality"
|
||||||
|
class="emby-select-withcolor emby-select">
|
||||||
|
<option value="Auto">Auto (Smart)</option>
|
||||||
|
<option value="Maximum">Maximum (4K+)</option>
|
||||||
|
<option value="1080p">1080p</option>
|
||||||
|
<option value="720p">720p</option>
|
||||||
|
</select>
|
||||||
|
<div class="fieldDescription">"Auto" selects Maximum if screen width > 1920px, otherwise
|
||||||
|
1080p.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="StartMuted" name="StartMuted" />
|
||||||
|
<span>Start Muted</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Start trailer video playback muted. (Known issue: In the
|
||||||
|
Android/IOS app, backdrop trailers are always muted.)</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="FullWidthVideo" name="FullWidthVideo" />
|
||||||
|
<span>Full Width Video</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Stretch video to full width. Very nice on desktops, on mobile
|
||||||
|
devices only the middle of the video is visible.<br>Disable to get the full aspect ratio
|
||||||
|
on
|
||||||
|
mobile devices. (looks bad on desktops)</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableLoadingScreen"
|
||||||
|
name="EnableLoadingScreen" />
|
||||||
|
<span>Enable Loading Screen</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
|
||||||
|
may have to reload the page twice)</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
|
||||||
|
name="AlwaysShowArrows" />
|
||||||
|
<span>Always Show Arrows</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead
|
||||||
|
of only on hover.</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" id="EnableKeyboardControls"
|
||||||
|
name="EnableKeyboardControls" />
|
||||||
|
<span>Enable Keyboard Controls</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">Enable keyboard shortcuts (Arrows left/right (change slide),
|
||||||
|
Space (pause), M (mute/unmute)) for
|
||||||
|
the slideshow.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sectionTitle">Time Settings</h2>
|
||||||
|
<p>Leave a setting blank to use the default value.</p>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="ShuffleInterval">Shuffle Interval
|
||||||
|
(ms)</label>
|
||||||
|
<input is="emby-input" type="number" id="ShuffleInterval" name="ShuffleInterval" />
|
||||||
|
<div class="fieldDescription">Time in milliseconds between changing slides.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="RetryInterval">Retry Interval
|
||||||
|
(ms)</label>
|
||||||
|
<input is="emby-input" type="number" id="RetryInterval" name="RetryInterval" />
|
||||||
|
<div class="fieldDescription">Time in milliseconds to wait before retrying failed
|
||||||
|
operations.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="LoadingCheckInterval">Loading Check
|
||||||
|
Interval (ms)</label>
|
||||||
|
<input is="emby-input" type="number" id="LoadingCheckInterval"
|
||||||
|
name="LoadingCheckInterval" />
|
||||||
|
<div class="fieldDescription">Frequency of checking wether the login screen or home screen
|
||||||
|
has loaded (in milliseconds).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="FadeTransitionDuration">Fade
|
||||||
|
Transition Duration (ms)</label>
|
||||||
|
<input is="emby-input" type="number" id="FadeTransitionDuration"
|
||||||
|
name="FadeTransitionDuration" />
|
||||||
|
<div class="fieldDescription">Duration in milliseconds of the transition between slides.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MinSwipeDistance">Min Swipe Distance
|
||||||
|
(px)</label>
|
||||||
|
<input is="emby-input" type="number" id="MinSwipeDistance" name="MinSwipeDistance" />
|
||||||
|
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
|
||||||
|
mobile).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sectionTitle">Content Limits</h2>
|
||||||
|
<p>Leave a setting blank to use the default value.</p>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxItems">Total Max Items</label>
|
||||||
|
<input is="emby-input" type="number" id="MaxItems" name="MaxItems" />
|
||||||
|
<div class="fieldDescription">Maximum total items to fetch (for all content types combined).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxMovies">Max Movies</label>
|
||||||
|
<input is="emby-input" type="number" id="MaxMovies" name="MaxMovies" />
|
||||||
|
<div class="fieldDescription">Maximum movies to include in slideshow (for random selection).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxTvShows">Max TV Shows</label>
|
||||||
|
<input is="emby-input" type="number" id="MaxTvShows" name="MaxTvShows" />
|
||||||
|
<div class="fieldDescription">Maximum TV shows to include in slideshow (for random
|
||||||
|
selection).</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
|
||||||
|
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
|
||||||
|
<div class="fieldDescription">Number of images to preload.</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
|
||||||
|
Dots</label>
|
||||||
|
<input is="emby-input" type="number" id="MaxPaginationDots" name="MaxPaginationDots" />
|
||||||
|
<div class="fieldDescription">Maximum number of dots to show in navigation. If the number
|
||||||
|
will be exceeded, the dots will "turn" into a counter style (e.g. 1/100). Set to 0 to
|
||||||
|
enable counter style directly.</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxPlotLength">Max Plot
|
||||||
|
Length</label>
|
||||||
|
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
|
||||||
|
<div class="fieldDescription">Maximum characters for the plot summary.</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
All changes require a page refresh (ctrl + r or F5) after saving for changes to take effect.
|
||||||
|
<br />
|
||||||
|
If old settings persist, please force clear browser cache.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
|
<span>${Save}</span>
|
||||||
|
</button>
|
||||||
|
<button is="emby-button" type="button" class="raised button-cancel block btnCancel"
|
||||||
|
onclick="history.back();">
|
||||||
|
<span>${ButtonCancel}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showTab(tabId, btn) {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
|
||||||
|
document.getElementById(tabId).style.display = 'block';
|
||||||
|
|
||||||
|
document.querySelectorAll('.jellyfin-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 MediaBarEnhancedConfigurationPage = {
|
||||||
|
pluginId: 'd7e11d57-819b-4bdd-a88d-53c5f5560225',
|
||||||
|
loadConfiguration: function (page) {
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) {
|
||||||
|
|
||||||
|
var keys = [
|
||||||
|
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
|
||||||
|
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
|
||||||
|
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
|
||||||
|
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
|
||||||
|
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
||||||
|
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
||||||
|
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
||||||
|
'EnableSeasonalContent', 'EnableClientSideSettings'
|
||||||
|
];
|
||||||
|
|
||||||
|
keys.forEach(function (key) {
|
||||||
|
var el = page.querySelector('#' + key);
|
||||||
|
if (el) {
|
||||||
|
if (el.type === 'checkbox') {
|
||||||
|
el.checked = config[key];
|
||||||
|
} else {
|
||||||
|
el.value = config[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Seasonal UI logic
|
||||||
|
var seasonalCheckbox = page.querySelector('#EnableSeasonalContent');
|
||||||
|
var normalDesc = page.querySelector('#customMediaIdsDesc');
|
||||||
|
var seasonalDesc = page.querySelector('#seasonalMediaIdsDesc');
|
||||||
|
|
||||||
|
function updateDesc() {
|
||||||
|
if (seasonalCheckbox && seasonalCheckbox.checked) {
|
||||||
|
if (normalDesc) normalDesc.style.display = 'none';
|
||||||
|
if (seasonalDesc) seasonalDesc.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
if (normalDesc) normalDesc.style.display = 'block';
|
||||||
|
if (seasonalDesc) seasonalDesc.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seasonalCheckbox) {
|
||||||
|
seasonalCheckbox.addEventListener('change', updateDesc);
|
||||||
|
updateDesc();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveConfiguration: function (page) {
|
||||||
|
Dashboard.showLoadingMsg();
|
||||||
|
|
||||||
|
var config = {};
|
||||||
|
var keys = [
|
||||||
|
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
|
||||||
|
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
|
||||||
|
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
|
||||||
|
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
|
||||||
|
'WaitForTrailerToEnd', 'StartMuted', 'FullWidthVideo', 'EnableMobileVideo',
|
||||||
|
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
|
||||||
|
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
|
||||||
|
'EnableSeasonalContent', 'EnableClientSideSettings'
|
||||||
|
];
|
||||||
|
|
||||||
|
keys.forEach(function (key) {
|
||||||
|
var el = page.querySelector('#' + key);
|
||||||
|
if (el) {
|
||||||
|
if (el.type === 'checkbox') {
|
||||||
|
config[key] = el.checked;
|
||||||
|
} else {
|
||||||
|
config[key] = (el.type === 'number') ? parseInt(el.value, 10) : el.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) {
|
||||||
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector('#MediaBarEnhancedConfigurationPage').addEventListener('pageshow', function () {
|
||||||
|
MediaBarEnhancedConfigurationPage.loadConfiguration(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#mediaBarEnhancedConfigForm').addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
MediaBarEnhancedConfigurationPage.saveConfiguration(document.querySelector('#MediaBarEnhancedConfigurationPage'));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.jellyfin-tab-button.active {
|
||||||
|
color: #fff !important;
|
||||||
|
border-bottom: 2px solid #00a4dc !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3225
without_list_txt/mediaBarEnhanced.js
Normal file
3225
without_list_txt/mediaBarEnhanced.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user