feat: Series recommendations and styling

This commit is contained in:
Aleksi Lassila
2024-04-16 18:11:35 +03:00
parent 32bde1ff9e
commit 9d647c0ae2
11 changed files with 162 additions and 25 deletions

View File

@@ -37,7 +37,7 @@
<slot name="icon" />
</div>
{/if}
<div class="flex-1 text-center">
<div class="flex-1 text-center text-nowrap">
<slot {hasFocus} />
</div>
{#if $$slots['icon-after']}

View File

@@ -1,19 +1,35 @@
<script lang="ts">
import Card from './Card.svelte';
import type { TmdbMovie2 } from '../../apis/tmdb/tmdb-api';
import type { TmdbMovie2, TmdbSeries2 } from '../../apis/tmdb/tmdb-api';
import type { ComponentProps } from 'svelte';
import { TMDB_POSTER_SMALL } from '../../constants';
import type { TitleType } from '../../types';
export let item: TmdbMovie2 | TmdbSeries2;
let title = '';
let subtitle = '';
let type: TitleType = 'movie';
if ('title' in item) {
title = item.title || title;
subtitle = item.release_date || subtitle;
type = 'movie';
} else if ('name' in item) {
title = item.name || title;
subtitle = item.first_air_date || subtitle;
type = 'series';
}
export let item: TmdbMovie2;
const props: ComponentProps<Card> = {
tmdbId: item.id,
title: item.title,
subtitle: item.release_date,
title,
subtitle,
backdropUrl: TMDB_POSTER_SMALL + item.poster_path,
type: 'movie',
type,
orientation: 'portrait',
rating: item.vote_average
rating: item.vote_average,
size: 'lg'
};
</script>
<Card {...props} />
<Card {...$$restProps} {...props} on:enter />

View File

@@ -6,7 +6,7 @@
import { PLATFORM_TV } from '../../constants';
export let gradientFromColor = 'from-secondary-500';
export let heading = '';
export let hideControls = false;
let carousel: HTMLDivElement | undefined;
let scrollX = 0;
@@ -15,15 +15,18 @@
<div class={classNames('flex flex-col group/carousel', $$restProps.class)}>
<div class={'flex justify-between items-center mb-2 ' + scrollClass}>
<slot name="title">
<div class="font-semibold text-xl">{heading}</div>
</slot>
<div class="font-medium tracking-wide text-2xl">
<slot name="header" />
</div>
<div
class={classNames(
'flex gap-2 ml-4',
//'sm:opacity-0 transition-opacity sm:group-hover/carousel:opacity-100',
{
hidden: (carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0) || PLATFORM_TV
hidden:
(carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0) ||
PLATFORM_TV ||
hideControls
}
)}
>
@@ -49,6 +52,7 @@
<div
class={classNames(
'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide',
'[&>*]:p-4 -mx-4 w-full',
scrollClass
)}
bind:this={carousel}

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import AnimateScale from '../AnimateScale.svelte';
import type { Readable } from 'svelte/store';
export let tmdbId: number;
// export let type: TitleType = 'person';
export let backdropUrl: string;
export let name: string;
export let subtitle: string;
export let size: 'dynamic' | 'md' | 'lg' = 'md';
let hasFocus: Readable<boolean>;
</script>
<AnimateScale hasFocus={$hasFocus}>
<Container
class={classNames(
'flex flex-col justify-start rounded-xl overflow-hidden relative shadow-lg shrink-0 selectable hover:text-inherit hover:bg-stone-800 focus-visible:bg-stone-800 bg-stone-900 group text-left',
{
'w-56 h-80': size === 'md',
'h-52': size === 'lg',
'w-full': size === 'dynamic'
}
)}
on:clickOrSelect
on:click={() => {
// if (openInModal) {
// openTitleModal({ type, id: tmdbId, provider: 'tmdb' });
// } else {
// window.location.href = `/${type}/${tmdbId}`;
// }
}}
on:enter
bind:hasFocus
>
<!-- <div-->
<!-- class="mx-auto rounded-full overflow-hidden flex-shrink-0 aspect-square w-full bg-zinc-200 bg-opacity-20"-->
<!-- >-->
<!-- <div-->
<!-- style={"background-image: url('" + backdropUrl + "')"}-->
<!-- class="bg-center bg-cover group-hover:scale-105 group-focus-visible:scale-105 transition-transform w-full h-full"-->
<!-- />-->
<!-- </div>-->
<div
style={"background-image: url('" + backdropUrl + "')"}
class="bg-center bg-cover w-full h-full"
/>
<div class="p-4">
<h2 class="text-sm text-zinc-300 font-medium line-clamp-1">{subtitle}</h2>
<h1 class="font-semibold line-clamp-2">
{name}
</h1>
</div>
</Container>
</AnimateScale>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import PersonCard from './PersonCard.svelte';
import type { TmdbCredit } from '../../apis/tmdb/tmdb-api';
import { TMDB_PROFILE_LARGE, TMDB_PROFILE_SMALL } from '../../constants';
export let tmdbCredit: TmdbCredit;
let subtitle = 'Uncredited';
if ('roles' in tmdbCredit) {
subtitle = tmdbCredit.roles?.[0]?.character || subtitle;
} else if ('character' in tmdbCredit) {
subtitle = tmdbCredit?.character || subtitle;
}
</script>
<PersonCard
tmdbId={tmdbCredit.id || -1}
name={tmdbCredit.original_name || 'Unknown'}
{subtitle}
backdropUrl={TMDB_PROFILE_LARGE + tmdbCredit.profile_path}
on:clickOrSelect
on:enter
/>

View File

@@ -90,7 +90,7 @@
'-translate-y-20': scrollTop < 140
})}
>
<svelte:fragment slot="title">
<svelte:fragment slot="header">
<UICarousel
class={classNames('flex -mx-2 transition-opacity', {
'opacity-0': scrollTop < 140

View File

@@ -17,6 +17,10 @@
import { scrollIntoView, Selectable } from '../../selectable';
import ScrollHelper from '../ScrollHelper.svelte';
import SonarrMediaMangerModal from '../MediaManager/sonarr/SonarrMediaMangerModal.svelte';
import Carousel from '../Carousel/Carousel.svelte';
import PersonCard from '../PersonCard/PersonCard.svelte';
import TmdbPersonCard from '../PersonCard/TmdbPersonCard.svelte';
import TmdbCard from '../Card/TmdbCard.svelte';
export let id: string;
@@ -29,6 +33,7 @@
(id: string) => jellyfinApi.getLibraryItemFromTmdbId(id),
id
);
const { promise: recommendations } = useRequest(tmdbApi.getSeriesRecommendations, Number(id));
const { data: jellyfinEpisodes } = useDependantRequest(
jellyfinApi.getJellyfinEpisodes,
jellyfinItemData,
@@ -199,7 +204,7 @@
</div>
</HeroCarousel>
</Container>
<Container on:enter={scrollIntoView({ vertical: 64 })} bind:selectable={episodesSelectable}>
<Container on:enter={scrollIntoView({ bottom: 32 })} bind:selectable={episodesSelectable}>
<EpisodeCarousel
id={Number(id)}
tmdbSeries={tmdbSeriesData}
@@ -208,5 +213,27 @@
bind:selectedTmdbEpisode
/>
</Container>
<Container on:enter={scrollIntoView({ top: 0 })} class="min-h-screen flex flex-col">
{#await $tmdbSeries then series}
<Carousel scrollClass="px-20" class="mt-8">
<div slot="header">Show Cast</div>
{#each series?.aggregate_credits?.cast?.slice(0, 15) || [] as credit}
<TmdbPersonCard
on:enter={scrollIntoView({ horizontal: 64 + 30 })}
tmdbCredit={credit}
/>
{/each}
</Carousel>
{/await}
{#await $recommendations then recommendations}
<Carousel scrollClass="px-20" class="mt-8">
<div slot="header">Recommendations</div>
{#each recommendations || [] as recommendation}
<TmdbCard item={recommendation} on:enter={scrollIntoView({ horizontal: 64 + 30 })} />
{/each}
</Carousel>
{/await}
<Container class="flex-1 bg-secondary-950 mt-4 pt-4">More info</Container>
</Container>
</div>
</DetachedPage>