Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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>
|
||||||
|
|
||||||
@@ -153,8 +161,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>
|
||||||
@@ -221,15 +238,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 +282,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="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="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">
|
||||||
@@ -370,7 +405,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 +440,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 +472,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.5.0.18</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 |
@@ -161,7 +161,7 @@
|
|||||||
|
|
||||||
.homeSectionsContainer {
|
.homeSectionsContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 65vh;
|
margin-top: 65vh;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Jellyfin Slideshow by M0RPH3US v3.0.9
|
* Jellyfin Slideshow by M0RPH3US v4.0.1
|
||||||
* Modified by CodeDevMLH v1.1.0.0
|
* Modified by CodeDevMLH v1.1.0.0
|
||||||
*
|
*
|
||||||
* New features:
|
* New features:
|
||||||
@@ -38,6 +38,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 +52,8 @@ const CONFIG = {
|
|||||||
customMediaIds: "",
|
customMediaIds: "",
|
||||||
enableLoadingScreen: true,
|
enableLoadingScreen: true,
|
||||||
enableClientSideSettings: false,
|
enableClientSideSettings: false,
|
||||||
|
sortBy: "Random",
|
||||||
|
sortOrder: "Ascending",
|
||||||
};
|
};
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
@@ -81,6 +84,7 @@ const STATE = {
|
|||||||
sponsorBlockInterval: null,
|
sponsorBlockInterval: null,
|
||||||
isMuted: CONFIG.startMuted,
|
isMuted: CONFIG.startMuted,
|
||||||
customTrailerUrls: {},
|
customTrailerUrls: {},
|
||||||
|
ytPromise: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -459,6 +463,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
|
||||||
@@ -582,23 +646,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;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -957,8 +1023,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 +1096,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 +1324,39 @@ 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<string|null>} Stream 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;
|
||||||
|
|
||||||
|
// Construct stream URL
|
||||||
|
return `${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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1432,12 +1539,21 @@ 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];
|
trailerUrl = STATE.slideshow.customTrailerUrls[itemId];
|
||||||
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
|
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
|
||||||
} else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) {
|
}
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1589,7 +1705,7 @@ const SlideCreator = {
|
|||||||
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) {
|
||||||
@@ -1931,6 +2047,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"
|
||||||
@@ -2530,29 +2651,25 @@ 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 isSlideshowFocused = container.contains(activeElement) || activeElement === container;
|
||||||
|
|
||||||
|
if (!isSlideshowFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
if (focusElement && focusElement.classList.contains("detail-button")) {
|
SlideshowManager.nextSlide();
|
||||||
focusElement.previousElementSibling.focus();
|
|
||||||
} else {
|
|
||||||
SlideshowManager.nextSlide();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
if (focusElement && focusElement.classList.contains("play-button")) {
|
SlideshowManager.prevSlide();
|
||||||
focusElement.nextElementSibling.focus();
|
|
||||||
} else {
|
|
||||||
SlideshowManager.prevSlide();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -2568,8 +2685,15 @@ const SlideshowManager = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "Enter":
|
case "Enter":
|
||||||
if (focusElement) {
|
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
|
||||||
focusElement.click();
|
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 +2887,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 +3104,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 +3168,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 +3191,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 +3213,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 +3251,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 +3442,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,12 @@
|
|||||||
"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.5.0.18",
|
||||||
"changelog": "- feat: Add client-side settings feature for selected media bar settings",
|
"changelog": "- fix: keyboard controls in TV mode\n- Add sorting options for content\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 4.0.1 from original repo",
|
||||||
"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.5.0.18/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||||
"checksum": "348ebf449ac77fd156e2afbd03e80fce",
|
"checksum": "13d18e5d2498f73324d09bf796788e94",
|
||||||
"timestamp": "2026-02-04T16:40:21Z"
|
"timestamp": "2026-02-09T16:21: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