style: Major visual overhaul with various improvements and fixes
This commit is contained in:
@@ -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 />
|
||||
|
||||
14
src/app.css
14
src/app.css
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
19
src/lib/components/AnimateScale.svelte
Normal file
19
src/lib/components/AnimateScale.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
on:navigate={({ detail }) => {
|
||||
if (
|
||||
detail.direction === 'left' &&
|
||||
detail.options.willLeaveContainer &&
|
||||
detail.willLeaveContainer &&
|
||||
detail.selectable === detail.options.target
|
||||
) {
|
||||
history.back();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">-->
|
||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
||||
<!-- <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>-->
|
||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
||||
<!-- <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>-->
|
||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
||||
<!-- <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>-->
|
||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
||||
<!-- <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>-->
|
||||
|
||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
||||
<!-- <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>
|
||||
|
||||
@@ -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
|
||||
})}
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
video.src = playbackUrl;
|
||||
}
|
||||
|
||||
muted = true; //TODO REMOVE
|
||||
|
||||
if (startTime) {
|
||||
progressTime = startTime;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
62
src/lib/pages/MoviesHomePage.svelte
Normal file
62
src/lib/pages/MoviesHomePage.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user