Compare commits

...

126 Commits

Author SHA1 Message Date
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
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
CodeDevMLH
3da16c4c5c Bump version to 1.7.0.8
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 02:16:28 +01:00
CodeDevMLH
c7cd7be3ee Add low-power device detection and adjust video playback settings 2026-03-06 02:16:09 +01:00
CodeDevMLH
6d90523eef Update manifest.json for release v1.7.0.7 [skip ci] 2026-03-06 00:45:38 +00:00
CodeDevMLH
2a3e8057a1 Bump version to 1.7.0.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 01:44:24 +01:00
CodeDevMLH
42026b0ee8 test revert 2026-03-06 01:44:04 +01:00
CodeDevMLH
64dbc3cfd3 Update manifest.json for release v1.7.0.6 [skip ci] 2026-03-06 00:25:04 +00:00
CodeDevMLH
c998266dd7 Bump version to 1.7.0.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-03-06 01:24:11 +01:00
CodeDevMLH
9b941e5a77 test again 2026-03-06 01:23:49 +01:00
CodeDevMLH
1d70d7166d Update manifest.json for release v1.7.0.5 [skip ci] 2026-03-05 23:59:06 +00:00
CodeDevMLH
5331f0faf1 Bump version to 1.7.0.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-06 00:58:15 +01:00
CodeDevMLH
0508188705 test nochmal
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-03-06 00:57:58 +01:00
CodeDevMLH
cc861f4263 Update manifest.json for release v1.7.0.4 [skip ci] 2026-03-05 23:35:23 +00:00
CodeDevMLH
10e6cdc4a2 Bump version to 1.7.0.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-03-06 00:34:32 +01:00
CodeDevMLH
a8c7faab6b Add Safari support for YouTube video playback using plain iframe embed 2026-03-06 00:34:19 +01:00
CodeDevMLH
6df390fa18 Update manifest.json for release v1.7.0.3 [skip ci] 2026-03-05 23:23:07 +00:00
CodeDevMLH
d0c3d7ee4d Bump version to 1.7.0.3 (test 3)
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-03-06 00:22:14 +01:00
CodeDevMLH
bc621aacdf test 2026-03-06 00:21:54 +01:00
CodeDevMLH
73eb30d671 Update manifest.json for release v1.7.0.2 [skip ci] 2026-03-05 22:44:56 +00:00
CodeDevMLH
2cfbec95c9 Bump version to 1.7.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-03-05 23:44:05 +01:00
CodeDevMLH
08fc29cba3 Improve video backdrop handling: optimize autoplay logic for fresh loads and enhance Safari compatibility with loadedmetadata event 2026-03-05 23:43:52 +01:00
CodeDevMLH
0d6b835486 Update manifest.json for release v1.7.0.1 [skip ci] 2026-03-05 21:43:51 +00:00
CodeDevMLH
bf620e447f Bump version to 1.7.0.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-03-05 22:42:58 +01:00
CodeDevMLH
3117d627dd Enhance video playback handling: enable autoplay, adjust mute settings for Safari compatibility, and improve navigation checks during autoplay. 2026-03-05 22:42:40 +01:00
CodeDevMLH
71402f7e86 Update manifest.json for release v1.7.0.0 [skip ci] 2026-03-05 01:05:37 +00:00
CodeDevMLH
cce202b88d Bump version to 1.7.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-03-05 02:04:43 +01:00
CodeDevMLH
1d334e4d95 Add YouTube no-cookie host and referrer policy for iframe security 2026-03-05 02:00:54 +01:00
CodeDevMLH
142063ce63 Update configPage.html to add warning about autoplay failure when disabling start muted option 2026-03-05 02:00:19 +01:00
CodeDevMLH
1a0050ae1a Update manifest.json for release v1.6.6.4 [skip ci] 2026-02-19 17:21:41 +00:00
CodeDevMLH
46ebfdbafc Bump version to 1.6.6.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-19 18:20:41 +01:00
CodeDevMLH
14d2bb957b Remove server configuration check for plugin enablement 2026-02-19 18:20:25 +01:00
CodeDevMLH
7a0c1e4488 Enhance script injection by removing legacy tags and improving logging 2026-02-19 18:20:17 +01:00
18 changed files with 5097 additions and 4028 deletions

246
Injector_new.cs Normal file
View File

@@ -0,0 +1,246 @@
using System;
using System.Reflection;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Loader;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using MediaBrowser.Common.Configuration;
using Jellyfin.Plugin.MediaBarEnhanced.Helpers;
namespace Jellyfin.Plugin.MediaBarEnhanced
{
/// <summary>
/// Handles the injection of the MediaBarEnhanced script into the Jellyfin web interface.
/// </summary>
public class ScriptInjector
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>";
public const string CssTag = "<link rel=\"stylesheet\" href=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />";
public const string ScriptMarker = "</body>";
public const string CssMarker = "</head>";
/// <summary>
/// Initializes a new instance of the <see cref="ScriptInjector"/> class.
/// </summary>
/// <param name="appPaths">The application paths.</param>
/// <param name="logger">The logger.</param>
public ScriptInjector(IApplicationPaths appPaths, ILogger<ScriptInjector> logger)
{
_appPaths = appPaths;
_logger = logger;
}
/// <summary>
/// Injects the script tag into index.html if it's not already present.
/// </summary>
public void Inject()
{
try
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath))
{
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
RegisterFileTransformation();
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath))
{
_logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
RegisterFileTransformation();
return;
}
var content = File.ReadAllText(indexPath);
var injectedJS = false;
var injectedCSS = false;
if (!content.Contains(ScriptTag))
{
var index = content.IndexOf(ScriptMarker, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
content = content.Insert(index, ScriptTag + Environment.NewLine);
injectedJS = true;
}
}
if (!content.Contains(CssTag))
{
var index = content.IndexOf(CssMarker, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
content = content.Insert(index, CssTag + Environment.NewLine);
injectedCSS = true;
}
}
if (injectedJS && injectedCSS)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced script injected into index.html.");
} else if (injectedJS)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected.");
}
else if (injectedCSS)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected.");
}
else
{
_logger.LogInformation("MediaBarEnhanced script and CSS already present in index.html. Or could not be injected.");
}
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
RegisterFileTransformation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error injecting MediaBarEnhanced resources. Attempting fallback.");
RegisterFileTransformation();
}
}
/// <summary>
/// Removes the script tag from index.html.
/// </summary>
public void Remove()
{
UnregisterFileTransformation();
try
{
var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath))
{
return;
}
var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath))
{
return;
}
var content = File.ReadAllText(indexPath);
var modified = false;
if (content.Contains(ScriptTag))
{
content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
modified = true;
}
if (content.Contains(CssTag))
{
content = content.Replace(CssTag + Environment.NewLine, "").Replace(CssTag, "");
modified = true;
}
if (modified)
{
File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced script removed from index.html.");
} else
{
_logger.LogInformation("MediaBarEnhanced script not found in index.html. No removal necessary.");
}
}
catch (UnauthorizedAccessException uaEx)
{
_logger.LogError(uaEx, "Unauthorized access when trying to remove MediaBarEnhanced script. Check file permissions.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing MediaBarEnhanced script.");
}
}
private string? GetWebPath()
{
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
return prop?.GetValue(_appPaths) as string;
}
private void RegisterFileTransformation()
{
_logger.LogInformation("MediaBarEnhanced Fallback. Registering file transformations.");
List<JObject> payloads = new List<JObject>();
{
JObject payload = new JObject();
payload.Add("id", "0dfac9d7-d898-4944-900b-1c1837707279");
payload.Add("fileNamePattern", "index.html");
payload.Add("callbackAssembly", GetType().Assembly.FullName);
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
payloads.Add(payload);
}
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
foreach (JObject payload in payloads)
{
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
}
_logger.LogInformation("File transformations registered successfully.");
}
else
{
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
}
}
else
{
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback failed.");
}
}
private void UnregisterFileTransformation()
{
try
{
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
Guid id = Guid.Parse("0dfac9d7-d898-4944-900b-1c1837707279");
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
_logger.LogInformation("File transformation unregistered successfully.");
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
}
}
}
}

View File

@@ -0,0 +1,188 @@
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;
_imageDirectory = Path.Combine(applicationPaths.PluginConfigurationsPath, "MediaBarEnhancedAssets");
}
/// <summary>
/// Uploads a new custom overlay image.
/// </summary>
// [Microsoft.AspNetCore.Authorization.Authorize]
[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 (as extensions might differ)
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);
// 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>
// [Microsoft.AspNetCore.Authorization.Authorize]
[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
// We use FileShare.ReadWrite | FileShare.Delete so that if someone is currently overwriting the file (uploading), we don't block them.
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>
// [Microsoft.AspNetCore.Authorization.Authorize]
[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>
// [Microsoft.AspNetCore.Authorization.Authorize]
[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

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

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>
@@ -43,7 +47,8 @@
<h2 class="sectionTitle">Main Plugin Settings</h2> <h2 class="sectionTitle">Main Plugin Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="MediaBarIsEnabled" name="MediaBarIsEnabled" /> <input is="emby-checkbox" type="checkbox" id="MediaBarIsEnabled"
name="MediaBarIsEnabled" />
<span>Enable Media Bar Enhanced Plugin</span> <span>Enable Media Bar Enhanced Plugin</span>
</label> </label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div> <div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
@@ -57,23 +62,27 @@
<div class="fieldDescription">Show trailers as background if available.<br>Adds a <div class="fieldDescription">Show trailers as background if available.<br>Adds a
mute/unmute and pause/play button to control the video in the right top corner.</div> mute/unmute and pause/play button to control the video in the right top corner.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalTrailersContainer"> <div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalTrailersContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers" <input is="emby-checkbox" type="checkbox" id="PreferLocalTrailers"
name="PreferLocalTrailers" /> name="PreferLocalTrailers" />
<span>Prefer Local Trailers</span> <span>Prefer Local Trailers</span>
</label> </label>
<div class="fieldDescription">If enabled, local trailers will be preferred over remote (YouTube) trailers.</div> <div class="fieldDescription">If enabled, local trailers will be preferred over remote
(YouTube) trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription" id="PreferLocalBackdropsContainer"> <div class="checkboxContainer checkboxContainer-withDescription"
id="PreferLocalBackdropsContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops" <input is="emby-checkbox" type="checkbox" id="PreferLocalBackdrops"
name="PreferLocalBackdrops" /> name="PreferLocalBackdrops" />
<span>Prefer Local Backdrops / Theme Videos</span> <span>Prefer Local Backdrops / Theme Videos</span>
</label> </label>
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be preferred over remote and local trailers.</div> <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be
preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="WaitForTrailerToEndContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd" <input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" /> name="WaitForTrailerToEnd" />
@@ -81,7 +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" />
@@ -95,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>
@@ -111,7 +120,8 @@
<span>Enable Custom Media IDs</span> <span>Enable Custom Media IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the slideshow will show the items listed <div class="fieldDescription">If enabled, the slideshow will show the items listed
below as the default content. If the list is empty, random items from your library are used.</div> below as the default content. If the list is empty, random items from your library are
used.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -119,35 +129,47 @@
name="ApplyLimitsToCustomIds" /> name="ApplyLimitsToCustomIds" />
<span>Apply Limits to Custom IDs</span> <span>Apply Limits to Custom IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content Limits) will also apply to Custom Media IDs and Collections. By default, custom lists are not limited.</div> <div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content
Limits) will also apply to Custom Media IDs and Collections. By default, custom lists
are not limited.</div>
</div> </div>
<div id="customMediaIdsContainer"> <div id="customMediaIdsContainer">
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist IDs (Newline or Comma-separated)</label> <label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds" Media/Collection/Playlist IDs (Newline or Comma-separated)</label>
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds"
name="CustomMediaIds"
style="width: 100%; height: 150px; font-family: monospace;"></textarea> style="width: 100%; height: 150px; font-family: monospace;"></textarea>
<div class="fieldDescription">Enter the IDs of the items you want to show in the slideshow as your default content. You can separate them by new line or comma. <div class="fieldDescription">Enter the IDs of the items you want to show in the
slideshow as your default content. You can separate them by new line or comma.
<br><br> <br><br>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in <b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>. Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or
<code>ID [JellyfinItemId] DESCRIPTION</code>.
<br> <br>
Methods: Methods:
<ul> <ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li> <li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li> <li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g.
a Theme Video or Backdrop Video) using the native player.</li>
</ul> </ul>
You can also add a description after the ID using any separator like space, pipe (|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code> You can also add a description after the ID using any separator like space, pipe (|)
or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<br><br> <br><br>
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use the pipe (|) separator. <b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a
description, you <b>MUST</b> use the pipe (|) separator.
<br> <br>
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f). <b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).
</div> </div>
<p>You can find the IDs of your items in the URL of the item page in the web interface.<br> <p>You can find the IDs of your items in the URL of the item page in the web
interface.<br>
Example: Example:
<code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br> <code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br>
You can also insert a name of a collection or playlist to fetch the IDs of all items in You can also insert a name of a collection or playlist to fetch the IDs of all items
it (will take the first hit.<br><b>Note:</b> There is currently no feedback if the name in it (will take the first hit.<br><b>Note:</b> There is currently no feedback if
resolution succeeded, you will have to look if the bar displays the correct items). the name resolution succeeded, you will have to look if the bar displays the correct
items).
</p> </p>
</div> </div>
</div> </div>
@@ -160,14 +182,26 @@
name="EnableSeasonalContent" /> name="EnableSeasonalContent" />
<span>Enable Seasonal Content</span> <span>Enable Seasonal Content</span>
</label> </label>
<div class="fieldDescription">When enabled, seasonal sections below will override the default list or random selection <div class="fieldDescription">When enabled, seasonal sections below will override the
during their active date ranges. If no season matches the current date, the default Custom Media IDs above or random selection are used as fallback.</div> default list or random selection
during their active date ranges. If no season matches the current date, the default
Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<div id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ExcludeSeasonalContent"
name="ExcludeSeasonalContent" />
<span>Exclude Seasonal Content from Random Lists</span>
</label>
<div class="fieldDescription">When enabled, any items defined in your Seasonal Sections below will be explicitly excluded from being shown when the plugin pulls random items from your library.</div>
</div>
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;"> <div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i> <i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>Define seasonal rules to automatically select a selection of items based on the date. Rules are evaluated from top to bottom. The first matching rule wins.</div> <div>Define seasonal rules to automatically select a selection of items based on the
date. Rules are evaluated from top to bottom. The first matching rule wins.</div>
</div> </div>
<div id="seasonalSectionsList"></div> <div id="seasonalSectionsList"></div>
@@ -180,6 +214,106 @@
<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 securely save it to the 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>
</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>
</select>
<div class="fieldDescription">Choose a visual effect to apply to your overlay image.</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>
@@ -189,8 +323,9 @@
name="SlideAnimationEnabled" /> name="SlideAnimationEnabled" />
<span>Enable Slide Animations</span> <span>Enable Slide Animations</span>
</label> </label>
<div class="fieldDescription">Enable the zooming-in effect on background images when a new slide is <div class="fieldDescription">Enable the zooming-in effect on background images when a new
shown (does not affect trailer backdrops). Attention: This may cause performance issues on weaker client hardware.</div> slide is shown (does not affect trailer backdrops). Attention: This may cause
performance issues on weaker client hardware.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -199,7 +334,8 @@
<span>Enable Client-Side Settings</span> <span>Enable Client-Side Settings</span>
</label> </label>
<div class="fieldDescription">If enabled, users will see a media bar icon in the header to <div class="fieldDescription">If enabled, users will see a media bar icon in the header to
override settings (like disabling the bar or trailer backdrops) locally on their device.</div> override settings (like disabling the bar or trailer backdrops) locally on their device.
</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -207,7 +343,8 @@
name="RandomizeThemeVideos" /> name="RandomizeThemeVideos" />
<span>Randomize Backdrop Video</span> <span>Randomize Backdrop Video</span>
</label> </label>
<div class="fieldDescription">If enabled, a random video from the backdrops/theme videos will be selected instead of the first one (if multiple exist).</div> <div class="fieldDescription">If enabled, a random video from the backdrops/theme videos
will be selected instead of the first one (if multiple exist).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -215,18 +352,22 @@
name="RandomizeLocalTrailers" /> name="RandomizeLocalTrailers" />
<span>Randomize Local Trailer</span> <span>Randomize Local Trailer</span>
</label> </label>
<div class="fieldDescription">If enabled, a random local trailer will be selected instead of the first one (if multiple exist).</div> <div class="fieldDescription">If enabled, a random local trailer will be selected instead of
the first one (if multiple exist).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" /> <input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" />
<span>Use SponsorBlock</span> <span>Use SponsorBlock</span>
</label> </label>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is available).</div> <div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is
available).</div>
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label> <label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
<select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Auto">Auto (Smart)</option> <option value="Auto">Auto (Smart)</option>
<option value="Maximum">Maximum (4K+)</option> <option value="Maximum">Maximum (4K+)</option>
<option value="1080p">1080p</option> <option value="1080p">1080p</option>
@@ -241,7 +382,9 @@
<span>Start Muted</span> <span>Start Muted</span>
</label> </label>
<div class="fieldDescription">Start trailer video playback muted. (Known issue: In the <div class="fieldDescription">Start trailer video playback muted. (Known issue: In the
Android/IOS app, backdrop trailers are always muted.)</div> Android/IOS app, backdrop trailers are always muted.)<br>
<b style="color:#ffcc00">Warning:</b> Disabling this may cause autoplay to fail on
certain browsers due to strict autoplay policies.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -260,16 +403,23 @@
<span>Enable Loading Screen</span> <span>Enable Loading Screen</span>
</label> </label>
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You <div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
may have to reload the page twice)</div> may have to reload the page twice (after changing this setting) to take effect)</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows" <input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" /> name="AlwaysShowArrows" />
<span>Always Show Arrows</span> <span>Always Show Arrow Navigation Buttons</span>
</label> </label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead <div class="fieldDescription">Force the UI arrow navigation buttons to always be visible instead of only when hovered.</div>
of only on hover.</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="HideArrowsOnMobile"
name="HideArrowsOnMobile" />
<span>Hide Arrows on Mobile</span>
</label>
<div class="fieldDescription">Completely disable the navigation arrows on mobile devices (since swiping is available).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -281,6 +431,7 @@
Space (pause), M (mute/unmute)) for Space (pause), M (mute/unmute)) for
the slideshow.</div> the slideshow.</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Time Settings</h2> <h2 class="sectionTitle">Time Settings</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -324,11 +475,14 @@
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for <div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</div> mobile).</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Sorting</h2> <h2 class="sectionTitle">Content Sorting and Filtering</h2>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortBy">Sort By</label> <label class="selectLabel" for="SortBy">Sort By</label>
<select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="SortBy" name="SortBy"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Random">Random</option> <option value="Random">Random</option>
<option value="Original">Original (Custom List Order)</option> <option value="Original">Original (Custom List Order)</option>
<option value="PremiereDate">Premiere Date</option> <option value="PremiereDate">Premiere Date</option>
@@ -342,15 +496,38 @@
</div> </div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortOrder">Sort Order</label> <label class="selectLabel" for="SortOrder">Sort Order</label>
<select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select" style="width: 100%; -webkit-appearance: menulist; appearance: menulist;"> <select is="emby-select" id="SortOrder" name="SortOrder"
class="selectLayout emby-select-withcolor emby-select"
style="width: 100%; -webkit-appearance: menulist; appearance: menulist;">
<option value="Ascending">Ascending</option> <option value="Ascending">Ascending</option>
<option value="Descending">Descending</option> <option value="Descending">Descending</option>
</select> </select>
<div class="fieldDescription">Sort items in Ascending or Descending order.</div> <div class="fieldDescription">Sort items in Ascending or Descending order.</div>
</div> </div>
<div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;"> <div class="fieldDescription" style="margin-bottom: 2em; color: #ffcc00;">
<b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original' preserves Custom List order. <b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original'
preserves Custom List order.
</div> </div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxParentalRating">Max Parental Rating (Age Limit)</label>
<input is="emby-input" type="number" id="MaxParentalRating" name="MaxParentalRating" />
<div class="fieldDescription">Items exceeding this age rating will not be shown. Leave blank or set to 0 for no limit. Examples: 12, 16, 18.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxDaysRecent">Max Days Recent</label>
<input is="emby-input" type="number" id="MaxDaysRecent" name="MaxDaysRecent" />
<div class="fieldDescription">Only show items added in the last X days. Leave blank or set to 0 for no limit. Example: 30.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Limits</h2> <h2 class="sectionTitle">Content Limits</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -377,6 +554,15 @@
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" /> <input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of slides to preload.</div> <div class="fieldDescription">Number of slides to preload.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ShowPaginationDots"
name="ShowPaginationDots" />
<span>Show Pagination Dots/Counter</span>
</label>
<div class="fieldDescription">Show or hide the pagination dots/counter navigation at the
bottom of the slideshow.</div>
</div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination <label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination
Dots</label> Dots</label>
@@ -391,14 +577,6 @@
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" /> <input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</div> <div class="fieldDescription">Maximum characters for the plot summary.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random selection results.</div>
</div>
</div> </div>
<div <div
@@ -459,7 +637,10 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent' 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -506,25 +687,31 @@
var addSeasonBtn = page.querySelector('#addSeasonBtn'); var addSeasonBtn = page.querySelector('#addSeasonBtn');
if (addSeasonBtn) { if (addSeasonBtn) {
// Remove existing listeners to avoid duplicates if re-attached // Remove existing listeners to avoid duplicates if re-attached
var newBtn = addSeasonBtn.cloneNode(true); var newBtn = addSeasonBtn.cloneNode(true);
addSeasonBtn.parentNode.replaceChild(newBtn, addSeasonBtn); addSeasonBtn.parentNode.replaceChild(newBtn, addSeasonBtn);
newBtn.addEventListener('click', function() { newBtn.addEventListener('click', function () {
MediaBarEnhancedConfigurationPage.addSeasonalSection(page); MediaBarEnhancedConfigurationPage.addSeasonalSection(page);
}); });
} }
// Handle Prefer Local Trailers visibility // Handle Prefer Local Trailers visibility
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';
} }
} }
@@ -533,6 +720,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 = 'block';
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();
}); });
}, },
@@ -563,7 +881,10 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent' 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -582,16 +903,16 @@
}); });
}, },
renderSeasonalSections: function(page, sections) { renderSeasonalSections: function (page, sections) {
var container = page.querySelector('#seasonalSectionsList'); var container = page.querySelector('#seasonalSectionsList');
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
sections.forEach(function(section, index) { sections.forEach(function (section, index) {
MediaBarEnhancedConfigurationPage.createSectionElement(container, section, index + 1); MediaBarEnhancedConfigurationPage.createSectionElement(container, section, index + 1);
}); });
}, },
addSeasonalSection: function(page) { addSeasonalSection: function (page) {
var container = page.querySelector('#seasonalSectionsList'); var container = page.querySelector('#seasonalSectionsList');
if (!container) return; if (!container) return;
var index = container.children.length + 1; var index = container.children.length + 1;
@@ -599,29 +920,31 @@
Name: 'New Season', Name: 'New Season',
StartDay: 1, StartMonth: 1, StartDay: 1, StartMonth: 1,
EndDay: 1, EndMonth: 1, EndDay: 1, EndMonth: 1,
MediaIds: '' MediaIds: '',
OverlayText: '',
OverlayImageUrl: ''
}, index); }, index);
}, },
createSectionElement: function(container, data, index) { createSectionElement: function (container, data, index) {
var div = document.createElement('div'); var div = document.createElement('div');
div.className = 'seasonal-section'; 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);'; 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 = []; var days = [];
for(var i=1; i<=31; i++) days.push(i); for (var i = 1; i <= 31; i++) days.push(i);
var months = [ var months = [
{v:1, n:'Jan'}, {v:2, n:'Feb'}, {v:3, n:'Mar'}, {v:4, n:'Apr'}, { 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: 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: 9, n: 'Sep' }, { v: 10, n: 'Oct' }, { v: 11, n: 'Nov' }, { v: 12, n: 'Dec' }
]; ];
function mkSelect(val, opts, cls) { 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;">'; 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 v = o.v || o;
var n = o.n || 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>'; h += '</select>';
return h; return h;
@@ -648,11 +971,11 @@
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' + ' <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;">' + ' <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 0.5em;">' +
' <span>From:</span>' + ' <span>From:</span>' +
mkSelect(data.StartDay, days, 'start-day') + mkSelect(data.StartDay, days, 'start-day') +
mkSelect(data.StartMonth, months, 'start-month') + mkSelect(data.StartMonth, months, 'start-month') +
' <span style="margin-left: 1em;">To:</span>' + ' <span style="margin-left: 1em;">To:</span>' +
mkSelect(data.EndDay, days, 'end-day') + mkSelect(data.EndDay, days, 'end-day') +
mkSelect(data.EndMonth, months, 'end-month') + mkSelect(data.EndMonth, months, 'end-month') +
' </div>' + ' </div>' +
' <div class="fieldDescription">Date range (inclusive) when this content is active.</div>' + ' <div class="fieldDescription">Date range (inclusive) when this content is active.</div>' +
'</div>' + '</div>' +
@@ -660,33 +983,234 @@
' <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: 80px; 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" title="Clear Image" style="display: none; position: absolute; top: 0px; right: 0px; z-index: 3; 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 () {
div.remove(); div.remove();
MediaBarEnhancedConfigurationPage.updateSectionTitles(container); MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}); });
div.querySelector('.btn-move-up').addEventListener('click', function() { div.querySelector('.btn-move-up').addEventListener('click', function () {
if (div.previousElementSibling) { if (div.previousElementSibling) {
container.insertBefore(div, div.previousElementSibling); container.insertBefore(div, div.previousElementSibling);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container); MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
} }
}); });
div.querySelector('.btn-move-down').addEventListener('click', function() { div.querySelector('.btn-move-down').addEventListener('click', function () {
if (div.nextElementSibling) { if (div.nextElementSibling) {
container.insertBefore(div.nextElementSibling, div); container.insertBefore(div.nextElementSibling, div);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container); 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 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 = 'block';
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);
}, },
updateSectionTitles: function(container) { updateSectionTitles: function (container) {
var sections = container.querySelectorAll('.seasonal-section'); var sections = container.querySelectorAll('.seasonal-section');
sections.forEach(function(section, index) { sections.forEach(function (section, index) {
var title = section.querySelector('.section-title'); var title = section.querySelector('.section-title');
if (title) { if (title) {
title.innerText = 'Season list #' + (index + 1); title.innerText = 'Season list #' + (index + 1);
@@ -694,17 +1218,19 @@
}); });
}, },
getSeasonalSectionsFromUI: function(page) { getSeasonalSectionsFromUI: function (page) {
var sections = []; var sections = [];
var els = page.querySelectorAll('.seasonal-section'); var els = page.querySelectorAll('.seasonal-section');
els.forEach(function(el) { els.forEach(function (el) {
sections.push({ sections.push({
Name: el.querySelector('.section-name').value, Name: el.querySelector('.section-name').value,
StartDay: parseInt(el.querySelector('.start-day').value), StartDay: parseInt(el.querySelector('.start-day').value),
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.6.6.3</Version> <Version>1.7.2.5</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

@@ -60,6 +60,11 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
var injectedJS = false; var injectedJS = false;
var injectedCSS = false; var injectedCSS = false;
var modified = false;
// Cleanup legacy tags first to avoid duplicates or conflicts
content = RemoveLegacyTags(content, ref modified);
if (!content.Contains(ScriptTag)) if (!content.Contains(ScriptTag))
{ {
@@ -81,19 +86,26 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
} }
} }
if (injectedJS && injectedCSS) if (injectedJS || injectedCSS || modified)
{ {
File.WriteAllText(indexPath, content); File.WriteAllText(indexPath, content);
_logger.LogInformation("MediaBarEnhanced script injected into index.html.");
} else if (injectedJS) if (injectedJS && injectedCSS)
{ {
File.WriteAllText(indexPath, content); _logger.LogInformation("MediaBarEnhanced script injected into index.html.");
_logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected."); }
} else if (injectedJS)
else if (injectedCSS) {
{ _logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected.");
File.WriteAllText(indexPath, content); }
_logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected."); else if (injectedCSS)
{
_logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected.");
}
else
{
_logger.LogInformation("MediaBarEnhanced script and CSS already present. Legacy tags removed if found.");
}
} }
else else
{ {
@@ -148,6 +160,9 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
modified = true; modified = true;
} }
// Remove legacy tags
content = RemoveLegacyTags(content, ref modified);
if (modified) if (modified)
{ {
File.WriteAllText(indexPath, content); File.WriteAllText(indexPath, content);
@@ -242,5 +257,33 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered."); _logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
} }
} }
/// <summary>
/// Removes legacy script and css tags from the content.
/// </summary>
/// <param name="content">The file content.</param>
/// <param name="modified">Ref bool to track if changes were made.</param>
/// <returns>The modified content.</returns>
private string RemoveLegacyTags(string content, ref bool modified)
{
// Legacy tags (used in versions prior to 1.6.3.0 where paths started with / instead of ../)
const string LegacyScriptTag = "<script src=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>";
const string LegacyCssTag = "<link rel=\"stylesheet\" href=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />";
if (content.Contains(LegacyScriptTag))
{
content = content.Replace(LegacyScriptTag + Environment.NewLine, "").Replace(LegacyScriptTag, "");
modified = true;
_logger.LogInformation("Legacy MediaBarEnhanced script tag removed.");
}
if (content.Contains(LegacyCssTag))
{
content = content.Replace(LegacyCssTag + Environment.NewLine, "").Replace(LegacyCssTag, "");
modified = true;
_logger.LogInformation("Legacy MediaBarEnhanced CSS tag removed.");
}
return content;
}
} }
} }

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 {
@@ -768,7 +772,22 @@
.button-container { .button-container {
top: calc(50% + 25vh); top: calc(50% + 25vh);
left: 50%; left: 50%;
transform: translateX(-50%) scale(0.95); transform: translateX(-50%);
width: max-content;
max-width: 98vw;
flex-wrap: nowrap;
justify-content: center;
gap: 15px;
}
.button-container button {
margin: 0 !important;
min-width: 0 !important;
}
.button-container .detail-button,
.button-container .favorite-button {
flex-shrink: 0;
} }
.logo { .logo {
@@ -825,10 +844,6 @@
.genre { .genre {
top: calc(50% + 16vh); top: calc(50% + 16vh);
} }
.button-container {
top: calc(50% + 27vh);
}
} }
} }
@@ -997,6 +1012,397 @@
margin: 0; margin: 0;
} }
.layout-tv .backdrop-container{ .layout-tv .backdrop-container {
top: -5%; top: -5%;
} }
@media screen and (min-width: 960px) and (-webkit-device-pixel-ratio: 1) {
.layout-tv .backdrop.animate {
animation: none !important;
}
.layout-tv .logo.animate {
animation: none !important;
}
.layout-tv .slide-counter,
.layout-tv .dots-container {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}
/* 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;
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: 8%; /* oder 5vh? */
left: 50%;
transform: translateX(-50%);
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.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;
}
}
.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); }
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 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); }
}

File diff suppressed because it is too large Load Diff

2911
configPageExample.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,28 @@
"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.6.6.3", "version": "1.7.2.5",
"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.2.5/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "d464b8ce83fe85018623c3de17ec5c1c",
"timestamp": "2026-03-10T19:47:06Z"
},
{
"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",
"changelog": "- feat: add static backdrop also for video backdrops\n- fix: renaming issue of settings (avoiding conflict with other plugins)", "changelog": "- feat: add static backdrop also for video backdrops\n- fix: renaming issue of settings (avoiding conflict with other plugins)",
"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.6.6.3/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.6.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "7841bf8916ae070449b04783960dacca", "checksum": "2c55cf9687e44b04a0824997e2980dc9",
"timestamp": "2026-02-19T15:50:04Z" "timestamp": "2026-02-19T17:21:40Z"
}, },
{ {
"version": "1.6.5.2", "version": "1.6.5.2",

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