mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 00:35:12 +02:00
Initial work on seasons section
This commit is contained in:
@@ -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'
|
||||
})}
|
||||
|
||||
@@ -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'
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
7
src/lib/components/Carousel/UICarousel.svelte
Normal file
7
src/lib/components/Carousel/UICarousel.svelte
Normal 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>
|
||||
55
src/lib/components/EpisodeCard/EpisodeCard.svelte
Normal file
55
src/lib/components/EpisodeCard/EpisodeCard.svelte
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
88
src/lib/components/ResourceDetails/SeasonsDetails.svelte
Normal file
88
src/lib/components/ResourceDetails/SeasonsDetails.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user