Featured Content Bar - Printable Version +- Jellyfin Forum (https://forum.jellyfin.org) +-- Forum: Support (https://forum.jellyfin.org/f-support) +--- Forum: General Questions (https://forum.jellyfin.org/f-general-questions) +--- Thread: Featured Content Bar (/t-featured-content-bar) |
RE: Featured Content Bar - BobHasNoSoul - 2023-11-11 some screens of the updated look of it after taking a few pms into consideration RE: Featured Content Bar - SethBacon - 2023-11-12 Not a linux guy, ive been editing the slideshow js directly; I've managed to get it selectable by remote with slide.setAttribute('tabindex', '0'); slide.addEventListener('keydown', function(event) { switch(event.keyCode) { case 13: // Enter key // Code to handle the selection window.location.href = this.href; break; // Add cases for other keys if needed } }); Didn't feel like worth a full fork, but heres my slideshow.html Changes: -Reads list.txt (ignoring labels after id codes), if list.txt is not present, grabs 200 movies from server and randomizes a selection of them to display (because i cant find an API call for random selection) -Fetches the plot through API - movie.Overview -Accessible / Clickable in webUI (see video) -Offset top border & title font to match my JF theme (ymmv) -Text bottom justified (this number of chrs looks good on my desktop widescreen and on my phone, though ive set my slide frame to 350px tall) -Config vars grouped at beginning of JS -Various visual changes Issues: Pressing the webui back button (or remote controll back) after selecting a slide and going to its page will lock up JF. Using browser back button works fine, or pressing the webui home button works fine. I tried different referral options but no luck so far. My border highlight for the selection of the frame doesnt seem to be working, I have a hard time testing this accessibility on desktop. I dont know how you got this to work on Jellyfin for Android TV, it wont pick up Any server css for me. <!DOCTYPE html> <html> <head> <title>Jellyfin Featured Slideshow</title> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap" rel="stylesheet"> <style> /* CSS styles for the slideshow elements */ body { margin: 0; padding: 0; overflow: hidden; } .slide { position: relative; width: 100vw; height: 100vh; box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); cursor: pointer; /* Indicates the element is clickable */ outline: none; /* Custom focus style will be used */ } .slide:focus { outline: 2px solid #fff; /* Visual focus indicator */ } .backdrop { position: absolute; top: 50px; left: 0; width: 100%; height: calc(100% - 50px); object-fit: cover; object-position: center 20%; border-radius: 5px; z-index: 1; } .logo { position: absolute; top: 65%; left: 10px; transform: translateY(-50%); max-height: 50%; max-width: 30%; width: auto; z-index: 3; } .featured-content { position: absolute; top: 0; left: 0; width: 100%; height: 50px; background-color: transparent; font-family: 'Noto Sans', sans-serif; color: #D3D3D3; font-size: 22px; display: flex; align-items: center; justify-content: flex-start; z-index: 2; } .plot { position: absolute; bottom: 0px; left: 0; /* Align to the left edge */ right: 0; /* Stretch to the right edge */ color: white; width: 100%; /* Full width */ font-family: 'Noto Sans', sans-serif; font-size: 15px; background: linear-gradient(to top, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 0) 100%); padding: 10px; border-radius: 5px; z-index: 4; box-sizing: border-box; } </style> </head> <body> <!-- Container for dynamic slides --> <div id="slides-container"></div> <!-- JavaScript for fetching movies and creating the slideshow --> <script> // Configuration variables let title = 'Spotlight'; // Default title const userId = '0e0755005f074a59bc0108cc2c3e650b'; // Replace with your User ID const token = 'de81a85d55024d7b9fa7b11031de7539'; // Replace with your API token const shuffleInterval = 10000; // Time in milliseconds between slide changes (25000ms = 25 seconds) const listFileName = 'list.txt'; // Name of the file containing the list of movie IDs function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function truncateText(element, maxLength) { var truncated = element.innerText; if (truncated.length > maxLength) { truncated = truncated.substr(0, maxLength) + '...'; } element.innerText = truncated; } function createSlideForMovie(movie, title) { const container = document.getElementById('slides-container'); const slideElement = createSlideElement(movie, title); container.appendChild(slideElement); } function createSlideElement(movie, title) { const itemId = movie.Id; const plot = movie.Overview; const slide = document.createElement('a'); slide.className = 'slide'; slide.href = /#!/details?id=${itemId} ;slide.target = '_top'; slide.rel = 'noreferrer'; slide.setAttribute('tabindex', '0'); // Make the slide focusable // Key event listener for remote control input slide.addEventListener('keydown', function(event) { if (event.keyCode === 13) { // Enter key window.location.href = this.href; } }); const backdrop = document.createElement('img'); backdrop.className = 'backdrop'; backdrop.src = /Items/${itemId}/Images/Backdrop/0 ;backdrop.alt = 'Backdrop'; const logo = document.createElement('img'); logo.className = 'logo'; logo.src = /Items/${itemId}/Images/Logo ;logo.alt = 'Logo'; const featuredContent = document.createElement('div'); featuredContent.className = 'featured-content'; featuredContent.textContent = title; const plotElement = document.createElement('div'); plotElement.className = 'plot'; plotElement.textContent = plot; // Truncate the text of this specific plot element truncateText(plotElement, 240); // Adjust 240 to your preferred character limit slide.appendChild(backdrop); slide.appendChild(logo); slide.appendChild(featuredContent); slide.appendChild(plotElement); return slide; } function initializeSlideshow() { var slides = document.querySelectorAll(".slide"); var currentSlide = 0; var shuffledIndexes = shuffleArray(Array.from({ length: slides.length }, (_, i) => i)); function showSlide(index) { for (var i = 0; i < slides.length; i++) { slides[i].style.display = "none"; } slides[shuffledIndexes[index]].style.display = "block"; } function nextSlide() { currentSlide = (currentSlide + 1) % slides.length; showSlide(currentSlide); } showSlide(currentSlide); setInterval(nextSlide, shuffleInterval); } function fetchMovies() { const noCacheUrl = listFileName + '?' + new Date().getTime(); fetch(noCacheUrl) .then(response => { if (response.ok) { return response.text(); } else { throw new Error('list.txt not found, fetching movies from server.'); } }) .then(text => { const lines = text.split('\n').filter(Boolean); title = lines.shift() || 'Spotlight'; // Set the global title const movieIds = lines.map(line => line.substring(0, 32)); return Promise.all(movieIds.map(id => fetchMovieDetails(id))); }) .then(movies => { movies.forEach(movie => createSlideForMovie(movie, title)); initializeSlideshow(); }) .catch(error => { console.error(error); fetchMoviesFromServer(); // Fallback to fetching movies from the server }); } function fetchMovieDetails(movieId) { return fetch( /Users/${userId}/Items/${movieId} , {headers: { 'Authorization': MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}" } }) .then(response => response.json()) .then(movie => { console.log("Movie Title:", movie.Name); console.log("Movie Overview:", movie.Overview); return movie; }); } function fetchMoviesFromServer() { title = 'Spotlight'; // Reset title to 'Spotlight' when fetching from server fetch( /Users/${userId}/Items?IncludeItemTypes=Movie,Series&Recursive=true&Limit=300 , {headers: { 'Authorization': MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}" } }) .then(response => response.json()) .then(data => { const movies = data.Items; const shuffledMovies = shuffleArray(movies); const selectedMovieIds = shuffledMovies.slice(0, 30).map(movie => movie.Id); return Promise.all(selectedMovieIds.map(id => fetchMovieDetails(id))); }) .then(movies => { movies.forEach(movie => createSlideForMovie(movie, 'Spotlight')); initializeSlideshow(); }) .catch(error => console.error('Error fetching movies:', error)); } fetchMovies(); </script> </body> </html> RE: Featured Content Bar - SethBacon - 2023-11-12 Oof it butchered my spacing. Attached. So in the video, we see the selection moving by remote (This is in Jellyfin for Android Not Jellyfin for Android TV, android has some remote accessibility, its limited, but still) so anyways we see the selection move up through ozark and (presumably, though my highlight isnt working) onto Jarhead. ... and now i cant upload my vid cause this forum be trippin here https://imgur.com/a/TSAhqkR Attachment size limit RE: Featured Content Bar - SethBacon - 2023-11-12 So, all that being said, Im still surprised you mention getting this to run in Jellyfin for Android TV: I thought that app did not use the web UI / respect any css customization (and its the one im forced to use for firesticks 😠). Or maybe, like me, you're testing in Jellyfin for Android on a TV? or the Roku app maybe? RE: Featured Content Bar - BobHasNoSoul - 2023-11-12 it works in the android jellyfin app on fire stick, also works on the lg tv jellyfin client.. (except the actual clicking on the item inside the featured bar.. still having issues with that i can get it to select the item now but i need to figure out a way to dymanically load the links inside the page.. with work and kids im not sure it will be a fast thing i implement however this is on the todo list) works on xbox also incase anyone is wondering using the standard jellyfin app and the clicking on that works because it uses a pointer for some reason. Also holy hell thats a decent upgrade to it.. quick question you mentioned slideshow.js can you elaborate a bit more on that (i will merge it into the github) (2023-11-12, 09:28 AM)SethBacon Wrote: So, all that being said, Im still surprised you mention getting this to run in Jellyfin for Android TV: I thought that app did not use the web UI / respect any css customization (and its the one im forced to use for firesticks 😠). Or maybe, like me, you're testing in Jellyfin for Android on a TV? or the Roku app maybe? RE: Featured Content Bar - SethBacon - 2023-11-12 Oh, I just meant the javascript section in the slideshow.html, its all in there. Anyways cheers! its got everything I need for now (i was playing around with different transition option or a button to go back and forth between slides but meh..) Im already finding and watching stuff Id forgotten about! I still think were talking about different tv apps but I'm really happy to have this new functionality for some users. I really really think this is an invaluable UI experience when you get into larger libraries. Super happy to help in anyway to push this towards a full plugin or official feature, ping anytime. RE: Featured Content Bar - 1hitsong - 2023-11-13 This looks neat and would be a fairly simple thing to add to Roku. Are y'all only featuring movies, or are all libraries fair game? RE: Featured Content Bar - SethBacon - 2023-11-13 This should work as pictured for any content with fanart, logos and plots so Im pulling from Movies And Series in the line - fetch(/Users/${userId}/Items?IncludeItemTypes=Movie,Series&Recursive=true&Limit=300 I bet episodes would work too. The recursive flag is me trying to get a random selection, which it does, the problem is its always the Same random selection, so we grab 30 of them and shuffle that list (before pulling any content) RE: Featured Content Bar - BobHasNoSoul - 2023-11-13 (2023-11-13, 02:05 AM)SethBacon Wrote: This should work as pictured for any content with fanart, logos and plots so Im pulling from Movies And Series in the line - i ran into a problem trying to get the focus to work, can you maybe explain what you did to get yours to work.. i have got the html to work but needed to fix a few syntax issues (think the forum messed up a few things on it) but for the life of me the focus doesnt work at all on it from what i have tried the downside to using episodes is the logo isnt always linked the same as the parent so you would end up with a blank space or a white background if there isnt a backdrop im going to add the html as another option in the github while i figure a few things out if thats okay? RE: Featured Content Bar - SethBacon - 2023-11-13 Absolutely, all open source here, but this is your riceball (a solution I've been searching for for like seriously 2 years so i'm happy to help). [Clarity here for newcomers] Attached are my 2 files needed to run this - slideshow.html, list.txt (these live in \Jellyfin\Server\jellyfin-web\avatars) after putting them there, go to the /jellyfin-web folder and find the file home-html.randomcharacters.chunk.js, open it with a text editor and insert this line: <style>.featurediframe { width: 93vw; height: 350px; display: block; border: 0px solid #000; margin: 0 auto; margin-bottom: 40px}</style><iframe class="featurediframe" src="/web/avatars/slideshow.html"></iframe> directly after data-backdroptype="movie,series,book">. Restart JF and clear browser cache to get it loaded. If you remove list.txt it will pick pseudo-random movies and series. (Randomization could use work) I tried to fix the missing logo issue 3 different ways - checking for return errors, checking content length, and prefetching images but none worked. Will need fresh eyes later. Regarding selection, did you definitely have this line in .slide? cursor: pointer; |