Compare commits

...

73 Commits

Author SHA1 Message Date
CodeDevMLH
773c49a228 Update manifest.json for release v1.6.1.25 [skip ci] 2026-02-14 02:21:15 +00:00
CodeDevMLH
41a309e0d1 Bump version to 1.6.1.25
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-14 03:20:25 +01:00
CodeDevMLH
43797fbb98 Refactor video playback handling and improve tab visibility management 2026-02-14 03:20:04 +01:00
CodeDevMLH
f13a1ba1af test [skip ci] 2026-02-14 02:18:13 +01:00
CodeDevMLH
d489c22f28 Update manifest.json for release v1.6.1.24 [skip ci] 2026-02-14 01:12:45 +00:00
CodeDevMLH
7816c87543 Bump version to 1.6.1.24
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m19s
2026-02-14 02:11:09 +01:00
CodeDevMLH
720567bafc Enhance video playback logic with improved state handling and retry mechanism for YouTube players 2026-02-14 02:10:55 +01:00
CodeDevMLH
2289a1f83e Update manifest.json for release v1.6.1.23 [skip ci] 2026-02-14 00:58:27 +00:00
CodeDevMLH
a269318f58 Bump version to 1.6.1.23
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-14 01:57:36 +01:00
CodeDevMLH
fdb409fd3b Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced 2026-02-14 01:57:18 +01:00
CodeDevMLH
9bb4b9d355 Refactor video playback logic to improve handling of active slides and paused state 2026-02-14 01:57:14 +01:00
CodeDevMLH
1e18c22937 Update manifest.json for release v1.6.1.22 [skip ci] 2026-02-14 00:43:45 +00:00
CodeDevMLH
a83913d15c Bump version to 1.6.1.22
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-14 01:42:53 +01:00
CodeDevMLH
2f50931beb Fix YouTube player readiness checks and improve polling logic for video playback 2026-02-14 01:42:42 +01:00
CodeDevMLH
5b14bdba35 Update manifest.json for release v1.6.1.21 [skip ci] 2026-02-14 00:35:03 +00:00
CodeDevMLH
9ba3b1e49f Bump version to 1.6.1.21
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-14 01:34:12 +01:00
CodeDevMLH
bf7c7fb8e8 Enhance video backdrop handling to support YouTube iframe integration and improve video playback logic 2026-02-14 01:33:54 +01:00
CodeDevMLH
39e29046de Update manifest.json for release v1.6.1.20 [skip ci] 2026-02-14 00:10:58 +00:00
CodeDevMLH
18260f8eac Bump version to 1.6.1.20
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-14 01:10:09 +01:00
CodeDevMLH
59c07f3c45 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-02-14 01:09:50 +01:00
CodeDevMLH
b06d1e9375 Enhance video playback logic to handle pending play state and improve pause behavior for video backdrops 2026-02-14 01:09:48 +01:00
CodeDevMLH
e5bf23a7bc Enhance README with detailed configuration options for Custom Media IDs, Content Sorting, and Content Limits 2026-02-14 01:09:29 +01:00
CodeDevMLH
0d7113969b Improve clarity and consistency in configuration page labels and descriptions 2026-02-14 01:09:16 +01:00
CodeDevMLH
f69f676a68 Update manifest.json for release v1.6.1.19 [skip ci] 2026-02-13 18:44:00 +00:00
CodeDevMLH
f448c89ef2 Bump version to 1.6.1.19
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-13 19:43:04 +01:00
CodeDevMLH
daf26fe53a Refactor slide playback logic to remove redundant checks and improve clarity 2026-02-13 19:42:49 +01:00
CodeDevMLH
26ef307838 Update manifest.json for release v1.6.1.18 [skip ci] 2026-02-13 18:35:24 +00:00
CodeDevMLH
c296483583 Bump version to 1.6.1.18
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-13 19:34:31 +01:00
CodeDevMLH
7992e20715 Refactor slide playback logic to improve active slide checks and remove commented code 2026-02-13 19:34:13 +01:00
CodeDevMLH
1ae59f5da5 Update manifest.json for release v1.6.1.17 [skip ci] 2026-02-13 13:49:23 +00:00
CodeDevMLH
92eaf91173 Bump version to 1.6.1.17
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m2s
2026-02-13 14:48:24 +01:00
CodeDevMLH
e7410ec22a Fix slideshow preloading logic to prevent redundant slide creation 2026-02-13 14:48:07 +01:00
CodeDevMLH
bb43d1e679 Update manifest.json for release v1.6.1.16 [skip ci] 2026-02-13 02:53:20 +00:00
CodeDevMLH
b6609d23a2 Bump version to 1.6.1.16
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-13 03:52:27 +01:00
CodeDevMLH
9d4cbf37d3 Refactor slideshow state management and improve video playback logic 2026-02-13 03:52:07 +01:00
CodeDevMLH
b5e63ef3b7 Update manifest.json for release v1.6.1.15 [skip ci] 2026-02-13 02:41:24 +00:00
CodeDevMLH
22f9906188 Bump version to 1.6.1.15
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 03:40:35 +01:00
CodeDevMLH
ae54ab41a8 Refactor video playback logic to simplify conditions for active slides 2026-02-13 03:40:23 +01:00
CodeDevMLH
9663ab78d2 Update manifest.json for release v1.6.1.14 [skip ci] 2026-02-13 02:23:29 +00:00
CodeDevMLH
f633e4273f Bump version to 1.6.1.14
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 03:22:39 +01:00
CodeDevMLH
c0895fd8d7 Fix slideshow current slide index update logic 2026-02-13 03:22:21 +01:00
CodeDevMLH
002ccdb08b Enhance label styling and adjust layout in configuration form 2026-02-13 03:11:53 +01:00
CodeDevMLH
7cb03410ee Update manifest.json for release v1.6.1.13 [skip ci] 2026-02-13 02:02:19 +00:00
CodeDevMLH
17c8681e93 Bump version to 1.6.1.13 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 03:01:29 +01:00
CodeDevMLH
3a4c663c0e Enhance configuration layout and improve seasonal section handling 2026-02-13 03:01:17 +01:00
CodeDevMLH
3385196611 Update manifest.json for release v1.6.1.12 [skip ci] 2026-02-13 01:41:56 +00:00
CodeDevMLH
2538556f7c Bump version to 1.6.1.12
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-13 02:41:06 +01:00
CodeDevMLH
550ebed942 Update manifest.json for release v1.6.1.11 [skip ci] 2026-02-13 01:19:07 +00:00
CodeDevMLH
21d55711d4 Enhance configuration layout for season name input and update select appearance
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-13 02:18:18 +01:00
CodeDevMLH
81a0d375be Update manifest.json for release v1.6.1.11 [skip ci] 2026-02-13 01:12:47 +00:00
CodeDevMLH
23cbc0a85a Bump version to 1.6.1.11
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 02:11:58 +01:00
CodeDevMLH
2de066cca8 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced 2026-02-13 02:11:46 +01:00
CodeDevMLH
138e50ff15 Enhance configuration layout and add disablePictureInPicture option for video playback 2026-02-13 02:11:20 +01:00
CodeDevMLH
bf72dc08a3 Update manifest.json for release v1.6.1.10 [skip ci] 2026-02-13 00:41:36 +00:00
CodeDevMLH
65a63b4aa0 Bump version to 1.6.1.10
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 01:40:48 +01:00
CodeDevMLH
a1df756c56 Enhance seasonal section input fields with improved layout and descriptions 2026-02-13 01:40:33 +01:00
CodeDevMLH
f2d383ec61 Refactor video playback logic in SlideshowManager for improved fallback handling 2026-02-13 01:36:26 +01:00
CodeDevMLH
b85f52d8d3 Update manifest.json for release v1.6.1.9 [skip ci] 2026-02-13 00:16:40 +00:00
CodeDevMLH
ad18acb011 Bump version to 1.6.1.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-13 01:15:47 +01:00
CodeDevMLH
2ae147ac01 Add seasonal content support and enhance custom media ID handling 2026-02-13 01:15:33 +01:00
CodeDevMLH
9896044988 Update manifest.json for release v1.6.1.8 [skip ci] 2026-02-12 23:51:47 +00:00
CodeDevMLH
93e91e2e60 Bump version to 1.6.1.8 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 58s
2026-02-13 00:50:52 +01:00
CodeDevMLH
b613b028d0 Add configuration options for custom ID limits 2026-02-13 00:50:34 +01:00
CodeDevMLH
9906784845 Enhance visibility change handling to manage video playback more effectively when tab visibility changes 2026-02-12 21:35:29 +01:00
CodeDevMLH
009a3c4720 Update manifest.json for release v1.6.1.7 [skip ci] 2026-02-12 15:57:17 +00:00
CodeDevMLH
595056230a Bump version to 1.6.1.7 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 59s
2026-02-12 16:54:36 +01:00
CodeDevMLH
b18060dfd7 Refactor visibility observer initialization and remove debounce logic 2026-02-12 16:54:20 +01:00
CodeDevMLH
ebb2af9d24 Update manifest.json for release v1.6.1.6 [skip ci] 2026-02-12 02:03:10 +00:00
CodeDevMLH
743af20b8e Bump version to 1.6.1.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-12 03:02:17 +01:00
CodeDevMLH
9844b186d7 Enhance focus management in TV mode by delaying focus call after iframe removal 2026-02-12 03:02:10 +01:00
CodeDevMLH
104b76aa41 Update manifest.json for release v1.6.1.5 [skip ci] 2026-02-12 01:51:53 +00:00
CodeDevMLH
7493c8fa93 Bump version to 1.6.1.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-12 02:51:04 +01:00
CodeDevMLH
77c03157a1 Enhance slide pruning logic to restore focus in TV mode after pruning slides 2026-02-12 02:50:50 +01:00
8 changed files with 4165 additions and 395 deletions

View File

@@ -34,8 +34,10 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
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 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;
public bool ApplyLimitsToCustomIds { 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";
} }

View File

@@ -94,6 +94,7 @@
<!-- CUSTOM CONTENT TAB --> <!-- CUSTOM CONTENT TAB -->
<div id="custom" class="tab-content" style="display:none;"> <div id="custom" class="tab-content" style="display:none;">
<!-- Default Custom Media IDs -->
<h2 class="sectionTitle">Custom Media IDs</h2> <h2 class="sectionTitle">Custom Media IDs</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -101,61 +102,68 @@
name="EnableCustomMediaIds" /> name="EnableCustomMediaIds" />
<span>Enable Custom Media IDs</span> <span>Enable Custom Media IDs</span>
</label> </label>
<div class="fieldDescription">If enabled, the slideshow will try to show the items listed <div class="fieldDescription">If enabled, the slideshow will show the items listed
below. If the list is empty, default behavior (random items) is used. Disable this below as the default content. If the list is empty, random items from your library are used.</div>
to temporarily ignore your custom list without deleting it.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ApplyLimitsToCustomIds"
name="ApplyLimitsToCustomIds" />
<span>Apply Limits to Custom IDs</span>
</label>
<div class="fieldDescription">If enabled, the Max Items limit (Advanced &rarr; Content Limits) will also apply to Custom Media IDs and Collections. By default, custom lists are not limited.</div>
</div>
<div id="customMediaIdsContainer">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist IDs (Newline or Comma-separated)</label>
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds"
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.
<br><br>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>.
<br>
Methods:
<ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li>
</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>
<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>
<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>
<!-- Seasonal Content -->
<h2 class="sectionTitle" style="margin-top: 2em;">Seasonal Content</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent" <input is="emby-checkbox" type="checkbox" id="EnableSeasonalContent"
name="EnableSeasonalContent" /> name="EnableSeasonalContent" />
<span>Enable Seasonal Content Mode</span> <span>Enable Seasonal Content</span>
</label> </label>
<div class="fieldDescription">Enable this to define time-based lists in the field below. <div class="fieldDescription">When enabled, seasonal sections below will override the default list
</div> during their active date ranges. If no season matches the current date, the default Custom Media IDs above are used as fallback.</div>
</div> </div>
<div class="inputContainer"> <div id="seasonalContentContainer" style="display: none;">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Media/Collection/Playlist <div id="seasonalSectionsList"></div>
IDs <button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button"
(Newline or Comma separated)</label> style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
<textarea is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds" <i class="material-icons" style="font-size: 24px;">add</i>
style="width: 100%; height: 150px; font-family: monospace;"></textarea> <span>Add Season</span>
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow. </button>
You can separate them by new line or comma.
<br><br>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [Method] DESCRIPTION</code>.
<br>
Methods:
<ul>
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li>
</ul>
<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
fetch the list.txt or 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>
<input type="hidden" id="SeasonalSections" name="SeasonalSections" value="[]" />
</div> </div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
@@ -184,11 +192,11 @@
<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.</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"> <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>
@@ -290,7 +298,7 @@
<h2 class="sectionTitle">Content Sorting</h2> <h2 class="sectionTitle">Content Sorting</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"> <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>
@@ -304,7 +312,7 @@
</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"> <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>
@@ -411,7 +419,7 @@
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls', 'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen', 'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers' 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -425,24 +433,38 @@
} }
}); });
// Handle Seasonal UI logic // Render Seasonal Sections
var seasonalCheckbox = page.querySelector('#EnableSeasonalContent'); try {
var normalDesc = page.querySelector('#customMediaIdsDesc'); var sections = JSON.parse(config.SeasonalSections || "[]");
var seasonalDesc = page.querySelector('#seasonalMediaIdsDesc'); MediaBarEnhancedConfigurationPage.renderSeasonalSections(page, sections);
} catch (e) {
console.error("Error parsing SeasonalSections", e);
}
function updateDesc() { // Handle Seasonal UI visibility
if (seasonalCheckbox && seasonalCheckbox.checked) { var seasonalCheckbox = page.querySelector('#EnableSeasonalContent');
if (normalDesc) normalDesc.style.display = 'none'; var seasonalContainer = page.querySelector('#seasonalContentContainer');
if (seasonalDesc) seasonalDesc.style.display = 'block';
} else { function updateSeasonalVisibility() {
if (normalDesc) normalDesc.style.display = 'block'; if (seasonalContainer) {
if (seasonalDesc) seasonalDesc.style.display = 'none'; seasonalContainer.style.display = seasonalCheckbox && seasonalCheckbox.checked ? 'block' : 'none';
} }
} }
if (seasonalCheckbox) { if (seasonalCheckbox) {
seasonalCheckbox.addEventListener('change', updateDesc); seasonalCheckbox.addEventListener('change', updateSeasonalVisibility);
updateDesc(); updateSeasonalVisibility();
}
// Add Season Button
var addSeasonBtn = page.querySelector('#addSeasonBtn');
if (addSeasonBtn) {
// Remove existing listeners to avoid duplicates if re-attached
var newBtn = addSeasonBtn.cloneNode(true);
addSeasonBtn.parentNode.replaceChild(newBtn, addSeasonBtn);
newBtn.addEventListener('click', function() {
MediaBarEnhancedConfigurationPage.addSeasonalSection(page);
});
} }
// Handle Prefer Local Trailers visibility // Handle Prefer Local Trailers visibility
@@ -468,6 +490,11 @@
saveConfiguration: function (page) { saveConfiguration: function (page) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
var sections = MediaBarEnhancedConfigurationPage.getSeasonalSectionsFromUI(page);
var sectionsJson = JSON.stringify(sections);
var seasonalInput = page.querySelector('#SeasonalSections');
if (seasonalInput) seasonalInput.value = sectionsJson;
var config = {}; var config = {};
var keys = [ var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
@@ -478,7 +505,7 @@
'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls', 'ShowTrailerButton', 'AlwaysShowArrows', 'EnableKeyboardControls',
'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen', 'EnableCustomMediaIds', 'CustomMediaIds', 'EnableLoadingScreen',
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers' 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {
@@ -495,6 +522,105 @@
ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) { ApiClient.updatePluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result); Dashboard.processPluginConfigurationUpdateResult(result);
}); });
},
renderSeasonalSections: function(page, sections) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
container.innerHTML = '';
sections.forEach(function(section, index) {
MediaBarEnhancedConfigurationPage.createSectionElement(container, section, index + 1);
});
},
addSeasonalSection: function(page) {
var container = page.querySelector('#seasonalSectionsList');
if (!container) return;
var index = container.children.length + 1;
MediaBarEnhancedConfigurationPage.createSectionElement(container, {
Name: 'New Season',
StartDay: 1, StartMonth: 1,
EndDay: 1, EndMonth: 1,
MediaIds: ''
}, index);
},
createSectionElement: function(container, data, index) {
var div = document.createElement('div');
div.className = 'seasonal-section';
div.style.cssText = 'background: rgba(0,0,0,0.2); padding: 1em; margin-bottom: 1em; border-radius: 4px; border: 1px solid rgba(255,255,255,0.1);';
var days = [];
for(var i=1; i<=31; i++) days.push(i);
var months = [
{v:1, n:'Jan'}, {v:2, n:'Feb'}, {v:3, n:'Mar'}, {v:4, n:'Apr'},
{v:5, n:'May'}, {v:6, n:'Jun'}, {v:7, n:'Jul'}, {v:8, n:'Aug'},
{v:9, n:'Sep'}, {v:10, n:'Oct'}, {v:11, n:'Nov'}, {v:12, n:'Dec'}
];
function mkSelect(val, opts, cls) {
var h = '<select class="emby-select emby-select-withcolor ' + cls + '" style="width: auto; display: inline-block; margin-right: 5px; -webkit-appearance: menulist; appearance: menulist;">';
opts.forEach(function(o) {
var v = o.v || o;
var n = o.n || o;
h += '<option value="'+v+'" ' + (v == val ? 'selected' : '') + '>' + n + '</option>';
});
h += '</select>';
return h;
}
var labelText = 'Season list #' + (index || 1);
div.innerHTML =
'<div class="inputContainer" style="margin-bottom: 0.5em;">' +
' <label class="inputLabel" style="font-size: 1.2em; font-weight: bold; margin-bottom:0.5em; display:block;">' + labelText + '</label>' +
' <div style="display: flex; align-items: center;">' +
' <div style="flex-grow:1;">' +
' <input is="emby-input" type="text" class="emby-input section-name" value="' + (data.Name || '') + '" />' +
' </div>' +
' <button type="button" class="raised emby-button remove-section" style="background: #a94442; min-width: unset; margin-left: 1em;">Remove</button>' +
' </div>' +
' <div class="fieldDescription">Name of the season</div>' +
'</div>' +
'<div class="inputContainer" style="margin-bottom: 1em;">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' +
' <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 0.5em;">' +
' <span>From:</span>' +
mkSelect(data.StartDay, days, 'start-day') +
mkSelect(data.StartMonth, months, 'start-month') +
' <span style="margin-left: 1em;">To:</span>' +
mkSelect(data.EndDay, days, 'end-day') +
mkSelect(data.EndMonth, months, 'end-month') +
' </div>' +
' <div class="fieldDescription">Date range (inclusive) when this content is active.</div>' +
'</div>' +
'<div class="inputContainer">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Media IDs</label>' +
' <textarea is="emby-textarea" class="emby-textarea section-ids" style="width: 100%; height: 80px; font-family: monospace;">' + (data.MediaIds || '') + '</textarea>' +
' <div class="fieldDescription">Comma-separated or Newline separated list of Movie/Series/Collection IDs to show during this season.<br>Same options available as for the default media IDs.</div>' +
'</div>';
div.querySelector('.remove-section').addEventListener('click', function() {
div.remove();
});
container.appendChild(div);
},
getSeasonalSectionsFromUI: function(page) {
var sections = [];
var els = page.querySelectorAll('.seasonal-section');
els.forEach(function(el) {
sections.push({
Name: el.querySelector('.section-name').value,
StartDay: parseInt(el.querySelector('.start-day').value),
StartMonth: parseInt(el.querySelector('.start-month').value),
EndDay: parseInt(el.querySelector('.end-day').value),
EndMonth: parseInt(el.querySelector('.end-month').value),
MediaIds: el.querySelector('.section-ids').value
});
});
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.6.1.4</Version> <Version>1.6.1.25</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

@@ -175,6 +175,7 @@
overflow: hidden; overflow: hidden;
margin: 0 auto; margin: 0 auto;
pointer-events: auto; pointer-events: auto;
outline: none;
} }
#slides-container[style*="display: none"], #slides-container[style*="display: none"],

View File

@@ -57,6 +57,8 @@ const CONFIG = {
enableClientSideSettings: false, enableClientSideSettings: false,
sortBy: "Random", sortBy: "Random",
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false,
seasonalSections: "[]",
}; };
// State management // State management
@@ -611,7 +613,8 @@ const SlideUtils = {
if (!container) { if (!container) {
container = this.createElement("div", { container = this.createElement("div", {
id: "slides-container", id: "slides-container",
className: "noautofocus" className: "noautofocus",
tabIndex: "-1"
}); });
document.body.appendChild(container); document.body.appendChild(container);
} }
@@ -1316,7 +1319,7 @@ const ApiUtils = {
async fetchCollectionItems(collectionId) { async fetchCollectionItems(collectionId) {
try { try {
const response = await fetch( const response = await fetch(
`${STATE.jellyfinData.serverAddress}/Items?ParentId=${collectionId}&Recursive=true&IncludeItemTypes=Movie,Series&fields=Id&userId=${STATE.jellyfinData.userId}`, `${STATE.jellyfinData.serverAddress}/Items?ParentId=${collectionId}&Recursive=true&IncludeItemTypes=Movie,Series&fields=Id,Type&userId=${STATE.jellyfinData.userId}`,
{ {
headers: this.getAuthHeaders(), headers: this.getAuthHeaders(),
} }
@@ -1330,7 +1333,7 @@ const ApiUtils = {
const data = await response.json(); const data = await response.json();
const items = data.Items || []; const items = data.Items || [];
console.log(`Resolved collection ${collectionId} to ${items.length} items`); console.log(`Resolved collection ${collectionId} to ${items.length} items`);
return items.map(i => i.Id); return items.map(i => ({ Id: i.Id, Type: i.Type }));
} catch (error) { } catch (error) {
console.error(`Error fetching collection items for ${collectionId}:`, error); console.error(`Error fetching collection items for ${collectionId}:`, error);
return []; return [];
@@ -1432,19 +1435,16 @@ const VisibilityObserver = {
// If a full screen video player is active, hide slideshow and stop playback // If a full screen video player is active, hide slideshow and stop playback
if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) { if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) {
if (this._lastVisibleState !== 'player-active') { const container = document.getElementById("slides-container");
this._lastVisibleState = 'player-active'; if (container) {
const container = document.getElementById("slides-container"); container.style.display = "none";
if (container) { container.style.visibility = "hidden";
container.style.display = "none"; container.style.pointerEvents = "none";
container.style.visibility = "hidden";
container.style.pointerEvents = "none";
}
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
} }
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
return; return;
} }
@@ -1459,27 +1459,20 @@ const VisibilityObserver = {
activeTab && activeTab &&
activeTab.getAttribute("data-index") === "0"; activeTab.getAttribute("data-index") === "0";
const newState = isVisible ? 'visible' : 'hidden'; container.style.display = isVisible ? "block" : "none";
container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none";
// Only update DOM and trigger actions when state actually changes if (isVisible) {
if (this._lastVisibleState !== newState) { if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) {
this._lastVisibleState = newState; STATE.slideshow.slideInterval.start();
SlideshowManager.resumeActivePlayback();
container.style.display = isVisible ? "block" : "none";
container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none";
if (isVisible) {
if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) {
STATE.slideshow.slideInterval.start();
SlideshowManager.resumeActivePlayback();
}
} else {
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
} }
} else {
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
} }
}, },
@@ -1487,13 +1480,7 @@ const VisibilityObserver = {
* Initializes visibility observer * Initializes visibility observer
*/ */
init() { init() {
// MARK: Mark const observer = new MutationObserver(() => this.updateVisibility());
// const observer = new MutationObserver(() => this.updateVisibility());
let debounceTimer = null;
const observer = new MutationObserver(() => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => this.updateVisibility(), 250);
});
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
document.body.addEventListener("click", () => this.updateVisibility()); document.body.addEventListener("click", () => this.updateVisibility());
@@ -1620,6 +1607,11 @@ const SlideCreator = {
else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) { else if (item.RemoteTrailers && item.RemoteTrailers.length > 0) {
trailerUrl = item.RemoteTrailers[0].Url; trailerUrl = item.RemoteTrailers[0].Url;
} }
// 1d. Final Fallback to Local Trailer (even if not preferred)
else if (item.LocalTrailerCount > 0 && item.localTrailerUrl) {
trailerUrl = item.localTrailerUrl;
console.log(`Using local trailer fallback for ${itemId}: ${trailerUrl}`);
}
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
@@ -1712,13 +1704,6 @@ const SlideCreator = {
playerVars: playerVars, playerVars: playerVars,
events: { events: {
'onReady': (event) => { 'onReady': (event) => {
// Prevent iframe from stealing focus (critical for TV mode)
const iframe = event.target.getIframe();
if (iframe) {
iframe.setAttribute('tabindex', '-1');
iframe.setAttribute('inert', '');
}
// Store start/end time and videoId for later use // Store start/end time and videoId for later use
event.target._startTime = playerVars.start || 0; event.target._startTime = playerVars.start || 0;
event.target._endTime = playerVars.end || undefined; event.target._endTime = playerVars.end || undefined;
@@ -1735,18 +1720,11 @@ const SlideCreator = {
event.target.setPlaybackQuality(quality); event.target.setPlaybackQuality(quality);
} }
// Only play if this is the active slide AND the slideshow is visible // Only play if this is the active slide
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer'); const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) { if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (currentItemId !== itemId) {
console.log(`Slide ${itemId} is no longer active (current: ${currentItemId}), aborting playback.`);
event.target.mute(); // Mute just in case
return;
}
event.target.playVideo(); event.target.playVideo();
// Check if it actually started playing after a short delay (handling autoplay blocks) // Check if it actually started playing after a short delay (handling autoplay blocks)
@@ -1809,6 +1787,7 @@ const SlideCreator = {
autoplay: false, autoplay: false,
preload: "auto", preload: "auto",
loop: false, loop: false,
disablePictureInPicture: true,
style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;" style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;"
}; };
@@ -1824,18 +1803,7 @@ const SlideCreator = {
STATE.slideshow.videoPlayers[itemId] = backdrop; STATE.slideshow.videoPlayers[itemId] = backdrop;
backdrop.addEventListener('play', (event) => { backdrop.addEventListener('play', () => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (!slide || !slide.classList.contains('active') || currentItemId !== itemId) {
console.log(`Local video ${itemId} started playing but is not active, pausing.`);
event.target.pause();
event.target.currentTime = 0;
return;
}
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
@@ -2279,16 +2247,6 @@ const SlideshowManager = {
let previousVisibleSlide; let previousVisibleSlide;
try { try {
const container = SlideUtils.getOrCreateSlidesContainer(); const container = SlideUtils.getOrCreateSlidesContainer();
const activeElement = document.activeElement;
let focusSelector = null;
if (container.contains(activeElement)) {
if (activeElement.classList.contains('play-button')) focusSelector = '.play-button';
else if (activeElement.classList.contains('detail-button')) focusSelector = '.detail-button';
else if (activeElement.classList.contains('favorite-button')) focusSelector = '.favorite-button';
else if (activeElement.classList.contains('trailer-button')) focusSelector = '.trailer-button';
}
const totalItems = STATE.slideshow.totalItems; const totalItems = STATE.slideshow.totalItems;
index = Math.max(0, Math.min(index, totalItems - 1)); index = Math.max(0, Math.min(index, totalItems - 1));
@@ -2317,27 +2275,91 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Restore focus for TV mode navigation continuity // Manage Video Playback: Stop others, Play current
requestAnimationFrame(() => {
if (focusSelector) { // 1. Pause all other YouTube players
const target = currentSlide.querySelector(focusSelector); if (STATE.slideshow.videoPlayers) {
if (target) { Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
target.focus(); if (id !== currentItemId) {
return; const p = STATE.slideshow.videoPlayers[id];
if (p && typeof p.pauseVideo === 'function') {
p.pauseVideo();
}
} }
} });
// Always ensure container has focus in TV mode to keep keyboard navigation working }
const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') || // 2. Pause all other HTML5 videos e.g. local trailers
document.body.classList.contains('layout-tv'); document.querySelectorAll('video').forEach(video => {
if (isTvMode) { if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) {
container.focus({ preventScroll: true }); video.pause();
} }
}); });
// Manage Video Playback: Stop others, Play current // 3. Play and Reset current video
this.pauseOtherVideos(currentItemId); const videoBackdrop = currentSlide.querySelector('.video-backdrop');
this.playCurrentVideo(currentSlide, currentItemId);
// Update mute button visibility
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
const hasVideo = !!videoBackdrop;
muteButton.style.display = hasVideo ? 'block' : 'none';
}
if (videoBackdrop) {
if (videoBackdrop.tagName === 'VIDEO') {
videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) {
videoBackdrop.volume = 0.4;
}
videoBackdrop.play().catch(e => {
// Check if it actually started playing after a short delay (handling autoplay blocks)
setTimeout(() => {
if (videoBackdrop.paused) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err));
}
}, 1000);
});
} else if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
if (player && typeof player.loadVideoById === 'function' && player._videoId) {
// Use loadVideoById to enforce start and end times
player.loadVideoById({
videoId: player._videoId,
startSeconds: player._startTime || 0,
endSeconds: player._endTime
});
if (STATE.slideshow.isMuted) {
player.mute();
} else {
player.unMute();
player.setVolume(40);
}
// Check if playback successfully started, otherwise fallback to muted
setTimeout(() => {
if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.log("YouTube loadVideoById didn't start playback, retrying muted...");
player.mute();
player.playVideo();
}
}, 1000);
} else if (player && typeof player.seekTo === 'function') {
// Fallback if loadVideoById is not available or videoId missing
const startTime = player._startTime || 0;
player.seekTo(startTime);
player.playVideo();
}
}
}
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled); const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
@@ -2431,19 +2453,26 @@ const SlideshowManager = {
async preloadAdjacentSlides(currentIndex) { async preloadAdjacentSlides(currentIndex) {
const totalItems = STATE.slideshow.totalItems; const totalItems = STATE.slideshow.totalItems;
const preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5); const preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5);
const preloadedIds = new Set();
// Preload next slides // Preload next slides
for (let i = 1; i <= preloadCount; i++) { for (let i = 1; i <= preloadCount; i++) {
const nextIndex = (currentIndex + i) % totalItems; const nextIndex = (currentIndex + i) % totalItems;
if (nextIndex === currentIndex) break;
const itemId = STATE.slideshow.itemIds[nextIndex]; const itemId = STATE.slideshow.itemIds[nextIndex];
SlideCreator.createSlideForItemId(itemId); if (!preloadedIds.has(itemId)) {
preloadedIds.add(itemId);
SlideCreator.createSlideForItemId(itemId);
}
} }
// Preload previous slides // Preload previous slides
for (let i = 1; i <= preloadCount; i++) { for (let i = 1; i <= preloadCount; i++) {
const prevIndex = (currentIndex - i + totalItems) % totalItems; const prevIndex = (currentIndex - i + totalItems) % totalItems;
if (prevIndex === currentIndex) break;
const prevItemId = STATE.slideshow.itemIds[prevIndex]; const prevItemId = STATE.slideshow.itemIds[prevIndex];
SlideCreator.createSlideForItemId(prevItemId); if (!preloadedIds.has(prevItemId)) {
preloadedIds.add(prevItemId);
SlideCreator.createSlideForItemId(prevItemId);
}
} }
}, },
@@ -2472,14 +2501,13 @@ const SlideshowManager = {
pruneSlideCache() { pruneSlideCache() {
const currentIndex = STATE.slideshow.currentSlideIndex; const currentIndex = STATE.slideshow.currentSlideIndex;
const keepRange = 5; const keepRange = 5;
let prunedAny = false;
Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => { Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => {
const index = STATE.slideshow.itemIds.indexOf(itemId); const index = STATE.slideshow.itemIds.indexOf(itemId);
if (index === -1) return; if (index === -1) return;
const totalItems = STATE.slideshow.itemIds.length; const totalItems = STATE.slideshow.itemIds.length;
// Calculate wrapped distance
let distance = Math.abs(index - currentIndex); let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) { if (totalItems > keepRange * 2) {
distance = Math.min(distance, totalItems - distance); distance = Math.min(distance, totalItems - distance);
@@ -2503,10 +2531,25 @@ const SlideshowManager = {
if (slide) slide.remove(); if (slide) slide.remove();
delete STATE.slideshow.createdSlides[itemId]; delete STATE.slideshow.createdSlides[itemId];
prunedAny = true;
console.log(`Pruned slide ${itemId} at distance ${distance} from view`); console.log(`Pruned slide ${itemId} at distance ${distance} from view`);
} }
}); });
if (prunedAny) {
const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') ||
document.body.classList.contains('layout-tv');
if (isTvMode) {
setTimeout(() => {
const container = document.getElementById("slides-container");
if (container && container.style.display !== 'none') {
container.focus({ preventScroll: true });
}
}, 0);
}
}
}, },
toggleMute() { toggleMute() {
@@ -2625,114 +2668,6 @@ const SlideshowManager = {
} }
}, },
/**
* Pauses all video players except the one with the given item ID
* @param {string} excludeItemId - Item ID to exclude from pausing
*/
pauseOtherVideos(excludeItemId) {
// Pause YouTube players
if (STATE.slideshow.videoPlayers) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id !== excludeItemId) {
const p = STATE.slideshow.videoPlayers[id];
if (p) {
try {
if (typeof p.pauseVideo === 'function') {
p.pauseVideo();
if (typeof p.mute === 'function') {
p.mute();
}
}
else if (p.tagName === 'VIDEO') {
p.pause();
p.muted = true;
}
} catch (e) { console.warn("Error pausing player", id, e); }
}
}
});
}
// Pause HTML5 videos
document.querySelectorAll('video').forEach(video => {
const slideParent = video.closest('.slide');
if (slideParent && slideParent.dataset.itemId !== excludeItemId) {
try {
video.pause();
video.muted = true;
} catch (e) {}
}
});
},
/**
* Plays the video backdrop on the given slide and updates mute button visibility
* @param {Element} slide - The slide DOM element
* @param {string} itemId - The item ID of the slide
* @returns {boolean} Whether a video was found and playback attempted
*/
playCurrentVideo(slide, itemId) {
const videoBackdrop = slide.querySelector('.video-backdrop');
// Update mute button visibility
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
muteButton.style.display = videoBackdrop ? 'block' : 'none';
}
if (!videoBackdrop) return false;
if (videoBackdrop.tagName === 'VIDEO') {
videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) videoBackdrop.volume = 0.4;
videoBackdrop.play().catch(() => {
setTimeout(() => {
if (videoBackdrop.paused) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
videoBackdrop.muted = true;
videoBackdrop.play().catch(err => console.error("Muted fallback failed", err));
}
}, 1000);
});
return true;
}
// YouTube player
const player = STATE.slideshow.videoPlayers?.[itemId];
if (player && typeof player.loadVideoById === 'function' && player._videoId) {
player.loadVideoById({
videoId: player._videoId,
startSeconds: player._startTime || 0,
endSeconds: player._endTime
});
if (STATE.slideshow.isMuted) {
player.mute();
} else {
player.unMute();
player.setVolume(40);
}
setTimeout(() => {
if (player.getPlayerState &&
player.getPlayerState() !== YT.PlayerState.PLAYING &&
player.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.log("YouTube loadVideoById didn't start playback, retrying muted...");
player.mute();
player.playVideo();
}
}, 1000);
return true;
} else if (player && typeof player.seekTo === 'function') {
player.seekTo(player._startTime || 0);
player.playVideo();
return true;
}
return false;
},
/** /**
* Stops all video playback (YouTube and HTML5) * Stops all video playback (YouTube and HTML5)
* Used when navigating away from the home screen * Used when navigating away from the home screen
@@ -2789,24 +2724,18 @@ const SlideshowManager = {
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
if (!currentSlide) return; if (!currentSlide) return;
// YouTube player: just resume, don't reload // 1. Try YouTube Player
const ytPlayer = STATE.slideshow.videoPlayers?.[currentItemId]; const ytPlayer = STATE.slideshow.videoPlayers[currentItemId];
if (ytPlayer && typeof ytPlayer.playVideo === 'function') { if (ytPlayer && typeof ytPlayer.playVideo === 'function') {
if (STATE.slideshow.isMuted) {
if (typeof ytPlayer.mute === 'function') ytPlayer.mute();
} else {
if (typeof ytPlayer.unMute === 'function') ytPlayer.unMute();
if (typeof ytPlayer.setVolume === 'function') ytPlayer.setVolume(40);
}
ytPlayer.playVideo(); ytPlayer.playVideo();
return;
} }
// HTML5 video: just resume, don't reset currentTime // 2. Try HTML5 Video
const html5Video = currentSlide.querySelector('video.video-backdrop'); const html5Video = currentSlide.querySelector('video');
if (html5Video) { if (html5Video) {
html5Video.muted = STATE.slideshow.isMuted; if (STATE.slideshow.isMuted) {
if (!STATE.slideshow.isMuted) html5Video.volume = 0.4; html5Video.muted = true;
}
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));
} }
}, },
@@ -2881,7 +2810,7 @@ const SlideshowManager = {
// Determine if we should handle navigation keys (Arrows, Space, M) // Determine if we should handle navigation keys (Arrows, Space, M)
// TV Mode: Strict focus required (must be on slideshow) // TV Mode: Strict focus required (must be on slideshow)
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused) // Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused)
let canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused); const canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused);
// Check for Input Fields (always ignore typing) // Check for Input Fields (always ignore typing)
const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable); const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
@@ -2954,22 +2883,96 @@ const SlideshowManager = {
}, },
/** /**
* Parses custom media IDs, handling seasonal content if enabled * Parses custom media IDs, handling seasonal content if enabled.
* If Seasonal Content is enabled:
* - Check if any defined season matches the current date.
* - If match: Return IDs from that season.
* - If NO match: Fall back to Default Custom IDs.
* If Custom Media IDs are enabled (and no seasonal match):
* - Return Default Custom IDs.
* If no Custom Media IDs are enabled:
* - Return empty array (triggering random fallback).
* @returns {string[]} Array of media IDs * @returns {string[]} Array of media IDs
*/ */
parseCustomIds() { parseCustomIds() {
if (!CONFIG.enableSeasonalContent) { let idsString = CONFIG.customMediaIds;
return CONFIG.customMediaIds let usingSeasonal = false;
.split(/[\n,]/).map((line) => {
if (CONFIG.enableSeasonalContent) {
try {
const sections = JSON.parse(CONFIG.seasonalSections || "[]");
const currentDate = new Date();
const currentMonth = currentDate.getMonth() + 1; // 1-12
const currentDay = currentDate.getDate(); // 1-31
for (const section of sections) {
const startDay = parseInt(section.StartDay);
const startMonth = parseInt(section.StartMonth);
const endDay = parseInt(section.EndDay);
const endMonth = parseInt(section.EndMonth);
let isInRange = false;
if (startMonth === endMonth) {
if (currentMonth === startMonth && currentDay >= startDay && currentDay <= endDay) {
isInRange = true;
}
} else if (startMonth < endMonth) {
// Normal range
if (
(currentMonth > startMonth && currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
} else {
// Wrap around year
if (
(currentMonth > startMonth || currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
}
if (isInRange) {
console.log(`Seasonal match found: ${section.Name}`);
idsString = section.MediaIds;
usingSeasonal = true;
break; // Use first matching season
}
}
} catch (e) {
console.error("Error parsing seasonal sections in JS:", e);
}
}
// If NOT using seasonal content (disabled or no match),
// Custom IDs are disabled, return empty to skip to random
if (!usingSeasonal && !CONFIG.enableCustomMediaIds) {
return [];
}
// Parse the resulting string (either seasonal or default)
if (!idsString) return [];
return idsString
.split(/[\n,]/)
.map((line) => {
const urlMatch = line.match(/\[(.*?)\]/); const urlMatch = line.match(/\[(.*?)\]/);
let id = line; let id = line;
if (urlMatch) { if (urlMatch) {
const url = urlMatch[1]; const url = urlMatch[1];
// Remove the [url] part from the ID string for parsing
id = line.replace(/\[.*?\]/, '').trim(); id = line.replace(/\[.*?\]/, '').trim();
// Attempt to extract GUID if present
const guidMatch = id.match(/([0-9a-f]{32})/i); const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) { if (guidMatch) {
id = guidMatch[1]; id = guidMatch[1];
} else { } else {
// Fallback: split by pipe if used
id = id.split('|')[0].trim(); id = id.split('|')[0].trim();
} }
STATE.slideshow.customTrailerUrls[id] = url; STATE.slideshow.customTrailerUrls[id] = url;
@@ -2978,83 +2981,6 @@ const SlideshowManager = {
}) })
.map((id) => id.trim()) .map((id) => id.trim())
.filter((id) => id); .filter((id) => id);
} else {
return this.parseSeasonalIds();
}
},
/**
* Parses custom media IDs, handling seasonal content if enabled
* @returns {string[]} Array of media IDs
*/
parseSeasonalIds() {
console.log("Using Seasonal Content Mode");
const lines = CONFIG.customMediaIds.split('\n');
const currentDate = new Date();
const currentMonth = currentDate.getMonth() + 1; // 1-12
const currentDay = currentDate.getDate(); // 1-31
const rawIds = [];
for (const line of lines) {
const match = line.match(/^\s*(\d{1,2})\.(\d{1,2})-(\d{1,2})\.(\d{1,2})\s*\|.*\|(.*)$/) ||
line.match(/^\s*(\d{1,2})\.(\d{1,2})-(\d{1,2})\.(\d{1,2})\s*\|(.*)$/);
if (match) {
const startDay = parseInt(match[1]);
const startMonth = parseInt(match[2]);
const endDay = parseInt(match[3]);
const endMonth = parseInt(match[4]);
const idsPart = match[5];
let isInRange = false;
if (startMonth === endMonth) {
if (currentMonth === startMonth && currentDay >= startDay && currentDay <= endDay) {
isInRange = true;
}
} else if (startMonth < endMonth) {
// Normal range spanning months (e.g. 15.06 - 15.08)
if (
(currentMonth > startMonth && currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
} else {
// Wrap around year (e.g. 01.12 - 15.01)
if (
(currentMonth > startMonth || currentMonth < endMonth) ||
(currentMonth === startMonth && currentDay >= startDay) ||
(currentMonth === endMonth && currentDay <= endDay)
) {
isInRange = true;
}
}
if (isInRange) {
console.log(`Seasonal match found: ${line}`);
const ids = idsPart.split(/[,]/).map(line => {
const urlMatch = line.match(/\[(.*?)\]/);
let id = line;
if (urlMatch) {
const url = urlMatch[1];
id = line.replace(/\[.*?\]/, '').trim();
const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) {
id = guidMatch[1];
} else {
id = id.split('|')[0].trim();
}
STATE.slideshow.customTrailerUrls[id] = url;
}
return id.trim();
}).filter(id => id);
rawIds.push(...ids);
}
}
}
return rawIds;
}, },
/** /**
@@ -3096,7 +3022,7 @@ const SlideshowManager = {
const children = await ApiUtils.fetchCollectionItems(id); const children = await ApiUtils.fetchCollectionItems(id);
finalIds.push(...children); finalIds.push(...children);
} else if (item) { } else if (item) {
finalIds.push(id); finalIds.push({ Id: item.Id, Type: item.Type });
} }
} catch (e) { } catch (e) {
console.warn(`Error resolving item ${rawId}:`, e); console.warn(`Error resolving item ${rawId}:`, e);
@@ -3114,10 +3040,41 @@ const SlideshowManager = {
let itemIds = []; let itemIds = [];
// 1. Try Custom Media/Collection IDs from Config & seasonal content // 1. Try Custom Media/Collection IDs from Config & seasonal content
if (CONFIG.enableCustomMediaIds && CONFIG.customMediaIds) { if (CONFIG.enableCustomMediaIds || CONFIG.enableSeasonalContent) {
console.log("Using Custom Media IDs from configuration"); console.log("Using Custom Media IDs from configuration");
const rawIds = this.parseCustomIds(); const rawIds = this.parseCustomIds();
itemIds = await this.resolveCollectionsAndItems(rawIds); const resolvedItems = await this.resolveCollectionsAndItems(rawIds);
// Apply max items limit to custom IDs if enabled
if (CONFIG.applyLimitsToCustomIds) {
let movieCount = 0;
let showCount = 0;
let keptItems = [];
for (const item of resolvedItems) {
if (keptItems.length >= CONFIG.maxItems) break;
if (item.Type === 'Movie') {
if (movieCount < CONFIG.maxMovies) {
movieCount++;
keptItems.push(item);
}
} else if (item.Type === 'Series' || item.Type === 'Season' || item.Type === 'Episode') {
// Count Seasons/Episodes as TV Shows
if (showCount < CONFIG.maxTvShows) {
showCount++;
keptItems.push(item);
}
} else {
// Other types: count towards total only
keptItems.push(item);
}
}
itemIds = keptItems.map(i => i.Id);
console.log(`Applied limits to custom IDs: ${itemIds.length} items (Movies: ${movieCount}, Shows: ${showCount})`);
} else {
itemIds = resolvedItems.map(i => i.Id);
}
} }
// 2. Try Avatar List (list.txt) // 2. Try Avatar List (list.txt)
@@ -3497,25 +3454,75 @@ const MediaBarEnhancedSettingsManager = {
* Initialize page visibility handling to pause when tab is inactive * Initialize page visibility handling to pause when tab is inactive
*/ */
const initPageVisibilityHandler = () => { const initPageVisibilityHandler = () => {
let wasVideoPlayingBeforeHide = false;
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (document.hidden) { if (document.hidden) {
console.log("Tab inactive - stopping all slideshow playback"); console.log("Tab inactive - pausing slideshow and videos");
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
if (STATE.slideshow.slideInterval) { if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
SlideshowManager.stopAllPlayback();
// Pause active video if playing
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (currentItemId) {
// YouTube
if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
const player = STATE.slideshow.videoPlayers[currentItemId];
if (typeof player.pauseVideo === "function") {
try {
player.pauseVideo();
STATE.slideshow.isVideoPlaying = false;
} catch (e) {
console.warn("Error pausing video on tab hide:", e);
}
} else if (player.tagName === 'VIDEO') { // HTML5 Video
player.pause();
STATE.slideshow.isVideoPlaying = false;
}
}
}
} else { } else {
console.log("Tab active - resuming slideshow"); console.log("Tab active - resuming slideshow");
// Only resume if we're on the home page and not paused if (!STATE.slideshow.isPaused) {
const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home"; const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') || document.querySelector('.youtubePlayerContainer:not(.hide)');
if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) { if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
SlideshowManager.resumeActivePlayback(); const player = STATE.slideshow.videoPlayers[currentItemId];
if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) { // YouTube
STATE.slideshow.slideInterval.start(); if (typeof player.playVideo === "function") {
try {
player.playVideo();
STATE.slideshow.isVideoPlaying = true;
} catch (e) {
console.warn("Error resuming video on tab show:", e);
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.start();
}
}
} else if (player.tagName === 'VIDEO') { // HTML5 Video
try {
player.play().catch(e => console.warn("Error resuming HTML5 video:", e));
STATE.slideshow.isVideoPlaying = true;
} catch(e) { console.warn(e); }
}
} else {
// No video was playing, just restart interval
const activeSlide = document.querySelector('.slide.active');
const hasVideo = activeSlide && activeSlide.querySelector('.video-backdrop');
if (CONFIG.waitForTrailerToEnd && hasVideo) {
// Don't restart interval if waiting for trailer
} else {
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.start();
}
}
} }
wasVideoPlayingBeforeHide = false;
} }
} }
}); });

View File

@@ -155,21 +155,48 @@ Configure the plugin via **Dashboard** > **Plugins** > **Media Bar Enhanced**.
Define exactly what shows up in your bar. Define exactly what shows up in your bar.
* **Enable Custom Media IDs**: Restrict the slideshow to a specific list of IDs. * **Enable Custom Media IDs**: Restrict the slideshow to a specific list of IDs.
* **Manual Trailer Override**: Add `[YouTube_URL]` after an ID to force a specific trailer. * **Manual Trailer Override**: Add `[YouTube_URL]` or `[Jellyfin_ID]` after an ID to force a specific trailer/video.
* Example ID: `a1b2c3d4e5... [https://www.youtube.com/watch?v=VIDEO_ID]` * Example ID: `a1b2c3d4e5... [https://www.youtube.com/watch?v=VIDEO_ID]`
* Example ID: `z1b2c3d4e5... [Jellyfin_ID]`
* **Example Mixed List**:
```
a1b2c3d4e5f6... <-- Plays local item video
6bdu812812hd... [https://youtu.be/...] <-- Item metadata + Custom YouTube Trailer
12h44h124sf7... [hdc78127z4ff...] <-- Item metadata + Custom Jellyfin Trailer/Video etc.
```
* Example Collection Name: `Halloween Collection [https://...] | My Description` (Note: Use `|` to separate description from name if using a name instead of an ID) * Example Collection Name: `Halloween Collection [https://...] | My Description` (Note: Use `|` to separate description from name if using a name instead of an ID)
* **Apply Limits to Custom IDs**: If enabled, the "Content Limits" (see below) will also apply to your Custom Media IDs list. By default, custom lists show all listed items regardless of limits.
* **Enable Seasonal Content Mode**: Advanced date-based scheduling. * **Enable Seasonal Content Mode**: Advanced date-based scheduling.
* Format: `DD.MM-DD.MM | Name | ID1, ID2, ID3` * **GUI Configuration**: You can easily add "Seasons" via the **Add Season** button.
* Example: `20.10-31.10 | Halloween | <ID_OF_HALLOWEEN_COLLECTION>` * **Active Period**: Select the Start and End Day/Month for each season.
* If the current date matches a range, those IDs are used. Otherwise, it defaults to standard behavior or the Custom Media IDs list. * **Media IDs**: Enter the Comma-separated list of IDs (Movies, Series, Collections) for that season.
* **Priority**: If the current date matches a defined season, those IDs are used. If multiple seasons overlap, the first matching one is used. If no season matches, it falls back to the Default Custom Media IDs.
**How to get IDs:** **How to get IDs:**
Check the URL of an item in the web interface: Check the URL of an item in the web interface:
`.../web/#/details?id=YOUR_ITEM_ID_HERE&...` `.../web/#/details?id=YOUR_ITEM_ID_HERE&...`
### Content Sorting
Customize the order of slides in the Media Bar.
* **Sort By**: Choose criteria like *Random*, *Premiere Date*, *Production Year*, *Critic Rating*, *Community Rating*, *Name*, or *Runtime*.
* **Sort Order**: Ascending or Descending.
* **Note**: Sorting applies to both server-fetched content AND Custom Media IDs. Select **Original** to preserve the exact order of your Custom Media IDs list.
### Content Limits
Fine-tune performance by limiting the number of items fetched from the server.
* **Total Max Items**: Maximum total items to fetch (combined).
* **Max Movies**: Maximum movies to include (for random selection).
* **Max Tv Shows**: Maximum TV shows to include (for random selection).
* **Preload Count**: Number of slides to preload for smooth transitions.
* *Intelligent Preloading*: The plugin uses a safe preloading strategy that respects this count but handles small lists gracefully to avoid playback issues.
* **Max Pagination Dots**: Maximum number of dots to show. If exceeded, it switches to a counter (e.g., 1/20).
### Advanced Settings ### Advanced Settings
* **Slide Animations**: Enable/disable the "Zoom In" effect. * **Slide Animations**: Enable/disable the "Zoom In" effect.
* **Use SponsorBlock**: Skips non-content segments in YouTube trailers (if the data exists). * **Use SponsorBlock**: Skips non-content segments in YouTube trailers (if the data exists).
* **Preferred YouTube Quality**: Select your preferred resolution (*Auto*, *Maximum*, *1080p*, *720p*).
* **Start Muted**: Videos start without sound (user can unmute). * **Start Muted**: Videos start without sound (user can unmute).
* **Full Width Video**: Stretches video to cover the entire width (good for desktop, crop on mobile). * **Full Width Video**: Stretches video to cover the entire width (good for desktop, crop on mobile).
* **Enable Loading Screen**: Enable/disable the loading indicator while the bar initializes. * **Enable Loading Screen**: Enable/disable the loading indicator while the bar initializes.

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.6.1.4", "version": "1.6.1.25",
"changelog": "- fix tv mode issue\n- refactor video playback management", "changelog": "- fix tv mode issue\n- refactor video playback management",
"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.1.4/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.25/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "d0d3f67b52c82c0f01c89b74ae83af01", "checksum": "283886aa51ccd1d40f057b65617b7ea4",
"timestamp": "2026-02-12T01:29:36Z" "timestamp": "2026-02-14T02:21:14Z"
}, },
{ {
"version": "1.6.0.2", "version": "1.6.0.2",

3607
tmp.js Normal file

File diff suppressed because it is too large Load Diff