mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-21 16:25:11 +02:00
Context menus and setting watch state
This commit is contained in:
@@ -173,3 +173,26 @@ export const reportJellyfinPlaybackStopped = (
|
||||
MediaSourceId: itemId
|
||||
}
|
||||
});
|
||||
|
||||
export const setJellyfinItemWatched = (jellyfinId: string) =>
|
||||
JellyfinApi.post('/Users/{userId}/PlayedItems/{itemId}', {
|
||||
params: {
|
||||
path: {
|
||||
userId: JELLYFIN_USER_ID,
|
||||
itemId: jellyfinId
|
||||
},
|
||||
query: {
|
||||
datePlayed: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const setJellyfinItemUnwatched = (jellyfinId: string) =>
|
||||
JellyfinApi.del('/Users/{userId}/PlayedItems/{itemId}', {
|
||||
params: {
|
||||
path: {
|
||||
userId: JELLYFIN_USER_ID,
|
||||
itemId: jellyfinId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
import { formatMinutesToTime } from '$lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { Clock, Star } from 'radix-icons-svelte';
|
||||
import ContextMenu from '../ContextMenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '../ContextMenu/ContextMenuItem.svelte';
|
||||
import { setJellyfinItemUnwatched, setJellyfinItemWatched } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let jellyfinId: string | undefined = undefined;
|
||||
export let type: 'movie' | 'series' = 'movie';
|
||||
export let title: string;
|
||||
export let genres: string[] = [];
|
||||
@@ -17,75 +22,99 @@
|
||||
export let available = true;
|
||||
export let progress = 0;
|
||||
export let size: 'dynamic' | 'md' | 'lg' = 'md';
|
||||
export let randomProgress = false;
|
||||
if (randomProgress) {
|
||||
progress = Math.random() > 0.3 ? Math.random() * 100 : 0;
|
||||
|
||||
let watched = false;
|
||||
$: watched = !available && !!jellyfinId;
|
||||
|
||||
function handleSetWatched() {
|
||||
if (jellyfinId) {
|
||||
setJellyfinItemWatched(jellyfinId).finally(() => library.refreshIn(3000));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetUnwatched() {
|
||||
if (jellyfinId) {
|
||||
setJellyfinItemUnwatched(jellyfinId).finally(() => library.refreshIn(3000));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
class={classNames(
|
||||
'rounded overflow-hidden relative shadow-lg shrink-0 aspect-video selectable block hover:text-inherit',
|
||||
{
|
||||
'h-40': size === 'md',
|
||||
'h-60': size === 'lg',
|
||||
'w-full': size === 'dynamic'
|
||||
}
|
||||
)}
|
||||
href={`/${type}/${tmdbId}`}
|
||||
>
|
||||
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
|
||||
<div
|
||||
class="h-full w-full opacity-0 hover:opacity-100 transition-opacity flex flex-col justify-between cursor-pointer p-2 px-3 relative z-[1] peer"
|
||||
style={progress > 0 ? 'padding-bottom: 0.6rem;' : ''}
|
||||
<ContextMenu heading={title} disabled={!jellyfinId}>
|
||||
<svelte:fragment slot="menu">
|
||||
<ContextMenuItem on:click={handleSetWatched} disabled={!jellyfinId || watched}>
|
||||
Mark as watched
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem on:click={handleSetUnwatched} disabled={!jellyfinId || !watched}>
|
||||
Mark as unwatched
|
||||
</ContextMenuItem>
|
||||
</svelte:fragment>
|
||||
<a
|
||||
class={classNames(
|
||||
'rounded overflow-hidden relative shadow-lg shrink-0 aspect-video selectable block hover:text-inherit',
|
||||
{
|
||||
'h-40': size === 'md',
|
||||
'h-60': size === 'lg',
|
||||
'w-full': size === 'dynamic'
|
||||
}
|
||||
)}
|
||||
href={`/${type}/${tmdbId}`}
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-bold tracking-wider text-lg">{title}</h1>
|
||||
<div class="text-xs text-zinc-300 tracking-wider font-medium">
|
||||
{genres.map((genre) => genre.charAt(0).toUpperCase() + genre.slice(1)).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-end">
|
||||
{#if completionTime}
|
||||
<div class="text-sm font-medium text-zinc-200 tracking-wide">
|
||||
Downloaded in <b
|
||||
>{formatMinutesToTime((new Date(completionTime).getTime() - Date.now()) / 1000 / 60)}</b
|
||||
>
|
||||
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
|
||||
<div
|
||||
class="h-full w-full opacity-0 hover:opacity-100 transition-opacity flex flex-col justify-between cursor-pointer p-2 px-3 relative z-[1] peer"
|
||||
style={progress > 0 ? 'padding-bottom: 0.6rem;' : ''}
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-bold tracking-wider text-lg">{title}</h1>
|
||||
<div class="text-xs text-zinc-300 tracking-wider font-medium">
|
||||
{genres.map((genre) => genre.charAt(0).toUpperCase() + genre.slice(1)).join(', ')}
|
||||
</div>
|
||||
{:else}
|
||||
{#if runtimeMinutes}
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<Clock />
|
||||
</div>
|
||||
<div class="flex justify-between items-end">
|
||||
{#if completionTime}
|
||||
<div class="text-sm font-medium text-zinc-200 tracking-wide">
|
||||
Downloaded in <b
|
||||
>{formatMinutesToTime(
|
||||
(new Date(completionTime).getTime() - Date.now()) / 1000 / 60
|
||||
)}</b
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
{#if runtimeMinutes}
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<Clock />
|
||||
<div class="text-sm text-zinc-200">
|
||||
{progress
|
||||
? formatMinutesToTime(runtimeMinutes - runtimeMinutes * (progress / 100)) +
|
||||
' left'
|
||||
: formatMinutesToTime(runtimeMinutes)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if seasons}
|
||||
<div class="text-sm text-zinc-200">
|
||||
{progress
|
||||
? formatMinutesToTime(runtimeMinutes - runtimeMinutes * (progress / 100)) + ' left'
|
||||
: formatMinutesToTime(runtimeMinutes)}
|
||||
{seasons} Season{seasons > 1 ? 's' : ''}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<Star />
|
||||
<div class="text-sm text-zinc-200">
|
||||
{rating ? rating.toFixed(1) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if seasons}
|
||||
<div class="text-sm text-zinc-200">
|
||||
{seasons} Season{seasons > 1 ? 's' : ''}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<Star />
|
||||
<div class="text-sm text-zinc-200">
|
||||
{rating ? rating.toFixed(1) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_BACKDROP_SMALL + backdropUri + "')"}
|
||||
class="absolute inset-0 bg-center bg-cover peer-hover:scale-105 transition-transform"
|
||||
/>
|
||||
<div
|
||||
class={classNames('absolute inset-0 transition-opacity', {
|
||||
'bg-darken opacity-0 peer-hover:opacity-100': available,
|
||||
'bg-[#00000055] peer-hover:bg-darken': !available
|
||||
})}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_BACKDROP_SMALL + backdropUri + "')"}
|
||||
class="absolute inset-0 bg-center bg-cover peer-hover:scale-105 transition-transform"
|
||||
/>
|
||||
<div
|
||||
class={classNames('absolute inset-0 transition-opacity', {
|
||||
'bg-darken opacity-0 peer-hover:opacity-100': available,
|
||||
'bg-[#00000055] peer-hover:bg-darken': !available
|
||||
})}
|
||||
/>
|
||||
</a>
|
||||
</ContextMenu>
|
||||
|
||||
82
src/lib/components/ContextMenu/ContextMenu.svelte
Normal file
82
src/lib/components/ContextMenu/ContextMenu.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { contextMenu } from './ContextMenu';
|
||||
|
||||
export let heading = '';
|
||||
export let disabled = false;
|
||||
|
||||
const id = Symbol();
|
||||
|
||||
let menu: HTMLDivElement;
|
||||
|
||||
let position = { x: 0, y: 0 };
|
||||
|
||||
function close() {
|
||||
contextMenu.hide();
|
||||
}
|
||||
|
||||
function handleOpen(event: MouseEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
position = { x: event.clientX, y: event.clientY };
|
||||
contextMenu.show(id);
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (!menu?.contains(event.target as Node)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && $contextMenu === id) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleShortcuts} on:click={handleClickOutside} />
|
||||
<svelte:head>
|
||||
{#if $contextMenu === id}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<!-- <svelte:body bind:this={body} /> -->
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:contextmenu|preventDefault={handleOpen}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if $contextMenu === id}
|
||||
{#key position}
|
||||
<div
|
||||
class="fixed z-50 px-1 py-1 bg-zinc-800 bg-opacity-50 rounded-lg backdrop-blur-xl flex flex-col"
|
||||
style="left: {position.x}px; top: {position.y}px;"
|
||||
bind:this={menu}
|
||||
in:fly|global={{ y: 5, duration: 100, delay: 100 }}
|
||||
out:fly|global={{ y: 5, duration: 100 }}
|
||||
>
|
||||
<slot name="title">
|
||||
{#if heading}
|
||||
<h2
|
||||
class="text-xs text-zinc-200 opacity-60 tracking-wide font-semibold px-3 py-1 line-clamp-1"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
{/if}
|
||||
</slot>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex flex-col gap-0.5" on:click={() => close()}>
|
||||
<slot name="menu" />
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
17
src/lib/components/ContextMenu/ContextMenu.ts
Normal file
17
src/lib/components/ContextMenu/ContextMenu.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function createContextMenu() {
|
||||
const visibleItem = writable<Symbol | null>(null);
|
||||
|
||||
return {
|
||||
subscribe: visibleItem.subscribe,
|
||||
show: (item: Symbol) => {
|
||||
visibleItem.set(item);
|
||||
},
|
||||
hide: () => {
|
||||
visibleItem.set(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const contextMenu = createContextMenu();
|
||||
17
src/lib/components/ContextMenu/ContextMenuItem.svelte
Normal file
17
src/lib/components/ContextMenu/ContextMenuItem.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click
|
||||
class={classNames(
|
||||
'text-sm font-medium px-3 py-1 hover:bg-zinc-200 hover:bg-opacity-10 rounded-md text-left cursor-default',
|
||||
{
|
||||
'opacity-80 pointer-events-none': disabled
|
||||
}
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { TMDB_BACKDROP_SMALL } from '$lib/constants';
|
||||
import classNames from 'classnames';
|
||||
import { Check, CheckCircled, DotsHorizontal, TriangleRight } from 'radix-icons-svelte';
|
||||
import { Check } from 'radix-icons-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import ContextMenu from '../ContextMenu/ContextMenu.svelte';
|
||||
import PlayButton from '../PlayButton.svelte';
|
||||
import ProgressBar from '../ProgressBar.svelte';
|
||||
import ContextMenuItem from '../ContextMenu/ContextMenuItem.svelte';
|
||||
import { setJellyfinItemUnwatched, setJellyfinItemWatched } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
|
||||
export let backdropPath: string;
|
||||
|
||||
@@ -17,84 +20,125 @@
|
||||
export let progress = 0;
|
||||
export let watched = false;
|
||||
|
||||
export let handlePlay: (() => void) | undefined = undefined;
|
||||
export let jellyfinId: string | undefined = undefined;
|
||||
|
||||
export let size: 'md' | 'dynamic' = 'md';
|
||||
|
||||
function handleSetWatched() {
|
||||
if (!jellyfinId) return;
|
||||
|
||||
watched = true;
|
||||
setJellyfinItemWatched(jellyfinId).finally(() => library.refreshIn(5000));
|
||||
}
|
||||
|
||||
function handleSetUnwatched() {
|
||||
if (!jellyfinId) return;
|
||||
|
||||
watched = false;
|
||||
setJellyfinItemUnwatched(jellyfinId).finally(() => library.refreshIn(5000));
|
||||
}
|
||||
|
||||
function handlePlay() {
|
||||
if (!jellyfinId) return;
|
||||
|
||||
playerState.streamJellyfinId(jellyfinId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<button
|
||||
on:click
|
||||
class={classNames(
|
||||
'aspect-video bg-center bg-cover bg-no-repeat rounded-lg overflow-hidden transition-all shadow-lg relative cursor-pointer selectable flex-shrink-0 placeholder-image',
|
||||
{
|
||||
'h-40': size === 'md',
|
||||
'h-full': size === 'dynamic',
|
||||
group: !!handlePlay
|
||||
}
|
||||
)}
|
||||
style={"background-image: url('" + TMDB_BACKDROP_SMALL + backdropPath + "');"}
|
||||
in:fade|global={{ duration: 100, delay: 100 }}
|
||||
out:fade|global={{ duration: 100 }}
|
||||
>
|
||||
<div
|
||||
class={classNames('flex flex-col justify-between h-full group-hover:opacity-0 transition-all', {
|
||||
'px-2 lg:px-3 pt-2': true,
|
||||
'pb-4 lg:pb-6': progress,
|
||||
'pb-2': !progress,
|
||||
'bg-gradient-to-t from-darken': !!handlePlay,
|
||||
'bg-darken': !handlePlay || watched
|
||||
})}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<slot name="left-top">
|
||||
{#if episodeNumber}
|
||||
<p class="text-xs lg:text-sm font-medium text-zinc-300">{episodeNumber}</p>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="right-top">
|
||||
{#if runtime}
|
||||
<p class="text-xs lg:text-sm font-medium text-zinc-300">
|
||||
{runtime} min
|
||||
</p>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<slot name="left-bottom">
|
||||
<div class="flex flex-col items-start">
|
||||
{#if subtitle}
|
||||
<h2 class="text-zinc-300 text-sm font-medium">{subtitle}</h2>
|
||||
{/if}
|
||||
{#if title}
|
||||
<h1 class="font-medium text-left">
|
||||
{title}
|
||||
</h1>
|
||||
{/if}
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="right-bottom">
|
||||
{#if watched}
|
||||
<Check size={20} class="opacity-80" />
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<PlayButton
|
||||
on:click={handlePlay}
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
{#if progress}
|
||||
<div
|
||||
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 group-hover:opacity-0 transition-opacity"
|
||||
<ContextMenu heading={subtitle}>
|
||||
<svelte:fragment slot="menu">
|
||||
<ContextMenuItem
|
||||
on:click={handleSetWatched}
|
||||
disabled={!handleSetWatched || watched || !jellyfinId}
|
||||
>
|
||||
<ProgressBar {progress} />
|
||||
Mark as watched
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
on:click={handleSetUnwatched}
|
||||
disabled={!handleSetUnwatched || !watched || !jellyfinId}
|
||||
>
|
||||
Mark as unwatched
|
||||
</ContextMenuItem>
|
||||
</svelte:fragment>
|
||||
<button
|
||||
on:click
|
||||
class={classNames(
|
||||
'aspect-video bg-center bg-cover bg-no-repeat rounded-lg overflow-hidden transition-all shadow-lg relative selectable flex-shrink-0 placeholder-image',
|
||||
{
|
||||
'h-40': size === 'md',
|
||||
'h-full': size === 'dynamic',
|
||||
group: !!jellyfinId,
|
||||
'cursor-default': !jellyfinId
|
||||
}
|
||||
)}
|
||||
style={"background-image: url('" + TMDB_BACKDROP_SMALL + backdropPath + "');"}
|
||||
in:fade|global={{ duration: 100, delay: 100 }}
|
||||
out:fade|global={{ duration: 100 }}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex flex-col justify-between h-full group-hover:opacity-0 transition-all',
|
||||
{
|
||||
'px-2 lg:px-3 pt-2': true,
|
||||
'pb-4 lg:pb-6': progress,
|
||||
'pb-2': !progress,
|
||||
'bg-gradient-to-t from-darken': !!jellyfinId,
|
||||
'bg-darken': !jellyfinId || watched
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<slot name="left-top">
|
||||
{#if episodeNumber}
|
||||
<p class="text-xs lg:text-sm font-medium text-zinc-300">{episodeNumber}</p>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="right-top">
|
||||
{#if runtime}
|
||||
<p class="text-xs lg:text-sm font-medium text-zinc-300">
|
||||
{runtime} min
|
||||
</p>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<slot name="left-bottom">
|
||||
<div class="flex flex-col items-start">
|
||||
{#if subtitle}
|
||||
<h2 class="text-zinc-300 text-sm font-medium">{subtitle}</h2>
|
||||
{/if}
|
||||
{#if title}
|
||||
<h1 class="font-medium text-left line-clamp-2">
|
||||
{title}
|
||||
</h1>
|
||||
{/if}
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="right-bottom">
|
||||
{#if watched}
|
||||
<div class="flex-shrink-0">
|
||||
<Check size={20} class="opacity-80" />
|
||||
</div>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<PlayButton
|
||||
on:click={handlePlay}
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
{#if progress}
|
||||
<div
|
||||
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 group-hover:opacity-0 transition-opacity"
|
||||
>
|
||||
<ProgressBar {progress} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</ContextMenu>
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
)}
|
||||
href={`/person/${tmdbId}`}
|
||||
>
|
||||
<div class="mx-auto rounded-full overflow-hidden flex-shrink-0 aspect-square w-full">
|
||||
<div
|
||||
class="mx-auto rounded-full overflow-hidden flex-shrink-0 aspect-square w-full bg-zinc-200 bg-opacity-20"
|
||||
>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_PROFILE_SMALL + backdropUri + "')"}
|
||||
class="bg-center bg-cover group-hover:scale-105 transition-transform w-full h-full"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
export let overview: string;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col max-h-screen bg-black pb-4 gap-4" transition:fade>
|
||||
<div class="flex flex-col max-h-screen bg-black gap-4 pb-4" transition:fade>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_IMAGES_ORIGINAL + backdropPath + "')"}
|
||||
class="flex-shrink relative flex pt-24 aspect-video min-h-[70vh] px-4 sm:px-8 bg-center bg-cover sm:bg-fixed"
|
||||
@@ -47,10 +47,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="episodes-carousel" />
|
||||
<div class="py-2">
|
||||
<slot name="episodes-carousel" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col py-4 gap-8 bg-black">
|
||||
<div class="flex flex-col gap-8 bg-black">
|
||||
<div
|
||||
class="mx-4 sm:mx-8 py-4 sm:py-6 px-6 sm:px-10 grid grid-cols-4 sm:grid-cols-6 gap-4 sm:gap-x-8 bg-stone-900 rounded-xl"
|
||||
>
|
||||
|
||||
@@ -176,13 +176,13 @@
|
||||
{/if}
|
||||
{#if UIVisible}
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-darken via-[30%] to-darken"
|
||||
class="absolute inset-0 bg-gradient-to-t from-stone-950 via-darken via-[30%] to-darken opacity-50"
|
||||
in:fade={{ duration: ANIMATION_DURATION }}
|
||||
out:fade={{ duration: ANIMATION_DURATION }}
|
||||
/>
|
||||
{:else if !UIVisible}
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-darken"
|
||||
class="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-darken opacity-50"
|
||||
in:fade={{ duration: ANIMATION_DURATION }}
|
||||
out:fade={{ duration: ANIMATION_DURATION }}
|
||||
/>
|
||||
|
||||
@@ -220,6 +220,7 @@ async function getLibrary(): Promise<Library> {
|
||||
};
|
||||
}
|
||||
|
||||
let delayedRefreshTimeout: NodeJS.Timeout;
|
||||
function createLibraryStore() {
|
||||
const { update, set, ...library } = writable<Promise<Library>>(getLibrary()); //TODO promise to undefined
|
||||
|
||||
@@ -232,6 +233,12 @@ function createLibraryStore() {
|
||||
return {
|
||||
...library,
|
||||
refresh: async () => getLibrary().then((r) => set(Promise.resolve(r))),
|
||||
refreshIn: async (ms: number) => {
|
||||
clearTimeout(delayedRefreshTimeout);
|
||||
delayedRefreshTimeout = setTimeout(() => {
|
||||
getLibrary().then((r) => set(Promise.resolve(r)));
|
||||
}, ms);
|
||||
},
|
||||
filterNotInLibrary
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user