style: Major visual overhaul with various improvements and fixes

This commit is contained in:
Aleksi Lassila
2024-04-15 17:17:01 +03:00
parent d3a47555fd
commit f519fb7447
24 changed files with 853 additions and 599 deletions

View File

@@ -3,8 +3,8 @@
import { Route, Router } from 'svelte-navigator';
import { handleKeyboardNavigation } from './lib/selectable';
import Container from './Container.svelte';
import BrowseSeriesPage from './lib/pages/BrowseSeriesPage.svelte';
import MoviesPage from './lib/pages/MoviesPage.svelte';
import BrowseSeriesPage from './lib/pages/SeriesHomePage.svelte';
import MoviesPage from './lib/pages/MoviesHomePage.svelte';
import LibraryPage from './lib/pages/LibraryPage.svelte';
import ManagePage from './lib/pages/ManagePage.svelte';
import SearchPage from './lib/pages/SearchPage.svelte';
@@ -16,12 +16,29 @@
import ModalStack from './lib/components/Modal/ModalStack.svelte';
import PageNotFound from './lib/pages/PageNotFound.svelte';
import NavigationDebugger from './lib/components/NavigationDebugger.svelte';
import { onMount } from 'svelte';
import { isTizen } from './lib/utils/browser-detection';
appState.subscribe((s) => console.log('appState', s));
// onMount(() => {
// if (isTizen()) {
// var myMediaKeyChangeListener = {
// onpressed: function (key) {
// console.log('Pressed key: ' + key);
// },
// onreleased: function (key) {
// console.log('Released key: ' + key);
// }
// };
//
// tizen.mediakey.setMediaKeyEventListener(myMediaKeyChangeListener);
// }
// });
</script>
<I18n />
<Container class="w-full h-full overflow-auto bg-stone-950 text-white">
<Container class="w-full h-full overflow-auto text-white">
{#if $appState.user === undefined}
<div class="h-full w-full flex flex-col items-center justify-center">
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
@@ -34,7 +51,7 @@
<LoginPage />
{:else}
<Router>
<Container class="flex flex-col" direction="horizontal" trapFocus>
<Container class="flex flex-col relative" direction="horizontal" trapFocus>
<Sidebar />
<Route path="/">
<BrowseSeriesPage />

View File

@@ -27,7 +27,7 @@ a {
/*}*/
/*.selectable, .selectable-offset {*/
/* @apply outline outline-0 outline-highlight-foreground*/
/* @apply outline outline-0 outline-primary-500*/
/*}*/
/*.selectable {*/
@@ -39,19 +39,23 @@ a {
/*}*/
html:not([data-useragent*="Tizen"]) .selectable {
@apply outline-none outline-0 border-2 border-[#00000000] focus-visible:border-highlight-foreground;
@apply focus-visible:border-primary-500;
}
html[data-useragent*="Tizen"] .selectable {
@apply outline-none outline-0 border-2 border-[#00000000] focus-within:border-highlight-foreground;
@apply focus-within:border-primary-500;
}
.selectable {
@apply outline-none outline-0 border-2 border-[#00000000] transition-colors hover:border-primary-500;
}
.selectable:focus, .selectable:focus-within {
border-width: 2px;
border-width: 2px;
}
.selected {
@apply outline-none outline-0 border-2 border-highlight-foreground;
@apply outline-none outline-0 border-2 border-primary-500;
}
.unselected {

View File

@@ -54,6 +54,20 @@ export class JellyfinApi implements Api<paths> {
})
.then((r) => r.data?.Items || []);
getContinueWatchingSeries = async () => {
const seriesIds = [
...new Set(
await this.getContinueWatching('series')
.then((items) => items?.map((i) => i.SeriesId) || [])
.then((ids) => ids.filter((i) => !!i) as string[])
)
];
return Promise.all(seriesIds.map((id) => this.getLibraryItem(id))).then((is) =>
is.filter((i): i is JellyfinItem => !!i)
);
};
jellyfinItemsCache: JellyfinItem[] = [];
async getLibraryItems(refreshCache = false) {
if (refreshCache || !this.jellyfinItemsCache.length) {

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import classNames from 'classnames';
export let hasFocus: boolean;
</script>
<div
class={classNames(
'relative transition-all',
{
'scale-105': hasFocus
},
$$restProps.class
)}
style="transition: transform 200ms;"
{...$$restProps}
>
<slot />
</div>

View File

@@ -2,44 +2,48 @@
import Container from '../../Container.svelte';
import type { Readable } from 'svelte/store';
import classNames from 'classnames';
import AnimatedSelection from './AnimateScale.svelte';
export let inactive: boolean = false;
export let focusOnMount: boolean = false;
let hasFoucus: Readable<boolean>;
let hasFocus: Readable<boolean>;
</script>
<Container
bind:hasFocus={hasFoucus}
class={classNames(
'px-6 py-2 rounded-lg font-medium tracking-wide flex items-center',
{
'bg-highlight-foreground text-stone-900': $hasFoucus,
'hover:bg-highlight-foreground hover:text-stone-900': true,
'bg-highlight-background': !$hasFoucus,
'cursor-pointer': !inactive,
'cursor-not-allowed pointer-events-none opacity-40': inactive
},
$$restProps.class
)}
on:click
on:select
on:clickOrSelect
on:enter
let:hasFocus
{focusOnMount}
>
{#if $$slots.icon}
<div class="mr-2">
<slot name="icon" />
<AnimatedSelection hasFocus={$hasFocus}>
<Container
bind:hasFocus
class={classNames(
'px-6 py-2 rounded-lg font-medium tracking-wide flex items-center',
{
// 'bg-primary-500 text-secondary-700': $hasFocus,
// 'bg-secondary-700': !$hasFocus,
// 'hover:bg-primary-500 hover:text-secondary-700': true,
'bg-secondary-700 selectable': true,
'cursor-pointer': !inactive,
'cursor-not-allowed pointer-events-none opacity-40': inactive
},
$$restProps.class
)}
on:click
on:select
on:clickOrSelect
on:enter
let:hasFocus
{focusOnMount}
>
{#if $$slots.icon}
<div class="mr-2">
<slot name="icon" />
</div>
{/if}
<div class="flex-1 text-center">
<slot {hasFocus} />
</div>
{/if}
<div class="flex-1 text-center">
<slot {hasFocus} />
</div>
{#if $$slots['icon-after']}
<div class="ml-2">
<slot name="icon-after" />
</div>
{/if}
</Container>
{#if $$slots['icon-after']}
<div class="ml-2">
<slot name="icon-after" />
</div>
{/if}
</Container>
</AnimatedSelection>

View File

@@ -7,7 +7,8 @@
import type { TitleType } from '../../types';
import Container from '../../../Container.svelte';
import { useNavigate } from 'svelte-navigator';
import { scrollIntoView } from '../../selectable';
import type { Readable } from 'svelte/store';
import AnimatedSelection from '../AnimateScale.svelte';
export let tmdbId: number | undefined = undefined;
export let tvdbId: number | undefined = undefined;
@@ -26,97 +27,105 @@
export let orientation: 'portrait' | 'landscape' = 'landscape';
const navigate = useNavigate();
let hasFocus: Readable<boolean>;
</script>
<Container
active={focusable}
on:clickOrSelect={() => {
if (tmdbId || tvdbId) {
navigate(`${type}/${tmdbId || tvdbId}`);
}
}}
on:enter
class={classNames(
'relative flex rounded-xl selectable group hover:text-inherit flex-shrink-0 overflow-hidden text-left cursor-pointer',
{
'aspect-video': orientation === 'landscape',
'aspect-[2/3]': orientation === 'portrait',
'w-32 h-48': size === 'sm' && orientation === 'portrait',
'h-32 w-56': size === 'sm' && orientation === 'landscape',
'w-44 h-64': size === 'md' && orientation === 'portrait',
'h-44 w-80': size === 'md' && orientation === 'landscape',
'w-60 h-96': size === 'lg' && orientation === 'portrait',
'h-60 w-96': size === 'lg' && orientation === 'landscape',
'w-full': size === 'dynamic',
'shadow-lg': shadow
}
)}
>
<LazyImg src={backdropUrl} class="absolute inset-0 group-hover:scale-105 transition-transform" />
<div
class="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity bg-black"
style="filter: blur(50px); transform: scale(3);"
<AnimatedSelection hasFocus={$hasFocus}>
<Container
active={focusable}
on:clickOrSelect={() => {
if (tmdbId || tvdbId) {
navigate(`${type}/${tmdbId || tvdbId}`);
}
}}
on:enter
class={classNames(
'relative flex flex-shrink-0 rounded-xl group hover:text-inherit overflow-hidden text-left cursor-pointer',
'transition-transform selectable',
{
'aspect-video': orientation === 'landscape',
'aspect-[2/3]': orientation === 'portrait',
'w-32 h-48': size === 'sm' && orientation === 'portrait',
'h-32 w-56': size === 'sm' && orientation === 'landscape',
'w-44 h-64': size === 'md' && orientation === 'portrait',
'h-44 w-80': size === 'md' && orientation === 'landscape',
'w-60 h-96': size === 'lg' && orientation === 'portrait',
'h-60 w-96': size === 'lg' && orientation === 'landscape',
'w-full': size === 'dynamic',
'shadow-lg': shadow
}
)}
bind:hasFocus
>
<!-- This is the tinted and blurred hover overlay -->
<LazyImg src={backdropUrl} />
</div>
<!-- <div
<LazyImg
src={backdropUrl}
class="absolute inset-0 group-hover:scale-105 transition-transform"
/>
<div
class="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity bg-black"
style="filter: blur(50px); transform: scale(3);"
>
<!-- This is the tinted and blurred hover overlay -->
<LazyImg src={backdropUrl} />
</div>
<!-- <div
style={`background-image: url(${backdropUrl}); background-size: cover; background-position: center; filter: blur(50px); transform: scale(3);`}
class="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity bg-black"
/> -->
<div
class={classNames(
'flex-1 flex flex-col justify-between bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity z-[1]',
{
'py-2 px-3': true
}
)}
>
<div class="flex justify-self-start justify-between">
<slot name="top-left">
<div>
<h1 class="text-zinc-100 font-bold line-clamp-2 text-lg">{title}</h1>
<h2 class="text-zinc-300 text-sm font-medium line-clamp-2">{subtitle}</h2>
</div>
</slot>
<slot name="top-right">
<div />
</slot>
<div
class={classNames(
'flex-1 flex flex-col justify-between bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity z-[1]',
{
'py-2 px-3': true
}
)}
>
<div class="flex justify-self-start justify-between">
<slot name="top-left">
<div>
<h1 class="text-zinc-100 font-bold line-clamp-2 text-lg">{title}</h1>
<h2 class="text-zinc-300 text-sm font-medium line-clamp-2">{subtitle}</h2>
</div>
</slot>
<slot name="top-right">
<div />
</slot>
</div>
<div class="flex justify-self-end justify-between">
<slot name="bottom-left">
<div>
{#if rating}
<h2 class="flex items-center gap-1.5 text-sm text-zinc-300 font-medium">
<Star />{rating.toFixed(1)}
</h2>
{/if}
</div>
</slot>
<slot name="bottom-right">
<div />
</slot>
</div>
</div>
<div class="flex justify-self-end justify-between">
<slot name="bottom-left">
<div>
{#if rating}
<h2 class="flex items-center gap-1.5 text-sm text-zinc-300 font-medium">
<Star />{rating.toFixed(1)}
</h2>
{/if}
</div>
</slot>
<slot name="bottom-right">
<div />
</slot>
</div>
</div>
<!-- <div
<!-- <div
class="absolute inset-0 bg-gradient-to-t from-darken group-hover:opacity-0 transition-opacity z-[1]"
/> -->
{#if jellyfinId}
<div class="absolute inset-0 flex items-center justify-center z-[1]">
<PlayButton
on:click={(e) => {
e.preventDefault();
jellyfinId && true; //playerState.streamJellyfinId(jellyfinId);
}}
class="sm:opacity-0 group-hover:opacity-100 transition-opacity"
/>
</div>
{/if}
{#if progress}
<div
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 bg-gradient-to-t ease-in-out z-[1]"
>
<ProgressBar {progress} />
</div>
{/if}
</Container>
{#if jellyfinId}
<div class="absolute inset-0 flex items-center justify-center z-[1]">
<PlayButton
on:click={(e) => {
e.preventDefault();
jellyfinId && true; //playerState.streamJellyfinId(jellyfinId);
}}
class="sm:opacity-0 group-hover:opacity-100 transition-opacity"
/>
</div>
{/if}
{#if progress}
<div
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 bg-gradient-to-t ease-in-out z-[1]"
>
<ProgressBar {progress} />
</div>
{/if}
</Container>
</AnimatedSelection>

View File

@@ -5,7 +5,7 @@
import Container from '../../../Container.svelte';
import { PLATFORM_TV } from '../../constants';
export let gradientFromColor = 'from-stone-950';
export let gradientFromColor = 'from-secondary-500';
export let heading = '';
let carousel: HTMLDivElement | undefined;
@@ -45,7 +45,12 @@
</div>
<div class="relative">
<Container direction="horizontal" handleNavigateOut={{ left: () => true }} let:focusIndex>
<Container
direction="horizontal"
handleNavigateOut={{ left: () => true }}
let:focusIndex
on:enter
>
<div
class={classNames(
'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide',

View File

@@ -6,7 +6,7 @@
on:navigate={({ detail }) => {
if (
detail.direction === 'left' &&
detail.options.willLeaveContainer &&
detail.willLeaveContainer &&
detail.selectable === detail.options.target
) {
history.back();

View File

@@ -2,48 +2,59 @@
import Container from '../../../Container.svelte';
import classNames from 'classnames';
import { TriangleRight } from 'radix-icons-svelte';
import type { Readable } from 'svelte/store';
import AnimateScale from '../AnimateScale.svelte';
export let episodeNumber: number;
export let episodeName: string;
export let backdropUrl: string;
export let handlePlay: () => void = () => {};
let hasFocus: Readable<boolean>;
</script>
<Container
class={classNames(
'rounded-xl overflow-hidden cursor-pointer group',
'w-[420px] h-[236.25px] px-4 py-3',
'flex flex-col shrink-0 relative selectable'
)}
let:hasFocus
on:select={handlePlay}
on:enter
on:mount
focusOnClick
>
<div class="flex-1 flex flex-col justify-end z-10">
<h2 class="text-zinc-300 font-medium">Episode {episodeNumber}</h2>
<h1 class="text-zinc-100 text-lg font-medium line-clamp-2">{episodeName}</h1>
</div>
<div
class="absolute inset-0 bg-center bg-cover"
style={`background-image: url('${backdropUrl}')`}
/>
<div
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-transparent via-40% to-transparent"
/>
{#if handlePlay}
<div
class={classNames(
'opacity-0 group-hover:opacity-100 absolute inset-0 z-20 flex items-center justify-center',
{
'opacity-100': hasFocus
}
)}
>
<div class="rounded-full bg-stone-950/50 p-2.5 cursor-pointer" on:click={handlePlay}>
<TriangleRight size={32} />
</div>
<AnimateScale hasFocus={$hasFocus}>
<Container
class={classNames(
'w-[420px] h-[236.25px] ',
'flex flex-col shrink-0',
'overflow-hidden rounded-xl cursor-pointer group relative px-4 py-3 selectable'
)}
on:select={handlePlay}
on:enter
on:mount
bind:hasFocus
focusOnClick
>
<div class="flex-1 flex flex-col justify-end z-10">
<h2 class="text-zinc-300 font-medium">Episode {episodeNumber}</h2>
<h1 class="text-zinc-100 text-lg font-medium line-clamp-2">{episodeName}</h1>
</div>
{/if}
</Container>
<div
class="absolute inset-0 bg-center bg-cover"
style={`background-image: url('${backdropUrl}')`}
/>
<div
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-transparent via-40% to-transparent"
/>
{#if handlePlay}
<div
class={classNames(
'group-hover:opacity-100 absolute inset-0 z-20 flex items-center justify-center'
)}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames('rounded-full p-2.5 cursor-pointer', {
// 'bg-primary-500 text-black': hasFocus,
// 'bg-zinc-900/90 hover:bg-primary-500 hover:text-black': !hasFocus
'bg-zinc-900/90 hover:bg-primary-500 hover:text-black': true
})}
on:click={handlePlay}
>
<TriangleRight size={32} />
</div>
</div>
{/if}
</Container>
</AnimateScale>

View File

@@ -2,9 +2,11 @@
import { PLATFORM_TV } from '../../constants';
import classNames from 'classnames';
import { onDestroy } from 'svelte';
import { isFirefox } from '../../utils/browser-detection';
export let urls: Promise<string[]>;
export let index: number;
export let hasFocus = true;
let visibleIndex = -2;
let visibleIndexTimeout: ReturnType<typeof setTimeout>;
@@ -32,23 +34,25 @@
onDestroy(() => visibleIndexTimeout && clearTimeout(visibleIndexTimeout));
</script>
<div class="absolute inset-0">
{#if PLATFORM_TV}
<div class="fixed inset-0 -z-10">
{#if !isFirefox()}
{#await urls then urls}
{#each urls as url, i}
<div
class={classNames('absolute inset-0 bg-center bg-cover transition-opacity duration-500', {
class={classNames('absolute inset-0 bg-center bg-cover', {
'opacity-100': visibleIndex === i,
'opacity-0': visibleIndex !== i
'opacity-0': visibleIndex !== i,
'scale-125': !hasFocus
})}
style={`background-image: url('${url}');`}
style={`background-image: url('${url}'); transition: opacity 500ms, transform 500ms;`}
/>
{/each}
<div class="bg-gradient-to-t from-stone-950 to-transparent absolute inset-0" />
{/await}
{:else}
<div
class="flex overflow-hidden h-full w-full"
class={classNames('flex overflow-hidden h-full w-full transition-transform duration-500', {
'scale-125': !hasFocus
})}
style="perspective: 1px; -webkit-perspective: 1px;"
>
{#await urls then urls}
@@ -69,6 +73,9 @@
{/each}
{/await}
</div>
<div class="bg-gradient-to-t from-stone-950 to-transparent absolute inset-0" />
{/if}
</div>
<div class="absolute inset-0 flex flex-col -z-10">
<div class="h-screen bg-gradient-to-t from-secondary-500 to-transparent" />
<div class="flex-1 bg-secondary-500" />
</div>

View File

@@ -6,6 +6,7 @@
import { ChevronRight } from 'radix-icons-svelte';
import PageDots from '../HeroShowcase/PageDots.svelte';
import SidebarMargin from '../SidebarMargin.svelte';
import type { Readable, Writable } from 'svelte/store';
export let urls: Promise<string[]>;
@@ -37,22 +38,30 @@
index = i;
return true;
}
let hasFocusWithin: Readable<boolean>;
let focusIndex: Writable<number>;
$: backgroundHasFocus = $hasFocusWithin && $focusIndex === 0;
</script>
<Container class="flex-1 flex">
<HeroShowcaseBackground {urls} {index} />
<Container
on:navigate={({ detail }) => {
if (detail.options.direction === 'right') {
if (onNext()) detail.preventNavigation();
} else if (detail.options.direction === 'left') {
if (onPrevious()) detail.preventNavigation();
} else if (detail.options.direction === 'up') {
Selectable.giveFocus('left', false);
detail.preventNavigation();
}
}}
/>
<Container
class="flex-1 flex"
on:enter
on:navigate={({ detail }) => {
if (!backgroundHasFocus) return;
if (detail.options.direction === 'right') {
if (onNext()) detail.preventNavigation();
} else if (detail.options.direction === 'left') {
if (onPrevious()) detail.preventNavigation();
} else if (detail.options.direction === 'up') {
Selectable.giveFocus('left', false);
detail.preventNavigation();
}
}}
bind:hasFocusWithin
bind:focusIndex
>
<HeroShowcaseBackground {urls} {index} hasFocus={backgroundHasFocus} />
<div class="flex flex-1 z-10">
<slot />
<div class="flex flex-col justify-end ml-4">

View File

@@ -16,6 +16,7 @@
<HeroCarousel
urls={items.then((items) => items.map((i) => `${TMDB_IMAGES_ORIGINAL}${i.backdropUrl}`))}
bind:index={showcaseIndex}
on:enter
>
<div class="h-full flex-1 flex overflow-hidden z-10 relative">
{#await items}
@@ -30,13 +31,9 @@
{:then items}
{@const item = items[showcaseIndex]}
{#if item}
<div class="flex-1 flex items-end">
<div class="flex-1 flex items-end p-3">
<div class="mr-8">
<Card
focusable={false}
orientation="portrait"
backdropUrl={TMDB_POSTER_SMALL + item.posterUrl}
/>
<Card orientation="portrait" backdropUrl={TMDB_POSTER_SMALL + item.posterUrl} />
</div>
<div class="flex flex-col">
<div

View File

@@ -89,7 +89,7 @@
<Carousel
scrollClass="px-20"
class={classNames('transition-transform', {
'-translate-y-16': scrollTop < 140
'-translate-y-20': scrollTop < 140
})}
>
<svelte:fragment slot="title">
@@ -114,7 +114,7 @@
'px-3 py-1 cursor-pointer whitespace-nowrap text-xl tracking-wide font-medium rounded-lg',
'hover:font-semibold hover:tracking-wide hover:text-white',
{
'bg-highlight-foreground text-black': hasFocus,
'bg-primary-500 text-black': hasFocus,
//'bg-stone-800/50': hasFocus,
'text-zinc-400': !(focusIndex === i),
'text-white': focusIndex === i && !hasFocus
@@ -127,7 +127,7 @@
{/each}
</UICarousel>
</svelte:fragment>
<div class="flex -mx-2">
<div class="flex -mx-4">
{#each $tmdbSeasons as season}
{#each season?.episodes || [] as episode}
{@const jellyfinEpisodeId = $jellyfinEpisodes?.find(
@@ -135,10 +135,10 @@
i.IndexNumber === episode.episode_number &&
i.ParentIndexNumber === episode.season_number
)?.Id}
<div class="mx-2">
<div class="m-4">
<TmdbEpisodeCard
on:enter={(event) => {
scrollIntoView({ left: 64 + 16 })(event);
scrollIntoView({ horizontal: 64 + 32 })(event);
focusSeason(season);
selectedTmdbEpisode = episode;
}}

View File

@@ -51,151 +51,155 @@
<DetachedPage>
<ScrollHelper bind:scrollTop />
<Container
class="h-screen flex flex-col py-12 px-20 relative"
on:enter={scrollIntoView({ top: 0 })}
on:navigate={({ detail }) => {
if (detail.direction === 'down' && detail.willLeaveContainer) {
if (episodesSelectable?.focusChild(1)) detail.preventNavigation();
}
}}
>
<HeroCarousel
urls={$tmdbSeries.then(
(series) =>
series?.images.backdrops
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
?.map((i) => TMDB_IMAGES_ORIGINAL + i.file_path)
.slice(0, 5) || []
)}
<div class="relative">
<Container
class="h-screen flex flex-col py-12 px-20"
on:enter={scrollIntoView({ top: 0 })}
on:navigate={({ detail }) => {
if (detail.direction === 'down' && detail.willLeaveContainer) {
if (episodesSelectable?.focusChild(1)) detail.preventNavigation();
}
}}
>
<div class="h-full flex-1 flex flex-col justify-end">
{#await $tmdbSeries then series}
{#if showEpisodeInfo && selectedTmdbEpisode}
{@const episode = selectedTmdbEpisode}
<div
class={classNames(
'text-left font-medium tracking-wider text-stone-200 hover:text-amber-200 mt-2',
{
'text-4xl sm:text-5xl 2xl:text-6xl': episode.name?.length || 0 < 15,
'text-3xl sm:text-4xl 2xl:text-5xl': episode?.name?.length || 0 >= 15
}
)}
>
{episode.name}
</div>
<div
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2 text-lg"
>
<p class="flex-shrink-0">
{episode.runtime} Minutes
</p>
<!-- <DotFilled />
<HeroCarousel
urls={$tmdbSeries.then(
(series) =>
series?.images.backdrops
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
?.map((i) => TMDB_IMAGES_ORIGINAL + i.file_path)
.slice(0, 5) || []
)}
>
<Container />
<div class="h-full flex-1 flex flex-col justify-end">
{#await $tmdbSeries then series}
{#if showEpisodeInfo && selectedTmdbEpisode}
{@const episode = selectedTmdbEpisode}
<div
class={classNames(
'text-left font-medium tracking-wider text-stone-200 hover:text-amber-200 mt-2',
{
'text-4xl sm:text-5xl 2xl:text-6xl': episode.name?.length || 0 < 15,
'text-3xl sm:text-4xl 2xl:text-5xl': episode?.name?.length || 0 >= 15
}
)}
>
{episode.name}
</div>
<div
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2 text-lg"
>
<p class="flex-shrink-0">
{episode.runtime} Minutes
</p>
<!-- <DotFilled />
<p class="flex-shrink-0">{movie.runtime}</p> -->
<DotFilled />
<p class="flex-shrink-0">
<a href={`https://www.themoviedb.org/movie/${series?.id}/episode/${episode.id}`}
>{episode.vote_average?.toFixed(1)} TMDB</a
>
</p>
</div>
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4">
{episode.overview}
</div>
{:else if series}
<div
class={classNames(
'text-left font-medium tracking-wider text-stone-200 hover:text-amber-200 mt-2',
{
'text-4xl sm:text-5xl 2xl:text-6xl': series.name?.length || 0 < 15,
'text-3xl sm:text-4xl 2xl:text-5xl': series?.name?.length || 0 >= 15
}
)}
>
{series?.name}
</div>
<div
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2 text-lg"
>
<p class="flex-shrink-0">
{#if series.status !== 'Ended'}
Since {new Date(series.first_air_date || Date.now())?.getFullYear()}
{:else}
Ended {new Date(series.last_air_date || Date.now())?.getFullYear()}
{/if}
</p>
<!-- <DotFilled />
<DotFilled />
<p class="flex-shrink-0">
<a href={`https://www.themoviedb.org/movie/${series?.id}/episode/${episode.id}`}
>{episode.vote_average?.toFixed(1)} TMDB</a
>
</p>
</div>
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4">
{episode.overview}
</div>
{:else if series}
<div
class={classNames(
'text-left font-medium tracking-wider text-stone-200 hover:text-amber-200 mt-2',
{
'text-4xl sm:text-5xl 2xl:text-6xl': series.name?.length || 0 < 15,
'text-3xl sm:text-4xl 2xl:text-5xl': series?.name?.length || 0 >= 15
}
)}
>
{series?.name}
</div>
<div
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2 text-lg"
>
<p class="flex-shrink-0">
{#if series.status !== 'Ended'}
Since {new Date(series.first_air_date || Date.now())?.getFullYear()}
{:else}
Ended {new Date(series.last_air_date || Date.now())?.getFullYear()}
{/if}
</p>
<!-- <DotFilled />
<p class="flex-shrink-0">{movie.runtime}</p> -->
<DotFilled />
<p class="flex-shrink-0">
<a href={'https://www.themoviedb.org/movie/' + series.id}
>{series.vote_average} TMDB</a
<DotFilled />
<p class="flex-shrink-0">
<a href={'https://www.themoviedb.org/movie/' + series.id}
>{series.vote_average} TMDB</a
>
</p>
</div>
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4">
{series.overview}
</div>
{/if}
{/await}
{#await Promise.all([$jellyfinItem, $sonarrItem]) then [jellyfinItem, sonarrItem]}
<Container direction="horizontal" class="flex mt-8" focusOnMount>
{#if $nextJellyfinEpisode}
<Button
class="mr-2"
on:clickOrSelect={() =>
$nextJellyfinEpisode?.Id &&
playerState.streamJellyfinId($nextJellyfinEpisode.Id)}
>
</p>
</div>
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4">
{series.overview}
</div>
{/if}
{/await}
{#await Promise.all([$jellyfinItem, $sonarrItem]) then [jellyfinItem, sonarrItem]}
<Container direction="horizontal" class="flex mt-8" focusOnMount>
{#if $nextJellyfinEpisode}
<Button
class="mr-2"
on:clickOrSelect={() =>
$nextJellyfinEpisode?.Id && playerState.streamJellyfinId($nextJellyfinEpisode.Id)}
>
Play Season {$nextJellyfinEpisode?.ParentIndexNumber} Episode
{$nextJellyfinEpisode?.IndexNumber}
<Play size={19} slot="icon" />
</Button>
{/if}
{#if sonarrItem}
<Button
class="mr-2"
on:clickOrSelect={() =>
modalStack.create(SonarrMediaMangerModal, { id: sonarrItem.id || -1 })}
>
{#if jellyfinItem}
Manage Files
{:else}
Request
{/if}
<svelte:component this={jellyfinItem ? File : Download} size={19} slot="icon" />
</Button>
{:else}
<Button
class="mr-2"
on:clickOrSelect={() => addSeriesToSonarr(Number(id))}
inactive={$addSeriesToSonarrFetching}
>
Add to Sonarr
<Plus slot="icon" size={19} />
</Button>
{/if}
{#if PLATFORM_WEB}
<Button class="mr-2">
Open In TMDB
<ExternalLink size={19} slot="icon-after" />
</Button>
<Button class="mr-2">
Open In Jellyfin
<ExternalLink size={19} slot="icon-after" />
</Button>
{/if}
</Container>
{/await}
</div>
</HeroCarousel>
</Container>
<Container on:enter={scrollIntoView({ vertical: 64 })} bind:container={episodesSelectable}>
<EpisodeCarousel
id={Number(id)}
tmdbSeries={tmdbSeriesData}
{jellyfinEpisodes}
{nextJellyfinEpisode}
bind:selectedTmdbEpisode
/>
</Container>
Play Season {$nextJellyfinEpisode?.ParentIndexNumber} Episode
{$nextJellyfinEpisode?.IndexNumber}
<Play size={19} slot="icon" />
</Button>
{/if}
{#if sonarrItem}
<Button
class="mr-2"
on:clickOrSelect={() =>
modalStack.create(SonarrMediaMangerModal, { id: sonarrItem.id || -1 })}
>
{#if jellyfinItem}
Manage Files
{:else}
Request
{/if}
<svelte:component this={jellyfinItem ? File : Download} size={19} slot="icon" />
</Button>
{:else}
<Button
class="mr-2"
on:clickOrSelect={() => addSeriesToSonarr(Number(id))}
inactive={$addSeriesToSonarrFetching}
>
Add to Sonarr
<Plus slot="icon" size={19} />
</Button>
{/if}
{#if PLATFORM_WEB}
<Button class="mr-2">
Open In TMDB
<ExternalLink size={19} slot="icon-after" />
</Button>
<Button class="mr-2">
Open In Jellyfin
<ExternalLink size={19} slot="icon-after" />
</Button>
{/if}
</Container>
{/await}
</div>
</HeroCarousel>
</Container>
<Container on:enter={scrollIntoView({ vertical: 64 })} bind:container={episodesSelectable}>
<EpisodeCarousel
id={Number(id)}
tmdbSeries={tmdbSeriesData}
{jellyfinEpisodes}
{nextJellyfinEpisode}
bind:selectedTmdbEpisode
/>
</Container>
</div>
</DetachedPage>

View File

@@ -1,135 +1,284 @@
<script lang="ts">
import { Bookmark, CardStack, Gear, Laptop, MagnifyingGlass } from 'radix-icons-svelte';
import classNames from 'classnames';
import type { Readable, Writable } from 'svelte/store';
import { type Readable, writable, type Writable } from 'svelte/store';
import Container from '../../../Container.svelte';
import { useNavigate } from 'svelte-navigator';
let isNavBarOpen: Readable<boolean>;
let focusIndex: Writable<number>;
import type { Selectable } from '../../selectable';
const navigate = useNavigate();
let selectedIndex = 0;
let isNavBarOpen: Readable<boolean>;
let focusIndex: Writable<number> = writable(0);
let selectable: Selectable;
focusIndex.subscribe((v) => (selectedIndex = v));
const itemContainer = (index: number, _focusIndex: number) =>
classNames('h-12 flex items-center cursor-pointer', {
'text-amber-300': _focusIndex === index,
'text-primary-500': _focusIndex === index,
'text-stone-300': _focusIndex !== index
});
const selectIndex = (index: number) => () => {
selectable.focusChild(index);
const path =
{
0: '/',
1: 'movies',
2: 'library',
3: 'search',
4: 'manage'
}[index] || '/';
navigate(path);
selectedIndex = index;
};
</script>
<Container
class={classNames('flex items-stretch fixed z-20 left-0 inset-y-0 group', 'py-4 w-16', {
//'max-w-[64px]': !$isNavBarOpen,
//'max-w-64': $isNavBarOpen
})}
class={classNames(
'flex flex-col items-stretch fixed z-20 left-0 inset-y-0 group',
'py-4 w-16 select-none',
{
//'max-w-[64px]': !$isNavBarOpen,
//'max-w-64': $isNavBarOpen
}
)}
bind:hasFocusWithin={isNavBarOpen}
bind:focusIndex
bind:container={selectable}
>
<!-- <div>-->
<!-- <Link to="" class="rounded-sm flex items-center">-->
<!-- <div class="rounded-full bg-amber-300 h-4 w-4 mr-2" />-->
<!-- <h1 class="font-display uppercase font-semibold tracking-wider text-xl">Reiverr</h1>-->
<!-- </Link>-->
<!-- </div>-->
<!-- Background -->
<div
class={classNames(
'absolute inset-y-0 left-0 w-[25vw] transition-opacity bg-gradient-to-r from-secondary-500 to-transparent',
{
'opacity-0': !$isNavBarOpen,
'group-hover:opacity-100 pointer-events-none': true
}
)}
/>
<!-- Keep group hovered and sidebar open for width of this -->
<div
class={classNames('absolute inset-y-0 left-0 w-48 ', {
'pointer-events-none': !$isNavBarOpen,
'group-hover:pointer-events-auto': true
})}
/>
<div class={'flex flex-col flex-1 relative z-20 items-center'}>
<div class={'flex flex-col flex-1 justify-center self-stretch'}>
<Container
class={classNames(itemContainer(0, $focusIndex), 'w-full flex justify-center')}
on:clickOrSelect={() => navigate('/')}
<div class={'flex-1 flex flex-col justify-center self-stretch'}>
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(0)} let:hasFocus>
<div
class={classNames('w-full h-full relative flex items-center justify-center', {
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 0),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 0)
})}
>
<Laptop class="w-8 h-8" />
</Container>
<Container
class={classNames(itemContainer(1, $focusIndex), 'w-full flex justify-center')}
on:clickOrSelect={() => navigate('movies')}
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
}
)}
>
Series
</span>
</div>
</Container>
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(1)} let:hasFocus>
<div
class={classNames('w-full h-full relative flex items-center justify-center', {
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 1),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 1)
})}
>
<CardStack class="w-8 h-8" />
</Container>
<Container
class={classNames(itemContainer(2, $focusIndex), 'w-full flex justify-center')}
on:clickOrSelect={() => navigate('library')}
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
}
)}
>
Movies
</span>
</div>
</Container>
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(2)} let:hasFocus>
<div
class={classNames('w-full h-full relative flex items-center justify-center', {
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 2),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 2)
})}
>
<Bookmark class="w-8 h-8" />
</Container>
<Container
class={classNames(itemContainer(3, $focusIndex), 'w-full flex justify-center')}
on:clickOrSelect={() => navigate('search')}
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
}
)}
>
Library
</span>
</div>
</Container>
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(3)} let:hasFocus>
<div
class={classNames('w-full h-full relative flex items-center justify-center', {
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 3),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 3)
})}
>
<MagnifyingGlass class="w-8 h-8" />
</Container>
</div>
<Container
class={classNames(itemContainer(4, $focusIndex), 'w-full flex justify-center')}
on:clickOrSelect={() => navigate('manage')}
>
<Gear class="w-8 h-8" />
<span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
}
)}
>
Search
</span>
</div>
</Container>
</div>
<div
class={classNames(
'absolute inset-y-0 left-0 pl-[64px] pr-10 z-10 transition-all bg-stone-900/90',
'flex flex-col flex-1 p-4',
{
// 'translate-x-full opacity-100': $isNavBarOpen,
'-translate-x-full opacity-0': !$isNavBarOpen,
'group-hover:translate-x-0 group-hover:opacity-100': true
}
)}
>
<div class="flex flex-col flex-1 justify-center">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={itemContainer(0, $focusIndex)} on:click={() => navigate('/')}>
<span
class={classNames('text-xl transition-opacity font-medium', {
// 'opacity-0': $isNavBarOpen === false
})}
>
Series</span
>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={itemContainer(1, $focusIndex)} on:click={() => navigate('movies')}>
<span
class={classNames('text-xl transition-opacity font-medium', {
// 'opacity-0': $isNavBarOpen === false
})}
>
Movies</span
>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={itemContainer(2, $focusIndex)} on:click={() => navigate('library')}>
<span
class={classNames('text-xl transition-opacity font-medium', {
// 'opacity-0': $isNavBarOpen === false
})}
>
Library</span
>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={itemContainer(3, $focusIndex)} on:click={() => navigate('search')}>
<span
class={classNames('text-xl transition-opacity font-medium', {
// 'opacity-0': $isNavBarOpen === false
})}
>
Search</span
>
</div>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={itemContainer(4, $focusIndex)} on:click={() => navigate('manage')}>
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(4)} let:hasFocus>
<div
class={classNames('w-full h-full relative flex items-center justify-center', {
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 4),
'text-stone-300 hover:text-primary-500':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 4)
})}
>
<Gear class="w-8 h-8" />
<span
class={classNames('text-xl transition-opacity font-medium', {
// 'opacity-0': $isNavBarOpen === false
})}
>
Manage</span
class={classNames(
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-16',
{
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'group-hover:opacity-100 group-hover:pointer-events-auto': true
}
)}
>
Manage
</span>
</div>
</div>
</Container>
<!-- <div class={'flex flex-col flex-1 relative z-20 items-center'}>-->
<!-- <div class={'flex flex-col flex-1 justify-center self-stretch'}>-->
<!-- <Container-->
<!-- class={classNames(itemContainer(0, $focusIndex), 'w-full flex justify-center')}-->
<!-- on:clickOrSelect={selectIndex(0)}-->
<!-- >-->
<!-- <Laptop class="w-8 h-8" />-->
<!-- </Container>-->
<!-- <Container-->
<!-- class={classNames(itemContainer(1, $focusIndex), 'w-full flex justify-center')}-->
<!-- on:clickOrSelect={selectIndex(1)}-->
<!-- >-->
<!-- <CardStack class="w-8 h-8" />-->
<!-- </Container>-->
<!-- <Container-->
<!-- class={classNames(itemContainer(2, $focusIndex), 'w-full flex justify-center')}-->
<!-- on:clickOrSelect={selectIndex(2)}-->
<!-- >-->
<!-- <Bookmark class="w-8 h-8" />-->
<!-- </Container>-->
<!-- <Container-->
<!-- class={classNames(itemContainer(3, $focusIndex), 'w-full flex justify-center')}-->
<!-- on:clickOrSelect={selectIndex(3)}-->
<!-- >-->
<!-- <MagnifyingGlass class="w-8 h-8" />-->
<!-- </Container>-->
<!-- </div>-->
<!-- <Container-->
<!-- class={classNames(itemContainer(4, $focusIndex), 'w-full flex justify-center')}-->
<!-- on:clickOrSelect={selectIndex(4)}-->
<!-- >-->
<!-- <Gear class="w-8 h-8" />-->
<!-- </Container>-->
<!-- </div>-->
<!-- <div-->
<!-- class={classNames(-->
<!-- 'absolute inset-y-0 left-0 pl-[64px] pr-96 z-10 transition-all bg-gradient-to-r from-secondary-500 to-transparent',-->
<!-- 'flex flex-col flex-1 p-4',-->
<!-- {-->
<!-- // 'translate-x-full opacity-100': $isNavBarOpen,-->
<!-- 'opacity-0 pointer-events-none': !$isNavBarOpen,-->
<!-- 'group-hover:translate-x-0 group-hover:opacity-100 group-hover:pointer-events-auto': true-->
<!-- }-->
<!-- )}-->
<!-- >-->
<!-- <div class="flex flex-col flex-1 justify-center">-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <div class={itemContainer(0, $focusIndex)} on:click={selectIndex(0)}>-->
<!-- <span-->
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
<!-- // 'opacity-0': $isNavBarOpen === false-->
<!-- })}-->
<!-- >-->
<!-- Series</span-->
<!-- >-->
<!-- </div>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <div class={itemContainer(1, $focusIndex)} on:click={selectIndex(1)}>-->
<!-- <span-->
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
<!-- // 'opacity-0': $isNavBarOpen === false-->
<!-- })}-->
<!-- >-->
<!-- Movies</span-->
<!-- >-->
<!-- </div>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <div class={itemContainer(2, $focusIndex)} on:click={selectIndex(2)}>-->
<!-- <span-->
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
<!-- // 'opacity-0': $isNavBarOpen === false-->
<!-- })}-->
<!-- >-->
<!-- Library</span-->
<!-- >-->
<!-- </div>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <div class={itemContainer(3, $focusIndex)} on:click={selectIndex(3)}>-->
<!-- <span-->
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
<!-- // 'opacity-0': $isNavBarOpen === false-->
<!-- })}-->
<!-- >-->
<!-- Search</span-->
<!-- >-->
<!-- </div>-->
<!-- </div>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <div class={itemContainer(4, $focusIndex)} on:click={selectIndex(4)}>-->
<!-- <span-->
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
<!-- // 'opacity-0': $isNavBarOpen === false-->
<!-- })}-->
<!-- >-->
<!-- Manage</span-->
<!-- >-->
<!-- </div>-->
<!-- </div>-->
</Container>

View File

@@ -33,7 +33,7 @@
<slot>Label</slot>
</label>
<input
class={classNames('bg-highlight-background px-4 py-1.5 rounded-lg', {
class={classNames('bg-secondary-500 px-4 py-1.5 rounded-lg', {
selected: hasFocus,
unselected: !hasFocus
})}

View File

@@ -45,8 +45,6 @@
video.src = playbackUrl;
}
muted = true; //TODO REMOVE
if (startTime) {
progressTime = startTime;
}

View File

@@ -2,15 +2,36 @@
import Container from '../../Container.svelte';
import { appState } from '../stores/app-state.store';
import Button from '../components/Button.svelte';
import { onMount } from 'svelte';
import { isTizen } from '../utils/browser-detection';
let lastKeyCode = 0;
let lastKey = '';
let tizenMediaKey = '';
onMount(() => {
if (isTizen()) {
var myMediaKeyChangeListener = {
onpressed: function (key: string) {
console.log('Pressed key: ' + key);
tizenMediaKey = key;
}
};
// eslint-disable-next-line no-undef
tizen.tvinputdevice.registerKey('MediaPlayPause');
(tizen as any).mediakey.setMediaKeyEventListener(myMediaKeyChangeListener);
}
});
</script>
<Container class="pl-24 flex flex-col items-start" focusOnMount>
User agent: {window.navigator.userAgent}
<div>Last key code: {lastKeyCode}</div>
<div>Last key: {lastKey}</div>
{#if tizenMediaKey}
<div>Tizen media key: {tizenMediaKey}</div>
{/if}
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
</Container>

View File

@@ -43,6 +43,7 @@
.slice(0, 5) || []
)}
>
<Container />
<div class="h-full flex-1 flex flex-col justify-end">
{#await $movieDataP then movie}
{#if movie}

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import Container from '../../Container.svelte';
import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte';
import { tmdbApi } from '../apis/tmdb/tmdb-api';
import { getShowcasePropsFromTmdbMovie } from '../components/HeroShowcase/HeroShowcase';
import Carousel from '../components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
import { scrollIntoView } from '../selectable';
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
import { useRequest } from '../stores/data.store';
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
const { data: continueWatching, isLoading: isLoadingContinueWatching } = useRequest(
jellyfinApi.getContinueWatching,
'movie'
);
const { data: recentlyAdded, isLoading: isLoadingRecentlyAdded } = useRequest(
jellyfinApi.getRecentlyAdded,
'movie'
);
const popularMovies = tmdbApi.getPopularMovies();
</script>
<Container focusOnMount class="flex flex-col">
<div class="h-[calc(100vh-12rem)] flex px-20">
<HeroShowcase
items={popularMovies.then(getShowcasePropsFromTmdbMovie)}
on:enter={scrollIntoView({ top: 0 })}
/>
</div>
<div class="mt-16">
<Carousel scrollClass="px-20" on:enter={scrollIntoView({ vertical: 64 })}>
<div class="text-xl font-semibold text-zinc-300" slot="title">
{$isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)
? 'Loading...'
: $continueWatching?.length
? 'Continue Watching'
: 'Recently Added'}
</div>
{#if $isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)}
<CarouselPlaceholderItems />
{:else if $continueWatching?.length}
<div class="flex -mx-4">
{#each $continueWatching as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
{/each}
</div>
{:else if $recentlyAdded?.length}
<div class="flex -mx-4">
{#each $recentlyAdded as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
{/each}
</div>
{/if}
</Carousel>
</div>
</Container>

View File

@@ -1,61 +0,0 @@
<script lang="ts">
import Container from '../../Container.svelte';
import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte';
import { tmdbApi } from '../apis/tmdb/tmdb-api';
import { getShowcasePropsFromTmdbMovie } from '../components/HeroShowcase/HeroShowcase';
import Carousel from '../components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
import { scrollIntoView } from '../selectable';
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
import { useRequest } from '../stores/data.store';
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
const { data: continueWatching, isLoading: isLoadingContinueWatching } = useRequest(
jellyfinApi.getContinueWatching,
'movie'
);
const { data: recentlyAdded, isLoading: isLoadingRecentlyAdded } = useRequest(
jellyfinApi.getRecentlyAdded,
'movie'
);
const popularMovies = tmdbApi.getPopularMovies();
</script>
<Container focusOnMount class="flex flex-col">
<div class="flex flex-col h-screen">
<div class="flex-1 flex relative px-20">
<HeroShowcase items={popularMovies.then(getShowcasePropsFromTmdbMovie)} />
</div>
<div class="mt-8">
<Carousel scrollClass="px-20">
<div class="text-xl font-semibold text-zinc-300" slot="title">
{$isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)
? 'Loading...'
: $continueWatching?.length
? 'Continue Watching'
: 'Recently Added'}
</div>
{#if $isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)}
<CarouselPlaceholderItems />
{:else if $continueWatching?.length}
<div class="flex -mx-2">
{#each $continueWatching as item (item.Id)}
<Container class="m-2" on:enter={scrollIntoView({ left: 64 + 16 })}>
<JellyfinCard {item} />
</Container>
{/each}
</div>
{:else if $recentlyAdded?.length}
<div class="flex -mx-2">
{#each $recentlyAdded as item (item.Id)}
<Container class="m-2" on:enter={scrollIntoView({ left: 64 + 16 })}>
<JellyfinCard {item} />
</Container>
{/each}
</div>
{/if}
</Carousel>
</div>
</div>
</Container>

View File

@@ -10,11 +10,9 @@
import { getShowcasePropsFromTmdbSeries } from '../components/HeroShowcase/HeroShowcase';
import { scrollIntoView } from '../selectable';
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
import JellyfinEpisodeCard from '../components/EpisodeCard/JellyfinEpisodeCard.svelte';
const { data: continueWatching, isLoading: isLoadingContinueWatching } = useRequest(
jellyfinApi.getContinueWatching,
'series'
jellyfinApi.getContinueWatchingSeries
);
const { data: recentlyAdded, isLoading: isLoadingRecentlyAdded } = useRequest(
jellyfinApi.getRecentlyAdded,
@@ -72,40 +70,41 @@
// }
</script>
<Container focusOnMount>
<div class="flex flex-col h-screen">
<div class="flex-1 flex relative px-20">
<HeroShowcase items={tmdbApi.getPopularSeries().then(getShowcasePropsFromTmdbSeries)} />
</div>
<div class="mt-8">
<Carousel scrollClass="px-20">
<div class="text-xl font-semibold text-zinc-300" slot="title">
{$isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)
? 'Loading...'
: $continueWatching?.length
? 'Continue Watching'
: 'Recently Added'}
<Container focusOnMount class="flex flex-col">
<div class="h-[calc(100vh-12rem)] flex px-20">
<HeroShowcase
items={tmdbApi.getPopularSeries().then(getShowcasePropsFromTmdbSeries)}
on:enter={scrollIntoView({ top: 0 })}
/>
</div>
<div class="mt-16">
<Carousel scrollClass="px-20" on:enter={scrollIntoView({ vertical: 64 })}>
<div class="text-xl font-semibold text-zinc-300" slot="title">
{$isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)
? 'Loading...'
: $continueWatching?.length
? 'Continue Watching'
: 'Recently Added'}
</div>
{#if $isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)}
<CarouselPlaceholderItems />
{:else if $continueWatching?.length}
<div class="flex -mx-2">
{#each $continueWatching as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
{/each}
</div>
{#if $isLoadingContinueWatching || ($isLoadingRecentlyAdded && !$continueWatching?.length)}
<CarouselPlaceholderItems />
{:else if $continueWatching?.length}
<div class="flex -mx-2">
{#each $continueWatching as item (item.Id)}
<Container class="m-2" on:enter={scrollIntoView({ left: 64 + 16 })}>
<JellyfinEpisodeCard episode={item} />
</Container>
{/each}
</div>
{:else if $recentlyAdded?.length}
<div class="flex -mx-2">
{#each $recentlyAdded as item (item.Id)}
<Container class="m-2" on:enter={scrollIntoView({ left: 64 + 16 })}>
<JellyfinCard {item} />
</Container>
{/each}
</div>
{/if}
</Carousel>
</div>
{:else if $recentlyAdded?.length}
<div class="flex -mx-4">
{#each $recentlyAdded as item (item.Id)}
<Container class="m-4" on:enter={scrollIntoView({ horizontal: 64 + 20 })}>
<JellyfinCard size="lg" {item} />
</Container>
{/each}
</div>
{/if}
</Carousel>
</div>
</Container>

View File

@@ -296,56 +296,9 @@ export class Selectable {
return true;
}
return false;
if (navigationEventOptions.preventNavigation) return true;
// const focusIndex = get(this.focusIndex);
//
// const indexAddition = {
// up: this.direction === 'vertical' ? -1 : -this.gridColumns,
// down: this.direction === 'vertical' ? 1 : this.gridColumns,
// left:
// this.direction === 'horizontal'
// ? (focusIndex % this.gridColumns) - 1 < 0
// ? 0
// : -1
// : -this.gridColumns,
// right:
// this.direction === 'horizontal'
// ? (focusIndex % this.gridColumns) + 1 >= this.gridColumns
// ? 0
// : 1
// : this.gridColumns
// }[direction];
//
// // Cycle siblings
// if (indexAddition !== 0) {
// let index = focusIndex + indexAddition;
// while (index >= 0 && index < this.children.length) {
// const children = this.children[index];
// if (children && children.isFocusable()) {
// propagateNavigationEvent(this, navigationEventOptions);
// if (navigationEventOptions.preventNavigation) return true;
// children.focus();
// return true;
// }
// index += indexAddition;
// }
// }
//
// // About to leave this container (=coulnd't cycle siblings)
// navigationEventOptions.willLeaveContainer = true;
// if (!bypassActions) {
// propagateNavigationEvent(this, navigationEventOptions);
// if (navigationEventOptions.preventNavigation) return true;
// }
// if (this.neighbors[direction]?.isFocusable()) {
// this.neighbors[direction]?.focus();
// return true;
// } else if (!this.trapFocus) {
// return this.parent?.giveFocus(direction, bypassActions) || false;
// }
//
// return false;
return false;
}
static giveFocus(direction: Direction, fireActions?: boolean) {

View File

@@ -1,3 +1,9 @@
/**
* https://huemint.com/website-monochrome/#palette=353633-fbfdff
* https://huemint.com/website-monochrome/#palette=161718-dfd1a3 Very Nice
* https://huemint.com/website-monochrome/#palette=151a1a-ebab2e
*/
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
@@ -11,8 +17,34 @@ export default {
darken: '#07050166',
lighten: '#fde68a20',
// 'highlight-foreground': '#E7E5E4'
'highlight-foreground': '#ffe6abcc',
'highlight-background': '#2925247F'
'highlight-foreground': '#f6c304',
'highlight-background': '#161517',
primary: {
50: '#FDF8EC',
100: '#FBEED5',
200: '#F7DEAB',
300: '#F3CD81',
400: '#EFBC57',
500: '#EBAB2E',
600: '#CD8F14',
700: '#9A6B0F',
800: '#66480A',
900: '#332405',
950: '#1C1403'
},
secondary: {
50: '#E6EAEA',
100: '#CCD6D6',
200: '#99ADAD',
300: '#698282',
400: '#3E4C4C',
500: '#0a0807',
600: '#101414',
700: '#211a17',
800: '#171310',
900: '#0a0807',
950: '#020303'
}
},
keyframes: {
timer: {