diff --git a/README.md b/README.md
index da7313c..8bb524e 100644
--- a/README.md
+++ b/README.md
@@ -65,12 +65,8 @@ Credits for this plugin go to @MakD for his original work and to @BobHasNoSoul a
# Jellyfin-Media-Bar - Now with Play Now Function
-> [!NOTE]
-> The Media Bar is currently partly compatible with the Jellyfin 10.11.x update. We’re aware of the changes required to make it fully compatible and will be addressing them soon.
->
-> I’ll be away on vacation for a short while, so there will be a temporary delay in releasing the next update. I kindly ask that you refrain from opening new bug reports related to 10.11.x compatibility during this period — we’ve got it on our radar.
->
-> Thank you for your patience and understanding! The fixes will be rolled out shortly after I return.
+
+
**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 8982932..7a7e1c7 100644
--- a/slideshowpure.css
+++ b/slideshowpure.css
@@ -1,5 +1,5 @@
/*
- * Jellyfin Slideshow by M0RPH3US v3.0.6
+ * Jellyfin Slideshow by M0RPH3US v3.0.8
*/
@import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap);
@@ -28,11 +28,11 @@
@keyframes kenBurnsZoomIn {
from {
- transform: scale(1);
+ transform: scale3d(1, 1, 0);
}
to {
- transform: scale(1.1);
+ transform: scale3d(1.1, 1.1, 0.1);
}
}
@@ -115,6 +115,10 @@
transition: width 0.2s ease-in-out;
}
+.layout-mobile .splashLogo {
+height: 12%;
+}
+
.backdrop.low-quality {
filter: blur(0.5px);
transform: scale(1.01);
diff --git a/slideshowpure.js b/slideshowpure.js
index 256b3cf..f5f6924 100644
--- a/slideshowpure.js
+++ b/slideshowpure.js
@@ -1,5 +1,5 @@
/*
- * Jellyfin Slideshow by M0RPH3US v3.0.6
+ * Jellyfin Slideshow by M0RPH3US v3.0.8
*/
//Core Module Configuration
@@ -216,31 +216,53 @@ const initLoadingScreen = () => {
const checkInterval = setInterval(() => {
const loginFormLoaded = document.querySelector(".manualLoginForm");
- const homePageLoaded =
- document.querySelector(".homeSectionsContainer") &&
- document.querySelector("#slides-container");
+ const activeTab = document.querySelector(".pageTabContent.is-active");
- if (loginFormLoaded || homePageLoaded) {
- clearInterval(progressInterval);
- clearInterval(checkInterval);
+ if (loginFormLoaded) {
+ finishLoading();
+ return;
+ }
- progressBar.style.transition = "width 300ms ease-in-out";
- progressBar.style.width = "100%";
- unfilledBar.style.width = "0%";
+ if (activeTab) {
+ const tabIndex = activeTab.getAttribute("data-index");
- progressBar.addEventListener('transitionend', () => {
- requestAnimationFrame(() => {
- const loader = document.querySelector(".bar-loading");
- if (loader) {
- loader.style.opacity = '0';
- setTimeout(() => {
- loader.remove();
- }, 300);
- }
- });
- })
+ if (tabIndex === "0") {
+ const homeSections = document.querySelector(".homeSectionsContainer");
+ const slidesContainer = document.querySelector("#slides-container");
+
+ if (homeSections && slidesContainer) {
+ finishLoading();
+ }
+ } else {
+ if (
+ activeTab.children.length > 0 ||
+ activeTab.innerText.trim().length > 0
+ ) {
+ finishLoading();
+ }
+ }
}
}, CONFIG.loadingCheckInterval);
+
+ const finishLoading = () => {
+ clearInterval(progressInterval);
+ clearInterval(checkInterval);
+ progressBar.style.transition = "width 300ms ease-in-out";
+ progressBar.style.width = "100%";
+ unfilledBar.style.width = "0%";
+
+ progressBar.addEventListener("transitionend", () => {
+ requestAnimationFrame(() => {
+ const loader = document.querySelector(".bar-loading");
+ if (loader) {
+ loader.style.opacity = "0";
+ setTimeout(() => {
+ loader.remove();
+ }, 300);
+ }
+ });
+ });
+ };
};
/**
@@ -490,18 +512,9 @@ const LocalizationUtils = {
if (window.ApiClient && STATE.jellyfinData?.accessToken) {
try {
- const userId = window.ApiClient.getCurrentUserId();
- if (userId) {
- const userUrl = window.ApiClient.getUrl(`Users/${userId}`);
- const userResponse = await fetch(userUrl, {
- headers: ApiUtils.getAuthHeaders(),
- });
- if (userResponse.ok) {
- const userData = await userResponse.json();
- if (userData.Configuration?.AudioLanguagePreference) {
- locale = userData.Configuration.AudioLanguagePreference.toLowerCase();
- }
- }
+ const userData = window.ApiClient.getCurrentUser();
+ if (userData.Configuration?.AudioLanguagePreference) {
+ locale = userData.Configuration.AudioLanguagePreference.toLowerCase();
}
} catch (error) {
console.warn("Could not fetch user language preference:", error);
@@ -510,7 +523,7 @@ const LocalizationUtils = {
if (!locale && window.ApiClient && STATE.jellyfinData?.accessToken) {
try {
- const configUrl = window.ApiClient.getUrl('System/Configuration');
+ const configUrl = window.ApiClient.getUrl("System/Configuration");
const configResponse = await fetch(configUrl, {
headers: ApiUtils.getAuthHeaders(),
});
@@ -540,6 +553,18 @@ const LocalizationUtils = {
locale = navLang ? navLang.toLowerCase() : "en-us";
}
+ // Convert 3-letter country codes to 2-letter if necessary
+ if (locale.length === 3) {
+ const countriesData = await window.ApiClient.getCountries();
+ const countryData = Object.values(countriesData).find(
+ (countryData) =>
+ countryData.ThreeLetterISORegionName === locale.toUpperCase()
+ );
+ if (countryData) {
+ locale = countryData.TwoLetterISORegionName.toLowerCase();
+ }
+ }
+
this.cachedLocale = locale;
return locale;
},
@@ -550,7 +575,7 @@ const LocalizationUtils = {
* @returns {string|null} URL to translation chunk or null
*/
findTranslationChunkUrl(locale) {
- const localePrefix = locale.split('-')[0];
+ const localePrefix = locale.split("-")[0];
if (this.chunkUrlCache[localePrefix]) {
return this.chunkUrlCache[localePrefix];
@@ -558,10 +583,14 @@ const LocalizationUtils = {
if (window.performance && window.performance.getEntriesByType) {
try {
- const resources = window.performance.getEntriesByType('resource');
+ const resources = window.performance.getEntriesByType("resource");
for (const resource of resources) {
const url = resource.name || resource.url;
- if (url && url.includes(`${localePrefix}-json`) && url.includes('.chunk.js')) {
+ if (
+ url &&
+ url.includes(`${localePrefix}-json`) &&
+ url.includes(".chunk.js")
+ ) {
this.chunkUrlCache[localePrefix] = url;
return url;
}
@@ -596,17 +625,19 @@ const LocalizationUtils = {
const response = await fetch(chunkUrl);
if (!response.ok) {
- throw new Error(`Failed to fetch translations: ${response.statusText}`);
+ throw new Error(
+ `Failed to fetch translations: ${response.statusText}`
+ );
}
const chunkText = await response.text();
-
+
let jsonMatch = chunkText.match(/JSON\.parse\(['"](.*?)['"]\)/);
if (jsonMatch) {
let jsonString = jsonMatch[1]
.replace(/\\"/g, '"')
- .replace(/\\n/g, '\n')
- .replace(/\\\\/g, '\\')
+ .replace(/\\n/g, "\n")
+ .replace(/\\\\/g, "\\")
.replace(/\\'/g, "'");
try {
this.translations[locale] = JSON.parse(jsonString);
@@ -615,9 +646,9 @@ const LocalizationUtils = {
// Try direct extraction
}
}
-
- const jsonStart = chunkText.indexOf('{');
- const jsonEnd = chunkText.lastIndexOf('}') + 1;
+
+ const jsonStart = chunkText.indexOf("{");
+ const jsonEnd = chunkText.lastIndexOf("}") + 1;
if (jsonStart !== -1 && jsonEnd > jsonStart) {
const jsonString = chunkText.substring(jsonStart, jsonEnd);
try {
@@ -645,17 +676,17 @@ const LocalizationUtils = {
* @returns {string} Localized string or fallback
*/
getLocalizedString(key, fallback, ...args) {
- const locale = this.cachedLocale || 'en-us';
+ const locale = this.cachedLocale || "en-us";
let translated = this.translations[locale]?.[key] || fallback;
if (args.length > 0) {
for (let i = 0; i < args.length; i++) {
- translated = translated.replace(new RegExp(`\\{${i}\\}`, 'g'), args[i]);
+ translated = translated.replace(new RegExp(`\\{${i}\\}`, "g"), args[i]);
}
}
return translated;
- }
+ },
};
/**
@@ -823,7 +854,8 @@ const ApiUtils = {
async getSessionId() {
try {
const response = await fetch(
- `${STATE.jellyfinData.serverAddress
+ `${
+ STATE.jellyfinData.serverAddress
}/Sessions?deviceId=${encodeURIComponent(STATE.jellyfinData.deviceId)}`,
{
headers: this.getAuthHeaders(),
@@ -876,7 +908,7 @@ const ApiUtils = {
} catch (error) {
console.error("Error toggling favorite:", error);
}
- }
+ },
};
/**
@@ -989,7 +1021,11 @@ const SlideCreator = {
// Handle Backdrop images
if (imageType === "Backdrop") {
// Check BackdropImageTags array first
- if (item.BackdropImageTags && Array.isArray(item.BackdropImageTags) && item.BackdropImageTags.length > 0) {
+ if (
+ item.BackdropImageTags &&
+ Array.isArray(item.BackdropImageTags) &&
+ item.BackdropImageTags.length > 0
+ ) {
const backdropIndex = index !== undefined ? index : 0;
if (backdropIndex < item.BackdropImageTags.length) {
tag = item.BackdropImageTags[backdropIndex];
@@ -1017,7 +1053,7 @@ const SlideCreator = {
// Build URL with tag and quality if tag is available, otherwise quality-only fallback
if (tag) {
// Use both tag and quality for cacheable, quality-controlled images
- const qualityParam = quality !== undefined ? `&quality=${quality}` : '';
+ const qualityParam = quality !== undefined ? `&quality=${quality}` : "";
return `${baseUrl}?tag=${tag}${qualityParam}`;
} else {
// Fallback to quality-only URL if no tag is available
@@ -1052,7 +1088,7 @@ const SlideCreator = {
const backdrop = SlideUtils.createElement("img", {
className: "backdrop high-quality",
src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60),
- alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'),
+ alt: LocalizationUtils.getLocalizedString("Backdrop", "Backdrop"),
loading: "eager",
});
@@ -1113,7 +1149,7 @@ const SlideCreator = {
const genreElement = SlideUtils.createElement("div", {
className: "genre",
- innerHTML: SlideUtils.parseGenres(item.Genres)
+ innerHTML: SlideUtils.parseGenres(item.Genres),
});
const buttonContainer = SlideUtils.createElement("div", {
@@ -1162,7 +1198,9 @@ const SlideCreator = {
if (typeof communityRating === "number") {
const container = SlideUtils.createElement("div", {
className: "star-rating-container",
- innerHTML: `${communityRating.toFixed(1)}`,
+ innerHTML: `${communityRating.toFixed(
+ 1
+ )}`,
});
miscInfo.appendChild(container);
miscInfo.appendChild(SlideUtils.createSeparator());
@@ -1170,14 +1208,17 @@ const SlideCreator = {
// Critic Rating Section (Rotten Tomatoes)
if (typeof criticRating === "number") {
- const svgIcon = criticRating < 60 ? CONFIG.IMAGE_SVG.rottenTomato : CONFIG.IMAGE_SVG.freshTomato;
+ const svgIcon =
+ criticRating < 60
+ ? CONFIG.IMAGE_SVG.rottenTomato
+ : CONFIG.IMAGE_SVG.freshTomato;
const container = SlideUtils.createElement("div", {
className: "critic-rating",
innerHTML: `${svgIcon}${criticRating.toFixed(0)}%`,
- })
+ });
miscInfo.appendChild(container);
miscInfo.appendChild(SlideUtils.createSeparator());
- };
+ }
// Year Section
if (typeof premiereDate === "string" && !isNaN(new Date(premiereDate))) {
@@ -1187,7 +1228,7 @@ const SlideCreator = {
});
miscInfo.appendChild(container);
miscInfo.appendChild(SlideUtils.createSeparator());
- };
+ }
// Age Rating Section
if (typeof ageRating === "string") {
@@ -1200,7 +1241,7 @@ const SlideCreator = {
});
miscInfo.appendChild(container);
miscInfo.appendChild(SlideUtils.createSeparator());
- };
+ }
// Runtime / Seasons Section
if (seasonCount !== undefined || runtime !== undefined) {
@@ -1208,7 +1249,13 @@ const SlideCreator = {
className: "runTime",
});
if (seasonCount) {
- const seasonText = seasonCount <= 1 ? LocalizationUtils.getLocalizedString('Season', 'Season') : LocalizationUtils.getLocalizedString('TypeOptionPluralSeason', 'Seasons');
+ const seasonText =
+ seasonCount <= 1
+ ? LocalizationUtils.getLocalizedString("Season", "Season")
+ : LocalizationUtils.getLocalizedString(
+ "TypeOptionPluralSeason",
+ "Seasons"
+ );
container.innerHTML = `${seasonCount} ${seasonText}`;
} else {
const milliseconds = runtime / 10000;
@@ -1216,7 +1263,11 @@ const SlideCreator = {
const endTime = new Date(currentTime.getTime() + milliseconds);
const options = { hour: "2-digit", minute: "2-digit", hour12: false };
const formattedEndTime = endTime.toLocaleTimeString([], options);
- const endsAtText = LocalizationUtils.getLocalizedString('EndsAtValue', 'Ends at {0}', formattedEndTime);
+ const endsAtText = LocalizationUtils.getLocalizedString(
+ "EndsAtValue",
+ "Ends at {0}",
+ formattedEndTime
+ );
container.innerText = endsAtText;
}
miscInfo.appendChild(container);
@@ -1231,7 +1282,7 @@ const SlideCreator = {
* @returns {HTMLElement} Play button element
*/
createPlayButton(itemId) {
- const playText = LocalizationUtils.getLocalizedString('Play', 'Play');
+ const playText = LocalizationUtils.getLocalizedString("Play", "Play");
return SlideUtils.createElement("button", {
className: "detailButton btnPlay play-button",
innerHTML: `
@@ -1291,7 +1342,6 @@ const SlideCreator = {
return button;
},
-
/**
* Creates a placeholder slide for loading
* @param {string} itemId - Item ID to load
@@ -1350,7 +1400,6 @@ const SlideCreator = {
* Manages slideshow functionality
*/
const SlideshowManager = {
-
createPaginationDots() {
let dotsContainer = document.querySelector(".dots-container");
if (!dotsContainer) {
@@ -1580,17 +1629,20 @@ const SlideshowManager = {
togglePause() {
STATE.slideshow.isPaused = !STATE.slideshow.isPaused;
- const pauseButton = document.querySelector('.pause-button');
+ const pauseButton = document.querySelector(".pause-button");
if (STATE.slideshow.isPaused) {
STATE.slideshow.slideInterval.stop();
pauseButton.innerHTML = 'play_arrow';
- const playLabel = LocalizationUtils.getLocalizedString('Play', 'Play');
+ const playLabel = LocalizationUtils.getLocalizedString("Play", "Play");
pauseButton.setAttribute("aria-label", playLabel);
pauseButton.setAttribute("title", playLabel);
} else {
STATE.slideshow.slideInterval.start();
pauseButton.innerHTML = 'pause';
- const pauseLabel = LocalizationUtils.getLocalizedString('ButtonPause', 'Pause');
+ const pauseLabel = LocalizationUtils.getLocalizedString(
+ "ButtonPause",
+ "Pause"
+ );
pauseButton.setAttribute("aria-label", pauseLabel);
pauseButton.setAttribute("title", pauseLabel);
}
@@ -1769,13 +1821,13 @@ const initArrowNavigation = () => {
className: "pause-button",
innerHTML: 'pause',
tabIndex: "0",
- "aria-label": LocalizationUtils.getLocalizedString('ButtonPause', 'Pause'),
- title: LocalizationUtils.getLocalizedString('ButtonPause', 'Pause'),
+ "aria-label": LocalizationUtils.getLocalizedString("ButtonPause", "Pause"),
+ title: LocalizationUtils.getLocalizedString("ButtonPause", "Pause"),
onclick: (e) => {
e.preventDefault();
e.stopPropagation();
SlideshowManager.togglePause();
- }
+ },
});
container.appendChild(leftArrow);