Compare commits

..

53 Commits

Author SHA1 Message Date
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
CodeDevMLH
a7929e1ff6 Update manifest.json for release v1.6.1.4 [skip ci] 2026-02-12 01:29:37 +00:00
CodeDevMLH
c78e07de62 Bump version to 1.6.1.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-12 02:28:47 +01:00
CodeDevMLH
a90f805ea8 Refactor visibility management in slideshow and video playback; optimize state updates and debounce observer for improved performance. 2026-02-12 02:28:32 +01:00
CodeDevMLH
ccba1857e1 Update manifest.json for release v1.6.1.3 [skip ci] 2026-02-12 00:43:53 +00:00
CodeDevMLH
ff56c9370b Bump version to 1.6.1.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-02-12 01:43:05 +01:00
CodeDevMLH
162600c43f Enhance TV mode support by preventing iframe focus stealing and ensuring container focus continuity; refactor video playback management for improved clarity and functionality. 2026-02-12 01:42:22 +01:00
CodeDevMLH
a21549af47 Update manifest.json for release v1.6.1.2 [skip ci] 2026-02-11 23:50:41 +00:00
CodeDevMLH
1b319ade40 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 52s
2026-02-12 00:49:49 +01:00
CodeDevMLH
75757d67e7 Adjust backdrop container positioning for TV layout 2026-02-11 20:15:17 +01:00
CodeDevMLH
92fc8d72f7 Update manifest.json for release v1.6.1.2 [skip ci] 2026-02-11 19:13:25 +00:00
CodeDevMLH
cfe9dec550 Bump version to 1.6.1.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-11 20:12:30 +01:00
CodeDevMLH
1bef573aaf Enhance video playback handling and improve slide preloading logic 2026-02-11 20:12:16 +01:00
CodeDevMLH
29a365b690 Update manifest.json for release v1.6.1.1 [skip ci] 2026-02-11 18:57:36 +00:00
CodeDevMLH
0ee0a65309 Bump version to 1.6.1.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-11 19:56:42 +01:00
CodeDevMLH
152d22b709 Refine video playback logic in slideshow and update preload description 2026-02-11 19:56:20 +01:00
CodeDevMLH
216dddad94 Update manifest.json for release v1.6.1.0 [skip ci] 2026-02-11 17:24:36 +00:00
CodeDevMLH
a6de148ca1 Bump version to 1.6.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-11 18:23:42 +01:00
CodeDevMLH
1f9378d74d Enhance video backdrop styling for TV layout and improve focus management in slideshow 2026-02-11 18:22:44 +01:00
CodeDevMLH
cc025779dc Update manifest.json for release v1.6.0.2 [skip ci] 2026-02-10 22:07:34 +00:00
CodeDevMLH
3d10fd59b5 Enhance manual trailer/video override instructions to support Jellyfin Item IDs and clarify usage
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-10 23:06:40 +01:00
CodeDevMLH
bf261eba96 Update manifest.json for release v1.6.0.2 [skip ci] 2026-02-10 21:44:36 +00:00
CodeDevMLH
e3116c30cf Bump version to 1.6.0.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-10 22:43:47 +01:00
CodeDevMLH
bb39c91d32 Enhance custom trailer URL handling to support Jellyfin Item IDs and standard URLs 2026-02-10 22:43:40 +01:00
CodeDevMLH
4e0c74614a Update manifest.json for release v1.6.0.1 [skip ci] 2026-02-10 21:27:25 +00:00
CodeDevMLH
b61bf92437 Bump version to 1.6.0.1 in project files and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-10 22:26:32 +01:00
CodeDevMLH
dfbd6ce964 Enable autoplay for video playback in SlideUtils 2026-02-10 22:26:28 +01:00
CodeDevMLH
f1cbcad177 Update manifest.json for release v1.6.0.0 [skip ci] 2026-02-10 21:18:26 +00:00
CodeDevMLH
feedd5d95f Bump version to 1.6.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-10 22:17:36 +01:00
CodeDevMLH
87d82cca15 Enhance trailer handling: support object format for trailer URLs and add widget referrer for YouTube embeds 2026-02-10 22:17:24 +01:00
CodeDevMLH
a70746e095 Update manifest.json for release v1.5.1.3 [skip ci] 2026-02-10 20:12:59 +00:00
CodeDevMLH
f32283e0bf Bump version to 1.5.1.3 and update changelog for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-10 21:12:05 +01:00
CodeDevMLH
8f3985f307 Update manifest.json for release v1.5.1.2 [skip ci] 2026-02-10 17:39:23 +00:00
CodeDevMLH
0b2817ecff Bump version to 1.5.1.2 and update changelog for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-10 18:38:31 +01:00
CodeDevMLH
84faee2db4 Update manifest.json for release v1.5.1.1 [skip ci] 2026-02-10 16:58:48 +00:00
CodeDevMLH
3efa07ec51 Bump version to 1.5.1.1 and update changelog for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 55s
2026-02-10 17:57:54 +01:00
CodeDevMLH
af603e8803 Enhance options in mediaBarEnhanced for seasonal content, local trailers, and sorting criteria [skip ci] 2026-02-10 01:57:51 +01:00
CodeDevMLH
fe63414e4b Update manifest.json for release v1.5.1.0 [skip ci] 2026-02-10 00:51:17 +00:00
CodeDevMLH
614c86083f Bump version to 1.5.1.0 and update changelog for iOS/MacOS playback fix
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-10 01:50:27 +01:00
CodeDevMLH
17b9e8921e Update manifest.json for release v1.5.0.28 [skip ci] 2026-02-10 00:35:42 +00:00
CodeDevMLH
5075226ba8 fix wording
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 56s
2026-02-10 01:34:47 +01:00
CodeDevMLH
443fb1008b Update version to 1.5.0.28 and enhance changelog with new features and fixes [skip ci] 2026-02-10 01:32:33 +01:00
CodeDevMLH
8f12140aad Update manifest.json for release v1.5.0.28 [skip ci] 2026-02-10 00:22:00 +00:00
CodeDevMLH
1469712bb5 reverted to 25
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-10 01:21:08 +01:00
CodeDevMLH
76ff03ac17 Update manifest.json for release v1.5.0.27 [skip ci] 2026-02-10 00:16:23 +00:00
CodeDevMLH
25b1ba5f2b Bump version to 1.5.0.26 and update changelog for recent changes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-10 01:15:33 +01:00
CodeDevMLH
eed3ca1860 Update manifest.json for release v1.5.0.25 [skip ci] 2026-02-10 00:07:06 +00:00
CodeDevMLH
0f14577f5d Bump version to 1.5.0.25 and update changelog for recent changes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-10 01:06:15 +01:00
5 changed files with 444 additions and 262 deletions

View File

@@ -123,9 +123,15 @@
<div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow. <div class="fieldDescription" id="customMediaIdsDesc">Enter the IDs of the items you want to show in the slideshow.
You can separate them by new line or comma. You can separate them by new line or comma.
<br><br> <br><br>
<b>Manual Trailer Override:</b> You can specify a YouTube URL for an item by adding it in <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 [https://youtu.be/...] DESCRIPTION</code> brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [Method] DESCRIPTION</code>.
<br><br> <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 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> (|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<br><br> <br><br>
@@ -182,8 +188,7 @@
</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" <select is="emby-select" id="PreferredVideoQuality" name="PreferredVideoQuality" class="selectLayout emby-select-withcolor emby-select">
class="emby-select-withcolor emby-select">
<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>
@@ -285,7 +290,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="emby-select-withcolor emby-select"> <select is="emby-select" id="SortBy" name="SortBy" class="selectLayout emby-select-withcolor emby-select">
<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>
@@ -299,7 +304,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="emby-select-withcolor emby-select"> <select is="emby-select" id="SortOrder" name="SortOrder" class="selectLayout emby-select-withcolor emby-select">
<option value="Ascending">Ascending</option> <option value="Ascending">Ascending</option>
<option value="Descending">Descending</option> <option value="Descending">Descending</option>
</select> </select>
@@ -332,7 +337,7 @@
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label> <label class="inputLabel inputLabelUnfocused" for="PreloadCount">Preload Count</label>
<input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" /> <input is="emby-input" type="number" id="PreloadCount" name="PreloadCount" />
<div class="fieldDescription">Number of images to preload.</div> <div class="fieldDescription">Number of slides to preload.</div>
</div> </div>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination <label class="inputLabel inputLabelUnfocused" for="MaxPaginationDots">Max Pagination

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.5.0.24</Version> <Version>1.6.1.6</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

@@ -1,6 +1,6 @@
/* /*
* Jellyfin Slideshow by M0RPH3US v3.0.9 * Jellyfin Slideshow by M0RPH3US v4.0.1
* Modified by CodeDevMLH v1.1.0.0 * Modified by CodeDevMLH
* *
* New features: * New features:
* - optional Trailer background video support * - optional Trailer background video support
@@ -14,6 +14,9 @@
* - option to disable loading screen * - option to disable loading screen
* - option to put collection (boxsets) IDs into the slideshow to display their items * - option to put collection (boxsets) IDs into the slideshow to display their items
* - option to enable client-side settings (allow users to override settings locally on their device) * - option to enable client-side settings (allow users to override settings locally on their device)
* - option to enable seasonal content (only show items that are relevant to the current season/holiday)
* - option to prefer local trailers (from the media item) over online sources
* - options to sort the content by various criteria (PremiereDate, ProductionYear, Random, Original order, etc.)
*/ */
@import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap); @import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap);
@@ -161,7 +164,7 @@
.homeSectionsContainer { .homeSectionsContainer {
position: relative; position: relative;
margin-top: 65vh; top: 65vh;
z-index: 6; z-index: 6;
} }
@@ -992,3 +995,7 @@
.dots-container .slide-counter { .dots-container .slide-counter {
margin: 0; margin: 0;
} }
.layout-tv .backdrop-container{
top: -5%;
}

View File

@@ -1,6 +1,6 @@
/* /*
* Jellyfin Slideshow by M0RPH3US v4.0.1 * Jellyfin Slideshow by M0RPH3US v4.0.1
* Modified by CodeDevMLH v1.1.0.0 * Modified by CodeDevMLH
* *
* New features: * New features:
* - optional Trailer background video support * - optional Trailer background video support
@@ -14,6 +14,9 @@
* - option to disable loading screen * - option to disable loading screen
* - option to put collection (boxsets) IDs into the slideshow to display their items * - option to put collection (boxsets) IDs into the slideshow to display their items
* - option to enable client-side settings (allow users to override settings locally on their device) * - option to enable client-side settings (allow users to override settings locally on their device)
* - option to enable seasonal content (only show items that are relevant to the current season/holiday)
* - option to prefer local trailers (from the media item) over online sources
* - options to sort the content by various criteria (PremiereDate, ProductionYear, Random, Original order, etc.)
*/ */
//Core Module Configuration //Core Module Configuration
@@ -608,7 +611,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);
} }
@@ -737,7 +741,11 @@ const SlideUtils = {
autoplay: 1, autoplay: 1,
controls: 1, controls: 1,
iv_load_policy: 3, iv_load_policy: 3,
rel: 0 rel: 0,
playsinline: 1,
origin: window.location.origin,
widget_referrer: window.location.href,
enablejsapi: 1
} }
}); });
}); });
@@ -1333,7 +1341,7 @@ const ApiUtils = {
/** /**
* Fetches the first local trailer for an item * Fetches the first local trailer for an item
* @param {string} itemId - Item ID * @param {string} itemId - Item ID
* @returns {Promise<string|null>} Stream URL or null * @returns {Promise<Object|null>} Trailer data object {id, url} or null
*/ */
async fetchLocalTrailer(itemId) { async fetchLocalTrailer(itemId) {
try { try {
@@ -1353,8 +1361,11 @@ const ApiUtils = {
const trailer = trailers[0]; const trailer = trailers[0];
const mediaSourceId = trailer.MediaSources && trailer.MediaSources[0] ? trailer.MediaSources[0].Id : trailer.Id; const mediaSourceId = trailer.MediaSources && trailer.MediaSources[0] ? trailer.MediaSources[0].Id : trailer.Id;
// Construct stream URL // Return object with ID and URL
return `${STATE.jellyfinData.serverAddress}/Videos/${trailer.Id}/stream.mp4?Static=true&mediaSourceId=${mediaSourceId}&api_key=${STATE.jellyfinData.accessToken}`; return {
id: trailer.Id,
url: `${STATE.jellyfinData.serverAddress}/Videos/${trailer.Id}/stream.mp4?Static=true&mediaSourceId=${mediaSourceId}&api_key=${STATE.jellyfinData.accessToken}`
};
} }
return null; return null;
} catch (error) { } catch (error) {
@@ -1422,16 +1433,19 @@ 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'))) {
const container = document.getElementById("slides-container"); if (this._lastVisibleState !== 'player-active') {
if (container) { this._lastVisibleState = 'player-active';
container.style.display = "none"; const container = document.getElementById("slides-container");
container.style.visibility = "hidden"; if (container) {
container.style.pointerEvents = "none"; container.style.display = "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;
} }
@@ -1446,20 +1460,27 @@ const VisibilityObserver = {
activeTab && activeTab &&
activeTab.getAttribute("data-index") === "0"; activeTab.getAttribute("data-index") === "0";
container.style.display = isVisible ? "block" : "none"; const newState = isVisible ? 'visible' : 'hidden';
container.style.visibility = isVisible ? "visible" : "hidden";
container.style.pointerEvents = isVisible ? "auto" : "none";
if (isVisible) { // Only update DOM and trigger actions when state actually changes
if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) { if (this._lastVisibleState !== newState) {
STATE.slideshow.slideInterval.start(); this._lastVisibleState = newState;
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();
} }
}, },
@@ -1467,7 +1488,13 @@ const VisibilityObserver = {
* Initializes visibility observer * Initializes visibility observer
*/ */
init() { init() {
const observer = new MutationObserver(() => this.updateVisibility()); // MARK: Mark
// 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());
@@ -1566,8 +1593,24 @@ const SlideCreator = {
// 1a. Custom URL override // 1a. Custom URL override
if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) { if (STATE.slideshow.customTrailerUrls && STATE.slideshow.customTrailerUrls[itemId]) {
trailerUrl = STATE.slideshow.customTrailerUrls[itemId]; const customValue = STATE.slideshow.customTrailerUrls[itemId];
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
// Check if the custom value is a Jellyfin Item ID (GUID)
const guidMatch = customValue.match(/^([0-9a-f]{32})$/i);
if (guidMatch) {
const videoId = guidMatch[1];
console.log(`Using custom local video ID for ${itemId}: ${videoId}`);
trailerUrl = {
id: videoId,
url: `${STATE.jellyfinData.serverAddress}/Videos/${videoId}/stream.mp4?Static=true&api_key=${STATE.jellyfinData.accessToken}`
};
} else {
// Assume it's a standard URL (YouTube, etc.)
trailerUrl = customValue;
console.log(`Using custom trailer URL for ${itemId}: ${trailerUrl}`);
}
} }
// 1b. Check Local Trailer if preferred // 1b. Check Local Trailer if preferred
else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) { else if (CONFIG.preferLocalTrailers && item.LocalTrailerCount > 0 && item.localTrailerUrl) {
@@ -1592,12 +1635,17 @@ const SlideCreator = {
let videoId = null; let videoId = null;
try { try {
const urlObj = new URL(trailerUrl); let urlToCheck = trailerUrl;
if (urlObj.hostname.includes('youtube.com') || urlObj.hostname.includes('youtu.be')) { if (typeof trailerUrl === 'object' && trailerUrl.url) {
urlToCheck = trailerUrl.url;
}
const urlObjChecked = new URL(urlToCheck);
if (urlObjChecked.hostname.includes('youtube.com') || urlObjChecked.hostname.includes('youtu.be')) {
isYoutube = true; isYoutube = true;
videoId = urlObj.searchParams.get('v'); videoId = urlObjChecked.searchParams.get('v');
if (!videoId && urlObj.hostname.includes('youtu.be')) { if (!videoId && urlObjChecked.hostname.includes('youtu.be')) {
videoId = urlObj.pathname.substring(1); videoId = urlObjChecked.pathname.substring(1);
} }
} }
} catch (e) { } catch (e) {
@@ -1626,7 +1674,11 @@ const SlideCreator = {
fs: 0, fs: 0,
iv_load_policy: 3, iv_load_policy: 3,
rel: 0, rel: 0,
loop: 0 loop: 0,
playsinline: 1,
origin: window.location.origin,
widget_referrer: window.location.href,
enablejsapi: 1
}; };
// Determine video quality // Determine video quality
@@ -1661,6 +1713,13 @@ 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;
@@ -1677,11 +1736,18 @@ const SlideCreator = {
event.target.setPlaybackQuality(quality); event.target.setPlaybackQuality(quality);
} }
// Only play if this is the active slide // Only play if this is the active slide AND the slideshow is visible
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)
@@ -1722,7 +1788,8 @@ const SlideCreator = {
} }
} }
}, },
'onError': () => { 'onError': (event) => {
console.warn(`YouTube player error ${event.data} for video ${videoId}`);
// Fallback to next slide on error // Fallback to next slide on error
if (CONFIG.waitForTrailerToEnd) { if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
@@ -1739,7 +1806,7 @@ const SlideCreator = {
const videoAttributes = { const videoAttributes = {
className: "backdrop video-backdrop", className: "backdrop video-backdrop",
src: trailerUrl, src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl),
autoplay: false, autoplay: false,
preload: "auto", preload: "auto",
loop: false, loop: false,
@@ -1758,7 +1825,18 @@ const SlideCreator = {
STATE.slideshow.videoPlayers[itemId] = backdrop; STATE.slideshow.videoPlayers[itemId] = backdrop;
backdrop.addEventListener('play', () => { backdrop.addEventListener('play', (event) => {
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();
} }
@@ -2030,11 +2108,20 @@ const SlideCreator = {
/** /**
* Creates a trailer button * Creates a trailer button
* @param {string} url - Trailer URL * @param {string|Object} trailerInfo - Trailer URL string or object {id, url}
* @returns {HTMLElement} Trailer button element * @returns {HTMLElement} Trailer button element
*/ */
createTrailerButton(url) { createTrailerButton(trailerInfo) {
const trailerText = LocalizationUtils.getLocalizedString('Trailer', 'Trailer'); const trailerText = LocalizationUtils.getLocalizedString('Trailer', 'Trailer');
let url = trailerInfo;
let localTrailerId = null;
if (typeof trailerInfo === 'object' && trailerInfo !== null) {
url = trailerInfo.url;
localTrailerId = trailerInfo.id;
}
return SlideUtils.createElement("button", { return SlideUtils.createElement("button", {
className: "detailButton trailer-button", className: "detailButton trailer-button",
innerHTML: `<span class="material-icons">movie</span> <span class="trailer-text">${trailerText}</span>`, innerHTML: `<span class="material-icons">movie</span> <span class="trailer-text">${trailerText}</span>`,
@@ -2042,7 +2129,13 @@ const SlideCreator = {
onclick: (e) => { onclick: (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
SlideUtils.openVideoModal(url);
if (localTrailerId) {
// Play local trailer using native player
ApiUtils.playItem(localTrailerId);
} else {
SlideUtils.openVideoModal(url);
}
}, },
}); });
}, },
@@ -2187,6 +2280,16 @@ 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));
@@ -2215,91 +2318,27 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current // Restore focus for TV mode navigation continuity
requestAnimationFrame(() => {
// 1. Pause all other YouTube players if (focusSelector) {
if (STATE.slideshow.videoPlayers) { const target = currentSlide.querySelector(focusSelector);
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { if (target) {
if (id !== currentItemId) { target.focus();
const p = STATE.slideshow.videoPlayers[id]; return;
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) ||
// 2. Pause all other HTML5 videos e.g. local trailers document.documentElement.classList.contains('layout-tv') ||
document.querySelectorAll('video').forEach(video => { document.body.classList.contains('layout-tv');
if (!video.closest(`.slide[data-item-id="${currentItemId}"]`)) { if (isTvMode) {
video.pause(); container.focus({ preventScroll: true });
} }
}); });
// 3. Play and Reset current video // Manage Video Playback: Stop others, Play current
const videoBackdrop = currentSlide.querySelector('.video-backdrop'); this.pauseOtherVideos(currentItemId);
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);
@@ -2308,7 +2347,8 @@ const SlideshowManager = {
if (backdrop && !backdrop.classList.contains("video-backdrop")) { if (backdrop && !backdrop.classList.contains("video-backdrop")) {
backdrop.classList.add("animate"); backdrop.classList.add("animate");
} }
currentSlide.querySelector(".logo").classList.add("animate"); const logo = currentSlide.querySelector(".logo");
if (logo) logo.classList.add("animate");
} }
STATE.slideshow.currentSlideIndex = index; STATE.slideshow.currentSlideIndex = index;
@@ -2391,18 +2431,20 @@ const SlideshowManager = {
*/ */
async preloadAdjacentSlides(currentIndex) { async preloadAdjacentSlides(currentIndex) {
const totalItems = STATE.slideshow.totalItems; const totalItems = STATE.slideshow.totalItems;
const preloadCount = CONFIG.preloadCount; const preloadCount = Math.min(Math.max(CONFIG.preloadCount || 1, 1), 5);
const nextIndex = (currentIndex + 1) % totalItems; // Preload next slides
const itemId = STATE.slideshow.itemIds[nextIndex]; for (let i = 1; i <= preloadCount; i++) {
const nextIndex = (currentIndex + i) % totalItems;
const itemId = STATE.slideshow.itemIds[nextIndex];
SlideCreator.createSlideForItemId(itemId);
}
await SlideCreator.createSlideForItemId(itemId); // Preload previous slides
for (let i = 1; i <= preloadCount; i++) {
if (preloadCount > 1) { const prevIndex = (currentIndex - i + totalItems) % totalItems;
const prevIndex = (currentIndex - 1 + totalItems) % totalItems; const prevItemId = STATE.slideshow.itemIds[prevIndex];
const prevItemId = STATE.slideshow.itemIds[prevIndex]; SlideCreator.createSlideForItemId(prevItemId);
SlideCreator.createSlideForItemId(prevItemId);
} }
}, },
@@ -2431,12 +2473,20 @@ 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 distance = Math.abs(index - currentIndex); const totalItems = STATE.slideshow.itemIds.length;
// Calculate wrapped distance
let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) {
distance = Math.min(distance, totalItems - distance);
}
if (distance > keepRange) { if (distance > keepRange) {
// Destroy video player if exists // Destroy video player if exists
if (STATE.slideshow.videoPlayers[itemId]) { if (STATE.slideshow.videoPlayers[itemId]) {
@@ -2455,10 +2505,27 @@ 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`);
} }
}); });
// After pruning, restore focus to container in TV mode
if (prunedAny) {
const isTvMode = (window.layoutManager && window.layoutManager.tv) ||
document.documentElement.classList.contains('layout-tv') ||
document.body.classList.contains('layout-tv');
if (isTvMode) {
// Use setTimeout to execute AFTER Jellyfin's focus manager processes the iframe removal
setTimeout(() => {
const container = document.getElementById("slides-container");
if (container && container.style.display !== 'none') {
container.focus({ preventScroll: true });
}
}, 0);
}
}
}, },
toggleMute() { toggleMute() {
@@ -2577,6 +2644,114 @@ 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
@@ -2606,14 +2781,16 @@ const SlideshowManager = {
}); });
} }
// 2. Pause all HTML5 videos // 2. Stop and mute all HTML5 videos
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
if (container) { if (container) {
container.querySelectorAll('video').forEach(video => { container.querySelectorAll('video').forEach(video => {
try { try {
video.pause(); video.pause();
video.muted = true;
video.currentTime = 0;
} catch (e) { } catch (e) {
console.warn("Error pausing HTML5 video:", e); console.warn("Error stopping HTML5 video:", e);
} }
}); });
} }
@@ -2631,18 +2808,24 @@ 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;
// 1. Try YouTube Player // YouTube player: just resume, don't reload
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;
} }
// 2. Try HTML5 Video // HTML5 video: just resume, don't reset currentTime
const html5Video = currentSlide.querySelector('video'); const html5Video = currentSlide.querySelector('video.video-backdrop');
if (html5Video) { if (html5Video) {
if (STATE.slideshow.isMuted) { html5Video.muted = STATE.slideshow.isMuted;
html5Video.muted = true; if (!STATE.slideshow.isMuted) html5Video.volume = 0.4;
}
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));
} }
}, },
@@ -2705,62 +2888,75 @@ const SlideshowManager = {
} }
const activeElement = document.activeElement; const activeElement = document.activeElement;
// Check if we're in TV mode (checking class on html or body is more reliable) const isTvDevice = window.browser && window.browser.tv;
const isTvMode = document.documentElement.classList.contains('layout-tv') || document.body.classList.contains('layout-tv') || (window.layoutManager && window.layoutManager.tv); const isTvLayout = window.layoutManager && window.layoutManager.tv;
const isSlideshowFocused = container.contains(activeElement) || activeElement === container || (!isTvMode && activeElement === document.body); const hasTvClass = document.documentElement.classList.contains('layout-tv') || document.body.classList.contains('layout-tv');
const isTvMode = isTvDevice || isTvLayout || hasTvClass;
// Check Focus State
const isBodyFocused = activeElement === document.body;
const hasDirectFocus = container.contains(activeElement) || activeElement === container;
// Determine if we should handle navigation keys (Arrows, Space, M)
// TV Mode: Strict focus required (must be on slideshow)
// Desktop Mode: Loose focus allowed (slideshow OR body/nothing focused)
let canControlSlideshow = isTvMode ? hasDirectFocus : (hasDirectFocus || isBodyFocused);
// 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);
if (isInputElement) return;
if (isInputElement) { // Check active video players (ignore if video is playing/overlay is open)
return;
}
// Check if video player is open
const videoPlayer = document.querySelector('.videoPlayerContainer'); const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer'); const trailerPlayer = document.querySelector('.youtubePlayerContainer');
if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) { const isVideoOpen = (videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'));
return; if (isVideoOpen) return;
}
switch (e.key) { switch (e.key) {
case "ArrowRight": case "ArrowRight":
if (isTvMode && !isSlideshowFocused) return; if (canControlSlideshow) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
e.preventDefault(); e.preventDefault();
}
break; break;
case "ArrowLeft": case "ArrowLeft":
if (isTvMode && !isSlideshowFocused) return; if (canControlSlideshow) {
SlideshowManager.prevSlide(); SlideshowManager.prevSlide();
e.preventDefault(); e.preventDefault();
}
break; break;
case " ": // Space bar case " ": // Space bar
if (isTvMode && !isSlideshowFocused) return; if (canControlSlideshow) {
this.togglePause(); this.togglePause();
e.preventDefault(); e.preventDefault();
}
break; break;
case "m": // Mute toggle case "m": // Mute toggle
case "M": case "M":
if (isTvMode && !isSlideshowFocused) return; if (canControlSlideshow) {
this.toggleMute(); this.toggleMute();
e.preventDefault(); e.preventDefault();
}
break; break;
case "Enter": case "Enter":
if (!isSlideshowFocused) return; // Enter always requires direct focus on the slideshow to avoid conflicts
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; if (hasDirectFocus) {
if (currentItemId) { const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (window.Emby && window.Emby.Page) { if (currentItemId) {
Emby.Page.show( if (window.Emby && window.Emby.Page) {
`/details?id=${currentItemId}&serverId=${STATE.jellyfinData.serverId}` Emby.Page.show(
); `/details?id=${currentItemId}&serverId=${STATE.jellyfinData.serverId}`
} else { );
window.location.href = `#/details?id=${currentItemId}&serverId=${STATE.jellyfinData.serverId}`; } else {
} window.location.href = `#/details?id=${currentItemId}&serverId=${STATE.jellyfinData.serverId}`;
}
}
e.preventDefault();
} }
e.preventDefault();
break; break;
} }
}); });
@@ -2922,7 +3118,7 @@ const SlideshowManager = {
finalIds.push(id); finalIds.push(id);
} }
} catch (e) { } catch (e) {
console.warn(`Error resolving item ${id}:`, e); console.warn(`Error resolving item ${rawId}:`, e);
} }
} }
return finalIds; return finalIds;
@@ -3320,75 +3516,25 @@ 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 - pausing slideshow and videos"); console.log("Tab inactive - stopping all slideshow playback");
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");
if (!STATE.slideshow.isPaused) { // Only resume if we're on the home page and not paused
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex]; const isOnHome = window.location.hash === "#/home.html" || window.location.hash === "#/home";
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer:not(.hide)') || document.querySelector('.youtubePlayerContainer:not(.hide)');
if (wasVideoPlayingBeforeHide && currentItemId && STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) { if (isOnHome && !STATE.slideshow.isPaused && !isVideoPlayerOpen) {
const player = STATE.slideshow.videoPlayers[currentItemId]; SlideshowManager.resumeActivePlayback();
// YouTube if (STATE.slideshow.slideInterval && !CONFIG.waitForTrailerToEnd) {
if (typeof player.playVideo === "function") { STATE.slideshow.slideInterval.start();
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

@@ -9,12 +9,36 @@
"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.5.0.24", "version": "1.6.1.6",
"changelog": "- fix: Keyboard controls in TV mode\n- Add sorting options for content\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 4.0.1 from original repo", "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.5.0.24/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.6/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "3a845b9a81b6305f84ea6bc7058155f8", "checksum": "7cebae52060938493b5dd6bec894f3d1",
"timestamp": "2026-02-09T23:53:20Z" "timestamp": "2026-02-12T02:03:09Z"
},
{
"version": "1.6.0.2",
"changelog": "- add local trailer support on trailer button\nfix: iOS/MacOS playback issue?",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "cdd0208f8cc4f4b04f50e7138e508370",
"timestamp": "2026-02-10T22:07:33Z"
},
{
"version": "1.5.1.3",
"changelog": "- fix: iOS/MacOS playback issue?",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.5.1.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "9d9dbed453673d4b78acf2adaaaee126",
"timestamp": "2026-02-10T20:12:59Z"
},
{
"version": "1.5.0.28",
"changelog": "- fix: Keyboard controls in TV mode\n- Add sorting options for content\n- Add local trailer support\n- fix performance issue\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 4.0.1 from upstream repo",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.5.0.28/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "0261ff27be18d48cefa5078706954240",
"timestamp": "2026-02-10T00:35:41Z"
}, },
{ {
"version": "1.3.0.3", "version": "1.3.0.3",