Compare commits

..

48 Commits

Author SHA1 Message Date
CodeDevMLH
0b60ff031e x 2025-09-28 02:37:34 +02:00
CodeDevMLH
c9cedf909f changed version 2025-09-28 02:35:00 +02:00
CodeDevMLH
b46dae9ff0 fix skip, back button position 2025-09-28 02:34:35 +02:00
CodeDevMLH
078ec24814 add gh readme 2025-09-27 02:55:26 +02:00
CodeDevMLH
c18282396c fix typo 2025-09-27 02:34:28 +02:00
CodeDevMLH
01469c1b06 fix missing list 2025-09-27 02:30:02 +02:00
CodeDevMLH
8f92dcaca3 ... 2025-09-27 02:24:10 +02:00
CodeDevMLH
4f42929965 changed version 2025-09-27 02:23:54 +02:00
CodeDevMLH
cc67c3abc0 fix typo 2025-09-27 02:20:37 +02:00
CodeDevMLH
1b05d4f863 add auto seasonal lists 2025-09-27 02:19:09 +02:00
CodeDevMLH
1748a8ab3d fix big screen 2025-09-27 01:45:15 +02:00
CodeDevMLH
2c9e3defd8 fix ratio issue when using titel 2025-09-27 01:37:40 +02:00
CodeDevMLH
4323cdb367 add title option 2025-09-26 13:53:04 +02:00
CodeDevMLH
018f78a6df .. 2025-09-26 01:09:06 +02:00
CodeDevMLH
a3449ec834 .. 2025-09-26 01:07:22 +02:00
CodeDevMLH
fc3d695af6 update readme 2025-09-26 01:05:46 +02:00
CodeDevMLH
6828e93bda add gh readme 2025-09-25 21:53:57 +02:00
d26c7667c9 some fixes 2025-02-25 01:47:27 +01:00
062c22bfa8 add language specific series descriptors 2025-02-22 00:44:33 +01:00
632a27983e updated version 2025-02-21 13:42:27 +01:00
de1ef45c41 no more hard coded api key 2025-02-21 01:51:49 +01:00
353e7714bf add chunk content for both 2024-12-31 20:21:51 +01:00
1b828c511a add new (unused) style 2024-12-31 20:20:28 +01:00
252ca17a34 adjusted skip/back button mobile 2024-12-31 20:17:30 +01:00
5e8eede1cd remove comment 2024-12-31 20:09:35 +01:00
55a19ce7e5 fix mobile css 2024-12-31 20:08:19 +01:00
e6384c096b fix hover error 2024-12-29 04:06:46 +01:00
d80c8c01ee fix showonotherpages 2024-12-28 02:12:16 +01:00
cdceb555f0 mod1 2024-12-28 01:48:25 +01:00
548aafb019 .. 2024-12-27 03:43:34 +01:00
50756f3c2f fix first load issue 2024-12-27 03:40:51 +01:00
3bdd185a53 fix skip intro typo 2024-12-27 03:15:54 +01:00
5362f15b80 add enable skip intro 2024-12-27 03:07:03 +01:00
361698e84b add intro skipping 2024-12-27 03:04:34 +01:00
d6214d0441 var name changed 2024-12-27 01:23:43 +01:00
232f55923f remove dep function, typo 2024-12-27 00:52:32 +01:00
3105ed3088 add muteon in list.txt 2024-12-27 00:37:01 +01:00
7b2f5acb21 del old files 2024-12-27 00:33:57 +01:00
8018010aae .. 2024-12-27 00:32:26 +01:00
d9e638ec9e add new commits in my way 2024-12-26 22:08:46 +01:00
ef5f893637 new version test 2024-12-25 13:57:49 +01:00
a4a8b4e7f2 fix mobile height in home-html 2024-11-18 23:47:40 +01:00
bcac8a402a add list.txt 2024-11-15 11:32:31 +01:00
d9b216ce49 start video muted 2024-11-15 00:59:39 +01:00
161fad5fc4 readme 2024-11-15 00:55:55 +01:00
753b821480 readme and pictures 2024-11-15 00:55:44 +01:00
83c9f601d2 merge 2024-11-15 00:27:28 +01:00
f48b67bc29 pictures 2024-11-15 00:12:12 +01:00
37 changed files with 1824 additions and 391 deletions

171
README.md
View File

@@ -1,39 +1,118 @@
# SpotlightTrailer - Featured Content Bar # SpotlightTrailer - Featured Content Bar
Thanks to [SethBacon](https://forum.jellyfin.org/u-sethbacon) & [BobHasNoSoul](https://github.com/BobHasNoSoul) & [MakD](https://github.com/MakD) & [tedhinklater](https://github.com/tedhinklater) for their talents and work Thanks to [SethBacon](https://forum.jellyfin.org/u-sethbacon) & [BobHasNoSoul](https://github.com/BobHasNoSoul) & [MakD](https://github.com/MakD) & [tedhinklater](https://github.com/tedhinklater) for their talents and work
# Main Differences in this fork ## Main Differences in this fork
- activate random selection of movie from txt list - automatically skip outros/endcards and intros on trailers using SponsorBlock API
- show the featured bar only at the main page, hide it in favorites/requests tab - show the featured bar only at the main page, hide it in favorites/requests tab
- back button added - automatically switch between diffrent seasonal lists
- set txt list name
- small ui style fixes - small ui style fixes
- html, css and javascript in seperate files
- display title for featured content (optional)
- **no hard coded api key needed**
# Currently working on Testet on Jellyfin 10.10.7
- show a title above the spotlight banner, e.g. 'Popular at Jellyfin'
- if local trailer available, play it instead of youtube trailer ## Table of Contents
- [SpotlightTrailer - Featured Content Bar](#spotlighttrailer---featured-content-bar)
- [Main Differences in this fork](#main-differences-in-this-fork)
- [Table of Contents](#table-of-contents)
- [Configuration Parameters](#configuration-parameters)
- [Installation](#installation)
- [Uninstall](#uninstall)
- [How to feature specific content in the bar](#how-to-feature-specific-content-in-the-bar)
- [Desktop View](#desktop-view)
- [Mobile View (Landscape / Portrait)](#mobile-view-landscape--portrait)
---
Testet on Jellyfin 10.10.0 ![overview](https://github.com/user-attachments/assets/cb6c5a44-9121-4fbf-820c-e888efcf20aa)
# Impressions ## Configuration Parameters
![feat3](https://i.imgur.com/1Bfwz9w.png)
The following configuration parameters are used to customize the behavior and appearance of the slideshow. You can adjust them at the beginning of `script.js`:
- **`title`**:
The title of the slideshow. Set this to true to show the title from first line of `list.txt` (see [list.txt](/list.txt)).
![title view](https://github.com/user-attachments/assets/74297c7d-4060-4aff-a5fd-a170320166dd)
- **`useSeasonalLists`**:
Use seasonal lists, see [seasonal lists doc](/SEASONAL_LISTS.md).
- **`listFileName`**:
The name of the file containing the list of movie or series IDs. Ensure this file exists in the correct location.
- **`moviesSeriesBoth`**:
Specifies the type of content to display:
- `1` for movies only,
- `2` for series only,
- `3` for both.
Default is `3`
- **`shuffleInterval`**:
Time interval (in milliseconds) between slides, unless a trailer is playing. Adjust for desired slide transition speed.
- **`useTrailers`**:
Enable (`true`) or disable (`false`) the display of trailers in the slideshow.
- **`setRandomMovie`**:
Enable (`true`) or disable (`false`) random selection of movies or series from the list. Default is `true`
- **`showOnOtherPages`**:
Set to `true` to show the slideshow on additional pages, such as "Favorites" or "Requests." Default is `false`.
- **`disableTrailerControls`**:
Set to `true` to hide trailer controls; `false` enables user control over the trailer. Default is `true`
- **`setMutedHover`**:
Set to `false` to disable unmuting the video on hover
Default mute setting for trailers on hover over entire slideshow element:
Default is `true`
- **`umuteOnHover`**:
Unmute video when hovered over (`true`) or keep muted (`false`).
Default is `true`
- **`unmutedVolume`**:
Volume level (0100) when unmuted.
Default is `20`
- **`useSponsorBlock`**:
Enable (`true`) or disable (`false`) the use of SponsorBlock data for skipping segments (outros, intros) in trailers.
Default is `true`
- **`skipIntro`**:
Enable (`true`) or disable (`false`) to skip the intro segment of the trailer.
Default is `true`
- **`plotMaxLength`**:
Maximum number of characters for plot descriptions. Adjust as needed.
- **`trailerMaxLength`**:
Maximum duration (in milliseconds) of trailers. Set to `0` to disable trailer length limitation. Could be used instead of SponsorBlock, but SponsorBlock is recommended
- **`isMuted`**:
Default mute state of videos (`true` for muted, `false` for unmuted).
By adjusting these parameters, you can fine-tune the slideshow's behavior and appearance to suit your needs.
## How to install ## Installation
1. Download [spotlight.html] 1. Download [spotlight.html](/spotlight.html), [script.js](/script.js) and [styles.css](/styles.css)
2. Go to your ```jellyfin-web``` folder and create a folder named ```ui``` and drop ```spotlight.html, script.js and styles.css``` in that folder 2. Modify `script.js` like [explained above](#configuration-parameters) if necessary.
3. In your Jellyfin Dashboard, under ```API Keys``` create an API key for Spotlight, copy the key, and insert it as the value for ```token``` in ```script.js```. You can also set the corresponding values for list name, random selection, show it only on main page, show title, plot length, etc. 3. Go to your ```jellyfin-web``` folder and create a folder named ```featured``` and drop ```spotlight.html, script.js and styles.css``` in that folder
4. ```Important: Use Notepad++ for this``` In the jellyfin-web folder, open the file ```home-html.RANDOMSTRINGHERE.chunk.js``` 4. ```Important: Use Notepad++ for this```\
In the jellyfin-web folder, open the file ```home-html.RANDOMSTRINGHERE.chunk.js```
5. Ctrl+F and search for ```data-backdroptype="movie,series,book">``` 5. Ctrl+F and search for ```data-backdroptype="movie,series,book">```
6. Paste this after the > 6. Paste this after the >
```js ```js
<style> .featurediframe {width: 95vw; height: 24em; display: block; border: 0; margin: -1em auto 0;} @media (min-width: 2100px) {.featurediframe {height: 33em;}} @media (max-width: 1599px) {.featurediframe {margin-top: 1.2em;}} @media (max-width: 800px) {.featurediframe {margin-top: 0.8em;}} </style> <iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe> <style> .featurediframe {width: 95vw; height: 24em; display: block; border: 0; margin: -1em auto 0;} @media (min-width: 2100px) {.featurediframe {height: 33em;}} @media (max-width: 1599px) {.featurediframe {margin-top: 1.2em;}} @media (max-width: 800px) {.featurediframe {margin-top: 0.8em; height: 25em;}} </style> <iframe class="featurediframe" src="/web/featured/spotlight.html"></iframe>
``` ```
7. Save the file. 7. Save the file.
@@ -41,45 +120,25 @@ Testet on Jellyfin 10.10.0
9. That's it. 9. That's it.
![feat17](https://github.com/user-attachments/assets/af916d90-ec7c-4af0-b6e8-0f6f94ef1f07)
## Uninstall
Simply delete Step 7's snippet added to ```home-html.chunk.js``` then refresh your browser's cache. You can, but not have to delete the featured folder.
## How to feature specific content in the bar
By default, the bar will feature content at random as long as it is available to the current user and no `list.txt` is available or if it is empty `below` line 1.
In the first line, you can set a title for the featured bar, which can then be displayed. In addition, set MuteOn or MuteOff behind the title to control the behavoir of the trailer audio.
To preselect content, place a [list.txt](/list.txt) in the ```featured``` folder and paste the ID of each piece of content to be featured (IDs can be found in the address bar).
`IMPORTANT` If you use list.txt to preselect content and a User has an Age Rating limit on their account (U, PG. FSK etc) make sure you add content for them to see too, or it will just be blank (content above their Age Limit is hidden to them)
![list](https://github.com/user-attachments/assets/5f8f7924-7a9b-49c1-aefa-198cefce0f60)
# Desktop View
![fcb](https://github.com/user-attachments/assets/eb0c9ce0-b96e-4a7e-bf71-ba9a637c25a3)
# Mobile View (Landscape / Portrait) # Mobile View (Landscape / Portrait)
![mobile](https://i.imgur.com/Y0wEa81.png) ![mobile](https://i.imgur.com/OrOzpBK.png)
# How to feature specific content in the bar
By default, the bar will feature content at random as long as it is available to the current user.
To preselect content, place a [list.txt](link) in the ```ui``` folder and paste the ID of each piece of content to be featured (IDs can be found in the address bar).
# Uninstallation
Simply delete Step 5's snippet added to ```home-html.chunk.js``` then refresh your browser's cache.
# Fullscreen Version
No changes here from my side...
![feat8](https://github.com/user-attachments/assets/d6855e23-8c08-4a8b-b05d-6ba9c9895672)
Same as above except use [this version of spotlight.html](https://github.com/tedhinklater/Jellyfin-Featured-Content-Bar/blob/main/fullscreen/spotlight.html)
insert this into home-html.RANDOMSTRINGHERE.chunk.js after ```data-backdroptype="movie,series,book">```
```js
<style>.featurediframe { width: 100vw; height: 100vh; display: block; border: 0px solid #000; margin: 0 auto; margin-bottom: 40px} @media (max-width:1000px) and (orientation:portrait) {.featurediframe {height: 46vh; width: 95vw;}} @media (max-width:1000px) and (orientation:landscape) {.featurediframe {height: 98vh; width: 95vw;}} @media (min-width: 2000px) { .featurediframe {height:102vh;}}</style><iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe>
```
and add this CSS to the very ```end``` of your Custom CSS
```css
.layout-desktop .page.homePage.libraryPage.allLibraryPage.backdropPage.pageWithAbsoluteTabs.withTabs.mainAnimatedPage { margin-top:-4.5em;}
.layout-desktop .overflowBackdropCard, .overflowSmallBackdropCard { width: 12.7vw !important; padding-right: 1.85em;}
.layout-desktop .skinHeader-withBackground {background-color: transparent; backdrop-filter: blur(0px);}
.layout-desktop #homeTab .section0 .sectionTitle.sectionTitle-cards.padded-left { display: none !important;}
.layout-desktop #homeTab .verticalSection.section1.emby-scroller-container { position: relative; top: -27em; left: 73em; width: 44vw; margin-bottom: -17em;}
.layout-desktop #homeTab .verticalSection.section2.emby-scroller-container::after { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100vw; background: black; z-index: -1;}
[dir="ltr"] #homeTab .verticalSection.section0.emby-scroller-container .emby-scrollbuttons {right: -5em; top: -2em;}
.layout-desktop #homeTab .verticalSection.section0 .cardText-first {display: none !important;}
.layout-desktop #homeTab .sections.homeSectionsContainer { margin-top: 2em;}
```

140
README_GitHub.md Normal file
View File

@@ -0,0 +1,140 @@
# SpotlightTrailer - Featured Content Bar
Thanks to [SethBacon](https://forum.jellyfin.org/u-sethbacon) & [BobHasNoSoul](https://github.com/BobHasNoSoul) & [MakD](https://github.com/MakD) & [tedhinklater](https://github.com/tedhinklater) for their talents and work
## Main Differences in this fork
- automatically skip outros/endcards and intros on trailers using SponsorBlock API
- show the featured bar only at the main page, hide it in favorites/requests tab
- automatically switch between diffrent seasonal lists
- small ui style fixes
- html, css and javascript in seperate files
- display title for featured content (optional)
- **no hard coded api key needed**
Testet on Jellyfin 10.10.7
## Table of Contents
- [SpotlightTrailer - Featured Content Bar](#spotlighttrailer---featured-content-bar)
- [Main Differences in this fork](#main-differences-in-this-fork)
- [Table of Contents](#table-of-contents)
- [Configuration Parameters](#configuration-parameters)
- [Installation](#installation)
- [Uninstall](#uninstall)
- [How to feature specific content in the bar](#how-to-feature-specific-content-in-the-bar)
- [Desktop View](#desktop-view)
- [Mobile View (Landscape / Portrait)](#mobile-view-landscape--portrait)
---
![overview](https://github.com/user-attachments/assets/cb6c5a44-9121-4fbf-820c-e888efcf20aa)
## Configuration Parameters
The following configuration parameters are used to customize the behavior and appearance of the slideshow. You can adjust them at the beginning of `script.js`:
- **`title`**:
The title of the slideshow. Set this to true to show the title from first line of `list.txt` (see [list.txt](/list.txt)).
- **`listFileName`**:
The name of the file containing the list of movie or series IDs. Ensure this file exists in the correct location.
- **`moviesSeriesBoth`**:
Specifies the type of content to display:
- `1` for movies only,
- `2` for series only,
- `3` for both.
Default is `3`
- **`shuffleInterval`**:
Time interval (in milliseconds) between slides, unless a trailer is playing. Adjust for desired slide transition speed.
- **`useTrailers`**:
Enable (`true`) or disable (`false`) the display of trailers in the slideshow.
- **`setRandomMovie`**:
Enable (`true`) or disable (`false`) random selection of movies or series from the list. Default is `true`
- **`showOnOtherPages`**:
Set to `true` to show the slideshow on additional pages, such as "Favorites" or "Requests." Default is `false`.
- **`disableTrailerControls`**:
Set to `true` to hide trailer controls; `false` enables user control over the trailer. Default is `true`
- **`setMutedHover`**:
Set to `false` to disable unmuting the video on hover
Default mute setting for trailers on hover over entire slideshow element:
Default is `true`
- **`umuteOnHover`**:
Unmute video when hovered over (`true`) or keep muted (`false`).
Default is `true`
- **`unmutedVolume`**:
Volume level (0100) when unmuted.
Default is `20`
- **`useSponsorBlock`**:
Enable (`true`) or disable (`false`) the use of SponsorBlock data for skipping segments (outros, intros) in trailers.
Default is `true`
- **`skipIntro`**:
Enable (`true`) or disable (`false`) to skip the intro segment of the trailer.
Default is `true`
- **`plotMaxLength`**:
Maximum number of characters for plot descriptions. Adjust as needed.
- **`trailerMaxLength`**:
Maximum duration (in milliseconds) of trailers. Set to `0` to disable trailer length limitation. Could be used instead of SponsorBlock, but SponsorBlock is recommended
- **`isMuted`**:
Default mute state of videos (`true` for muted, `false` for unmuted).
By adjusting these parameters, you can fine-tune the slideshow's behavior and appearance to suit your needs.
## Installation
1. Download [spotlight.html](/spotlight.html), [script.js](/script.js) and [styles.css](/styles.css)
2. Modify `script.js` like [explained above](#configuration-parameters) if necessary.
3. Go to your ```jellyfin-web``` folder and create a folder named ```featured``` and drop ```spotlight.html, script.js and styles.css``` in that folder
4. ```Important: Use Notepad++ for this```\
In the jellyfin-web folder, open the file ```home-html.RANDOMSTRINGHERE.chunk.js```
5. Ctrl+F and search for ```data-backdroptype="movie,series,book">```
6. Paste this after the >
```js
<style> .featurediframe {width: 95vw; height: 24em; display: block; border: 0; margin: -1em auto 0;} @media (min-width: 2100px) {.featurediframe {height: 33em;}} @media (max-width: 1599px) {.featurediframe {margin-top: 1.2em;}} @media (max-width: 800px) {.featurediframe {margin-top: 0.8em; height: 25em;}} </style> <iframe class="featurediframe" src="/web/featured/spotlight.html"></iframe>
```
7. Save the file.
8. Empty your browser's cached web content (Ctrl+F5 or empty it from your browser's Cookies and Site Data settings section)
9. That's it.
## Uninstall
Simply delete Step 7's snippet added to ```home-html.chunk.js``` then refresh your browser's cache. You can, but not have to delete the featured folder.
## How to feature specific content in the bar
By default, the bar will feature content at random as long as it is available to the current user and no `list.txt` is available or if it is empty `below` line 1.
In the first line, you can set a title for the featured bar, which can then be displayed. In addition, set MuteOn or MuteOff behind the title to control the behavoir of the trailer audio.
To preselect content, place a [list.txt](/list.txt) in the ```featured``` folder and paste the ID of each piece of content to be featured (IDs can be found in the address bar).
`IMPORTANT` If you use list.txt to preselect content and a User has an Age Rating limit on their account (U, PG. FSK etc) make sure you add content for them to see too, or it will just be blank (content above their Age Limit is hidden to them)
![list](https://github.com/user-attachments/assets/5f8f7924-7a9b-49c1-aefa-198cefce0f60)
# Desktop View
![fcb](https://github.com/user-attachments/assets/eb0c9ce0-b96e-4a7e-bf71-ba9a637c25a3)
# Mobile View (Landscape / Portrait)
![mobile](https://i.imgur.com/OrOzpBK.png)

52
SEASONAL_LISTS.md Normal file
View File

@@ -0,0 +1,52 @@
# Seasonal Lists Documentation
This document explains how to use seasonal lists with the Featured Content Bar.
## Configuration
To enable seasonal lists, set `useSeasonalLists = true` in the main script.js file.
## Seasonal Periods
The system automatically detects the following periods:
### Special Events (take precedence over seasons)
- **New Year**: January 1-7 → `newyear_list.txt`
- **Valentine's day**: February 10-20 → `valentine_list.txt`
- **Easter**: 1 week around Easter Sunday → `easter_list.txt`
- **Halloween**: October 20-31 → `halloween_list.txt`
### Regular Seasons
- **Spring**: March-May → `spring_list.txt`
- **Summer**: June-August → `summer_list.txt`
- **Autumn**: September-November → `autumn_list.txt`
- **Winter**: December-February → `winter_list.txt`
## List Files
Create the following files in your `featured` directory:
- `spring_list.txt` - Spring movies/shows
- `summer_list.txt` - Summer movies/shows
- `autumn_list.txt` - Autumn movies/shows
- `winter_list.txt` - Winter movies/shows
- `newyear_list.txt` - New Year themed content
- `valentine_list.txt` - Romance/Valentine themed content
- `easter_list.txt` - Easter/Family themed content
- `halloween_list.txt` - Horror/Halloween themed content
## File Format
Each seasonal list file follows the same format as the main `list.txt`:
```
Title of List [muteon/muteoff]
movie_id_1
movie_id_2
series_id_1
...
```
## Fallback
If seasonal lists are disabled or a seasonal file doesn't exist, the system will fall back to using the default `list.txt` file.

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
.layout-desktop .page.homePage.libraryPage.allLibraryPage.backdropPage.pageWithAbsoluteTabs.withTabs.mainAnimatedPage { margin-top:-4.5em;}
.layout-desktop .overflowBackdropCard, .overflowSmallBackdropCard { width: 12.7vw !important; padding-right: 1.85em;}
.layout-desktop .skinHeader-withBackground {background-color: transparent; backdrop-filter: blur(0px);}
.layout-desktop #homeTab .section0 .sectionTitle.sectionTitle-cards.padded-left { display: none !important;}
.layout-desktop #homeTab .verticalSection.section1.emby-scroller-container { position: relative; top: -27em; left: 73em; width: 44vw; margin-bottom: -17em;}
.layout-desktop #homeTab .verticalSection.section2.emby-scroller-container::after { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100vw; background: black; z-index: -1;}
[dir="ltr"] #homeTab .verticalSection.section0.emby-scroller-container .emby-scrollbuttons {right: -5em; top: -2em;}
.layout-desktop #homeTab .verticalSection.section0 .cardText-first {display: none !important;}
.layout-desktop #homeTab .sections.homeSectionsContainer { margin-top: 2em;}

View File

@@ -0,0 +1 @@
<style>.featurediframe { width: 100vw; height: 100vh; display: block; border: 0px solid #000; margin: 0 auto; margin-bottom: 40px} @media (max-width:1000px) and (orientation:portrait) {.featurediframe {height: 46vh; width: 95vw;}} @media (max-width:1000px) and (orientation:landscape) {.featurediframe {height: 98vh; width: 95vw;}} @media (min-width: 2000px) { .featurediframe {height:102vh;}}</style><iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe>

476
fullscreen/spotlight.html Normal file
View File

@@ -0,0 +1,476 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<title>Jellyfin Spotlight v2.3.2</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap');
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);}
.slide:focus { outline: 2px solid #fff; }
.backdrop { position: absolute; top: 3.15em; left: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 30%; z-index: 1; transition: width 0.5s ease, filter 0.8s ease, scale 2s ease; transform-origin: top; animation: none;}
.logo { position: relative; transform: translateX(-50%) translateY(-50%); top: 56%; left: 14.5%; z-index: 3; text-shadow: -2px 2px 4px rgba(0, 0, 0, 0.5); filter: drop-shadow(1px 1px 1px); pointer-events: none; transition: transform 0.3s ease, max-height 0.3s ease, max-width 0.3s ease;}
.heading { position: absolute; top: 0; left: 0; width: 100%; height: 2.3em; background-color: transparent; font-family: "Titillium Web", sans-serif; color: #D3D3D3; font-size: 22px; display: flex; align-items: center; justify-content: flex-start; z-index: 2; padding: 10px; padding-left: 0px; box-sizing: border-box; text-shadow: 1px 1px 4px rgba(0, 0, 0, 1); }
.next-button { position: absolute; top: 0.4em; right: 0.25em; font-size: 1.5em; color: #D3D3D3; cursor: pointer; z-index: 10; font-family: "Titillium Web", sans-serif; font-size: 1.35em; text-shadow: 1px 1px 4px rgba(0, 0, 0, 1);}
.skip-button {font-size: 1.62em; top: 0.5em; opacity: 0.5; transition: opacity 0.3s ease, background 0.3s ease; padding-left: 0.15em; padding-right: 0.15em; padding-top: 0.1em; border-radius: 50%; box-sizing: border-box; font-style: normal; font-weight: 400; letter-spacing: normal; line-height: 1; text-transform: none; word-wrap: normal; direction: inherit; white-space: nowrap; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; -moz-osx-font-smoothing: grayscale; -webkit-font-feature-settings: "liga"; font-feature-settings: "liga";}
.skip-button:hover {background: #ffffff70; opacity: 1;}
.video-container { position: absolute; right: 0; top: 50px; width: 0; height: calc(100% - 50px); overflow: hidden; z-index: 2; transition: width 0.5s ease; z-index: 6; border-top-right-radius: 0.5em; border-bottom-right-radius: 0.5em; transform-origin: right;}
.video-player { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 7; }
.clickable-overlay { position: absolute; top: 50px; left: 0; width: 100%; height: calc(100%); z-index: 2; pointer-events: none; background: transparent; transition: background 0.8s ease, backdrop-filter 0.8s ease;}
.clickable-overlay::before { content: ''; position: absolute; top: 25%; left: 0; width: 100%; height: 75%; background: linear-gradient(0deg, rgb(0% 0% 0%) 0%, rgba(0, 0, 0, 0.99) 6.25%, rgba(0, 0, 0, 0.96) 12.5%, rgba(0, 0, 0, 0.92) 18.75%, rgba(0, 0, 0, 0.86) 25%, rgba(0, 0, 0, 0.78) 31.25%, rgba(0, 0, 0, 0.69) 37.5%, rgba(0, 0, 0, 0.6) 43.75%, rgba(0, 0, 0, 0.5) 50%, rgba(0, 0, 0, 0.4) 56.25%, rgba(0, 0, 0, 0.31) 62.5%, rgba(0, 0, 0, 0.22) 68.75%, rgba(0, 0, 0, 0.14) 75%, rgba(0, 0, 0, 0.08) 81.25%, rgba(0, 0, 0, 0.04) 87.5%, rgba(0, 0, 0, 0.01) 93.75%, rgba(0, 0, 0, 0) 100% ); opacity: 0.15; transition: opacity 0.3s ease; z-index: 1;}
.clickable-overlay::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(180deg, rgb(0% 0% 0% / 0.9) 0%, rgb(0% 0% 0% / 0.6952285766601562) 6.25%, rgb(0% 0% 0% / 0.5275634765625) 12.5%, rgb(0% 0% 0% / 0.39222564697265627) 18.75%, rgb(0% 0% 0% / 0.284765625) 25%, rgb(0% 0% 0% / 0.20106353759765627) 31.25%, rgb(0% 0% 0% / 0.1373291015625) 37.5%, rgb(0% 0% 0% / 0.09010162353515627) 43.75%, rgb(0% 0% 0% / 0.05625000000000002) 50%, rgb(0% 0% 0% / 0.03297271728515627) 56.25%, rgb(0% 0% 0% / 0.017797851562500022) 62.5%, rgb(0% 0% 0% / 0.00858306884765625) 68.75%, rgb(0% 0% 0% / 0.003515625000000022) 75%, rgb(0% 0% 0% / 0.0011123657226562722) 81.25%, rgb(0% 0% 0% / 0.0002197265625000222) 87.5%, rgb(0% 0% 0% / 0.000013732910156272204) 93.75%, rgb(0% 0% 0% / 0) 100% ); opacity: 0.25; transition: opacity 0.3s ease, visibility 0.8s ease; z-index: 3; border-radius: 0.25em; }
.lorem-ipsum {position: relative; bottom: -44em; left: 59em; text-align: left; font-family: "Titillium Web", sans-serif; font-size: 92%; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); opacity: 1; color: #fff; z-index: 4; box-sizing: border-box; background: transparent; max-width: fit-content; padding-left: 0.3em; padding-right: 0.3em; display: flex; flex-direction: row; max-height: 1.9em; padding-top: 0.15em; white-space: nowrap;}
.lorem-ipsum span { margin-right: 0.2em;}
.age-rating {position: absolute; top: 3.5em; left: 0em; text-align: left; font-family: "Titillium Web", sans-serif; font-size: 95%; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); opacity: 1; color: #fff; z-index: 4; box-sizing: border-box; background: transparent; max-width: fit-content; padding-left: 0.3em; padding-right: 0.3em; display: flex; flex-direction: row; max-height: 1.9em; padding-top: 0.15em; white-space: nowrap;}
.plot { position: absolute; bottom: 0; left: -0.25em; right: 0; color: #fff; font-family: "Titillium Web", sans-serif; font-size: 0.9em; font-weight: 500; background: transparent; padding: 10px; z-index: 7; box-sizing: border-box; padding-bottom: 3px; padding-top: 0.5em; line-height: 1.2em; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; border-bottom-left-radius: 0.45em; transition: opacity 0.1s ease, max-height 0.8s ease; opacity: 0; max-width: 31%; opacity: 1; -webkit-line-clamp: 3; max-height: 7.8em;}
.genres { position: relative; bottom: -44.1em; color: #fff; z-index: 2; font-family: "Titillium Web", sans-serif; font-size: 0.79em; opacity: 1; transition: opacity 0.3s ease; max-width: 39em; overflow: hidden; height: 1.95em; padding-top: 0.25em; line-height: 2em; white-space: nowrap;}
.premiere-year {position: absolute; top: 5.75em; left: 0.5em; font-size: 0.9em; font-family: "Titillium Web", sans-serif; color: #fff; z-index: 3; opacity: 0; transition: opacity 0.3s; text-align: center; width: 3.5em;}
.additional-info {position: absolute; top: 7.3em; left: 31em; font-size: 0.9em; font-family: "Titillium Web", sans-serif; color: #fff; z-index: 3; opacity: 1; transition: opacity 0.3s; text-align: right; width: 5em;}
.community-rating {position: absolute; top: 5.75em; right: 69.5%; font-size: 0.9em; font-family: "Titillium Web", sans-serif; color: #fff; z-index: 3; opacity: 1; transition: opacity 0.3s; text-align: center; width: 3.6em; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.critic-rating {position: absolute; top: 5.75em; right: 73%; font-size: 0.9em; font-family: "Titillium Web", sans-serif; color: #fff; z-index: 3; opacity: 1; transition: opacity 0.3s; text-align: center; width: 3.9em; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
@keyframes objectPositionAnimation { 0% { object-position: center 30%; } 40% { object-position: center 80%; } 80% { object-position: center 20%; } 100% { object-position: center 30%; }}
.star-icon { color: gold; font-size: 0.9em; padding-right: 0.2em; margin-left: -0.1em;}
@media (max-width: 1000px) { .age-rating {position: absolute; top: 3em; left: 0em; font-size: 0.7em;} .lorem-ipsum {font-size: 0.7em; transform: translateX(50%); right: 50vw !important; padding-left: 0; padding-right: 1.5em;} .lorem-ipsum span {background:transparent !important; border: none !important;}.logo { transform: translateX(-50%) translateY(-50%); top: 10.5em; left: 50% !important; max-width: 70%; max-height: 45%; transform-origin: 50% 50%;} .plot {display: none; opacity: 1 !important; font-size: 90%; bottom: 0.5em; -webkit-line-clamp: 2; max-height: 3.3em; overflow: hidden; width: 100% !important; max-width: 100%; line-height: 1.45em;} .lorem-ipsum::before { opacity:0; padding-bottom: 0.25em; } .clickable-overlay { background: linear-gradient(0deg, rgb(0% 0% 0%) 0%, rgb(0% 0% 0% / 0.9990234375) 6.25%, rgba(0, 0, 0, 0.99) 12.5%, rgba(0, 0, 0, 0.97) 18.75%, rgba(0, 0, 0, 0.94) 25%, rgba(0, 0, 0, 0.88) 31.25%, rgba(0, 0, 0, 0.79) 37.5%, rgba(0, 0, 0, 0.67) 43.75%, rgba(0, 0, 0, 0.5) 50%, rgba(0, 0, 0, 0.33) 56.25%, rgba(0, 0, 0, 0.21) 62.5%, rgba(0, 0, 0, 0.12) 68.75%, rgba(0, 0, 0, 0.06) 75%, rgba(0, 0, 0, 0.03) 81.25%, rgba(0, 0, 0, 0.01) 87.5%, rgba(0, 0, 0, 0) 93.75%, rgba(0, 0, 0, 0) 100% ); opacity: 0.8;} .backdrop {left: 0% !important; width: 100% !important; top: 2.95em;} .lorem-ipsum .full-content { opacity: 1; max-width: 100%;} .lorem-ipsum::before { opacity:1; backdrop-filter: blur(2px); transform: translateX(0);} .clickable-overlay::before { opacity: 0.98; backdrop-filter: blur(0px);} .slide:hover .clickable-overlay::before { opacity: 0.98;} .skip-button {opacity: 1 !important; font-size: 0.9em; top: 1.1em; color: #fff;} .genres {bottom: 4em; left: unset !important; overflow: hidden; max-width: 100vw !important; white-space: nowrap; left: 0; z-index: 8; color: #fff; font-size: 0.7em;} .genres span {background: transparent !important; border: none !important;} .additional-info {left: 18.5%; top: 5em; text-align: right;} .community-rating {left: 86vw !important; top: 19.45em !important; text-align: left; z-index: 8; font-size: 0.85em !important;} .critic-rating {font-size: 0.85em !important; left: 70vw !important; top: 19.45em !important; z-index: 8;} .additional-info {left: 18.5%; top: 5em; text-align: left;} .heading {z-index: 0;} .heading::before {display: none;} .video-container {max-width: 0 !important;} .watch-trailer-button {position: absolute; border: none; border-radius: 5px; padding: 0.5em 1em; padding-right: 1.5em; font-family: "Titillium Web", sans-serif; cursor: pointer; z-index: 10; transition: background-color 0.3s ease; align-items: center; justify-content: center; display: flex !important; top: unset !important; bottom: 0.4em; font-size: 0.7em !important; background: #fff9 !important; color: #000e !important; font-weight: 600; transform: translateX(-100%) !important; left: 47% !important;} .slide { box-shadow: none;} .text-container::before { display: none !important;} .buttons-container {top: unset !important; display: flex; position: absolute; bottom: 0.6em; left: 0em !important; width: 100vw; z-index: 10;} .details-button { background: #fff9 !important; color: 000e !important; position: absolute; border: none; border-radius: 5px; padding: 0.5em 1.5em; font-family: "Titillium Web", sans-serif; cursor: pointer; z-index: 10; transition: background-color 0.3s ease; align-items: center; justify-content: center; bottom: -0.4em; font-size: 0.7em !important; background: #fff9; color: #000e; font-weight: 600; transform: translateX(100%) !important; right: 47%; display: flex; padding-right: 2em;} .details-button:hover { background-color: #fffc;} .watch-trailer-button:hover { background-color: #fffc !important;} .details-button:hover { background-color: #fffc !important;} .fas {padding: 0.5em;}}
@media (max-width:1000px) and (orientation:portrait) {.back-button {right:58vw;} .skip-button {right: 0em; top: 0.25em !important;} .genres {right: 50vw; position: absolute; max-width: 95vw !important; overflow:hidden; top: unset; transform: translateX(50%); bottom: 6.5em;} .lorem-ipsum {position: absolute; left: unset; bottom: 4.25em;}}
@media (max-width:1000px) and (orientation:landscape) {.community-rating {left:89vw !important;} .critic-rating {left:76vw !important;} .back-button {right:70vw;} .skip-button {right: 0em; top: 0.25em !important;} .genres {position: absolute; bottom: 6.5em; top: unset; transform: translateX(50%); right: 50vw;} .lorem-ipsum {position: absolute; left: unset; bottom: 4.25em;}}
@media (min-width: 1001px) { .material-icons {font-size: 2em;}.details-button { color: white; background: #101010e3; font-size: 1.75em; padding: 0.25em; padding-left: 0.5em; padding-right: 0.75em; transition: background-color 0.3s ease; cursor: pointer; border-radius: 0.5em;} .lorem-ipsum { bottom: -47em; left: 59em; font-size: 1.1em; transform: translateX(-100%); transition: left 0.5s;} clickable-overlay {pointer-events: none;} .logo {max-height: 23em; position: absolute; top: 29em; transition: transform 0.1s ease, max-height 0.3s ease; max-width: 47em; left: 34em; transform: translateX(-50%) translateY(-50%); transition: left 0.5s, max-width 0.5s;} @media screen and (max-width:1400px){ .logo {max-width: 37em; left: 30em;} .lorem-ipsum {left: 55em;}} .watch-trailer-button {display: none;} .backdrop {filter: brightness(95%) contrast(105%) saturate(105%); left: 0%; width: 100%; transition: width 0.5s ease, filter 0.8s ease, transform 2s ease; filter: brightness(95%) contrast(105%) saturate(105%);} .slide {margin-top: -3.15em; height: 100vh;} .skip-button {top: 17.5em; right: 0.5em; font-size: 2em;} .video-container {top: 9em; height: 18.62em; right: 4em; border-radius: 0em; transform-origin: 100% 0; transition: transform 0.5s ease; transition-delay: 1s;} .plot {position: relative; bottom: -33.5em; left: 4em; font-size: 1.25em; max-width: 49em !important; width: 49em !important; -webkit-line-clamp: 3;} .genres {z-index: 3; bottom: unset; top: 10.5em; left: 5.1em; font-size: 1.1em; max-width: 74em;} .age-rating {font-size: 1.3em !important; position: absolute; left: 4em; top: 6.2em; white-space: nowrap;} .community-rating {top: 44em; left: 50em; font-size: 1.2em;} .critic-rating {top: 44em; left: 43em; font-size: 1.2em;} .heading::before {content: ''; position: absolute; top: 2.27em; left: 0; width: 100%; background: linear-gradient(0deg, rgb(0% 0% 0%) 0%, rgb(0% 0% 0% / 0.9903926402016152) 6.25%, rgb(0% 0% 0% / 0.9619397662556434) 12.5%, rgb(0% 0% 0% / 0.9157348061512727) 18.75%, rgb(0% 0% 0% / 0.8535533905932737) 25%, rgb(0% 0% 0% / 0.7777851165098011) 31.25%, rgb(0% 0% 0% / 0.6913417161825449) 37.5%, rgb(0% 0% 0% / 0.5975451610080642) 43.75%, rgb(0% 0% 0% / 0.5) 50%, rgb(0% 0% 0% / 0.4024548389919359) 56.25%, rgb(0% 0% 0% / 0.3086582838174552) 62.5%, rgb(0% 0% 0% / 0.22221488349019902) 68.75%, rgb(0% 0% 0% / 0.14644660940672627) 75%, rgb(0% 0% 0% / 0.08426519384872733) 81.25%, rgb(0% 0% 0% / 0.03806023374435663) 87.5%, rgb(0% 0% 0% / 0.009607359798384785) 93.75%, rgb(0% 0% 0% / 0) 100% ); height: 100vh; z-index:6; opacity: 1; bottom: 0;} .skip-button { position: absolute; top: 0.4em; right: 0.25em; font-size: 1.5em; color: #D3D3D3; cursor: pointer; z-index: 10; font-family: "Titillium Web", sans-serif; font-size: 1.35em; text-shadow: 1px 1px 4px rgba(0, 0, 0, 1);}}
@media (max-width: 500px) { .community-rating {left: 0em; top: 5.6em;} .critic-rating {left: 0em; top: 7.6em;} .additional-info {left: 18.5%; top: 5em; text-align: right;} .heading::before {display: none;} .logo {max-width: 60%; max-height: 35%; left: 50% !important;}}
.age-rating { font-weight: 600; font-size: 1.1em;} .additional-info {display: none;}
.age-rating span.gb-u, .age-rating span.u { background-color: #078c6d; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.gb-pg, .age-rating span.pg { background-color: #d7a203; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.gb-12a, .age-rating span.\31 2A { background-color: #ee7600; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.gb-12, .age-rating span.\31 2, .age-rating span.gb-15, .age-rating span.\31 2 { background-color: #e19887; color: #e2002d; border: 0.09em solid white !important; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.pg-13 { background-color: #157c0d; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.tv-y7, .age-rating span.tv-13, .age-rating span.tv-14, .age-rating span.\31 6, .age-rating span.tv-ma, .age-rating span.tv-y, .age-rating span.tv-g, .age-rating span.tv-pg { background-color: #157c0d; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.nc-17, .age-rating span.gb-18, .age-rating span.\31 8, .age-rating span.r { background-color: #c10f1f; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);}
.age-rating span.au-g, .age-rating span.nz-g, .age-rating span.eu-pg, .age-rating span.ca-g, .age-rating span.jp-g, .age-rating span.de-0 { background-color: #078c6d; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); }
.age-rating span.au-pg, .age-rating span.nz-pg, .age-rating span.eu-12, .age-rating span.ca-pg, .age-rating span.jp-pg12, .age-rating span.de-6 { background-color: #d7a203; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); }
.age-rating span.au-m, .age-rating span.nz-m, .age-rating span.eu-16, .age-rating span.ca-14a, .age-rating span.jp-r15, .age-rating span.de-12 { background-color: #07b1e0; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); }
.age-rating span.au-ma15, .age-rating span.nz-r15, .age-rating span.eu-18, .age-rating span.ca-18a, .age-rating span.jp-r18, .age-rating span.de-16 { background-color: #fc9712; color: white; border: 0.09em solid white !important; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); }
.age-rating span.au-r18, .age-rating span.nz-r18, .age-rating span.ca-r, .age-rating span.jp-r18+, .age-rating span.de-18 { background-color: #c10f1f; color: white; text-shadow: 1px 1px 1px rgba(0, 0, 0, 1); }
.video-container {overflow: visible;}
.watch-trailer-button { }
.watch-trailer-button .fa-play { margin-right: 0.5em;}
.slide { opacity: 1; transition: opacity 0.31s ease-in-out, font-size 1s ease; }
.fade-in { opacity: 0; visibility: hidden; transition: opacity 0.31s ease-in-out;}
.fade-in-active { opacity: 1; visibility: visible;}
.fade-out { opacity: 0;}
.backdrop { transition: opacity 0.3s ease-in-out, width 0.5s ease, transform 1.5s ease, filter 1s ease;}
#video-overlay { position: fixed; top: 13vh; left: 0vw; width: 100%; height: 140vh; background-color: rgb(0, 0, 0); z-index: 1000; display: flex; background-size: cover; padding: 6vh; padding-top: 10vh; margin-top: -10vh;}
#video-overlay-content { position: relative; width: 90%; max-width: 100%; height: 60%; background-color: #000;}
#close-overlay { position: absolute; top: -1em; right: 0em; font-size: 1.5em; color: #7b7b7b; cursor: pointer;}
#countdown-bar-container { display: none; position: absolute; left: 0%; bottom: 0; width: 110%; height: 0.15em !important; background-color: rgba(0,0,0,0); z-index: 10;}
#countdown-bar { height: 100%; width: 0%; background-color: #ffffff; opacity: 0.5; transition: width 0.1s linear; border-radius: 1em;}
.video-player { mask-image: linear-gradient(45deg, rgb(0% 0% 0% / 0) 0%, rgb(0% 0% 0% / 0.1) 25%, rgb(0% 0% 0% / 0.7) 50%, rgb(0% 0% 0% / 1) 100% ); -webkit-mask-image: linear-gradient(45deg, rgb(0% 0% 0% / 0) 0%, rgb(0% 0% 0% / 0.1) 25%, rgb(0% 0% 0% / 0.7) 50%, rgb(0% 0% 0% / 1) 100% ); transition: mask-image 5s ease, -webkit-mask-image 5s ease; }
.video-container:hover { mask-image: none; -webkit-mask-image: none; transform:scale(1.335); transition-delay: 0s;}
.video-container:hover .video-player { mask-image: none; -webkit-mask-image: none;}
.skip-button {font-size: 2em; top: 50vh; position: absolute; z-index: 20; cursor: pointer; opacity: 0.5; transition: opacity 0.3s ease;} .skip-button:hover {opacity: 1;}
.buttons-container {display: flex; position: absolute; top: 53.6em; left: 8em; z-index: 10;}
.details-button:hover { background-color: #444;}
@media (min-width: 2000px) { .slide {font-size: 0.85vw;} .backdrop {top: 2em;}}
@media (min-width: 3000px) { .slide {.backdrop {top: 0em;} .slide {margin-top: -4vh;}}
@media screen and (min-aspect-ratio: 21/9) { .slide {font-size: 1.6vh; }}
.watch-trailer-button { position: absolute; border: none; border-radius: 5px; padding: 0.5em 1.5em; font-size: 0.9em; font-family: "Titillium Web", sans-serif; cursor: pointer; z-index: 10; transition: background-color 0.3s ease; align-items: center; justify-content: center; padding-right: 2em;}
.material-symbols-outlined { vertical-align: middle; font-size: 0.85em; opacity: 0.75; }
</style>
</head>
<body>
<div id="slides-container"></div>
<div id="video-overlay" style="display: none;">
<div id="video-overlay-content">
<span id="close-overlay" class="fas fa-times"></span>
<iframe id="trailer-video" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>
</div>
</div>
<script>
let title = 'Spotlight', moviesSeriesBoth = 3, shuffleInterval = 15000, plotMaxLength = 350, token = 'YOURAPIKEYHERE', useTrailers = true;
let isChangingSlide = false, player = null, slideChangeTimeout = null, isHomePageActive = false;
let currentLocation = window.top.location.href;
let movieList = [], currentMovieIndex = 0;
const createElem = (tag, className, textContent, src, alt) => {
const elem = document.createElement(tag);
if (className) elem.className = className;
if (textContent) elem.textContent = textContent;
if (src) elem.src = src;
if (alt) elem.alt = alt;
return elem;
};
// Check for screen size below 1000px
function isMobile() {
return window.innerWidth <= 1000;
}
const truncateText = (text, maxLength) => text.length > maxLength ? text.substr(0, maxLength) + '...' : text;
const cleanup = () => {
if (player) { player.destroy(); player = null; }
clearTimeout(slideChangeTimeout);
const container = document.getElementById('slides-container');
if (container) container.innerHTML = '';
};
const createSlideElement = (movie, hasVideo = false) => {
cleanup();
const container = document.getElementById('slides-container');
const slide = createElem('div', 'slide');
['backdrop', 'logo'].forEach(type => slide.appendChild(createElem('img', type, null, `/Items/${movie.Id}/Images/${type.charAt(0).toUpperCase() + type.slice(1)}${type === 'backdrop' ? '/0' : ''}`, type)));
slide.appendChild(createElem('div', 'heading', title));
const textContainer = createElem('div', 'text-container');
const premiereYear = movie.PremiereDate ? new Date(movie.PremiereDate).getFullYear() : 'Unknown';
const additionalInfo = movie.Type === 'Series' ?
(movie.ChildCount ? `${movie.ChildCount} Season${movie.ChildCount > 1 ? 's' : ''}` : 'Unknown Seasons') :
(movie.RunTimeTicks ? `${Math.round(movie.RunTimeTicks / 600000000)} min` : 'Unknown Runtime');
let loremText = `
<span style="border: 1px solid #fff8; background: #101010e3; border-radius: 0.5em; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">${additionalInfo}</span>
<span style="border: 1px solid #fff8; background: #101010e3; border-radius: 0.5em; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">${premiereYear}</span> `;
if (movie.CommunityRating) {
loremText += `<span style="border: 1px solid #fff8; background: #101010e3; border-radius: 0.5em; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">
<i class="star-icon fas fa-star"></i> ${movie.CommunityRating.toFixed(1)}
</span> `;
}
if (movie.CriticRating) {
loremText += `<span style="border: 1px solid #fff8; background: #101010e3; border-radius: 0.5em; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">
<img src="https://i.imgur.com/rMvyQMt.png" alt="Rotten Tomatoes" style="width: 1.05em; height: 1.25em; font-size: 0.9em; padding-right: 0.1em; margin-left: -0.1em; vertical-align: middle;">
${movie.CriticRating}%</span>`;
}
// Age Rating
const ageRating = movie.OfficialRating ? movie.OfficialRating : 'NR';
const ratingClass = ageRating.toLowerCase().replace(/ /g, '-');
const ageRatingDiv = createElem('div', 'age-rating');
const ageRatingSpan = createElem('span', ratingClass, ageRating);
ageRatingSpan.style.cssText = 'border: 0.09em solid currentColor; border-radius: .1em; padding: 0.2em; display: inline-block; text-align: center; line-height: 0.8em;';
ageRatingDiv.appendChild(ageRatingSpan);
slide.appendChild(ageRatingDiv);
// Genres Section
const genresDiv = createElem('div', 'genres');
if (movie.Genres && movie.Genres.length > 0) {
movie.Genres.forEach(genre => {
const genreElem = createElem('span', 'genre-item');
genreElem.textContent = genre;
genreElem.style.backgroundColor = '#101010e3';
genreElem.style.paddingLeft = '0.5em';
genreElem.style.paddingRight = '0.5em';
genreElem.style.paddingTop = '0.1em';
genreElem.style.paddingBottom = '0.1em';
genreElem.style.color = 'white';
genreElem.style.borderRadius = '0em';
genreElem.style.marginRight = '1em';
genreElem.style.border = '1px solid #fff8';
genreElem.style.borderRadius = '0.5em';
genresDiv.appendChild(genreElem);
});
} else {
genresDiv.textContent = 'Genres: N/A';
}
slide.appendChild(genresDiv);
const loremDiv = createElem('div', 'lorem-ipsum');
loremDiv.innerHTML = loremText;
textContainer.appendChild(loremDiv);
textContainer.appendChild(createElem('div', 'plot', truncateText(movie.Overview, plotMaxLength)));
slide.appendChild(textContainer);
// Create Details buttons
const buttonsContainer = createElem('div', 'buttons-container');
// Details Button
const detailsButton = createElem('button', 'details-button');
const playIcon = createElem('i', 'fas fa-play');
detailsButton.appendChild(playIcon);
detailsButton.appendChild(document.createTextNode(' Play'));
detailsButton.onclick = (e) => {
e.stopPropagation();
window.top.location.href = `/#!/details?id=${movie.Id}`; // Navigate to details page
};
// Append buttons to the container
buttonsContainer.appendChild(detailsButton);
slide.appendChild(buttonsContainer);
const backButton = createElem('div', 'back-button');
const backIcon = createElem('i', 'material-icons');
backIcon.textContent = 'chevron_left';
const skipButton = createElem('div', 'skip-button');
const skipIcon = createElem('i', 'material-icons');
skipIcon.textContent = 'chevron_right';
backButton.appendChild(backIcon);
skipButton.appendChild(skipIcon);
skipIcon.onclick = (e) => { e.stopPropagation(); fetchRandomMovie(); };
slide.appendChild(backButton);
slide.appendChild(skipButton);
const overlay = createElem('div', 'clickable-overlay');
overlay.onclick = () => window.top.location.href = `/#!/details?id=${movie.Id}`;
slide.appendChild(overlay);
if (hasVideo && movie.RemoteTrailers && movie.RemoteTrailers.length > 0) {
const trailerUrl = movie.RemoteTrailers[0].Url;
const watchTrailerButton = createElem('button', 'watch-trailer-button');
const playIcon = document.createElement('i');
playIcon.className = 'fas fa-film';
watchTrailerButton.appendChild(playIcon);
watchTrailerButton.appendChild(document.createTextNode(' Trailer'));
watchTrailerButton.onclick = (e) => {
e.stopPropagation();
if (isMobile()) {
// Show the video in an overlay for mobile
showVideoOverlay(trailerUrl);
} else {
// Open the trailer in a new tab for desktop
window.open(trailerUrl, '_blank');
}
};
slide.appendChild(watchTrailerButton);
}
if (useTrailers && hasVideo && movie.RemoteTrailers?.length > 0) {
const videoId = new URL(movie.RemoteTrailers[0].Url).searchParams.get('v');
const videoContainer = createElem('div', 'video-container');
const videoElement = createElem('div', 'video-player');
videoContainer.appendChild(videoElement);
slide.appendChild(videoContainer);
player = new YT.Player(videoElement, {
height: '100%', width: '100%', videoId,
events: {
'onReady': event => {
event.target.playVideo();
['backdrop', 'plot', 'lorem-ipsum'].forEach(cls => {
const element = document.querySelector(`.${cls}`);
if (element) element.style.width = '100%';
});
videoContainer.style.width = '27.6vw';
},
'onStateChange': event => { if (event.data === YT.PlayerState.ENDED) setTimeout(fetchRandomMovie, 100); },
'onError': () => {
console.error(`YouTube prevented playback of '${movie.Name}'`);
if (player) { player.destroy(); player = null; }
['backdrop', 'plot', 'lorem-ipsum'].forEach(cls => {
const element = document.querySelector(`.${cls}`);
if (element) element.style.width = '100%';
});
videoContainer.style.width = '0';
startSlideChangeTimer();
}
}
});
} else startSlideChangeTimer();
container.innerHTML = '';
container.appendChild(slide);
};
function addSwipeListeners(slide) {
let startX, startY, distX, distY;
const threshold = 50;
const restraint = 100;
slide.addEventListener('touchstart', e => {
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
});
slide.addEventListener('touchmove', e => {
const touch = e.touches[0];
distX = touch.clientX - startX;
distY = touch.clientY - startY;
});
slide.addEventListener('touchend', () => {
if (Math.abs(distX) > threshold && Math.abs(distY) < restraint) {
if (distX > 0) {
console.log('Swipe Right');
} else {
console.log('Swipe Left');
fetchRandomMovie();
}
}
distX = distY = 0;
});
}
// Show the video overlay
function showVideoOverlay(trailerUrl) {
const videoOverlay = document.getElementById('video-overlay');
const videoFrame = document.getElementById('trailer-video');
const closeOverlay = document.getElementById('close-overlay');
// Extract video ID from trailer URL
const videoId = new URL(trailerUrl).searchParams.get('v');
const embedUrl = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
// Set iframe's source to trailer URL
videoFrame.src = embedUrl;
// Show the overlay
videoOverlay.style.display = 'block';
// Pause the slide timer when the video overlay is open
clearSlideChangeTimeout();
// Close the overlay when the X is clicked
closeOverlay.onclick = () => {
videoOverlay.style.display = 'none';
videoFrame.src = ''; // Stop the video
};
// Close the overlay when clicking outside content
window.onclick = (event) => {
if (event.target === videoOverlay) {
videoOverlay.style.display = 'none';
videoFrame.src = '';
}
};
}
// Close video overlay and restart slide timer
function closeVideoOverlay() {
const videoOverlay = document.getElementById('video-overlay');
const videoFrame = document.getElementById('trailer-video');
// Hide overlay
videoOverlay.style.display = 'none';
// Reset the iframe source
videoFrame.src = '';
// Restart slide change timer when video overlay is closed
startSlideChangeTimer();
}
function clearSlideChangeTimeout() {
if (slideChangeTimeout) {
clearTimeout(slideChangeTimeout);
slideChangeTimeout = null;
}
}
const startSlideChangeTimer = () => { clearTimeout(slideChangeTimeout); slideChangeTimeout = setTimeout(fetchRandomMovie, shuffleInterval); };
const checkBackdropAndLogo = movie => {
Promise.all(['/Images/Backdrop/0', '/Images/Logo'].map(url =>
fetch(`/Items/${movie.Id}${url}`, { method: 'HEAD' }).then(response => response.ok)
)).then(([backdropExists, logoExists]) =>
backdropExists && logoExists ? createSlideElement(movie, true) : fetchRandomMovie()
).catch(() => fetchRandomMovie());
};
const readCustomList = () =>
fetch('list.txt?' + new Date().getTime())
.then(response => response.ok ? response.text() : null)
.then(text => {
if (!text) return null;
const lines = text.split('\n').filter(Boolean);
title = lines.shift() || title;
return lines.map(line => line.trim().substring(0, 32));
})
.catch(() => null);
const fetchRandomMovie = () => {
if (isChangingSlide) return;
isChangingSlide = true;
if (movieList.length === 0) {
readCustomList().then(list => {
if (list) { movieList = list; currentMovieIndex = 0; }
fetchNextMovie();
});
} else fetchNextMovie();
};
const fetchNextMovie = () => {
const fetchCurrentUserId = () =>
fetch('/Sessions', {
headers: { 'Authorization': `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"` }
})
.then(response => response.json())
.then(sessions => {
const currentSession = sessions.find(session => session.UserId);
return currentSession ? currentSession.UserId : null;
})
.catch(() => null);
fetchCurrentUserId().then(currentUserId => {
if (!currentUserId) {
console.error('Could not retrieve the current user ID.');
return;
}
const headers = { 'Authorization': `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"` };
if (movieList.length > 0) {
if (currentMovieIndex >= movieList.length) currentMovieIndex = 0;
const movieId = movieList[currentMovieIndex];
currentMovieIndex++;
fetch(`/Users/${currentUserId}/Items/${movieId}?Fields=Overview,RemoteTrailers,PremiereDate,RunTimeTicks,ChildCount,Genres`, { headers })
.then(response => response.json())
.then(checkBackdropAndLogo)
.catch(() => startSlideChangeTimer())
.finally(() => { isChangingSlide = false; });
} else {
const itemTypes = moviesSeriesBoth === 1 ? 'Movie' : (moviesSeriesBoth === 2 ? 'Series' : 'Movie,Series');
fetch(`/Users/${currentUserId}/Items?IncludeItemTypes=${itemTypes}&Recursive=true&Limit=1&SortBy=random&Fields=Id,Overview,RemoteTrailers,PremiereDate,RunTimeTicks,ChildCount,Genres`, { headers })
.then(response => response.json())
.then(data => { if (data.Items[0]) checkBackdropAndLogo(data.Items[0]); })
.catch(() => startSlideChangeTimer())
.finally(() => { isChangingSlide = false; });
}
});
};
const checkNavigation = () => {
const newLocation = window.top.location.href;
if (newLocation !== currentLocation) {
currentLocation = newLocation;
const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html');
if (isHomePage(newLocation)) {
if (!isHomePageActive) {
console.log("Returning to homepage, reactivating slideshow");
isHomePageActive = true;
cleanup();
fetchRandomMovie();
}
} else if (isHomePageActive) {
console.log("Leaving homepage, cleaning up slideshow");
isHomePageActive = false;
cleanup();
}
}
};
setInterval(checkNavigation, 100);
document.addEventListener('DOMContentLoaded', () => {
if (window.innerWidth < 1001) useTrailers = false;
const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html');
if (isHomePage(window.top.location.href)) {
isHomePageActive = true;
readCustomList().then(list => {
if (list) { movieList = list; currentMovieIndex = 0; }
fetchRandomMovie();
});
}
});
window.addEventListener('unload', cleanup);
window.addEventListener('popstate', checkNavigation);
</script>
<script src="https://www.youtube.com/iframe_api"></script>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<style> .featurediframe {width: 95vw; height: 24em; display: block; border: 0; margin: -1em auto 0;} @media (min-width: 2100px) {.featurediframe {height: 33em;}} @media (max-width: 1599px) {.featurediframe {margin-top: 1.2em;}} @media (max-width: 800px) {.featurediframe {margin-top: 0.8em;}} </style> <iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe> <style> .featurediframe {width: 95vw; height: 24em; display: block; border: 0; margin: -1em auto 0;} @media (min-width: 2100px) {.featurediframe {height: 33em;}} @media (max-width: 1599px) {.featurediframe {margin-top: 1.2em;}} @media (max-width: 800px) {.featurediframe {margin-top: 0.8em; height: 25em;}} </style> <iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe>
<style> <style>
@@ -25,7 +25,40 @@
@media (max-width: 800px) { @media (max-width: 800px) {
.featurediframe { .featurediframe {
margin-top: 0.8em; margin-top: 0.8em;
height: 25em;
} }
} }
</style> </style>
<iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe> <iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe>
"use strict"; (self.webpackChunk = self.webpackChunk || []).push([[8372], { 5939: function (a, e, t) { t.r(e), e.default = '<div id="indexPage" style="outline:0" data-role="page" data-dom-cache="true" class="page homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie,series,book"><style> .featurediframe {width: 95vw; height: 24em; display: block; border: 0; margin: -1em auto 0;} @media (min-width: 2100px) {.featurediframe {height: 33em;}} @media (max-width: 1599px) {.featurediframe {margin-top: 1.2em;}} @media (max-width: 800px) {.featurediframe {margin-top: 0.8em; height: 25em;}} </style> <iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe> <style>:root { --save-gut: max(env(safe-area-inset-left), .3%) } .requestIframe { margin: 0 .4em; padding: 0 var(--save-gut); width: calc(100% - (.4em * 2) - (var(--save-gut) * 2)); height: 90vh; border: none; position: absolute; top: 5.3em } @media (max-width: 1599px) { .requestIframe { height: 83vh; top: 8.2em; } }</style><script>setTimeout(() => { createRequestTab() }, 500)</script> <div class="tabContent pageTabContent" id="homeTab" data-index="0"> <div class="sections"></div> </div> <div class="tabContent pageTabContent" id="favoritesTab" data-index="1"> <div class="sections"></div> </div><div class="tabContent pageTabContent" id="requestsTab" data-index="2"> <div class="sections"><iframe class="requestIframe" src="https://jellyseerr.mahom03-spacecloud.de"></iframe></div> </div> </div> ' } }]);
NEU (aber nicht genutzt):
<style> .featurediframe {width: 95vw; height: 23.5em; display: block; border: 0px solid #000; margin: 0 auto; margin-bottom: 0em; margin-top: 1em;} @media (min-width: 3158px) {.featurediframe {height: 50em;} } @media (min-width: 2601px) and (max-width: 3157px) {.featurediframe {height: 33em;} } @media (min-width: 2000px) and (max-width: 2600px) {.featurediframe {height: 27em; font-size: 133%;} .layout-desktop #homeTab .sections.homeSectionsContainer {margin-top: -3em !important;} } @media (max-width: 1000px) and (orientation: portrait) {.featurediframe {height: 25em; margin-bottom: -3em;} } @media (max-width: 1000px) and (orientation: landscape) {.featurediframe {height: 26em; margin-bottom: -7em;} } @media (max-width: 400px) and (orientation: portrait) {.featurediframe {height: 45vh; margin-bottom: 0em;} } @media (max-height: 400px) and (orientation: landscape) {.featurediframe {height: 100vh;} } @media screen and (aspect-ratio: 4/3) {.featurediframe {height: 25em;} } @media screen and (aspect-ratio: 3/4) {.featurediframe {height: 25em; margin-bottom: -5em;} } @media screen and (aspect-ratio: 16/10) and (max-height: 1200px) {.featurediframe {height: 34em; margin-bottom: -5em;} } @media screen and (aspect-ratio: 10/16) and (max-height: 1280px) {.featurediframe {height: 25em; margin-bottom: -5em;} } @media (min-aspect-ratio: 21/9) and (min-width: 3000px) {.featurediframe { height: 50em;} } </style> <iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe>
<style>
.featurediframe {width: 95vw; height: 23.5em; display: block; border: 0px solid #000; margin: 0 auto; margin-bottom: 0em; margin-top: 1em;}
@media (min-width: 3158px) {.featurediframe {height: 50em;} }
@media (min-width: 2601px) and (max-width: 3157px) {.featurediframe {height: 33em;} }
@media (min-width: 2000px) and (max-width: 2600px) {.featurediframe {height: 27em; font-size: 133%;} .layout-desktop #homeTab .sections.homeSectionsContainer {margin-top: -3em !important;} }
@media (max-width: 1000px) and (orientation: portrait) {.featurediframe {height: 25em; margin-bottom: -3em;} }
@media (max-width: 1000px) and (orientation: landscape) {.featurediframe {height: 26em; margin-bottom: -7em;} }
@media (max-width: 500px) and (orientation: portrait) {.featurediframe {height: 45vh; margin-bottom: 0em;} }
@media (max-height: 500px) and (orientation: landscape) {.featurediframe {height: 100vh;} }
@media screen and (aspect-ratio: 4/3) {.featurediframe {height: 25em;} }
@media screen and (aspect-ratio: 3/4) {.featurediframe {height: 25em; margin-bottom: -5em;} }
@media screen and (aspect-ratio: 16/10) and (max-height: 1200px) {.featurediframe {height: 34em; margin-bottom: -5em;} }
@media screen and (aspect-ratio: 10/16) and (max-height: 1280px) {.featurediframe {height: 25em; margin-bottom: -5em;} }
@media (min-aspect-ratio: 21/9) and (min-width: 3000px) {.featurediframe { height: 50em;} }
</style>
<iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe>
"use strict"; (self.webpackChunk = self.webpackChunk || []).push([[8372], { 5939: function (a, e, t) { t.r(e), e.default = '<div id="indexPage" style="outline:0" data-role="page" data-dom-cache="true" class="page homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie,series,book"><style> .featurediframe {width: 95vw; height: 23.5em; display: block; border: 0px solid #000; margin: 0 auto; margin-bottom: 0em; margin-top: 1em;} @media (min-width: 3158px) {.featurediframe {height: 50em;}} @media (min-width: 2601px) and (max-width: 3157px) {.featurediframe {height: 33em;}} @media (min-width: 2000px) and (max-width: 2600px) {.featurediframe {height: 27em; font-size: 133%;} .layout-desktop #homeTab .sections.homeSectionsContainer {margin-top: -3em !important;}} @media (max-width: 1000px) and (orientation: portrait) {.featurediframe {height: 25em; margin-bottom: -3em;}} @media (max-width: 1000px) and (orientation: landscape) {.featurediframe {height: 26em; margin-bottom: -7em;}} @media (max-width: 400px) and (orientation: portrait) {.featurediframe {height: 45vh; margin-bottom: 0em;}} @media (max-height: 400px) and (orientation: landscape) {.featurediframe {height: 100vh;}} @media screen and (aspect-ratio: 4/3) {.featurediframe {height: 25em;}} @media screen and (aspect-ratio: 3/4) {.featurediframe {height: 25em; margin-bottom: -5em;}} @media screen and (aspect-ratio: 16/10) and (max-height: 1200px) {.featurediframe {height: 34em; margin-bottom: -5em;}} @media screen and (aspect-ratio: 10/16) and (max-height: 1280px) {.featurediframe {height: 25em; margin-bottom: -5em;}} @media (min-aspect-ratio: 21/9) and (min-width: 3000px) {.featurediframe { height: 50em;}}</style><iframe class="featurediframe" src="/web/ui/spotlight.html"></iframe> <style>:root { --save-gut: max(env(safe-area-inset-left), .3%) } .requestIframe { margin: 0 .4em; padding: 0 var(--save-gut); width: calc(100% - (.4em * 2) - (var(--save-gut) * 2)); height: 90vh; border: none; position: absolute; top: 5.3em } @media (max-width: 1599px) { .requestIframe { height: 83vh; top: 8.2em; } }</style><script>setTimeout(() => { createRequestTab() }, 500)</script> <div class="tabContent pageTabContent" id="homeTab" data-index="0"> <div class="sections"></div> </div> <div class="tabContent pageTabContent" id="favoritesTab" data-index="1"> <div class="sections"></div> </div><div class="tabContent pageTabContent" id="requestsTab" data-index="2"> <div class="sections"><iframe class="requestIframe" src="https://jellyseerr.mahom03-spacecloud.de"></iframe></div> </div> </div> ' } }]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
images/all_clips.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

BIN
images/demo1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

BIN
images/desktop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
images/fullscreen.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 MiB

BIN
images/mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

View File

@@ -1 +0,0 @@
wsfaf

9
list.txt Normal file
View File

@@ -0,0 +1,9 @@
Title of List [muteon/muteoff]
f81c9b854e3edc62fb049e252c488115 Hunger Games
7d515072462d6cedd74de8e2df71888f Interstellar
df4f3da3066455404e6b874d22ba86aa Fast X
0eab5162a26ed05546215e762dde8544 Greatest Showman
820d0a5c3eaee93e8d3bfbb670a9a41e Mission Impossible 7
b948bde88c4e1f48e84f26fec46df211 Oppenheimer
daeab01d279b7df4f6726892a09d3846 Im Westen nichts neues
5c70c7506a20814c6cfa15d139b4c0b5 Lupin

738
script.js
View File

@@ -1,17 +1,249 @@
let title = 'Spotlight'; // Title of the slideshow let title = 'Spotlight'; // Title of the slideshow TBD
let listFileName = 'list.txt'; // Name of the file containing the list of movie IDs let listFileName = 'list.txt'; // Name of the file containing the list of movie IDs
let token = 'YOURAPIKEYHERE'; // Your Jellyfin API key
let moviesSeriesBoth = 3; // 1 for movies, 2 for series, 3 for both let moviesSeriesBoth = 3; // 1 for movies, 2 for series, 3 for both
let shuffleInterval = 15000; // Time in milliseconds before the next slide is shown, unless trailer is playing let shuffleInterval = 15000; // Time in milliseconds before the next slide is shown, unless trailer is playing
let useTrailers = true; // Set to false to disable trailers let useTrailers = true; // Set to false to disable trailers
let setRandomMovie = true; // Set to false to disable random movie selection from the list let setRandomMovie = true; // Set to false to disable random movie selection from the list
let showOnOtherPages = false; // Set to true to show the slideshow on all pages eg. favorites tab, requests tab, etc. let showOnOtherPages = false; // Set to true to show the slideshow on all pages eg. favorites tab, requests tab, etc.
let showTitle = false; // Set to false to hide the title let showTitle = false; // Set to true to place the slideshow title above the banner
let disableTrailerControls = false; // Set to false to enable trailer controls
let setMutedHover = true; // Set to false to disable unmuting the video on hover
let unmutedVolume = 20; // Set the volume level when the video is unmuted
let useSponsorBlock = true; // Set to true to use SponsorBlock data to skip intro/outro segments of trailers
let skipIntro = true; // Set to true to skip the intro segment of the trailer
let plotMaxLength = 550; // Maximum number of characters in the plot let plotMaxLength = 550; // Maximum number of characters in the plot
let trailerMaxLength = 0; // Default value 0; length measured in ms, set to 0 to disable, could be used instead of SponsorBlock
let startTrailerMuted = true; // Default value true; set to false to start the video unmuted
// Seasonal lists configuration
let useSeasonalLists = false; // Set to true to enable automatic seasonal list switching
const seasonalLists = {
spring: 'spring_list.txt', // spring (march-may)
summer: 'summer_list.txt', // summer (june-august)
autumn: 'autumn_list.txt', // autumn (september-november)
winter: 'winter_list.txt', // winter (december-february)
newyear: 'newyear_list.txt', // new year (1.-7. januar)
valentine: 'valentine_list.txt', // valentines day (10.-20. februar)
easter: 'easter_list.txt', // easter (variable dates, March-April)
halloween: 'halloween_list.txt' // halloween (20.-31. october)
};
// Language specific strings
// currently implemented: en, de, fr, es, it, pl, nl
const seasonTerms = ["Season", "Staffel", "Saison", "Temporada", "Stagione", "Sezon", "Seizoen"];
const seasonsTerms = ["Seasons", "Staffeln", "Saisons", "Temporadas", "Stagioni", "Sezony", "Seizoenen"];
const unknownSeasonsTerms = ["Unknown seasons", "Unbekannte Staffeln","Saisons inconnues", "Temporadas desconocidas", "Stagioni sconosciute", "Nieznane sezony", "Onbekende seizoenen"];
const movieTerms = ["Movie", "Film", "Film", "Película", "Film", "Film", "Film"];
const moviesTerms = ["Movies", "Filme", "Films", "Películas", "Film", "Filmy", "Films"];
const unknownMoviesTerms = ["Unknown movies", "Unbekannte Filme", "Films inconnus", "Películas desconocidas", "Film sconosciuti", "Nieznane filmy", "Onbekende films"];
const unknownYearTerms = ["Unknown year", "Unbekanntes Jahr", "Année inconnue", "Año desconocido", "Anno sconosciuto", "Nieznany rok", "Onbekend jaar"];
const unknownRuntimeTerms = ["Unknown Runtime", "Unbekannte Länge", "Durée inconnue", "Duración desconocida", "Durata sconosciuta", "Nieznany czas trwania", "Onbekende duur"];
// get the Jellyfin credentials from the local storage (api token and user id)
const getJellyfinCredentials = () => {
const jellyfinCreds = localStorage.getItem("jellyfin_credentials");
try {
const serverCredentials = JSON.parse(jellyfinCreds);
const firstServer = serverCredentials.Servers[0];
if (!firstServer) {
console.error("Could not find credentials for the client");
return;
}
return { token: firstServer.AccessToken, userId: firstServer.UserId };
} catch (e) {
console.error("Could not parse jellyfin credentials", e);
}
};
const { token, userId } = getJellyfinCredentials();
// variables
let isChangingSlide = false, player = null, slideChangeTimeout = null, isHomePageActive = false; let isChangingSlide = false, player = null, slideChangeTimeout = null, isHomePageActive = false;
let currentLocation = window.top.location.href; let currentLocation = window.top.location.href;
let movieList = [], currentMovieIndex = 0; let movieList = [], currentMovieIndex = 0;
let previousMovies = [];
let forwardMovies = [];
let monitorOutroInterval = null; // Global interval variable to monitor the outro segment of the trailer
let isMuted = startTrailerMuted; userInteracted = false;
const baseBackdropOverlapVW = 11.4;
let activeTrailerLayout = null;
// Get the current browser language
const getBrowserLanguage = () => {
const language = navigator.language || navigator.userLanguage;
return language.split('-')[0]; // Return the language code (e.g., 'en', 'de')
};
const browserLanguage = getBrowserLanguage();
// set corresponding language index
const setLanguage = (language) => {
switch (language) {
case 'de':
return 1;
case 'fr':
return 2;
case 'es':
return 3;
case 'it':
return 4;
case 'pl':
return 5;
case 'nl':
return 6;
default:
return 0;
}
};
const languageIndex = setLanguage(browserLanguage);
// Seasonal list detection
const getCurrentSeason = () => {
if (!useSeasonalLists) {
return null;
}
const now = new Date();
const month = now.getMonth() + 1; // 1-12
const day = now.getDate();
// Special events (take precedence over seasons)
// new year: 1-7 january
if (month === 1 && day <= 7) return 'newyear';
// valentines day: 10-20 february
if (month === 2 && day >= 10 && day <= 20) return 'valentine';
// halloween: 20-31 cotober
if (month === 10 && day >= 20) return 'halloween';
// Easter calculation (simplified - around March-April)
const easterStart = getEasterPeriod(now.getFullYear());
if (isInEasterPeriod(now, easterStart)) return 'easter';
// Regular seasons
if (month >= 3 && month <= 5) return 'spring'; // march-may
if (month >= 6 && month <= 8) return 'summer'; // june-august
if (month >= 9 && month <= 11) return 'autumn'; // september-november
if (month === 12 || month <= 2) return 'winter'; // december-february
return null;
};
// Simplified Easter calculation (Western Easter)
const getEasterPeriod = (year) => {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return { month, day };
};
// Check if current date is in Easter period (2 weeks around Easter)
const isInEasterPeriod = (currentDate, easter) => {
const easterDate = new Date(currentDate.getFullYear(), easter.month - 1, easter.day);
const timeDiff = Math.abs(currentDate.getTime() - easterDate.getTime());
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
return daysDiff <= 7; // 1 week before and after Easter
};
// Get the appropriate list filename based on season
const getSeasonalListFileName = async () => {
const season = getCurrentSeason();
if (!season || !seasonalLists[season]) {
console.log('Using default list:', listFileName);
return listFileName;
}
// Check if seasonal file exists
const seasonalFile = seasonalLists[season];
try {
const response = await fetch(seasonalFile + '?' + new Date().getTime(), { method: 'HEAD' });
if (response.ok) {
console.log(`Using seasonal list for ${season}:`, seasonalFile);
return seasonalFile;
} else {
console.warn(`Seasonal file ${seasonalFile} not found, falling back to default list:`, listFileName);
return listFileName;
}
} catch (error) {
console.warn(`Error checking seasonal file ${seasonalFile}:`, error.message, '- falling back to default list:', listFileName);
return listFileName;
}
};
// Get SponsorBlock-Data for the outro/intro segment of the trailer
const fetchSponsorBlockData = async (videoId) => {
try {
const response = await fetch(`https://sponsor.ajay.app/api/skipSegments?videoID=${videoId}&categories=["intro","outro"]`);
const segments = await response.json();
let intro = null;
let outro = null;
segments.forEach(segment => {
if (segment.category === "intro" && Array.isArray(segment.segment)) {
intro = segment.segment; // [start, end] of the intro
} else if (segment.category === "outro" && Array.isArray(segment.segment)) {
outro = segment.segment; // [start, end] of the outro
}
});
return { intro, outro };
} catch (error) {
console.error('Error fetching SponsorBlock data:', error);
return { intro: null, outro: null };
}
};
// Monitor the video player for the outro segment
function monitorOutro(player, outroSegment) {
if (monitorOutroInterval) { // Clear the interval if it's already running
clearInterval(monitorOutroInterval);
}
monitorOutroInterval = setInterval(() => {
if (!outroSegment || !player) {
console.log('Invalid outro segment or player not initialized');
clearInterval(monitorOutroInterval);
return;
}
const currentTime = player.getCurrentTime();
if (currentTime >= outroSegment[0] && currentTime < outroSegment[1]) {
clearInterval(monitorOutroInterval);
player.stopVideo(); // stop video
setTimeout(fetchRandomMovie, 100); // fetch next movie
}
}, 500); // check every 500ms
}
const clearMonitorOutroInterval = () => {
if (monitorOutroInterval) {
clearInterval(monitorOutroInterval);
monitorOutroInterval = null;
}
};
const createElem = (tag, className, textContent, src, alt) => { const createElem = (tag, className, textContent, src, alt) => {
const elem = document.createElement(tag); const elem = document.createElement(tag);
@@ -30,24 +262,175 @@ function isMobile() {
const truncateText = (text, maxLength) => text.length > maxLength ? text.substr(0, maxLength) + '...' : text; const truncateText = (text, maxLength) => text.length > maxLength ? text.substr(0, maxLength) + '...' : text;
const cleanup = () => { const cleanup = () => {
if (player) { player.destroy(); player = null; } clearTrailerLayout();
if (player && typeof player.destroy === 'function') {
player.destroy();
}
player = null;
clearTimeout(slideChangeTimeout); clearTimeout(slideChangeTimeout);
slideChangeTimeout = null;
clearMonitorOutroInterval();
const container = document.getElementById('slides-container'); const container = document.getElementById('slides-container');
if (container) container.innerHTML = ''; if (container) container.innerHTML = '';
}; };
const clearTrailerLayout = () => {
if (!activeTrailerLayout) {
return;
}
const { videoContainer, backdrop, plot, genres, loremIpsum, logo } = activeTrailerLayout;
if (videoContainer) {
videoContainer.style.removeProperty('width');
}
if (backdrop) {
backdrop.style.removeProperty('width');
backdrop.style.removeProperty('left');
}
if (plot) {
plot.style.removeProperty('left');
plot.style.removeProperty('max-width');
}
if (genres) {
genres.style.removeProperty('left');
}
if (loremIpsum) {
loremIpsum.style.removeProperty('left');
}
if (logo) {
logo.style.removeProperty('left');
}
activeTrailerLayout = null;
};
const applyTrailerLayout = (videoContainer) => {
if (!videoContainer) {
return;
}
if (activeTrailerLayout && activeTrailerLayout.videoContainer !== videoContainer) {
clearTrailerLayout();
}
const slideWrapper = videoContainer.closest('.slide-wrapper');
if (!slideWrapper) {
return;
}
const slideElement = slideWrapper.querySelector('.slide');
const containerRect = videoContainer.getBoundingClientRect();
const slideRect = slideElement ? slideElement.getBoundingClientRect() : null;
const effectiveHeight = containerRect.height || (slideRect ? slideRect.height : 0);
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 1920;
if (!effectiveHeight || !viewportWidth) {
return;
}
const idealWidthVw = (effectiveHeight * (16 / 9)) / viewportWidth * 100;
const videoWidthVw = Math.max(idealWidthVw, 18);
const videoWidthVwCapped = Math.min(videoWidthVw, 34.4);
const videoWidthVwRounded = Math.min(Math.max(videoWidthVwCapped, 10), 60);
const backdrop = slideWrapper.querySelector('.backdrop');
const plot = slideWrapper.querySelector('.plot');
const genres = slideWrapper.querySelector('.genres');
const loremIpsum = slideWrapper.querySelector('.lorem-ipsum');
const logo = slideWrapper.querySelector('.logo');
videoContainer.style.width = `${videoWidthVwRounded.toFixed(3)}vw`;
if (backdrop) {
const backdropGap = Math.max(videoWidthVwRounded - baseBackdropOverlapVW, 0).toFixed(3);
backdrop.style.width = `calc(100% - ${backdropGap}vw)`;
backdrop.style.left = '0';
}
if (plot) {
const availableAreaVw = Math.max(100 - videoWidthVwRounded, 20);
const maxWidth = Math.max(availableAreaVw - 4, 30).toFixed(3);
plot.style.left = `calc(50% - ${(videoWidthVwRounded / 2).toFixed(3)}vw)`;
plot.style.maxWidth = `${maxWidth}vw`;
}
const anchorShift = `calc(50% - ${(videoWidthVwRounded / 2).toFixed(3)}vw)`;
if (genres) {
genres.style.left = anchorShift;
}
if (loremIpsum) {
loremIpsum.style.left = anchorShift;
}
if (logo) {
logo.style.left = anchorShift;
}
activeTrailerLayout = {
videoContainer,
backdrop,
plot,
genres,
loremIpsum,
logo
};
};
window.addEventListener('resize', () => {
if (activeTrailerLayout && document.contains(activeTrailerLayout.videoContainer)) {
applyTrailerLayout(activeTrailerLayout.videoContainer);
} else {
activeTrailerLayout = null;
}
});
const createSlideElement = (movie, hasVideo = false) => { const createSlideElement = (movie, hasVideo = false) => {
cleanup(); cleanup();
isMuted = startTrailerMuted;
const container = document.getElementById('slides-container'); const container = document.getElementById('slides-container');
const slideWrapper = createElem('div', 'slide-wrapper');
const slide = createElem('div', 'slide'); const slide = createElem('div', 'slide');
if (movie && (!previousMovies.length || previousMovies[previousMovies.length - 1].Id !== movie.Id)) {
previousMovies.push(movie);
}
if (previousMovies.length > 50) {
previousMovies.shift();
}
['backdrop', 'logo'].forEach(type => slide.appendChild(createElem('img', type, null, `/Items/${movie.Id}/Images/${type.charAt(0).toUpperCase() + type.slice(1)}${type === 'backdrop' ? '/0' : ''}`, type))); ['backdrop', 'logo'].forEach(type => slide.appendChild(createElem('img', type, null, `/Items/${movie.Id}/Images/${type.charAt(0).toUpperCase() + type.slice(1)}${type === 'backdrop' ? '/0' : ''}`, type)));
slide.appendChild(createElem('div', 'heading', title));
if (showTitle && title) {
const heading = createElem('div', 'heading', title);
heading.style.display = 'flex';
slideWrapper.classList.add('has-heading');
slideWrapper.appendChild(heading);
}
const textContainer = createElem('div', 'text-container'); const textContainer = createElem('div', 'text-container');
const premiereYear = movie.PremiereDate ? new Date(movie.PremiereDate).getFullYear() : 'Unknown'; const premiereYear = movie.PremiereDate ? new Date(movie.PremiereDate).getFullYear() : unknownYearTerms[languageIndex];
const additionalInfo = movie.Type === 'Series' ?
(movie.ChildCount ? `${movie.ChildCount} Season${movie.ChildCount > 1 ? 's' : ''}` : 'Unknown Seasons') : let additionalInfo;
(movie.RunTimeTicks ? `${Math.round(movie.RunTimeTicks / 600000000)} min` : 'Unknown Runtime'); if (movie.Type === 'Series') {
additionalInfo = movie.ChildCount
? `${movie.ChildCount} ${movie.ChildCount > 1 ? seasonsTerms[languageIndex] : seasonTerms[languageIndex]}`
: unknownSeasonsTerms[languageIndex];
} else if (movie.Type === 'BoxSet') {
additionalInfo = movie.ChildCount
? `${movie.ChildCount} ${movie.ChildCount > 1 ? moviesTerms[languageIndex] : movieTerms[languageIndex]}`
: unknownMoviesTerms[languageIndex];
} else {
additionalInfo = movie.RunTimeTicks
? `${Math.round(movie.RunTimeTicks / 600000000)} min`
: unknownRuntimeTerms[languageIndex];
}
let loremText = ` let loremText = `
<span style="background: transparent; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">${additionalInfo}</span> <span style="background: transparent; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">${additionalInfo}</span>
@@ -60,7 +443,7 @@ const createSlideElement = (movie, hasVideo = false) => {
} }
if (movie.CriticRating) { if (movie.CriticRating) {
loremText += `<span style="background: transparent; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;"> loremText += `<span style="background: transparent; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.05em; padding-bottom: 0.05em; margin-left: 1em;">
<img src="https://i.imgur.com/rMvyQMt.png" alt="Rotten Tomatoes" style="width: 1.05em; height: 1.25em; font-size: 0.9em; padding-right: 0.1em; margin-left: -0.1em; vertical-align: bottom;"> <img src="https://i.imgur.com/aoLXyXx.png" alt="Rotten Tomatoes" style="width: 1.05em; height: 1.25em; font-size: 0.83em; padding-right: 0.4em; margin-left: -0.1em; vertical-align: middle; padding-bottom: 0.3em;">
${movie.CriticRating}%</span>`; ${movie.CriticRating}%</span>`;
} }
@@ -111,15 +494,22 @@ const createSlideElement = (movie, hasVideo = false) => {
const backButton = createElem('div', 'back-button'); const backButton = createElem('div', 'back-button');
const backIcon = createElem('i', 'material-icons'); const backIcon = createElem('i', 'material-icons');
backIcon.textContent = 'chevron_left'; backIcon.textContent = 'chevron_left';
backButton.appendChild(backIcon);
backButton.onclick = (e) => {
e.stopPropagation();
navigateBack();
};
// Skip button logic
const skipButton = createElem('div', 'skip-button'); const skipButton = createElem('div', 'skip-button');
const skipIcon = createElem('i', 'material-icons'); const skipIcon = createElem('i', 'material-icons');
skipIcon.textContent = 'chevron_right'; skipIcon.textContent = 'chevron_right';
backButton.appendChild(backIcon);
skipButton.appendChild(skipIcon); skipButton.appendChild(skipIcon);
skipButton.onclick = (e) => {
e.stopPropagation();
navigateForward();
};
skipIcon.onclick = (e) => { e.stopPropagation(); fetchRandomMovie(); };
backIcon.onclick = (e) => { e.stopPropagation(); previousMovie(); };
slide.appendChild(backButton); slide.appendChild(backButton);
slide.appendChild(skipButton); slide.appendChild(skipButton);
@@ -177,79 +567,94 @@ const createSlideElement = (movie, hasVideo = false) => {
height: '100%', height: '100%',
width: '100%', width: '100%',
videoId, videoId,
events: { playerVars: {
'onReady': event => { mute: isMuted ? 1 : 0, // Mute the video
event.target.playVideo(); controls: disableTrailerControls ? 0 : 1, // Hide the controls
disablekb: 1, // Disable keyboard controls
iv_load_policy: 3, // Disable annotations
cc_load_policy: 3, // Disable captions
}, },
'onStateChange': event => { events: {
if (event.data === YT.PlayerState.PLAYING) { 'onReady': () => {
// Only show when YT video is successfully playing if (useSponsorBlock) {
const backdrop = document.querySelector('.backdrop'); fetchSponsorBlockData(videoId).then(({ intro, outro }) => {
if (backdrop) { if (intro && skipIntro) {
backdrop.style.width = 'calc(100% - 23vw)'; console.log(`SponsorBlock intro segment: Start - ${intro[0]}s, End - ${intro[1]}s`);
backdrop.style.left = '0vw'; if (player && typeof player.seekTo === 'function') {
player.seekTo(intro[1], true);
}
} else {
console.log('No intro segment found/provided or intro skipping is disabled');
} }
const plot = document.querySelector('.plot'); if (outro) {
if (plot) plot.style.width = 'calc(100% - 36.4vw)'; console.log(`SponsorBlock outro segment: Start - ${outro[0]}s, End - ${outro[1]}s`);
monitorOutro(player, outro);
} else {
console.log('No outro segment found/provided');
}
}).catch(error => {
console.error('Error reading SponsorBlock data:', error);
});
}
if (trailerMaxLength > 0) {
clearTimeout(slideChangeTimeout);
slideChangeTimeout = setTimeout(() => {
clearMonitorOutroInterval();
if (player) {
player.stopVideo();
player.destroy();
player = null;
}
fetchRandomMovie();
}, trailerMaxLength);
}
if (isMuted) {
player.mute(); // Mute the video if the setting is MuteOn
} else {
player.unMute();
}
const loremIpsum = document.querySelector('.lorem-ipsum'); player.playVideo();
if (loremIpsum) loremIpsum.style.paddingRight = '32.4vw'; player.setVolume(unmutedVolume); // Set the volume, value between 0 and 100
console.log(`Playing trailer for '${movie.Name}'`);
const logo = document.querySelector('.logo'); },
if (logo) logo.style.left = 'calc(50% - 14.2vw)'; 'onStateChange': (event) => {
if (event.data === YT.PlayerState.PLAYING) {
videoContainer.style.width = '34.4vw'; applyTrailerLayout(videoContainer);
} else if (event.data === YT.PlayerState.ENDED) { } else if (event.data === YT.PlayerState.ENDED) {
setTimeout(fetchRandomMovie, 100); clearTrailerLayout();
clearMonitorOutroInterval();
setTimeout(fetchRandomMovie, 20);
} }
}, },
'onError': () => { 'onError': () => {
console.error(`YouTube prevented playback of '${movie.Name}'`); console.error(`YouTube prevented playback of '${movie.Name}'`);
if (player) { if (player) {
clearMonitorOutroInterval();
player.destroy(); player.destroy();
player = null; player = null;
} }
// Reset style when a YT error occurs clearTrailerLayout();
const backdrop = document.querySelector('.backdrop');
if (backdrop) backdrop.style.width = '100%';
const plot = document.querySelector('.plot');
if (plot) plot.style.width = '98%';
const loremIpsum = document.querySelector('.lorem-ipsum');
if (loremIpsum) loremIpsum.style.paddingRight = '0';
const logo = document.querySelector('.logo');
if (logo) logo.style.left = '50%';
videoContainer.style.width = '0'; videoContainer.style.width = '0';
startSlideChangeTimer(); startSlideChangeTimer();
} }
} }
}); });
} else { } else {
startSlideChangeTimer(); startSlideChangeTimer();
} }
slideWrapper.appendChild(slide);
container.innerHTML = ''; container.innerHTML = '';
container.appendChild(slide); container.appendChild(slideWrapper);
}; };
// Fetch the previous movie in the list
function previousMovie() {
if (isChangingSlide) return;
isChangingSlide = true;
// Reset index or set to the end of the list if we are at the first element
currentMovieIndex = (currentMovieIndex - 2 + movieList.length) % movieList.length;
fetchNextMovie();
}
function addSwipeListeners(slide) { function addSwipeListeners(slide) {
let startX, startY, distX, distY; let startX, startY, distX, distY;
const threshold = 50; const threshold = 50;
@@ -280,6 +685,33 @@ function addSwipeListeners(slide) {
}); });
} }
const navigateBack = () => {
if (previousMovies.length < 2) {
console.log("No previous slides in history.");
return;
}
const currentMovie = previousMovies.pop();
forwardMovies.unshift(currentMovie);
const previousMovie = previousMovies[previousMovies.length - 1];
createSlideElement(previousMovie, true);
};
const navigateForward = () => {
if (forwardMovies.length === 0) {
console.log("No forward slides in history.");
fetchRandomMovie();
return;
}
const currentMovie = forwardMovies.shift();
previousMovies.push(currentMovie);
createSlideElement(currentMovie, true);
};
// Show the video overlay // Show the video overlay
function showVideoOverlay(trailerUrl) { function showVideoOverlay(trailerUrl) {
const videoOverlay = document.getElementById('video-overlay'); const videoOverlay = document.getElementById('video-overlay');
@@ -346,115 +778,135 @@ const checkBackdropAndLogo = movie => {
).catch(() => fetchRandomMovie()); ).catch(() => fetchRandomMovie());
}; };
const readCustomList = () => const readCustomList = async () => {
fetch(listFileName + '?' + new Date().getTime()) const currentListFile = await getSeasonalListFileName();
return fetch(currentListFile + '?' + new Date().getTime())
.then(response => response.ok ? response.text() : null) .then(response => response.ok ? response.text() : null)
.then(text => { .then(text => {
if (!text) return null; if (!text || !text.trim()) {
const lines = text.split('\n').filter(Boolean); console.warn('List.txt is empty or could not be loaded.');
title = lines.shift() || title; return null; // Fallback to random selection
return lines.map(line => line.trim().substring(0, 32));
})
.catch(() => null);
// using Fisher-Yates shuffle algorithm if list is available and setRandomMovie is set to true
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
// Generate a random index between 0 and i
const j = Math.floor(Math.random() * (i + 1));
// Swap elements at indices i and j
[array[i], array[j]] = [array[j], array[i]];
//var temp = array[i];
//array[i] = array[j];
//array[j] = temp;
} }
return array; const lines = text.split('\n').filter(Boolean);
const firstLine = lines.shift().trim();
const tokens = firstLine.split(/\s+/).filter(Boolean);
let muteSetting = null;
if (tokens.length > 0) {
const lastToken = tokens[tokens.length - 1].toLowerCase();
if (lastToken === 'muteon' || lastToken === 'muteoff') {
muteSetting = lastToken;
tokens.pop();
}
}
const parsedTitle = tokens.join(' ').trim();
if (parsedTitle) {
title = parsedTitle;
}
if (muteSetting) {
startTrailerMuted = muteSetting === 'muteon';
}
isMuted = startTrailerMuted;
// Remaining lines are media IDs
const mediaList = lines.map(line => line.trim().substring(0, 32));
if (setRandomMovie) {
return shuffleArray(mediaList); // Shuffle the list before returning it if set
}
return mediaList; // return exact list
})
.catch(() => {
console.error('Error reading List.txt. Falling back to random selection.');
return null;
});
}; };
//const shuffleArray = (array) => array.sort(() => Math.random() - 0.5); //better use Fisher-Yates shuffle algorithm
const fetchRandomMovie = () => { const fetchRandomMovie = () => {
if (isChangingSlide) return; if (isChangingSlide) return;
isChangingSlide = true; isChangingSlide = true;
if (movieList.length === 0) { if (movieList.length === 0) {
readCustomList().then(list => { readCustomList().then(list => {
if (list) { if (list && list.length > 0) {
movieList = list; movieList = list;
//// Shuffle the list if it was set by the user
//if (setRandomMovie) {
// shuffleArray(movieList);
//}
currentMovieIndex = 0; currentMovieIndex = 0;
}
fetchNextMovie(); fetchNextMovie();
} else {
console.warn("Fallback to random selection.");
fetchNextMovie(true);
}
}); });
} else fetchNextMovie(); } else {
fetchNextMovie();
}
}; };
const fetchNextMovie = () => { const shuffleArray = (array) => {
const fetchCurrentUserId = () => for (let i = array.length - 1; i > 0; i--) {
fetch('/Sessions', { const j = Math.floor(Math.random() * (i + 1));
headers: { 'Authorization': `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"` } [array[i], array[j]] = [array[j], array[i]];
}) }
.then(response => response.json()) return array;
.then(sessions => { };
const currentSession = sessions.find(session => session.UserId);
return currentSession ? currentSession.UserId : null;
})
.catch(() => null);
fetchCurrentUserId().then(currentUserId => { const fetchNextMovie = (useRandom = false) => {
if (!currentUserId) { if (!userId) {
console.error('Could not retrieve the current user ID.'); console.error('Could not retrieve the current user ID.');
isChangingSlide = false;
return; return;
} }
const headers = { 'Authorization': `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"` }; const headers = { 'Authorization': `MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"` };
if (movieList.length > 0) { if (!useRandom && movieList.length > 0) {
if (currentMovieIndex >= movieList.length) currentMovieIndex = 0; if (currentMovieIndex >= movieList.length) currentMovieIndex = 0;
const movieId = movieList[currentMovieIndex]; const movieId = movieList[currentMovieIndex];
currentMovieIndex++; currentMovieIndex++;
fetch(`/Users/${currentUserId}/Items/${movieId}?Fields=Overview,RemoteTrailers,PremiereDate,RunTimeTicks,ChildCount,Genres`, { headers }) fetch(`/Users/${userId}/Items/${movieId}?Fields=Overview,RemoteTrailers,PremiereDate,RunTimeTicks,ChildCount,Genres`, { headers })
.then(response => response.json()) .then(response => response.json())
.then(checkBackdropAndLogo) .then(checkBackdropAndLogo)
.catch(() => startSlideChangeTimer()) .catch(() => startSlideChangeTimer())
.finally(() => { isChangingSlide = false; }); .finally(() => { isChangingSlide = false; });
} else { } else {
const itemTypes = moviesSeriesBoth === 1 ? 'Movie' : (moviesSeriesBoth === 2 ? 'Series' : 'Movie,Series'); const itemTypes = moviesSeriesBoth === 1 ? 'Movie' : (moviesSeriesBoth === 2 ? 'Series' : 'Movie,Series');
fetch(`/Users/${currentUserId}/Items?IncludeItemTypes=${itemTypes}&Recursive=true&Limit=1&SortBy=random&Fields=Id,Overview,RemoteTrailers,PremiereDate,RunTimeTicks,ChildCount,Genres`, { headers }) fetch(`/Users/${userId}/Items?IncludeItemTypes=${itemTypes}&Recursive=true&Limit=1&SortBy=random&Fields=Id,Overview,RemoteTrailers,PremiereDate,RunTimeTicks,ChildCount,Genres`, { headers })
.then(response => response.json()) .then(response => response.json())
.then(data => { if (data.Items[0]) checkBackdropAndLogo(data.Items[0]); }) .then(data => { if (data.Items[0]) checkBackdropAndLogo(data.Items[0]); })
.catch(() => startSlideChangeTimer()) .catch(() => startSlideChangeTimer())
.finally(() => { isChangingSlide = false; }); .finally(() => { isChangingSlide = false; });
} }
});
}; };
const checkNavigation = () => { const checkNavigation = () => {
const newLocation = window.top.location.href; const newLocation = window.top.location.href;
// Check if the user is navigating to or from the homepage
if (newLocation !== currentLocation) { if (newLocation !== currentLocation) {
currentLocation = newLocation; currentLocation = newLocation;
const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html'); const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html');
if (isHomePage(newLocation)) {
if (!isHomePageActive) { if (!isHomePage(newLocation) && isHomePageActive) {
console.log("Returning to homepage, reactivating slideshow"); console.log("Leaving home page, cleaning up slideshow and stopping video");
isHomePageActive = true;
cleanup();
fetchRandomMovie();
}
} else if (isHomePageActive) {
console.log("Leaving homepage, cleaning up slideshow");
isHomePageActive = false; isHomePageActive = false;
stopBackgroundVideo();
cleanup(); cleanup();
} }
if (isHomePage(newLocation) && !isHomePageActive) {
console.log("Returning to home page, reactivating slideshow");
isHomePageActive = true;
fetchRandomMovie();
}
} }
// Check if parent is available and if the iframe is in an embedded environment // Check if parent is available and if the iframe is in an embedded environment
if (!showOnOtherPages && window.parent && window.parent !== window) { if (!showOnOtherPages && window.parent && window.parent !== window) {
const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html');
if (isHomePage(newLocation)) {
const homeTab = window.parent.document.getElementById('homeTab'); const homeTab = window.parent.document.getElementById('homeTab');
const isHomeTabActive = homeTab && homeTab.classList.contains('is-active'); const isHomeTabActive = homeTab && homeTab.classList.contains('is-active');
@@ -463,19 +915,28 @@ const checkNavigation = () => {
console.log("HomeTab is active, reactivating slideshow"); console.log("HomeTab is active, reactivating slideshow");
isHomePageActive = true; isHomePageActive = true;
window.parent.document.querySelector('.featurediframe').style.display = 'block'; window.parent.document.querySelector('.featurediframe').style.display = 'block';
cleanup();
fetchRandomMovie(); fetchRandomMovie();
} else if (!isHomeTabActive && isHomePageActive) { } else if (!isHomeTabActive && isHomePageActive) {
console.log("Leaving HomeTab, cleaning up slideshow"); console.log("Leaving HomeTab, cleaning up slideshow");
isHomePageActive = false; isHomePageActive = false;
window.parent.document.querySelector('.featurediframe').style.display = 'none'; window.parent.document.querySelector('.featurediframe').style.display = 'none';
stopBackgroundVideo();
cleanup(); cleanup();
} }
}
} else { } else {
console.error("Spotlight iframe is not in an embedded environment, has a different domain or showOnOtherPages is set to true"); console.error("Spotlight iframe is not in an embedded environment, has a different domain or showOnOtherPages is set to true");
} }
}; };
const stopBackgroundVideo = () => {
if (player && typeof player.stopVideo === 'function') {
player.stopVideo();
player.destroy();
}
player = null;
};
setInterval(checkNavigation, 60); setInterval(checkNavigation, 60);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -483,17 +944,46 @@ document.addEventListener('DOMContentLoaded', () => {
const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html'); const isHomePage = url => url.includes('/home') || url.endsWith('/web/') || url.endsWith('/web/index.html');
if (isHomePage(window.top.location.href)) { if (isHomePage(window.top.location.href)) {
isHomePageActive = true; isHomePageActive = true;
console.log("Home page detected, starting slideshow");
readCustomList().then(list => { readCustomList().then(list => {
if (list) { if (list) { movieList = list; currentMovieIndex = 0; }
movieList = list;
// Shuffle the list if it was set by the user
if (setRandomMovie) {
shuffleArray(movieList);
}
currentMovieIndex = 0;
}
fetchRandomMovie(); fetchRandomMovie();
}); });
if (setMutedHover) {
const parentBody = window.parent.document.body;
const hoverContainer = document.getElementById('slides-container');
// prevent error: Unmuting failed and the element was paused instead because the user didn't interact with the document before.
const onUserInteraction = () => {
userInteracted = true;
console.log('User interacted with the page');
parentBody.removeEventListener('click', onUserInteraction);
parentBody.removeEventListener('keydown', onUserInteraction);
hoverContainer.removeEventListener('click', onUserInteraction);
hoverContainer.removeEventListener('keydown', onUserInteraction);
};
parentBody.addEventListener('click', onUserInteraction);
parentBody.addEventListener('keydown', onUserInteraction);
hoverContainer.addEventListener('click', onUserInteraction);
hoverContainer.addEventListener('keydown', onUserInteraction);
hoverContainer.addEventListener('mouseenter', () => {
if (userInteracted && player && typeof player.unMute === 'function') {
player.unMute();
isMuted = false;
}
});
hoverContainer.addEventListener('mouseleave', () => {
if (userInteracted && player && typeof player.mute === 'function') {
player.mute();
isMuted = true;
}
});
}
} }
}); });

View File

@@ -1,12 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css">
<title>Jellyfin Spotlight v2.3.2 Fork v1.0</title> <title>Jellyfin Featured Content Bar v3.4.1</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" /> <link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div id="slides-container"></div> <div id="slides-container"></div>
<div id="video-overlay" style="display: none;"> <div id="video-overlay" style="display: none;">
@@ -15,7 +18,8 @@
<iframe id="trailer-video" width="100%" height="100%" frameborder="0" allowfullscreen></iframe> <iframe id="trailer-video" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>
</div> </div>
</div> </div>
<script src="script.js"></script>
<script src="https://www.youtube.com/iframe_api"></script> <script src="https://www.youtube.com/iframe_api"></script>
<script src="script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,31 @@
@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap');
:root {
--slide-heading-gap: 2.6em;
--slide-heading-accent: #38bdf8;
--slide-heading-accent-strong: #a855f7;
--slide-heading-bg: rgba(15, 23, 42, 0.45);
--slide-heading-border: rgba(148, 163, 184, 0.38);
--slide-heading-text: #f8fafc;
}
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
.slide-wrapper {
position: relative;
width: 100vw;
max-width: 100%;
margin: 0 auto;
}
.slide-wrapper.has-heading {
padding-top: var(--slide-heading-gap);
}
.slide { .slide {
position: relative; position: relative;
width: 100vw; width: 100vw;
@@ -14,6 +34,10 @@ body {
border-radius: 2em; border-radius: 2em;
} }
.slide-wrapper.has-heading .slide {
height: calc(22em - var(--slide-heading-gap));
}
.slide:focus { .slide:focus {
outline: 2px solid #fff; outline: 2px solid #fff;
} }
@@ -30,7 +54,7 @@ body {
transition: width 0.5s ease, filter 0.8s ease, scale 2s ease; transition: width 0.5s ease, filter 0.8s ease, scale 2s ease;
transform-origin: top; transform-origin: top;
animation: objectPositionAnimation 45s ease-in-out infinite; animation: objectPositionAnimation 45s ease-in-out infinite;
border-radius: 1em; border-radius: 2.1em;
} }
.logo { .logo {
@@ -40,100 +64,82 @@ body {
max-height: 9em; max-height: 9em;
max-width: 29em; max-width: 29em;
width: auto; width: auto;
z-index: 3; z-index: 7;
text-shadow: -2px 2px 4px rgba(0, 0, 0, 0.5); text-shadow: -2px 2px 4px rgba(0, 0, 0, 0.5);
filter: drop-shadow(1px 1px 1px); filter: drop-shadow(1px 1px 1px);
pointer-events: none; pointer-events: none;
transition: transform 0.3s ease, max-height 0.3s ease, max-width 0.3s ease, left 0.5s ease; transition: transform 0.3s ease, max-height 0.3s ease, max-width 0.3s ease, left 0.5s ease;
} }
.heading {
position: absolute;
top: calc((var(--slide-heading-gap) - 2em) / 2);
left: 0em;
min-height: 2em;
padding: 0.25em 1.8em;
width: max-content;
font-family: "Titillium Web", sans-serif;
color: var(--slide-heading-text);
font-size: 1.3em;
font-weight: 620;
letter-spacing: 0.04em;
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 0.5em;
z-index: 8;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.82) 0%, rgba(15, 23, 42, 0.45) 100%);
border-radius: 0.75em;
border: 1px solid var(--slide-heading-border);
box-shadow: 0 16px 32px -18px rgba(15, 23, 42, 0.8), 0 0 32px -26px rgba(56, 189, 248, 0.6);
backdrop-filter: blur(18px) saturate(165%);
-webkit-backdrop-filter: blur(18px) saturate(165%);
text-shadow: 0 2px 10px rgba(15, 23, 42, 0.65);
box-sizing: border-box;
isolation: isolate;
overflow: hidden;
}
.heading::before { .heading::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 2.27em; inset: -35% -20% 45% -35%;
left: 0; background: radial-gradient(circle at top left, rgba(56, 189, 248, 0.75), transparent 60%);
width: 100%; opacity: 0.65;
height: 100vh; filter: blur(22px);
background: linear-gradient(0deg, rgb(0% 0% 0%) 0%, rgb(0% 0% 0% / 0.9990234375) 6.25%, rgba(0, 0, 0, 0.99) 12.5%, rgba(0, 0, 0, 0.97) 18.75%, rgba(0, 0, 0, 0.94) 25%, rgba(0, 0, 0, 0.88) 31.25%, rgba(0, 0, 0, 0.79) 37.5%, rgba(0, 0, 0, 0.67) 43.75%, rgba(0, 0, 0, 0.5) 50%, rgba(0, 0, 0, 0.33) 56.25%, rgba(0, 0, 0, 0.21) 62.5%, rgba(0, 0, 0, 0.12) 68.75%, rgba(0, 0, 0, 0.06) 75%, rgba(0, 0, 0, 0.03) 81.25%, rgba(0, 0, 0, 0.01) 87.5%, rgba(0, 0, 0, 0) 93.75%, rgba(0, 0, 0, 0) 100%); z-index: -1;
z-index: 7; pointer-events: none;
opacity: 0;
} }
.heading { .heading::after {
content: '';
position: absolute; position: absolute;
top: 0; left: 1.6em;
left: 0; bottom: -0.45em;
width: 100%; width: 3.8em;
height: 2.3em; height: 0.26em;
background-color: transparent; border-radius: 999px;
font-family: "Titillium Web", sans-serif; background: linear-gradient(90deg, var(--slide-heading-accent) 0%, var(--slide-heading-accent-strong) 100%);
color: #D3D3D3; box-shadow: 0 8px 18px rgba(56, 189, 248, 0.55);
font-size: 22px; opacity: 0.95;
display: none; pointer-events: none;
align-items: center;
justify-content: flex-start;
z-index: 2;
padding: 10px;
padding-left: 0px;
box-sizing: border-box;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 1);
} }
.details-button {
display: none;
}
/* MARK: modified
css for back and skip button
*/
.back-button, .back-button,
.skip-button { .skip-button {
position: absolute; position: absolute;
color: #D3D3D3; color: #fff;
cursor: pointer; cursor: pointer;
z-index: 10; z-index: 10;
font-family: "Titillium Web", sans-serif; font-family: "Titillium Web", sans-serif;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 1); text-shadow: 1px 1px 4px rgba(0, 0, 0, 1);
font-style: normal; font-size: 2em;
font-weight: 400;
letter-spacing: normal;
line-height: 1;
text-transform: none;
white-space: nowrap;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
-webkit-font-feature-settings: "liga";
font-feature-settings: "liga";
border-radius: 50%;
box-sizing: border-box;
opacity: 0.5;
transition: opacity 0.3s ease, background 0.3s ease;
font-size: 1.62em;
width: 1.7em;
height: 1.7em;
display: flex;
align-items: center;
justify-content: center;
}
.back-button:hover,
.skip-button:hover {
background: #ffffff70;
opacity: 1;
}
.skip-button {
right: 0.5em;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
} }
.back-button { .details-button {
left: 0.5em; display: none;
top: 50%;
transform: translateY(-50%);
/* display: none; */ /* Optional, if activated later */
} }
.material-icons { .material-icons {
@@ -203,7 +209,7 @@ css for back and skip button
.age-rating { .age-rating {
position: absolute; position: absolute;
top: 0.1em; top: 0.5em;
left: 2em; left: 2em;
text-align: left; text-align: left;
font-family: "Titillium Web", sans-serif; font-family: "Titillium Web", sans-serif;
@@ -226,9 +232,9 @@ css for back and skip button
.plot { .plot {
position: absolute; position: absolute;
bottom: 0.1em; top: -0.3em;
left: 2.2em; left: 50vw;
right: 0; transform: translateX(-50%);
color: #fff; color: #fff;
font-family: "Titillium Web", sans-serif; font-family: "Titillium Web", sans-serif;
font-size: 0.95em; font-size: 0.95em;
@@ -245,13 +251,14 @@ css for back and skip button
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
border-bottom-left-radius: 0.45em; border-bottom-left-radius: 0.45em;
transition: opacity 0.1s ease, max-height 0.8s ease, width 0.3s ease; transition: opacity 0.1s ease, max-height 0.8s ease, width 0.3s ease, left 0.3s ease;
opacity: 0; opacity: 0;
max-width: 96vw; max-width: 96vw;
opacity: 1; opacity: 1;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
max-height: 7.8em; max-height: 7.8em;
text-align: center;
} }
.genres { .genres {
@@ -349,19 +356,17 @@ css for back and skip button
transform: scale(1); transform: scale(1);
} }
/* .slide:hover .clickable-overlay {} */ /*.slide:hover .clickable-overlay {}*/
.slide:hover .clickable-overlay::before { .slide:hover .clickable-overlay::before {
opacity: 0; opacity: 0;
backdrop-filter: blur(0px); backdrop-filter: blur(0px);
} }
/* /*
.slide:hover .logo {} .slide:hover .logo {}
.slide:hover .plot {} .slide:hover .plot {}
*/ */
.slide:hover .lorem-ipsum::before { .slide:hover .lorem-ipsum::before {
opacity: 1; opacity: 1;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
@@ -376,6 +381,34 @@ css for back and skip button
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {
:root {
--slide-heading-gap: 2.2em;
}
.heading {
left: 0em;
font-size: 1.05em;
padding: 0.3em 1.1em;
border-radius: 0.65em;
gap: 0.4em;
box-shadow: 0 12px 22px -18px rgba(15, 23, 42, 0.72), 0 0 0 1px rgba(148, 163, 184, 0.18);
backdrop-filter: blur(14px) saturate(150%);
-webkit-backdrop-filter: blur(14px) saturate(150%);
}
.heading::before {
inset: -42% -26% 48% -38%;
filter: blur(16px);
opacity: 0.55;
}
.heading::after {
left: 1em;
bottom: -0.32em;
width: 2.6em;
height: 0.22em;
}
.age-rating { .age-rating {
position: absolute; position: absolute;
top: 1em; top: 1em;
@@ -393,7 +426,7 @@ css for back and skip button
.logo { .logo {
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
top: 10.5em; top: 6.5em;
left: 50% !important; left: 50% !important;
max-width: 70%; max-width: 70%;
max-height: 45%; max-height: 45%;
@@ -490,11 +523,7 @@ css for back and skip button
} }
.heading { .heading {
z-index: 0; z-index: 9;
}
.heading::before {
display: none;
} }
.video-container { .video-container {
@@ -567,12 +596,15 @@ css for back and skip button
@media (max-width:1000px) and (orientation:portrait) { @media (max-width:1000px) and (orientation:portrait) {
.back-button { .back-button {
left: 0em; top: 5em !important;
left: 0.5em;
transform: none;
} }
.skip-button { .skip-button {
right: 0em; top: 5em !important;
/*top: 50% !important;*/ right: 0.5em;
transform: none;
} }
.genres { .genres {
@@ -593,6 +625,10 @@ css for back and skip button
} }
@media (max-width:1000px) and (orientation:landscape) { @media (max-width:1000px) and (orientation:landscape) {
.slide-wrapper.has-heading .slide {
height: calc(22em - var(--slide-heading-gap));
}
.community-rating { .community-rating {
left: 89vw !important; left: 89vw !important;
} }
@@ -602,12 +638,15 @@ css for back and skip button
} }
.back-button { .back-button {
left: 0em; top: 5em !important;
left: 0.5em;
transform: none;
} }
.skip-button { .skip-button {
right: 0em; top: 5em !important;
/*top: 50% !important;*/ right: 0.5em;
transform: none;
} }
.genres { .genres {
@@ -628,43 +667,85 @@ css for back and skip button
@media (min-width: 1001px) { @media (min-width: 1001px) {
.logo { .logo {
max-height: 45%; max-height: 45%;
top: 50vh; top: 34vh;
max-width: 47em; max-width: 47em;
left: 50%; left: 50%;
transition: transform 0.1s ease, max-height 0.3s ease; transition: transform 0.1s ease, left 0.3s ease, max-height 0.3s ease;
} }
}
.watch-trailer-button { .lorem-ipsum {
left: 50vw;
transition: left 0.3s ease;
transform: translateX(-50%);
top: unset;
bottom: 3.2em;
padding-left: unset;
}
.watch-trailer-button {
display: none; display: none;
} }
.backdrop { .backdrop {
left: 0em; left: 0em;
width: 100%; width: 100%;
transition: width 0.5s ease, left 0.5s ease, filter 0.8s ease, transform 2s ease; transition: width 0.5s ease, left 0.5s ease, filter 0.8s ease, transform 2s ease;
} }
.genres { .genres {
left: 8em; left: 50vw;
top: unset;
bottom: 5em;
transition: left 0.3s;
z-index: 9; z-index: 9;
max-width: 39em; max-width: 39em;
} transform: translateX(-50%);
}
.community-rating { .community-rating {
top: 16.45em; top: 16.45em;
left: 31em; left: 31em;
font-size: 1em; font-size: 1em;
} }
.critic-rating { .critic-rating {
position: absolute; position: absolute;
top: 16.45em; top: 16.45em;
left: 26.5em; left: 26.5em;
font-size: 1em; font-size: 1em;
} }
.text-container::before { .back-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 1.2em;
height: 1.2em;
border-radius: 50%;
overflow: hidden;
transition: background 0.3s, color 0.3s;
color: #ffffffa1;
left: 0.5em;
top: 50%;
transform: translateY(-50%);
}
.skip-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 1.2em;
height: 1.2em;
border-radius: 50%;
overflow: hidden;
transition: background 0.3s, color 0.3s;
color: #ffffffa1;
right: 0.5em;
top: 50%;
transform: translateY(-50%);
}
.text-container::before {
content: ''; content: '';
z-index: 5; z-index: 5;
display: flex; display: flex;
@@ -672,30 +753,40 @@ css for back and skip button
width: 100vw; width: 100vw;
top: -19.3em; top: -19.3em;
height: 3em; height: 3em;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.7) 6.25%, rgba(0, 0, 0, 0.68) 12.5%, rgba(0, 0, 0, 0.66) 18.75%, rgba(0, 0, 0, 0.64) 25%, rgba(0, 0, 0, 0.6) 31.25%, rgba(0, 0, 0, 0.56) 37.5%, rgba(0, 0, 0, 0.51) 43.75%, rgba(0, 0, 0, 0.45) 50%, rgba(0, 0, 0, 0.38) 56.25%, rgba(0, 0, 0, 0.31) 62.5%, rgba(0, 0, 0, 0.22) 68.75%, rgba(0, 0, 0, 0.14) 75%, rgba(0, 0, 0, 0.13) 81.25%, rgba(0, 0, 0, 0) 100%); background: transparent;
opacity: 0.8; backdrop-filter: blur(0px);
backdrop-filter: blur(1px);
border-top-right-radius: 1em; border-top-right-radius: 1em;
border-top-left-radius: 1em; border-top-left-radius: 1em;
-webkit-mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 3%, rgba(0, 0, 0, 0.5) 7%, rgba(0, 0, 0, 0.75) 15%, rgba(0, 0, 0, 0.9) 25%, rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 100%); -webkit-mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 3%, rgba(0, 0, 0, 0.5) 7%, rgba(0, 0, 0, 0.75) 15%, rgba(0, 0, 0, 0.9) 25%, rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 100%);
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 3%, rgba(0, 0, 0, 0.5) 7%, rgba(0, 0, 0, 0.75) 15%, rgba(0, 0, 0, 0.9) 25%, rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 100%); mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 3%, rgba(0, 0, 0, 0.5) 7%, rgba(0, 0, 0, 0.75) 15%, rgba(0, 0, 0, 0.9) 25%, rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 100%);
} }
.text-container::after { .text-container::after {
content: ''; content: '';
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.7) 6.25%, rgba(0, 0, 0, 0.68) 12.5%, rgba(0, 0, 0, 0.66) 18.75%, rgba(0, 0, 0, 0.64) 25%, rgba(0, 0, 0, 0.6) 31.25%, rgba(0, 0, 0, 0.56) 37.5%, rgba(0, 0, 0, 0.51) 43.75%, rgba(0, 0, 0, 0.45) 50%, rgba(0, 0, 0, 0.38) 56.25%, rgba(0, 0, 0, 0.31) 62.5%, rgba(0, 0, 0, 0.22) 68.75%, rgba(0, 0, 0, 0.14) 75%, rgba(0, 0, 0, 0.13) 81.25%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(0deg, rgb(0% 0% 0% / 0.98) 0%, rgb(0% 0% 0% / 0.9705847873975829) 6.25%, rgb(0% 0% 0% / 0.9427009709305305) 12.5%, rgb(0% 0% 0% / 0.8974201100282472) 18.75%, rgb(0% 0% 0% / 0.8364823227814083) 25%, rgb(0% 0% 0% / 0.7622294141796051) 31.25%, rgb(0% 0% 0% / 0.6775148818588941) 37.5%, rgb(0% 0% 0% / 0.5855942577879029) 43.75%, rgb(0% 0% 0% / 0.49000000000000005) 50%, rgb(0% 0% 0% / 0.39440574221209723) 56.25%, rgb(0% 0% 0% / 0.3024851181411061) 62.5%, rgb(0% 0% 0% / 0.217770585820395) 68.75%, rgb(0% 0% 0% / 0.14351767721859177) 75%, rgb(0% 0% 0% / 0.0825798899717528) 81.25%, rgb(0% 0% 0% / 0.03729902906946947) 87.5%, rgb(0% 0% 0% / 0.009415212602417067) 93.75%, rgb(0% 0% 0% / 0) 100%);
z-index: 5; z-index: 5;
display: flex; display: flex;
position: absolute; position: absolute;
width: 100vw; width: 100vw;
border-top-right-radius: 1em; border-bottom-left-radius: 1em;
border-top-left-radius: 1em;
bottom: 0em; bottom: 0em;
height: 3em; height: 22.5em;
opacity: 0.8; opacity: 1;
backdrop-filter: blur(1px); backdrop-filter: blur(0px);
-webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 3%, rgba(0, 0, 0, 0.5) 7%, rgba(0, 0, 0, 0.75) 15%, rgba(0, 0, 0, 0.9) 25%, rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 100%); -webkit-mask-image: linear-gradient(0deg, rgb(0% 0% 0% / 0.98) 0%, rgb(0% 0% 0% / 0.9705847873975829) 6.25%, rgb(0% 0% 0% / 0.9427009709305305) 12.5%, rgb(0% 0% 0% / 0.8974201100282472) 18.75%, rgb(0% 0% 0% / 0.8364823227814083) 25%, rgb(0% 0% 0% / 0.7622294141796051) 31.25%, rgb(0% 0% 0% / 0.6775148818588941) 37.5%, rgb(0% 0% 0% / 0.5855942577879029) 43.75%, rgb(0% 0% 0% / 0.49000000000000005) 50%, rgb(0% 0% 0% / 0.39440574221209723) 56.25%, rgb(0% 0% 0% / 0.3024851181411061) 62.5%, rgb(0% 0% 0% / 0.217770585820395) 68.75%, rgb(0% 0% 0% / 0.14351767721859177) 75%, rgb(0% 0% 0% / 0.0825798899717528) 81.25%, rgb(0% 0% 0% / 0.03729902906946947) 87.5%, rgb(0% 0% 0% / 0.009415212602417067) 93.75%, rgb(0% 0% 0% / 0) 100%);
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 3%, rgba(0, 0, 0, 0.5) 7%, rgba(0, 0, 0, 0.75) 15%, rgba(0, 0, 0, 0.9) 25%, rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 100%); mask-image: linear-gradient(0deg, rgb(0% 0% 0% / 0.98) 0%, rgb(0% 0% 0% / 0.9705847873975829) 6.25%, rgb(0% 0% 0% / 0.9427009709305305) 12.5%, rgb(0% 0% 0% / 0.8974201100282472) 18.75%, rgb(0% 0% 0% / 0.8364823227814083) 25%, rgb(0% 0% 0% / 0.7622294141796051) 31.25%, rgb(0% 0% 0% / 0.6775148818588941) 37.5%, rgb(0% 0% 0% / 0.5855942577879029) 43.75%, rgb(0% 0% 0% / 0.49000000000000005) 50%, rgb(0% 0% 0% / 0.39440574221209723) 56.25%, rgb(0% 0% 0% / 0.3024851181411061) 62.5%, rgb(0% 0% 0% / 0.217770585820395) 68.75%, rgb(0% 0% 0% / 0.14351767721859177) 75%, rgb(0% 0% 0% / 0.0825798899717528) 81.25%, rgb(0% 0% 0% / 0.03729902906946947) 87.5%, rgb(0% 0% 0% / 0.009415212602417067) 93.75%, rgb(0% 0% 0% / 0) 100%);
pointer-events: none;
}
.skip-button:hover {
background: #101010a8;
color: #fff;
}
.back-button:hover {
background: #101010a8;
color: #fff;
}
} }
.age-rating { .age-rating {
@@ -876,18 +967,11 @@ css for back and skip button
font-size: 133%; font-size: 133%;
} }
/* .backdrop {} /*
.backdrop {}
.age-rating {} .age-rating {}
.back-button {
top: 38vh;
}
.skip-button {
top: 38vh;
}
.video-container {} .video-container {}
.text-container::before {} .text-container::before {}
@@ -896,14 +980,11 @@ css for back and skip button
.lorem-ipsum {} .lorem-ipsum {}
*/ */
.logo {
top: 38vh;
}
} }
@media (min-width: 3000px) { @media (min-width: 3000px) {
.slide { .slide {
font-size: 200%; font-size: 140%;
} }
/* /*
@@ -923,10 +1004,11 @@ css for back and skip button
#video-overlay { #video-overlay {
position: fixed; position: fixed;
top: 13vh; top: 10vh;
border-radius: 1em;
left: 0vw; left: 0vw;
width: 100%; width: 94.5vw;
height: 140vh; height: 55vh;
background-color: rgb(0, 0, 0); background-color: rgb(0, 0, 0);
z-index: 1000; z-index: 1000;
display: flex; display: flex;
@@ -938,12 +1020,30 @@ css for back and skip button
#video-overlay-content { #video-overlay-content {
position: relative; position: relative;
width: 90%; width: 100%;
max-width: 100%; max-width: 100%;
height: 60%; height: 100%;
background-color: #000; background-color: #000;
} }
@media (orientation: portrait) {
#video-overlay {
position: fixed;
top: 10vh;
border-radius: 1em;
left: 0vw;
width: 87.5vw;
height: 68vh;
background-color: rgb(0, 0, 0);
z-index: 1000;
display: flex;
background-size: cover;
padding: 6vh;
padding-top: 10vh;
margin-top: -10vh;
}
}
#close-overlay { #close-overlay {
position: absolute; position: absolute;
top: -1em; top: -1em;
@@ -999,3 +1099,63 @@ css for back and skip button
font-size: 0.85em; font-size: 0.85em;
opacity: 0.75; opacity: 0.75;
} }
.logo {
opacity: 0;
animation: fadeIn 1.5s ease-in forwards;
animation-delay: 1s;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
.plot {
opacity: 0;
animation: fadeIn 1s ease-in forwards;
animation-delay: 2s;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
.genres {
opacity: 0;
animation: fadeIn 1s ease-in forwards;
animation-delay: 1.5s;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
.lorem-ipsum {
opacity: 0;
animation: fadeIn 1s ease-in forwards;
animation-delay: 1.5s;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
.text-container::after {
opacity: 0.7;
animation: fadeIn 1.5s ease-in forwards;
animation-delay: 1s;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}