mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-21 08:15:12 +02:00
feat: Series recommendations and styling
This commit is contained in:
@@ -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']}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
58
src/lib/components/PersonCard/PersonCard.svelte
Normal file
58
src/lib/components/PersonCard/PersonCard.svelte
Normal 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>
|
||||
22
src/lib/components/PersonCard/TmdbPersonCard.svelte
Normal file
22
src/lib/components/PersonCard/TmdbPersonCard.svelte
Normal 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
|
||||
/>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user