Compare commits

..

26 Commits

Author SHA1 Message Date
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
6 changed files with 319 additions and 101 deletions

View File

@@ -36,6 +36,7 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool EnableLoadingScreen { get; set; } = true; public bool EnableLoadingScreen { get; set; } = true;
public bool EnableKeyboardControls { get; set; } = true; public bool EnableKeyboardControls { get; set; } = true;
public bool AlwaysShowArrows { get; set; } = false; public bool AlwaysShowArrows { get; set; } = false;
public bool HideArrowsOnMobile { get; set; } = true;
public string CustomMediaIds { get; set; } = ""; public string CustomMediaIds { get; set; } = "";
public bool EnableCustomMediaIds { get; set; } = true; public bool EnableCustomMediaIds { get; set; } = true;
public string PreferredVideoQuality { get; set; } = "Auto"; public string PreferredVideoQuality { get; set; } = "Auto";
@@ -48,5 +49,9 @@ 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; } = "";
} }
} }

View File

@@ -78,7 +78,7 @@
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be
preferred over remote and local trailers.</div> preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="WaitForTrailerToEndContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd" <input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" /> name="WaitForTrailerToEnd" />
@@ -86,7 +86,7 @@
</label> </label>
<div class="fieldDescription">Delay slide transition until trailer finishes.</div> <div class="fieldDescription">Delay slide transition until trailer finishes.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="EnableMobileVideoContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo" <input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
name="EnableMobileVideo" /> name="EnableMobileVideo" />
@@ -100,8 +100,8 @@
name="ShowTrailerButton" /> name="ShowTrailerButton" />
<span>Show Trailer Button</span> <span>Show Trailer Button</span>
</label> </label>
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if <div class="fieldDescription">Display a button to open trailer in modal. Button only
trailer is not set as backdrop or if no trailer is available.</div> visible if trailer is not set as backdrop.</div>
</div> </div>
</div> </div>
@@ -183,15 +183,15 @@
during their active date ranges. If no season matches the current date, the default during their active date ranges. If no season matches the current date, the default
Custom Media IDs above or random selection are used as fallback.</div> Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ExcludeSeasonalContent"
name="ExcludeSeasonalContent" />
<span>Exclude Seasonal Content from Random Lists</span>
</label>
<div class="fieldDescription">When enabled, any items defined in your Seasonal Sections below will be explicitly excluded from being shown when the plugin pulls random items from your library.</div>
</div>
<div id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ExcludeSeasonalContent"
name="ExcludeSeasonalContent" />
<span>Exclude Seasonal Content from Random Lists</span>
</label>
<div class="fieldDescription">When enabled, any items defined in your Seasonal Sections below will be explicitly excluded from being shown when the plugin pulls random items from your library.</div>
</div>
<div <div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;"> style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
@@ -212,6 +212,29 @@
<!-- 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">Custom Slideshow Overlay</h2>
<p>Inject a custom text or floating image over the slideshow. This can be overridden by specific Seasonal Sections.</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="inputContainer">
<input is="emby-input" type="text" id="CustomOverlayText" name="CustomOverlayText" />
<div class="fieldDescription">Text to display on the overlay (e.g. "Movie Night!"). Leave blank to use an image instead.</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="text" id="CustomOverlayImageUrl" name="CustomOverlayImageUrl" />
<div class="fieldDescription">Absolute URL to an image to display on the overlay. If provided, this overrides the text.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Features</h2> <h2 class="sectionTitle">Features</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -299,16 +322,23 @@
<span>Enable Loading Screen</span> <span>Enable Loading Screen</span>
</label> </label>
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You <div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
may have to reload the page twice)</div> may have to reload the page twice (after changing this setting) to take effect)</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows" <input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" /> name="AlwaysShowArrows" />
<span>Always Show Arrows</span> <span>Always Show Arrow Navigation Buttons</span>
</label> </label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead <div class="fieldDescription">Force the UI arrow navigation buttons to always be visible instead of only when hovered.</div>
of only on hover.</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="HideArrowsOnMobile"
name="HideArrowsOnMobile" />
<span>Hide Arrows on Mobile</span>
</label>
<div class="fieldDescription">Completely disable the navigation arrows on mobile devices (since swiping is available).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -320,6 +350,7 @@
Space (pause), M (mute/unmute)) for Space (pause), M (mute/unmute)) for
the slideshow.</div> the slideshow.</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Time Settings</h2> <h2 class="sectionTitle">Time Settings</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -363,6 +394,7 @@
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for <div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</div> mobile).</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Sorting and Filtering</h2> <h2 class="sectionTitle">Content Sorting and Filtering</h2>
<div class="selectContainer"> <div class="selectContainer">
@@ -405,6 +437,16 @@
<input is="emby-input" type="number" id="MaxDaysRecent" name="MaxDaysRecent" /> <input is="emby-input" type="number" id="MaxDaysRecent" name="MaxDaysRecent" />
<div class="fieldDescription">Only show items added in the last X days. Leave blank or set to 0 for no limit. Example: 30.</div> <div class="fieldDescription">Only show items added in the last X days. Leave blank or set to 0 for no limit. Example: 30.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Limits</h2> <h2 class="sectionTitle">Content Limits</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -435,7 +477,7 @@
<label> <label>
<input is="emby-checkbox" type="checkbox" id="ShowPaginationDots" <input is="emby-checkbox" type="checkbox" id="ShowPaginationDots"
name="ShowPaginationDots" /> name="ShowPaginationDots" />
<span>Show Pagination Dots</span> <span>Show Pagination Dots/Counter</span>
</label> </label>
<div class="fieldDescription">Show or hide the pagination dots/counter navigation at the <div class="fieldDescription">Show or hide the pagination dots/counter navigation at the
bottom of the slideshow.</div> bottom of the slideshow.</div>
@@ -454,15 +496,6 @@
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" /> <input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</div> <div class="fieldDescription">Maximum characters for the plot summary.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
</div> </div>
<div <div
@@ -524,7 +557,8 @@
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating', 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent' 'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -582,14 +616,20 @@
var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop'); var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop');
var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer'); var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer');
var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer'); var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer');
var waitForTrailerContainer = page.querySelector('#WaitForTrailerToEndContainer');
var enableMobileVideoContainer = page.querySelector('#EnableMobileVideoContainer');
function updatePreferLocalVisibility() { function updatePreferLocalVisibility() {
if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) { if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) {
if (preferLocalContainer) preferLocalContainer.style.display = 'block'; if (preferLocalContainer) preferLocalContainer.style.display = 'block';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'block';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'block';
} else { } else {
if (preferLocalContainer) preferLocalContainer.style.display = 'none'; if (preferLocalContainer) preferLocalContainer.style.display = 'none';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'none';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'none';
} }
} }
@@ -629,7 +669,8 @@
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating', 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent' 'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -665,7 +706,9 @@
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);
}, },
@@ -726,6 +769,14 @@
' <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>' +
'<div class="inputContainer" style="margin-top: 1em;">' +
' <input is="emby-input" type="text" class="emby-input section-overlay-text" style="width: 100%;" value="' + (data.OverlayText ? data.OverlayText.replace(/"/g, '&quot;') : '') + '" placeholder="Seasonal Custom Overlay Text (e.g. Oscars Time!)" />' +
' <div class="fieldDescription">Optional: Override the global custom overlay text during this season.</div>' +
'</div>' +
'<div class="inputContainer">' +
' <input is="emby-input" type="text" class="emby-input section-overlay-image" style="width: 100%;" value="' + (data.OverlayImageUrl ? data.OverlayImageUrl.replace(/"/g, '&quot;') : '') + '" placeholder="Seasonal Custom Overlay Image URL" />' +
' <div class="fieldDescription">Optional: Override the global custom overlay image during this season. Overrides the text if provided.</div>' +
'</div>'; '</div>';
div.querySelector('.btn-remove').addEventListener('click', function () { div.querySelector('.btn-remove').addEventListener('click', function () {
@@ -770,7 +821,9 @@
StartMonth: parseInt(el.querySelector('.start-month').value), StartMonth: parseInt(el.querySelector('.start-month').value),
EndDay: parseInt(el.querySelector('.end-day').value), EndDay: parseInt(el.querySelector('.end-day').value),
EndMonth: parseInt(el.querySelector('.end-month').value), EndMonth: parseInt(el.querySelector('.end-month').value),
MediaIds: el.querySelector('.section-ids').value MediaIds: el.querySelector('.section-ids').value,
OverlayText: el.querySelector('.section-overlay-text').value,
OverlayImageUrl: el.querySelector('.section-overlay-image').value
}); });
}); });
return sections; return sections;

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Media Bar Enhanced Plugin</Title> <Title>Jellyfin Media Bar Enhanced Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.7.1.6</Version> <Version>1.7.1.14</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

@@ -354,13 +354,13 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.backdrop-container.full-width-video { .backdrop-container.full-width-video {
@@ -384,13 +384,13 @@
border-radius: 5px; border-radius: 5px;
z-index: 3; z-index: 3;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.backdrop-overlay { .backdrop-overlay {
@@ -403,13 +403,13 @@
border-radius: 5px; border-radius: 5px;
z-index: 4; z-index: 4;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
} }
.gradient-overlay { .gradient-overlay {
@@ -424,13 +424,13 @@
rgba(29, 29, 29, 0) 100%); rgba(29, 29, 29, 0) 100%);
z-index: 4; z-index: 4;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 4%, rgba(0, 0, 0, 0.5) 4%,
#000000 6%); #000000 6%);
} }
.gradient-overlay.full-width-video { .gradient-overlay.full-width-video {
@@ -525,6 +525,8 @@
font-family: "Archivo Narrow", sans-serif; font-family: "Archivo Narrow", sans-serif;
font-size: 18px; font-size: 18px;
white-space: nowrap; white-space: nowrap;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
font-weight: 700; font-weight: 700;
@@ -535,6 +537,7 @@
.detail-button { .detail-button {
font-size: 18px; font-size: 18px;
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0); color: rgb(0, 0, 0);
border-radius: 50%; border-radius: 50%;
height: 50px; height: 50px;
@@ -547,6 +550,7 @@
.favorite-button { .favorite-button {
font-size: 18px; font-size: 18px;
background-color: rgb(255, 255, 255);
color: red; color: red;
border-radius: 50%; border-radius: 50%;
height: 50px; height: 50px;
@@ -662,7 +666,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 5px; border-radius: 5px;
background: rgb(255 255 255 / 0.8); background: rgba(255, 255, 255, 0.8);
color: #000; color: #000;
border: none; border: none;
font-weight: 600; font-weight: 600;
@@ -712,13 +716,13 @@
object-position: center 20%; object-position: center 20%;
z-index: 3; z-index: 3;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.gradient-overlay { .gradient-overlay {
@@ -727,17 +731,17 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgb(0 0 0 / 0.25); background: rgba(0, 0, 0, 0.25);
z-index: 4; z-index: 4;
pointer-events: none; pointer-events: none;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
-webkit-mask-image: linear-gradient(to top, -webkit-mask-image: linear-gradient(to top,
#fff0 2%, rgba(255, 255, 255, 0) 2%,
rgb(0 0 0 / 0.5) 6%, rgba(0, 0, 0, 0.5) 6%,
#000000 8%); #000000 8%);
} }
.dots-container { .dots-container {
@@ -1008,7 +1012,7 @@
margin: 0; margin: 0;
} }
.layout-tv .backdrop-container{ .layout-tv .backdrop-container {
top: -5%; top: -5%;
} }
@@ -1027,3 +1031,75 @@
-webkit-backdrop-filter: none; -webkit-backdrop-filter: none;
} }
} }
/* Floating Custom Overlay Styling */
.custom-overlay-container {
position: absolute;
top: 8vh;
left: 4vw;
z-index: 15;
display: flex;
align-items: center;
justify-content: flex-start;
pointer-events: none; /* Let clicks pass through to the slider */
animation: fadeInOverlay 1.5s ease-in-out forwards;
}
.custom-overlay-text {
font-family: "Archivo Narrow", sans-serif;
color: #fff;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.8), -1px -1px 4px rgba(0, 0, 0, 0.5);
margin: 0;
letter-spacing: 1px;
}
.custom-overlay-image {
max-width: 300px;
max-height: 120px;
object-fit: contain;
filter: drop-shadow(2px 4px 6px rgba(0,0,0,0.5));
}
@keyframes fadeInOverlay {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Make it smaller on mobile portrait */
@media only screen and (max-width: 767px) and (orientation: portrait) {
.custom-overlay-container {
top: 5vh;
left: 50%;
transform: translateX(-50%);
width: 90%;
justify-content: center;
text-align: center;
}
.custom-overlay-text {
font-size: 1.8rem;
}
.custom-overlay-image {
max-width: 200px;
max-height: 80px;
}
@keyframes fadeInOverlay {
from {
opacity: 0;
transform: translate(-50%, -10px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
}

View File

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

View File

@@ -9,12 +9,12 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{ {
"version": "1.7.1.6", "version": "1.7.1.14",
"changelog": "- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- fix button issue on mobile when using ElegantFin Theme", "changelog": "- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- Add hide arrows on mobile option \n- fix button issue on mobile when using ElegantFin Theme",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.6/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.14/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "d334b961ac8c6a527dba490b5a926c40", "checksum": "d4a115b5e3fd192572e21be8e95c55a7",
"timestamp": "2026-03-08T18:33:15Z" "timestamp": "2026-03-09T01:29:55Z"
}, },
{ {
"version": "1.7.0.14", "version": "1.7.0.14",