feat: Episode card frames
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
37
src/lib/components/Carousel/UICarousel.svelte
Normal file
37
src/lib/components/Carousel/UICarousel.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
28
src/lib/components/SeriesPage/EpisodeCard.svelte
Normal file
28
src/lib/components/SeriesPage/EpisodeCard.svelte
Normal 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>
|
||||
60
src/lib/components/SeriesPage/EpisodeCarousel.svelte
Normal file
60
src/lib/components/SeriesPage/EpisodeCarousel.svelte
Normal 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}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user