feat: Episode card frames
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
import LibraryPage from './lib/pages/LibraryPage.svelte';
|
import LibraryPage from './lib/pages/LibraryPage.svelte';
|
||||||
import ManagePage from './lib/pages/ManagePage.svelte';
|
import ManagePage from './lib/pages/ManagePage.svelte';
|
||||||
import SearchPage from './lib/pages/SearchPage.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 Sidebar from './lib/components/Sidebar/Sidebar.svelte';
|
||||||
import LoginPage from './lib/pages/LoginPage.svelte';
|
import LoginPage from './lib/pages/LoginPage.svelte';
|
||||||
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
|
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { type NavigationActions, type RevealStrategy, Selectable } from './lib/selectable';
|
import { type NavigationActions, type RevealStrategy, Selectable } from './lib/selectable';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export let element: HTMLElement;
|
||||||
|
|
||||||
export let name: string = '';
|
export let name: string = '';
|
||||||
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
|
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
|
||||||
export let gridCols: number = 0;
|
export let gridCols: number = 0;
|
||||||
@@ -10,7 +12,7 @@
|
|||||||
export let canFocusEmpty = true;
|
export let canFocusEmpty = true;
|
||||||
export let trapFocus = false;
|
export let trapFocus = false;
|
||||||
export let debugOutline = 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 childrenRevealStrategy: RevealStrategy | undefined = undefined;
|
||||||
|
|
||||||
export let active = true;
|
export let active = true;
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
'outline-none': debugOutline === false
|
'outline-none': debugOutline === false
|
||||||
})}
|
})}
|
||||||
use:registerer
|
use:registerer
|
||||||
|
bind:this={element}
|
||||||
>
|
>
|
||||||
<slot hasFocus={$hasFocus} hasFocusWithin={$hasFocusWithin} focusIndex={$focusIndex} />
|
<slot hasFocus={$hasFocus} hasFocusWithin={$hasFocusWithin} focusIndex={$focusIndex} />
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type TmdbSeries2 =
|
|||||||
operations['tv-series-details']['responses']['200']['content']['application/json'];
|
operations['tv-series-details']['responses']['200']['content']['application/json'];
|
||||||
export type TmdbSeason =
|
export type TmdbSeason =
|
||||||
operations['tv-season-details']['responses']['200']['content']['application/json'];
|
operations['tv-season-details']['responses']['200']['content']['application/json'];
|
||||||
|
export type TmdbEpisode = NonNullable<TmdbSeason['episodes']>[0];
|
||||||
export type TmdbPerson =
|
export type TmdbPerson =
|
||||||
operations['person-details']['responses']['200']['content']['application/json'];
|
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
|
focusOnMount
|
||||||
trapFocus
|
trapFocus
|
||||||
class="fixed inset-0 z-20 bg-stone-950"
|
class="fixed inset-0 z-20 bg-stone-950 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Container>
|
</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">
|
<script lang="ts">
|
||||||
import Container from '../../Container.svelte';
|
import Container from '../../../Container.svelte';
|
||||||
import SidebarMargin from '../components/SidebarMargin.svelte';
|
import SidebarMargin from '../SidebarMargin.svelte';
|
||||||
import HeroCarousel from '../components/HeroCarousel/HeroCarousel.svelte';
|
import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte';
|
||||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
import DetachedPage from '../DetachedPage/DetachedPage.svelte';
|
||||||
import { useActionRequest, useDependantRequest, useRequest } from '../stores/data.store';
|
import { useActionRequest, useDependantRequest, useRequest } from '../../stores/data.store';
|
||||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
||||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DotFilled, Download, ExternalLink, File, Play, Plus } from 'radix-icons-svelte';
|
import { DotFilled, Download, ExternalLink, File, Play, Plus } from 'radix-icons-svelte';
|
||||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
|
||||||
import { sonarrApi } from '../apis/sonarr/sonarr-api';
|
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../Button.svelte';
|
||||||
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||||
import { modalStack } from '../components/Modal/modal.store';
|
import { modalStack } from '../Modal/modal.store';
|
||||||
import ManageMediaModal from '../components/ManageMedia/ManageMediaModal.svelte';
|
import ManageMediaModal from '../ManageMedia/ManageMediaModal.svelte';
|
||||||
import { derived } from 'svelte/store';
|
import { derived } from 'svelte/store';
|
||||||
|
import EpisodeCarousel from './EpisodeCarousel.svelte';
|
||||||
|
|
||||||
export let id: string;
|
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(
|
const { promise: sonarrItem, data: sonarrItemData } = useRequest(
|
||||||
sonarrApi.getSeriesByTmdbId,
|
sonarrApi.getSeriesByTmdbId,
|
||||||
Number(id)
|
Number(id)
|
||||||
@@ -42,7 +46,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetachedPage>
|
<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
|
<HeroCarousel
|
||||||
urls={$tmdbSeries.then(
|
urls={$tmdbSeries.then(
|
||||||
(series) =>
|
(series) =>
|
||||||
@@ -140,4 +144,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</HeroCarousel>
|
</HeroCarousel>
|
||||||
</div>
|
</div>
|
||||||
|
<EpisodeCarousel id={Number(id)} tmdbSeries={tmdbSeriesData} />
|
||||||
</DetachedPage>
|
</DetachedPage>
|
||||||
Reference in New Issue
Block a user