Compare commits

..

78 Commits

Author SHA1 Message Date
CodeDevMLH
1b65843b20 Update manifest.json for release v1.7.2.13 [skip ci] 2026-03-11 00:13:26 +00:00
CodeDevMLH
a70690c0de Bump version to 1.7.2.13 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-11 01:12:40 +01:00
CodeDevMLH
1e21286a66 Refactor seasonal image clear button styling in configPage.html 2026-03-11 01:12:28 +01:00
CodeDevMLH
90a64d49ed Update manifest.json for release v1.7.2.12 [skip ci] 2026-03-11 00:05:56 +00:00
CodeDevMLH
009798ea06 Bump version to 1.7.2.12 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-11 01:05:03 +01:00
CodeDevMLH
53f0f3c401 Refactor seasonal image clear button visibility and styling in configPage.html 2026-03-11 01:04:50 +01:00
CodeDevMLH
81dad8c6c6 Update manifest.json for release v1.7.2.11 [skip ci] 2026-03-10 23:58:38 +00:00
CodeDevMLH
66fe4dc3cb Bump version to 1.7.2.11 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-11 00:57:45 +01:00
CodeDevMLH
7be968d1c6 test 2026-03-11 00:57:31 +01:00
CodeDevMLH
2d492f3fed Update manifest.json for release v1.7.2.10 [skip ci] 2026-03-10 23:50:23 +00:00
CodeDevMLH
2160e2963c Bump version to 1.7.2.10 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-11 00:49:37 +01:00
CodeDevMLH
dc64bbe345 Improve styling and positioning of the clear image button in configPage.html 2026-03-11 00:49:25 +01:00
CodeDevMLH
3a61b3e548 Update manifest.json for release v1.7.2.9 [skip ci] 2026-03-10 23:38:07 +00:00
CodeDevMLH
950116a775 Bump version to 1.7.2.9 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-11 00:37:17 +01:00
CodeDevMLH
b9040c20fc Update field description and adjust seasonal dropzone height in configPage.html 2026-03-11 00:37:02 +01:00
CodeDevMLH
8388463880 delete example configPage 2026-03-11 00:36:58 +01:00
CodeDevMLH
233e569c94 Update field description for image upload in configPage.html 2026-03-11 00:27:32 +01:00
CodeDevMLH
93c265ffed Add authorization to overlay image upload, retrieval, and deletion endpoints 2026-03-11 00:27:26 +01:00
CodeDevMLH
8f4dfa31c8 Remove fallback logic for resource stream retrieval in MediaBarEnhancedController 2026-03-11 00:27:21 +01:00
CodeDevMLH
99b8ef316c Update manifest.json for release v1.7.2.8 [skip ci] 2026-03-10 23:05:14 +00:00
CodeDevMLH
6880ccc9eb Bump version to 1.7.2.8 in project files and manifest.json
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-11 00:04:28 +01:00
CodeDevMLH
f77c3fbf71 Add custom overlay positioning and scaling options to configuration 2026-03-11 00:04:15 +01:00
CodeDevMLH
81b28952cc Update manifest.json for release v1.7.2.7 [skip ci] 2026-03-10 22:49:35 +00:00
CodeDevMLH
911c96216a Bump version to 1.7.2.7 in project files and manifest.json
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-03-10 23:48:40 +01:00
CodeDevMLH
b3b6296798 Add custom overlay positioning and scaling options in configuration 2026-03-10 23:43:20 +01:00
CodeDevMLH
bca2d577b9 Update manifest.json for release v1.7.2.6 [skip ci] 2026-03-10 22:08:51 +00:00
CodeDevMLH
11b6316338 Bump version to 1.7.2.6 in project files and manifest.json
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-10 23:08:07 +01:00
CodeDevMLH
bbc6da9d41 Add new visual styles and effects for custom overlays in configuration 2026-03-10 23:07:54 +01:00
CodeDevMLH
c313cbf37d Update image directory path in OverlayImageController for asset organization 2026-03-10 23:07:49 +01:00
CodeDevMLH
79405c6e95 Update manifest.json for release v1.7.2.5 [skip ci] 2026-03-10 19:47:07 +00:00
CodeDevMLH
a542e38dc3 Bump version to 1.7.2.5 in project files and manifest.json
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m6s
2026-03-10 20:44:51 +01:00
CodeDevMLH
a27566632c Update image directory path in OverlayImageController 2026-03-10 20:44:31 +01:00
CodeDevMLH
779e20bfb2 Update manifest.json for release v1.7.2.4 [skip ci] 2026-03-10 00:28:44 +00:00
CodeDevMLH
0624624190 Bump version to 1.7.2.4 in project files and manifest.json
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-10 01:27:57 +01:00
CodeDevMLH
dd07c452ca Enhance image upload handling and update UI elements for clarity 2026-03-10 01:27:46 +01:00
CodeDevMLH
0cd64d4eea Update manifest.json for release v1.7.2.3 [skip ci] 2026-03-10 00:06:07 +00:00
CodeDevMLH
3480131e3c bum to 1.7.2.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-10 01:02:54 +01:00
CodeDevMLH
f33c5be75f Refactor code structure for improved readability and maintainability 2026-03-10 01:02:52 +01:00
CodeDevMLH
c1d06f3bb8 Add global overlay text and image configuration options; enhance upload functionality 2026-03-10 00:02:07 +01:00
CodeDevMLH
c493212f65 Update manifest.json for release v1.7.2.2 [skip ci] 2026-03-09 22:29:30 +00:00
CodeDevMLH
d497f22416 Bump version to 1.7.2.2 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-09 23:27:59 +01:00
CodeDevMLH
f65475673d Add custom overlay image style and priority options; enhance overlay handling 2026-03-09 23:27:28 +01:00
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
10 changed files with 1312 additions and 116 deletions

View File

@@ -44,18 +44,6 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Api
var stream = assembly.GetManifestResourceStream(resourceName); var stream = assembly.GetManifestResourceStream(resourceName);
// if (stream == null)
// {
// // Try fallback/debug matching
// var allNames = assembly.GetManifestResourceNames();
// var match = Array.Find(allNames, n => n.EndsWith(resourcePath, StringComparison.OrdinalIgnoreCase));
// if (match != null)
// {
// stream = assembly.GetManifestResourceStream(match);
// }
// }
if (stream == null) if (stream == null)
{ {
return NotFound($"Resource not found: {resourceName}"); return NotFound($"Resource not found: {resourceName}");

View File

@@ -0,0 +1,182 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Authorization;
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;
_imageDirectory = Path.Combine(applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.MediaBarEnhanced", "Assets");
}
/// <summary>
/// Uploads a new custom overlay image.
/// </summary>
[Authorize(Policy = "RequiresElevation")]
[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.");
}
// Extract original extension or fallback to .jpg
string extension = Path.GetExtension(file.FileName);
if (string.IsNullOrWhiteSpace(extension)) extension = ".jpg";
// Delete any existing file with this prefix before saving the new one
string prefix = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image_global" : $"custom_overlay_image_{filename}";
try
{
if (!Directory.Exists(_imageDirectory))
{
Directory.CreateDirectory(_imageDirectory);
}
// Remove existing
var existingFiles = Directory.GetFiles(_imageDirectory, $"{prefix}.*");
foreach(var extFile in existingFiles)
{
System.IO.File.Delete(extFile);
}
string targetFileName = $"{prefix}{extension}";
string targetPath = Path.Combine(_imageDirectory, targetFileName);
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 prefix = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image_global" : $"custom_overlay_image_{filename}";
if (!Directory.Exists(_imageDirectory))
return NotFound();
var existingFiles = Directory.GetFiles(_imageDirectory, $"{prefix}.*");
if (existingFiles.Length == 0)
return NotFound();
string targetPath = existingFiles[0];
// Read the file and return with appropriate MIME type
var stream = new FileStream(targetPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
string mimeType = "application/octet-stream";
string ext = Path.GetExtension(targetPath).ToLowerInvariant();
switch (ext) {
case ".jpg": case ".jpeg": mimeType = "image/jpeg"; break;
case ".png": mimeType = "image/png"; break;
case ".gif": mimeType = "image/gif"; break;
case ".webp": mimeType = "image/webp"; break;
}
return File(stream, mimeType);
}
/// <summary>
/// Deletes a custom overlay image.
/// </summary>
[Authorize(Policy = "RequiresElevation")]
[HttpDelete("OverlayImage")]
public IActionResult DeleteImage([FromQuery] string? filename = null)
{
string prefix = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image_global" : $"custom_overlay_image_{filename}";
if (Directory.Exists(_imageDirectory))
{
var existingFiles = Directory.GetFiles(_imageDirectory, $"{prefix}.*");
foreach(var file in existingFiles)
{
try
{
System.IO.File.Delete(file);
}
catch (Exception ex)
{
return StatusCode(500, $"Error deleting file: {ex.Message}");
}
}
return Ok();
}
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.");
}
if (!Directory.Exists(_imageDirectory))
return Ok();
var oldFiles = Directory.GetFiles(_imageDirectory, $"custom_overlay_image_{oldName}.*");
if (oldFiles.Length == 0)
return Ok();
try
{
string oldPath = oldFiles[0];
string extension = Path.GetExtension(oldPath);
string newPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{newName}{extension}");
// If a file with the new name already exists, delete it first to avoid conflicts
var existingNewFiles = Directory.GetFiles(_imageDirectory, $"custom_overlay_image_{newName}.*");
foreach(var existing in existingNewFiles) {
System.IO.File.Delete(existing);
}
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

@@ -36,6 +36,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool EnableLoadingScreen { get; set; } = true; public bool EnableLoadingScreen { get; set; } = true;
public bool EnableKeyboardControls { get; set; } = true; public bool EnableKeyboardControls { get; set; } = true;
public bool AlwaysShowArrows { get; set; } = false; public bool AlwaysShowArrows { get; set; } = false;
public bool HideArrowsOnMobile { get; set; } = true;
public string CustomMediaIds { get; set; } = ""; public string CustomMediaIds { get; set; } = "";
public bool EnableCustomMediaIds { get; set; } = true; public bool EnableCustomMediaIds { get; set; } = true;
public string PreferredVideoQuality { get; set; } = "Auto"; public string PreferredVideoQuality { get; set; } = "Auto";
@@ -48,5 +49,16 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool IncludeWatchedContent { get; set; } = false; public bool IncludeWatchedContent { get; set; } = false;
public string SortBy { get; set; } = "Random"; public string SortBy { get; set; } = "Random";
public string SortOrder { get; set; } = "Ascending"; 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";
public string CustomOverlayImageStyle { get; set; } = "None";
public string CustomOverlayPriority { get; set; } = "Image";
public int CustomOverlayPositionX { get; set; } = 0;
public int CustomOverlayPositionY { get; set; } = 0;
public int CustomOverlayScale { get; set; } = 100;
} }
} }

View File

@@ -30,6 +30,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;"> 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> <h3>Custom Content</h3>
</button> </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>
<button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-advanced', this)" <button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-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;"> 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> <h3>Advanced Settings</h3>
@@ -78,7 +82,7 @@
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be
preferred over remote and local trailers.</div> preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="WaitForTrailerToEndContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd" <input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" /> name="WaitForTrailerToEnd" />
@@ -86,7 +90,7 @@
</label> </label>
<div class="fieldDescription">Delay slide transition until trailer finishes.</div> <div class="fieldDescription">Delay slide transition until trailer finishes.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="EnableMobileVideoContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo" <input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
name="EnableMobileVideo" /> name="EnableMobileVideo" />
@@ -100,8 +104,8 @@
name="ShowTrailerButton" /> name="ShowTrailerButton" />
<span>Show Trailer Button</span> <span>Show Trailer Button</span>
</label> </label>
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if <div class="fieldDescription">Display a button to open trailer in modal. Button only
trailer is not set as backdrop or if no trailer is available.</div> visible if trailer is not set as backdrop.</div>
</div> </div>
</div> </div>
@@ -183,15 +187,15 @@
during their active date ranges. If no season matches the current date, the default 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> Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<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 id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ExcludeSeasonalContent"
name="ExcludeSeasonalContent" />
<span>Exclude Seasonal Content from Random Lists</span>
</label>
<div class="fieldDescription">When enabled, any items defined in your Seasonal Sections below will be explicitly excluded from being shown when the plugin pulls random items from your library.</div>
</div>
<div <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;"> 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;">
@@ -210,6 +214,128 @@
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" /> <input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div> </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" style="margin-top: 1em;">
<label class="selectLabel" for="CustomOverlayPriority">Priority (If Both Are Set)</label>
<select is="emby-select" id="CustomOverlayPriority" name="CustomOverlayPriority"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Image">Show Image</option>
<option value="Text">Show Text</option>
</select>
<div class="fieldDescription">What should be displayed if you have both an Image and Text configured?</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!").</div>
</div>
<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.</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; color: #a94442;" title="Clear Image">
<i class="material-icons">delete</i>
</button>
</div>
<div class="fieldDescription">Uploading an image will save it to your server and automatically update the URL field above.</div>
</div>
<h2 class="sectionTitle" style="margin-top: 1.2em;">Styles</h2>
<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="None">None (Plain White)</option>
<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="SteadyNeon">Steady Neon</option>
<option value="Neon">Neon Cyberpunk (Glitching)</option>
<option value="Typewriter">Typewriter Pop</option>
<option value="Bubble">Floating Bubble</option>
<option value="SlideIn">Cinematic Slide-In</option>
<option value="Glitch">Cyberpunk Glitch</option>
<option value="RetroPop">Retro Pop 3D</option>
<option value="Shimmer">Glowing Shimmer</option>
<option value="Wave">Liquid Wave</option>
<option value="VHS">VHS Tracking</option>
<option value="Matrix">Digital Matrix</option>
<option value="Ghost">Ghostly Apparition</option>
<option value="PulseGlow">Breathing Pulse Glow</option>
</select>
<div class="fieldDescription">Choose the visual styling animation for your custom text.</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="CustomOverlayImageStyle">Image Style</label>
<select is="emby-select" id="CustomOverlayImageStyle" name="CustomOverlayImageStyle"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="None">None (Default)</option>
<option value="RoundedShadow">Rounded & Shadow</option>
<option value="GlowingBorder">Neon Glow</option>
<option value="Polaroid">Polaroid Frame</option>
<option value="Vintage">Vintage Sepia</option>
<option value="Grayscale">Grayscale</option>
<option value="Hologram">Hologram</option>
<option value="CRT">CRT Monitor</option>
<option value="Floating">Floating Float</option>
<option value="VHSTracking">VHS Tracking Glitch</option>
<option value="ColorCycle">Rainbow Color Cycle</option>
<option value="Mirror">Mirror Reflection</option>
</select>
<div class="fieldDescription">Choose a visual effect to apply to your overlay image.</div>
</div>
<h3 class="sectionTitle" style="margin-top:2em; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 0.5em;">Layout Tweaks</h3>
<div style="display: flex; gap: 1em; flex-wrap: wrap;">
<div class="inputContainer" style="flex: 1; min-width: 150px;">
<label class="inputLabel inputLabelUnfocused" for="CustomOverlayPositionX">X Offset (% vw)</label>
<input is="emby-input" type="number" id="CustomOverlayPositionX" name="CustomOverlayPositionX" min="-50" max="50" step="1" />
<div class="fieldDescription">Horizontal nudge (negative = left, positive = right). Default is 0.</div>
</div>
<div class="inputContainer" style="flex: 1; min-width: 150px;">
<label class="inputLabel inputLabelUnfocused" for="CustomOverlayPositionY">Y Offset (% vh)</label>
<input is="emby-input" type="number" id="CustomOverlayPositionY" name="CustomOverlayPositionY" min="-50" max="50" step="1" />
<div class="fieldDescription">Vertical nudge (negative = up, positive = down). Default is 0.</div>
</div>
<div class="inputContainer" style="flex: 1; min-width: 150px;">
<label class="inputLabel inputLabelUnfocused" for="CustomOverlayScale">Scale (%)</label>
<input is="emby-input" type="number" id="CustomOverlayScale" name="CustomOverlayScale" min="50" max="200" step="1" />
<div class="fieldDescription">Overlay zoom level. Default is 100%.</div>
</div>
</div>
</div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
<div id="media-bar-enhanced-advanced" class="tab-content" style="display:none;"> <div id="media-bar-enhanced-advanced" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Features</h2> <h2 class="sectionTitle">Features</h2>
@@ -299,16 +425,23 @@
<span>Enable Loading Screen</span> <span>Enable Loading Screen</span>
</label> </label>
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You <div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
may have to reload the page twice)</div> may have to reload the page twice (after changing this setting) to take effect)</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows" <input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" /> name="AlwaysShowArrows" />
<span>Always Show Arrows</span> <span>Always Show Arrow Navigation Buttons</span>
</label> </label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead <div class="fieldDescription">Force the UI arrow navigation buttons to always be visible instead of only when hovered.</div>
of only on hover.</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="HideArrowsOnMobile"
name="HideArrowsOnMobile" />
<span>Hide Arrows on Mobile</span>
</label>
<div class="fieldDescription">Completely disable the navigation arrows on mobile devices (since swiping is available).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -320,6 +453,7 @@
Space (pause), M (mute/unmute)) for Space (pause), M (mute/unmute)) for
the slideshow.</div> the slideshow.</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Time Settings</h2> <h2 class="sectionTitle">Time Settings</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -363,6 +497,7 @@
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for <div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</div> mobile).</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Sorting and Filtering</h2> <h2 class="sectionTitle">Content Sorting and Filtering</h2>
<div class="selectContainer"> <div class="selectContainer">
@@ -405,6 +540,16 @@
<input is="emby-input" type="number" id="MaxDaysRecent" name="MaxDaysRecent" /> <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 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>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Limits</h2> <h2 class="sectionTitle">Content Limits</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -435,7 +580,7 @@
<label> <label>
<input is="emby-checkbox" type="checkbox" id="ShowPaginationDots" <input is="emby-checkbox" type="checkbox" id="ShowPaginationDots"
name="ShowPaginationDots" /> name="ShowPaginationDots" />
<span>Show Pagination Dots</span> <span>Show Pagination Dots/Counter</span>
</label> </label>
<div class="fieldDescription">Show or hide the pagination dots/counter navigation at the <div class="fieldDescription">Show or hide the pagination dots/counter navigation at the
bottom of the slideshow.</div> bottom of the slideshow.</div>
@@ -454,15 +599,6 @@
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" /> <input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</div> <div class="fieldDescription">Maximum characters for the plot summary.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
</div> </div>
<div <div
@@ -524,7 +660,10 @@
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating', 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent' 'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority',
'CustomOverlayPositionX', 'CustomOverlayPositionY', 'CustomOverlayScale'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -582,14 +721,20 @@
var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop'); var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop');
var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer'); var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer');
var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer'); var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer');
var waitForTrailerContainer = page.querySelector('#WaitForTrailerToEndContainer');
var enableMobileVideoContainer = page.querySelector('#EnableMobileVideoContainer');
function updatePreferLocalVisibility() { function updatePreferLocalVisibility() {
if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) { if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) {
if (preferLocalContainer) preferLocalContainer.style.display = 'block'; if (preferLocalContainer) preferLocalContainer.style.display = 'block';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'block';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'block';
} else { } else {
if (preferLocalContainer) preferLocalContainer.style.display = 'none'; if (preferLocalContainer) preferLocalContainer.style.display = 'none';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'none';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'none';
} }
} }
@@ -598,6 +743,137 @@
updatePreferLocalVisibility(); 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() {
var icon = dropzone.querySelector('i');
var text = dropzone.querySelector('span');
if (urlInput.value && urlInput.value.trim() !== '') {
previewImg.src = urlInput.value;
previewImg.style.display = 'block';
clearBtn.style.display = 'inline-flex';
if(icon) icon.style.display = 'none';
if(text) {
text.textContent = 'Drag and drop a new image here, or click to select';
text.style.display = 'block';
text.style.position = 'absolute';
text.style.bottom = '10px';
text.style.zIndex = '3';
text.style.backgroundColor = 'rgba(0,0,0,0.6)';
text.style.padding = '4px 8px';
text.style.borderRadius = '4px';
}
} else {
previewImg.style.display = 'none';
clearBtn.style.display = 'none';
if(icon) icon.style.display = 'block';
if(text) {
text.textContent = 'Drag and drop an image here, or click to select';
text.style.display = 'block';
text.style.position = 'static';
text.style.backgroundColor = 'transparent';
}
}
}
// 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(); Dashboard.hideLoadingMsg();
}); });
}, },
@@ -629,7 +905,10 @@
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating', 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent' 'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority',
'CustomOverlayPositionX', 'CustomOverlayPositionY', 'CustomOverlayScale'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -664,8 +943,10 @@
MediaBarEnhancedConfigurationPage.createSectionElement(container, { MediaBarEnhancedConfigurationPage.createSectionElement(container, {
Name: 'New Season', Name: 'New Season',
StartDay: 1, StartMonth: 1, StartDay: 1, StartMonth: 1,
EndDay: 1, EndMonth: 1, EndDay: 31, EndMonth: 1,
MediaIds: '' MediaIds: '',
OverlayText: '',
OverlayImageUrl: ''
}, index); }, index);
}, },
@@ -726,6 +1007,28 @@
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Media IDs</label>' + ' <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>' + ' <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 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>' +
'<h3 class="inputLabel" style="margin-top: 1.5em; margin-bottom: 0.5em;">Custom Overlay Overrides</h3>' +
'<div style="display: flex; flex-wrap: wrap; gap: 1em; align-items: flex-start;">' +
' <div style="flex: 1; min-width: 250px; display: flex; flex-direction: column; gap: 0.5em;">' +
' <div class="inputContainer" style="margin: 0;">' +
' <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">Override the global text.</div>' +
' </div>' +
' <div class="inputContainer" style="margin: 0;">' +
' <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">Override the global image URL.</div>' +
' </div>' +
' </div>' +
' <div class="inputContainer" style="flex: 1; min-width: 250px; margin: 0; padding-bottom: 5px;">' +
' <div class="seasonal-dropzone" style="border: 2px dashed rgba(255,255,255,0.2); border-radius: 8px; padding: 1em; text-align: center; cursor: pointer; background: rgba(0,0,0,0.2); transition: all 0.2s ease; position: relative; min-height: 98px; display: flex; flex-direction: column; align-items: center; justify-content: center;">' +
' <i class="material-icons" style="font-size: 24px; color: rgba(255,255,255,0.4); margin-bottom: 4px;">cloud_upload</i>' +
' <span style="font-size: 0.85em; color: rgba(255,255,255,0.7);">Upload seasonal image</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: 100%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border-radius: 4px; z-index: 2; object-fit: contain;" />' +
' <button type="button" is="paper-icon-button-light" class="seasonal-clear-btn remove-img" title="Clear Image" style="display: none; color: #a94442;"><i class="material-icons">delete</i></button>' +
' </div>' +
' </div>' +
'</div>'; '</div>';
div.querySelector('.btn-remove').addEventListener('click', function () { div.querySelector('.btn-remove').addEventListener('click', function () {
@@ -747,6 +1050,185 @@
} }
}); });
// --- 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 icon = dropzone.querySelector('i');
var text = dropzone.querySelector('span');
var val = urlInput.value.trim();
if (val) {
previewImg.src = val;
previewImg.style.display = 'block';
clearBtn.style.display = 'inline-flex';
if(icon) icon.style.display = 'none';
if(text) {
text.textContent = 'Drag and drop a new image here, or click to select';
text.style.display = 'block';
text.style.position = 'absolute';
text.style.bottom = '5px';
text.style.zIndex = '3';
text.style.backgroundColor = 'rgba(0,0,0,0.6)';
text.style.padding = '2px 6px';
text.style.borderRadius = '4px';
text.style.fontSize = '0.7em';
}
} else {
previewImg.src = '';
previewImg.style.display = 'none';
clearBtn.style.display = 'none';
if(icon) icon.style.display = 'block';
if(text) {
text.textContent = 'Upload seasonal overlay image';
text.style.display = 'block';
text.style.position = 'static';
text.style.backgroundColor = 'transparent';
text.style.fontSize = '0.85em';
}
}
}
// 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); container.appendChild(div);
}, },
@@ -770,7 +1252,9 @@
StartMonth: parseInt(el.querySelector('.start-month').value), StartMonth: parseInt(el.querySelector('.start-month').value),
EndDay: parseInt(el.querySelector('.end-day').value), EndDay: parseInt(el.querySelector('.end-day').value),
EndMonth: parseInt(el.querySelector('.end-month').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; return sections;

View File

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

View File

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

View File

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

View File

@@ -354,13 +354,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.backdrop-container.full-width-video { .backdrop-container.full-width-video {
@@ -384,13 +384,13 @@
border-radius: 5px; border-radius: 5px;
z-index: 3; z-index: 3;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.backdrop-overlay { .backdrop-overlay {
@@ -403,13 +403,13 @@
border-radius: 5px; border-radius: 5px;
z-index: 4; z-index: 4;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
} }
.gradient-overlay { .gradient-overlay {
@@ -424,13 +424,13 @@
rgba(29, 29, 29, 0) 100%); rgba(29, 29, 29, 0) 100%);
z-index: 4; z-index: 4;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
} }
.gradient-overlay.full-width-video { .gradient-overlay.full-width-video {
@@ -525,6 +525,8 @@
font-family: "Archivo Narrow", sans-serif; font-family: "Archivo Narrow", sans-serif;
font-size: 18px; font-size: 18px;
white-space: nowrap; white-space: nowrap;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
font-weight: 700; font-weight: 700;
@@ -535,6 +537,7 @@
.detail-button { .detail-button {
font-size: 18px; font-size: 18px;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0); color: rgb(0, 0, 0);
border-radius: 50%; border-radius: 50%;
height: 50px; height: 50px;
@@ -547,6 +550,7 @@
.favorite-button { .favorite-button {
font-size: 18px; font-size: 18px;
background-color: rgb(255, 255, 255);
color: red; color: red;
border-radius: 50%; border-radius: 50%;
height: 50px; height: 50px;
@@ -662,7 +666,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 5px; border-radius: 5px;
background: rgb(255 255 255 / 0.8); background: rgba(255, 255, 255, 0.8);
color: #000; color: #000;
border: none; border: none;
font-weight: 600; font-weight: 600;
@@ -712,13 +716,13 @@
object-position: center 20%; object-position: center 20%;
z-index: 3; z-index: 3;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.gradient-overlay { .gradient-overlay {
@@ -727,17 +731,17 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgb(0 0 0 / 0.25); background: rgba(0, 0, 0, 0.25);
z-index: 4; z-index: 4;
pointer-events: none; pointer-events: none;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.dots-container { .dots-container {
@@ -1008,7 +1012,7 @@
margin: 0; margin: 0;
} }
.layout-tv .backdrop-container{ .layout-tv .backdrop-container {
top: -5%; top: -5%;
} }
@@ -1027,3 +1031,426 @@
-webkit-backdrop-filter: none; -webkit-backdrop-filter: none;
} }
} }
/* Custom Overlay Styling */
.custom-overlay-container {
position: absolute;
top: calc(8vh + var(--overlay-y, 0vh));
left: calc(4vw + var(--overlay-x, 0vw));
transform: scale(var(--overlay-scale, 1));
z-index: 15;
display: flex;
align-items: center;
justify-content: flex-start;
pointer-events: none;
animation: overlayFadeInGlobal 1.5s ease-in-out forwards;
/* animation: fadeInOverlay 1.5s ease-in-out forwards; */
}
@keyframes overlayFadeInGlobal {
from { opacity: 0; }
to { opacity: 1; }
}
/* @keyframes fadeInOverlay {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} */
.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));
}
@media only screen and (max-width: 767px) and (orientation: portrait) {
.custom-overlay-container {
top: calc(15vh + var(--overlay-y, 0vh));
left: calc(50% + var(--overlay-x, 0vw));
transform: translateX(-50%) scale(var(--overlay-scale, 1));
width: 90%;
justify-content: center;
text-align: center;
}
.custom-overlay-text {
font-size: 1.5rem; /* 1.5 - 1.8 */
}
/* @keyframes fadeInOverlay {
from {
opacity: 0;
transform: translate(-50%, -10px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
} */
.custom-overlay-image {
max-width: 150px; /* oder 200px? */
max-height: 60px; /* oder 80px? */
}
}
/* Custom Overlay Text Styles */
.custom-overlay-style-None {
color: #fff;
text-shadow: none;
}
.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: linear-gradient(135deg, rgba(255,255,255,0.15), rgba(255,255,255,0));
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
padding: 10px 30px;
border-radius: 50px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
.custom-overlay-style-Cinematic {
background: linear-gradient(to right, #bf953f, #fcf6ba, #b38728, #fbf5b7, #aa771c, #fcf6ba, #bf953f);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: none;
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 6s linear infinite;
background-size: 200% auto;
}
@keyframes shineCinematic {
0% { background-position: 0% center; }
100% { 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);
}
}
.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.7) 0%, rgba(0,0,0,0.6) 40%, transparent 100%);
z-index: -1;
}
@keyframes slideInCinematic {
from {
transform: translateX(-100vw);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.custom-overlay-style-SteadyNeon {
color: #fff;
text-shadow:
0 0 5px #fff,
0 0 10px #fff,
0 0 20px #ff00de,
0 0 40px #ff00de,
0 0 80px #ff00de;
}
.custom-overlay-style-Glitch {
color: #fff;
position: relative;
text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
animation: glitchText 3s infinite;
}
@keyframes glitchText {
0% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); }
20% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); }
21% { text-shadow: -2px 0 0 #ff00c1, 2px 0 0 #00fff9; transform: translate(-2px, 1px); }
23% { text-shadow: 2px 0 0 #ff00c1, -2px 0 0 #00fff9; transform: translate(2px, -1px); }
25% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); }
100% { text-shadow: 2px 2px 8px rgba(0,0,0,0.8); transform: translate(0); }
}
.custom-overlay-style-RetroPop {
color: #f7f7f7;
text-shadow:
2px 2px 0px #ff0055,
4px 4px 0px #00a4dc,
6px 6px 0px #ffcc00,
8px 8px 10px rgba(0,0,0,0.6);
font-weight: 900;
letter-spacing: 2px;
}
.custom-overlay-style-Shimmer {
color: rgba(255,255,255,0.7);
background: linear-gradient(to right, #222 20%, #fff 40%, #fff 60%, #222 80%);
background-size: 200% auto;
color: #000;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmerText 3s linear infinite;
text-shadow: 2px 2px 8px rgba(0,0,0,0.3);
}
@keyframes shimmerText {
to {
background-position: 200% center;
}
}
.custom-overlay-style-Wave {
color: #fff;
text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
display: inline-block;
animation: liquidWave 3s ease-in-out infinite;
}
@keyframes liquidWave {
0%, 100% { transform: translateY(0) skewY(0); }
25% { transform: translateY(-3px) skewY(1deg); }
50% { transform: translateY(0) skewY(0); }
75% { transform: translateY(3px) skewY(-1deg); }
}
.custom-overlay-style-VHS {
color: #fff;
text-shadow: 3px 0 0 #f00, -3px 0 0 #0ff;
animation: vhsTracking 2s steps(2, start) infinite;
}
@keyframes vhsTracking {
0%, 100% { text-shadow: 3px 0 0 #f00, -3px 0 0 #0ff; }
50% { text-shadow: 2px 1px 0 #f00, -2px -1px 0 #0ff; }
}
.custom-overlay-style-Matrix {
color: #0f0;
font-family: monospace;
text-shadow: 0 0 5px #0f0, 0 0 10px #0f0;
letter-spacing: 2px;
animation: matrixGlow 2s infinite alternate;
}
@keyframes matrixGlow {
to { text-shadow: 0 0 10px #0f0, 0 0 20px #0f0; }
}
.custom-overlay-style-Ghost {
color: rgba(255,255,255,0.8);
filter: blur(0px);
animation: ghostApparition 6s infinite alternate;
}
@keyframes ghostApparition {
0% { filter: blur(0px); opacity: 1; transform: scale(1); }
50% { filter: blur(3px); opacity: 0.6; transform: scale(1.02); }
100% { filter: blur(8px); opacity: 0; transform: scale(1.05); }
}
.custom-overlay-style-PulseGlow {
color: #fff;
text-shadow: 0 0 10px rgba(255,255,255,0.8);
animation: pulseGlowAura 3s ease-in-out infinite alternate;
}
@keyframes pulseGlowAura {
0% { text-shadow: 0 0 5px rgba(255,255,255,0.5), 0 0 10px #00a4dc; transform: scale(1); }
100% { text-shadow: 0 0 15px rgba(255,255,255,1), 0 0 30px #00a4dc, 0 0 40px #00a4dc; transform: scale(1.05); }
}
/* Custom Overlay Image Styles */
.custom-overlay-img-RoundedShadow {
border-radius: 12px;
filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.6));
}
.custom-overlay-img-GlowingBorder {
border-radius: 8px;
box-shadow: 0 0 10px #00a4dc, 0 0 20px #00a4dc, 0 0 30px #00a4dc, inset 0 0 10px #00a4dc;
animation: pulseGlowImg 2s infinite alternate;
}
@keyframes pulseGlowImg {
from { box-shadow: 0 0 10px #00a4dc, 0 0 20px #00a4dc, 0 0 30px #00a4dc; }
to { box-shadow: 0 0 15px #00a4dc, 0 0 30px #00a4dc, 0 0 45px #00a4dc; }
}
.custom-overlay-img-Polaroid {
background: white;
padding: 10px 10px 25px 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.4), 0 12px 24px rgba(0,0,0,0.3);
transform: rotate(-3deg);
border-radius: 2px;
}
.custom-overlay-img-Vintage {
filter: sepia(0.6) contrast(1.2) brightness(0.9) saturate(1.2) drop-shadow(2px 4px 8px rgba(0,0,0,0.6));
}
.custom-overlay-img-Grayscale {
filter: grayscale(100%) contrast(1.1) drop-shadow(2px 4px 8px rgba(0,0,0,0.6));
}
.custom-overlay-img-Hologram {
filter: drop-shadow(0 0 10px #0ff) sepia(0.8) hue-rotate(180deg) saturate(3);
opacity: 0.8;
animation: hologramFlicker 3s infinite;
}
@keyframes hologramFlicker {
0% { opacity: 0.8; transform: skewX(0); }
5% { opacity: 0.5; transform: skewX(2deg); }
10% { opacity: 0.9; transform: skewX(-2deg); }
15% { opacity: 0.8; transform: skewX(0); }
100% { opacity: 0.8; }
}
.custom-overlay-img-CRT {
filter: contrast(1.5) brightness(1.2) drop-shadow(3px 0 0 rgba(255,0,0,0.5)) drop-shadow(-3px 0 0 rgba(0,0,255,0.5));
}
.custom-overlay-img-Floating {
filter: drop-shadow(0 15px 20px rgba(0,0,0,0.6));
animation: floatImg 4s ease-in-out infinite;
}
@keyframes floatImg {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15px); }
}
.custom-overlay-img-VHSTracking {
filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5));
animation: imgVhsTracking 2s infinite;
}
@keyframes imgVhsTracking {
0% { transform: translateX(0); clip-path: inset(0 0 0 0); filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5)) hue-rotate(0deg); }
5% { transform: translateX(-5px); clip-path: inset(10% 0 80% 0); filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5)) hue-rotate(90deg); }
10% { transform: translateX(5px); clip-path: inset(40% 0 40% 0); filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5)) hue-rotate(-90deg); }
15% { transform: translateX(0); clip-path: inset(0 0 0 0); filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5)) hue-rotate(0deg); }
100% { transform: translateX(0); clip-path: inset(0 0 0 0); }
}
.custom-overlay-img-ColorCycle {
filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5));
animation: imgColorCycle 10s linear infinite;
}
@keyframes imgColorCycle {
from { filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5)) hue-rotate(0deg); }
to { filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5)) hue-rotate(360deg); }
}
.custom-overlay-img-Mirror {
filter: drop-shadow(2px 4px 8px rgba(0,0,0,0.5));
-webkit-box-reflect: below 2px linear-gradient(transparent, rgba(255,255,255,0.3));
}

View File

@@ -57,6 +57,16 @@ const CONFIG = {
preferredVideoQuality: "Auto", preferredVideoQuality: "Auto",
enableKeyboardControls: true, enableKeyboardControls: true,
alwaysShowArrows: false, alwaysShowArrows: false,
hideArrowsOnMobile: true,
enableCustomOverlay: false,
customOverlayText: "",
customOverlayImageUrl: "",
customOverlayStyle: "Shadowed",
customOverlayImageStyle: "None",
customOverlayPriority: "Image",
customOverlayPositionX: 0,
customOverlayPositionY: 0,
customOverlayScale: 100,
enableCustomMediaIds: true, enableCustomMediaIds: true,
enableSeasonalContent: false, enableSeasonalContent: false,
customMediaIds: "", customMediaIds: "",
@@ -748,7 +758,7 @@ const SlideUtils = {
if (isYoutube && videoId) { if (isYoutube && videoId) {
const ytIframe = this.createElement('iframe', { const ytIframe = this.createElement('iframe', {
id: 'modal-yt-player', id: 'modal-yt-player',
src: `https://www.youtube-nocookie.com/embed/${videoId}?enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`, src: `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&controls=1&iv_load_policy=3&rel=0&playsinline=1`,
allow: 'autoplay; encrypted-media', allow: 'autoplay; encrypted-media',
style: 'width: 100%; height: 100%; border: none;', style: 'width: 100%; height: 100%; border: none;',
referrerpolicy: 'strict-origin-when-cross-origin', referrerpolicy: 'strict-origin-when-cross-origin',
@@ -758,20 +768,6 @@ const SlideUtils = {
contentContainer.appendChild(ytIframe); contentContainer.appendChild(ytIframe);
overlay.append(closeButton, contentContainer); overlay.append(closeButton, contentContainer);
document.body.appendChild(overlay); document.body.appendChild(overlay);
this.loadYouTubeIframeAPI().then(() => {
new YT.Player(ytIframe, {
playerVars: {
autoplay: 1,
controls: 1,
iv_load_policy: 3,
rel: 0,
playsinline: 1,
origin: window.location.origin,
enablejsapi: 1
}
});
});
} else { } else {
const video = this.createElement('video', { const video = this.createElement('video', {
src: url, src: url,
@@ -779,6 +775,7 @@ const SlideUtils = {
autoplay: true, autoplay: true,
className: 'video-modal-player' className: 'video-modal-player'
}); });
video.setAttribute('playsinline', '');
contentContainer.appendChild(video); contentContainer.appendChild(video);
overlay.append(closeButton, contentContainer); overlay.append(closeButton, contentContainer);
document.body.appendChild(overlay); document.body.appendChild(overlay);
@@ -835,7 +832,7 @@ const LocalizationUtils = {
} }
} }
if (window.ApiClient && STATE.jellyfinData?.accessToken) { if (window.ApiClient && STATE.jellyfinData && STATE.jellyfinData.accessToken) {
try { try {
const userId = window.ApiClient.getCurrentUserId(); const userId = window.ApiClient.getCurrentUserId();
if (userId) { if (userId) {
@@ -845,7 +842,7 @@ const LocalizationUtils = {
}); });
if (userResponse.ok) { if (userResponse.ok) {
const userData = await userResponse.json(); const userData = await userResponse.json();
if (userData.Configuration?.AudioLanguagePreference) { if (userData.Configuration && userData.Configuration.AudioLanguagePreference) {
locale = userData.Configuration.AudioLanguagePreference.toLowerCase(); locale = userData.Configuration.AudioLanguagePreference.toLowerCase();
} }
} }
@@ -855,7 +852,7 @@ const LocalizationUtils = {
} }
} }
if (!locale && window.ApiClient && STATE.jellyfinData?.accessToken) { if (!locale && window.ApiClient && (STATE.jellyfinData && STATE.jellyfinData.accessToken)) {
try { try {
const configUrl = window.ApiClient.getUrl('System/Configuration'); const configUrl = window.ApiClient.getUrl('System/Configuration');
const configResponse = await fetch(configUrl, { const configResponse = await fetch(configUrl, {
@@ -1030,7 +1027,7 @@ const LocalizationUtils = {
*/ */
getLocalizedString(key, fallback, ...args) { getLocalizedString(key, fallback, ...args) {
const locale = this.cachedLocale || 'en-us'; const locale = this.cachedLocale || 'en-us';
let translated = this.translations[locale]?.[key] || fallback; let translated = (this.translations[locale] && this.translations[locale][key]) || fallback;
if (args.length > 0) { if (args.length > 0) {
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
@@ -1775,9 +1772,12 @@ const SlideCreator = {
} }
const isLowPower = isLowPowerDevice(); const isLowPower = isLowPowerDevice();
const isIOSApp = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const limitVideos = isLowPower || isIOSApp;
const itemIndex = STATE.slideshow.itemIds ? STATE.slideshow.itemIds.indexOf(itemId) : -1; const itemIndex = STATE.slideshow.itemIds ? STATE.slideshow.itemIds.indexOf(itemId) : -1;
const isActiveSlide = itemIndex !== -1 && itemIndex === STATE.slideshow.currentSlideIndex; const isActiveSlide = itemIndex !== -1 && itemIndex === STATE.slideshow.currentSlideIndex;
const shouldCreateVideo = !isLowPower || isActiveSlide; // Limit YouTube iframe bulk creation on low power devices OR iOS (which kills the WebProcess on OOM)
const shouldCreateVideo = !limitVideos || isActiveSlide;
if (isYoutube && videoId && shouldCreateVideo) { if (isYoutube && videoId && shouldCreateVideo) {
isVideo = true; isVideo = true;
@@ -1945,6 +1945,7 @@ const SlideCreator = {
}; };
videoAttributes.muted = ""; videoAttributes.muted = "";
videoAttributes.playsinline = "";
videoBackdrop = SlideUtils.createElement("video", videoAttributes); videoBackdrop = SlideUtils.createElement("video", videoAttributes);
videoBackdrop.volume = 0.4; videoBackdrop.volume = 0.4;
@@ -2453,6 +2454,7 @@ const SlideshowManager = {
previousVisibleSlide.classList.remove("active"); previousVisibleSlide.classList.remove("active");
} }
void currentSlide.offsetWidth;
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
@@ -2730,9 +2732,9 @@ const SlideshowManager = {
const totalItems = STATE.slideshow.itemIds.length; const totalItems = STATE.slideshow.itemIds.length;
let distance = Math.abs(index - currentIndex); let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) {
distance = Math.min(distance, totalItems - distance); // Always calculate circular distance for slideshow
} distance = Math.min(distance, totalItems - distance);
if (distance > keepRange) { if (distance > keepRange) {
// Destroy video player if exists // Destroy video player if exists
@@ -2802,7 +2804,7 @@ const SlideshowManager = {
if (currentItemId) { if (currentItemId) {
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
const video = currentSlide?.querySelector('video'); const video = currentSlide ? currentSlide.querySelector('video') : null;
if (video) { if (video) {
video.muted = STATE.slideshow.isMuted; video.muted = STATE.slideshow.isMuted;
@@ -2962,7 +2964,7 @@ const SlideshowManager = {
if (!currentSlide) return; if (!currentSlide) return;
// YouTube player: just resume, don't reload // YouTube player: just resume, don't reload
const ytPlayer = STATE.slideshow.videoPlayers?.[currentItemId]; const ytPlayer = (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) ? STATE.slideshow.videoPlayers[currentItemId] : undefined;
if (ytPlayer && typeof ytPlayer.playVideo === 'function') { if (ytPlayer && typeof ytPlayer.playVideo === 'function') {
if (STATE.slideshow.isMuted) { if (STATE.slideshow.isMuted) {
if (typeof ytPlayer.mute === 'function') ytPlayer.mute(); if (typeof ytPlayer.mute === 'function') ytPlayer.mute();
@@ -3469,6 +3471,10 @@ const initArrowNavigation = () => {
container.appendChild(muteButton); container.appendChild(muteButton);
const showArrows = () => { const showArrows = () => {
if (CONFIG.hideArrowsOnMobile && window.matchMedia("only screen and (max-width: 768px)").matches) {
return; // disable arrow display on mobile
}
leftArrow.style.display = "block"; leftArrow.style.display = "block";
rightArrow.style.display = "block"; rightArrow.style.display = "block";
@@ -3780,6 +3786,102 @@ const slidesInit = async () => {
return; return;
} }
const renderCustomOverlay = () => {
let activeOverlayText = CONFIG.customOverlayText;
let activeOverlayImage = CONFIG.customOverlayImageUrl;
let isSeasonOverride = false;
if (CONFIG.enableSeasonalContent && CONFIG.seasonalSections) {
try {
const sections = JSON.parse(CONFIG.seasonalSections || "[]");
const now = new Date();
const currentMonth = now.getMonth() + 1;
const currentDay = now.getDate();
for (const section of sections) {
const startMonth = parseInt(section.StartMonth);
const startDay = parseInt(section.StartDay);
const endMonth = parseInt(section.EndMonth);
const endDay = parseInt(section.EndDay);
let isActive = false;
if (startMonth === endMonth) {
if (currentMonth === startMonth && currentDay >= startDay && currentDay <= endDay) {
isActive = true;
}
} else if (startMonth < endMonth) {
if (currentMonth > startMonth && currentMonth < endMonth) {
isActive = true;
} else if (currentMonth === startMonth && currentDay >= startDay) {
isActive = true;
} else if (currentMonth === endMonth && currentDay <= endDay) {
isActive = true;
}
} else { // Wraps around year
if (currentMonth > startMonth || currentMonth < endMonth) {
isActive = true;
} else if (currentMonth === startMonth && currentDay >= startDay) {
isActive = true;
} else if (currentMonth === endMonth && currentDay <= endDay) {
isActive = true;
}
}
if (isActive) {
if (section.OverlayText || section.OverlayImageUrl) {
isSeasonOverride = true;
// Season fully overrides global overlay, even if empty
activeOverlayImage = section.OverlayImageUrl || null;
activeOverlayText = section.OverlayText || null;
}
break;
}
}
} catch (e) {
console.error("🎬 Media Bar:", "Error parsing seasonal sections for overlay:", e);
}
}
if (!CONFIG.enableCustomOverlay && !isSeasonOverride) {
return;
}
if (!activeOverlayText && !activeOverlayImage) return;
const overlayContainer = document.createElement("div");
overlayContainer.className = "custom-overlay-container";
const overlayPriority = CONFIG.customOverlayPriority || "Image";
const showImage = activeOverlayImage && (overlayPriority === "Image" || !activeOverlayText);
const showText = activeOverlayText && (!showImage);
if (showImage) {
const img = document.createElement("img");
const imgStyle = CONFIG.customOverlayImageStyle || "None";
img.className = `custom-overlay-image custom-overlay-img-${imgStyle}`;
img.src = activeOverlayImage;
overlayContainer.appendChild(img);
} else if (showText) {
const p = document.createElement("p");
p.className = `custom-overlay-text custom-overlay-style-${CONFIG.customOverlayStyle || 'Shadowed'}`;
p.textContent = activeOverlayText;
overlayContainer.appendChild(p);
}
const slidesContainer = document.getElementById("slides-container");
if (slidesContainer) {
const posX = CONFIG.customOverlayPositionX || 0;
const posY = CONFIG.customOverlayPositionY || 0;
const scaleValue = (CONFIG.customOverlayScale !== undefined ? CONFIG.customOverlayScale : 100) / 100;
overlayContainer.style.setProperty('--overlay-x', `${posX}vw`);
overlayContainer.style.setProperty('--overlay-y', `${posY}vh`);
overlayContainer.style.setProperty('--overlay-scale', scaleValue);
slidesContainer.appendChild(overlayContainer);
}
};
if (CONFIG.enableClientSideSettings) { if (CONFIG.enableClientSideSettings) {
MediaBarEnhancedSettingsManager.init(); MediaBarEnhancedSettingsManager.init();
const isClientSideEnabled = MediaBarEnhancedSettingsManager.getSetting('enabled', true); const isClientSideEnabled = MediaBarEnhancedSettingsManager.getSetting('enabled', true);
@@ -3878,6 +3980,8 @@ const slidesInit = async () => {
initArrowNavigation(); initArrowNavigation();
renderCustomOverlay();
await SlideshowManager.loadSlideshowData(); await SlideshowManager.loadSlideshowData();
SlideshowManager.initTouchEvents(); SlideshowManager.initTouchEvents();

View File

@@ -9,12 +9,12 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{ {
"version": "1.7.1.6", "version": "1.7.2.13",
"changelog": "- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- fix button issue on mobile when using ElegantFin Theme", "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", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.6/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.2.13/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "d334b961ac8c6a527dba490b5a926c40", "checksum": "60f05a0b795c860675970c51ef41a2e7",
"timestamp": "2026-03-08T18:33:15Z" "timestamp": "2026-03-11T00:13:26Z"
}, },
{ {
"version": "1.7.0.14", "version": "1.7.0.14",