Merge pull request #114 from MrRussLuther/sync-page-backdrop

Sync the page backdrop with the featured slide
This commit is contained in:
M0RPH3US
2026-08-11 21:07:52 +05:30
committed by GitHub
+161
View File
@@ -36,6 +36,7 @@ const CONFIG = {
"described audio",
"vertical",
],
syncPageBackdrop: true,
youtubeApiLoadTimeoutMs: 8000,
};
@@ -1133,6 +1134,7 @@ const VisibilityObserver = {
STATE.slideshow.players = {};
container.querySelectorAll(".slide").forEach((slide) => slide.remove());
STATE.slideshow.createdSlides = {};
PageBackdrop.clear();
}
this.wasVisible = isVisible;
@@ -1159,6 +1161,163 @@ const VisibilityObserver = {
},
};
/**
* Mirrors the featured slide into Jellyfin's own page backdrop layer, so the
* background behind the home page follows whatever the slideshow is showing
* instead of staying on unrelated library art.
*
* Reuses the exact backdrop URL the active slide already requested, so the
* browser serves it from cache and no extra image is fetched.
*
* Only active while the slideshow is visible. Leaving the home page restores
* Jellyfin's normal backdrop behaviour, so no other page is affected.
* Disable with CONFIG.syncPageBackdrop.
*/
const PageBackdrop = {
LAYER_CLASS: "slideshow-page-backdrop",
observer: null,
isWriting: false,
currentItemId: null,
/**
* Jellyfin creates .backdropContainer lazily, so it may not exist yet
* @returns {HTMLElement} The backdrop container
*/
getOrCreateContainer() {
let container = document.querySelector(".backdropContainer");
if (!container) {
container = SlideUtils.createElement("div", {
className: "backdropContainer",
});
document.body.insertBefore(container, document.body.firstChild);
}
return container;
},
/**
* @param {HTMLElement} container - The backdrop container
* @returns {HTMLElement} Our own backdrop layer, created if needed
*/
getOrCreateLayer(container) {
let layer = container.querySelector(`.${this.LAYER_CLASS}`);
if (!layer || !layer.isConnected) {
layer = SlideUtils.createElement("div", {
className: `backdropImage ${this.LAYER_CLASS}`,
});
container.appendChild(layer);
}
return layer;
},
/**
* Jellyfin runs its own backdrop rotator into the same container: it adds
* .backdropImage siblings and cycles them on a timer, which paints over
* whatever we set. While the slideshow owns the screen, keep our layer the
* only one and make sure it stays last.
* @param {HTMLElement} container - The backdrop container
*/
evictForeignLayers(container) {
const layers = container.querySelectorAll(".backdropImage");
layers.forEach((layer) => {
if (!layer.classList.contains(this.LAYER_CLASS)) {
layer.remove();
}
});
const ours = container.querySelector(`.${this.LAYER_CLASS}`);
if (ours && ours !== container.lastElementChild) {
container.appendChild(ours);
}
},
/**
* Reacts to the rotator instead of polling for it
*/
startObserver(container) {
if (this.observer) return;
this.observer = new MutationObserver(() => {
// Our own eviction mutates the container, which would re-enter here
if (this.isWriting) return;
this.isWriting = true;
try {
this.evictForeignLayers(container);
} finally {
this.isWriting = false;
}
});
this.observer.observe(container, { childList: true });
},
stopObserver() {
if (!this.observer) return;
this.observer.disconnect();
this.observer = null;
},
/**
* Points the page backdrop at the given item
* @param {string} itemId - Id of the item the slideshow is showing
*/
update(itemId) {
if (!CONFIG.syncPageBackdrop) return;
const item = STATE.slideshow.loadedItems[itemId];
// Item data arrives with the slide. Until it does, leave the previous
// backdrop up rather than clearing it, so the page does not flash.
if (!item) return;
const src = SlideCreator.buildImageUrl(
item,
"Backdrop",
0,
STATE.jellyfinData.serverAddress,
60,
);
if (!src) return;
const container = this.getOrCreateContainer();
this.isWriting = true;
try {
const layer = this.getOrCreateLayer(container);
if (this.currentItemId !== itemId || layer.style.backgroundImage === "") {
layer.style.backgroundImage = `url("${src.replace(/"/g, "%22")}")`;
layer.classList.remove("backdropImageFadeIn");
// Force a reflow so the fade replays for the new image
void layer.offsetWidth;
layer.classList.add("backdropImageFadeIn");
this.currentItemId = itemId;
}
this.evictForeignLayers(container);
} finally {
this.isWriting = false;
}
document
.querySelector(".backgroundContainer")
?.classList.add("withBackdrop");
this.startObserver(container);
},
/**
* Hands the backdrop back to Jellyfin
*/
clear() {
this.stopObserver();
this.currentItemId = null;
document.querySelector(`.${this.LAYER_CLASS}`)?.remove();
document
.querySelector(".backgroundContainer")
?.classList.remove("withBackdrop");
},
};
/**
* Slideshow UI creation and management
*/
@@ -1803,6 +1962,7 @@ const SlideshowManager = {
STATE.slideshow.currentSlideIndex = index;
this.updateDots();
PageBackdrop.update(currentItemId);
this.preloadAdjacentSlides(index);
this.pruneSlideCache();
@@ -2443,6 +2603,7 @@ window.slideshowPure = {
SlideCreator,
SlideshowManager,
VisibilityObserver,
PageBackdrop,
initSlideshowData: () => {
SlideshowManager.loadSlideshowData();
},