(scrollX = carousel?.scrollLeft || scrollX)}
diff --git a/src/lib/components/EpisodeCard/EpisodeCard.svelte b/src/lib/components/EpisodeCard/EpisodeCard.svelte
index 874c874..a1f8553 100644
--- a/src/lib/components/EpisodeCard/EpisodeCard.svelte
+++ b/src/lib/components/EpisodeCard/EpisodeCard.svelte
@@ -39,7 +39,10 @@
setJellyfinItemUnwatched(jellyfinId).finally(() => jellyfinItemsStore.refreshIn(5000));
}
- function handlePlay() {
+ function handlePlay(e: MouseEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+
if (!jellyfinId) return;
playerState.streamJellyfinId(jellyfinId);
@@ -67,7 +70,7 @@
'aspect-video bg-center bg-cover rounded-lg overflow-hidden transition-opacity shadow-lg selectable flex-shrink-0 placeholder-image relative',
'flex flex-col px-2 lg:px-3 py-2 gap-2 text-left',
{
- 'h-40': size === 'md',
+ 'h-44': size === 'md',
'h-full': size === 'dynamic',
group: !!jellyfinId,
'cursor-default': !jellyfinId
@@ -79,10 +82,10 @@
>
diff --git a/src/lib/components/Navbar/Navbar.svelte b/src/lib/components/Navbar/Navbar.svelte
index b1a9e1a..7879de7 100644
--- a/src/lib/components/Navbar/Navbar.svelte
+++ b/src/lib/components/Navbar/Navbar.svelte
@@ -62,9 +62,9 @@
{$_('navbar.home')}
-
+
{$_('navbar.library')}
diff --git a/src/lib/components/PageDots.svelte b/src/lib/components/PageDots.svelte
new file mode 100644
index 0000000..cec18e9
--- /dev/null
+++ b/src/lib/components/PageDots.svelte
@@ -0,0 +1,41 @@
+
+
+
+ {#each Array.from({ length }) as _, i}
+
+
+
+
onJump(i)}>
+
+
+ {/each}
+
diff --git a/src/lib/components/Poster/Poster.svelte b/src/lib/components/Poster/Poster.svelte
index 22466ff..e3f9fb6 100644
--- a/src/lib/components/Poster/Poster.svelte
+++ b/src/lib/components/Poster/Poster.svelte
@@ -20,6 +20,7 @@
export let rating: number | undefined = undefined;
export let progress = 0;
+ export let shadow = false;
export let size: 'dynamic' | 'md' | 'lg' | 'sm' = 'md';
export let orientation: 'portrait' | 'landscape' = 'landscape';
@@ -37,7 +38,7 @@
}
}}
class={classNames(
- 'relative flex shadow-lg rounded-xl selectable group hover:text-inherit flex-shrink-0 overflow-hidden text-left',
+ 'relative flex rounded-xl selectable group hover:text-inherit flex-shrink-0 overflow-hidden text-left',
{
'aspect-video': orientation === 'landscape',
'aspect-[2/3]': orientation === 'portrait',
@@ -47,7 +48,8 @@
'h-44': size === 'md' && orientation === 'landscape',
'w-60': size === 'lg' && orientation === 'portrait',
'h-60': size === 'lg' && orientation === 'landscape',
- 'w-full': size === 'dynamic'
+ 'w-full': size === 'dynamic',
+ 'shadow-lg': shadow
}
)}
>
diff --git a/src/lib/components/TitleShowcase/TitleShowcase.svelte b/src/lib/components/TitleShowcase/TitleShowcase.svelte
deleted file mode 100644
index 7c99628..0000000
--- a/src/lib/components/TitleShowcase/TitleShowcase.svelte
+++ /dev/null
@@ -1,254 +0,0 @@
-
-
-
-
- {#if UIVisible}
-
-
-
-
{releaseDate.getFullYear()}
-
-
{formatMinutesToTime(runtime)}
-
-
{tmdbRating.toFixed(1)} TMDB
-
-
= 15
- })}
- in:fly|global={{ y: -10, delay: ANIMATION_DURATION, duration: ANIMATION_DURATION }}
- out:fly|global={{ y: 10, duration: ANIMATION_DURATION }}
- >
- {title}
-
-
-
- {#each genres.slice(0, 3) as genre}
-
- {genre}
-
- {/each}
-
-
- {/if}
-
-
-
-
- {#if trailerId}
-
- {/if}
-
-
-
-
-
-
- {$_('titleShowcase.releaseDate')}
-
-
-
- {releaseDate.toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- })}
-
-
- {#if director}
-
-
- {$_('titleShowcase.directedBy')}
-
-
{director}
-
- {/if}
-
-
-
- {#if UIVisible}
-
- {#each Array.from({ length: showcaseLength }, (_, i) => i) as i}
- {#if i === showcaseIndex}
-
- {:else}
-
- {/if}
- {/each}
-
- {/if}
-
- {#if !trailerVisible}
-
- {/if}
- {#if trailerId && $settings.autoplayTrailers && trailerMounted}
-
-
-
- {/if}
- {#if UIVisible}
-
- {:else if !UIVisible}
-
- {/if}
- {#if UIVisible}
-
-
- {/if}
-
diff --git a/src/lib/components/TitleShowcase/TitleShowcaseBackground.svelte b/src/lib/components/TitleShowcase/TitleShowcaseBackground.svelte
new file mode 100644
index 0000000..344b349
--- /dev/null
+++ b/src/lib/components/TitleShowcase/TitleShowcaseBackground.svelte
@@ -0,0 +1,94 @@
+
+
+
+
+{#if !trailerVisible}
+ {#key tmdbId}
+
+ {/key}
+{/if}
+{#if trailerId && $settings.autoplayTrailers && trailerMounted}
+
+
+
+{/if}
+{#if UIVisible}
+
+{:else if !UIVisible}
+
+{/if}
diff --git a/src/lib/components/TitleShowcase/TitleShowcaseVisuals.svelte b/src/lib/components/TitleShowcase/TitleShowcaseVisuals.svelte
new file mode 100644
index 0000000..71a2b89
--- /dev/null
+++ b/src/lib/components/TitleShowcase/TitleShowcaseVisuals.svelte
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
{releaseDate.getFullYear()}
+
+
{formatMinutesToTime(runtime)}
+
+
{tmdbRating.toFixed(1)} TMDB
+
+
+
+
+ {#each genres.slice(0, 3) as genre}
+
+ {genre}
+
+ {/each}
+
+
+
diff --git a/src/lib/components/TitleShowcase/TitleShowcasesContainer.svelte b/src/lib/components/TitleShowcase/TitleShowcasesContainer.svelte
new file mode 100644
index 0000000..c5ba43b
--- /dev/null
+++ b/src/lib/components/TitleShowcase/TitleShowcasesContainer.svelte
@@ -0,0 +1,165 @@
+
+
+
+
+ {#await tmdbPopularMoviesPromise then movies}
+ {@const movie = movies[showcaseIndex]}
+
+ {#key movie?.id}
+
g.name || '') || []}
+ runtime={movie?.runtime || 0}
+ releaseDate={new Date(movie?.release_date || Date.now())}
+ tmdbRating={movie?.vote_average || 0}
+ posterUri={movie?.poster_path || ''}
+ {hideUI}
+ />
+ {/key}
+
+
+ {#if !hideUI}
+
+
+
+
+
+ {/if}
+
+ v.site === 'YouTube' && v.type === 'Trailer')
+ ?.key}
+ backdropUri={movie?.backdrop_path || ''}
+ />
+ {/await}
+
+
+ {#if !continueWatchingEmpty}
+
+ Continue Watching
+ {#await nextUpProps}
+
+ {:then props}
+ {#each props as prop}
+ (window.location.href = `/${prop.type}/${prop.tmdbId}`)}
+ {...prop}
+ />
+ {/each}
+ {/await}
+
+ {/if}
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 8b4bbd5..2153611 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2,15 +2,34 @@
import {
getJellyfinBackdrop,
getJellyfinContinueWatching,
- getJellyfinNextUp
+ getJellyfinNextUp,
+ type JellyfinItem
} from '$lib/apis/jellyfin/jellyfinApi';
- import { getTmdbMovie, getTmdbPopularMovies } from '$lib/apis/tmdb/tmdbApi';
+ import {
+ TmdbApiOpen,
+ getPosterProps,
+ getTmdbMovie,
+ getTmdbPopularMovies
+ } from '$lib/apis/tmdb/tmdbApi';
import Carousel from '$lib/components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
import EpisodeCard from '$lib/components/EpisodeCard/EpisodeCard.svelte';
- import TitleShowcase from '$lib/components/TitleShowcase/TitleShowcase.svelte';
+ import GenreCard from '$lib/components/GenreCard.svelte';
+ import NetworkCard from '$lib/components/NetworkCard.svelte';
+ import PeopleCard from '$lib/components/PeopleCard/PeopleCard.svelte';
+ import Poster from '$lib/components/Poster/Poster.svelte';
+ import TitleShowcase from '$lib/components/TitleShowcase/TitleShowcaseBackground.svelte';
+ import { genres, networks } from '$lib/discover';
import { jellyfinItemsStore } from '$lib/stores/data.store';
- import { log } from '$lib/utils';
+ import { settings } from '$lib/stores/settings.store';
+ import type { TitleType } from '$lib/types';
+ import { formatDateToYearMonthDay } from '$lib/utils';
+ import type { ComponentProps } from 'svelte';
+ import { fade } from 'svelte/transition';
+ import { _ } from 'svelte-i18n';
+ import LazyImg from '$lib/components/LazyImg.svelte';
+ import { TMDB_IMAGES_ORIGINAL } from '$lib/constants';
+ import TitleShowcases from '$lib/components/TitleShowcase/TitleShowcasesContainer.svelte';
let continueWatchingVisible = true;
@@ -74,9 +93,139 @@
(showcaseIndex - 1 + (await tmdbPopularMoviesPromise).length) %
(await tmdbPopularMoviesPromise).length;
}
+
+ const jellyfinItemsPromise = new Promise
((resolve) => {
+ jellyfinItemsStore.subscribe((data) => {
+ if (data.loading) return;
+ resolve(data.data || []);
+ });
+ });
+
+ const fetchCardProps = async (
+ items: {
+ name?: string;
+ title?: string;
+ id?: number;
+ vote_average?: number;
+ number_of_seasons?: number;
+ first_air_date?: string;
+ poster_path?: string;
+ }[],
+ type: TitleType | undefined = undefined
+ ): Promise[]> => {
+ const filtered = $settings.discover.excludeLibraryItems
+ ? items.filter(
+ async (item) =>
+ !(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
+ )
+ : items;
+
+ return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
+ props.filter((p) => p.backdropUrl)
+ );
+ };
+
+ const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
+ params: {
+ path: {
+ time_window: 'day'
+ },
+ query: {
+ language: $settings.language
+ }
+ }
+ }).then((res) => res.data?.results || []);
+
+ const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
+
+ const fetchTrendingActorProps = () =>
+ TmdbApiOpen.get('/3/trending/person/{time_window}', {
+ params: {
+ path: {
+ time_window: 'week'
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then((actors) =>
+ actors
+ .filter((a) => a.profile_path)
+ .map((actor) => ({
+ tmdbId: actor.id || 0,
+ backdropUri: actor.profile_path || '',
+ name: actor.name || '',
+ subtitle: actor.known_for_department || ''
+ }))
+ );
+
+ const fetchUpcomingMovies = () =>
+ TmdbApiOpen.get('/3/discover/movie', {
+ params: {
+ query: {
+ 'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
+ sort_by: 'popularity.desc',
+ language: $settings.language,
+ region: $settings.discover.region,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then(fetchCardProps);
+
+ const fetchUpcomingSeries = () =>
+ TmdbApiOpen.get('/3/discover/tv', {
+ params: {
+ query: {
+ 'first_air_date.gte': formatDateToYearMonthDay(new Date()),
+ sort_by: 'popularity.desc',
+ language: $settings.language,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then((i) => fetchCardProps(i, 'series'));
+
+ const fetchDigitalReleases = () =>
+ TmdbApiOpen.get('/3/discover/movie', {
+ params: {
+ query: {
+ with_release_type: 4,
+ sort_by: 'popularity.desc',
+ 'release_date.lte': formatDateToYearMonthDay(new Date()),
+ language: $settings.language,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ // region: $settings.discover.region
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then(fetchCardProps);
+
+ const fetchNowStreaming = () =>
+ TmdbApiOpen.get('/3/discover/tv', {
+ params: {
+ query: {
+ 'air_date.gte': formatDateToYearMonthDay(new Date()),
+ 'first_air_date.lte': formatDateToYearMonthDay(new Date()),
+ sort_by: 'popularity.desc',
+ language: $settings.language,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then((i) => fetchCardProps(i, 'series'));
+
+ function parseIncludedLanguages(includedLanguages: string) {
+ return includedLanguages.replace(' ', '').split(',').join('|');
+ }
-
+
+
+
-
-
- Continue Watching
- {#await nextUpProps}
+
+
+
+
+
+
+
+
+
+
+ {$_('discover.popularPeople')}
+
+ {#await fetchTrendingActorProps()}
{:then props}
- {#each props as prop}
- (window.location.href = `/${prop.type}/${prop.tmdbId}`)}
- {...prop}
- />
+ {#each props as prop (prop.tmdbId)}
+
{/each}
{/await}
+
+
+ {$_('discover.upcomingMovies')}
+
+ {#await fetchUpcomingMovies()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.upcomingSeries')}
+
+ {#await fetchUpcomingSeries()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.genres')}
+
+ {#each Object.values(genres) as genre (genre.tmdbGenreId)}
+
+ {/each}
+
+
+
+ {$_('discover.newDigitalReleases')}
+
+ {#await fetchDigitalReleases()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.streamingNow')}
+
+ {#await fetchNowStreaming()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.TVNetworks')}
+
+ {#each Object.values(networks) as network (network.tmdbNetworkId)}
+
+ {/each}
+
diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte
index d74f769..ad05d6c 100644
--- a/src/routes/discover/+page.svelte
+++ b/src/routes/discover/+page.svelte
@@ -1,14 +1,12 @@
+
+
[] = [];
@@ -93,7 +93,12 @@
/>
{/await}
-