Compare commits

..

9 Commits

Author SHA1 Message Date
CodeDevMLH
a44bf7ebf4 Update manifest.json for release v1.6.6.2 [skip ci] 2026-02-19 02:36:42 +00:00
CodeDevMLH
1f273906bf Bump version to 1.6.6.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-19 03:35:50 +01:00
CodeDevMLH
0534d0458e Add delay before stopping other video players during slideshow transitions 2026-02-19 03:35:38 +01:00
CodeDevMLH
8b0d6f137d Update manifest.json for release v1.6.6.1 [skip ci] 2026-02-19 02:25:34 +00:00
CodeDevMLH
2208b86a47 Bump version to 1.6.6.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-19 03:24:42 +01:00
CodeDevMLH
5a1048687c Enhance video backdrop handling with opacity transitions for smoother playback 2026-02-19 03:24:27 +01:00
CodeDevMLH
d3f6641158 Update manifest.json for release v1.6.6.0 [skip ci] 2026-02-19 01:01:21 +00:00
CodeDevMLH
c214a620e4 Bump version to 1.6.6.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-19 01:52:31 +01:00
CodeDevMLH
f0c9462878 Implement manual mapping for MediaBarIsEnabled and enhance video backdrop handling 2026-02-19 01:52:17 +01:00
4 changed files with 100 additions and 42 deletions

View File

@@ -449,7 +449,7 @@
ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) { ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) {
var keys = [ var keys = [
'MediaBarIsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows', 'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots', 'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock', 'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
@@ -462,6 +462,12 @@
'IncludeWatchedContent' 'IncludeWatchedContent'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
mediaBarEnabledCheckbox.checked = config.IsEnabled;
}
keys.forEach(function (key) { keys.forEach(function (key) {
var el = page.querySelector('#' + key); var el = page.querySelector('#' + key);
if (el) { if (el) {
@@ -539,8 +545,15 @@
if (seasonalInput) seasonalInput.value = sectionsJson; if (seasonalInput) seasonalInput.value = sectionsJson;
var config = {}; var config = {};
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
config.IsEnabled = mediaBarEnabledCheckbox.checked;
}
var keys = [ var keys = [
'MediaBarIsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows', 'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots', 'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock', 'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',

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.5.2</Version> <Version>1.6.6.2</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

@@ -63,6 +63,7 @@ const CONFIG = {
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false, applyLimitsToCustomIds: false,
seasonalSections: "[]", seasonalSections: "[]",
isEnabled: true,
}; };
// State management // State management
@@ -1638,6 +1639,7 @@ const SlideCreator = {
"data-item-id": itemId, "data-item-id": itemId,
}); });
let videoBackdrop;
let backdrop; let backdrop;
let isVideo = false; let isVideo = false;
let trailerUrl = null; let trailerUrl = null;
@@ -1721,11 +1723,19 @@ const SlideCreator = {
// Create container for YouTube API // Create container for YouTube API
const videoClass = CONFIG.fullWidthVideo ? "video-backdrop-full" : "video-backdrop-default"; const videoClass = CONFIG.fullWidthVideo ? "video-backdrop-full" : "video-backdrop-default";
backdrop = SlideUtils.createElement("div", { // Create a wrapper for opacity transition
className: `backdrop video-backdrop ${videoClass}`, videoBackdrop = SlideUtils.createElement("div", {
id: `youtube-player-${itemId}` className: `backdrop video-backdrop ${videoClass}`,
style: "opacity: 0; transition: opacity 1.2s ease-in-out;" // Start interrupted/transparent
}); });
const ytPlayerDiv = SlideUtils.createElement("div", {
id: `youtube-player-${itemId}`,
style: "width: 100%; height: 100%;"
});
videoBackdrop.appendChild(ytPlayerDiv);
// Initialize YouTube Player // Initialize YouTube Player
SlideUtils.loadYouTubeIframeAPI().then(() => { SlideUtils.loadYouTubeIframeAPI().then(() => {
// Fetch SponsorBlock data // Fetch SponsorBlock data
@@ -1781,6 +1791,9 @@ const SlideCreator = {
event.target._startTime = playerVars.start || 0; event.target._startTime = playerVars.start || 0;
event.target._endTime = playerVars.end || undefined; event.target._endTime = playerVars.end || undefined;
event.target._videoId = videoId; event.target._videoId = videoId;
// Store reference to wrapper for fading
event.target._wrapperDiv = videoBackdrop;
if (STATE.slideshow.isMuted) { if (STATE.slideshow.isMuted) {
event.target.mute(); event.target.mute();
@@ -1830,6 +1843,13 @@ const SlideCreator = {
} }
}, },
'onStateChange': (event) => { 'onStateChange': (event) => {
// Fade in when playing
if (event.data === YT.PlayerState.PLAYING) {
if (event.target._wrapperDiv) {
event.target._wrapperDiv.style.opacity = "1";
}
}
if (event.data === YT.PlayerState.ENDED) { if (event.data === YT.PlayerState.ENDED) {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
@@ -1859,17 +1879,17 @@ const SlideCreator = {
preload: "none", preload: "none",
disablePictureInPicture: true, disablePictureInPicture: true,
"data-src": videoSrc, "data-src": videoSrc,
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; opacity: 0; transition: opacity 1.2s ease-in-out;"
}; };
videoAttributes.muted = ""; videoAttributes.muted = "";
backdrop = SlideUtils.createElement("video", videoAttributes); videoBackdrop = SlideUtils.createElement("video", videoAttributes);
backdrop.volume = 0.4; videoBackdrop.volume = 0.4;
STATE.slideshow.videoPlayers[itemId] = backdrop; STATE.slideshow.videoPlayers[itemId] = videoBackdrop;
backdrop.addEventListener('play', (event) => { videoBackdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (!slide || !slide.classList.contains('active')) { if (!slide || !slide.classList.contains('active')) {
console.log(`Local video ${itemId} started playing but slide is not active, pausing.`); console.log(`Local video ${itemId} started playing but slide is not active, pausing.`);
@@ -1877,19 +1897,23 @@ const SlideCreator = {
event.target.currentTime = 0; event.target.currentTime = 0;
return; return;
} }
// Fade in
event.target.style.opacity = "1";
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) { if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop(); STATE.slideshow.slideInterval.stop();
} }
}); });
backdrop.addEventListener('ended', (event) => { videoBackdrop.addEventListener('ended', (event) => {
const slide = event.target.closest('.slide'); const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}); });
backdrop.addEventListener('error', (event) => { videoBackdrop.addEventListener('error', (event) => {
console.warn(`Local video error for item ${itemId}`); console.warn(`Local video error for item ${itemId}`);
const slide = event.target.closest('.slide'); const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
@@ -1899,13 +1923,18 @@ const SlideCreator = {
} }
} }
if (!isVideo) { // Always create a static backdrop image (to show while video loads or if no video)
backdrop = SlideUtils.createElement("img", { backdrop = SlideUtils.createElement("img", {
className: "backdrop high-quality", className: "backdrop high-quality",
src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60), src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60),
alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'), alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'),
loading: "eager", loading: "eager",
}); });
// If video, static backdrop should be strictly a background (no animation)
if (isVideo) {
backdrop.style.animation = "none";
backdrop.style.transition = "none";
} }
const backdropOverlay = SlideUtils.createElement("div", { const backdropOverlay = SlideUtils.createElement("div", {
@@ -1915,8 +1944,14 @@ const SlideCreator = {
const backdropContainer = SlideUtils.createElement("div", { const backdropContainer = SlideUtils.createElement("div", {
className: "backdrop-container" + (isVideo && CONFIG.fullWidthVideo ? " full-width-video" : ""), className: "backdrop-container" + (isVideo && CONFIG.fullWidthVideo ? " full-width-video" : ""),
}); });
backdropContainer.append(backdrop, backdropOverlay); backdropContainer.append(backdrop, backdropOverlay);
// If video exists, append on top of static backdrop
if (isVideo && videoBackdrop) {
backdropContainer.appendChild(videoBackdrop);
}
const logo = SlideUtils.createElement("img", { const logo = SlideUtils.createElement("img", {
className: "logo high-quality", className: "logo high-quality",
src: this.buildImageUrl(item, "Logo", undefined, serverAddress, 40), src: this.buildImageUrl(item, "Logo", undefined, serverAddress, 40),
@@ -2358,30 +2393,32 @@ const SlideshowManager = {
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
// 1. Stop all other YouTube players and local video elements, release connections // 1. Stop all other YouTube players and local video elements, release connections
if (STATE.slideshow.videoPlayers) { setTimeout(() => {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { if (STATE.slideshow.videoPlayers) {
if (id !== currentItemId) { Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
const p = STATE.slideshow.videoPlayers[id]; if (id !== currentItemId) {
if (!p) return; const p = STATE.slideshow.videoPlayers[id];
// YouTube player if (!p) return;
if (typeof p.pauseVideo === 'function') { // YouTube player
p.pauseVideo(); if (typeof p.pauseVideo === 'function') {
} p.pauseVideo();
// HTML5 <video> element (local trailers), release HTTP connection }
if (p instanceof HTMLVideoElement) { // HTML5 <video> element (local trailers), release HTTP connection
p.pause(); if (p instanceof HTMLVideoElement) {
p.muted = true; p.pause();
p.currentTime = 0; p.muted = true;
// Save src to data-src and release the HTTP streaming connection p.currentTime = 0;
if (p.src && !p.getAttribute('data-src')) { // Save src to data-src and release the HTTP streaming connection
p.setAttribute('data-src', p.src); if (p.src && !p.getAttribute('data-src')) {
p.setAttribute('data-src', p.src);
}
p.removeAttribute('src');
p.load();
} }
p.removeAttribute('src');
p.load();
} }
} });
}); }
} }, CONFIG.fadeTransitionDuration);
// 2. Pause all other HTML5 videos e.g. local trailers // 2. Pause all other HTML5 videos e.g. local trailers
document.querySelectorAll('video').forEach(video => { document.querySelectorAll('video').forEach(video => {

View File

@@ -8,6 +8,14 @@
"category": "General", "category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{
"version": "1.6.6.2",
"changelog": "- feat: add static backdrop also for video backdrops\n- fix: renaming issue of settings (avoiding conflict with other plugins)",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.6.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "5672ad96720adf049e780c85174a88bc",
"timestamp": "2026-02-19T02:36:41Z"
},
{ {
"version": "1.6.5.2", "version": "1.6.5.2",
"changelog": "- refactored seasonal UI settings", "changelog": "- refactored seasonal UI settings",