Compare commits
12 Commits
v1.8.0.0
...
8804048c61
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8804048c61 | ||
|
|
992f854dd2 | ||
|
|
c30300d1ba | ||
|
|
387f0dd26f | ||
|
|
71d9cddebb | ||
|
|
9abcdf522d | ||
|
|
8c703ce171 | ||
|
|
a40ee4a40d | ||
|
|
d7e9238e21 | ||
|
|
a296caf70d | ||
|
|
86e9968243 | ||
|
|
e9fe356cee |
@@ -49,6 +49,8 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
|
||||
public bool IncludeWatchedContent { get; set; } = false;
|
||||
public string SortBy { get; set; } = "Random";
|
||||
public string SortOrder { get; set; } = "Ascending";
|
||||
public int BackdropVideoDelay { get; set; } = 0;
|
||||
public bool ConstrainPlotWidth { get; set; } = false;
|
||||
|
||||
public bool EnableCustomOverlay { get; set; } = false;
|
||||
public string CustomOverlayText { get; set; } = "";
|
||||
|
||||
@@ -418,6 +418,13 @@
|
||||
on
|
||||
mobile devices. (looks bad on desktops)</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="ConstrainPlotWidth" name="ConstrainPlotWidth" />
|
||||
<span>Constrain Plot Width</span>
|
||||
</label>
|
||||
<div class="fieldDescription">Align the description text left to match the logo width, preventing it from crossing the entire screen. (Increases the text limit to 3 lines instead of 2).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="EnableLoadingScreen"
|
||||
@@ -463,6 +470,11 @@
|
||||
<input is="emby-input" type="number" id="ShuffleInterval" name="ShuffleInterval" />
|
||||
<div class="fieldDescription">Time in milliseconds between changing slides.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="BackdropVideoDelay">Backdrop Video Delay (ms)</label>
|
||||
<input is="emby-input" type="number" id="BackdropVideoDelay" name="BackdropVideoDelay" />
|
||||
<div class="fieldDescription">Time in milliseconds to wait before playing backdrops/theme videos (leaves static backdrop visible longer).</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="RetryInterval">Retry Interval
|
||||
@@ -663,7 +675,8 @@
|
||||
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
|
||||
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
|
||||
'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority',
|
||||
'CustomOverlayPositionX', 'CustomOverlayPositionY', 'CustomOverlayScale'
|
||||
'CustomOverlayPositionX', 'CustomOverlayPositionY', 'CustomOverlayScale',
|
||||
'BackdropVideoDelay', 'ConstrainPlotWidth'
|
||||
];
|
||||
|
||||
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
|
||||
@@ -908,7 +921,8 @@
|
||||
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile',
|
||||
'EnableCustomOverlay', 'CustomOverlayText', 'CustomOverlayImageUrl',
|
||||
'CustomOverlayStyle', 'CustomOverlayImageStyle', 'CustomOverlayPriority',
|
||||
'CustomOverlayPositionX', 'CustomOverlayPositionY', 'CustomOverlayScale'
|
||||
'CustomOverlayPositionX', 'CustomOverlayPositionY', 'CustomOverlayScale',
|
||||
'BackdropVideoDelay', 'ConstrainPlotWidth'
|
||||
];
|
||||
|
||||
keys.forEach(function (key) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
|
||||
<Title>Jellyfin Media Bar Enhanced Plugin</Title>
|
||||
<Authors>CodeDevMLH</Authors>
|
||||
<Version>1.8.0.0</Version>
|
||||
<Version>1.8.1.3</Version>
|
||||
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -489,6 +489,15 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plot-container.constrained-plot {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.plot-container.constrained-plot .plot {
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.genre {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
@@ -749,6 +758,15 @@
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.8);
|
||||
background-color: #ffffff00;
|
||||
width: max-content;
|
||||
max-width: 90vw;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.dot {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
|
||||
@@ -340,6 +340,15 @@ const initLoadingScreen = () => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Global Failsafe, force remove loading screen after 15 seconds to prevent infinite lockouts
|
||||
setTimeout(() => {
|
||||
const loader = document.querySelector(".bar-loading");
|
||||
if (loader) {
|
||||
console.warn("🎬 Media Bar:", "Loading screen timed out! Forcing removal as a failsafe.");
|
||||
finishLoading();
|
||||
}
|
||||
}, 15000);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2046,7 +2055,7 @@ const SlideCreator = {
|
||||
SlideUtils.truncateText(plotElement, CONFIG.maxPlotLength);
|
||||
|
||||
const plotContainer = SlideUtils.createElement("div", {
|
||||
className: "plot-container",
|
||||
className: "plot-container" + (CONFIG.constrainPlotWidth ? " constrained-plot" : ""),
|
||||
});
|
||||
plotContainer.appendChild(plotElement);
|
||||
|
||||
@@ -2363,8 +2372,17 @@ const SlideshowManager = {
|
||||
|
||||
const totalItems = STATE.slideshow.totalItems || 0;
|
||||
|
||||
// dynamically lower the max dots threshold on small screens
|
||||
let effectiveMaxDots = CONFIG.maxPaginationDots;
|
||||
if (window.matchMedia("(max-width: 767px) and (orientation: portrait)").matches) {
|
||||
const availableWidth = window.innerWidth * 0.9;
|
||||
const dotWidth = 18; // approximate width per dot
|
||||
const fittingDots = Math.floor(availableWidth / dotWidth) - 1;
|
||||
effectiveMaxDots = Math.min(effectiveMaxDots, fittingDots);
|
||||
}
|
||||
|
||||
// Switch to counter style if too many items
|
||||
if (totalItems > CONFIG.maxPaginationDots) {
|
||||
if (totalItems > effectiveMaxDots) {
|
||||
const counter = document.createElement("span");
|
||||
counter.className = "slide-counter";
|
||||
counter.id = "slide-counter";
|
||||
@@ -2425,6 +2443,11 @@ const SlideshowManager = {
|
||||
|
||||
STATE.slideshow.isTransitioning = true;
|
||||
|
||||
if (STATE.slideshow.backdropVideoTimeout) {
|
||||
clearTimeout(STATE.slideshow.backdropVideoTimeout);
|
||||
STATE.slideshow.backdropVideoTimeout = null;
|
||||
}
|
||||
|
||||
let previousVisibleSlide;
|
||||
try {
|
||||
const container = SlideUtils.getOrCreateSlidesContainer();
|
||||
@@ -2516,11 +2539,13 @@ const SlideshowManager = {
|
||||
}
|
||||
|
||||
if (videoBackdrop) {
|
||||
// preload logic
|
||||
if (videoBackdrop.tagName === 'VIDEO') {
|
||||
// Restore src from data-src if it was deactivated to release connections
|
||||
const lazySrc = videoBackdrop.getAttribute('data-src');
|
||||
if (lazySrc && !videoBackdrop.src) {
|
||||
videoBackdrop.src = lazySrc;
|
||||
videoBackdrop.load(); // Force pre-buffering
|
||||
}
|
||||
|
||||
videoBackdrop.currentTime = 0;
|
||||
@@ -2529,22 +2554,11 @@ const SlideshowManager = {
|
||||
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 && currentSlide.classList.contains('active')) {
|
||||
console.warn("🎬 Media Bar:", `Autoplay blocked for ${currentItemId}, attempting muted fallback`);
|
||||
videoBackdrop.muted = true;
|
||||
videoBackdrop.play().catch(err => console.error("🎬 Media Bar:", "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({
|
||||
if (player && typeof player.cueVideoById === 'function' && player._videoId) {
|
||||
// Use cueVideoById to buffer video without auto-playing it
|
||||
player.cueVideoById({
|
||||
videoId: player._videoId,
|
||||
startSeconds: player._startTime || 0,
|
||||
endSeconds: player._endTime
|
||||
@@ -2556,25 +2570,52 @@ const SlideshowManager = {
|
||||
player.unMute();
|
||||
player.setVolume(40);
|
||||
}
|
||||
|
||||
// Check if playback successfully started, otherwise fallback to muted
|
||||
setTimeout(() => {
|
||||
if (!currentSlide.classList.contains('active')) return;
|
||||
if (player.getPlayerState &&
|
||||
player.getPlayerState() !== YT.PlayerState.PLAYING &&
|
||||
player.getPlayerState() !== YT.PlayerState.BUFFERING) {
|
||||
console.log("🎬 Media Bar:", "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();
|
||||
}
|
||||
}
|
||||
|
||||
// play logic
|
||||
const playVideoLogic = () => {
|
||||
if (!currentSlide.classList.contains('active')) return;
|
||||
|
||||
if (videoBackdrop.tagName === 'VIDEO') {
|
||||
videoBackdrop.play().catch(e => {
|
||||
// Check if it actually started playing after a short delay (handling autoplay blocks)
|
||||
setTimeout(() => {
|
||||
if (videoBackdrop.paused && currentSlide.classList.contains('active')) {
|
||||
console.warn("🎬 Media Bar:", `Autoplay blocked for ${currentItemId}, attempting muted fallback`);
|
||||
videoBackdrop.muted = true;
|
||||
videoBackdrop.play().catch(err => console.error("🎬 Media Bar:", "Muted fallback failed", err));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
} else if (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) {
|
||||
const player = STATE.slideshow.videoPlayers[currentItemId];
|
||||
if (player && typeof player.playVideo === 'function') {
|
||||
player.playVideo();
|
||||
|
||||
// Check if playback successfully started, otherwise fallback to muted
|
||||
setTimeout(() => {
|
||||
if (!currentSlide.classList.contains('active')) return;
|
||||
if (player.getPlayerState &&
|
||||
player.getPlayerState() !== YT.PlayerState.PLAYING &&
|
||||
player.getPlayerState() !== YT.PlayerState.BUFFERING) {
|
||||
console.log("🎬 Media Bar:", "YouTube didn't start playback, retrying muted...");
|
||||
player.mute();
|
||||
player.playVideo();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (CONFIG.backdropVideoDelay > 0) {
|
||||
STATE.slideshow.backdropVideoTimeout = setTimeout(playVideoLogic, CONFIG.backdropVideoDelay);
|
||||
} else {
|
||||
playVideoLogic();
|
||||
}
|
||||
}
|
||||
|
||||
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
|
||||
@@ -3892,8 +3933,16 @@ const slidesInit = async () => {
|
||||
homeSections.style.top = '0';
|
||||
homeSections.style.marginTop = '0';
|
||||
}
|
||||
const container = document.getElementById('slides-container');
|
||||
if (container) container.style.display = 'none';
|
||||
let container = document.getElementById('slides-container');
|
||||
if (container) {
|
||||
container.style.display = 'none';
|
||||
} else {
|
||||
// Create dummy container so loading screen's interval can trigger its own cleanup
|
||||
container = document.createElement('div');
|
||||
container.id = 'slides-container';
|
||||
container.style.display = 'none';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
"category": "General",
|
||||
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.8.1.3",
|
||||
"changelog": "- fix pagination dot issue on mobile when showing more than 10 dots (should now dynamically adjust the max dots threshold based on screen size)\n- add option to delay trailer playback\n- add option to limit the plot width",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.8.1.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "5ca1f5b5ad5dd20d521533b815aec656",
|
||||
"timestamp": "2026-03-23T17:35:37Z"
|
||||
},
|
||||
{
|
||||
"version": "1.8.0.0",
|
||||
"changelog": "feat: add custom text/image overlay option\n- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- Add hide arrows on mobile option \n- fix button issue on mobile when using ElegantFin Theme",
|
||||
"changelog": "- feat: add custom text/image overlay option\n- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- Add hide arrows on mobile option \n- fix button issue on mobile when using ElegantFin Theme",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.8.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||
"checksum": "0aac723796d41fc15987c94ac0476584",
|
||||
|
||||
Reference in New Issue
Block a user