Compare commits

...

46 Commits

Author SHA1 Message Date
CodeDevMLH
70b0a2a192 Update manifest.json for release v1.7.1.8 [skip ci] 2026-03-08 19:29:25 +00:00
CodeDevMLH
300c76890b Bump version to 1.7.1.8 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-08 20:28:38 +01:00
CodeDevMLH
64e5441aff Optimize slideshow distance calculation for circular navigation 2026-03-08 20:28:20 +01:00
CodeDevMLH
f47c9dde88 Update manifest.json for release v1.7.1.7 [skip ci] 2026-03-08 19:15:10 +00:00
CodeDevMLH
9d42b5af8d Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-08 20:14:23 +01:00
CodeDevMLH
8c5f66716f Update version to 1.7.1.7 and enhance changelog with new features and fixes 2026-03-08 20:09:58 +01:00
CodeDevMLH
41e6c1032d Add Hide Arrows on Mobile option to configuration and update related logic 2026-03-08 20:09:47 +01:00
CodeDevMLH
fe07fe9f5e Update manifest.json for release v1.7.1.6 [skip ci] 2026-03-08 18:33:15 +00:00
CodeDevMLH
22a7eb8dcb Update version to 1.7.1.6 and enhance changelog with new features and fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-08 19:32:27 +01:00
CodeDevMLH
07658f4fbc Add Exclude Seasonal Content option to configuration and update related logic 2026-03-08 19:32:15 +01:00
CodeDevMLH
25ee5b73b4 Update field descriptions for Max Parental Rating and Max Days Recent inputs to clarify no limit options 2026-03-08 19:21:04 +01:00
CodeDevMLH
8f8e251054 Add max parental rating and max days recent filters to configuration 2026-03-08 19:19:20 +01:00
CodeDevMLH
05529e5627 add new tests 2026-03-08 19:18:30 +01:00
CodeDevMLH
b3e00e8ad6 Update manifest.json for release v1.7.1.5 [skip ci] 2026-03-08 16:17:52 +00:00
CodeDevMLH
39649a6dd3 Bump version to 1.7.1.5 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-03-08 17:17:04 +01:00
CodeDevMLH
11aca32158 Enhance mobile layout by adjusting button container and adding styles for misc-info and genre 2026-03-08 17:17:00 +01:00
CodeDevMLH
9bcb2f984a Update manifest.json for release v1.7.1.4 [skip ci] 2026-03-08 15:58:12 +00:00
CodeDevMLH
c23a614f9f Bump version to 1.7.1.4 and update changelog for new features and fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-08 16:57:21 +01:00
CodeDevMLH
3a367cb2be Add ShowPaginationDots configuration option and update related UI elements 2026-03-08 16:57:16 +01:00
CodeDevMLH
2993bfe3f2 del old tmp 2026-03-08 16:56:18 +01:00
CodeDevMLH
3ffa2c262a Update manifest.json for release v1.7.1.3 [skip ci] 2026-03-08 15:20:14 +00:00
CodeDevMLH
dc88110e9c Bump version to 1.7.1.3 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-03-08 16:19:17 +01:00
CodeDevMLH
f9ae62a459 Refactor button-container styles for improved layout and responsiveness 2026-03-08 16:19:07 +01:00
CodeDevMLH
9e2f861213 Update manifest.json for release v1.7.1.2 [skip ci] 2026-03-08 15:08:30 +00:00
CodeDevMLH
4781618a52 Bump version to 1.7.1.2 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-08 16:07:39 +01:00
CodeDevMLH
2bed81c1f8 Refactor button-container styles for improved layout and responsiveness 2026-03-08 16:07:32 +01:00
CodeDevMLH
292fcfc389 Update manifest.json for release v1.7.1.1 [skip ci] 2026-03-08 14:48:17 +00:00
CodeDevMLH
da718a0e57 Bump version to 1.7.1.1 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-03-08 15:47:26 +01:00
CodeDevMLH
95a8907496 test2 2026-03-08 15:47:09 +01:00
CodeDevMLH
0498756529 Update manifest.json for release v1.7.1.0 [skip ci] 2026-03-08 14:32:40 +00:00
CodeDevMLH
f1d92080b2 Bump version to 1.7.1.0 and update changelog for mobile button fix
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-03-08 15:31:52 +01:00
CodeDevMLH
586b57d23e Enhance button layout in button-container for better responsiveness 2026-03-08 15:30:21 +01:00
CodeDevMLH
47b05a82ba Update changelog for version 1.7.0.14: enhance iframe security, fix playback issues on iOS/MacOS, disable animations for TV layout, remove list.txt functionality, and improve logging. [skip ci] 2026-03-06 04:30:04 +01:00
CodeDevMLH
cb45e0cb43 Update manifest.json for release v1.7.0.14 [skip ci] 2026-03-06 03:13:48 +00:00
CodeDevMLH
19246c3a19 Bump version to 1.7.0.14
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 04:12:58 +01:00
CodeDevMLH
99b4b09306 Disable animations and backdrop filters for TV layout on high pixel density screens 2026-03-06 04:12:52 +01:00
CodeDevMLH
563a1e5484 Update manifest.json for release v1.7.0.13 [skip ci] 2026-03-06 03:10:54 +00:00
CodeDevMLH
788708370d Bump version to 1.7.0.13
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 04:10:04 +01:00
CodeDevMLH
4bb0de11da Remove fetchItemIdsFromList function and related logic for item ID retrieval 2026-03-06 04:09:36 +01:00
CodeDevMLH
d2abfc6b46 Enhance logging in media bar with contextual messages 2026-03-06 04:06:18 +01:00
CodeDevMLH
8ba14d4df0 Update manifest.json for release v1.7.0.12 [skip ci] 2026-03-06 02:26:17 +00:00
CodeDevMLH
538138fcf3 Bump version to 1.7.0.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 03:25:23 +01:00
CodeDevMLH
08efb11d95 Remove picture-in-picture support from YouTube player integration 2026-03-06 03:25:08 +01:00
CodeDevMLH
de62c794f9 Update manifest.json for release v1.7.0.11 [skip ci] 2026-03-06 02:19:18 +00:00
CodeDevMLH
fc2491a4ef Bump version to 1.7.0.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 03:18:26 +01:00
CodeDevMLH
03276d7722 Enhance YouTube player integration with fullscreen and picture-in-picture support 2026-03-06 03:18:13 +01:00
12 changed files with 676 additions and 3929 deletions

View File

@@ -15,9 +15,12 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public int MaxMovies { get; set; } = 15; public int MaxMovies { get; set; } = 15;
public int MaxTvShows { get; set; } = 15; public int MaxTvShows { get; set; } = 15;
public int MaxItems { get; set; } = 500; public int MaxItems { get; set; } = 500;
public int MaxParentalRating { get; set; } = 0;
public int MaxDaysRecent { get; set; } = 0;
public int PreloadCount { get; set; } = 3; public int PreloadCount { get; set; } = 3;
public int FadeTransitionDuration { get; set; } = 500; public int FadeTransitionDuration { get; set; } = 500;
public int MaxPaginationDots { get; set; } = 15; public int MaxPaginationDots { get; set; } = 15;
public bool ShowPaginationDots { get; set; } = true;
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;
@@ -33,10 +36,12 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool EnableLoadingScreen { get; set; } = true; public bool EnableLoadingScreen { get; set; } = true;
public bool EnableKeyboardControls { get; set; } = true; public bool EnableKeyboardControls { get; set; } = true;
public bool AlwaysShowArrows { get; set; } = false; public bool AlwaysShowArrows { get; set; } = false;
public bool HideArrowsOnMobile { get; set; } = true;
public string CustomMediaIds { get; set; } = ""; public string CustomMediaIds { get; set; } = "";
public bool EnableCustomMediaIds { get; set; } = true; public bool EnableCustomMediaIds { get; set; } = true;
public string PreferredVideoQuality { get; set; } = "Auto"; public string PreferredVideoQuality { get; set; } = "Auto";
public bool EnableSeasonalContent { get; set; } = false; public bool EnableSeasonalContent { get; set; } = false;
public bool ExcludeSeasonalContent { get; set; } = true;
public string SeasonalSections { get; set; } = "[]"; public string SeasonalSections { get; set; } = "[]";
public bool IsEnabled { get; set; } = true; public bool IsEnabled { get; set; } = true;
public bool EnableClientSideSettings { get; set; } = false; public bool EnableClientSideSettings { get; set; } = false;

View File

@@ -43,7 +43,8 @@
<h2 class="sectionTitle">Main Plugin Settings</h2> <h2 class="sectionTitle">Main Plugin Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="MediaBarIsEnabled" name="MediaBarIsEnabled" /> <input is="emby-checkbox" type="checkbox" id="MediaBarIsEnabled"
name="MediaBarIsEnabled" />
<span>Enable Media Bar Enhanced Plugin</span> <span>Enable Media Bar Enhanced Plugin</span>
</label> </label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div> <div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
@@ -57,23 +58,27 @@
<div class="fieldDescription">Show 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"> <div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalTrailersContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers" <input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers"
name="PreferLocalTrailers" /> name="PreferLocalTrailers" />
<span>Prefer Local Trailers</span> <span>Prefer Local Trailers</span>
</label> </label>
<div class="fieldDescription">If enabled, local trailers will be preferred over remote (YouTube) trailers.</div> <div class="fieldDescription">If enabled, local trailers will be preferred over remote
(YouTube) trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalBackdropsContainer"> <div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalBackdropsContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops" <input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops"
name="PreferLocalBackdrops" /> name="PreferLocalBackdrops" />
<span>Prefer Local Backdrops / Theme Videos</span> <span>Prefer Local Backdrops / Theme Videos</span>
</label> </label>
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be preferred over remote and local trailers.</div> <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be
preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="WaitForTrailerToEndContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd" <input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" /> name="WaitForTrailerToEnd" />
@@ -81,7 +86,7 @@
</label> </label>
<div class="fieldDescription">Delay slide transition until trailer finishes.</div> <div class="fieldDescription">Delay slide transition until trailer finishes.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="EnableMobileVideoContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo" <input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
name="EnableMobileVideo" /> name="EnableMobileVideo" />
@@ -95,8 +100,8 @@
name="ShowTrailerButton" /> name="ShowTrailerButton" />
<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. Button only
trailer is not set as backdrop or if no trailer is available.</div> visible if trailer is not set as backdrop.</div>
</div> </div>
</div> </div>
@@ -111,7 +116,8 @@
<span>Enable Custom Media IDs</span> <span>Enable Custom Media IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the slideshow will show the items listed <div class="fieldDescription">If enabled, the slideshow will show the items listed
below as the default content. If the list is empty, random items from your library are used.</div> below as the default content. If the list is empty, random items from your library are
used.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -119,35 +125,47 @@
name="ApplyLimitsToCustomIds" /> name="ApplyLimitsToCustomIds" />
<span>Apply Limits to Custom IDs</span> <span>Apply Limits to Custom IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content Limits) will also apply to Custom Media IDs and Collections. By default, custom lists are not limited.</div> <div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content
Limits) will also apply to Custom Media IDs and Collections. By default, custom lists
are not limited.</div>
</div> </div>
<div id="customMediaIdsContainer"> <div id="customMediaIdsContainer">
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist IDs (Newline or Comma-separated)</label> <label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds" Media/Collection/Playlist IDs (Newline or Comma-separated)</label>
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds"
name="CustomMediaIds"
style="width: 100%; height: 150px; font-family: monospace;"></textarea> style="width: 100%; height: 150px; font-family: monospace;"></textarea>
<div class="fieldDescription">Enter the IDs of the items you want to show in the slideshow as your default content. You can separate them by new line or comma. <div class="fieldDescription">Enter the IDs of the items you want to show in the
slideshow as your default content. You can separate them by new line or comma.
<br><br> <br><br>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in <b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>. Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or
<code>ID [JellyfinItemId] DESCRIPTION</code>.
<br> <br>
Methods: Methods:
<ul> <ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li> <li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li> <li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g.
a Theme Video or Backdrop Video) using the native player.</li>
</ul> </ul>
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> 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> <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. <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> <br>
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f). <b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).
</div> </div>
<p>You can find the IDs of your items in the URL of the item page in the web interface.<br> <p>You can find the IDs of your items in the URL of the item page in the web
interface.<br>
Example: Example:
<code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br> <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 You can also insert a name of a collection or playlist to fetch the IDs of all items
it (will take the first hit.<br><b>Note:</b> There is currently no feedback if the name in it (will take the first hit.<br><b>Note:</b> There is currently no feedback if
resolution succeeded, you will have to look if the bar displays the correct items). the name resolution succeeded, you will have to look if the bar displays the correct
items).
</p> </p>
</div> </div>
</div> </div>
@@ -160,14 +178,26 @@
name="EnableSeasonalContent" /> name="EnableSeasonalContent" />
<span>Enable Seasonal Content</span> <span>Enable Seasonal Content</span>
</label> </label>
<div class="fieldDescription">When enabled, seasonal sections below will override the default list or random selection <div class="fieldDescription">When enabled, seasonal sections below will override the
during their active date ranges. If no season matches the current date, the default Custom Media IDs above or random selection are used as fallback.</div> default list or random selection
during their active date ranges. If no season matches the current date, the default
Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<div id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ExcludeSeasonalContent"
name="ExcludeSeasonalContent" />
<span>Exclude Seasonal Content from Random Lists</span>
</label>
<div class="fieldDescription">When enabled, any items defined in your Seasonal Sections below will be explicitly excluded from being shown when the plugin pulls random items from your library.</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;"> <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> <i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>Define seasonal rules to automatically select a selection of items based on the date. Rules are evaluated from top to bottom. The first matching rule wins.</div> <div>Define seasonal rules to automatically select a selection of items based on the
date. Rules are evaluated from top to bottom. The first matching rule wins.</div>
</div> </div>
<div id="seasonalSectionsList"></div> <div id="seasonalSectionsList"></div>
@@ -189,8 +219,9 @@
name="SlideAnimationEnabled" /> name="SlideAnimationEnabled" />
<span>Enable Slide Animations</span> <span>Enable Slide Animations</span>
</label> </label>
<div class="fieldDescription">Enable the zooming-in effect on background images when a new slide is <div class="fieldDescription">Enable the zooming-in effect on background images when a new
shown (does not affect trailer backdrops). Attention: This may cause performance issues on weaker client hardware.</div> slide is shown (does not affect trailer backdrops). Attention: This may cause
performance issues on weaker client hardware.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -199,7 +230,8 @@
<span>Enable Client-Side Settings</span> <span>Enable Client-Side Settings</span>
</label> </label>
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to <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> 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>
@@ -207,7 +239,8 @@
name="RandomizeThemeVideos" /> name="RandomizeThemeVideos" />
<span>Randomize Backdrop Video</span> <span>Randomize Backdrop Video</span>
</label> </label>
<div class="fieldDescription">If enabled, a random video from the backdrops/theme videos will be selected instead of the first one (if multiple exist).</div> <div class="fieldDescription">If enabled, a random video from the backdrops/theme videos
will be selected instead of the first one (if multiple exist).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -215,18 +248,22 @@
name="RandomizeLocalTrailers" /> name="RandomizeLocalTrailers" />
<span>Randomize Local Trailer</span> <span>Randomize Local Trailer</span>
</label> </label>
<div class="fieldDescription">If enabled, a random local trailer will be selected instead of the first one (if multiple exist).</div> <div class="fieldDescription">If enabled, a random local trailer will be selected instead of
the first one (if multiple exist).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" /> <input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" />
<span>Use SponsorBlock</span> <span>Use SponsorBlock</span>
</label> </label>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is available).</div> <div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is
available).</div>
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label> <label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Auto">Auto (Smart)</option> <option value="Auto">Auto (Smart)</option>
<option value="Maximum">Maximum (4K+)</option> <option value="Maximum">Maximum (4K+)</option>
<option value="1080p">1080p</option> <option value="1080p">1080p</option>
@@ -241,7 +278,9 @@
<span>Start Muted</span> <span>Start Muted</span>
</label> </label>
<div class="fieldDescription">Start trailer video playback muted. (Known issue: In the <div class="fieldDescription">Start trailer video playback muted. (Known issue: In the
Android/IOS app, backdrop trailers are always muted.)<br><b style="color:#ffcc00">Warning:</b> Disabling this may cause autoplay to fail on certain browsers due to strict autoplay policies.</div> Android/IOS app, backdrop trailers are always muted.)<br>
<b style="color:#ffcc00">Warning:</b> Disabling this may cause autoplay to fail on
certain browsers due to strict autoplay policies.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -260,16 +299,23 @@
<span>Enable Loading Screen</span> <span>Enable Loading Screen</span>
</label> </label>
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You <div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
may have to reload the page twice)</div> may have to reload the page twice (after changing this setting) to take effect)</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows" <input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" /> name="AlwaysShowArrows" />
<span>Always Show Arrows</span> <span>Always Show Arrow Navigation Buttons</span>
</label> </label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead <div class="fieldDescription">Force the UI arrow navigation buttons to always be visible instead of only when hovered.</div>
of only on hover.</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="HideArrowsOnMobile"
name="HideArrowsOnMobile" />
<span>Hide Arrows on Mobile</span>
</label>
<div class="fieldDescription">Completely disable the navigation arrows on mobile devices (since swiping is available).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -281,6 +327,7 @@
Space (pause), M (mute/unmute)) for Space (pause), M (mute/unmute)) for
the slideshow.</div> the slideshow.</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<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>
@@ -324,11 +371,14 @@
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for <div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</div> mobile).</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Sorting</h2> <h2 class="sectionTitle">Content Sorting and Filtering</h2>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortBy">Sort By</label> <label class="selectLabel" for="SortBy">Sort By</label>
<select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="SortBy" name="SortBy"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Random">Random</option> <option value="Random">Random</option>
<option value="Original">Original (Custom List Order)</option> <option value="Original">Original (Custom List Order)</option>
<option value="PremiereDate">Premiere Date</option> <option value="PremiereDate">Premiere Date</option>
@@ -342,15 +392,38 @@
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortOrder">Sort Order</label> <label class="selectLabel" for="SortOrder">Sort Order</label>
<select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="SortOrder" name="SortOrder"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Ascending">Ascending</option> <option value="Ascending">Ascending</option>
<option value="Descending">Descending</option> <option value="Descending">Descending</option>
</select> </select>
<div class="fieldDescription">Sort items in Ascending or Descending order.</div> <div class="fieldDescription">Sort items in Ascending or Descending order.</div>
</div> </div>
<div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;"> <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. <b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original'
preserves Custom List order.
</div> </div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxParentalRating">Max Parental Rating (Age Limit)</label>
<input is="emby-input" type="number" id="MaxParentalRating" name="MaxParentalRating" />
<div class="fieldDescription">Items exceeding this age rating will not be shown. Leave blank or set to 0 for no limit. Examples: 12, 16, 18.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxDaysRecent">Max Days Recent</label>
<input is="emby-input" type="number" id="MaxDaysRecent" name="MaxDaysRecent" />
<div class="fieldDescription">Only show items added in the last X days. Leave blank or set to 0 for no limit. Example: 30.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<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>
@@ -377,6 +450,15 @@
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" /> <input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of slides to preload.</div> <div class="fieldDescription">Number of slides to preload.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ShowPaginationDots"
name="ShowPaginationDots" />
<span>Show Pagination Dots/Counter</span>
</label>
<div class="fieldDescription">Show or hide the pagination dots/counter navigation at the
bottom of the slideshow.</div>
</div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination <label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
Dots</label> Dots</label>
@@ -391,14 +473,6 @@
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" /> <input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</div> <div class="fieldDescription">Maximum characters for the plot summary.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random selection results.</div>
</div>
</div> </div>
<div <div
@@ -459,7 +533,8 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent' 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -517,14 +592,20 @@
var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop'); var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop');
var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer'); var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer');
var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer'); var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer');
var waitForTrailerContainer = page.querySelector('#WaitForTrailerToEndContainer');
var enableMobileVideoContainer = page.querySelector('#EnableMobileVideoContainer');
function updatePreferLocalVisibility() { function updatePreferLocalVisibility() {
if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) { if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) {
if (preferLocalContainer) preferLocalContainer.style.display = 'block'; if (preferLocalContainer) preferLocalContainer.style.display = 'block';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'block';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'block';
} else { } else {
if (preferLocalContainer) preferLocalContainer.style.display = 'none'; if (preferLocalContainer) preferLocalContainer.style.display = 'none';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'none';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'none';
} }
} }
@@ -563,7 +644,8 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent' 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {

View File

@@ -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.7.0.10</Version> <Version>1.7.1.8</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>

View File

@@ -768,7 +768,22 @@
.button-container { .button-container {
top: calc(50% + 25vh); top: calc(50% + 25vh);
left: 50%; left: 50%;
transform: translateX(-50%) scale(0.95); transform: translateX(-50%);
width: max-content;
max-width: 98vw;
flex-wrap: nowrap;
justify-content: center;
gap: 15px;
}
.button-container button {
margin: 0 !important;
min-width: 0 !important;
}
.button-container .detail-button,
.button-container .favorite-button {
flex-shrink: 0;
} }
.logo { .logo {
@@ -825,10 +840,6 @@
.genre { .genre {
top: calc(50% + 16vh); top: calc(50% + 16vh);
} }
.button-container {
top: calc(50% + 27vh);
}
} }
} }
@@ -1000,3 +1011,19 @@
.layout-tv .backdrop-container{ .layout-tv .backdrop-container{
top: -5%; top: -5%;
} }
@media screen and (min-width: 960px) and (-webkit-device-pixel-ratio: 1) {
.layout-tv .backdrop.animate {
animation: none !important;
}
.layout-tv .logo.animate {
animation: none !important;
}
.layout-tv .slide-counter,
.layout-tv .dots-container {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}

View File

@@ -1,4 +1,4 @@
/* /*
* Jellyfin Slideshow by M0RPH3US v4.0.1 * Jellyfin Slideshow by M0RPH3US v4.0.1
* Modified by CodeDevMLH * Modified by CodeDevMLH
* *
@@ -38,6 +38,9 @@ const CONFIG = {
preloadCount: 3, preloadCount: 3,
fadeTransitionDuration: 500, fadeTransitionDuration: 500,
maxPaginationDots: 15, maxPaginationDots: 15,
showPaginationDots: true,
maxParentalRating: null,
maxDaysRecent: null,
slideAnimationEnabled: true, slideAnimationEnabled: true,
enableVideoBackdrop: true, enableVideoBackdrop: true,
useSponsorBlock: true, useSponsorBlock: true,
@@ -54,6 +57,7 @@ const CONFIG = {
preferredVideoQuality: "Auto", preferredVideoQuality: "Auto",
enableKeyboardControls: true, enableKeyboardControls: true,
alwaysShowArrows: false, alwaysShowArrows: false,
hideArrowsOnMobile: true,
enableCustomMediaIds: true, enableCustomMediaIds: true,
enableSeasonalContent: false, enableSeasonalContent: false,
customMediaIds: "", customMediaIds: "",
@@ -63,6 +67,7 @@ const CONFIG = {
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false, applyLimitsToCustomIds: false,
seasonalSections: "[]", seasonalSections: "[]",
excludeSeasonalContent: true,
isEnabled: true, isEnabled: true,
}; };
@@ -124,7 +129,7 @@ const processNextRequest = () => {
}) })
.then(callback) .then(callback)
.catch((error) => { .catch((error) => {
console.error("Error in throttled request:", error); console.error("🎬 Media Bar:", "Error in throttled request:", error);
}) })
.finally(() => { .finally(() => {
setTimeout(processNextRequest, 100); setTimeout(processNextRequest, 100);
@@ -158,7 +163,7 @@ const isUserLoggedIn = () => {
window.ApiClient._serverInfo.AccessToken window.ApiClient._serverInfo.AccessToken
); );
} catch (error) { } catch (error) {
console.error("Error checking login status:", error); console.error("🎬 Media Bar:", "Error checking login status:", error);
return false; return false;
} }
}; };
@@ -177,7 +182,7 @@ const isLowPowerDevice = () => {
*/ */
const initJellyfinData = (callback) => { const initJellyfinData = (callback) => {
if (!window.ApiClient) { if (!window.ApiClient) {
console.warn("⏳ window.ApiClient is not available yet. Retrying..."); console.warn("🎬 Media Bar:", "⏳ window.ApiClient is not available yet. Retrying...");
setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval); setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval);
return; return;
} }
@@ -198,7 +203,7 @@ const initJellyfinData = (callback) => {
callback(); callback();
} }
} catch (error) { } catch (error) {
console.error("Error initializing Jellyfin data:", error); console.error("🎬 Media Bar:", "Error initializing Jellyfin data:", error);
setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval); setTimeout(() => initJellyfinData(callback), CONFIG.retryInterval);
} }
}; };
@@ -210,9 +215,9 @@ const initLocalization = async () => {
try { try {
const locale = await LocalizationUtils.getCurrentLocale(); const locale = await LocalizationUtils.getCurrentLocale();
await LocalizationUtils.loadTranslations(locale); await LocalizationUtils.loadTranslations(locale);
console.log("✅ Localization initialized"); console.log("🎬 Media Bar:", "✅ Localization initialized");
} catch (error) { } catch (error) {
console.error("Error initializing localization:", error); console.error("🎬 Media Bar:", "Error initializing localization:", error);
} }
}; };
@@ -332,7 +337,7 @@ const initLoadingScreen = () => {
* Resets the slideshow state completely * Resets the slideshow state completely
*/ */
const resetSlideshowState = () => { const resetSlideshowState = () => {
console.log("🔄 Resetting slideshow state..."); console.log("🎬 Media Bar:", "🔄 Resetting slideshow state...");
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
@@ -386,14 +391,14 @@ const startLoginStatusWatcher = () => {
if (isLoggedIn !== wasLoggedIn) { if (isLoggedIn !== wasLoggedIn) {
if (isLoggedIn) { if (isLoggedIn) {
console.log("👤 User logged in. Initializing slideshow..."); console.log("🎬 Media Bar:", "👤 User logged in. Initializing slideshow...");
if (!STATE.slideshow.hasInitialized) { if (!STATE.slideshow.hasInitialized) {
waitForApiClientAndInitialize(); waitForApiClientAndInitialize();
} else { } else {
console.log("🔄 Slideshow already initialized, skipping"); console.log("🎬 Media Bar:", "🔄 Slideshow already initialized, skipping");
} }
} else { } else {
console.log("👋 User logged out. Stopping slideshow..."); console.log("🎬 Media Bar:", "👋 User logged out. Stopping slideshow...");
resetSlideshowState(); resetSlideshowState();
} }
wasLoggedIn = isLoggedIn; wasLoggedIn = isLoggedIn;
@@ -411,7 +416,7 @@ const waitForApiClientAndInitialize = () => {
window.slideshowCheckInterval = setInterval(() => { window.slideshowCheckInterval = setInterval(() => {
if (!window.ApiClient) { if (!window.ApiClient) {
console.log("⏳ ApiClient not available yet. Waiting..."); console.log("🎬 Media Bar:", "⏳ ApiClient not available yet. Waiting...");
return; return;
} }
@@ -421,23 +426,23 @@ const waitForApiClientAndInitialize = () => {
window.ApiClient._serverInfo && window.ApiClient._serverInfo &&
window.ApiClient._serverInfo.AccessToken window.ApiClient._serverInfo.AccessToken
) { ) {
console.log( console.log("🎬 Media Bar:",
"🔓 User is fully logged in. Starting slideshow initialization..." "🔓 User is fully logged in. Starting slideshow initialization..."
); );
clearInterval(window.slideshowCheckInterval); clearInterval(window.slideshowCheckInterval);
if (!STATE.slideshow.hasInitialized) { if (!STATE.slideshow.hasInitialized) {
initJellyfinData(async () => { initJellyfinData(async () => {
console.log("✅ Jellyfin API client initialized successfully"); console.log("🎬 Media Bar:", "✅ Jellyfin API client initialized successfully");
await initLocalization(); await initLocalization();
await fetchPluginConfig(); await fetchPluginConfig();
slidesInit(); slidesInit();
}); });
} else { } else {
console.log("🔄 Slideshow already initialized, skipping"); console.log("🎬 Media Bar:", "🔄 Slideshow already initialized, skipping");
} }
} else { } else {
console.log( console.log("🎬 Media Bar:",
"🔒 Authentication incomplete. Waiting for complete login..." "🔒 Authentication incomplete. Waiting for complete login..."
); );
} }
@@ -468,11 +473,11 @@ const fetchPluginConfig = async () => {
// Sync to LocalStorage for next load // Sync to LocalStorage for next load
localStorage.setItem('mediaBarEnhanced-enableLoadingScreen', CONFIG.enableLoadingScreen); localStorage.setItem('mediaBarEnhanced-enableLoadingScreen', CONFIG.enableLoadingScreen);
console.log("✅ MediaBarEnhanced config loaded", CONFIG); console.log("🎬 Media Bar:", "✅ MediaBarEnhanced config loaded", CONFIG);
} }
} }
} catch (e) { } catch (e) {
console.error("Failed to load MediaBarEnhanced config", e); console.error("🎬 Media Bar:", "Failed to load MediaBarEnhanced config", e);
} }
}; };
@@ -738,7 +743,7 @@ const SlideUtils = {
} }
} }
} catch (e) { } catch (e) {
console.warn("Invalid URL for modal:", url); console.warn("🎬 Media Bar:", "Invalid URL for modal:", url);
} }
if (isYoutube && videoId) { if (isYoutube && videoId) {
@@ -747,8 +752,9 @@ const SlideUtils = {
src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`, src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`,
allow: 'autoplay; encrypted-media', allow: 'autoplay; encrypted-media',
style: 'width: 100%; height: 100%; border: none;', style: 'width: 100%; height: 100%; border: none;',
referrerpolicy: 'strict-origin-when-cross-origin',
allowfullscreen: 'true'
}); });
ytIframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
contentContainer.appendChild(ytIframe); contentContainer.appendChild(ytIframe);
overlay.append(closeButton, contentContainer); overlay.append(closeButton, contentContainer);
@@ -820,7 +826,7 @@ const LocalizationUtils = {
locale = localStorage.getItem("language").toLowerCase(); locale = localStorage.getItem("language").toLowerCase();
} }
} catch (e) { } catch (e) {
console.warn("Could not access localStorage for language:", e); console.warn("🎬 Media Bar:", "Could not access localStorage for language:", e);
} }
if (!locale) { if (!locale) {
@@ -846,7 +852,7 @@ const LocalizationUtils = {
} }
} }
} catch (error) { } catch (error) {
console.warn("Could not fetch user audio language preference:", error); console.warn("🎬 Media Bar:", "Could not fetch user audio language preference:", error);
} }
} }
@@ -866,7 +872,7 @@ const LocalizationUtils = {
} }
} }
} catch (error) { } catch (error) {
console.warn("Could not fetch server metadata language preference:", error); console.warn("🎬 Media Bar:", "Could not fetch server metadata language preference:", error);
} }
} }
@@ -911,7 +917,7 @@ const LocalizationUtils = {
} }
} }
} catch (e) { } catch (e) {
console.warn("Error checking performance entries:", e); console.warn("🎬 Media Bar:", "Error checking performance entries:", e);
} }
} }
@@ -976,7 +982,7 @@ const LocalizationUtils = {
this.translations[locale] = JSON.parse(jsonString); this.translations[locale] = JSON.parse(jsonString);
return; return;
} catch (e) { } catch (e) {
console.error('Failed to parse JSON from standard extraction.'); console.error("🎬 Media Bar:", 'Failed to parse JSON from standard extraction.');
// Try alternative extraction below // Try alternative extraction below
} }
@@ -988,7 +994,7 @@ const LocalizationUtils = {
this.translations[locale] = JSON.parse(jsonString); this.translations[locale] = JSON.parse(jsonString);
return; return;
} catch (e) { } catch (e) {
console.error('Failed to parse JSON from direct extraction.'); console.error("🎬 Media Bar:", 'Failed to parse JSON from direct extraction.');
// Try direct extraction // Try direct extraction
} }
} }
@@ -1002,11 +1008,11 @@ const LocalizationUtils = {
this.translations[locale] = JSON.parse(jsonString); this.translations[locale] = JSON.parse(jsonString);
return; return;
} catch (e) { } catch (e) {
console.error("Failed to parse JSON from chunk:", e); console.error("🎬 Media Bar:", "Failed to parse JSON from chunk:", e);
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading translations:", error); console.error("🎬 Media Bar:", "Error loading translations:", error);
} finally { } finally {
delete this.isLoading[locale]; delete this.isLoading[locale];
} }
@@ -1070,38 +1076,11 @@ const ApiUtils = {
return itemData; return itemData;
} catch (error) { } catch (error) {
console.error(`Error fetching details for item ${itemId}:`, error); console.error("🎬 Media Bar:", `Error fetching details for item ${itemId}:`, error);
return null; return null;
} }
}, },
/**
* Fetch item IDs from the list file
* @returns {Promise<Array>} Array of item IDs
*/
// MARK: LIST FILE
async fetchItemIdsFromList() {
try {
const listFileName = `${STATE.jellyfinData.serverAddress}/web/avatars/list.txt?userId=${STATE.jellyfinData.userId}`;
const response = await fetch(listFileName);
if (!response.ok) {
console.warn("list.txt not found or inaccessible. Using random items.");
return [];
}
const text = await response.text();
return text
.split("\n")
.map((id) => id.trim())
.filter((id) => id)
.slice(1);
} catch (error) {
console.error("Error fetching list.txt:", error);
return [];
}
},
/** /**
* Fetches random items from the server * Fetches random items from the server
* @returns {Promise<Array>} Array of item objects * @returns {Promise<Array>} Array of item objects
@@ -1112,7 +1091,7 @@ const ApiUtils = {
!STATE.jellyfinData.accessToken || !STATE.jellyfinData.accessToken ||
STATE.jellyfinData.accessToken === "Not Found" STATE.jellyfinData.accessToken === "Not Found"
) { ) {
console.warn("Access token not available. Delaying API request..."); console.warn("🎬 Media Bar:", "Access token not available. Delaying API request...");
return []; return [];
} }
@@ -1120,11 +1099,11 @@ const ApiUtils = {
!STATE.jellyfinData.serverAddress || !STATE.jellyfinData.serverAddress ||
STATE.jellyfinData.serverAddress === "Not Found" STATE.jellyfinData.serverAddress === "Not Found"
) { ) {
console.warn("Server address not available. Delaying API request..."); console.warn("🎬 Media Bar:", "Server address not available. Delaying API request...");
return []; return [];
} }
console.log("Fetching random items from server..."); console.log("🎬 Media Bar:", "Fetching random items from server...");
let sortParams = `sortBy=${CONFIG.sortBy}`; let sortParams = `sortBy=${CONFIG.sortBy}`;
@@ -1137,30 +1116,97 @@ const ApiUtils = {
// Filter by isPlayed=False unless IncludeWatchedContent is enabled // Filter by isPlayed=False unless IncludeWatchedContent is enabled
const playedFilter = CONFIG.includeWatchedContent ? '' : '&isPlayed=False'; const playedFilter = CONFIG.includeWatchedContent ? '' : '&isPlayed=False';
const response = await fetch( let parentalFilter = '';
`${STATE.jellyfinData.serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}${playedFilter}&enableUserData=true&Limit=${CONFIG.maxItems}&fields=Id`, if (CONFIG.maxParentalRating) {
{ parentalFilter = `&MaxOfficialRating=${CONFIG.maxParentalRating}`;
headers: this.getAuthHeaders(),
} }
);
let dateFilter = '';
if (CONFIG.maxDaysRecent) {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - CONFIG.maxDaysRecent);
dateFilter = `&minDateLastSaved=${pastDate.toISOString()}`;
}
// Exclude seasonal content from random lists
let excludeFilter = '';
if (CONFIG.excludeSeasonalContent && CONFIG.seasonalSections) {
try {
const sections = JSON.parse(CONFIG.seasonalSections || "[]");
let allExcludedIds = [];
for (const section of sections) {
if (section.MediaIds) {
const idsInThisSection = section.MediaIds.split(/[\n,]/)
.map((line) => {
const urlMatch = line.match(/\[(.*?)\]/);
let id = line;
if (urlMatch) {
id = line.replace(/\[.*?\]/, '').trim();
const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) { id = guidMatch[1]; } else { id = id.split('|')[0].trim(); }
}
return id.trim();
})
.filter((id) => id);
allExcludedIds.push(...idsInThisSection);
}
}
if (allExcludedIds.length > 0) {
excludeFilter = `&ExcludeItemIds=${allExcludedIds.join(',')}`;
}
} catch(e) {
console.error("🎬 Media Bar:", "Error extracting seasonal IDs for exclusion:", e);
}
}
const fetchItems = async (currentDateFilter) => {
const url = `${STATE.jellyfinData.serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}${playedFilter}${parentalFilter}${currentDateFilter}${excludeFilter}&enableUserData=true&Limit=${CONFIG.maxItems}&fields=Id,DateCreated`;
const resp = await fetch(url, { headers: this.getAuthHeaders() });
return resp;
};
let response = await fetchItems(dateFilter);
if (!response.ok) { if (!response.ok) {
console.error( console.error("🎬 Media Bar:",
`Failed to fetch items: ${response.status} ${response.statusText}` `Failed to fetch items: ${response.status} ${response.statusText}`
); );
return []; return [];
} }
const data = await response.json(); let data = await response.json();
const items = data.Items || []; let items = data.Items || [];
console.log( // Local exact DateCreated filter: minDateLastSaved pulls items that were merely modified recently (e.g. metadata updates)
`Successfully fetched ${items.length} random items from server` // explicitly discard them if their actual DateCreated is older than X days
); if (CONFIG.maxDaysRecent && dateFilter !== '') {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - CONFIG.maxDaysRecent);
items = items.filter(item => {
if (!item.DateCreated) return true;
return new Date(item.DateCreated) >= pastDate;
});
}
// Fallback: If we have a date filter but no items are returned, try again without it
if (items.length === 0 && dateFilter !== '') {
console.warn("🎬 Media Bar:", `No items found within the last ${CONFIG.maxDaysRecent} days. Falling back to random fetching.`);
response = await fetchItems('');
if (response.ok) {
data = await response.json();
items = data.Items || [];
}
}
console.log("🎬 Media Bar:", `Successfully fetched ${items.length} random items from server`);
return items.map((item) => item.Id); return items.map((item) => item.Id);
} catch (error) { } catch (error) {
console.error("Error fetching item IDs:", error); console.error("🎬 Media Bar:", "Error fetching item IDs:", error);
return []; return [];
} }
}, },
@@ -1184,7 +1230,7 @@ const ApiUtils = {
try { try {
const sessionId = await this.getSessionId(); const sessionId = await this.getSessionId();
if (!sessionId) { if (!sessionId) {
console.error("Session ID not found."); console.error("🎬 Media Bar:", "Session ID not found.");
return false; return false;
} }
@@ -1200,10 +1246,10 @@ const ApiUtils = {
); );
} }
console.log("Play command sent successfully to session:", sessionId); console.log("🎬 Media Bar:", "Play command sent successfully to session:", sessionId);
return true; return true;
} catch (error) { } catch (error) {
console.error("Error sending play command:", error); console.error("🎬 Media Bar:", "Error sending play command:", error);
return false; return false;
} }
}, },
@@ -1229,7 +1275,7 @@ const ApiUtils = {
const sessions = await response.json(); const sessions = await response.json();
if (!sessions || sessions.length === 0) { if (!sessions || sessions.length === 0) {
console.warn( console.warn("🎬 Media Bar:",
"No sessions found for deviceId:", "No sessions found for deviceId:",
STATE.jellyfinData.deviceId STATE.jellyfinData.deviceId
); );
@@ -1238,7 +1284,7 @@ const ApiUtils = {
return sessions[0].Id; return sessions[0].Id;
} catch (error) { } catch (error) {
console.error("Error fetching session data:", error); console.error("🎬 Media Bar:", "Error fetching session data:", error);
return null; return null;
} }
}, },
@@ -1266,7 +1312,7 @@ const ApiUtils = {
} }
button.classList.toggle("favorited", !isFavorite); button.classList.toggle("favorited", !isFavorite);
} catch (error) { } catch (error) {
console.error("Error toggling favorite:", error); console.error("🎬 Media Bar:", "Error toggling favorite:", error);
} }
}, },
@@ -1308,7 +1354,7 @@ const ApiUtils = {
this._sponsorBlockCache[videoId] = result; this._sponsorBlockCache[videoId] = result;
return result; return result;
} catch (error) { } catch (error) {
console.warn('Error fetching SponsorBlock data:', error); console.warn("🎬 Media Bar:", 'Error fetching SponsorBlock data:', error);
return { intro: null, outro: null }; return { intro: null, outro: null };
} }
}, },
@@ -1328,7 +1374,7 @@ const ApiUtils = {
); );
if (!response.ok) { if (!response.ok) {
console.warn(`Failed to search for '${name}'`); console.warn("🎬 Media Bar:", `Failed to search for '${name}'`);
return null; return null;
} }
@@ -1338,7 +1384,7 @@ const ApiUtils = {
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(`Error searching for '${name}':`, error); console.error("🎬 Media Bar:", `Error searching for '${name}':`, error);
return null; return null;
} }
}, },
@@ -1358,16 +1404,16 @@ const ApiUtils = {
); );
if (!response.ok) { if (!response.ok) {
console.warn(`Failed to fetch collection items for ${collectionId}`); console.warn("🎬 Media Bar:", `Failed to fetch collection items for ${collectionId}`);
return []; return [];
} }
const data = await response.json(); const data = await response.json();
const items = data.Items || []; const items = data.Items || [];
console.log(`Resolved collection ${collectionId} to ${items.length} items`); console.log("🎬 Media Bar:", `Resolved collection ${collectionId} to ${items.length} items`);
return items.map(i => ({ Id: i.Id, Type: i.Type })); return items.map(i => ({ Id: i.Id, Type: i.Type }));
} catch (error) { } catch (error) {
console.error(`Error fetching collection items for ${collectionId}:`, error); console.error("🎬 Media Bar:", `Error fetching collection items for ${collectionId}:`, error);
return []; return [];
} }
}, },
@@ -1397,7 +1443,7 @@ const ApiUtils = {
if (CONFIG.randomizeLocalTrailers && trailers.length > 1) { if (CONFIG.randomizeLocalTrailers && trailers.length > 1) {
const randomIndex = Math.floor(Math.random() * trailers.length); const randomIndex = Math.floor(Math.random() * trailers.length);
trailer = trailers[randomIndex]; trailer = trailers[randomIndex];
console.log(`Using random local trailer (${randomIndex + 1}/${trailers.length}) for ${itemId}: ${trailer.Name}`); console.log("🎬 Media Bar:", `Using random local trailer (${randomIndex + 1}/${trailers.length}) for ${itemId}: ${trailer.Name}`);
} else { } else {
trailer = trailers[0]; trailer = trailers[0];
} }
@@ -1411,7 +1457,7 @@ const ApiUtils = {
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(`Error fetching local trailer for ${itemId}:`, error); console.error("🎬 Media Bar:", `Error fetching local trailer for ${itemId}:`, error);
return null; return null;
} }
}, },
@@ -1437,10 +1483,10 @@ const ApiUtils = {
if (CONFIG.randomizeThemeVideos && items.length > 1) { if (CONFIG.randomizeThemeVideos && items.length > 1) {
const randomIndex = Math.floor(Math.random() * items.length); const randomIndex = Math.floor(Math.random() * items.length);
video = items[randomIndex]; video = items[randomIndex];
console.log(`Found Theme Video (Random ${randomIndex + 1}/${items.length}) via ThemeVideos endpoint: ${video.Name} (${video.Id})`); console.log("🎬 Media Bar:", `Found Theme Video (Random ${randomIndex + 1}/${items.length}) via ThemeVideos endpoint: ${video.Name} (${video.Id})`);
} else { } else {
video = items[0]; video = items[0];
console.log(`Found Theme Video (First) via ThemeVideos endpoint: ${video.Name} (${video.Id})`); console.log("🎬 Media Bar:", `Found Theme Video (First) via ThemeVideos endpoint: ${video.Name} (${video.Id})`);
} }
return { return {
@@ -1451,7 +1497,7 @@ const ApiUtils = {
} }
return null; return null;
} catch (error) { } catch (error) {
console.error(`Error fetching theme videos for ${itemId}:`, error); console.error("🎬 Media Bar:", `Error fetching theme videos for ${itemId}:`, error);
return null; return null;
} }
} }
@@ -1635,7 +1681,7 @@ const SlideCreator = {
*/ */
createSlideElement(item, title) { createSlideElement(item, title) {
if (!item || !item.Id) { if (!item || !item.Id) {
console.error("Invalid item data:", item); console.error("🎬 Media Bar:", "Invalid item data:", item);
return null; return null;
} }
@@ -1667,7 +1713,7 @@ const SlideCreator = {
if (guidMatch) { if (guidMatch) {
const videoId = guidMatch[1]; const videoId = guidMatch[1];
console.log(`Using custom local video ID for ${itemId}: ${videoId}`); console.log("🎬 Media Bar:", `Using custom local video ID for ${itemId}: ${videoId}`);
trailerUrl = { trailerUrl = {
id: videoId, id: videoId,
@@ -1676,18 +1722,18 @@ const SlideCreator = {
} else { } else {
// Assume it's a standard URL (YouTube, etc.) // Assume it's a standard URL (YouTube, etc.)
trailerUrl = customValue; trailerUrl = customValue;
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`); console.log("🎬 Media Bar:", `Using custom trailer URL for ${itemId}: ${trailerUrl}`);
} }
} }
// 1b. Check Theme Video if preferred (Local Backdrop) // 1b. Check Theme Video if preferred (Local Backdrop)
else if (CONFIG.preferLocalBackdrops && item.themeVideoUrl) { else if (CONFIG.preferLocalBackdrops && item.themeVideoUrl) {
trailerUrl = item.themeVideoUrl; trailerUrl = item.themeVideoUrl;
console.log(`Using theme video (local backdrop) for ${itemId}: ${trailerUrl.url || trailerUrl}`); console.log("🎬 Media Bar:", `Using theme video (local backdrop) for ${itemId}: ${trailerUrl.url || trailerUrl}`);
} }
// 1c. Check Local Trailer if preferred // 1c. Check Local Trailer if preferred
else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) { else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) {
trailerUrl = item.localTrailerUrl; trailerUrl = item.localTrailerUrl;
console.log(`Using local trailer for ${itemId}: ${trailerUrl}`); console.log("🎬 Media Bar:", `Using local trailer for ${itemId}: ${trailerUrl}`);
} }
// 1d. Fallback to Remote Trailer // 1d. Fallback to Remote Trailer
else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) { else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) {
@@ -1696,7 +1742,7 @@ const SlideCreator = {
// 1e. Final Fallback to Local Trailer (even if not preferred) // 1e. Final Fallback to Local Trailer (even if not preferred)
else if (item.LocalTrailerCount > 0 && item.localTrailerUrl) { else if (item.LocalTrailerCount > 0 && item.localTrailerUrl) {
trailerUrl = item.localTrailerUrl; trailerUrl = item.localTrailerUrl;
console.log(`Using local trailer fallback for ${itemId}: ${trailerUrl}`); console.log("🎬 Media Bar:", `Using local trailer fallback for ${itemId}: ${trailerUrl}`);
} }
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
@@ -1726,7 +1772,7 @@ const SlideCreator = {
} }
} }
} catch (e) { } catch (e) {
console.warn("Invalid trailer URL:", trailerUrl); console.warn("🎬 Media Bar:", "Invalid trailer URL:", trailerUrl);
} }
const isLowPower = isLowPowerDevice(); const isLowPower = isLowPowerDevice();
@@ -1751,7 +1797,8 @@ const SlideCreator = {
src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`, src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`,
style: "width: 100%; height: 100%; border: none;", style: "width: 100%; height: 100%; border: none;",
allow: "autoplay; encrypted-media", allow: "autoplay; encrypted-media",
referrerpolicy: "strict-origin-when-cross-origin" referrerpolicy: "strict-origin-when-cross-origin",
allowfullscreen: "true"
}); });
videoBackdrop.appendChild(ytPlayerIframe); videoBackdrop.appendChild(ytPlayerIframe);
@@ -1792,11 +1839,11 @@ const SlideCreator = {
// Apply SponsorBlock start/end times // Apply SponsorBlock start/end times
if (segments.intro) { if (segments.intro) {
playerVars.start = Math.ceil(segments.intro[1]); playerVars.start = Math.ceil(segments.intro[1]);
console.info(`SponsorBlock intro detected for video ${videoId}: skipping to ${playerVars.start}s`); console.info("🎬 Media Bar:", `SponsorBlock intro detected for video ${videoId}: skipping to ${playerVars.start}s`);
} }
if (segments.outro) { if (segments.outro) {
playerVars.end = Math.floor(segments.outro[0]); playerVars.end = Math.floor(segments.outro[0]);
console.info(`SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`); console.info("🎬 Media Bar:", `SponsorBlock outro detected for video ${videoId}: ending at ${playerVars.end}s`);
} }
STATE.slideshow.videoPlayers[itemId] = new YT.Player(ytPlayerIframe, { STATE.slideshow.videoPlayers[itemId] = new YT.Player(ytPlayerIframe, {
@@ -1834,16 +1881,16 @@ const SlideCreator = {
// Re-check conditions before processing fallback // Re-check conditions before processing fallback
const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) { if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) {
console.log(`Navigation detected during autoplay check for ${itemId}, stopping video.`); console.log("🎬 Media Bar:", `Navigation detected during autoplay check for ${itemId}, stopping video.`);
try { try {
event.target.stopVideo(); event.target.stopVideo();
} catch (e) { console.warn("Error stopping video in timeout:", e); } } catch (e) { console.warn("🎬 Media Bar:", "Error stopping video in timeout:", e); }
return; return;
} }
if (event.target.getPlayerState() !== YT.PlayerState.PLAYING && if (event.target.getPlayerState() !== YT.PlayerState.PLAYING &&
event.target.getPlayerState() !== YT.PlayerState.BUFFERING) { event.target.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); console.warn("🎬 Media Bar:", `Autoplay blocked for ${itemId}, attempting muted fallback`);
event.target.mute(); event.target.mute();
event.target.playVideo(); event.target.playVideo();
} }
@@ -1874,7 +1921,7 @@ const SlideCreator = {
} }
}, },
'onError': (event) => { 'onError': (event) => {
console.warn(`YouTube player error ${event.data} for video ${videoId}`); console.warn("🎬 Media Bar:", `YouTube player error ${event.data} for video ${videoId}`);
// Fallback to next slide on error // Fallback to next slide on error
if (CONFIG.waitForTrailerToEnd) { if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
@@ -1908,7 +1955,7 @@ const SlideCreator = {
videoBackdrop.addEventListener('play', (event) => { videoBackdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (!slide || !slide.classList.contains('active')) { if (!slide || !slide.classList.contains('active')) {
console.log(`Local video ${itemId} started playing but slide is not active, pausing.`); console.log("🎬 Media Bar:", `Local video ${itemId} started playing but slide is not active, pausing.`);
event.target.pause(); event.target.pause();
event.target.currentTime = 0; event.target.currentTime = 0;
return; return;
@@ -1930,7 +1977,7 @@ const SlideCreator = {
}); });
videoBackdrop.addEventListener('error', (event) => { videoBackdrop.addEventListener('error', (event) => {
console.warn(`Local video error for item ${itemId}`); console.warn("🎬 Media Bar:", `Local video error for item ${itemId}`);
const slide = event.target.closest('.slide'); const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
@@ -2293,7 +2340,7 @@ const SlideCreator = {
return slideElement; return slideElement;
} catch (error) { } catch (error) {
console.error("Error creating slide for item:", error, itemId); console.error("🎬 Media Bar:", "Error creating slide for item:", error, itemId);
return null; return null;
} }
}, },
@@ -2305,6 +2352,8 @@ const SlideCreator = {
const SlideshowManager = { const SlideshowManager = {
createPaginationDots() { createPaginationDots() {
if (!CONFIG.showPaginationDots) return;
let dotsContainer = document.querySelector(".dots-container"); let dotsContainer = document.querySelector(".dots-container");
if (!dotsContainer) { if (!dotsContainer) {
dotsContainer = document.createElement("div"); dotsContainer = document.createElement("div");
@@ -2392,7 +2441,7 @@ const SlideshowManager = {
this.upgradeSlideImageQuality(currentSlide); this.upgradeSlideImageQuality(currentSlide);
if (!currentSlide) { if (!currentSlide) {
console.error(`Failed to create slide for item ${currentItemId}`); console.error("🎬 Media Bar:", `Failed to create slide for item ${currentItemId}`);
STATE.slideshow.isTransitioning = false; STATE.slideshow.isTransitioning = false;
setTimeout(() => this.nextSlide(), 500); setTimeout(() => this.nextSlide(), 500);
return; return;
@@ -2405,6 +2454,7 @@ const SlideshowManager = {
previousVisibleSlide.classList.remove("active"); previousVisibleSlide.classList.remove("active");
} }
void currentSlide.offsetWidth;
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
@@ -2484,9 +2534,9 @@ const SlideshowManager = {
// Check if it actually started playing after a short delay (handling autoplay blocks) // Check if it actually started playing after a short delay (handling autoplay blocks)
setTimeout(() => { setTimeout(() => {
if (videoBackdrop.paused && currentSlide.classList.contains('active')) { if (videoBackdrop.paused && currentSlide.classList.contains('active')) {
console.warn(`Autoplay blocked for ${currentItemId}, attempting muted fallback`); console.warn("🎬 Media Bar:", `Autoplay blocked for ${currentItemId}, attempting muted fallback`);
videoBackdrop.muted = true; videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err)); videoBackdrop.play().catch(err => console.error("🎬 Media Bar:", "Muted fallback failed", err));
} }
}, 1000); }, 1000);
}); });
@@ -2513,7 +2563,7 @@ const SlideshowManager = {
if (player.getPlayerState && if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING && player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) { player.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.log("YouTube loadVideoById didn't start playback, retrying muted..."); console.log("🎬 Media Bar:", "YouTube loadVideoById didn't start playback, retrying muted...");
player.mute(); player.mute();
player.playVideo(); player.playVideo();
} }
@@ -2572,7 +2622,7 @@ const SlideshowManager = {
this.pruneSlideCache(); this.pruneSlideCache();
} catch (error) { } catch (error) {
console.error("Error updating current slide:", error); console.error("🎬 Media Bar:", "Error updating current slide:", error);
} finally { } finally {
setTimeout(() => { setTimeout(() => {
STATE.slideshow.isTransitioning = false; STATE.slideshow.isTransitioning = false;
@@ -2682,9 +2732,9 @@ const SlideshowManager = {
const totalItems = STATE.slideshow.itemIds.length; const totalItems = STATE.slideshow.itemIds.length;
let distance = Math.abs(index - currentIndex); let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) {
// Always calculate circular distance for slideshow
distance = Math.min(distance, totalItems - distance); distance = Math.min(distance, totalItems - distance);
}
if (distance > keepRange) { if (distance > keepRange) {
// Destroy video player if exists // Destroy video player if exists
@@ -2716,7 +2766,7 @@ const SlideshowManager = {
delete STATE.slideshow.createdSlides[itemId]; delete STATE.slideshow.createdSlides[itemId];
prunedAny = true; prunedAny = true;
console.log(`Pruned slide ${itemId} at distance ${distance} from view`); console.log("🎬 Media Bar:", `Pruned slide ${itemId} at distance ${distance} from view`);
} }
}); });
@@ -2763,7 +2813,7 @@ const SlideshowManager = {
} }
video.play().catch(error => { video.play().catch(error => {
console.warn("Unmuted play blocked, reverting to muted..."); console.warn("🎬 Media Bar:", "Unmuted play blocked, reverting to muted...");
STATE.slideshow.isMuted = true; STATE.slideshow.isMuted = true;
video.muted = true; video.muted = true;
video.play(); video.play();
@@ -2784,7 +2834,7 @@ const SlideshowManager = {
setTimeout(() => { setTimeout(() => {
const state = player.getPlayerState(); const state = player.getPlayerState();
if (state === 2) { if (state === 2) {
console.log("Video was paused after unmute..."); console.log("🎬 Media Bar:", "Video was paused after unmute...");
STATE.slideshow.isMuted = true; STATE.slideshow.isMuted = true;
player.mute(); player.mute();
player.playVideo(); player.playVideo();
@@ -2875,7 +2925,7 @@ const SlideshowManager = {
player.pauseVideo(); player.pauseVideo();
} }
} catch (e) { } catch (e) {
console.warn("Error pausing/stopping YouTube player:", e); console.warn("🎬 Media Bar:", "Error pausing/stopping YouTube player:", e);
} }
}); });
} }
@@ -2895,7 +2945,7 @@ const SlideshowManager = {
video.removeAttribute('src'); video.removeAttribute('src');
video.load(); video.load();
} catch (e) { } catch (e) {
console.warn("Error stopping HTML5 video:", e); console.warn("🎬 Media Bar:", "Error stopping HTML5 video:", e);
} }
}); });
} }
@@ -2936,7 +2986,7 @@ const SlideshowManager = {
} }
html5Video.muted = STATE.slideshow.isMuted; html5Video.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) html5Video.volume = 0.4; if (!STATE.slideshow.isMuted) html5Video.volume = 0.4;
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); html5Video.play().catch(e => console.warn("🎬 Media Bar:", "Error resuming HTML5 video:", e));
} }
}, },
@@ -3138,14 +3188,14 @@ const SlideshowManager = {
} }
if (isInRange) { if (isInRange) {
console.log(`Seasonal match found: ${section.Name}`); console.log("🎬 Media Bar:", `Seasonal match found: ${section.Name}`);
idsString = section.MediaIds; idsString = section.MediaIds;
usingSeasonal = true; usingSeasonal = true;
break; // Use first matching season break; // Use first matching season
} }
} }
} catch (e) { } catch (e) {
console.error("Error parsing seasonal sections in JS:", e); console.error("🎬 Media Bar:", "Error parsing seasonal sections in JS:", e);
} }
} }
@@ -3203,14 +3253,14 @@ const SlideshowManager = {
if (guidMatch) { if (guidMatch) {
id = guidMatch[1]; id = guidMatch[1];
} else { } else {
console.log(`Input '${rawId}' is not a GUID, searching for Collection/Playlist by name...`); console.log("🎬 Media Bar:", `Input '${rawId}' is not a GUID, searching for Collection/Playlist by name...`);
const resolvedId = await ApiUtils.findCollectionOrPlaylistByName(rawId); const resolvedId = await ApiUtils.findCollectionOrPlaylistByName(rawId);
if (resolvedId) { if (resolvedId) {
console.log(`Resolved name '${rawId}' to ID: ${resolvedId}`); console.log("🎬 Media Bar:", `Resolved name '${rawId}' to ID: ${resolvedId}`);
id = resolvedId; id = resolvedId;
} else { } else {
console.warn(`Could not find Collection or Playlist with name: '${rawId}'`); console.warn("🎬 Media Bar:", `Could not find Collection or Playlist with name: '${rawId}'`);
continue; // Skip if resolution failed continue; // Skip if resolution failed
} }
} }
@@ -3218,14 +3268,14 @@ const SlideshowManager = {
const item = await ApiUtils.fetchItemDetails(id); const item = await ApiUtils.fetchItemDetails(id);
if (item && (item.Type === 'BoxSet' || item.Type === 'Playlist')) { if (item && (item.Type === 'BoxSet' || item.Type === 'Playlist')) {
console.log(`Found Collection/Playlist: ${id} (${item.Type}), fetching children...`); console.log("🎬 Media Bar:", `Found Collection/Playlist: ${id} (${item.Type}), fetching children...`);
const children = await ApiUtils.fetchCollectionItems(id); const children = await ApiUtils.fetchCollectionItems(id);
finalIds.push(...children); finalIds.push(...children);
} else if (item) { } else if (item) {
finalIds.push({ Id: item.Id, Type: item.Type }); finalIds.push({ Id: item.Id, Type: item.Type });
} }
} catch (e) { } catch (e) {
console.warn(`Error resolving item ${rawId}:`, e); console.warn("🎬 Media Bar:", `Error resolving item ${rawId}:`, e);
} }
} }
return finalIds; return finalIds;
@@ -3241,7 +3291,7 @@ const SlideshowManager = {
// 1. Try Custom Media/Collection IDs from Config & seasonal content // 1. Try Custom Media/Collection IDs from Config & seasonal content
if (CONFIG.enableCustomMediaIds || CONFIG.enableSeasonalContent) { if (CONFIG.enableCustomMediaIds || CONFIG.enableSeasonalContent) {
console.log("Using Custom Media IDs from configuration"); console.log("🎬 Media Bar:", "Using Custom Media IDs from configuration");
const rawIds = this.parseCustomIds(); const rawIds = this.parseCustomIds();
const resolvedItems = await this.resolveCollectionsAndItems(rawIds); const resolvedItems = await this.resolveCollectionsAndItems(rawIds);
@@ -3271,20 +3321,15 @@ const SlideshowManager = {
} }
} }
itemIds = keptItems.map(i => i.Id); itemIds = keptItems.map(i => i.Id);
console.log(`Applied limits to custom IDs: ${itemIds.length} items (Movies: ${movieCount}, Shows: ${showCount})`); console.log("🎬 Media Bar:", `Applied limits to custom IDs: ${itemIds.length} items (Movies: ${movieCount}, Shows: ${showCount})`);
} else { } else {
itemIds = resolvedItems.map(i => i.Id); itemIds = resolvedItems.map(i => i.Id);
} }
} }
// 2. Try Avatar List (list.txt) // 2. Fallback to server query (Random)
if (itemIds.length === 0) { if (itemIds.length === 0) {
itemIds = await ApiUtils.fetchItemIdsFromList(); console.log("🎬 Media Bar:", "No custom list found, fetching random items from server...");
}
// 3. Fallback to server query (Random)
if (itemIds.length === 0) {
console.log("No custom list found, fetching random items from server...");
itemIds = await ApiUtils.fetchItemIdsFromServer(); itemIds = await ApiUtils.fetchItemIdsFromServer();
if (CONFIG.sortBy === 'Random') { if (CONFIG.sortBy === 'Random') {
@@ -3296,7 +3341,7 @@ const SlideshowManager = {
itemIds = SlideUtils.shuffleArray(itemIds); itemIds = SlideUtils.shuffleArray(itemIds);
} else if (CONFIG.sortBy !== 'Original') { } else if (CONFIG.sortBy !== 'Original') {
// Client-side sort required... // Client-side sort required...
console.log(`Sorting ${itemIds.length} custom items by ${CONFIG.sortBy} ${CONFIG.sortOrder}`); console.log("🎬 Media Bar:", `Sorting ${itemIds.length} custom items by ${CONFIG.sortBy} ${CONFIG.sortOrder}`);
const itemsWithDetails = []; const itemsWithDetails = [];
for (const id of itemIds) { for (const id of itemIds) {
const item = await ApiUtils.fetchItemDetails(id); const item = await ApiUtils.fetchItemDetails(id);
@@ -3338,7 +3383,7 @@ const SlideshowManager = {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading slideshow data:", error); console.error("🎬 Media Bar:", "Error loading slideshow data:", error);
} finally { } finally {
STATE.slideshow.isLoading = false; STATE.slideshow.isLoading = false;
} }
@@ -3426,6 +3471,10 @@ const initArrowNavigation = () => {
container.appendChild(muteButton); container.appendChild(muteButton);
const showArrows = () => { const showArrows = () => {
if (CONFIG.hideArrowsOnMobile && window.matchMedia("only screen and (max-width: 768px)").matches) {
return; // disable arrow display on mobile
}
leftArrow.style.display = "block"; leftArrow.style.display = "block";
rightArrow.style.display = "block"; rightArrow.style.display = "block";
@@ -3484,7 +3533,7 @@ const MediaBarEnhancedSettingsManager = {
this.initialized = true; this.initialized = true;
this.injectSettingsIcon(); this.injectSettingsIcon();
console.log("MediaBarEnhanced: Client-Side Settings Manager initialized."); console.log("🎬 Media Bar:", "Client-Side Settings Manager initialized.");
}, },
getSetting(key, defaultValue) { getSetting(key, defaultValue) {
@@ -3658,7 +3707,7 @@ const initPageVisibilityHandler = () => {
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (document.hidden) { if (document.hidden) {
console.log("Tab inactive - pausing slideshow and videos"); console.log("🎬 Media Bar:", "Tab inactive - pausing slideshow and videos");
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying; wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
@@ -3676,7 +3725,7 @@ const initPageVisibilityHandler = () => {
player.pauseVideo(); player.pauseVideo();
STATE.slideshow.isVideoPlaying = false; STATE.slideshow.isVideoPlaying = false;
} catch (e) { } catch (e) {
console.warn("Error pausing video on tab hide:", e); console.warn("🎬 Media Bar:", "Error pausing video on tab hide:", e);
} }
} else if (player.tagName === 'VIDEO') { // HTML5 Video } else if (player.tagName === 'VIDEO') { // HTML5 Video
player.pause(); player.pause();
@@ -3685,7 +3734,7 @@ const initPageVisibilityHandler = () => {
} }
} }
} else { } else {
console.log("Tab active - resuming slideshow"); console.log("🎬 Media Bar:", "Tab active - resuming slideshow");
if (!STATE.slideshow.isPaused) { if (!STATE.slideshow.isPaused) {
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
@@ -3698,16 +3747,16 @@ const initPageVisibilityHandler = () => {
player.playVideo(); player.playVideo();
STATE.slideshow.isVideoPlaying = true; STATE.slideshow.isVideoPlaying = true;
} catch (e) { } catch (e) {
console.warn("Error resuming video on tab show:", e); console.warn("🎬 Media Bar:", "Error resuming video on tab show:", e);
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.start(); STATE.slideshow.slideInterval.start();
} }
} }
} else if (player.tagName === 'VIDEO') { // HTML5 Video } else if (player.tagName === 'VIDEO') { // HTML5 Video
try { try {
player.play().catch(e => console.warn("Error resuming HTML5 video:", e)); player.play().catch(e => console.warn("🎬 Media Bar:", "Error resuming HTML5 video:", e));
STATE.slideshow.isVideoPlaying = true; STATE.slideshow.isVideoPlaying = true;
} catch(e) { console.warn(e); } } catch(e) { console.warn("🎬 Media Bar:", e); }
} }
} else { } else {
// No video was playing, just restart interval // No video was playing, just restart interval
@@ -3733,7 +3782,7 @@ const initPageVisibilityHandler = () => {
*/ */
const slidesInit = async () => { const slidesInit = async () => {
if (STATE.slideshow.hasInitialized) { if (STATE.slideshow.hasInitialized) {
console.log("⚠️ Slideshow already initialized, skipping"); console.log("🎬 Media Bar:", "⚠️ Slideshow already initialized, skipping");
return; return;
} }
@@ -3741,7 +3790,7 @@ const slidesInit = async () => {
MediaBarEnhancedSettingsManager.init(); MediaBarEnhancedSettingsManager.init();
const isClientSideEnabled = MediaBarEnhancedSettingsManager.getSetting('enabled', true); const isClientSideEnabled = MediaBarEnhancedSettingsManager.getSetting('enabled', true);
if (!isClientSideEnabled) { if (!isClientSideEnabled) {
console.log("MediaBarEnhanced: Disabled by client-side setting."); console.log("🎬 Media Bar:", "Disabled by client-side setting.");
const homeSections = document.querySelector('.homeSectionsContainer'); const homeSections = document.querySelector('.homeSectionsContainer');
if (homeSections) { if (homeSections) {
homeSections.style.top = '0'; homeSections.style.top = '0';
@@ -3831,7 +3880,7 @@ const slidesInit = async () => {
const lazyLoadObserver = initLazyLoading(); const lazyLoadObserver = initLazyLoading();
try { try {
console.log("🌟 Initializing Enhanced Jellyfin Slideshow"); console.log("🎬 Media Bar:", "🌟 Initializing Enhanced Jellyfin Slideshow");
initArrowNavigation(); initArrowNavigation();
@@ -3845,9 +3894,9 @@ const slidesInit = async () => {
VisibilityObserver.init(); VisibilityObserver.init();
console.log("✅ Enhanced Jellyfin Slideshow initialized successfully"); console.log("🎬 Media Bar:", "✅ Enhanced Jellyfin Slideshow initialized successfully");
} catch (error) { } catch (error) {
console.error("Error initializing slideshow:", error); console.error("🎬 Media Bar:", "Error initializing slideshow:", error);
STATE.slideshow.hasInitialized = false; STATE.slideshow.hasInitialized = false;
} }
}; };

View File

@@ -9,12 +9,20 @@
"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.7.0.10", "version": "1.7.1.8",
"changelog": "- Add YouTube no-cookie host and referrer policy for iframe security to fix playback issues on iOS/MacOS", "changelog": "- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- Add hide arrows on mobile option \n- fix button issue on mobile when using ElegantFin Theme",
"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.7.0.10/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.8/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "054793363c988cfd1dce37876a0e800f", "checksum": "d10d107db41ecf9cb26d4b248426c4a1",
"timestamp": "2026-03-06T02:02:52Z" "timestamp": "2026-03-08T19:29:24Z"
},
{
"version": "1.7.0.14",
"changelog": "- Switched to YouTube no-cookie host and referrer policy for iframe security\n- fix playback issues on iOS/MacOS \n- Disable animations and backdrop filters for TV layout\n- removed list.txt functionality to clean up, use the web ui instead\n- Enhance logging with contextual messages, in order to be able to clearly assign logs to this plugin",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.0.14/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "07875c74aab766657be3b8033be6d53f",
"timestamp": "2026-03-06T03:13:48Z"
}, },
{ {
"version": "1.6.6.4", "version": "1.6.6.4",

View File

@@ -0,0 +1,73 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
const maxDaysRecent = 30; // 30 Tage Limit
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
const dateStr = pastDate.toISOString();
console.log(`\n%c=== TEST: DateCreated Direkt-Abfrage ===`, "color: #00a4dc; font-weight: bold; font-size: 14px;");
console.log(`Wir suchen Filme, die nach dem ${dateStr} hinzugefügt wurden.\n`);
// Wir probieren alle denkbaren Parameter-Schreibweisen aus,
// die Jellyfin historisch oder in Forks für "DateCreated" akzeptieren könnte.
const testCases = [
{ name: "MinDateCreated (PascalCase)", param: `MinDateCreated=${dateStr}` },
{ name: "minDateCreated (camelCase)", param: `minDateCreated=${dateStr}` },
{ name: "DateCreatedMin", param: `DateCreatedMin=${dateStr}` },
{ name: "dateCreatedMin", param: `dateCreatedMin=${dateStr}` },
{ name: "StartDate", param: `StartDate=${dateStr}` },
{ name: "startDate", param: `startDate=${dateStr}` }
];
try {
for (let i = 0; i < testCases.length; i++) {
const test = testCases[i];
const apiUrl = `${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=5&fields=Id,DateCreated&userId=${userId}&${test.param}`;
console.log(`%cTest ${i+1}: ${test.name}`, "color: yellow;");
console.log(`URL: ${apiUrl}`);
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
console.error(`-> ❌ HTTP Fehler: ${response.status}`);
continue;
}
const data = await response.json();
const items = data.Items || [];
// Zur Auswertung: Wenn die Abfrage ignoriert wird, liefert er oft ALLE (hier max 5) zurück.
// Filtert er wirklich, sollten es WENIGER als 5, und zwar im besten Fall genau die RICHTIGEN sein.
console.log(`-> Ergebnis: ${items.length} Items zurückgeliefert.`);
if (items.length > 0) {
// Wir checken, ob die zurückgelieferten Items WIRKLICH neu sind
const oldItems = items.filter(item => new Date(item.DateCreated) < pastDate);
if (oldItems.length > 0) {
console.log(` ❌ Filter FEHLGESCHLAGEN: Es wurden ${oldItems.length} "alte" Filme zurückgegeben (z.B. ${oldItems[0].Name}). Er ignoriert also den Parameter.`);
} else {
console.log(` ✅ Filter KÖNNTE funktionieren: Alle zurückgegebenen Filme sind neuer als unser Datum! (Erster: ${items[0].Name})`);
}
} else {
console.log(` ❓ Keine Items gefunden (entweder strenger Filter oder gar keine neuen Filme vorhanden).`);
}
console.log("\n");
}
} catch (error) {
console.error("Fehler beim Abruf:", error);
}
})();

View File

@@ -0,0 +1,56 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
const maxDaysRecent = 30; // Test: Added in last 30 days
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
const dateStr = pastDate.toISOString();
console.log(`Searching for items added after: ${dateStr}`);
const testUrls = [
// Test 1: minDateCreated (CamelCase)
`${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=5&fields=Id,DateCreated&userId=${userId}&minDateCreated=${dateStr}`,
// Test 2: minDateLastSaved
`${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=5&fields=Id,DateCreated&userId=${userId}&minDateLastSaved=${dateStr}`,
];
try {
for (let i = 0; i < testUrls.length; i++) {
const apiUrl = testUrls[i];
console.log(`\n%cTest ${i+1}: Testing URL:\n${apiUrl}`, "color: yellow;");
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
console.error(`Failed to fetch items: ${response.status} ${response.statusText}`);
continue;
}
const data = await response.json();
const items = data.Items || [];
console.log(`%cErgebnis: ${items.length} Items gefunden!`, "color: #00a4dc; font-weight: bold;");
if(items.length > 0) {
console.log("Gefundene Items:");
items.forEach(item => {
console.log(`- Name: ${item.Name}, DateCreated: ${item.DateCreated}, Art: ${item.Type}`);
});
}
}
} catch (error) {
console.error("Fehler beim Abrufen der URL:", error);
}
})();

View File

@@ -0,0 +1,72 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
const maxDaysRecent = 30; // Test: Added in last 30 days
// 1. Calculate the cutoff date
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
const dateStr = pastDate.toISOString();
console.log(`\n%c=== TEST: 2-Stufen "Zuletzt Hinzugefügt" Filter ===`, "color: #00a4dc; font-weight: bold; font-size: 14px;");
console.log(`Suche Items neuer als: ${dateStr} (${maxDaysRecent} Tage alt)\n`);
const apiUrl = `${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=50&fields=Id,DateCreated&userId=${userId}&minDateLastSaved=${dateStr}`;
try {
console.log(`%cSchritt 1: API Call mit minDateLastSaved...`, "color: yellow;");
console.log(`URL: ${apiUrl}`);
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch items: ${response.status} ${response.statusText}`);
}
const data = await response.json();
let items = data.Items || [];
console.log(`-> API lieferte ${items.length} potenziell neue/geänderte Items zurück.\n`);
if (items.length > 0) {
console.log("Die API hielt diese Items für neu/aktuell:");
items.forEach(item => console.log(` - ${item.Name} (DateCreated: ${item.DateCreated})`));
}
console.log(`\n%cSchritt 2: Lokaler DateCreated Filter (Wie das Plugin ihn jetzt nutzt)...`, "color: yellow;");
// Exakt dieser Filter-Block ist jetzt auch so in mediaBarEnhanced.js
const finalItems = items.filter(item => {
if (!item.DateCreated) return true; // Fallback falls Jellyfin kein Datum schickt
const dCreated = new Date(item.DateCreated);
return dCreated >= pastDate;
});
console.log(`%c-> FINALES ERGEBNIS: ${finalItems.length} echte Neuzugänge bleiben übrig!`, "color: #00ff00; font-weight: bold; font-size: 14px;");
if(finalItems.length > 0) {
console.log("Diese Items schaffen es in die Slideshow:");
finalItems.forEach(item => {
console.log(` 🎬 Name: ${item.Name}, DateCreated: ${item.DateCreated}`);
});
}
// Teste den Fallback
if(finalItems.length === 0 && items.length > 0) {
console.log("\n%c💡 HINWEIS: Da nach Filterung 0 Items übrig bleiben, greift in der Slideshow jetzt automatisch unser Fallback und zeigt zufällige Filme aller Jahre!", "color: orange;");
}
} catch (error) {
console.error("Fehler beim Abruf:", error);
}
})();

View File

@@ -0,0 +1,62 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
// Example test configuration flags
const maxItems = 50;
const includeWatchedContent = false; // set to false to ONLY show unplayed (newly watched)
const maxParentalRating = 12; // Test age limit
const maxDaysRecent = 30; // Test recency limit
// Build the query parameters just like in mediaBarEnhanced.js
const sortParams = "sortBy=Random";
const playedFilter = includeWatchedContent ? '' : '&isPlayed=False';
let parentalFilter = '';
if (maxParentalRating) {
parentalFilter = `&MaxOfficialRating=${maxParentalRating}`;
}
let dateFilter = '';
if (maxDaysRecent) {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
dateFilter = `&MinDateCreated=${pastDate.toISOString()}`;
}
const apiUrl = `${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}${playedFilter}${parentalFilter}${dateFilter}&enableUserData=true&Limit=${maxItems}&fields=Id&userId=${userId}`;
try {
console.log(`Testing generated URL with filters:\n%c${apiUrl}`, "color: yellow;");
// Execute the fetch
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch items: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const items = data.Items || [];
console.log(`%cErgebnis: ${items.length} Items gefunden!`, "color: #00a4dc; font-weight: bold;");
if(items.length > 0) {
console.log("Erstes Item als Beispiel:");
console.dir(items[0]);
}
} catch (error) {
console.error("Fehler beim Abrufen der URL:", error);
}
})();

View File

@@ -1,462 +0,0 @@
<!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>

File diff suppressed because it is too large Load Diff