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 -![jsDelivr hits (GitHub)](https://img.shields.io/jsdelivr/gh/hm/makd/Jellyfin-Media-Bar?style=for-the-badge&logo=javascript&logoColor=white&labelColor=F0DB4F&color=323330&cacheSeconds=3600) +
+![jsDelivr hits (GitHub)](https://img.shields.io/jsdelivr/gh/hm/makd/Jellyfin-Media-Bar?style=for-the-badge&logo=javascript&logoColor=white&labelColor=F0DB4F&color=323330&cacheSeconds=3600) +![jsDelivr hits (Plugin)](https://img.shields.io/jsdelivr/gh/hm/IAmParadox27/jellyfin-plugin-media-bar?style=for-the-badge&logo=javascript&logoColor=white&labelColor=F0DB4F&color=435274&cacheSeconds=3600) + +
**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; }