Compare commits

...

36 Commits

Author SHA1 Message Date
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
CodeDevMLH
9999d6a633 Update manifest.json for release v1.5.0.24 [skip ci] 2026-02-09 23:53:21 +00:00
CodeDevMLH
a935fd7d5d Bump version to 1.5.0.24 and update changelog for recent changes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-10 00:52:29 +01:00
CodeDevMLH
60293e81e1 Update manifest.json for release v1.5.0.23 [skip ci] 2026-02-09 23:34:11 +00:00
CodeDevMLH
f54e55fe04 Bump version to 1.5.0.23 and update changelog for recent changes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-10 00:33:19 +01:00
5 changed files with 247 additions and 67 deletions

View File

@@ -123,8 +123,14 @@
<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>
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><br> <br><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>
@@ -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>

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.22</Version> <Version>1.6.1.0</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,15 @@
.dots-container .slide-counter { .dots-container .slide-counter {
margin: 0; margin: 0;
} }
.layout-tv .video-backdrop {
top: -5%;
mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
}

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
@@ -85,6 +88,7 @@ const STATE = {
isMuted: CONFIG.startMuted, isMuted: CONFIG.startMuted,
customTrailerUrls: {}, customTrailerUrls: {},
ytPromise: null, ytPromise: null,
autoplayTimeouts: [],
}, },
}; };
@@ -736,7 +740,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
} }
}); });
}); });
@@ -1332,7 +1340,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 {
@@ -1352,8 +1360,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) {
@@ -1416,6 +1427,24 @@ class SlideTimer {
*/ */
const VisibilityObserver = { const VisibilityObserver = {
updateVisibility() { updateVisibility() {
const videoPlayer = document.querySelector('.videoPlayerContainer');
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
// If a full screen video player is active, hide slideshow and stop playback
if ((videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'))) {
const container = document.getElementById("slides-container");
if (container) {
container.style.display = "none";
container.style.visibility = "hidden";
container.style.pointerEvents = "none";
}
if (STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
SlideshowManager.stopAllPlayback();
return;
}
const activeTab = document.querySelector(".emby-tab-button-active"); const activeTab = document.querySelector(".emby-tab-button-active");
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
@@ -1547,8 +1576,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) {
@@ -1573,12 +1618,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) {
@@ -1607,7 +1657,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
@@ -1660,10 +1714,23 @@ const SlideCreator = {
// Only play if this is the active slide // 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}"]`);
if (slide && slide.classList.contains('active')) { const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
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)
setTimeout(() => { const timeoutId = setTimeout(() => {
// Re-check conditions before processing fallback
const isVideoPlayerOpenNow = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (document.hidden || (isVideoPlayerOpenNow && !isVideoPlayerOpenNow.classList.contains('hide')) || !slide.classList.contains('active')) {
console.log(`Navigation detected during autoplay check for ${itemId}, stopping video.`);
try {
event.target.stopVideo();
} catch (e) { console.warn("Error stopping video in timeout:", e); }
return;
}
if (event.target.getPlayerState() !== YT.PlayerState.PLAYING && if (event.target.getPlayerState() !== YT.PlayerState.PLAYING &&
event.target.getPlayerState() !== YT.PlayerState.BUFFERING) { event.target.getPlayerState() !== YT.PlayerState.BUFFERING) {
console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`); console.warn(`Autoplay blocked for ${itemId}, attempting muted fallback`);
@@ -1672,6 +1739,9 @@ const SlideCreator = {
} }
}, 1000); }, 1000);
if (!STATE.slideshow.autoplayTimeouts) STATE.slideshow.autoplayTimeouts = [];
STATE.slideshow.autoplayTimeouts.push(timeoutId);
// Pause slideshow timer when video starts if configured // Pause slideshow timer when video starts if configured
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
@@ -1704,7 +1774,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,
@@ -1995,11 +2065,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>`,
@@ -2007,7 +2086,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);
}
}, },
}); });
}, },
@@ -2152,6 +2237,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));
@@ -2180,6 +2275,15 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
if (focusSelector) {
const target = currentSlide.querySelector(focusSelector);
if (target) {
requestAnimationFrame(() => {
target.focus();
});
}
}
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
// 1. Pause all other YouTube players // 1. Pause all other YouTube players
@@ -2547,15 +2651,26 @@ const SlideshowManager = {
* Used when navigating away from the home screen * Used when navigating away from the home screen
*/ */
stopAllPlayback() { stopAllPlayback() {
// 1. Pause all YouTube players // Clear any pending autoplay timeouts
if (STATE.slideshow.autoplayTimeouts) {
STATE.slideshow.autoplayTimeouts.forEach(id => clearTimeout(id));
STATE.slideshow.autoplayTimeouts = [];
}
// 1. Stop all YouTube players
if (STATE.slideshow.videoPlayers) { if (STATE.slideshow.videoPlayers) {
Object.values(STATE.slideshow.videoPlayers).forEach(player => { Object.values(STATE.slideshow.videoPlayers).forEach(player => {
try { try {
if (player && typeof player.pauseVideo === 'function') { if (player && typeof player.stopVideo === 'function') {
player.stopVideo();
if (typeof player.clearVideo === 'function') {
player.clearVideo();
}
} else if (player && typeof player.pauseVideo === 'function') {
player.pauseVideo(); player.pauseVideo();
} }
} catch (e) { } catch (e) {
console.warn("Error pausing YouTube player:", e); console.warn("Error pausing/stopping YouTube player:", e);
} }
}); });
} }
@@ -2659,54 +2774,75 @@ const SlideshowManager = {
} }
const activeElement = document.activeElement; const activeElement = document.activeElement;
const isTvMode = window.layoutManager && window.layoutManager.tv; const isTvDevice = window.browser && window.browser.tv;
const isSlideshowFocused = container.contains(activeElement) || activeElement === container || (!isTvMode && activeElement === document.body); const isTvLayout = window.layoutManager && window.layoutManager.tv;
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)
const 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; const videoPlayer = document.querySelector('.videoPlayerContainer');
} const trailerPlayer = document.querySelector('.youtubePlayerContainer');
const isVideoOpen = (videoPlayer && !videoPlayer.classList.contains('hide')) || (trailerPlayer && !trailerPlayer.classList.contains('hide'));
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;
} }
}); });

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.22", "version": "1.6.1.0",
"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",
"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.22/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "a9534b9ab12a29759c567ad7b5c7dc6f", "checksum": "646e58f7723dab118e767a57d62004c9",
"timestamp": "2026-02-09T23:08:40Z" "timestamp": "2026-02-11T17:24:36Z"
},
{
"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",