Styling improvements
This commit is contained in:
@@ -5,6 +5,7 @@ import axios from 'axios';
|
||||
import createClient from 'openapi-fetch';
|
||||
import { get } from 'svelte/store';
|
||||
import type { operations, paths } from './tmdb.generated';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const CACHE_ONE_DAY = 'max-age=86400';
|
||||
const CACHE_FOUR_DAYS = 'max-age=345600';
|
||||
@@ -30,6 +31,31 @@ export interface TmdbSeriesFull2 extends TmdbSeries2 {
|
||||
images: operations['tv-series-images']['responses']['200']['content']['application/json'];
|
||||
}
|
||||
|
||||
const backdropCache = browser ? window?.caches?.open('backdrops') : undefined;
|
||||
const posterCache = browser ? window?.caches?.open('posters') : undefined;
|
||||
|
||||
const getTmdbCache = async (
|
||||
cachePromise: typeof backdropCache,
|
||||
tmdbId: number,
|
||||
fn: () => Promise<string | undefined>
|
||||
) => {
|
||||
const cache = await cachePromise;
|
||||
|
||||
if (cache) {
|
||||
const cacheRes = await cache.match(String(tmdbId));
|
||||
if (cacheRes) return cacheRes.text();
|
||||
else {
|
||||
const backdropUri = await fn();
|
||||
if (backdropUri) {
|
||||
cache.put(String(tmdbId), new Response(backdropUri));
|
||||
}
|
||||
return backdropUri;
|
||||
}
|
||||
} else {
|
||||
return fn();
|
||||
}
|
||||
};
|
||||
|
||||
export const TmdbApiOpen = createClient<paths>({
|
||||
baseUrl: 'https://api.themoviedb.org',
|
||||
headers: {
|
||||
@@ -114,19 +140,6 @@ export const getTmdbSeriesImages = async (tmdbId: number) =>
|
||||
}
|
||||
}).then((res) => res.data);
|
||||
|
||||
export const getTmdbSeriesBackdrop = async (tmdbId: number) =>
|
||||
getTmdbSeries(tmdbId)
|
||||
.then((s) => s?.images)
|
||||
.then(
|
||||
(r) =>
|
||||
(
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
r?.backdrops?.[0]
|
||||
)?.file_path
|
||||
);
|
||||
|
||||
export const getTmdbMovieImages = async (tmdbId: number) =>
|
||||
await TmdbApiOpen.get('/3/movie/{movie_id}/images', {
|
||||
params: {
|
||||
@@ -139,18 +152,41 @@ export const getTmdbMovieImages = async (tmdbId: number) =>
|
||||
}
|
||||
}).then((res) => res.data);
|
||||
|
||||
export const getTmdbSeriesBackdrop = async (tmdbId: number) =>
|
||||
getTmdbCache(backdropCache, tmdbId, () =>
|
||||
getTmdbSeries(tmdbId)
|
||||
.then((s) => s?.images)
|
||||
.then(
|
||||
(r) =>
|
||||
(
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
r?.backdrops?.[0]
|
||||
)?.file_path
|
||||
)
|
||||
);
|
||||
|
||||
export const getTmdbMovieBackdrop = async (tmdbId: number) =>
|
||||
getTmdbMovie(tmdbId)
|
||||
.then((m) => m?.images)
|
||||
.then(
|
||||
(r) =>
|
||||
(
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
r?.backdrops?.[0]
|
||||
)?.file_path
|
||||
);
|
||||
getTmdbCache(backdropCache, tmdbId, () =>
|
||||
getTmdbMovie(tmdbId)
|
||||
.then((m) => m?.images)
|
||||
.then(
|
||||
(r) =>
|
||||
(
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
r?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
r?.backdrops?.[0]
|
||||
)?.file_path
|
||||
)
|
||||
);
|
||||
|
||||
export const getTmdbSeriesPoster = async (tmdbId: number) =>
|
||||
getTmdbCache(posterCache, tmdbId, () => getTmdbSeries(tmdbId).then((s) => s?.poster_path));
|
||||
|
||||
export const getTmdbMoviePoster = async (tmdbId: number) =>
|
||||
getTmdbCache(posterCache, tmdbId, () => getTmdbMovie(tmdbId).then((m) => m?.poster_path));
|
||||
|
||||
export const getTmdbPopularMovies = () =>
|
||||
TmdbApiOpen.get('/3/movie/popular', {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
'focus-visible:bg-zinc-200 focus-visible:text-zinc-800 hover:bg-zinc-200 hover:text-zinc-800':
|
||||
(type === 'secondary' || type === 'tertiary') && !disabled,
|
||||
'rounded-full': type === 'tertiary',
|
||||
'py-3 px-6': size === 'lg',
|
||||
'py-2 px-6 sm:py-3 sm:px-6': size === 'lg',
|
||||
'py-2 px-6': size === 'md',
|
||||
'py-1 px-3': size === 'sm',
|
||||
'opacity-50': disabled,
|
||||
|
||||
@@ -4,58 +4,62 @@
|
||||
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let gradientFromColor = 'from-stone-900';
|
||||
export let gradientFromColor = 'from-stone-950';
|
||||
export let heading = '';
|
||||
|
||||
let carousel: HTMLDivElement | undefined;
|
||||
let scrollX = 0;
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between items-center mx-2 sm:mx-8 gap-4">
|
||||
<slot name="title">
|
||||
<div class="font-semibold text-xl">{heading}</div>
|
||||
</slot>
|
||||
<div
|
||||
class={classNames('flex gap-2', {
|
||||
hidden: (carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0)
|
||||
})}
|
||||
>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
<div class={classNames('flex flex-col gap-4', $$restProps.class)}>
|
||||
<div class={'flex justify-between items-center gap-4'}>
|
||||
<slot name="title">
|
||||
<div class="font-semibold text-xl">{heading}</div>
|
||||
</slot>
|
||||
<div
|
||||
class={classNames('flex gap-2', {
|
||||
hidden: (carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0)
|
||||
})}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth * 0.8, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex overflow-x-scroll items-center overflow-y-hidden gap-4 relative px-2 sm:px-8 scrollbar-hide py-4"
|
||||
bind:this={carousel}
|
||||
tabindex="-1"
|
||||
on:scroll={() => (scrollX = carousel?.scrollLeft || scrollX)}
|
||||
>
|
||||
<slot />
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex overflow-x-scroll items-center overflow-y-hidden gap-4 relative scrollbar-hide"
|
||||
bind:this={carousel}
|
||||
tabindex="-1"
|
||||
on:scroll={() => (scrollX = carousel?.scrollLeft || scrollX)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{#if scrollX > 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 left-0 w-0 sm:w-16 md:w-24 bg-gradient-to-r ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 right-0 w-0 sm:w-16 md:w-24 bg-gradient-to-l ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if scrollX > 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-4 left-0 w-0 sm:w-16 md:w-24 bg-gradient-to-r ' + gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-4 right-0 w-0 sm:w-16 md:w-24 bg-gradient-to-l ' + gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<button
|
||||
on:click
|
||||
class={classNames(
|
||||
'aspect-video bg-center bg-cover bg-no-repeat rounded-lg overflow-hidden transition-all shadow-lg relative selectable flex-shrink-0 placeholder-image flex',
|
||||
'aspect-video bg-center bg-cover bg-no-repeat rounded-lg overflow-hidden transition-all shadow-lg relative selectable flex-shrink-0 placeholder-image flex flex-col',
|
||||
{
|
||||
'h-40': size === 'md',
|
||||
'h-full': size === 'dynamic',
|
||||
@@ -77,7 +77,7 @@
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex-1 flex flex-col justify-between group-hover:opacity-0 transition-all',
|
||||
'w-full flex-1 flex flex-col justify-between group-hover:opacity-0 transition-all',
|
||||
{
|
||||
'px-2 lg:px-3 pt-2': true,
|
||||
'pb-4 lg:pb-6': progress,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={windowHeight} />
|
||||
<svelte:window bind:outerHeight={windowHeight} />
|
||||
|
||||
<div
|
||||
style={"background-image: url('" +
|
||||
@@ -31,13 +31,25 @@
|
||||
"'); height: " +
|
||||
imageHeight.toFixed() +
|
||||
'px'}
|
||||
class="fixed inset-x-0 bg-center bg-cover z-[-10]"
|
||||
class="hidden sm:block fixed inset-x-0 bg-center bg-cover z-[-10]"
|
||||
>
|
||||
<div class="absolute inset-0 bg-darken" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col h-screen" transition:fade>
|
||||
<div class="flex-1 relative flex pt-24 px-4 sm:px-8">
|
||||
<div
|
||||
style={"background-image: url('" +
|
||||
TMDB_IMAGES_ORIGINAL +
|
||||
posterPath +
|
||||
"'); height: " +
|
||||
imageHeight.toFixed() +
|
||||
'px'}
|
||||
class="sm:hidden fixed inset-x-0 bg-center bg-cover z-[-10]"
|
||||
>
|
||||
<div class="absolute inset-0 bg-darken" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col h-[85vh] sm:h-screen" transition:fade>
|
||||
<div class="flex-1 relative flex pt-24 px-4 sm:px-8 pb-6">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-stone-950 to-30%" />
|
||||
<div class="z-[1] flex-1 flex justify-end gap-8 items-end max-w-screen-2xl mx-auto">
|
||||
<div
|
||||
@@ -67,18 +79,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div bind:clientHeight={bottomHeight} class="py-6 bg-stone-950">
|
||||
<div bind:clientHeight={bottomHeight} class="pb-6 bg-stone-950">
|
||||
<div class="max-w-screen-2xl mx-auto">
|
||||
<div class="2xl:-mx-8">
|
||||
<slot name="episodes-carousel" />
|
||||
</div>
|
||||
<slot name="episodes-carousel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6 bg-stone-950">
|
||||
<div class="flex flex-col gap-6 bg-stone-950 px-2 sm:px-4 lg:px-8 2xl:px-0 pb-6">
|
||||
<div
|
||||
class="mx-4 sm:mx-8 grid grid-cols-4 sm:grid-cols-6 gap-4 sm:gap-x-8 rounded-xl py-6 max-w-screen-2xl 2xl:mx-auto"
|
||||
class="grid grid-cols-4 sm:grid-cols-6 gap-4 sm:gap-x-8 rounded-xl max-w-screen-2xl 2xl:mx-auto py-4"
|
||||
>
|
||||
<slot name="info-description">
|
||||
<div
|
||||
@@ -126,34 +136,28 @@
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto">
|
||||
<div class="2xl:-mx-8">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="cast-crew-carousel-title" slot="title" />
|
||||
<slot name="cast-crew-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto w-full">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="cast-crew-carousel-title" slot="title" />
|
||||
<slot name="cast-crew-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto">
|
||||
<div class="2xl:-mx-8">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="recommendations-carousel-title" slot="title" />
|
||||
<slot name="recommendations-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto w-full">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="recommendations-carousel-title" slot="title" />
|
||||
<slot name="recommendations-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto">
|
||||
<div class="2xl:-mx-8">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="similar-carousel-title" slot="title" />
|
||||
<slot name="similar-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto w-full">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="similar-carousel-title" slot="title" />
|
||||
<slot name="similar-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,10 +67,15 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="h-screen relative pt-24 flex">
|
||||
<div class="relative z-[1] px-16 py-8 flex-1 grid grid-cols-6">
|
||||
<div class="h-[80vh] sm:h-screen relative pt-24 flex">
|
||||
<div
|
||||
class={classNames(
|
||||
'relative z-[1] px-4 lg:px-16 2xl:px-32 py-4 lg:py-8 2xl:py-16 flex-1 sm:grid grid-cols-6 grid-rows-3',
|
||||
'flex flex-col justify-end gap-8'
|
||||
)}
|
||||
>
|
||||
{#if UIVisible}
|
||||
<div class="flex flex-col justify-center col-span-3 gap-6 max-w-screen-md -mt-20">
|
||||
<div class="flex flex-col col-span-3 gap-6 max-w-screen-md">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div
|
||||
class="flex items-center gap-1 uppercase text-sm text-zinc-300 font-semibold tracking-wider"
|
||||
@@ -85,8 +90,8 @@
|
||||
</div>
|
||||
<h1
|
||||
class={classNames('font-medium tracking-wider text-stone-200', {
|
||||
'text-6xl': title.length < 15,
|
||||
'text-5xl': title.length >= 15
|
||||
'text-5xl sm:text-6xl 2xl:text-7xl': title.length < 15,
|
||||
'text-4xl sm:text-5xl 2xl:text-6xl': title.length >= 15
|
||||
})}
|
||||
in:fly|global={{ y: -10, delay: ANIMATION_DURATION, duration: ANIMATION_DURATION }}
|
||||
out:fly|global={{ y: 10, duration: ANIMATION_DURATION }}
|
||||
@@ -128,7 +133,7 @@
|
||||
</div> -->
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 flex flex-col gap-6 justify-end col-span-2 col-start-1"
|
||||
class="sm:flex-1 flex flex-col gap-6 justify-end col-span-2 col-start-1 row-start-3"
|
||||
in:fade|global={{ delay: ANIMATION_DURATION, duration: ANIMATION_DURATION }}
|
||||
out:fade|global={{ duration: ANIMATION_DURATION }}
|
||||
>
|
||||
@@ -215,7 +220,7 @@
|
||||
</div> -->
|
||||
</div>
|
||||
{/if}
|
||||
<div class="row-start-2 col-start-4 col-span-3 flex items-end justify-end">
|
||||
<div class="hidden lg:flex items-end justify-end col-start-4 row-start-3 col-span-3">
|
||||
<div class="flex gap-6 items-center">
|
||||
<div>
|
||||
<p class="text-zinc-400 text-sm font-medium">Release Date</p>
|
||||
@@ -239,6 +244,21 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if UIVisible}
|
||||
<div
|
||||
class="hidden lg:flex absolute inset-x-4 lg:inset-x-16 2xl:inset-x-32 bottom-4 lg:bottom-8 2xl:bottom-16 opacity-70 gap-3 justify-end lg:justify-center"
|
||||
in:fade={{ delay: ANIMATION_DURATION, duration: ANIMATION_DURATION }}
|
||||
out:fade={{ duration: ANIMATION_DURATION }}
|
||||
>
|
||||
{#each Array.from({ length: showcaseLength }, (_, i) => i) as i}
|
||||
{#if i === showcaseIndex}
|
||||
<DotFilled size={15} class="opacity-100" />
|
||||
{:else}
|
||||
<DotFilled size={15} class="opacity-20" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !trailerVisible}
|
||||
<div
|
||||
@@ -261,13 +281,13 @@
|
||||
{/if}
|
||||
{#if UIVisible}
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-darken via-[30%] to-darken opacity-50"
|
||||
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-darken via-[20%] to-darken"
|
||||
in:fade={{ duration: ANIMATION_DURATION }}
|
||||
out:fade={{ duration: ANIMATION_DURATION }}
|
||||
/>
|
||||
{:else if !UIVisible}
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-darken opacity-50"
|
||||
class="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-darken"
|
||||
in:fade={{ duration: ANIMATION_DURATION }}
|
||||
out:fade={{ duration: ANIMATION_DURATION }}
|
||||
/>
|
||||
@@ -292,19 +312,4 @@
|
||||
class="opacity-0 peer-hover:opacity-20 transition-opacity bg-gradient-to-l from-darken absolute inset-0"
|
||||
/>
|
||||
</div>
|
||||
{#if UIVisible}
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-8 flex justify-center opacity-70 gap-3"
|
||||
in:fade={{ delay: ANIMATION_DURATION, duration: ANIMATION_DURATION }}
|
||||
out:fade={{ duration: ANIMATION_DURATION }}
|
||||
>
|
||||
{#each Array.from({ length: showcaseLength }, (_, i) => i) as i}
|
||||
{#if i === showcaseIndex}
|
||||
<DotFilled size={15} class="opacity-100" />
|
||||
{:else}
|
||||
<DotFilled size={15} class="opacity-20" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
})}
|
||||
>
|
||||
<IconButton on:click={modalProps.close}>
|
||||
<Cross2 />
|
||||
<Cross2 size={25} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden w-full h-full">
|
||||
<div class="youtube-container scale-[150%] hidden sm:block h-full w-full">
|
||||
<div class="youtube-container scale-[150%] h-full w-full">
|
||||
<iframe
|
||||
src={'https://www.youtube.com/embed/' +
|
||||
videoId +
|
||||
|
||||
@@ -18,18 +18,19 @@ import {
|
||||
type SonarrSeries
|
||||
} from '$lib/apis/sonarr/sonarrApi';
|
||||
import {
|
||||
getTmdbMovie,
|
||||
getTmdbSeries,
|
||||
getTmdbMovieBackdrop,
|
||||
getTmdbMoviePoster,
|
||||
getTmdbSeriesBackdrop,
|
||||
getTmdbSeriesFromTvdbId,
|
||||
type TmdbMovieFull2,
|
||||
type TmdbSeriesFull2
|
||||
} from '$lib/apis/tmdb/tmdbApi';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { settings } from './settings.store';
|
||||
|
||||
export interface PlayableItem {
|
||||
tmdbRating: number;
|
||||
cardBackdropUrl: string;
|
||||
posterUri: string;
|
||||
download?: {
|
||||
progress: number;
|
||||
completionTime: string;
|
||||
@@ -50,8 +51,8 @@ export interface PlayableItem {
|
||||
radarrDownloads?: RadarrDownload[];
|
||||
sonarrSeries?: SonarrSeries;
|
||||
sonarrDownloads?: SonarrDownload[];
|
||||
tmdbMovie?: TmdbMovieFull2;
|
||||
tmdbSeries?: TmdbSeriesFull2;
|
||||
// tmdbMovie?: TmdbMovieFull2;
|
||||
// tmdbSeries?: TmdbSeriesFull2;
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
@@ -117,27 +118,31 @@ async function getLibrary(): Promise<Library> {
|
||||
? { length, progress: watchingProgress }
|
||||
: undefined;
|
||||
|
||||
const tmdbMovie = await getTmdbMovie(radarrMovie.tmdbId || 0);
|
||||
const backdropUrl = (
|
||||
tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
tmdbMovie?.images?.backdrops?.[0]
|
||||
)?.file_path;
|
||||
// const tmdbMovie = await getTmdbMovie(radarrMovie.tmdbId || 0);
|
||||
// const backdropUrl = (
|
||||
// tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
// tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
// tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
// tmdbMovie?.images?.backdrops?.[0]
|
||||
// )?.file_path;
|
||||
|
||||
const backdropUrl = await getTmdbMovieBackdrop(radarrMovie.tmdbId || 0);
|
||||
const posterUri = await getTmdbMoviePoster(radarrMovie.tmdbId || 0);
|
||||
|
||||
return {
|
||||
type: 'movie' as const,
|
||||
tmdbId: radarrMovie.tmdbId || 0,
|
||||
tmdbRating: radarrMovie.ratings?.tmdb?.value || 0,
|
||||
cardBackdropUrl: backdropUrl || '',
|
||||
posterUri: posterUri || '',
|
||||
download,
|
||||
continueWatching,
|
||||
isPlayed: jellyfinItem?.UserData?.Played || false,
|
||||
jellyfinId: jellyfinItem?.Id,
|
||||
jellyfinItem,
|
||||
radarrMovie,
|
||||
radarrDownloads: itemRadarrDownloads,
|
||||
tmdbMovie
|
||||
radarrDownloads: itemRadarrDownloads
|
||||
// tmdbMovie
|
||||
};
|
||||
});
|
||||
|
||||
@@ -179,19 +184,23 @@ async function getLibrary(): Promise<Library> {
|
||||
: undefined;
|
||||
const tmdbId = tmdbItem?.id || undefined;
|
||||
|
||||
const tmdbSeries = await getTmdbSeries(tmdbId || 0);
|
||||
const backdropUrl = (
|
||||
tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
tmdbSeries?.images?.backdrops?.[0]
|
||||
)?.file_path;
|
||||
// const tmdbSeries = await getTmdbSeries(tmdbId || 0);
|
||||
// const backdropUrl = (
|
||||
// tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
// tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
// tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
// tmdbSeries?.images?.backdrops?.[0]
|
||||
// )?.file_path;
|
||||
|
||||
const backdropUrl = await getTmdbSeriesBackdrop(tmdbId || 0);
|
||||
const posterUri = tmdbItem?.poster_path || '';
|
||||
|
||||
return {
|
||||
type: 'series' as const,
|
||||
tmdbId: tmdbId || 0,
|
||||
tmdbRating: tmdbItem?.vote_average || 0,
|
||||
cardBackdropUrl: backdropUrl || '',
|
||||
posterUri,
|
||||
download,
|
||||
continueWatching,
|
||||
isPlayed: jellyfinItem?.UserData?.Played || false,
|
||||
@@ -200,8 +209,8 @@ async function getLibrary(): Promise<Library> {
|
||||
sonarrSeries,
|
||||
sonarrDownloads: itemSonarrDownloads,
|
||||
jellyfinEpisodes: jellyfinEpisodes.filter((i) => i.SeriesId === jellyfinItem?.Id),
|
||||
nextJellyfinEpisode,
|
||||
tmdbSeries
|
||||
nextJellyfinEpisode
|
||||
// tmdbSeries
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -15,31 +15,31 @@
|
||||
.then((libraryData) => libraryData.continueWatching)
|
||||
.then((items) =>
|
||||
items.map((item) =>
|
||||
item.tmdbMovie
|
||||
item.radarrMovie
|
||||
? {
|
||||
tmdbId: item.tmdbMovie.id || 0,
|
||||
tmdbId: item.tmdbId || 0,
|
||||
jellyfinId: item.jellyfinId,
|
||||
backdropUri: item.tmdbMovie.poster_path || '',
|
||||
title: item.tmdbMovie.title,
|
||||
subtitle: item.tmdbMovie.genres?.map((g) => g.name).join(', ') || '',
|
||||
backdropUri: item.posterUri || '',
|
||||
title: item.radarrMovie.title || '',
|
||||
subtitle: item.radarrMovie.genres?.join(', ') || '',
|
||||
progress: item.continueWatching?.progress,
|
||||
runtime: item.tmdbMovie.runtime || 0
|
||||
runtime: item.radarrMovie.runtime || 0
|
||||
}
|
||||
: {
|
||||
tmdbId: item.tmdbSeries?.id || 0,
|
||||
tmdbId: item.tmdbId || 0,
|
||||
jellyfinId: item.nextJellyfinEpisode?.Id,
|
||||
type: 'series',
|
||||
backdropUri: item.tmdbSeries?.poster_path || '',
|
||||
title: item.nextJellyfinEpisode?.Name || item.tmdbSeries?.name || '',
|
||||
backdropUri: item.posterUri || '',
|
||||
title: item.nextJellyfinEpisode?.Name || item.sonarrSeries?.title || '',
|
||||
subtitle:
|
||||
(item.nextJellyfinEpisode?.IndexNumber &&
|
||||
'Episode ' + item.nextJellyfinEpisode?.IndexNumber) ||
|
||||
item.tmdbSeries?.genres?.map((g) => g.name).join(', ') ||
|
||||
item.sonarrSeries?.genres?.join(', ') ||
|
||||
'',
|
||||
progress: item.continueWatching?.progress,
|
||||
runtime: item.nextJellyfinEpisode?.RunTimeTicks
|
||||
? item.nextJellyfinEpisode?.RunTimeTicks / 10_000_000 / 60
|
||||
: item.tmdbSeries?.episode_run_time?.[0] || 0
|
||||
: item.sonarrSeries?.runtime || 0
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -84,7 +84,7 @@
|
||||
{/await}
|
||||
|
||||
<div class="py-8">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<Carousel gradientFromColor="from-stone-950" class="px-4 lg:px-16 2xl:px-32">
|
||||
<div slot="title" class="text-xl font-medium text-zinc-200">Continue Watching</div>
|
||||
{#await continueWatchingProps}
|
||||
<CarouselPlaceholderItems />
|
||||
|
||||
@@ -74,15 +74,8 @@
|
||||
const headerStyle = 'uppercase tracking-widest font-bold';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-4"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<div class="pt-24 bg-black">
|
||||
<div class="pt-24 bg-black pb-8">
|
||||
<div class="max-w-screen-2xl mx-auto">
|
||||
<Carousel gradientFromColor="from-black" heading="Trending">
|
||||
{#await fetchTrendingProps()}
|
||||
<CarouselPlaceholderItems size="lg" />
|
||||
@@ -93,73 +86,69 @@
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="Popular People">
|
||||
{#await fetchTrendingActorProps()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<PeopleCard {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="Upcoming Movies">
|
||||
{#await fetchUpcomingMovies()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="Upcoming Series">
|
||||
{#await fetchUpcomingSeries()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="Genres">
|
||||
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
|
||||
<GenreCard {genre} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="New Digital Releeases">
|
||||
{#await fetchDigitalReleases()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="Streaming Now">
|
||||
{#await fetchNowStreaming()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel heading="TV Networks">
|
||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
||||
<NetworkCard {network} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-8 max-w-screen-2xl mx-auto py-4"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="Popular People">
|
||||
{#await fetchTrendingActorProps()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<PeopleCard {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="Upcoming Movies">
|
||||
{#await fetchUpcomingMovies()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="Upcoming Series">
|
||||
{#await fetchUpcomingSeries()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="Genres">
|
||||
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
|
||||
<GenreCard {genre} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="New Digital Releeases">
|
||||
{#await fetchDigitalReleases()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="Streaming Now">
|
||||
{#await fetchNowStreaming()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading="TV Networks">
|
||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
||||
<NetworkCard {network} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
@@ -4,24 +4,12 @@
|
||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte';
|
||||
import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte';
|
||||
import { TMDB_IMAGES_ORIGINAL } from '$lib/constants';
|
||||
import { library, type PlayableItem } from '$lib/stores/library.store';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
CaretDown,
|
||||
ChevronDown,
|
||||
Gear,
|
||||
ListBullet,
|
||||
MagnifyingGlass,
|
||||
TextAlignBottom,
|
||||
TextAlignRight,
|
||||
Trash
|
||||
} from 'radix-icons-svelte';
|
||||
import { CaretDown, ChevronDown, Gear } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let itemsVisible: 'all' | 'movies' | 'shows' = 'all';
|
||||
let sortBy: 'added' | 'rating' | 'release' | 'size' | 'name' = 'name';
|
||||
@@ -220,9 +208,7 @@
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="relative grid grid-cols-3 grid-rows-3 z-[1] max-w-screen-2xl mx-auto">
|
||||
<div
|
||||
class="col-start-1 row-start-2 row-span-2 col-span-3 flex justify-end flex-col -mx-2 sm:-mx-8"
|
||||
>
|
||||
<div class="col-start-1 row-start-2 row-span-2 col-span-3 flex justify-end flex-col">
|
||||
<!-- <div class="flex flex-col gap-2">
|
||||
<div class="text-lg font-semibold">Downloading Now</div>
|
||||
<Card {...downloadingProps[0] || unavailableProps[0]} size="md" />
|
||||
|
||||
@@ -75,16 +75,21 @@
|
||||
<svelte:fragment slot="title-info-1"
|
||||
>{new Date(movie?.release_date || Date.now()).getFullYear()}</svelte:fragment
|
||||
>
|
||||
<svelte:fragment slot="title-info-2">{movie?.runtime} min</svelte:fragment>
|
||||
<svelte:fragment slot="title-info-2">
|
||||
{@const progress = $itemStore.item?.continueWatching?.progress}
|
||||
{#if progress}
|
||||
{progress.toFixed()} min left
|
||||
{:else}
|
||||
{movie?.runtime} min
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title-info-3">
|
||||
<a href={tmdbUrl} target="_blank">{movie?.vote_average?.toFixed(1)} TMDB</a>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="episodes-carousel">
|
||||
{@const progress = $itemStore.item?.continueWatching?.progress}
|
||||
{#if progress}
|
||||
<div class="mx-4 sm:mx-8">
|
||||
<ProgressBar {progress} />
|
||||
</div>
|
||||
<ProgressBar {progress} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<div slot="episodes-carousel">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<Carousel gradientFromColor="from-stone-950" class="px-2 sm:px-4 lg:px-8 2xl:px-0">
|
||||
<UiCarousel slot="title" class="flex gap-6">
|
||||
{#each [...Array(series?.number_of_seasons || 0).keys()].map((i) => i + 1) as seasonNumber}
|
||||
{@const season = series?.seasons?.find((s) => s.season_number === seasonNumber)}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
display: ['Montserrat', 'system', 'sans-serif']
|
||||
},
|
||||
colors: {
|
||||
darken: '#07050199',
|
||||
darken: '#07050166',
|
||||
lighten: '#fde68a20'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user