Initial work on seasons section

This commit is contained in:
Aleksi Lassila
2023-07-12 01:37:17 +03:00
parent e544eff886
commit ea6a42d8e2
14 changed files with 198 additions and 72 deletions

View File

@@ -16,7 +16,7 @@
export let available = true;
export let progress = 0;
export let size: 'dynamic' | 'normal' | 'large' = 'normal';
export let size: 'dynamic' | 'md' | 'large' = 'md';
export let randomProgress = false;
if (randomProgress) {
progress = Math.random() > 0.3 ? Math.random() * 100 : 0;
@@ -25,7 +25,7 @@
<div
class={classNames('rounded overflow-hidden relative shadow-2xl shrink-0 aspect-video', {
'h-40': size === 'normal',
'h-40': size === 'md',
'h-60': size === 'large',
'w-full': size === 'dynamic'
})}

View File

@@ -2,12 +2,12 @@
import classNames from 'classnames';
export let index = 0;
export let type: 'dynamic' | 'normal' | 'large' = 'normal';
export let type: 'dynamic' | 'md' | 'large' = 'md';
</script>
<div
class={classNames('rounded overflow-hidden shadow-2xl placeholder shrink-0 aspect-video', {
'h-40': type === 'normal',
'h-40': type === 'md',
'h-60': type === 'large',
'w-full': type === 'dynamic'
})}

View File

@@ -3,35 +3,40 @@
import IconButton from '../IconButton.svelte';
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
let carousel;
let scrollX;
let carousel: HTMLDivElement | undefined;
let scrollX: number;
</script>
<div class="flex justify-between items-center mx-8">
<slot name="title" />
<div class="flex gap-2">
<IconButton>
<ChevronLeft size="20" />
<ChevronLeft size={20} />
</IconButton>
<IconButton>
<ChevronRight size="20" />
<ChevronRight size={20} />
</IconButton>
</div>
</div>
<div class="relative">
<div
class="flex overflow-x-scroll items-center overflow-y-hidden gap-4 relative pl-8 scrollbar-hide py-4"
class="flex overflow-x-scroll items-center overflow-y-hidden gap-4 relative px-8 scrollbar-hide py-4"
bind:this={carousel}
on:scroll={() => (scrollX = carousel.scrollLeft)}
on:scroll={() => (scrollX = carousel?.scrollLeft || scrollX)}
>
<slot />
</div>
{#if scrollX > 0}
{#if scrollX > 50}
<div
transition:fade={{ duration: 200 }}
class="absolute inset-y-4 left-0 w-24 bg-gradient-to-r from-darken"
/>
{/if}
<div class="absolute inset-y-4 right-0 w-24 bg-gradient-to-l from-darken" />
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
<div
transition:fade={{ duration: 200 }}
class="absolute inset-y-4 right-0 w-24 bg-gradient-to-l from-darken"
/>
{/if}
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
export let type: 'dynamic' | 'normal' | 'large' = 'normal';
export let type: 'dynamic' | 'md' | 'large' = 'md';
</script>
{#each Array(10) as _, i (i)}

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import classNames from 'classnames';
</script>
<div class={classNames($$restProps.class, 'overflow-x-scroll scrollbar-hide')}>
<slot />
</div>

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { TMDB_IMAGES } from '$lib/constants';
import { TriangleRight } from 'radix-icons-svelte';
import IconButton from '../IconButton.svelte';
import classNames from 'classnames';
export let backdropPath: string;
export let title: string;
export let subtitle: string;
export let episodeTag: string | undefined = undefined;
export let runtime: number;
export let size: 'md' | 'dynamic' = 'md';
</script>
<div
class={classNames(
'aspect-video bg-center bg-cover bg-no-repeat rounded-lg overflow-hidden transition-all cursor-pointer group shadow-lg relative',
{
'h-40': size === 'md',
'h-full': size === 'dynamic'
}
)}
style={"background-image: url('" + TMDB_IMAGES + backdropPath + "');"}
>
<div
class="opacity-100 group-hover:opacity-0 flex flex-col justify-between p-2 lg:p-3 lg:px-3 bg-darken h-full transition-opacity"
>
<div class="flex justify-between items-center text-xs lg:text-sm font-medium text-zinc-300">
<div>
<slot name="episode-tag">
{episodeTag}
</slot>
</div>
<div>
{runtime} min
</div>
</div>
<div>
<div class="text-xs lg:text-sm text-zinc-300 font-medium tracking-wide">{subtitle}</div>
<div class="font-semibold lg:text-lg">
{title}
</div>
</div>
</div>
<div class="absolute inset-0 flex items-center justify-center">
<div
class="backdrop-blur-lg rounded-full p-1 bg-[#00000044] opacity-0 group-hover:opacity-100 transition-opacity"
>
<IconButton>
<TriangleRight size={30} />
</IconButton>
</div>
</div>
</div>

View File

@@ -70,7 +70,7 @@
{#each results.filter((m) => m).slice(0, 5) as result}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex px-4 py-2 gap-4 hover:bg-highlight-dim cursor-pointer"
class="flex px-4 py-2 gap-4 hover:bg-lighten cursor-pointer"
on:click={() => (window.location.href = '/movie/' + result.id)}
>
<div

View File

@@ -67,7 +67,7 @@
{#each showAllReleases ? data.allReleases : data.filtered as release}
<div>
<div
class="flex px-4 py-2 gap-4 hover:bg-highlight-dim items-center justify-between cursor-pointer text-sm"
class="flex px-4 py-2 gap-4 hover:bg-lighten items-center justify-between cursor-pointer text-sm"
on:click={() => toggleShowDetails(release.guid)}
>
<div class="flex gap-4">

View File

@@ -6,12 +6,14 @@
import { formatMinutesToTime } from '$lib/utils';
import classNames from 'classnames';
import { ChevronDown, Clock, Play, TriangleRight } from 'radix-icons-svelte';
import { getContext } from 'svelte';
import { getContext, type ComponentProps } from 'svelte';
import { fade, fly } from 'svelte/transition';
import HeightHider from '../HeightHider.svelte';
import type { PlayerState } from '../VideoPlayer/VideoPlayer';
import LibraryDetails from './LibraryDetails.svelte';
import IconButton from '../IconButton.svelte';
import SeasonsDetails from './SeasonsDetails.svelte';
import EpisodeCard from '../EpisodeCard/EpisodeCard.svelte';
export let tmdbId: number;
export let type: 'movie' | 'tv';
@@ -30,9 +32,7 @@
export let tmdbRating: number;
export let starring: CastMember[];
export let lastEpisode:
| { backdropPath: string; name: string; episodeTag: string; runtime: number }
| undefined = undefined;
export let lastEpisode: ComponentProps<EpisodeCard> | undefined = undefined;
export let videos: Video[];
export let showDetails = false;
@@ -54,20 +54,6 @@
// Transitions
const duration = 200;
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
const { playerState, close, streamJellyfinId } = getContext<PlayerState>('player');
function openTrailer() {
@@ -157,7 +143,7 @@
/>
<div
class={classNames(
'h-full w-full px-8 xl:px-16 pb-8 pt-32',
'h-full w-full px-8 lg:px-16 pb-8 pt-32',
'grid grid-cols-[1fr_max-content] grid-rows-[1fr_min-content] gap-x-16 gap-y-8 relative z-[2]',
'transition-colors',
{
@@ -179,7 +165,7 @@
{/if}
{:else if releaseDate}
<span class="font-bold uppercase tracking-wider"
>{monthNames[releaseDate.getMonth()]}</span
>{releaseDate.toLocaleString('en', { month: 'long' })}</span
>
{releaseDate.getFullYear()}
{/if}
@@ -243,7 +229,7 @@
class="flex flex-col gap-6 justify-between 2xl:w-96 xl:w-80 lg:w-64 w-52 row-span-full"
style={opacityStyle}
>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-6 self-end">
<h3 class="text-xs tracking-wide uppercase" in:fade={getFade()}>Details</h3>
<div class="flex flex-col gap-1 text-sm tracking-widest font-extralight">
<div in:fade={getFade()}>
@@ -303,39 +289,8 @@
{/if}
</div>
{#if lastEpisode}
<div
class="aspect-video bg-center bg-cover bg-no-repeat rounded overflow-hidden transition-all cursor-pointer group shadow-lg relative"
style={"background-image: url('" + TMDB_IMAGES + lastEpisode.backdropPath + "');"}
>
<div
class="opacity-100 group-hover:opacity-0 flex flex-col justify-between p-2 xl:p-3 xl:px-3 bg-darken h-full transition-opacity"
>
<div class="flex justify-between items-center">
<div class="text-xs xl:text-sm font-medium text-zinc-300 uppercase">
{lastEpisode.episodeTag}
</div>
<div class="text-xs xl:text-sm font-medium text-zinc-300">
{lastEpisode.runtime} min
</div>
</div>
<div>
<div class="text-xs xl:text-sm text-zinc-300 font-medium tracking-wide">
Next Episode
</div>
<div class="font-semibold xl:text-lg">
{lastEpisode.name}
</div>
</div>
</div>
<div class="absolute inset-0 flex items-center justify-center">
<div
class="backdrop-blur-lg rounded-full p-1 bg-[#00000044] opacity-0 group-hover:opacity-100 transition-opacity"
>
<IconButton>
<TriangleRight size={30} />
</IconButton>
</div>
</div>
<div class="w-full aspect-video">
<EpisodeCard size="dynamic" {...lastEpisode} />
</div>
{/if}
</div>
@@ -346,6 +301,11 @@
</div>
<HeightHider duration={1000} visible={detailsVisible}>
{#key tmdbId}
{#if type === 'tv' && seasons > 0}
<SeasonsDetails {tmdbId} totalSeasons={seasons} />
{/if}
{/key}
<div bind:this={localDetailsTop} />
{#key tmdbId}
<LibraryDetails

View File

@@ -0,0 +1,88 @@
<script lang="ts">
import { getTmdbSeriesSeason } from '$lib/apis/tmdb/tmdbApi';
import classNames from 'classnames';
import EpisodeCard from '../EpisodeCard/EpisodeCard.svelte';
import Carousel from '../Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../Carousel/CarouselPlaceholderItems.svelte';
import { Star, StarFilled } from 'radix-icons-svelte';
import UiCarousel from '../Carousel/UICarousel.svelte';
export let tmdbId: number;
export let totalSeasons: number;
let visibleSeason = 1;
async function fetchSeasons(seasons: number) {
const promises = [...Array(seasons).keys()].map((i) => getTmdbSeriesSeason(tmdbId, i + 1));
console.log(promises);
return Promise.all(promises);
}
</script>
<div class="py-4">
{#await fetchSeasons(totalSeasons)}
<Carousel>
<div slot="title" class="flex gap-4 my-1">
{#each [...Array(3).keys()] as season}
<div class={'rounded-full p-2 px-6 font-medium placeholder text-transparent'}>
Season 1
</div>
{/each}
</div>
<CarouselPlaceholderItems />
</Carousel>
{:then seasons}
<div class="flex flex-col gap-4">
<div>
{#each seasons as season}
<div
class={classNames({
hidden: visibleSeason !== (season?.season_number || 1)
})}
>
<Carousel>
<UiCarousel slot="title" class="flex gap-4 my-1">
{#each seasons as season}
<button
class={classNames('rounded-full p-2 px-6 font-medium whitespace-nowrap ', {
'text-amber-200 bg-darken shadow-lg':
visibleSeason === (season?.season_number || 1),
'text-zinc-300 hover:bg-lighten hover:text-amber-100':
visibleSeason !== (season?.season_number || 1)
})}
on:click={() => (visibleSeason = season?.season_number || 1)}
>{season?.name}</button
>
{/each}
</UiCarousel>
{#each season?.episodes || [] as episode}
{@const upcoming =
new Date(episode.air_date || Date.now()) > new Date() || episode.runtime === null}
<div class="flex-shrink-0 h-40 lg:h-48">
<EpisodeCard
backdropPath={episode.still_path || ''}
title={episode.name || ''}
subtitle={upcoming ? 'Upcoming' : 'Episode ' + episode.episode_number}
runtime={episode.runtime || 0}
size="dynamic"
>
<div slot="episode-tag" class="flex gap-1 items-center">
{#if upcoming}
{@const date = new Date(episode.air_date || Date.now())}
<span />
{:else}
{episode.vote_average?.toFixed(1)}
<StarFilled size={14} />
{/if}
</div>
</EpisodeCard>
</div>
{/each}
</Carousel>
</div>
{/each}
</div>
</div>
{/await}
</div>

View File

@@ -10,7 +10,7 @@
</p>
<ul class="flex flex-col gap-1">
{#each Object.keys(missingEnvironmentVariables).filter((k) => missingEnvironmentVariables[k]) as variableName}
<code class="bg-highlight-dim p-0.5 px-2 rounded self-start">{variableName}</code>
<code class="bg-lighten p-0.5 px-2 rounded self-start">{variableName}</code>
{/each}
</ul>
</div>