feat: Episode card frames

This commit is contained in:
Aleksi Lassila
2024-04-04 12:17:24 +03:00
parent d1eb3a4cfe
commit df1623eb53
8 changed files with 152 additions and 18 deletions

View File

@@ -8,7 +8,7 @@
import LibraryPage from './lib/pages/LibraryPage.svelte';
import ManagePage from './lib/pages/ManagePage.svelte';
import SearchPage from './lib/pages/SearchPage.svelte';
import SeriesPage from './lib/pages/SeriesPage.svelte';
import SeriesPage from './lib/components/SeriesPage/SeriesPage.svelte';
import Sidebar from './lib/components/Sidebar/Sidebar.svelte';
import LoginPage from './lib/pages/LoginPage.svelte';
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';

View File

@@ -3,6 +3,8 @@
import { type NavigationActions, type RevealStrategy, Selectable } from './lib/selectable';
import classNames from 'classnames';
export let element: HTMLElement;
export let name: string = '';
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
export let gridCols: number = 0;
@@ -10,7 +12,7 @@
export let canFocusEmpty = true;
export let trapFocus = false;
export let debugOutline = false;
export let revealStrategy: RevealStrategy | undefined = undefined;
export let revealStrategy: RevealStrategy | undefined = undefined; //TODO: change to on:focus
export let childrenRevealStrategy: RevealStrategy | undefined = undefined;
export let active = true;
@@ -58,6 +60,7 @@
'outline-none': debugOutline === false
})}
use:registerer
bind:this={element}
>
<slot hasFocus={$hasFocus} hasFocusWithin={$hasFocusWithin} focusIndex={$focusIndex} />
</svelte:element>

View File

@@ -16,6 +16,7 @@ export type TmdbSeries2 =
operations['tv-series-details']['responses']['200']['content']['application/json'];
export type TmdbSeason =
operations['tv-season-details']['responses']['200']['content']['application/json'];
export type TmdbEpisode = NonNullable<TmdbSeason['episodes']>[0];
export type TmdbPerson =
operations['person-details']['responses']['200']['content']['application/json'];

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import classNames from 'classnames';
import { onMount } from 'svelte';
import Container from '../../../Container.svelte';
let element: HTMLDivElement;
let scrollX = 0;
let maxScrollX = 0;
let fadeLeft = false;
let fadeRight = true;
$: {
fadeLeft = scrollX > 10;
fadeRight = scrollX < maxScrollX - 10;
}
function updateScrollPosition() {
scrollX = element.scrollLeft;
maxScrollX = element.scrollWidth - element.clientWidth;
}
onMount(() => {
updateScrollPosition();
});
</script>
<Container
direction="horizontal"
class={classNames($$restProps.class, 'overflow-x-scroll scrollbar-hide relative p-1')}
style={`mask-image: linear-gradient(to right, transparent 0%, ${
fadeLeft ? '' : 'black 0%, '
}black 5%, black 95%, ${fadeRight ? '' : 'black 100%, '}transparent 100%);`}
on:scroll={updateScrollPosition}
bind:element
>
<slot />
</Container>

View File

@@ -11,7 +11,7 @@
}}
focusOnMount
trapFocus
class="fixed inset-0 z-20 bg-stone-950"
class="fixed inset-0 z-20 bg-stone-950 overflow-y-auto"
>
<slot />
</Container>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import type { TmdbEpisode, TmdbSeason } from '../../apis/tmdb/tmdb-api';
import Container from '../../../Container.svelte';
import classNames from 'classnames';
import { TMDB_BACKDROP_SMALL } from '../../constants';
export let episode: TmdbEpisode;
</script>
<Container
class={classNames(
'rounded-xl overflow-hidden',
'h-56 w-96 px-4 py-3',
'flex flex-col shrink-0 relative selectable'
)}
>
<div class="flex-1 flex flex-col justify-end z-10">
<h2 class="text-zinc-300 font-medium">Episode {episode.episode_number}</h2>
<h1 class="text-zinc-100 text-lg font-medium line-clamp-2">{episode.name}</h1>
</div>
<div
class="absolute inset-0 bg-center bg-cover"
style={`background-image: url('${TMDB_BACKDROP_SMALL + episode.still_path}')`}
/>
<div
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-transparent via-40% to-transparent"
/>
</Container>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import type { JellyfinItem } from '../../apis/jellyfin/jellyfin-api';
import EpisodeCard from './EpisodeCard.svelte';
import { useDependantRequest } from '../../stores/data.store';
import type { Readable } from 'svelte/store';
import { tmdbApi, type TmdbSeason, type TmdbSeriesFull2 } from '../../apis/tmdb/tmdb-api';
import Carousel from '../Carousel/Carousel.svelte';
import Container from '../../../Container.svelte';
import { scrollWithOffset } from '../../selectable';
import UICarousel from '../Carousel/UICarousel.svelte';
import classNames from 'classnames';
export let id: number;
export let tmdbSeries: Readable<TmdbSeriesFull2 | undefined>;
export let nextEpisode: JellyfinItem | undefined = undefined;
const { data: tmdbSeasons, isLoading: isTmdbSeasonsLoading } = useDependantRequest(
(seasons: number) => tmdbApi.getTmdbSeriesSeasons(id, seasons),
tmdbSeries,
(series) => (series?.seasons?.length ? ([series.seasons.length] as const) : undefined)
);
function handleSelectSeason(season: TmdbSeason) {
console.log(season);
}
</script>
{#if $isTmdbSeasonsLoading}
Loading...
{:else if $tmdbSeasons}
<Carousel scrollClass="px-20">
<UICarousel slot="title" class="text-xl flex -mx-2 max-w-2xl">
{#each $tmdbSeasons as season}
<Container
let:hasFocus
class="mx-2 text-nowrap"
on:click={() => handleSelectSeason(season)}
>
<div
class={classNames({
'font-semibold tracking-wide': hasFocus,
'text-zinc-300 font-medium': !hasFocus
})}
>
Season {season.season_number}
</div>
</Container>
{/each}
</UICarousel>
<Container revealStrategy={scrollWithOffset('all', 64)} class="flex">
{#each $tmdbSeasons as season}
{#each season?.episodes || [] as episode}
<div class="mx-2">
<EpisodeCard {episode} />
</div>
{/each}
{/each}
</Container>
</Carousel>
{/if}

View File

@@ -1,24 +1,28 @@
<script lang="ts">
import Container from '../../Container.svelte';
import SidebarMargin from '../components/SidebarMargin.svelte';
import HeroCarousel from '../components/HeroCarousel/HeroCarousel.svelte';
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
import { useActionRequest, useDependantRequest, useRequest } from '../stores/data.store';
import { tmdbApi } from '../apis/tmdb/tmdb-api';
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
import Container from '../../../Container.svelte';
import SidebarMargin from '../SidebarMargin.svelte';
import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte';
import DetachedPage from '../DetachedPage/DetachedPage.svelte';
import { useActionRequest, useDependantRequest, useRequest } from '../../stores/data.store';
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
import classNames from 'classnames';
import { DotFilled, Download, ExternalLink, File, Play, Plus } from 'radix-icons-svelte';
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
import { sonarrApi } from '../apis/sonarr/sonarr-api';
import Button from '../components/Button.svelte';
import { playerState } from '../components/VideoPlayer/VideoPlayer';
import { modalStack } from '../components/Modal/modal.store';
import ManageMediaModal from '../components/ManageMedia/ManageMediaModal.svelte';
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
import Button from '../Button.svelte';
import { playerState } from '../VideoPlayer/VideoPlayer';
import { modalStack } from '../Modal/modal.store';
import ManageMediaModal from '../ManageMedia/ManageMediaModal.svelte';
import { derived } from 'svelte/store';
import EpisodeCarousel from './EpisodeCarousel.svelte';
export let id: string;
const { promise: tmdbSeries, ...tmdbSeriesRest } = useRequest(tmdbApi.getTmdbSeries, Number(id));
const { promise: tmdbSeries, data: tmdbSeriesData } = useRequest(
tmdbApi.getTmdbSeries,
Number(id)
);
const { promise: sonarrItem, data: sonarrItemData } = useRequest(
sonarrApi.getSeriesByTmdbId,
Number(id)
@@ -42,7 +46,7 @@
</script>
<DetachedPage>
<div class="min-h-screen flex flex-col py-12 px-20 relative">
<div class="h-screen flex flex-col py-12 px-20 relative">
<HeroCarousel
urls={$tmdbSeries.then(
(series) =>
@@ -140,4 +144,5 @@
</div>
</HeroCarousel>
</div>
<EpisodeCarousel id={Number(id)} tmdbSeries={tmdbSeriesData} />
</DetachedPage>