diff --git a/README.md b/README.md index 206feb2..da7313c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ As with a lot of devs, I am very momentum based in my personal life coding and t ### Prerequisites - This plugin is based on Jellyfin Version `10.10.7` - The following plugins are required to also be installed, please following their installation guides: - - File Transformation (https://github.com/IAmParadox27/jellyfin-plugin-file-transformation) at least v2.2.1.0 + - File Transformation (https://github.com/IAmParadox27/jellyfin-plugin-file-transformation) at least v2.2.1.0 ### Installation 1. Add `https://www.iamparadox.dev/jellyfin/plugins/manifest.json` to your plugin repositories. @@ -62,128 +62,64 @@ Credits for this plugin go to @MakD for his original work and to @BobHasNoSoul a
Original README.md from MakD - -## TODO - Design changes upcoming next release -Thanks to the Man, the Legend [BobHasNoSoul](https://github.com/BobHasNoSoul) for his work on the [jellyfinfeatured](https://github.com/BobHasNoSoul/jellyfin-featured) and [SethBacon](https://forum.jellyfin.org/u-sethbacon) and [TedHinklater](https://github.com/tedhinklater) for their take on the [Jellyfin-Featured-Content-Bar](https://github.com/tedhinklater/Jellyfin-Featured-Content-Bar). +# Jellyfin-Media-Bar - Now with Play Now Function -Here I present my version of the same with some code improvements, loading optimizations, and Security Enhancements. Works best with the [Zombie theme](https://github.com/MakD/zombie-release) (_Shameless Plug_), but it fits with every other theme the creators have put their hard work in. You might've to edit the color accents in the CSS to match yours. +> [!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)_** + +The fullscreen version has a new look (in beta), and support for different screen sizes has been added. For any visual goof-ups, please open a bug report, including the device being used and whether it is encountered in portrait or landscape mode. + + +Thanks to the Man, the Legend [BobHasNoSoul](https://github.com/BobHasNoSoul) for his work on the [jellyfinfeatured](https://github.com/BobHasNoSoul/jellyfin-featured) and [SethBacon](https://forum.jellyfin.org/u-sethbacon) and [TedHinklater](https://github.com/tedhinklater) for their take on the [Jellyfin-Featured-Content-Bar](https://github.com/tedhinklater/Jellyfin-Featured-Content-Bar). + +Here I present my version with some code improvements, loading optimizations, and security enhancements. Works best with the [Zombie theme](https://github.com/MakD/zombie-release) (_Shameless Plug_ `@import url(https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/zombie_revived.css);`, visit the repo for more color schemes). + + +> **Before Installing, please take a backup of your index.html file**
Desktop Layout - -![Jellyfin Desktop Layout](https://raw.githubusercontent.com/MakD/Jellyfin-Media-Bar/refs/heads/main/img/Jelly-Web.png) - + +![Jellyfin Desktop Layout](https://raw.githubusercontent.com/MakD/Jellyfin-Media-Bar/refs/heads/main/img/Jelly-Web%20-%20Fullscreen%20Mode.png) +
+ Mobile Layout - -![Jellyfin Mobile Layout](https://raw.githubusercontent.com/MakD/Jellyfin-Media-Bar/refs/heads/main/img/Jelly-Mobile.png) + +![Jellyfin Mobile Layout](https://raw.githubusercontent.com/MakD/Jellyfin-Media-Bar/refs/heads/main/img/Jelly-Mobile-Fullscreen.png)
-> **Before Installing, please take a backup of your index.html and home-html.xxxxxx.chunk.js files** - -# Prepping the Environment - -
- - Steps - -1. Create a folder `avatars` in your `jellyfin-web` folder. (Usually in C:\Program Files\Jellyfin\Server) -2. Download the files `slideshowpure.js` and `slideshowpure.css` -3. Paste them inside the avatars folder created, and you are ready to venture down the rabbit hole. - -
# Prepping the files
- + index.html - 1. Navigate to your `jellyfin-web` folder and search for the file index.html. (you can use any code editor, just remember to open with administrator privileges. - 2. Search for `` - 3. Just before the `` +3. Just before the ``, plug the below code ``` - - - - - + + ```
-
- -home-html.xxxxxx.chunk.js - -1. Similarly, search for `home-html` in the `jellyfin-web` directory. You should be able to see a file named `home-html.xxxxxx.chunk.js` with random numbers in place of the `xxxx`. Open it with any code editor with administrator privileges. -2. Search for `id="homeTab" data-index="0">` -3. Right after the `>`, paste the code block `
` - -
- And that is it. Hard refresh your web page (CTRL+Shift+R) twice, and Profit! # Want a Custom List to be showcased instead of random items?? -No worries this got you covered. +No worries this got you covered. ## Steps @@ -200,4 +136,27 @@ ItemID4 ItemID5 ``` The next time it loads, it will display these items. + +# Uninstall the Bar + +
+ + Roll Back + +Restore the `index.html` file / remove the lines added and you are good to go!!! + +
+ + +## License + +[![Custom: DBAD License](https://img.shields.io/badge/License-Don't_Be_A_Dick-red)](LICENSE) + + +This project is licensed under a DBAD license prohibiting any commercial use or redistribution. +All modifications must be contributed back to this repository. +Attribution to the original author (MakD) is required in any use or derivative work. + +Please take a look at the [LICENSE](LICENSE) file for full terms. +
diff --git a/slideshowpure.css b/slideshowpure.css index ab5e657..c21474d 100644 --- a/slideshowpure.css +++ b/slideshowpure.css @@ -1,11 +1,12 @@ /* - * Jellyfin Slideshow by M0RPH3US v3.0.0 + * Jellyfin Slideshow by M0RPH3US v3.0.4 */ @import url(https://fonts.googleapis.com/css2?family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap); .backdrop.animate { - animation: frostedGlass 1.2s cubic-bezier(0.4, 0, 0.2, 1), + animation: + frostedGlass 1.2s cubic-bezier(0.4, 0, 0.2, 1), kenBurnsZoomIn 10s ease-out forwards; } @@ -75,7 +76,7 @@ align-items: center; } -.bar-loading h1 img { +.bar-loading h1 div { width: 250px; max-height: 250px; display: block; @@ -117,13 +118,17 @@ .backdrop.low-quality { filter: blur(0.5px); transform: scale(1.01); - transition: filter 0.3s ease-in-out, transform 0.3s ease-in-out; + transition: + filter 0.3s ease-in-out, + transform 0.3s ease-in-out; } .backdrop.high-quality { filter: blur(0); transform: scale(1); - transition: filter 0.3s ease-in-out, transform 0.3s ease-in-out; + transition: + filter 0.3s ease-in-out, + transform 0.3s ease-in-out; } .logo.low-quality { @@ -174,6 +179,42 @@ font-size: 12px; } +.pause-button { + position: absolute; + top: 5rem; + right: 0.8rem; + cursor: pointer; + color: white; + z-index: 10; + opacity: 0.3; + transition: opacity 0.3s ease; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); +} + +.pause-button i { + font-size: 1.5rem; +} + +.pause-button:hover { + opacity: 0.9; +} + +@media (max-width: 1599px) { + .pause-button { + top: 8rem; + } + + .pause-button i { + font-size: 2rem; + } +} + +@media (max-width: 768px) { + .pause-button i { + font-size: 2.5rem; + } +} + .dots-container { position: absolute; top: calc(50% + 18vh); @@ -195,7 +236,8 @@ background-color: #cecece99; border-radius: 50%; transform-origin: center; - transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), + transition: + transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease-in-out; } @@ -330,6 +372,7 @@ width: auto; object-fit: contain; filter: brightness(1.5); + font-size: 4rem; } .plot-container { @@ -356,6 +399,10 @@ } .genre { + display: flex; + gap: 5px; + align-items: center; + justify-content: center; position: absolute; top: calc(50% + 4vh); left: 4vw; @@ -363,6 +410,7 @@ color: #fff; font-family: "Archivo Narrow", sans-serif; z-index: 5; + flex-wrap: wrap; } .button-container { @@ -390,6 +438,7 @@ font-weight: 700; gap: 6px; -webkit-tap-highlight-color: #fff0; + border-radius: 8px; } .detail-button { @@ -471,63 +520,54 @@ font-weight: 500; } -.rating-value { +.misc-info { font-family: "Archivo Narrow", sans-serif; display: flex; align-items: center; z-index: 5; position: relative; + gap: 10px; } .runTime { font-family: "Archivo Narrow", sans-serif; display: flex; align-items: center; - margin-left: 5px; - margin-right: 5px; align-content: center; justify-content: center; flex-wrap: wrap; font-weight: 500; } -.tomato-rating { - display: flex; - align-items: center; - margin-left: 0; +/* Star Icon Styling */ +.community-rating-star { + color: #f2b01e; + font-size: 1.4em; + height: auto !important; + margin-right: 0.125em; + width: auto !important; } -.imdb-logo { - margin-right: 1px; +.star-rating-container { display: flex; - flex-wrap: wrap; - align-content: center; - justify-content: center; align-items: center; } -.tomato-logo { - flex-wrap: wrap; - align-content: center; - display: flex; - justify-content: center; +/* Rotten Tomatoes Critic Rating Styles */ + +.critic-rating { + -webkit-align-items: center; align-items: center; - margin-left: 5px; + display: flex; + min-height: 1.2em; + gap: 0.25em; } -.critic-logo { - margin-right: 5px; - display: flex; - flex-wrap: wrap; - align-content: center; - justify-content: center; -} +/**/ .age-rating { display: flex; align-items: center; - margin-left: 5px; - margin-right: 5px; border-radius: 5px; background: rgb(255 255 255 / 0.8); color: #000; @@ -542,16 +582,12 @@ font-weight: 500; display: flex; align-items: center; - margin-left: 5px; - margin-right: 5px; flex-wrap: wrap; align-content: center; justify-content: center; } .separator-icon { - margin-left: 5px; - margin-right: 5px; font-size: 10px; color: aquamarine; } diff --git a/slideshowpure.js b/slideshowpure.js index c7bd11b..3689946 100644 --- a/slideshowpure.js +++ b/slideshowpure.js @@ -1,14 +1,10 @@ /* - * Jellyfin Slideshow by M0RPH3US v3.0.1 + * Jellyfin Slideshow by M0RPH3US v3.0.4 */ //Core Module Configuration const CONFIG = { IMAGE_SVG: { - imdbLogo: - '', - tomatoLogo: - '', freshTomato: 'image/svg+xml', rottenTomato: @@ -41,6 +37,7 @@ const STATE = { slideshow: { hasInitialized: false, isTransitioning: false, + isPaused: false, currentSlideIndex: 0, focusedSlide: null, containerFocused: false, @@ -156,11 +153,11 @@ const initJellyfinData = (callback) => { const initLoadingScreen = () => { const currentPath = window.location.href.toLowerCase().replace(window.location.origin, ""); const isHomePage = - currentPath === "/web/#/home.html" || - currentPath === "/web/#/home" || - currentPath === "/web/index.html#/home.html" || + currentPath.includes("/web/#/home.html") || + currentPath.includes("/web/#/home") || + currentPath.includes("/web/index.html#/home.html") || currentPath === "/web/index.html#/home" || - currentPath === "/web/"; + currentPath.endsWith("/web/"); if (!isHomePage) return; @@ -170,7 +167,7 @@ const initLoadingScreen = () => { loadingDiv.innerHTML = `

- Server Logo +

@@ -182,7 +179,7 @@ const initLoadingScreen = () => { document.body.appendChild(loadingDiv); requestAnimationFrame(() => { - document.querySelector(".bar-loading h1 img").style.opacity = "1"; + document.querySelector(".bar-loading h1 div").style.opacity = "1"; }); const progressBar = document.getElementById("progress-bar"); @@ -252,6 +249,7 @@ const resetSlideshowState = () => { STATE.slideshow.hasInitialized = false; STATE.slideshow.isTransitioning = false; + STATE.slideshow.isPaused = false; STATE.slideshow.currentSlideIndex = 0; STATE.slideshow.focusedSlide = null; STATE.slideshow.containerFocused = false; @@ -431,7 +429,7 @@ const SlideUtils = { */ parseGenres(genresArray) { if (Array.isArray(genresArray) && genresArray.length > 0) { - return genresArray.slice(0, 3).join(" ▫️ "); //🔹 + return genresArray.slice(0, 3).join(this.createSeparator().outerHTML); } return "No Genre Available"; }, @@ -742,7 +740,7 @@ const VisibilityObserver = { container.style.display = isVisible ? "block" : "none"; if (isVisible) { - if (STATE.slideshow.slideInterval) { + if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) { STATE.slideshow.slideInterval.start(); } } else { @@ -812,7 +810,7 @@ const SlideCreator = { const logo = SlideUtils.createElement("img", { className: "logo high-quality", src: `${serverAddress}/Items/${itemId}/Images/Logo?quality=40`, - alt: "logo", + alt: item.Name, loading: "eager", }); @@ -857,8 +855,8 @@ const SlideCreator = { const genreElement = SlideUtils.createElement("div", { className: "genre", + innerHTML: SlideUtils.parseGenres(item.Genres) }); - genreElement.innerHTML = SlideUtils.parseGenres(item.Genres); const buttonContainer = SlideUtils.createElement("div", { className: "button-container", @@ -890,139 +888,81 @@ const SlideCreator = { */ createRatingInfo(item) { const { - CommunityRating: rating, + CommunityRating: communityRating, CriticRating: criticRating, - OfficialRating: age, - PremiereDate: date, + OfficialRating: ageRating, + PremiereDate: premiereDate, RunTimeTicks: runtime, - ChildCount: season, + ChildCount: seasonCount, } = item; - const ratingTest = SlideUtils.createElement("div", { - className: "rating-value", + const miscInfo = SlideUtils.createElement("div", { + className: "misc-info", }); - const imdbLogo = SlideUtils.createElement("div", { - className: "imdb-logo", - innerHTML: CONFIG.IMAGE_SVG.imdbLogo, - style: { - width: "30px", - height: "30px", - }, - }); - - ratingTest.appendChild(imdbLogo); - - if (typeof rating === "number") { - const ratingSpan = document.createElement("span"); - ratingSpan.textContent = rating.toFixed(1); - ratingSpan.style.marginRight = "5px"; - ratingSpan.style.marginLeft = "5px"; - ratingTest.appendChild(ratingSpan); - } else { - const naSpan = document.createElement("span"); - naSpan.innerHTML = "N/A"; - naSpan.style.color = "#fff9"; - naSpan.style.marginRight = "5px"; - naSpan.style.marginLeft = "5px"; - ratingTest.appendChild(naSpan); + // Community Rating Section (IMDb) + if (typeof communityRating === "number") { + const container = SlideUtils.createElement("div", { + className: "star-rating-container", + innerHTML: `${communityRating.toFixed(1)}`, + }); + miscInfo.appendChild(container); + miscInfo.appendChild(SlideUtils.createSeparator()); } - ratingTest.appendChild(SlideUtils.createSeparator()); - - const tomatoRatingDiv = SlideUtils.createElement("div", { - className: "tomato-rating", - }); - - const tomatoLogo = SlideUtils.createElement("div", { - className: "tomato-logo", - innerHTML: CONFIG.IMAGE_SVG.tomatoLogo, - style: { - width: "18px", - height: "20px", - }, - }); - - let valueElement = SlideUtils.createElement("span", { - style: { - marginLeft: "5px", - marginRight: "5px", - }, - }); - + // Critic Rating Section (Rotten Tomatoes) if (typeof criticRating === "number") { - valueElement.textContent = `${criticRating}% `; - } else { - valueElement.style.color = "#fff9"; - valueElement.textContent = "N/A "; + 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))) { + const container = SlideUtils.createElement("div", { + className: "date", + innerHTML: new Date(premiereDate).getFullYear(), + }); + miscInfo.appendChild(container); + miscInfo.appendChild(SlideUtils.createSeparator()); + }; + + // Age Rating Section + if (typeof ageRating === "string") { + const container = SlideUtils.createElement("div", { + className: "age-rating mediaInfoOfficialRating", + rating: ageRating, + ariaLabel: `Content rated ${ageRating}`, + title: `Rating: ${ageRating}`, + innerHTML: ageRating, + }); + miscInfo.appendChild(container); + miscInfo.appendChild(SlideUtils.createSeparator()); + }; + + // Runtime / Seasons Section + if (seasonCount !== undefined || runtime !== undefined) { + const container = SlideUtils.createElement("div", { + className: "runTime", + }); + if (seasonCount) { + container.innerHTML = `${seasonCount} Season${seasonCount > 1 ? "s" : ""}`; + } else { + const milliseconds = runtime / 10000; + const currentTime = new Date(); + const endTime = new Date(currentTime.getTime() + milliseconds); + const options = { hour: "2-digit", minute: "2-digit", hour12: false }; + const formattedEndTime = endTime.toLocaleTimeString([], options); + container.innerText = `Ends at ${formattedEndTime}`; + } + miscInfo.appendChild(container); } - const criticLogo = SlideUtils.createElement("span", { - className: "critic-logo", - style: { - display: "flex", - width: "18", - height: "20", - }, - }); - criticLogo.innerHTML = - criticRating > 59 - ? CONFIG.IMAGE_SVG.freshTomato - : CONFIG.IMAGE_SVG.rottenTomato; - - tomatoRatingDiv.append(tomatoLogo, valueElement, criticLogo); - tomatoRatingDiv.appendChild(SlideUtils.createSeparator()); - - const ageRatingDiv = SlideUtils.createElement("div", { - className: "age-rating", - }); - const ageSpan = document.createElement("span"); - ageSpan.textContent = age || "N/A"; - ageRatingDiv.appendChild(ageSpan); - - const premiereDate = SlideUtils.createElement("div", { - className: "date", - }); - - const year = date ? new Date(date).getFullYear() : NaN; - if (isNaN(year)) { - const naSpan = SlideUtils.createElement( - "span", - { - style: { color: "#fff9" }, - }, - "N/A" - ); - premiereDate.appendChild(naSpan); - } else { - premiereDate.textContent = year; - } - - const runTimeElement = SlideUtils.createElement("div", { - className: "runTime", - }); - - if (season === undefined) { - const milliseconds = runtime / 10000; - const currentTime = new Date(); - const endTime = new Date(currentTime.getTime() + milliseconds); - const options = { hour: "2-digit", minute: "2-digit", hour12: false }; - const formattedEndTime = endTime.toLocaleTimeString([], options); - runTimeElement.textContent = `Ends at ${formattedEndTime}`; - } else { - runTimeElement.textContent = `${season} Season${season > 1 ? "s" : ""}`; - } - - ratingTest.append( - tomatoRatingDiv, - premiereDate, - SlideUtils.createSeparator(), - ageRatingDiv, - SlideUtils.createSeparator(), - runTimeElement - ); - - return ratingTest; + return miscInfo; }, /** @@ -1076,7 +1016,7 @@ const SlideCreator = { createFavoriteButton(item) { const isFavorite = item.UserData && item.UserData.IsFavorite === true; - + const button = SlideUtils.createElement("button", { className: `favorite-button ${isFavorite ? "favorited" : ""}`, tabIndex: "0", @@ -1266,7 +1206,7 @@ const SlideshowManager = { this.preloadAdjacentSlides(index); this.updateDots(); - if (STATE.slideshow.slideInterval) { + if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) { STATE.slideshow.slideInterval.restart(); } @@ -1377,6 +1317,22 @@ const SlideshowManager = { }); }, + togglePause() { + STATE.slideshow.isPaused = !STATE.slideshow.isPaused; + const pauseButton = document.querySelector('.pause-button'); + if (STATE.slideshow.isPaused) { + STATE.slideshow.slideInterval.stop(); + pauseButton.innerHTML = 'play_arrow'; + pauseButton.setAttribute("aria-label", "Play slideshow"); + pauseButton.setAttribute("title", "Play slideshow"); + } else { + STATE.slideshow.slideInterval.start(); + pauseButton.innerHTML = 'pause'; + pauseButton.setAttribute("aria-label", "Pause slideshow"); + pauseButton.setAttribute("title", "Pause slideshow"); + } + }, + /** * Initializes touch events for swiping */ @@ -1450,6 +1406,11 @@ const SlideshowManager = { e.preventDefault(); break; + case " ": // Space bar + this.togglePause(); + e.preventDefault(); + break; + case "Enter": focusElement.click(); e.preventDefault(); @@ -1491,7 +1452,9 @@ const SlideshowManager = { await this.updateCurrentSlide(0); STATE.slideshow.slideInterval = new SlideTimer(() => { - this.nextSlide(); + if (!STATE.slideshow.isPaused) { + this.nextSlide(); + } }, CONFIG.shuffleInterval); } catch (error) { console.error("Error loading slideshow data:", error); @@ -1539,8 +1502,22 @@ const initArrowNavigation = () => { }, }); + const pauseButton = SlideUtils.createElement("div", { + className: "pause-button", + innerHTML: 'pause', + tabIndex: "0", + "aria-label": "Pause slideshow", + title: "Pause slideshow", + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + SlideshowManager.togglePause(); + } + }); + container.appendChild(leftArrow); container.appendChild(rightArrow); + container.appendChild(pauseButton); const showArrows = () => { leftArrow.style.display = "block";