feat: Front page now streaming & upcoming, styling improvements

This commit is contained in:
Aleksi Lassila
2024-05-11 23:42:41 +03:00
parent 21db1c82a2
commit 1f372ea576
6 changed files with 239 additions and 143 deletions

View File

@@ -46,7 +46,7 @@ export interface TmdbSeriesFull2 extends TmdbSeries2 {
}
export class TmdbApi implements Api<paths> {
getClient() {
static getClient() {
return createClient<paths>({
baseUrl: 'https://api.themoviedb.org',
headers: {
@@ -55,6 +55,10 @@ export class TmdbApi implements Api<paths> {
});
}
getClient() {
return TmdbApi.getClient();
}
// MOVIES
getTmdbMovie = async (tmdbId: number) => {

View File

@@ -14,7 +14,7 @@
<div class={classNames('flex flex-col group/carousel', $$restProps.class)}>
<div class={'flex justify-between items-center mb-2 ' + scrollClass}>
<div class="font-medium tracking-wide text-2xl text-zinc-200">
<div class="header2">
<slot name="header" />
</div>
<div

View File

@@ -53,8 +53,12 @@
style={`left: ${x}px; top: ${y}px; width: ${width}px; height: ${height}px;`}
/>
<div class="fixed inset-0 border-x-[96px] border-y-[48px] border-green-500/10 z-50" />
<div class="fixed inset-0 px-32 grid grid-cols-12 gap-x-16 *:bg-purple-500/10 items-stretch z-50">
<div
class="fixed inset-0 border-x-[96px] border-y-[48px] border-green-500/10 z-50 pointer-events-none"
/>
<div
class="fixed inset-0 px-32 grid grid-cols-12 gap-x-16 *:bg-purple-500/10 items-stretch z-50 pointer-events-none"
>
<div />
<div />
<div />

View File

@@ -1,27 +1,43 @@
<script lang="ts">
import { Bookmark, CardStack, Gear, Laptop, MagnifyingGlass } from 'radix-icons-svelte';
import {
Bookmark,
CardStack,
DotFilled,
Gear,
Laptop,
MagnifyingGlass
} from 'radix-icons-svelte';
import classNames from 'classnames';
import { type Readable, writable, type Writable } from 'svelte/store';
import Container from '../../../Container.svelte';
import { useNavigate } from 'svelte-navigator';
import { registrars, type Selectable } from '../../selectable';
import { useLocation, useNavigate } from 'svelte-navigator';
import { registrars, Selectable } from '../../selectable';
const location = useLocation();
const navigate = useNavigate();
let selectedIndex = 0;
$: activeIndex = {
'': 0,
series: 0,
movies: 1,
library: 2,
search: 3,
manage: 4
}[$location.pathname.split('/')[1] || '/'];
$: console.log('activeIndex', activeIndex);
$: console.log($location.pathname.split('/')[1] || '/');
let isNavBarOpen: Readable<boolean>;
let focusIndex: Writable<number> = writable(0);
let selectable: Selectable;
focusIndex.subscribe((v) => (selectedIndex = v));
const itemContainer = (index: number, _focusIndex: number) =>
classNames('h-12 flex items-center cursor-pointer', {
'text-primary-500': _focusIndex === index,
'text-stone-300': _focusIndex !== index
});
const selectIndex = (index: number) => () => {
if (index === activeIndex) {
Selectable.giveFocus('right');
return;
}
selectable.focusChild(index);
const path =
{
@@ -39,7 +55,7 @@
<Container
class={classNames(
'flex flex-col items-stretch fixed z-20 left-0 inset-y-0 group',
'py-4 w-16 select-none',
'py-8 w-24 select-none',
{
//'max-w-[64px]': !$isNavBarOpen,
//'max-w-64': $isNavBarOpen
@@ -77,10 +93,16 @@
!hasFocus && !(!$isNavBarOpen && selectedIndex === 0)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 0 })}
size={19}
/>
</div>
<Laptop class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
@@ -99,10 +121,16 @@
!hasFocus && !(!$isNavBarOpen && selectedIndex === 1)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 1 })}
size={19}
/>
</div>
<CardStack class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
@@ -121,10 +149,16 @@
!hasFocus && !(!$isNavBarOpen && selectedIndex === 2)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 2 })}
size={19}
/>
</div>
<Bookmark class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
@@ -143,10 +177,16 @@
!hasFocus && !(!$isNavBarOpen && selectedIndex === 3)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 3 })}
size={19}
/>
</div>
<MagnifyingGlass class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
@@ -161,16 +201,27 @@
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(4)} let:hasFocus>
<div
class={classNames('w-full h-full relative flex items-center justify-center', {
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 4),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 4)
})}
class={classNames(
'w-full h-full relative flex items-center justify-center transition-opacity',
{
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 4),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 4),
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
}
)}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 4 })}
size={19}
/>
</div>
<Gear class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Container from '../../Container.svelte';
import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte';
import { tmdbApi } from '../apis/tmdb/tmdb-api';
import { TmdbApi, tmdbApi } from '../apis/tmdb/tmdb-api';
import { getShowcasePropsFromTmdbMovie } from '../components/HeroShowcase/HeroShowcase';
import Carousel from '../components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
@@ -11,53 +11,98 @@
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
import { Route } from 'svelte-navigator';
import MoviePage from './MoviePage.svelte';
import { formatDateToYearMonthDay } from '../utils';
import TmdbCard from '../components/Card/TmdbCard.svelte';
const { data: continueWatching, isLoading: isLoadingContinueWatching } = useRequest(
jellyfinApi.getContinueWatching,
'movie'
);
const { data: recentlyAdded, isLoading: isLoadingRecentlyAdded } = useRequest(
jellyfinApi.getRecentlyAdded,
'movie'
);
const continueWatching = jellyfinApi.getContinueWatching('movie');
const recentlyAdded = jellyfinApi.getRecentlyAdded('movie');
const popularMovies = tmdbApi.getPopularMovies();
const newDigitalReleases = getDigitalReleases();
const upcomingMovies = getUpcomingMovies();
function getUpcomingMovies() {
return TmdbApi.getClient()
.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 || []);
}
function getDigitalReleases() {
return TmdbApi.getClient()
.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 || []);
}
</script>
<Container focusOnMount class="flex flex-col">
<div class="h-[calc(100vh-12rem)] flex px-20">
<div class="h-[calc(100vh-12rem)] flex px-32">
<HeroShowcase
items={popularMovies.then(getShowcasePropsFromTmdbMovie)}
on:enter={scrollIntoView({ top: 0 })}
/>
</div>
<div class="mt-16">
<Carousel scrollClass="px-20" on:enter={scrollIntoView({ vertical: 64 })}>
<div slot="header">
{$isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)
? 'Loading...'
: $continueWatching?.length
? 'Continue Watching'
: 'Recently Added'}
</div>
{#if $isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)}
<CarouselPlaceholderItems />
{:else if $continueWatching?.length}
<div class="flex -mx-4">
{#each $continueWatching as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
<div class="my-16 space-y-8">
{#await continueWatching then continueWatching}
{#if continueWatching?.length}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Continue Watching</span>
{#each continueWatching as item (item.Id)}
<JellyfinCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</div>
{:else if $recentlyAdded?.length}
{#each $recentlyAdded as item (item.Id)}
<Container on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
{/each}
</Carousel>
{:else}
{#await recentlyAdded then recentlyAdded}
{#if recentlyAdded?.length}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Recently Added</span>
{#each recentlyAdded as item (item.Id)}
<JellyfinCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</Carousel>
{/if}
{/await}
{/if}
</Carousel>
{/await}
{#await newDigitalReleases then nowStreaming}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">New Digital Releases</span>
{#each nowStreaming as item}
<TmdbCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</Carousel>
{/await}
{#await upcomingMovies then upcomingSeries}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Upcoming Movies</span>
{#each upcomingSeries as item}
<TmdbCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</Carousel>
{/await}
</div>
</Container>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Container from '../../Container.svelte';
import { tmdbApi } from '../apis/tmdb/tmdb-api';
import { TmdbApi, tmdbApi } from '../apis/tmdb/tmdb-api';
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
import { useRequest } from '../stores/data.store';
@@ -12,102 +12,94 @@
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
import { Route } from 'svelte-navigator';
import SeriesPage from '../components/SeriesPage/SeriesPage.svelte';
import { formatDateToYearMonthDay } from '../utils';
import TmdbCard from '../components/Card/TmdbCard.svelte';
const { data: continueWatching, isLoading: isLoadingContinueWatching } = useRequest(
jellyfinApi.getContinueWatchingSeries
);
const { data: recentlyAdded, isLoading: isLoadingRecentlyAdded } = useRequest(
jellyfinApi.getRecentlyAdded,
'series'
);
const continueWatching = jellyfinApi.getContinueWatchingSeries();
const recentlyAdded = jellyfinApi.getRecentlyAdded('series');
// const jellyfinItemsPromise = new Promise<JellyfinItem[]>((resolve) => {
// jellyfinItemsStore.subscribe((data) => {
// if (data.loading) return;
// resolve(data.data || []);
// });
// });
const nowStreaming = getNowStreaming();
const upcomingSeries = fetchUpcomingSeries();
// 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<ComponentProps<Card>[]> => {
// 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).map((i) => ({ ...i, openInModal: false }))
// );
// };
//
// 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('|');
// }
function getNowStreaming() {
return TmdbApi.getClient()
.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 || []);
}
function fetchUpcomingSeries() {
return TmdbApi.getClient()
.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 || []);
}
</script>
<Container focusOnMount class="flex flex-col">
<div class="h-[calc(100vh-12rem)] flex px-20">
<div class="h-[calc(100vh-12rem)] flex px-32">
<HeroShowcase
items={tmdbApi.getPopularSeries().then(getShowcasePropsFromTmdbSeries)}
on:enter={scrollIntoView({ top: 0 })}
/>
</div>
<div class="mt-16">
<Carousel scrollClass="px-20" on:enter={scrollIntoView({ vertical: 64 })}>
<div class="text-xl font-semibold text-zinc-300" slot="header">
{$isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)
? 'Loading...'
: $continueWatching?.length
? 'Continue Watching'
: 'Recently Added'}
</div>
{#if $isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)}
<CarouselPlaceholderItems />
{:else if $continueWatching?.length}
<div class="flex -mx-2">
{#each $continueWatching as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
<div class="my-16 space-y-8">
{#await continueWatching then continueWatching}
{#if continueWatching?.length}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Continue Watching</span>
{#each continueWatching as item (item.Id)}
<JellyfinCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</div>
{:else if $recentlyAdded?.length}
<div class="flex -mx-4">
{#each $recentlyAdded as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
{/each}
</div>
</Carousel>
{:else}
{#await recentlyAdded then recentlyAdded}
{#if recentlyAdded?.length}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Recently Added</span>
{#each recentlyAdded as item (item.Id)}
<JellyfinCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</Carousel>
{/if}
{/await}
{/if}
</Carousel>
{/await}
{#await nowStreaming then nowStreaming}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Now Streaming</span>
{#each nowStreaming as item}
<TmdbCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</Carousel>
{/await}
{#await upcomingSeries then upcomingSeries}
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
<span slot="header">Upcoming Series</span>
{#each upcomingSeries as item}
<TmdbCard on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {item} />
{/each}
</Carousel>
{/await}
</div>
</Container>