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. +![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) + **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);