diff --git a/README.md b/README.md
index d2c416f..587a78a 100644
--- a/README.md
+++ b/README.md
@@ -66,8 +66,12 @@ Credits for this plugin go to @MakD for his original work and to @BobHasNoSoul a
# Jellyfin-Media-Bar - Now with Play Now Function
-
+
+
+
+
+
**IMP UPDATE — We have dropped support for the normal CSS version (for now). _(It still works, but there will be no further updates till the fullscreen mode is stabilized)_**
diff --git a/slideshowpure.css b/slideshowpure.css
index b2862d4..f9643c0 100644
--- a/slideshowpure.css
+++ b/slideshowpure.css
@@ -1,5 +1,5 @@
/*
- * Jellyfin Slideshow by M0RPH3US v6.0.0
+ * Jellyfin Slideshow by M0RPH3US v6.1.3
*
* The Archivo Narrow webfont is linked from index.html, not @import-ed here:
* an @import cannot start downloading until this stylesheet has itself been
diff --git a/slideshowpure.js b/slideshowpure.js
index 8f67dd5..dc310fc 100644
--- a/slideshowpure.js
+++ b/slideshowpure.js
@@ -1,5 +1,5 @@
/*
- * Jellyfin Slideshow by M0RPH3US v6.1.0
+ * Jellyfin Slideshow by M0RPH3US v6.1.3
*/
const CONFIG = {
@@ -73,6 +73,9 @@ const CONFIG = {
// 0-100. Volume once unmuted. Trailers always start muted.
trailerVolume: 50,
+ // true|false. Show the uploader's default captions on YouTube trailers. Off by default: the trailer fills the bar behind the home rows, so captions land on top of them.
+ trailerCaptions: false,
+
// px. At or below this width the layout is swipe-only: no arrows, no trailer, no volume.
touchLayoutMaxWidth: 767,
@@ -339,6 +342,10 @@ const setConfig = (patch) => {
saveStoredConfig(patch);
const changed = applyConfig(patch, { label: "runtime" });
+ if (changed.includes("shuffleInterval") && STATE.slideshow.slideInterval) {
+ STATE.slideshow.slideInterval.interval = CONFIG.shuffleInterval;
+ }
+
if (changed.some((key) => STRUCTURAL_CONFIG_KEYS.has(key))) {
resetSlideshowState();
bootstrap();
@@ -412,6 +419,17 @@ const STATE = {
},
};
+// YouTube loads its caption module once playback starts, so this runs on ready and again on play.
+const hideTrailerCaptions = (player) => {
+ if (CONFIG.trailerCaptions) return;
+ try {
+ player.unloadModule("captions");
+ player.unloadModule("cc");
+ } catch (err) {
+ // player not ready or no caption module: nothing to hide
+ }
+};
+
const loadYouTubeAPI = () => {
if (STATE.slideshow.ytPromise) return STATE.slideshow.ytPromise;
@@ -1889,10 +1907,11 @@ class SlideTimer {
this.startedAt = Date.now() - (this.interval - this.remaining);
this.resumeId = setTimeout(() => {
this.resumeId = null;
- this.callback();
+
this.startedAt = Date.now();
this.remaining = this.interval;
this.timerId = setInterval(this.callback, this.interval);
+ this.callback();
}, this.remaining);
return this;
}
@@ -2894,7 +2913,10 @@ const SlideCreator = {
);
const host = document.getElementById(`yt-player-${itemId}`);
- host?.replaceChildren(video);
+ if (host) {
+ host.textContent = "";
+ host.appendChild(video);
+ }
STATE.slideshow.players[itemId] = createLocalPlayer(video);
STATE.slideshow.slideVideoIds[itemId] = `local:${itemId}`;
} else {
@@ -2924,13 +2946,18 @@ const SlideCreator = {
start: startTime,
},
events: {
- onStateChange: (e) =>
+ onStateChange: (e) => {
+ if (e.data === YT.PlayerState.PLAYING) hideTrailerCaptions(e.target);
SlideshowManager.onPlayerStateChange(
e,
itemId,
trailerContainer,
- ),
- onReady: (e) => e.target.mute(),
+ );
+ },
+ onReady: (e) => {
+ e.target.mute();
+ hideTrailerCaptions(e.target);
+ },
onError: (e) =>
SlideshowManager.onPlayerError(e, itemId, trailerContainer),
@@ -2961,7 +2988,7 @@ const SlideshowManager = {
container.appendChild(dotsContainer);
}
- dotsContainer.replaceChildren();
+ dotsContainer.textContent = "";
const count = Math.min(STATE.slideshow.totalItems, CONFIG.maxDots);
@@ -3214,13 +3241,13 @@ const SlideshowManager = {
STATE.slideshow.trailerStartTimer = setTimeout(() => {
this.startTrailer(index, currentItemId, fallbackToTimer);
}, CONFIG.trailerStartDelayMs);
- } else {
- fallbackToTimer();
}
+ fallbackToTimer();
+
function fallbackToTimer() {
if (!STATE.slideshow.isPaused && STATE.slideshow.slideInterval) {
- STATE.slideshow.slideInterval.restart();
+ STATE.slideshow.slideInterval.start();
}
}
@@ -3686,6 +3713,8 @@ const SlideshowManager = {
STATE.slideshow.isVideoPlaying = true;
this.setTrailerVisible(itemId, trailerContainer, true);
STATE.slideshow.slideInterval?.stop();
+
+ this.setProgressRunning(false);
},
onTrailerEnded(itemId, trailerContainer) {
@@ -3694,6 +3723,28 @@ const SlideshowManager = {
this.nextSlide();
},
+ resumeAfterTrailer(itemId) {
+ const currentItemId =
+ STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
+ if (itemId !== currentItemId) return;
+
+ if (
+ STATE.slideshow.isPaused ||
+ STATE.slideshow.hoverHeld ||
+ document.hidden
+ ) {
+ return;
+ }
+
+ const timer = STATE.slideshow.slideInterval;
+ if (!timer || timer.timerId || timer.resumeId) return;
+
+ timer.start();
+ this.restartProgress(
+ document.querySelector(`.slide[data-item-id="${itemId}"]`),
+ );
+ },
+
onPlayerStateChange(event, itemId, trailerContainer) {
if (!document.querySelector(`.slide[data-item-id="${itemId}"]`)) return;
@@ -3708,7 +3759,13 @@ const SlideshowManager = {
event.data === YTRef.PlayerState.PAUSED ||
event.data === YTRef.PlayerState.CUED
) {
+
+ const currentItemId =
+ STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
+ if (itemId !== currentItemId) return;
+
STATE.slideshow.isVideoPlaying = false;
+ this.resumeAfterTrailer(itemId);
}
},
@@ -3717,10 +3774,6 @@ const SlideshowManager = {
`Trailer for ${itemId} failed with YouTube error ${event?.data}; continuing without it.`,
);
- clearTimeout(STATE.slideshow.trailerWatchdog);
- STATE.slideshow.trailerWatchdog = null;
- STATE.slideshow.isVideoPlaying = false;
-
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
trailerContainer?.classList.remove("active");
slide?.querySelector(".backdrop")?.classList.remove("with-video");
@@ -3728,9 +3781,15 @@ const SlideshowManager = {
STATE.slideshow.slideVideoIds[itemId] = null;
- if (!STATE.slideshow.isPaused && STATE.slideshow.slideInterval) {
- STATE.slideshow.slideInterval.restart();
- }
+ const currentItemId =
+ STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
+ if (itemId !== currentItemId) return;
+
+ clearTimeout(STATE.slideshow.trailerWatchdog);
+ STATE.slideshow.trailerWatchdog = null;
+ STATE.slideshow.isVideoPlaying = false;
+
+ this.resumeAfterTrailer(itemId);
},
async loadSlideshowData() {
@@ -3958,6 +4017,7 @@ const SettingsPanel = {
step: 5,
format: (value) => `${value}%`,
},
+ { key: "trailerCaptions", label: "Trailer captions", type: "switch" },
{
key: "trailerLibraries",
label: "Trailers from",
@@ -4399,9 +4459,11 @@ const initPageVisibilityHandler = () => {
if (document.hidden) {
console.log("Tab inactive - pausing slideshow and videos");
wasVideoPlayingBeforeHide = STATE.slideshow.isVideoPlaying;
+
if (STATE.slideshow.slideInterval) {
- STATE.slideshow.slideInterval.stop();
+ STATE.slideshow.slideInterval.pause();
}
+ SlideshowManager.setProgressRunning(false);
const currentItemId =
STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
if (currentItemId && STATE.slideshow.players[currentItemId]) {
@@ -4433,14 +4495,16 @@ const initPageVisibilityHandler = () => {
} catch (e) {
console.warn("Error resuming video on tab show:", e);
if (STATE.slideshow.slideInterval) {
- STATE.slideshow.slideInterval.start();
+ STATE.slideshow.slideInterval.resume();
}
+ SlideshowManager.setProgressRunning(true);
}
}
} else {
if (STATE.slideshow.slideInterval) {
- STATE.slideshow.slideInterval.start();
+ STATE.slideshow.slideInterval.resume();
}
+ SlideshowManager.setProgressRunning(true);
}
wasVideoPlayingBeforeHide = false;
}