mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 00:35:12 +02:00
Added "Dynamic modals"; fixed title modals not allowing child modals
This commit is contained in:
@@ -7,11 +7,14 @@
|
||||
import ContextMenuItem from '../ContextMenu/ContextMenuItem.svelte';
|
||||
import { setJellyfinItemUnwatched, setJellyfinItemWatched } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import { titlePageModal } from '../TitlePageLayout/TitlePageModal';
|
||||
|
||||
import { modalStack, openTitleModal } from '../Modal/Modal';
|
||||
import TitlePageModal from '../TitlePageLayout/TitlePageModal.svelte';
|
||||
import type { TitleType } from '$lib/types';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let jellyfinId: string | undefined = undefined;
|
||||
export let type: 'movie' | 'series' = 'movie';
|
||||
export let type: TitleType = 'movie';
|
||||
export let title: string;
|
||||
export let genres: string[] = [];
|
||||
export let runtimeMinutes = 0;
|
||||
@@ -23,6 +26,7 @@
|
||||
export let available = true;
|
||||
export let progress = 0;
|
||||
export let size: 'dynamic' | 'md' | 'lg' = 'md';
|
||||
export let openInModal = true;
|
||||
|
||||
let watched = false;
|
||||
$: watched = !available && !!jellyfinId;
|
||||
@@ -58,7 +62,13 @@
|
||||
'w-full': size === 'dynamic'
|
||||
}
|
||||
)}
|
||||
on:click={() => titlePageModal.set(tmdbId, type)}
|
||||
on:click={() => {
|
||||
if (openInModal) {
|
||||
openTitleModal(tmdbId, type);
|
||||
} else {
|
||||
window.location.href = `/${type}/${tmdbId}`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={'width: ' + (progress ? Math.max(progress, 2) : progress) + '%'}
|
||||
|
||||
54
src/lib/components/Modal/DynamicModal.svelte
Normal file
54
src/lib/components/Modal/DynamicModal.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { modalStack } from './Modal';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && $modalStack.top) {
|
||||
modalStack.close($modalStack.top.id);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
modalStack.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleShortcuts} />
|
||||
|
||||
<svelte:head>
|
||||
{#if $modalStack.top}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#each $modalStack.stack || [] as modal (modal.id)}
|
||||
{@const hidden = $modalStack.top?.group === modal.group && $modalStack.top?.id !== modal.id}
|
||||
|
||||
{#if modal.group === modal.id}
|
||||
<div
|
||||
class="fixed inset-0 bg-stone-950 bg-opacity-80 z-20"
|
||||
transition:fade={{ duration: 150 }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'fixed inset-0 justify-center items-center z-20 overflow-hidden flex transition-opacity reltaive',
|
||||
{
|
||||
'opacity-0': hidden
|
||||
}
|
||||
)}
|
||||
on:click|self={() => modalStack.close(modal.id)}
|
||||
transition:fade|global={{ duration: 100 }}
|
||||
>
|
||||
<svelte:component this={modal.component} {...modal.props} modalId={modal.id} />
|
||||
</div>
|
||||
{/each}
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { modalStack } from './Modal';
|
||||
|
||||
// export let visible = false;
|
||||
export let close: () => void;
|
||||
export let id: Symbol;
|
||||
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && $modalStack.top === id) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
modalStack.push(id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleShortcuts} />
|
||||
<svelte:head>
|
||||
{#if $modalStack.top}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#if $modalStack.top === id}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed inset-0 justify-center items-center z-20 overflow-hidden flex"
|
||||
on:click|self={close}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,56 +1,71 @@
|
||||
import type { TitleType } from '$lib/types';
|
||||
import { writable } from 'svelte/store';
|
||||
import TitlePageModal from '../TitlePageLayout/TitlePageModal.svelte';
|
||||
|
||||
function createModalStack() {
|
||||
const store = writable<{ stack: Symbol[]; top: Symbol | undefined }>({
|
||||
type ModalItem = {
|
||||
id: Symbol;
|
||||
group: Symbol;
|
||||
component: ConstructorOfATypedSvelteComponent;
|
||||
props: Record<string, any>;
|
||||
};
|
||||
function createDynamicModalStack() {
|
||||
const store = writable<{ stack: ModalItem[]; top: ModalItem | undefined }>({
|
||||
stack: [],
|
||||
top: undefined
|
||||
});
|
||||
|
||||
store.subscribe(console.log);
|
||||
|
||||
function close(symbol: Symbol) {
|
||||
store.update((s) => {
|
||||
s.stack = s.stack.filter((i) => i.id !== symbol);
|
||||
s.top = s.stack[s.stack.length - 1];
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
function closeGroup(group: Symbol) {
|
||||
store.update((s) => {
|
||||
s.stack = s.stack.filter((i) => i.group !== group);
|
||||
s.top = s.stack[s.stack.length - 1];
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
function create(
|
||||
component: ConstructorOfATypedSvelteComponent,
|
||||
props: Record<string, any>,
|
||||
group: Symbol | undefined = undefined
|
||||
) {
|
||||
const id = Symbol();
|
||||
const item = { id, component, props, group: group || id };
|
||||
store.update((s) => {
|
||||
s.stack.push(item);
|
||||
s.top = item;
|
||||
return s;
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
store.set({ stack: [], top: undefined });
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
push: (symbol: Symbol) => {
|
||||
store.update((s) => {
|
||||
if (s.stack.includes(symbol)) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s.stack.push(symbol);
|
||||
s.top = symbol;
|
||||
return s;
|
||||
});
|
||||
},
|
||||
remove: (symbol: Symbol) => {
|
||||
store.update((s) => {
|
||||
s.stack = s.stack.filter((x) => x !== symbol);
|
||||
s.top = s.stack[s.stack.length - 1];
|
||||
return s;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const modalStack = createModalStack();
|
||||
|
||||
export type ModalProps = ReturnType<typeof createModalProps>;
|
||||
|
||||
export function createModalProps(onClose: () => void, onBack?: () => void) {
|
||||
const id = Symbol();
|
||||
|
||||
function close() {
|
||||
onClose(); // ORDER MATTERS HERE
|
||||
modalStack.remove(id);
|
||||
}
|
||||
|
||||
function back() {
|
||||
onBack?.();
|
||||
modalStack.remove(id);
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
close,
|
||||
back: onBack ? back : undefined,
|
||||
id
|
||||
closeGroup,
|
||||
reset
|
||||
};
|
||||
}
|
||||
|
||||
export const modalStack = createDynamicModalStack();
|
||||
|
||||
let lastTitleModal: Symbol | undefined = undefined;
|
||||
export function openTitleModal(tmdbId: number, type: TitleType) {
|
||||
if (lastTitleModal) {
|
||||
modalStack.close(lastTitleModal);
|
||||
}
|
||||
lastTitleModal = modalStack.create(TitlePageModal, { tmdbId, type });
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { modalStack } from './Modal';
|
||||
</script>
|
||||
|
||||
{#if $modalStack.top}
|
||||
<div
|
||||
class="fixed inset-0 bg-stone-900 bg-opacity-50 z-[19] overflow-hidden"
|
||||
transition:fade|global={{ duration: 100 }}
|
||||
/>
|
||||
{/if}
|
||||
@@ -5,12 +5,12 @@
|
||||
import TitleSearchModal from './TitleSearchModal.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
|
||||
let y = 0;
|
||||
let transparent = true;
|
||||
let baseStyle = '';
|
||||
|
||||
let isSearchVisible = false;
|
||||
let isMobileMenuVisible = false;
|
||||
|
||||
function getLinkStyle(path: string) {
|
||||
@@ -20,7 +20,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: on mobile don't act sticky
|
||||
function openSearchModal() {
|
||||
modalStack.create(TitleSearchModal, {});
|
||||
}
|
||||
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
openSearchModal();
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
transparent = y <= 0;
|
||||
baseStyle = classNames(
|
||||
@@ -35,7 +45,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY={y} />
|
||||
<svelte:window bind:scrollY={y} on:keydown={handleShortcuts} />
|
||||
|
||||
<div class={classNames(baseStyle, 'hidden sm:grid')}>
|
||||
<a
|
||||
@@ -55,7 +65,7 @@
|
||||
<a href="/settings" class={$page && getLinkStyle('/settings')}>Settings</a>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconButton on:click={() => (isSearchVisible = true)}>
|
||||
<IconButton on:click={openSearchModal}>
|
||||
<MagnifyingGlass size={20} />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
@@ -71,7 +81,7 @@
|
||||
</a>
|
||||
<div />
|
||||
<div class="flex items-center gap-2">
|
||||
<IconButton on:click={() => (isSearchVisible = true)}>
|
||||
<IconButton on:click={openSearchModal}>
|
||||
<MagnifyingGlass size={20} />
|
||||
</IconButton>
|
||||
|
||||
@@ -119,5 +129,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<TitleSearchModal bind:visible={isSearchVisible} />
|
||||
|
||||
@@ -3,20 +3,22 @@
|
||||
import { searchTmdbTitles } from '$lib/apis/tmdb/tmdbApi';
|
||||
import { TMDB_POSTER_SMALL } from '$lib/constants';
|
||||
import { MagnifyingGlass } from 'radix-icons-svelte';
|
||||
import { createModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContent from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { TitleType } from '$lib/types';
|
||||
|
||||
export let modalId: Symbol;
|
||||
|
||||
export let visible = false;
|
||||
let inputElement: HTMLInputElement;
|
||||
let inputValue = '';
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
let fetching = false;
|
||||
let resultProps:
|
||||
| {
|
||||
tmdbId: number;
|
||||
type: 'movie' | 'series';
|
||||
type: TitleType;
|
||||
overview: string;
|
||||
posterUri: string;
|
||||
title: string;
|
||||
@@ -25,19 +27,8 @@
|
||||
}[]
|
||||
| undefined = undefined;
|
||||
|
||||
const modalProps = createModalProps(() => {
|
||||
visible = false;
|
||||
});
|
||||
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
function clear() {
|
||||
inputValue = '';
|
||||
fetching = false;
|
||||
resultProps = undefined;
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
const handleInput = () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
@@ -53,7 +44,7 @@
|
||||
.filter((i) => i.media_type !== 'person')
|
||||
.filter((i: TmdbMovie2 & TmdbSeries2) => i.release_date || i.first_air_date)
|
||||
.map((i: TmdbMovie2 & TmdbSeries2) => ({
|
||||
// Types not accurate!
|
||||
// ^ Types not accurate! ^
|
||||
tmdbId: i.id || 0,
|
||||
type: (i as any).media_type === 'movie' ? 'movie' : 'series',
|
||||
posterUri: i.poster_path || '',
|
||||
@@ -66,72 +57,59 @@
|
||||
.finally(() => (fetching = false));
|
||||
};
|
||||
|
||||
$: if (visible && inputElement) inputElement.focus();
|
||||
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
visible = true;
|
||||
} else if (event.key === 'Escape' && visible) {
|
||||
clear();
|
||||
modalProps.close();
|
||||
}
|
||||
function handleClose() {
|
||||
modalStack.close(modalId);
|
||||
}
|
||||
|
||||
$: if (inputElement) inputElement.focus();
|
||||
|
||||
onMount(() => () => clearTimeout(searchTimeout));
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleShortcuts} />
|
||||
|
||||
{#if visible}
|
||||
<Modal {...modalProps}>
|
||||
<ModalContent>
|
||||
<ModalHeader {...modalProps}>
|
||||
<MagnifyingGlass size={20} class="text-zinc-400" />
|
||||
<input
|
||||
bind:value={inputValue}
|
||||
bind:this={inputElement}
|
||||
on:input={handleInput}
|
||||
type="text"
|
||||
class="flex-1 bg-transparent font-light outline-none"
|
||||
placeholder="Search for Movies and Shows..."
|
||||
/>
|
||||
</ModalHeader>
|
||||
{#if resultProps === undefined || inputValue === ''}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No recent searches</div>
|
||||
{:else if resultProps?.length === 0 && !fetching}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No search results</div>
|
||||
{:else}
|
||||
<div class="py-2">
|
||||
{#each resultProps.slice(0, 5) as result}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<a
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten focus-visible:bg-lighten cursor-pointer outline-none"
|
||||
href={`/${result.type}/${result.tmdbId}`}
|
||||
on:click={() => {
|
||||
modalProps.close();
|
||||
clear();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_POSTER_SMALL + result.posterUri + "');"}
|
||||
class="bg-center bg-cover w-16 h-24 rounded-sm"
|
||||
/>
|
||||
<div class="flex-1 flex flex-col gap-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="font-normal tracking-wide">{result.title}</div>
|
||||
<div class="text-zinc-400">
|
||||
{result.year}
|
||||
</div>
|
||||
{#if result.seasons}
|
||||
<div class="text-zinc-400">{result.seasons}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-300 line-clamp-3">{result.overview}</div>
|
||||
<ModalContent>
|
||||
<ModalHeader close={handleClose}>
|
||||
<MagnifyingGlass size={20} class="text-zinc-400" />
|
||||
<input
|
||||
bind:value={inputValue}
|
||||
bind:this={inputElement}
|
||||
on:input={handleInput}
|
||||
type="text"
|
||||
class="flex-1 bg-transparent font-light outline-none"
|
||||
placeholder="Search for Movies and Shows..."
|
||||
/>
|
||||
</ModalHeader>
|
||||
{#if resultProps === undefined || inputValue === ''}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No recent searches</div>
|
||||
{:else if resultProps?.length === 0 && !fetching}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No search results</div>
|
||||
{:else}
|
||||
<div class="py-2">
|
||||
{#each resultProps.slice(0, 5) as result}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<a
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten focus-visible:bg-lighten cursor-pointer outline-none"
|
||||
href={`/${result.type}/${result.tmdbId}`}
|
||||
on:click={handleClose}
|
||||
>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_POSTER_SMALL + result.posterUri + "');"}
|
||||
class="bg-center bg-cover w-16 h-24 rounded-sm"
|
||||
/>
|
||||
<div class="flex-1 flex flex-col gap-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="font-normal tracking-wide">{result.title}</div>
|
||||
<div class="text-zinc-400">
|
||||
{result.year}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/if}
|
||||
{#if result.seasons}
|
||||
<div class="text-zinc-400">{result.seasons}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-300 line-clamp-3">{result.overview}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
import classNames from 'classnames';
|
||||
import PlayButton from '../PlayButton.svelte';
|
||||
import ProgressBar from '../ProgressBar.svelte';
|
||||
import type { TitleType } from '$lib/types';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let jellyfinId: string = '';
|
||||
export let type: 'movie' | 'series' = 'movie';
|
||||
export let type: TitleType = 'movie';
|
||||
export let backdropUri: string;
|
||||
|
||||
export let title = '';
|
||||
|
||||
@@ -1,60 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { fetchSonarrEpisodes, type SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import type { ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import RequestModal from './RequestModal.svelte';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let modalId: Symbol;
|
||||
export let groupId: Symbol;
|
||||
|
||||
export let sonarrId: number;
|
||||
export let seasonNumber: number;
|
||||
export let selectEpisode: (episode: SonarrEpisode) => void;
|
||||
|
||||
async function fetchEpisodes(sonarrId: number, seasonNumber: number) {
|
||||
return fetchSonarrEpisodes(sonarrId).then((episodes) =>
|
||||
episodes.filter((episode) => episode.seasonNumber === seasonNumber)
|
||||
);
|
||||
}
|
||||
|
||||
function selectEpisode(episode: SonarrEpisode) {
|
||||
modalStack.create(
|
||||
RequestModal,
|
||||
{
|
||||
episode,
|
||||
sonarrId,
|
||||
groupId
|
||||
},
|
||||
groupId
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} text="Seasons" />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#await fetchEpisodes(sonarrId, seasonNumber)}
|
||||
Loading...
|
||||
{:then episodes}
|
||||
{#if episodes.length === 0}
|
||||
<div class="px-4 py-1 text-xs text-gray-400">No episodes</div>
|
||||
{:else}
|
||||
{#each episodes as episode}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="px-4 py-1 flex flex-row items-center justify-between cursor-pointer hover:bg-lighten"
|
||||
on:click={() => selectEpisode(episode)}
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium">{episode.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{episode.episodeNumber ? `Episode ${episode.episodeNumber}` : 'Special'}
|
||||
</div>
|
||||
</div>
|
||||
<ModalContainer>
|
||||
<ModalHeader
|
||||
close={() => modalStack.closeGroup(groupId)}
|
||||
back={() => modalStack.close(modalId)}
|
||||
text="Seasons"
|
||||
/>
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#await fetchEpisodes(sonarrId, seasonNumber)}
|
||||
Loading...
|
||||
{:then episodes}
|
||||
{#if episodes.length === 0}
|
||||
<div class="px-4 py-1 text-xs text-gray-400">No episodes</div>
|
||||
{:else}
|
||||
{#each episodes as episode}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="px-4 py-1 flex flex-row items-center justify-between cursor-pointer hover:bg-lighten"
|
||||
on:click={() => selectEpisode(episode)}
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium">{episode.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{new Date(episode.airDate || Date.now()).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
{episode.episodeNumber ? `Episode ${episode.episodeNumber}` : 'Special'}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
<div class="text-xs text-gray-400">
|
||||
{new Date(episode.airDate || Date.now()).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import HeightHider from '../HeightHider.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContent from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import type { ModalProps } from '../Modal/Modal';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// TODO: Switch to grid
|
||||
export let modalId: Symbol;
|
||||
export let groupId: Symbol | undefined = undefined;
|
||||
|
||||
export let title = 'Releases';
|
||||
export let modalProps: ModalProps;
|
||||
export let radarrId: number | undefined = undefined;
|
||||
export let sonarrEpisodeId: number | undefined = undefined;
|
||||
export let seasonPack: { sonarrId: number; seasonNumber: number } | undefined = undefined;
|
||||
@@ -100,76 +100,78 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContent>
|
||||
<ModalHeader {...modalProps} text={title} />
|
||||
{#await fetchReleases()}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">Loading...</div>
|
||||
{:then { releases, filtered, releasesSkipped }}
|
||||
{#if showAllReleases ? releases?.length : filtered?.length}
|
||||
<div class="flex flex-col py-2 divide-y divide-zinc-700 max-h-[60vh] overflow-y-scroll">
|
||||
{#each showAllReleases ? releases : filtered as release}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten items-center justify-between cursor-pointer text-sm"
|
||||
on:click={() => toggleShowDetails(release.guid || null)}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="tracking-wide font-medium">{release.indexer}</div>
|
||||
<div class="text-zinc-400">{release?.quality?.quality?.name}</div>
|
||||
<div class="text-zinc-400">{release.seeders} seeders</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-zinc-400">{formatSize(release?.size || 0)}</div>
|
||||
{#if release.guid !== downloadingGuid}
|
||||
<IconButton
|
||||
on:click={() => release.guid && handleDownload(release.guid)}
|
||||
disabled={downloadFetchingGuid === release.guid}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<div class="p-1">
|
||||
<Download size={20} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ModalContent>
|
||||
<ModalHeader
|
||||
back={groupId ? () => modalStack.close(modalId) : undefined}
|
||||
close={() => (groupId ? modalStack.closeGroup(groupId) : modalStack.close(modalId))}
|
||||
text={title}
|
||||
/>
|
||||
{#await fetchReleases()}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">Loading...</div>
|
||||
{:then { releases, filtered, releasesSkipped }}
|
||||
{#if showAllReleases ? releases?.length : filtered?.length}
|
||||
<div class="flex flex-col divide-y divide-zinc-700 max-h-[60vh] overflow-y-scroll">
|
||||
{#each showAllReleases ? releases : filtered as release}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten items-center justify-between cursor-pointer text-sm"
|
||||
on:click={() => toggleShowDetails(release.guid || null)}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="tracking-wide font-medium">{release.indexer}</div>
|
||||
<div class="text-zinc-400">{release?.quality?.quality?.name}</div>
|
||||
<div class="text-zinc-400">{release.seeders} seeders</div>
|
||||
</div>
|
||||
<HeightHider visible={showDetailsId === release.guid}>
|
||||
<div class="flex gap-1 text-xs text-zinc-400 px-4 py-2 items-center flex-wrap">
|
||||
<div>
|
||||
{release.title}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-zinc-400">{formatSize(release?.size || 0)}</div>
|
||||
{#if release.guid !== downloadingGuid}
|
||||
<IconButton
|
||||
on:click={() => release.guid && handleDownload(release.guid)}
|
||||
disabled={downloadFetchingGuid === release.guid}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<div class="p-1">
|
||||
<Download size={20} />
|
||||
</div>
|
||||
<DotFilled size={15} />
|
||||
<div>{formatMinutesToTime(release.ageMinutes || 0)} old</div>
|
||||
<DotFilled size={15} />
|
||||
<div><b>{release.seeders} seeders</b> / {release.leechers} leechers</div>
|
||||
<DotFilled size={15} />
|
||||
{#if release.seeders}
|
||||
<div>
|
||||
{formatSize((release.size || 0) / release.seeders)} per seeder
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</HeightHider>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if releasesSkipped > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="text-sm text-zinc-200 opacity-50 font-light px-4 py-2 hover:underline cursor-pointer"
|
||||
on:click={toggleShowAll}
|
||||
>
|
||||
{showAllReleases ? 'Show less' : `Show all ${releasesSkipped} releases`}
|
||||
<HeightHider visible={showDetailsId === release.guid}>
|
||||
<div class="flex gap-1 text-xs text-zinc-400 px-4 py-2 items-center flex-wrap">
|
||||
<div>
|
||||
{release.title}
|
||||
</div>
|
||||
<DotFilled size={15} />
|
||||
<div>{formatMinutesToTime(release.ageMinutes || 0)} old</div>
|
||||
<DotFilled size={15} />
|
||||
<div><b>{release.seeders} seeders</b> / {release.leechers} leechers</div>
|
||||
<DotFilled size={15} />
|
||||
{#if release.seeders}
|
||||
<div>
|
||||
{formatSize((release.size || 0) / release.seeders)} per seeder
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</HeightHider>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No releases found.</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if releasesSkipped > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="text-sm text-zinc-200 opacity-50 font-light px-4 py-2 hover:underline cursor-pointer"
|
||||
on:click={toggleShowAll}
|
||||
>
|
||||
{showAllReleases ? 'Show less' : `Show all ${releasesSkipped} releases`}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{:else}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No releases found.</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { SonarrSeries } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { createModalProps, type ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let sonarrId: number;
|
||||
export let sonarrSeries: SonarrSeries;
|
||||
|
||||
// let selectedEpisode: SonarrEpisode | undefined;
|
||||
let requestModalProps = createModalProps(() => {
|
||||
modalProps.close();
|
||||
// selectedEpisode = undefined;
|
||||
});
|
||||
|
||||
// async function fetchEpisodes(sonarrId: number) {
|
||||
// return fetchSonarrEpisodes(sonarrId).then((episodes) => {
|
||||
// // Group episodes by season
|
||||
// const seasons: SonarrEpisode[][] = [];
|
||||
// episodes.forEach((episode) => {
|
||||
// if (!episode.seasonNumber) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!seasons[episode.seasonNumber - 1]) {
|
||||
// seasons[episode.seasonNumber - 1] = [];
|
||||
// }
|
||||
// seasons[episode.seasonNumber - 1].push(episode);
|
||||
// });
|
||||
|
||||
// return seasons;
|
||||
// });
|
||||
// }
|
||||
|
||||
async function fetchSeasons() {}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} text="Seasons" />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#each sonarrSeries.seasons || [] as season}
|
||||
<div>{season.seasonNumber}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
|
||||
{console.log(sonarrSeries)}
|
||||
|
||||
<!-- {#if selectedEpisode?.id}
|
||||
<RequestModal
|
||||
modalProps={requestModalProps}
|
||||
sonarrEpisodeId={selectedEpisode.id}
|
||||
title={selectedEpisode.title || undefined}
|
||||
/>
|
||||
{/if} -->
|
||||
@@ -1,100 +1,65 @@
|
||||
<script lang="ts">
|
||||
import type { SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { createModalProps, type ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import EpisodeSelectModal from './EpisodeSelectModal.svelte';
|
||||
import RequestModal from './RequestModal.svelte';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let modalId: Symbol;
|
||||
export let sonarrId: number;
|
||||
export let seasons: number;
|
||||
export let heading = 'Seasons';
|
||||
|
||||
let episodeSelectProps: Omit<ComponentProps<EpisodeSelectModal>, 'modalProps'> | undefined =
|
||||
undefined;
|
||||
let episodeSelectModalProps = createModalProps(
|
||||
() => {
|
||||
episodeSelectProps = undefined;
|
||||
modalProps.close();
|
||||
},
|
||||
() => {
|
||||
episodeSelectProps = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
let requestProps: Omit<ComponentProps<RequestModal>, 'modalProps'> | undefined = undefined;
|
||||
let requestModalProps = createModalProps(
|
||||
() => {
|
||||
requestProps = undefined;
|
||||
episodeSelectModalProps.close();
|
||||
},
|
||||
() => {
|
||||
requestProps = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
function selectSeasonPack(seasonNumber: number) {
|
||||
requestProps = {
|
||||
seasonPack: {
|
||||
sonarrId,
|
||||
seasonNumber
|
||||
}
|
||||
};
|
||||
modalStack.create(
|
||||
RequestModal,
|
||||
{
|
||||
seasonPack: {
|
||||
sonarrId,
|
||||
seasonNumber
|
||||
}
|
||||
},
|
||||
modalId
|
||||
);
|
||||
}
|
||||
|
||||
function selectSeason(seasonNumber: number) {
|
||||
episodeSelectProps = {
|
||||
seasonNumber,
|
||||
selectEpisode,
|
||||
sonarrId
|
||||
};
|
||||
}
|
||||
|
||||
function selectEpisode(episode: SonarrEpisode) {
|
||||
requestProps = {
|
||||
sonarrEpisodeId: episode.id,
|
||||
title: episode.title || 'Episode'
|
||||
};
|
||||
modalStack.create(
|
||||
EpisodeSelectModal,
|
||||
{
|
||||
seasonNumber,
|
||||
sonarrId,
|
||||
groupId: modalId
|
||||
},
|
||||
modalId
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} back={undefined} text={heading} />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#each [...Array(seasons).keys()].map((i) => i + 1) as seasonNumber}
|
||||
<div
|
||||
class="px-4 py-3 flex justify-between items-center text-zinc-300 group-hover:text-zinc-300"
|
||||
>
|
||||
<div class="font-medium">
|
||||
Season {seasonNumber}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeasonPack(seasonNumber)}>
|
||||
<span>Season Packs</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeason(seasonNumber)}>
|
||||
<span>Episodes</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<ModalContainer>
|
||||
<ModalHeader close={() => modalStack.close(modalId)} back={undefined} text={heading} />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#each [...Array(seasons).keys()].map((i) => i + 1) as seasonNumber}
|
||||
<div
|
||||
class="px-4 py-3 flex justify-between items-center text-zinc-300 group-hover:text-zinc-300"
|
||||
>
|
||||
<div class="font-medium">
|
||||
Season {seasonNumber}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
|
||||
{#if episodeSelectProps}
|
||||
<EpisodeSelectModal modalProps={episodeSelectModalProps} {...episodeSelectProps} />
|
||||
{/if}
|
||||
|
||||
{#if requestProps}
|
||||
<RequestModal modalProps={requestModalProps} {...requestProps} />
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeasonPack(seasonNumber)}>
|
||||
<span>Season Packs</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeason(seasonNumber)}>
|
||||
<span>Episodes</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { TMDB_IMAGES_ORIGINAL, TMDB_POSTER_SMALL } from '$lib/constants';
|
||||
import classNames from 'classnames';
|
||||
import { ChevronLeft, DotFilled, ExternalLink } from 'radix-icons-svelte';
|
||||
import { ChevronLeft, Cross2, DotFilled, ExternalLink } from 'radix-icons-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Carousel from '../Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '../Carousel/CarouselPlaceholderItems.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import type { TitleType } from '$lib/types';
|
||||
|
||||
export let isModal = false;
|
||||
export let handleCloseModal: () => void = () => {};
|
||||
|
||||
export let tmdbId: number;
|
||||
export let type: 'movie' | 'series';
|
||||
export let type: TitleType;
|
||||
|
||||
export let backdropUriCandidates: string[];
|
||||
export let posterPath: string;
|
||||
@@ -79,11 +80,16 @@
|
||||
<ExternalLink size={20} />
|
||||
</IconButton>
|
||||
</a>
|
||||
<div class="sm:hidden absolute top-8 left-4 sm:left-8 z-10">
|
||||
<button class="flex items-center" on:click={handleCloseModal}>
|
||||
<div class="absolute top-8 left-4 sm:left-8 z-10">
|
||||
<button class="flex items-center sm:hidden" on:click={handleCloseModal}>
|
||||
<ChevronLeft size={20} />
|
||||
Back
|
||||
</button>
|
||||
<div class="hidden sm:block">
|
||||
<IconButton on:click={handleCloseModal}>
|
||||
<Cross2 size={20} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-stone-950 to-30%" />
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import MoviePage from '../../../routes/movie/[id]/MoviePage.svelte';
|
||||
import SeriesPage from '../../../routes/series/[id]/SeriesPage.svelte';
|
||||
import { createModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { titlePageModal } from './TitlePageModal';
|
||||
import { onMount } from 'svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import type { TitleType } from '$lib/types';
|
||||
|
||||
const modalProps = createModalProps(() => {
|
||||
titlePageModal.close();
|
||||
});
|
||||
export let tmdbId: number;
|
||||
export let type: TitleType;
|
||||
export let modalId: Symbol;
|
||||
|
||||
function handleCloseModal() {
|
||||
modalProps.close();
|
||||
modalStack.close(modalId);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
console.log('modal mounted');
|
||||
console.log('TitlePageModal mounted');
|
||||
|
||||
return () => modalProps.close();
|
||||
return () => console.log('TitlePageModal unmounted');
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $titlePageModal.tmdbId}
|
||||
{@const tmdbId = $titlePageModal.tmdbId}
|
||||
<Modal {...modalProps}>
|
||||
<div
|
||||
class="max-w-screen-2xl overflow-x-hidden overflow-y-scroll h-screen sm:mx-4 lg:mx-12 xl:mx-16 scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
class="relative overflow-hidden"
|
||||
in:fly|global={{ y: 20, duration: 200, delay: 200 }}
|
||||
out:fly|global={{ y: 20, duration: 200 }}
|
||||
>
|
||||
{#if $titlePageModal.type === 'movie'}
|
||||
<MoviePage {tmdbId} isModal={true} {handleCloseModal} />
|
||||
{:else}
|
||||
<SeriesPage {tmdbId} isModal={true} {handleCloseModal} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
<div
|
||||
class="max-w-screen-2xl overflow-x-hidden overflow-y-scroll h-screen sm:mx-4 lg:mx-12 xl:mx-16 scrollbar-hide"
|
||||
>
|
||||
<div
|
||||
class="relative overflow-hidden"
|
||||
in:fly|global={{ y: 20, duration: 200, delay: 200 }}
|
||||
out:fly|global={{ y: 20, duration: 200 }}
|
||||
>
|
||||
{#if type === 'movie'}
|
||||
<MoviePage {tmdbId} isModal={true} {handleCloseModal} />
|
||||
{:else}
|
||||
<SeriesPage {tmdbId} isModal={true} {handleCloseModal} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TitleType } from '$lib/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type Type = 'movie' | 'series' | undefined;
|
||||
type Type = TitleType | undefined;
|
||||
|
||||
function createTitlePageModalStore() {
|
||||
const store = writable<{ tmdbId: number | undefined; type: Type }>({
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import type { TitleType } from '$lib/types';
|
||||
|
||||
const TRAILER_TIMEOUT = 3000;
|
||||
const TRAILER_LOAD_TIME = 1000;
|
||||
const ANIMATION_DURATION = 150;
|
||||
|
||||
export let tmdbId: number;
|
||||
export let type: 'movie' | 'series';
|
||||
export let type: TitleType;
|
||||
|
||||
export let title: string;
|
||||
export let genres: string[];
|
||||
|
||||
@@ -13,23 +13,17 @@
|
||||
import { Cross2 } from 'radix-icons-svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { playerState, type PlayerStateValue } from './VideoPlayer';
|
||||
import { createModalProps } from '../Modal/Modal';
|
||||
import { playerState } from './VideoPlayer';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
|
||||
let modalProps = createModalProps(() => {
|
||||
playerState.close();
|
||||
video?.pause();
|
||||
clearInterval(progressInterval);
|
||||
stopCallback?.();
|
||||
});
|
||||
export let modalId: Symbol;
|
||||
|
||||
let uiVisible = false;
|
||||
|
||||
let video: HTMLVideoElement;
|
||||
|
||||
let mouseMovementTimeout: NodeJS.Timeout;
|
||||
let stopCallback: () => void;
|
||||
|
||||
let progressInterval: ReturnType<typeof setInterval>;
|
||||
onDestroy(() => clearInterval(progressInterval));
|
||||
let progressInterval: NodeJS.Timeout;
|
||||
|
||||
const fetchPlaybackInfo = (itemId: string) =>
|
||||
getJellyfinPlaybackInfo(itemId, getDeviceProfile()).then(
|
||||
@@ -65,46 +59,47 @@
|
||||
}
|
||||
);
|
||||
|
||||
let uiVisible = false;
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
function handleClose() {
|
||||
playerState.close();
|
||||
video?.pause();
|
||||
clearInterval(progressInterval);
|
||||
stopCallback?.();
|
||||
modalStack.close(modalId);
|
||||
}
|
||||
|
||||
function handleMouseMove() {
|
||||
uiVisible = true;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
clearTimeout(mouseMovementTimeout);
|
||||
mouseMovementTimeout = setTimeout(() => {
|
||||
uiVisible = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
let state: PlayerStateValue;
|
||||
playerState.subscribe((s) => (state = s));
|
||||
onDestroy(() => clearInterval(progressInterval));
|
||||
|
||||
$: {
|
||||
if (video && state.jellyfinId) {
|
||||
if (video && $playerState.jellyfinId) {
|
||||
if (!Hls.isSupported()) {
|
||||
throw new Error('HLS is not supported');
|
||||
}
|
||||
|
||||
if (video.src === '') fetchPlaybackInfo(state.jellyfinId);
|
||||
if (video.src === '') fetchPlaybackInfo($playerState.jellyfinId);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $playerState.visible}
|
||||
<Modal {...modalProps}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="bg-black w-screen h-screen relative" on:mousemove={handleMouseMove}>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video controls bind:this={video} class="w-full h-full inset-0" />
|
||||
<div
|
||||
class={classNames('absolute top-4 right-8 transition-opacity', {
|
||||
'opacity-0': !uiVisible,
|
||||
'opacity-100': uiVisible
|
||||
})}
|
||||
>
|
||||
<IconButton on:click={modalProps.close}>
|
||||
<Cross2 size={25} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="bg-black w-screen h-screen relative" on:mousemove={handleMouseMove}>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video controls bind:this={video} class="w-full h-full inset-0" />
|
||||
<div
|
||||
class={classNames('absolute top-4 right-8 transition-opacity', {
|
||||
'opacity-0': !uiVisible,
|
||||
'opacity-100': uiVisible
|
||||
})}
|
||||
>
|
||||
<IconButton on:click={handleClose}>
|
||||
<Cross2 size={25} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import { writable } from 'svelte/store';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import VideoPlayer from './VideoPlayer.svelte';
|
||||
|
||||
const initialValue = { visible: false, jellyfinId: '' };
|
||||
export type PlayerStateValue = typeof initialValue;
|
||||
@@ -11,6 +13,7 @@ function createPlayerState() {
|
||||
...store,
|
||||
streamJellyfinId: (id: string) => {
|
||||
store.set({ visible: true, jellyfinId: id });
|
||||
modalStack.create(VideoPlayer, {}); // FIXME
|
||||
},
|
||||
close: () => {
|
||||
store.set({ visible: false, jellyfinId: '' });
|
||||
|
||||
Reference in New Issue
Block a user