Compare commits

...

82 Commits

Author SHA1 Message Date
CodeDevMLH
0a8ba042db Update manifest.json for release v1.7.2.1 [skip ci] 2026-03-09 14:26:43 +00:00
CodeDevMLH
f9b8722259 Bump version to 1.7.2.1 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-09 15:25:58 +01:00
CodeDevMLH
66f6f7b434 Enhance overlay image handling: support dynamic filenames, add delete and rename functionality, and improve seasonal image management 2026-03-09 15:25:49 +01:00
CodeDevMLH
22c873d686 Update manifest.json for release v1.7.2.0 [skip ci] 2026-03-09 03:12:09 +00:00
CodeDevMLH
c3a73cc28b Bump version to 1.7.2.0 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 43s
2026-03-09 04:11:23 +01:00
CodeDevMLH
fc3a5f1e66 Add button for custom overlay settings in configuration page 2026-03-09 04:10:56 +01:00
CodeDevMLH
cd490cf0f3 Update manifest.json for release v1.7.1.15 [skip ci] 2026-03-09 03:03:01 +00:00
CodeDevMLH
bb6310381a Refactor overlay image handling and improve safety checks for plugin configuration
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 43s
2026-03-09 04:02:17 +01:00
CodeDevMLH
518fd5640e Bump version to 1.7.1.15 and update changelog for new features and fixes
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 51s
2026-03-09 03:40:38 +01:00
CodeDevMLH
a57f3db009 Add custom overlay image upload feature with style options 2026-03-09 03:40:13 +01:00
CodeDevMLH
8ff4f081f3 Update manifest.json for release v1.7.1.14 [skip ci] 2026-03-09 01:29:55 +00:00
CodeDevMLH
4a07c22091 Bump version to 1.7.1.14 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 43s
2026-03-09 02:29:11 +01:00
CodeDevMLH
4d1d442746 Add custom overlay feature with configuration options for text and image 2026-03-09 02:28:46 +01:00
CodeDevMLH
1df2b341e5 Refactor mask-image properties for improved readability in mediaBarEnhanced.css 2026-03-09 01:57:21 +01:00
CodeDevMLH
b2dbd6df45 Update manifest.json for release v1.7.1.13 [skip ci] 2026-03-08 22:50:48 +00:00
CodeDevMLH
60c72a01b1 Bump version to 1.7.1.13 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-08 23:50:03 +01:00
CodeDevMLH
9f7ef3c96b Refactor button background colors for improved visibility and update YouTube iframe attributes for better playback control 2026-03-08 23:49:49 +01:00
CodeDevMLH
7ffcfa68c1 Update manifest.json for release v1.7.1.12 [skip ci] 2026-03-08 22:15:28 +00:00
CodeDevMLH
aaf21d3c33 Bump version to 1.7.1.12 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-08 23:14:42 +01:00
CodeDevMLH
9758ecd417 Add background color to detail and favorite buttons for improved visibility 2026-03-08 23:14:09 +01:00
CodeDevMLH
a4547d80b1 Update manifest.json for release v1.7.1.11 [skip ci] 2026-03-08 22:06:37 +00:00
CodeDevMLH
671e38ff32 Bump version to 1.7.1.11 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-08 23:05:51 +01:00
CodeDevMLH
0e9d0f9d09 Remove appearance properties from button styles and add background color and text color to button for improved visibility 2026-03-08 23:05:38 +01:00
CodeDevMLH
5f296f3c88 Update manifest.json for release v1.7.1.10 [skip ci] 2026-03-08 21:36:22 +00:00
CodeDevMLH
a14b3ca8b5 Bump version to 1.7.1.10 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-08 22:35:35 +01:00
CodeDevMLH
4d12e34d01 Enhance CSS styles for buttons and overlays, improving appearance and consistency 2026-03-08 22:35:31 +01:00
CodeDevMLH
59fe6f7083 Update manifest.json for release v1.7.1.9 [skip ci] 2026-03-08 20:58:25 +00:00
CodeDevMLH
dcb2164ea1 Bump version to 1.7.1.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-08 21:57:41 +01:00
CodeDevMLH
2f71f7b46b Improve null checks and conditionals for better stability in localization and slideshow management
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-03-08 21:57:22 +01:00
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
CodeDevMLH
8676160e7b Update manifest.json for release v1.7.0.10 [skip ci] 2026-03-06 02:02:52 +00:00
CodeDevMLH
c562560bc0 Bump version to 1.7.0.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 03:02:01 +01:00
CodeDevMLH
98474d4ff6 Refactor YouTube player integration to use iframe for improved performance and security 2026-03-06 03:01:34 +01:00
CodeDevMLH
14c0eb43ed Update manifest.json for release v1.7.0.9 [skip ci] 2026-03-06 01:24:40 +00:00
CodeDevMLH
c4cbeda2b8 Bump version to 1.7.0.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 02:23:50 +01:00
CodeDevMLH
53ad568be4 Fix active slide detection logic in SlideCreator for improved video playback handling 2026-03-06 02:23:13 +01:00
CodeDevMLH
fba64bd0f6 Update manifest.json for release v1.7.0.8 [skip ci] 2026-03-06 01:17:19 +00:00
15 changed files with 1592 additions and 4018 deletions

View File

@@ -0,0 +1,167 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MediaBrowser.Common.Configuration;
namespace Jellyfin.Plugin.MediaBarEnhanced.Api
{
/// <summary>
/// Controller for handling custom overlay image uploads and retrieval.
/// </summary>
[ApiController]
[Route("MediaBarEnhanced")]
public class OverlayImageController : ControllerBase
{
private readonly IApplicationPaths _applicationPaths;
private readonly string _imageDirectory;
public OverlayImageController(IApplicationPaths applicationPaths)
{
_applicationPaths = applicationPaths;
// We use the plugin's data folder to store the image
_imageDirectory = MediaBarEnhancedPlugin.Instance?.DataFolderPath ?? Path.Combine(applicationPaths.DataPath, "plugins", "MediaBarEnhanced");
// We no longer define the exact path here, just the directory
// The filename is determined per request
}
/// <summary>
/// Uploads a new custom overlay image.
/// </summary>
[HttpPost("OverlayImage")]
[Consumes("multipart/form-data")]
public async Task<IActionResult> UploadImage([FromForm] IFormFile file, [FromQuery] string? filename = null)
{
if (file == null || file.Length == 0)
{
return BadRequest("No file uploaded.");
}
string targetFileName = string.IsNullOrWhiteSpace(filename)
? "custom_overlay_image.dat"
: $"custom_overlay_image_{filename}.dat";
string targetPath = Path.Combine(_imageDirectory, targetFileName);
try
{
if (!Directory.Exists(_imageDirectory))
{
Directory.CreateDirectory(_imageDirectory);
}
// Delete is not strictly necessary and can cause locking issues if someone is currently reading it.
// FileMode.Create will truncate the file if it exists, effectively overwriting it.
// We use FileShare.None to ensure we have exclusive write access, but handle potential IOExceptions gracefully.
using (var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await file.CopyToAsync(stream).ConfigureAwait(false);
}
// Return the GET URL that the frontend can use
var qs = string.IsNullOrWhiteSpace(filename) ? "" : $"?filename={Uri.EscapeDataString(filename)}&";
var getUrl = $"/MediaBarEnhanced/OverlayImage{qs}{(qs == "" ? "?" : "")}t={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
return Ok(new { url = getUrl });
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
/// <summary>
/// Retrieves the custom overlay image.
/// </summary>
[HttpGet("OverlayImage")]
public IActionResult GetImage([FromQuery] string? filename = null)
{
string targetFileName = string.IsNullOrWhiteSpace(filename)
? "custom_overlay_image.dat"
: $"custom_overlay_image_{filename}.dat";
string targetPath = Path.Combine(_imageDirectory, targetFileName);
if (!System.IO.File.Exists(targetPath))
{
return NotFound();
}
// Read the file and return as a generic octet stream.
// We use FileShare.ReadWrite so that if someone is currently overwriting the file (uploading), we don't block them,
// and we also don't get blocked by other readers.
var stream = new FileStream(targetPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// "image/*" works reliably as browsers will sniff the exact image mime type (jpeg, png, webp).
return File(stream, "image/*");
}
/// <summary>
/// Deletes a custom overlay image.
/// </summary>
[HttpDelete("OverlayImage")]
public IActionResult DeleteImage([FromQuery] string? filename = null)
{
string targetFileName = string.IsNullOrWhiteSpace(filename)
? "custom_overlay_image.dat"
: $"custom_overlay_image_{filename}.dat";
string targetPath = Path.Combine(_imageDirectory, targetFileName);
if (System.IO.File.Exists(targetPath))
{
try
{
System.IO.File.Delete(targetPath);
return Ok();
}
catch (Exception ex)
{
return StatusCode(500, $"Error deleting file: {ex.Message}");
}
}
return NotFound();
}
/// <summary>
/// Renames a custom overlay image (used when a seasonal section is renamed).
/// </summary>
[HttpPut("OverlayImage/Rename")]
public IActionResult RenameImage([FromQuery] string oldName, [FromQuery] string newName)
{
if (string.IsNullOrWhiteSpace(oldName) || string.IsNullOrWhiteSpace(newName))
{
return BadRequest("Both oldName and newName must be provided.");
}
string oldPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{oldName}.dat");
string newPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{newName}.dat");
if (!System.IO.File.Exists(oldPath))
{
// If it doesn't exist, there is nothing to rename, but we still consider it a success
// since the end state (file with oldName is gone, file with newName doesn't exist yet) is acceptable.
return Ok();
}
try
{
// If a file with the new name already exists, delete it first to avoid conflicts
if (System.IO.File.Exists(newPath))
{
System.IO.File.Delete(newPath);
}
System.IO.File.Move(oldPath, newPath);
var qs = $"?filename={Uri.EscapeDataString(newName)}&";
var getUrl = $"/MediaBarEnhanced/OverlayImage{qs}t={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
return Ok(new { url = getUrl });
}
catch (Exception ex)
{
return StatusCode(500, $"Error renaming file: {ex.Message}");
}
}
}
}

View File

@@ -15,9 +15,12 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public int MaxMovies { get; set; } = 15;
public int MaxTvShows { get; set; } = 15;
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 FadeTransitionDuration { get; set; } = 500;
public int MaxPaginationDots { get; set; } = 15;
public bool ShowPaginationDots { get; set; } = true;
public bool SlideAnimationEnabled { get; set; } = true;
public bool EnableVideoBackdrop { 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 EnableKeyboardControls { get; set; } = true;
public bool AlwaysShowArrows { get; set; } = false;
public bool HideArrowsOnMobile { get; set; } = true;
public string CustomMediaIds { get; set; } = "";
public bool EnableCustomMediaIds { get; set; } = true;
public string PreferredVideoQuality { get; set; } = "Auto";
public bool EnableSeasonalContent { get; set; } = false;
public bool ExcludeSeasonalContent { get; set; } = true;
public string SeasonalSections { get; set; } = "[]";
public bool IsEnabled { get; set; } = true;
public bool EnableClientSideSettings { get; set; } = false;
@@ -44,5 +49,10 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool IncludeWatchedContent { get; set; } = false;
public string SortBy { get; set; } = "Random";
public string SortOrder { get; set; } = "Ascending";
public bool EnableCustomOverlay { get; set; } = false;
public string CustomOverlayText { get; set; } = "";
public string CustomOverlayImageUrl { get; set; } = "";
public string CustomOverlayStyle { get; set; } = "Shadowed";
}
}

View File

@@ -34,6 +34,10 @@
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>
<button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-overlay', 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 Overlay</h3>
</button>
</div>
<form id="mediaBarEnhancedConfigForm">
@@ -43,7 +47,8 @@
<h2 class="sectionTitle">Main Plugin Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<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>
</label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
@@ -57,23 +62,27 @@
<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" id="PreferLocalTrailersContainer">
<div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalTrailersContainer">
<label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers"
name="PreferLocalTrailers" />
<span>Prefer Local Trailers</span>
</label>
<div class="fieldDescription">If enabled, local trailers will be preferred over remote (YouTube) trailers.</div>
<div class="fieldDescription">If enabled, local trailers will be preferred over remote
(YouTube) trailers.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalBackdropsContainer">
<div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalBackdropsContainer">
<label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops"
name="PreferLocalBackdrops" />
<span>Prefer Local Backdrops / Theme Videos</span>
</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 class="checkboxContainer checkboxContainer-withDescription">
<div class="checkboxContainer checkboxContainer-withDescription" id="WaitForTrailerToEndContainer">
<label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" />
@@ -81,7 +90,7 @@
</label>
<div class="fieldDescription">Delay slide transition until trailer finishes.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<div class="checkboxContainer checkboxContainer-withDescription" id="EnableMobileVideoContainer">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
name="EnableMobileVideo" />
@@ -95,8 +104,8 @@
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 class="fieldDescription">Display a button to open trailer in modal. Button only
visible if trailer is not set as backdrop.</div>
</div>
</div>
@@ -111,7 +120,8 @@
<span>Enable Custom Media IDs</span>
</label>
<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 class="checkboxContainer checkboxContainer-withDescription">
<label>
@@ -119,35 +129,47 @@
name="ApplyLimitsToCustomIds" />
<span>Apply Limits to Custom IDs</span>
</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 id="customMediaIdsContainer">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist IDs (Newline or Comma-separated)</label>
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds"
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default
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>
<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>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>.
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a
Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or
<code>ID [JellyfinItemId] DESCRIPTION</code>.
<br>
Methods:
<ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li>
<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>
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>
<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>
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).
</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:
<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).
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>
@@ -160,14 +182,26 @@
name="EnableSeasonalContent" />
<span>Enable Seasonal Content</span>
</label>
<div class="fieldDescription">When enabled, seasonal sections below will override the 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 class="fieldDescription">When enabled, seasonal sections below will override the
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 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>
<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 id="seasonalSectionsList"></div>
@@ -180,6 +214,66 @@
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div>
<!-- CUSTOM OVERLAY TAB -->
<div id="media-bar-enhanced-overlay" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Custom Slideshow Overlay</h2>
<p>Inject a custom text or floating image over the slideshow. This can be overridden by specific Seasonal Sections in the Custom Filters tab.</p>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="EnableCustomOverlay" name="EnableCustomOverlay" />
<span>Enable Custom Overlay</span>
</label>
<div class="fieldDescription">If enabled, the text or image below will hover over the slideshow globally.</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="CustomOverlayStyle">Overlay Style</label>
<select is="emby-select" id="CustomOverlayStyle" name="CustomOverlayStyle"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Shadowed">Classic Shadowed</option>
<option value="Frosted">Frosted Glass Pill</option>
<option value="Cinematic">Cinematic Golden Glow</option>
<option value="Pulse">Animated Pulse</option>
<option value="Neon">Neon Cyberpunk</option>
<option value="Typewriter">Typewriter Pop</option>
<option value="Bubble">Floating Bubble</option>
<option value="SlideIn">Cinematic Slide-In</option>
</select>
<div class="fieldDescription">Choose the visual styling animation for your custom text.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomOverlayText">Global Overlay Text</label>
<input is="emby-input" type="text" id="CustomOverlayText" name="CustomOverlayText" />
<div class="fieldDescription">Text to display on the overlay (e.g. "Movie Night!"). Leave blank to use an image instead.</div>
</div>
<h3 class="inputLabel" style="margin-top: 2em;">Global Overlay Image</h3>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomOverlayImageUrl">Global Overlay Image URL</label>
<input is="emby-input" type="text" id="CustomOverlayImageUrl" name="CustomOverlayImageUrl" />
<div class="fieldDescription">Absolute URL to an image to display on the overlay. If provided, this overrides the text above.</div>
</div>
<!-- Image Upload Dropzone -->
<div class="inputContainer" style="margin-top: 1em;">
<label class="inputLabel">Or Upload Local Image</label>
<div id="overlayImageDropzone" style="border: 2px dashed rgba(255,255,255,0.2); border-radius: 8px; padding: 2em; text-align: center; cursor: pointer; background: rgba(0,0,0,0.2); transition: all 0.2s ease; position: relative; min-height: 120px; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<i class="material-icons" style="font-size: 40px; color: rgba(255,255,255,0.4); margin-bottom: 10px;">cloud_upload</i>
<span style="font-size: 1.1em; color: rgba(255,255,255,0.7);">Drag and drop an image here, or click to select</span>
<input type="file" id="overlayImageInput" accept="image/png, image/jpeg, image/gif, image/webp" style="display: none;">
<img id="overlayImagePreview" style="display: none; max-width: 100%; max-height: 150px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border-radius: 4px; z-index: 2;" />
<!-- A semi-transparent overlay to clear the image -->
<button type="button" id="clearOverlayImageBtn" is="paper-icon-button-light" style="display: none; position: absolute; top: 10px; right: 10px; z-index: 3; background: rgba(0,0,0,0.6); border-radius: 50%; padding: 5px;" title="Clear Image">
<i class="material-icons" style="color: #a94442;">delete</i>
</button>
</div>
<div class="fieldDescription">Uploading an image will securely save it to the server and automatically update the URL field above.</div>
</div>
</div>
<!-- ADVANCED TAB -->
<div id="media-bar-enhanced-advanced" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Features</h2>
@@ -189,8 +283,9 @@
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 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>
@@ -199,7 +294,8 @@
<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>
override settings (like disabling the bar or trailer backdrops) locally on their device.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
@@ -207,7 +303,8 @@
name="RandomizeThemeVideos" />
<span>Randomize Backdrop Video</span>
</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 class="checkboxContainer checkboxContainer-withDescription">
<label>
@@ -215,18 +312,22 @@
name="RandomizeLocalTrailers" />
<span>Randomize Local Trailer</span>
</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 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 (if data is available).</div>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is
available).</div>
</div>
<div class="selectContainer">
<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="Maximum">Maximum (4K+)</option>
<option value="1080p">1080p</option>
@@ -241,7 +342,9 @@
<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.)<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 class="checkboxContainer checkboxContainer-withDescription">
<label>
@@ -260,16 +363,23 @@
<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>
may have to reload the page twice (after changing this setting) to take effect)</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" />
<span>Always Show Arrows</span>
<span>Always Show Arrow Navigation Buttons</span>
</label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead
of only on hover.</div>
<div class="fieldDescription">Force the UI arrow navigation buttons to always be visible instead of only when hovered.</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 class="checkboxContainer checkboxContainer-withDescription">
<label>
@@ -281,6 +391,7 @@
Space (pause), M (mute/unmute)) for
the slideshow.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Time Settings</h2>
<p>Leave a setting blank to use the default value.</p>
@@ -324,11 +435,14 @@
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</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">
<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="Original">Original (Custom List Order)</option>
<option value="PremiereDate">Premiere Date</option>
@@ -342,15 +456,38 @@
</div>
<div class="selectContainer">
<label class="selectLabel" for="SortOrder">Sort Order</label>
<select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select" 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="Descending">Descending</option>
</select>
<div class="fieldDescription">Sort items in Ascending or Descending order.</div>
</div>
<div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;">
<b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original' preserves Custom List order.
<b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original'
preserves Custom List order.
</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>
<p>Leave a setting blank to use the default value.</p>
@@ -377,6 +514,15 @@
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of slides to preload.</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">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
Dots</label>
@@ -391,14 +537,6 @@
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</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
@@ -459,7 +597,10 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent'
'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
'CustomOverlayStyle'
];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -501,30 +642,36 @@
seasonalCheckbox.addEventListener('change', updateSeasonalVisibility);
updateSeasonalVisibility();
}
// Add Season Button
var addSeasonBtn = page.querySelector('#addSeasonBtn');
if (addSeasonBtn) {
// Remove existing listeners to avoid duplicates if re-attached
var newBtn = addSeasonBtn.cloneNode(true);
addSeasonBtn.parentNode.replaceChild(newBtn, addSeasonBtn);
newBtn.addEventListener('click', function() {
MediaBarEnhancedConfigurationPage.addSeasonalSection(page);
});
var newBtn = addSeasonBtn.cloneNode(true);
addSeasonBtn.parentNode.replaceChild(newBtn, addSeasonBtn);
newBtn.addEventListener('click', function () {
MediaBarEnhancedConfigurationPage.addSeasonalSection(page);
});
}
// Handle Prefer Local Trailers visibility
var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop');
var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer');
var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer');
var waitForTrailerContainer = page.querySelector('#WaitForTrailerToEndContainer');
var enableMobileVideoContainer = page.querySelector('#EnableMobileVideoContainer');
function updatePreferLocalVisibility() {
if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) {
if (preferLocalContainer) preferLocalContainer.style.display = 'block';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'block';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'block';
} else {
if (preferLocalContainer) preferLocalContainer.style.display = 'none';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'none';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'none';
}
}
@@ -533,6 +680,117 @@
updatePreferLocalVisibility();
}
// Overlay Image Upload Logic
var dropzone = page.querySelector('#overlayImageDropzone');
var fileInput = page.querySelector('#overlayImageInput');
var urlInput = page.querySelector('#CustomOverlayImageUrl');
var previewImg = page.querySelector('#overlayImagePreview');
var clearBtn = page.querySelector('#clearOverlayImageBtn');
function updatePreview() {
if (urlInput.value && urlInput.value.trim() !== '') {
previewImg.src = urlInput.value;
previewImg.style.display = 'block';
clearBtn.style.display = 'block';
} else {
previewImg.style.display = 'none';
clearBtn.style.display = 'none';
}
}
// Initial check
updatePreview();
// Listen to manual URL input changes
urlInput.addEventListener('input', updatePreview);
clearBtn.addEventListener('click', function(e) {
e.stopPropagation(); // prevent triggering file dialog
// Call DELETE API to remove global image
fetch(ApiClient.serverAddress() + '/MediaBarEnhanced/OverlayImage', {
method: 'DELETE',
headers: {
'Authorization': 'MediaBrowser Client="' + ApiClient.appName() + '", Device="' + ApiClient.deviceName() + '", DeviceId="' + ApiClient.deviceId() + '", Version="' + ApiClient.appVersion() + '", Token="' + ApiClient.accessToken() + '"'
}
}).catch(console.error);
urlInput.value = '';
fileInput.value = '';
updatePreview();
});
dropzone.addEventListener('click', function() {
fileInput.click();
});
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
dropzone.style.borderColor = '#00a4dc';
dropzone.style.background = 'rgba(0, 164, 220, 0.2)';
});
dropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
dropzone.style.borderColor = 'rgba(255,255,255,0.2)';
dropzone.style.background = 'rgba(0,0,0,0.2)';
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
dropzone.style.borderColor = 'rgba(255,255,255,0.2)';
dropzone.style.background = 'rgba(0,0,0,0.2)';
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
uploadImage(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', function() {
if (this.files && this.files.length > 0) {
uploadImage(this.files[0]);
}
});
function uploadImage(file) {
// Validate it's an image
if (!file.type.match('image.*')) {
Dashboard.alert('Please select a valid image file.');
return;
}
Dashboard.showLoadingMsg();
var formData = new FormData();
formData.append('file', file);
// The Endpoint we created in C#
fetch(ApiClient.serverAddress() + '/MediaBarEnhanced/OverlayImage', {
method: 'POST',
body: formData,
headers: {
'Authorization': 'MediaBrowser Client="' + ApiClient.appName() + '", Device="' + ApiClient.deviceName() + '", DeviceId="' + ApiClient.deviceId() + '", Version="' + ApiClient.appVersion() + '", Token="' + ApiClient.accessToken() + '"'
}
})
.then(response => {
if (response.ok) return response.json();
throw new Error('Network response was not ok.');
})
.then(data => {
// Update URL input
urlInput.value = ApiClient.serverAddress() + data.url;
updatePreview();
Dashboard.hideLoadingMsg();
})
.catch(error => {
console.error('Upload error:', error);
Dashboard.alert('Image upload failed. Please verify API controller is active.');
Dashboard.hideLoadingMsg();
});
}
Dashboard.hideLoadingMsg();
});
},
@@ -545,7 +803,7 @@
if (seasonalInput) seasonalInput.value = sectionsJson;
var config = {};
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
@@ -563,7 +821,10 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent'
'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
'CustomOverlayStyle'
];
keys.forEach(function (key) {
@@ -582,16 +843,16 @@
});
},
renderSeasonalSections: function(page, sections) {
renderSeasonalSections: function (page, sections) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
container.innerHTML = '';
sections.forEach(function(section, index) {
sections.forEach(function (section, index) {
MediaBarEnhancedConfigurationPage.createSectionElement(container, section, index + 1);
});
},
addSeasonalSection: function(page) {
addSeasonalSection: function (page) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
var index = container.children.length + 1;
@@ -599,29 +860,31 @@
Name: 'New Season',
StartDay: 1, StartMonth: 1,
EndDay: 1, EndMonth: 1,
MediaIds: ''
MediaIds: '',
OverlayText: '',
OverlayImageUrl: ''
}, index);
},
createSectionElement: function(container, data, index) {
createSectionElement: function (container, data, index) {
var div = document.createElement('div');
div.className = 'seasonal-section';
div.style.cssText = 'background: rgba(0,0,0,0.2); padding: 1em; margin-bottom: 1em; border-radius: 4px; border: 1px solid rgba(255,255,255,0.1);';
var days = [];
for(var i=1; i<=31; i++) days.push(i);
for (var i = 1; i <= 31; i++) days.push(i);
var months = [
{v:1, n:'Jan'}, {v:2, n:'Feb'}, {v:3, n:'Mar'}, {v:4, n:'Apr'},
{v:5, n:'May'}, {v:6, n:'Jun'}, {v:7, n:'Jul'}, {v:8, n:'Aug'},
{v:9, n:'Sep'}, {v:10, n:'Oct'}, {v:11, n:'Nov'}, {v:12, n:'Dec'}
{ v: 1, n: 'Jan' }, { v: 2, n: 'Feb' }, { v: 3, n: 'Mar' }, { v: 4, n: 'Apr' },
{ v: 5, n: 'May' }, { v: 6, n: 'Jun' }, { v: 7, n: 'Jul' }, { v: 8, n: 'Aug' },
{ v: 9, n: 'Sep' }, { v: 10, n: 'Oct' }, { v: 11, n: 'Nov' }, { v: 12, n: 'Dec' }
];
function mkSelect(val, opts, cls) {
var h = '<select class="emby-select emby-select-withcolor ' + cls + '" style="width: auto; display: inline-block; margin-right: 5px; -webkit-appearance: menulist; appearance: menulist;">';
opts.forEach(function(o) {
opts.forEach(function (o) {
var v = o.v || o;
var n = o.n || o;
h += '<option value="'+v+'" ' + (v == val ? 'selected' : '') + '>' + n + '</option>';
h += '<option value="' + v + '" ' + (v == val ? 'selected' : '') + '>' + n + '</option>';
});
h += '</select>';
return h;
@@ -648,11 +911,11 @@
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' +
' <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 0.5em;">' +
' <span>From:</span>' +
mkSelect(data.StartDay, days, 'start-day') +
mkSelect(data.StartMonth, months, 'start-month') +
mkSelect(data.StartDay, days, 'start-day') +
mkSelect(data.StartMonth, months, 'start-month') +
' <span style="margin-left: 1em;">To:</span>' +
mkSelect(data.EndDay, days, 'end-day') +
mkSelect(data.EndMonth, months, 'end-month') +
mkSelect(data.EndDay, days, 'end-day') +
mkSelect(data.EndMonth, months, 'end-month') +
' </div>' +
' <div class="fieldDescription">Date range (inclusive) when this content is active.</div>' +
'</div>' +
@@ -660,33 +923,209 @@
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Media IDs</label>' +
' <textarea is="emby-textarea" class="emby-textarea section-ids" style="width: 100%; height: 80px; font-family: monospace;">' + (data.MediaIds || '') + '</textarea>' +
' <div class="fieldDescription">Comma-separated or Newline separated list of Movie/Series/Collection IDs to show during this season.<br>Same options available as for the default media IDs.</div>' +
'</div>' +
'<div class="inputContainer" style="margin-top: 1em;">' +
' <input is="emby-input" type="text" class="emby-input section-overlay-text" style="width: 100%;" value="' + (data.OverlayText ? data.OverlayText.replace(/"/g, '&quot;') : '') + '" placeholder="Seasonal Custom Overlay Text (e.g. Oscars Time!)" />' +
' <div class="fieldDescription">Optional: Override the global custom overlay text during this season.</div>' +
'</div>' +
'<div class="inputContainer">' +
' <input is="emby-input" type="text" class="emby-input section-overlay-image" style="width: 100%;" value="' + (data.OverlayImageUrl ? data.OverlayImageUrl.replace(/"/g, '&quot;') : '') + '" placeholder="Seasonal Custom Overlay Image URL" />' +
' <div class="fieldDescription">Optional: Override the global custom overlay image during this season. Overrides the text if provided.</div>' +
'</div>' +
'<div class="inputContainer" style="margin-top: 1em; margin-bottom: 0;">' +
' <div class="seasonal-dropzone" style="border: 2px dashed rgba(255,255,255,0.2); border-radius: 8px; padding: 1.5em; text-align: center; cursor: pointer; background: rgba(0,0,0,0.2); transition: all 0.2s ease; position: relative; min-height: 100px; display: flex; flex-direction: column; align-items: center; justify-content: center;">' +
' <i class="material-icons" style="font-size: 32px; color: rgba(255,255,255,0.4); margin-bottom: 8px;">cloud_upload</i>' +
' <span style="font-size: 0.9em; color: rgba(255,255,255,0.7);">Drag and drop a seasonal image here, or click</span>' +
' <input type="file" class="seasonal-file-input" accept="image/png, image/jpeg, image/gif, image/webp" style="display: none;">' +
' <img class="seasonal-preview-img" style="display: none; max-width: 100%; max-height: 120px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border-radius: 4px; z-index: 2;" />' +
' <button type="button" class="seasonal-clear-btn" is="paper-icon-button-light" style="display: none; position: absolute; top: 10px; right: 10px; z-index: 3; background: rgba(0,0,0,0.6); border-radius: 50%; padding: 5px;" title="Clear Image">' +
' <i class="material-icons" style="color: #a94442;">delete</i>' +
' </button>' +
' </div>' +
'</div>';
div.querySelector('.btn-remove').addEventListener('click', function() {
div.querySelector('.btn-remove').addEventListener('click', function () {
div.remove();
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
});
div.querySelector('.btn-move-up').addEventListener('click', function() {
div.querySelector('.btn-move-up').addEventListener('click', function () {
if (div.previousElementSibling) {
container.insertBefore(div, div.previousElementSibling);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}
});
div.querySelector('.btn-move-down').addEventListener('click', function() {
div.querySelector('.btn-move-down').addEventListener('click', function () {
if (div.nextElementSibling) {
container.insertBefore(div.nextElementSibling, div);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}
});
// --- Seasonal Drag and Drop Logic ---
var sectionNameInput = div.querySelector('.section-name');
var urlInput = div.querySelector('.section-overlay-image');
var dropzone = div.querySelector('.seasonal-dropzone');
var fileInput = div.querySelector('.seasonal-file-input');
var previewImg = div.querySelector('.seasonal-preview-img');
var clearBtn = div.querySelector('.seasonal-clear-btn');
var currentSectionName = sectionNameInput.value.trim();
// Track Name Changes to rename server file
sectionNameInput.addEventListener('focus', function() {
currentSectionName = this.value.trim();
});
sectionNameInput.addEventListener('blur', function() {
var newName = this.value.trim();
if (newName && currentSectionName && newName !== currentSectionName) {
// If they have an image attached, rename it
if (urlInput.value && urlInput.value.indexOf('OverlayImage') !== -1) {
fetch(ApiClient.serverAddress() + '/MediaBarEnhanced/OverlayImage/Rename?oldName=' + encodeURIComponent(currentSectionName) + '&newName=' + encodeURIComponent(newName), {
method: 'PUT',
headers: {
'Authorization': 'MediaBrowser Client="' + ApiClient.appName() + '", Device="' + ApiClient.deviceName() + '", DeviceId="' + ApiClient.deviceId() + '", Version="' + ApiClient.appVersion() + '", Token="' + ApiClient.accessToken() + '"'
}
})
.then(response => {
if(response.ok) return response.json();
throw new Error('Rename failed');
})
.then(data => {
urlInput.value = ApiClient.serverAddress() + data.url;
currentSectionName = newName;
}).catch(console.error);
} else {
currentSectionName = newName;
}
} else {
currentSectionName = newName;
}
});
function updatePreview() {
var val = urlInput.value.trim();
if (val) {
previewImg.src = val;
previewImg.style.display = 'block';
clearBtn.style.display = 'block';
} else {
previewImg.src = '';
previewImg.style.display = 'none';
clearBtn.style.display = 'none';
}
}
// Initial state
updatePreview();
urlInput.addEventListener('input', updatePreview);
clearBtn.addEventListener('click', function(e) {
e.stopPropagation();
var name = sectionNameInput.value.trim();
if (name) {
fetch(ApiClient.serverAddress() + '/MediaBarEnhanced/OverlayImage?filename=' + encodeURIComponent(name), {
method: 'DELETE',
headers: {
'Authorization': 'MediaBrowser Client="' + ApiClient.appName() + '", Device="' + ApiClient.deviceName() + '", DeviceId="' + ApiClient.deviceId() + '", Version="' + ApiClient.appVersion() + '", Token="' + ApiClient.accessToken() + '"'
}
}).catch(console.error);
}
urlInput.value = '';
fileInput.value = '';
updatePreview();
});
div.querySelector('.btn-remove').addEventListener('click', function () {
// Cleanup image if deleted
var name = sectionNameInput.value.trim();
if (name) {
fetch(ApiClient.serverAddress() + '/MediaBarEnhanced/OverlayImage?filename=' + encodeURIComponent(name), {
method: 'DELETE',
headers: {
'Authorization': 'MediaBrowser Client="' + ApiClient.appName() + '", Device="' + ApiClient.deviceName() + '", DeviceId="' + ApiClient.deviceId() + '", Version="' + ApiClient.appVersion() + '", Token="' + ApiClient.accessToken() + '"'
}
}).catch(console.error);
}
div.remove();
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
});
dropzone.addEventListener('click', function() { fileInput.click(); });
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
dropzone.style.borderColor = '#00a4dc';
dropzone.style.background = 'rgba(0, 164, 220, 0.2)';
});
dropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
dropzone.style.borderColor = 'rgba(255,255,255,0.2)';
dropzone.style.background = 'rgba(0,0,0,0.2)';
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
dropzone.style.borderColor = 'rgba(255,255,255,0.2)';
dropzone.style.background = 'rgba(0,0,0,0.2)';
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
uploadSeasonalImage(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', function() {
if (this.files && this.files.length > 0) uploadSeasonalImage(this.files[0]);
});
function uploadSeasonalImage(file) {
if (!file.type.match('image.*')) {
Dashboard.alert('Please select a valid image file.');
return;
}
var name = sectionNameInput.value.trim();
if (!name) {
Dashboard.alert('Please enter a Name for this season before uploading an image (used for file saving).');
return;
}
Dashboard.showLoadingMsg();
var formData = new FormData();
formData.append('file', file);
var qs = '?filename=' + encodeURIComponent(name);
fetch(ApiClient.serverAddress() + '/MediaBarEnhanced/OverlayImage' + qs, {
method: 'POST',
body: formData,
headers: {
'Authorization': 'MediaBrowser Client="' + ApiClient.appName() + '", Device="' + ApiClient.deviceName() + '", DeviceId="' + ApiClient.deviceId() + '", Version="' + ApiClient.appVersion() + '", Token="' + ApiClient.accessToken() + '"'
}
})
.then(response => {
if (response.ok) return response.json();
throw new Error('Upload failed');
})
.then(data => {
urlInput.value = ApiClient.serverAddress() + data.url;
updatePreview();
Dashboard.hideLoadingMsg();
})
.catch(error => {
console.error('Upload error:', error);
Dashboard.alert('Image upload failed.');
Dashboard.hideLoadingMsg();
});
}
container.appendChild(div);
},
updateSectionTitles: function(container) {
updateSectionTitles: function (container) {
var sections = container.querySelectorAll('.seasonal-section');
sections.forEach(function(section, index) {
sections.forEach(function (section, index) {
var title = section.querySelector('.section-title');
if (title) {
title.innerText = 'Season list #' + (index + 1);
@@ -694,17 +1133,19 @@
});
},
getSeasonalSectionsFromUI: function(page) {
getSeasonalSectionsFromUI: function (page) {
var sections = [];
var els = page.querySelectorAll('.seasonal-section');
els.forEach(function(el) {
els.forEach(function (el) {
sections.push({
Name: el.querySelector('.section-name').value,
StartDay: parseInt(el.querySelector('.start-day').value),
StartMonth: parseInt(el.querySelector('.start-month').value),
EndDay: parseInt(el.querySelector('.end-day').value),
EndMonth: parseInt(el.querySelector('.end-month').value),
MediaIds: el.querySelector('.section-ids').value
MediaIds: el.querySelector('.section-ids').value,
OverlayText: el.querySelector('.section-overlay-text').value,
OverlayImageUrl: el.querySelector('.section-overlay-image').value
});
});
return sections;

View File

@@ -18,7 +18,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Helpers
try
{
// Safety Check: If plugin is disabled, do nothing
if (!MediaBarEnhancedPlugin.Instance.Configuration.IsEnabled)
if (MediaBarEnhancedPlugin.Instance?.Configuration?.IsEnabled != true)
{
return originalContents;
}

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Media Bar Enhanced Plugin</Title>
<Authors>CodeDevMLH</Authors>
<Version>1.7.0.8</Version>
<Version>1.7.2.1</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
</PropertyGroup>

View File

@@ -17,7 +17,6 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
{
private readonly ScriptInjector _scriptInjector;
private readonly ILoggerFactory _loggerFactory;
public IServiceProvider ServiceProvider { get; }
/// <summary>
/// Initializes a new instance of the <see cref="MediaBarEnhancedPlugin"/> class.

View File

@@ -354,13 +354,13 @@
width: 100%;
height: 100%;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
}
.backdrop-container.full-width-video {
@@ -384,13 +384,13 @@
border-radius: 5px;
z-index: 3;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
}
.backdrop-overlay {
@@ -403,13 +403,13 @@
border-radius: 5px;
z-index: 4;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 4%,
#000000 6%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 4%,
#000000 6%);
}
.gradient-overlay {
@@ -424,13 +424,13 @@
rgba(29, 29, 29, 0) 100%);
z-index: 4;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 4%,
#000000 6%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 4%,
#000000 6%);
}
.gradient-overlay.full-width-video {
@@ -525,6 +525,8 @@
font-family: "Archivo Narrow", sans-serif;
font-size: 18px;
white-space: nowrap;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
cursor: pointer;
transition: all 0.3s ease;
font-weight: 700;
@@ -535,6 +537,7 @@
.detail-button {
font-size: 18px;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
border-radius: 50%;
height: 50px;
@@ -547,6 +550,7 @@
.favorite-button {
font-size: 18px;
background-color: rgb(255, 255, 255);
color: red;
border-radius: 50%;
height: 50px;
@@ -662,7 +666,7 @@
display: flex;
align-items: center;
border-radius: 5px;
background: rgb(255 255 255 / 0.8);
background: rgba(255, 255, 255, 0.8);
color: #000;
border: none;
font-weight: 600;
@@ -712,13 +716,13 @@
object-position: center 20%;
z-index: 3;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
}
.gradient-overlay {
@@ -727,17 +731,17 @@
left: 0;
width: 100%;
height: 100%;
background: rgb(0 0 0 / 0.25);
background: rgba(0, 0, 0, 0.25);
z-index: 4;
pointer-events: none;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
rgba(255, 255, 255, 0) 2%,
rgba(0, 0, 0, 0.5) 6%,
#000000 8%);
}
.dots-container {
@@ -768,7 +772,22 @@
.button-container {
top: calc(50% + 25vh);
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 {
@@ -825,10 +844,6 @@
.genre {
top: calc(50% + 16vh);
}
.button-container {
top: calc(50% + 27vh);
}
}
}
@@ -997,6 +1012,239 @@
margin: 0;
}
.layout-tv .backdrop-container{
.layout-tv .backdrop-container {
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;
}
}
/* Floating Custom Overlay Styling */
.custom-overlay-container {
position: absolute;
top: 8vh;
left: 4vw;
z-index: 15;
display: flex;
align-items: center;
justify-content: flex-start;
pointer-events: none; /* Let clicks pass through to the slider */
animation: fadeInOverlay 1.5s ease-in-out forwards;
}
.custom-overlay-text {
font-family: "Archivo Narrow", sans-serif;
color: #fff;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.8), -1px -1px 4px rgba(0, 0, 0, 0.5);
margin: 0;
letter-spacing: 1px;
}
.custom-overlay-image {
max-width: 300px;
max-height: 120px;
object-fit: contain;
filter: drop-shadow(2px 4px 6px rgba(0,0,0,0.5));
}
@keyframes fadeInOverlay {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Make it smaller on mobile portrait */
@media only screen and (max-width: 767px) and (orientation: portrait) {
.custom-overlay-container {
top: 5vh;
left: 50%;
transform: translateX(-50%);
width: 90%;
justify-content: center;
text-align: center;
}
.custom-overlay-text {
font-size: 1.8rem;
}
.custom-overlay-image {
max-width: 200px;
max-height: 80px;
}
@keyframes fadeInOverlay {
from {
opacity: 0;
transform: translate(-50%, -10px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
}
/* Custom Overlay Styles */
.custom-overlay-style-Shadowed {
color: #fff;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.9), -1px -1px 4px rgba(0, 0, 0, 0.8);
}
.custom-overlay-style-Frosted {
color: #fff;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 8px 24px;
border-radius: 50px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
text-shadow: none; /* override default */
}
.custom-overlay-style-Cinematic {
background: linear-gradient(to right, #bf953f, #fcf6ba, #b38728, #fbf5b7, #aa771c);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: none; /* override default */
filter: drop-shadow(0px 2px 8px rgba(255, 215, 0, 0.4)) drop-shadow(2px 2px 4px rgba(0,0,0,0.8));
animation: shineCinematic 4s linear infinite;
background-size: 200% auto;
}
@keyframes shineCinematic {
to {
background-position: 200% center;
}
}
.custom-overlay-style-Pulse {
color: #fff;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.9), -1px -1px 4px rgba(0, 0, 0, 0.8);
animation: pulseOverlayText 3s ease-in-out infinite alternate;
}
@keyframes pulseOverlayText {
from {
transform: scale(1);
}
to {
transform: scale(1.05);
}
}
/* Text Overlay Styles */
.custom-overlay-style-Neon {
color: #fff;
text-shadow:
0 0 5px #fff,
0 0 10px #fff,
0 0 20px #ff00de,
0 0 40px #ff00de,
0 0 80px #ff00de,
0 0 90px #ff00de,
0 0 100px #ff00de,
0 0 150px #ff00de;
animation: flickerNeon 1.5s infinite alternate;
}
@keyframes flickerNeon {
0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% {
text-shadow:
0 0 5px #fff,
0 0 10px #fff,
0 0 20px #ff00de,
0 0 40px #ff00de,
0 0 80px #ff00de,
0 0 90px #ff00de,
0 0 100px #ff00de,
0 0 150px #ff00de;
}
20%, 24%, 55% {
text-shadow: none;
}
}
.custom-overlay-style-Typewriter {
font-family: 'Courier New', Courier, monospace;
background-color: #222;
color: #00ff00;
padding: 10px 20px;
border: 2px solid #00ff00;
border-radius: 4px;
box-shadow: 4px 4px 0px #00ff00;
text-transform: uppercase;
}
.custom-overlay-style-Bubble {
color: #fff;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
padding: 12px 30px;
border-radius: 100px;
border: 2px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 0 20px rgba(255,255,255,0.2);
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
animation: floatBubble 4s ease-in-out infinite;
}
@keyframes floatBubble {
0% { transform: translateY(0px); }
50% { transform: translateY(-15px); }
100% { transform: translateY(0px); }
}
.custom-overlay-style-SlideIn {
color: #fff;
text-transform: uppercase;
letter-spacing: 5px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
position: relative;
animation: slideInCinematic 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
}
.custom-overlay-style-SlideIn::before {
content: '';
position: absolute;
top: -10px;
bottom: -10px;
left: -50vw;
right: -50px;
background: linear-gradient(to right, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.8) 70%, transparent 100%);
z-index: -1;
border-left: 5px solid #00a4dc;
}
@keyframes slideInCinematic {
from {
transform: translateX(-100vw);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,20 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [
{
"version": "1.7.0.8",
"changelog": "- Add YouTube no-cookie host and referrer policy for iframe security to fix playback issues on iOS/MacOS",
"version": "1.7.2.1",
"changelog": "feat: add custom text/image overlay option\n- 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",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.0.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "932e1b1cd932d5c681175e3590b77458",
"timestamp": "2026-03-06T00:45:37Z"
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.2.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "c491aabf59a0a4b1d123a2647e53f76a",
"timestamp": "2026-03-09T14:26:42Z"
},
{
"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",

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