Fully working sonarr integration
This commit is contained in:
@@ -3,17 +3,27 @@
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
|
||||
|
||||
export let gradientFromColor = 'from-stone-900';
|
||||
|
||||
let carousel: HTMLDivElement | undefined;
|
||||
let scrollX: number;
|
||||
let scrollX = 0;
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between items-center mx-8 gap-4">
|
||||
<slot name="title" />
|
||||
<div class="flex gap-2">
|
||||
<IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<IconButton
|
||||
on:click={() => {
|
||||
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth, behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -30,13 +40,13 @@
|
||||
{#if scrollX > 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="absolute inset-y-4 left-0 w-24 bg-gradient-to-r from-darken"
|
||||
class={'absolute inset-y-4 left-0 w-24 bg-gradient-to-r ' + gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="absolute inset-y-4 right-0 w-24 bg-gradient-to-l from-darken"
|
||||
class={'absolute inset-y-4 right-0 w-24 bg-gradient-to-l ' + gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { modalStack } from './Modal';
|
||||
|
||||
export let visible = false;
|
||||
// export let visible = false;
|
||||
export let close: () => void;
|
||||
export let id: Symbol;
|
||||
|
||||
const modalId = Symbol();
|
||||
|
||||
$: {
|
||||
if (visible) {
|
||||
modalStack.push(modalId);
|
||||
} else {
|
||||
modalStack.remove(modalId);
|
||||
}
|
||||
}
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && visible && $modalStack.top === modalId) {
|
||||
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 visible && $modalStack.top === modalId}
|
||||
{#if $modalStack.top === id}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames('fixed inset-0 bg-stone-900 bg-opacity-50 justify-center items-center z-20', {
|
||||
hidden: !visible,
|
||||
'flex overflow-hidden': visible
|
||||
})}
|
||||
class="fixed inset-0 justify-center items-center z-20 overflow-hidden flex"
|
||||
on:click|self={close}
|
||||
transition:fade={{ duration: 100 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,10 @@ function createModalStack() {
|
||||
...store,
|
||||
push: (symbol: Symbol) => {
|
||||
store.update((s) => {
|
||||
if (s.stack.includes(symbol)) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s.stack.push(symbol);
|
||||
s.top = symbol;
|
||||
return s;
|
||||
@@ -26,3 +30,19 @@ function createModalStack() {
|
||||
}
|
||||
|
||||
export const modalStack = createModalStack();
|
||||
|
||||
export type ModalProps = ReturnType<typeof createModalProps>;
|
||||
|
||||
export function createModalProps(onClose: () => void) {
|
||||
const id = Symbol();
|
||||
|
||||
function close() {
|
||||
onClose(); // ORDER MATTERS HERE
|
||||
modalStack.remove(id);
|
||||
}
|
||||
|
||||
return {
|
||||
close,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
11
src/lib/components/Modal/ModalBackground.svelte
Normal file
11
src/lib/components/Modal/ModalBackground.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<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={{ duration: 100 }}
|
||||
/>
|
||||
{/if}
|
||||
10
src/lib/components/Modal/ModalContainer.svelte
Normal file
10
src/lib/components/Modal/ModalContainer.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="max-w-3xl self-start mt-[10vh] bg-[#33333388] backdrop-blur-xl rounded overflow-hidden flex flex-col flex-1 mx-4 sm:mx-16 lg:mx-24 drop-shadow-xl"
|
||||
in:fly|global={{ y: 20, duration: 200 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,10 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="max-w-3xl self-start mt-[10vh] bg-[#33333388] backdrop-blur-xl rounded overflow-hidden flex flex-col flex-1 mx-4 sm:mx-16 lg:mx-24 drop-shadow-xl"
|
||||
in:fly={{ y: 20, duration: 200 }}
|
||||
>
|
||||
<!-- TODO: Add horizontal carousel fade effect -->
|
||||
<div class="max-h-[70vh] scrollbar-hide overflow-y-scroll">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
import { MagnifyingGlass } from 'radix-icons-svelte';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalContent from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import { createModalProps } from '../Modal/Modal';
|
||||
|
||||
export let visible = false;
|
||||
let searchInput: HTMLInputElement;
|
||||
@@ -14,16 +15,16 @@
|
||||
let timeout: NodeJS.Timeout;
|
||||
let fetching = false;
|
||||
let results: MultiSearchResponse['results'] | null = null;
|
||||
|
||||
export let close = (clear = true) => {
|
||||
const modalProps = createModalProps(() => {
|
||||
visible = false;
|
||||
if (clear) {
|
||||
searchValue = '';
|
||||
fetching = false;
|
||||
results = null;
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function clear() {
|
||||
searchValue = '';
|
||||
fetching = false;
|
||||
results = null;
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
const searchTimeout = () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -52,54 +53,58 @@
|
||||
event.preventDefault();
|
||||
visible = true;
|
||||
} else if (event.key === 'Escape' && visible) {
|
||||
close(false);
|
||||
clear();
|
||||
modalProps.close();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleShortcuts} />
|
||||
|
||||
<Modal {visible} {close}>
|
||||
<ModalContent>
|
||||
<ModalHeader {close}>
|
||||
<MagnifyingGlass size={20} class="text-zinc-400" />
|
||||
<input
|
||||
bind:value={searchValue}
|
||||
bind:this={searchInput}
|
||||
on:input={searchTimeout}
|
||||
type="text"
|
||||
class="flex-1 bg-transparent font-light outline-none"
|
||||
placeholder="Search for Movies and Shows..."
|
||||
/>
|
||||
</ModalHeader>
|
||||
{#if !results || searchValue === ''}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No recent searches</div>
|
||||
{:else if results?.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 results.filter((m) => m).slice(0, 5) as result}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten cursor-pointer"
|
||||
on:click={() => (window.location.href = '/movie/' + result.id)}
|
||||
>
|
||||
{#if visible}
|
||||
<Modal {...modalProps}>
|
||||
<ModalContent>
|
||||
<ModalHeader {...modalProps}>
|
||||
<MagnifyingGlass size={20} class="text-zinc-400" />
|
||||
<input
|
||||
bind:value={searchValue}
|
||||
bind:this={searchInput}
|
||||
on:input={searchTimeout}
|
||||
type="text"
|
||||
class="flex-1 bg-transparent font-light outline-none"
|
||||
placeholder="Search for Movies and Shows..."
|
||||
/>
|
||||
</ModalHeader>
|
||||
{#if !results || searchValue === ''}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No recent searches</div>
|
||||
{:else if results?.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 results.filter((m) => m).slice(0, 5) as result}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_IMAGES + result.poster_path + "');"}
|
||||
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.original_title}</div>
|
||||
<div class="text-zinc-400">
|
||||
{new Date(result.release_date).getFullYear()}
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten cursor-pointer"
|
||||
on:click={() => (window.location.href = '/movie/' + result.id)}
|
||||
>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_IMAGES + result.poster_path + "');"}
|
||||
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.original_title}</div>
|
||||
<div class="text-zinc-400">
|
||||
{new Date(result.release_date).getFullYear()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-zinc-300 line-clamp-3">{result.overview}</div>
|
||||
</div>
|
||||
<div class="text-sm text-zinc-300 line-clamp-3">{result.overview}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { TmdbApi } from '$lib/apis/tmdb/tmdbApi';
|
||||
import type { TmdbMovie } from '$lib/apis/tmdb/tmdbApi';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
import { formatMinutesToTime } from '$lib/utils';
|
||||
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import type { PlayerState } from '../VideoPlayer/VideoPlayer';
|
||||
import { onMount } from 'svelte';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
|
||||
export let tmdbId: string;
|
||||
export let progress = 0;
|
||||
@@ -13,8 +12,6 @@
|
||||
export let randomProgress = false;
|
||||
if (randomProgress) progress = Math.random() > 0.5 ? Math.round(Math.random() * 100) : 100;
|
||||
|
||||
const { streamJellyfinId } = getContext<PlayerState>('player');
|
||||
|
||||
export let type: 'movie' | 'tv' = 'movie';
|
||||
|
||||
let bg = '';
|
||||
@@ -34,7 +31,7 @@
|
||||
if (streamFetching || !tmdbId) return;
|
||||
streamFetching = true;
|
||||
getJellyfinItemByTmdbId(tmdbId).then((item: any) => {
|
||||
if (item.Id) streamJellyfinId(item.Id);
|
||||
if (item.Id) playerState.streamJellyfinId(item.Id);
|
||||
streamFetching = false;
|
||||
});
|
||||
}
|
||||
@@ -53,7 +50,9 @@
|
||||
class="bg-center bg-cover aspect-[2/3] h-72 m-1.5"
|
||||
style={"background-image: url('" + bg + "')"}
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="w-full h-full hover:bg-darken transition-all flex">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity p-2 flex flex-col justify-between flex-1 cursor-pointer"
|
||||
on:click={() => (window.location.href = '/' + type + '/' + tmdbId)}
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
import HeightHider from '../HeightHider.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
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 visible = true; // FIXME
|
||||
export let title = 'Releases';
|
||||
export let modalProps: ModalProps;
|
||||
export let radarrId: number | undefined = undefined;
|
||||
export let sonarrEpisodeId: number | undefined = undefined;
|
||||
|
||||
@@ -22,12 +24,6 @@
|
||||
let downloadFetchingGuid: string | undefined;
|
||||
let downloadingGuid: string | undefined;
|
||||
|
||||
let releasesResponse: ReturnType<typeof fetchReleases>;
|
||||
|
||||
$: if (visible && !releasesResponse) {
|
||||
releasesResponse = fetchReleases();
|
||||
}
|
||||
|
||||
async function fetchReleases() {
|
||||
if (!radarrId && !sonarrEpisodeId) {
|
||||
return {
|
||||
@@ -85,18 +81,12 @@
|
||||
showDetailsId = id;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
downloadFetchingGuid = undefined;
|
||||
downloadingGuid = undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {visible} {close}>
|
||||
<Modal {...modalProps}>
|
||||
<ModalContent>
|
||||
<ModalHeader {close} text="Releases" />
|
||||
{#await releasesResponse}
|
||||
<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}
|
||||
@@ -150,7 +140,9 @@
|
||||
</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}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { fetchSonarrEpisodes, type SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { ChevronUp } from 'radix-icons-svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { createModalProps, type ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import RequestModal from './RequestModal.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
|
||||
export let visible = false;
|
||||
export let modalProps: ModalProps;
|
||||
export let sonarrId: number;
|
||||
|
||||
let episodesPromise: ReturnType<typeof fetchEpisodes>;
|
||||
|
||||
$: if (visible && !episodesPromise) {
|
||||
episodesPromise = fetchEpisodes(sonarrId);
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
let selectedEpisode: SonarrEpisode | undefined;
|
||||
let requestModalProps = createModalProps(() => {
|
||||
modalProps.close();
|
||||
selectedEpisode = undefined;
|
||||
});
|
||||
|
||||
async function fetchEpisodes(sonarrId: number) {
|
||||
return fetchSonarrEpisodes(sonarrId).then((episodes) => {
|
||||
@@ -37,43 +38,64 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {visible} {close}>
|
||||
<ModalContent>
|
||||
<ModalHeader {close} text="Seasons" />
|
||||
<div class="flex flex-col gap-2 sm:gap-4">
|
||||
{#await episodesPromise then seasons}
|
||||
{console.log('saesons', seasons)}
|
||||
{#each seasons as episodes, i}
|
||||
{#if i > 0}
|
||||
<div class="border-t border-gray-200" />
|
||||
{/if}
|
||||
|
||||
{#each episodes as episode}
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{episode.episodeNumber}. {episode.title}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{episode.airDate}
|
||||
</div>
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} text="Seasons" />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#await fetchEpisodes(sonarrId)}
|
||||
Loading...
|
||||
{:then seasons}
|
||||
{#each seasons as episodes, i}
|
||||
<div class="pb-2">
|
||||
<div
|
||||
class="px-4 py-3 flex justify-between items-center cursor-pointer text-zinc-300 group-hover:text-zinc-300"
|
||||
>
|
||||
<div class="uppercase font-bold text-sm">
|
||||
Season {i + 1}
|
||||
</div>
|
||||
<ChevronUp size={20} />
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{episode.episodeNumber}. {episode.title}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{episode.airDate}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#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={() => (selectedEpisode = 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>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</ModalContent>
|
||||
{/await}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
|
||||
{#if selectedEpisode?.id}
|
||||
<RequestModal
|
||||
modalProps={requestModalProps}
|
||||
sonarrEpisodeId={selectedEpisode.id}
|
||||
title={selectedEpisode.title || undefined}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -16,21 +16,21 @@
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import { ChevronDown, Plus, Trash, Update } from 'radix-icons-svelte';
|
||||
import { getContext, onMount, type ComponentProps } from 'svelte';
|
||||
import { onMount, type ComponentProps } from 'svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { createModalProps } from '../Modal/Modal';
|
||||
import RequestModal from '../RequestModal/RequestModal.svelte';
|
||||
import type { PlayerState } from '../VideoPlayer/VideoPlayer';
|
||||
import LibraryDetailsFile from './LibraryDetailsFile.svelte';
|
||||
import SeriesRequestModal from '../RequestModal/SeriesRequestModal.svelte';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
import LibraryDetailsFile from './LibraryDetailsFile.svelte';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let type: 'movie' | 'tv';
|
||||
|
||||
let { streamJellyfinId } = getContext<PlayerState>('player');
|
||||
|
||||
let servarrId: number | undefined = undefined;
|
||||
let isAdded = false;
|
||||
let isRequestModalVisible = false;
|
||||
const requestModalProps = createModalProps(() => (isRequestModalVisible = false));
|
||||
|
||||
let downloadProps: ComponentProps<LibraryDetailsFile>[] = [];
|
||||
let movieFileProps: ComponentProps<LibraryDetailsFile>[] = [];
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
jellyfinStreamDisabled: !jellyfinItem,
|
||||
openJellyfinStream: () => {
|
||||
if (jellyfinItem?.Id) streamJellyfinId(jellyfinItem.Id);
|
||||
if (jellyfinItem?.Id) playerState.streamJellyfinId(jellyfinItem.Id);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -138,7 +138,7 @@
|
||||
},
|
||||
jellyfinStreamDisabled: !jellyfinEpisode,
|
||||
openJellyfinStream: () => {
|
||||
if (jellyfinEpisode?.Id) streamJellyfinId(jellyfinEpisode.Id);
|
||||
if (jellyfinEpisode?.Id) playerState.streamJellyfinId(jellyfinEpisode.Id);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -326,15 +326,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isAdded && servarrId && type === 'movie'}
|
||||
<RequestModal
|
||||
bind:visible={isRequestModalVisible}
|
||||
radarrId={servarrId}
|
||||
on:download={() => setTimeout(refetch, 5000)}
|
||||
/>
|
||||
{:else if isAdded && servarrId && type === 'tv'}
|
||||
<SeriesRequestModal bind:visible={isRequestModalVisible} sonarrId={servarrId} />
|
||||
{:else}
|
||||
<div>NO CONTENT</div>
|
||||
{console.log('NO CONTENT')}
|
||||
{#if isRequestModalVisible}
|
||||
{#if isAdded && servarrId && type === 'movie'}
|
||||
<RequestModal
|
||||
modalProps={requestModalProps}
|
||||
radarrId={servarrId}
|
||||
on:download={() => setTimeout(refetch, 5000)}
|
||||
/>
|
||||
{:else if isAdded && servarrId && type === 'tv'}
|
||||
<SeriesRequestModal modalProps={requestModalProps} sonarrId={servarrId} />
|
||||
{:else}
|
||||
<div>NO CONTENT</div>
|
||||
{console.log('NO CONTENT')}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinEpisodes, getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getJellyfinEpisodes } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getTmdbSeriesSeasons, type CastMember, type Video } from '$lib/apis/tmdb/tmdbApi';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import { formatMinutesToTime } from '$lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { ChevronDown, Clock } from 'radix-icons-svelte';
|
||||
import { getContext, type ComponentProps } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import EpisodeCard from '../EpisodeCard/EpisodeCard.svelte';
|
||||
import HeightHider from '../HeightHider.svelte';
|
||||
import type { PlayerState } from '../VideoPlayer/VideoPlayer';
|
||||
import SeasonsDetails from './SeasonsDetails.svelte';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
import LibraryDetails from './LibraryDetails.svelte';
|
||||
import SeasonsDetails from './SeasonsDetails.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let type: 'movie' | 'tv';
|
||||
@@ -56,7 +56,6 @@
|
||||
|
||||
// Transitions
|
||||
const duration = 200;
|
||||
const { streamJellyfinId } = getContext<PlayerState>('player');
|
||||
|
||||
library.subscribe(async (libraryPromise) => {
|
||||
const libraryData = await libraryPromise;
|
||||
@@ -422,7 +421,8 @@
|
||||
<Button
|
||||
disabled={streamButtonDisabled}
|
||||
size="lg"
|
||||
on:click={() => jellyfinId && streamJellyfinId(jellyfinId)}>Stream</Button
|
||||
on:click={() => jellyfinId && playerState.streamJellyfinId(jellyfinId)}
|
||||
>Stream</Button
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -529,42 +529,9 @@
|
||||
|
||||
<HeightHider duration={1000} visible={detailsVisible}>
|
||||
{#if jellyfinId !== null && type === 'tv'}
|
||||
<SeasonsDetails {tmdbId} totalSeasons={seasons} {jellyfinId} />
|
||||
<SeasonsDetails {tmdbId} totalSeasons={seasons} {jellyfinId} bind:nextEpisodeCardProps />
|
||||
{/if}
|
||||
|
||||
<!-- {#await fetchPlayDetails(tmdbId, seasons)}
|
||||
{#if type === 'tv' && seasons > 0}
|
||||
<div class="py-4">
|
||||
<Carousel>
|
||||
<div slot="title" class="flex gap-4 my-1">
|
||||
{#each [...Array(3).keys()] as _}
|
||||
<div class={'rounded-full p-2 px-6 font-medium placeholder text-transparent'}>
|
||||
Season 1
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#each Array(10) as _, i (i)}
|
||||
<div class="aspect-video h-40 lg:h-48">
|
||||
<CardPlaceholder size="dynamic" />
|
||||
</div>
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
{/if}
|
||||
{:then details}
|
||||
{#key tmdbId}
|
||||
{#if type === 'tv' && seasons > 0}
|
||||
<SeasonsDetails
|
||||
jellyfinEpisodes={details.jellyfinEpisodes || []}
|
||||
tmdbSeasons={details.tmdbSeasons || []}
|
||||
bind:nextEpisodeCardProps
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
{/await} -->
|
||||
|
||||
{#key tmdbId}
|
||||
<div bind:this={localDetails}>
|
||||
<LibraryDetails {tmdbId} {type} />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinEpisodes } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getTmdbSeriesSeasons } from '$lib/apis/tmdb/tmdbApi';
|
||||
import classNames from 'classnames';
|
||||
import { Check, StarFilled } from 'radix-icons-svelte';
|
||||
import { getContext, onMount, type ComponentProps } from 'svelte';
|
||||
import { onMount, type ComponentProps } from 'svelte';
|
||||
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
|
||||
import Carousel from '../Carousel/Carousel.svelte';
|
||||
import UiCarousel from '../Carousel/UICarousel.svelte';
|
||||
import EpisodeCard from '../EpisodeCard/EpisodeCard.svelte';
|
||||
import type { PlayerState } from '../VideoPlayer/VideoPlayer';
|
||||
import { getTmdbSeriesSeasons, type SeasonDetails } from '$lib/apis/tmdb/tmdbApi';
|
||||
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let totalSeasons: number;
|
||||
@@ -51,7 +51,7 @@
|
||||
progress: nextEpisode?.jellyfinEpisode?.UserData?.PlayedPercentage || 0,
|
||||
episodeNumber: `S${tmdbEpisode.season_number}E${tmdbEpisode.episode_number}`,
|
||||
handlePlay: nextEpisode?.jellyfinEpisode?.Id
|
||||
? () => streamJellyfinId(nextEpisode?.jellyfinEpisode?.Id || '')
|
||||
? () => playerState.streamJellyfinId(nextEpisode?.jellyfinEpisode?.Id || '')
|
||||
: undefined
|
||||
}
|
||||
: undefined;
|
||||
@@ -65,97 +65,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
// export let tmdbSeasons: SeasonDetails[];
|
||||
// export let jellyfinEpisodes: Awaited<ReturnType<typeof getJellyfinEpisodes>>;
|
||||
|
||||
const { streamJellyfinId } = getContext<PlayerState>('player');
|
||||
|
||||
// async function fetchSeasons(seasons: number) {
|
||||
// const tmdbSeasonsPromises = getTmdbSeriesSeasons(tmdbId, seasons);
|
||||
|
||||
// const libraryData = await $library;
|
||||
// const jellyfinSeriesId = libraryData.getSeries(tmdbId)?.jellyfinId;
|
||||
// const jellyfinEpisodesPromise = jellyfinSeriesId
|
||||
// ? getJellyfinEpisodes(jellyfinSeriesId)
|
||||
// : undefined;
|
||||
|
||||
// const tmdbSeasons = await tmdbSeasonsPromises;
|
||||
// const jellyfinEpisodes = await jellyfinEpisodesPromise;
|
||||
|
||||
// jellyfinEpisodes?.sort((a, b) => (a.IndexNumber || 99) - (b.IndexNumber || 99));
|
||||
// const nextJellyfinEpisode = jellyfinEpisodes?.find((e) => e?.UserData?.Played === false);
|
||||
|
||||
// const nextEpisode = {
|
||||
// jellyfinEpisode: nextJellyfinEpisode,
|
||||
// tmdbEpisode: nextJellyfinEpisode
|
||||
// ? tmdbSeasons
|
||||
// .flatMap((s) => s?.episodes)
|
||||
// .find(
|
||||
// (e) =>
|
||||
// e?.episode_number === nextJellyfinEpisode.IndexNumber &&
|
||||
// e?.season_number === nextJellyfinEpisode.ParentIndexNumber
|
||||
// )
|
||||
// : undefined
|
||||
// };
|
||||
|
||||
// return {
|
||||
// currentSeason: nextEpisode?.tmdbEpisode?.season_number || 1,
|
||||
// nextEpisode,
|
||||
// tmdbSeasons,
|
||||
// jellyfinEpisodes
|
||||
// };
|
||||
// }
|
||||
|
||||
// const seasonsPromise = fetchSeasons(totalSeasons);
|
||||
// seasonsPromise.then(({ currentSeason }) => (visibleSeason = currentSeason));
|
||||
|
||||
// nextEpisodeCardPropsPromise = seasonsPromise.then(({ nextEpisode }) => {
|
||||
// const tmdbEpisode = nextEpisode?.tmdbEpisode;
|
||||
// if (!tmdbEpisode) return undefined;
|
||||
|
||||
// return {
|
||||
// title: tmdbEpisode.name || '',
|
||||
// subtitle: 'Next Episode',
|
||||
// backdropPath: tmdbEpisode.still_path || '',
|
||||
// runtime: tmdbEpisode.runtime || 0,
|
||||
// progress: nextEpisode?.jellyfinEpisode?.UserData?.PlayedPercentage || 0,
|
||||
// episodeNumber: `S${tmdbEpisode.season_number}E${tmdbEpisode.episode_number}`,
|
||||
// handlePlay: nextEpisode?.jellyfinEpisode?.Id
|
||||
// ? () => streamJellyfinId(nextEpisode?.jellyfinEpisode?.Id || '')
|
||||
// : undefined
|
||||
// };
|
||||
// });
|
||||
|
||||
// async function fetchShowData(tmdbId: number, numberOfSeasons: number) {
|
||||
// const tmdbSeasonsPromises = getTmdbSeriesSeasons(tmdbId, numberOfSeasons);
|
||||
|
||||
// const libraryData = await $library;
|
||||
// const librarySeries = libraryData.getSeries(tmdbId);
|
||||
|
||||
// const sonarrSeriesPromise = librarySeries?.tmdbId
|
||||
// ? getSonarrSeriesByTvdbId(librarySeries.tmdbId)
|
||||
// : undefined;
|
||||
// const sonarrDownloadsPromise = sonarrSeriesPromise?.then((series) =>
|
||||
// series?.id ? getSonarrDownloadsById(series.id) : undefined
|
||||
// );
|
||||
// const sonarrEpisodePromises = sonarrSeriesPromise?.then((series) =>
|
||||
// series ? getSonarrEpisodes(series.id) : undefined
|
||||
// );
|
||||
|
||||
// const jellyfinEpisodesPromise = librarySeries?.jellyfinId
|
||||
// ? getJellyfinEpisodes(librarySeries.jellyfinId)
|
||||
// : undefined;
|
||||
|
||||
// return {
|
||||
// playableSeries: librarySeries,
|
||||
// tmdbSeasons: await tmdbSeasonsPromises,
|
||||
// sonarrSeries: await sonarrSeriesPromise,
|
||||
// sonarrDownloads: await sonarrDownloadsPromise,
|
||||
// sonarrEpisodes: await sonarrEpisodePromises,
|
||||
// jellyfinEpisodes: await jellyfinEpisodesPromise
|
||||
// };
|
||||
// }
|
||||
|
||||
const seriesPromise = fetchSeriesData();
|
||||
|
||||
onMount(() => {
|
||||
@@ -234,7 +143,7 @@
|
||||
size="dynamic"
|
||||
progress={jellyfinEpisode?.UserData?.PlayedPercentage || 0}
|
||||
handlePlay={jellyfinEpisode?.Id
|
||||
? () => streamJellyfinId(jellyfinEpisode?.Id || '')
|
||||
? () => playerState.streamJellyfinId(jellyfinEpisode?.Id || '')
|
||||
: undefined}
|
||||
>
|
||||
<div slot="left-info" class="flex gap-1 items-center">
|
||||
|
||||
@@ -11,18 +11,25 @@
|
||||
|
||||
async function fetchStats() {
|
||||
const discSpacePromise = getDiskSpace();
|
||||
const { movies } = await $library;
|
||||
const availableMovies = movies.filter(
|
||||
(movie) => !movie.download && movie.isAvailable && movie.hasFile
|
||||
const { itemsArray } = await $library;
|
||||
const availableMovies = itemsArray.filter(
|
||||
(item) =>
|
||||
!item.download &&
|
||||
item.radarrMovie &&
|
||||
item.radarrMovie.isAvailable &&
|
||||
item.radarrMovie.movieFile
|
||||
);
|
||||
|
||||
const diskSpaceInfo =
|
||||
(await discSpacePromise).find((disk) => disk.path === '/') || (await discSpacePromise)[0];
|
||||
|
||||
const spaceOccupied = availableMovies.reduce((acc, movie) => acc + (movie.sizeOnDisk || 0), 0);
|
||||
const spaceOccupied = availableMovies.reduce(
|
||||
(acc, movie) => acc + (movie.radarrMovie?.sizeOnDisk || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
moviesAmount: availableMovies.length,
|
||||
moviesCount: availableMovies.length,
|
||||
spaceLeft: diskSpaceInfo.freeSpace || 0,
|
||||
spaceOccupied,
|
||||
spaceTotal: diskSpaceInfo.totalSpace || 0
|
||||
@@ -32,14 +39,14 @@
|
||||
|
||||
{#await fetchStats()}
|
||||
<StatsPlaceholder {large} />
|
||||
{:then { moviesAmount, spaceLeft, spaceOccupied, spaceTotal }}
|
||||
{:then { moviesCount, spaceLeft, spaceOccupied, spaceTotal }}
|
||||
<StatsContainer
|
||||
{large}
|
||||
title="Radarr"
|
||||
subtitle="Movies Provider"
|
||||
href={PUBLIC_RADARR_BASE_URL}
|
||||
stats={[
|
||||
{ title: 'Movies', value: String(moviesAmount) },
|
||||
{ title: 'Movies', value: String(moviesCount) },
|
||||
{ title: 'Space Taken', value: formatSize(spaceOccupied) },
|
||||
{ title: 'Space Left', value: formatSize(spaceLeft) }
|
||||
]}
|
||||
|
||||
@@ -5,33 +5,54 @@
|
||||
import StatsContainer from './StatsContainer.svelte';
|
||||
import SonarrIcon from '../svgs/SonarrIcon.svelte';
|
||||
import { PUBLIC_SONARR_BASE_URL } from '$env/static/public';
|
||||
import { getDiskSpace } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
|
||||
export let large = false;
|
||||
|
||||
let statsRequest: Promise<{ moviesAmount: number }> = new Promise((_) => {}) as any;
|
||||
async function fetchStats() {
|
||||
const discSpacePromise = getDiskSpace();
|
||||
const { itemsArray } = await $library;
|
||||
const availableSeries = itemsArray.filter(
|
||||
(item) => item.sonarrSeries && item.sonarrSeries.statistics?.episodeFileCount
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
statsRequest = fetch('/radarr/stats')
|
||||
.then((res) => res.json())
|
||||
.then((data) => ({
|
||||
moviesAmount: data?.movies?.length
|
||||
}));
|
||||
});
|
||||
const diskSpaceInfo =
|
||||
(await discSpacePromise).find((disk) => disk.path === '/') || (await discSpacePromise)[0];
|
||||
|
||||
const spaceOccupied = availableSeries.reduce(
|
||||
(acc, series) => acc + (series.sonarrSeries?.statistics?.sizeOnDisk || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const episodesCount = availableSeries.reduce(
|
||||
(acc, series) => acc + (series.sonarrSeries?.statistics?.episodeFileCount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
episodesCount,
|
||||
spaceLeft: diskSpaceInfo.freeSpace || 0,
|
||||
spaceOccupied,
|
||||
spaceTotal: diskSpaceInfo.totalSpace || 0
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await statsRequest}
|
||||
{#await fetchStats()}
|
||||
<StatsPlaceholder {large} />
|
||||
{:then { moviesAmount }}
|
||||
{:then { episodesCount, spaceLeft, spaceOccupied, spaceTotal }}
|
||||
<StatsContainer
|
||||
{large}
|
||||
title="Sonarr"
|
||||
subtitle="Shows Provider"
|
||||
href={PUBLIC_SONARR_BASE_URL}
|
||||
stats={[
|
||||
{ title: 'Movies', value: String(moviesAmount) },
|
||||
{ title: 'Space Taken', value: formatSize(120_000_000_000) },
|
||||
{ title: 'Space Left', value: formatSize(50_000_000_000) }
|
||||
{ title: 'Episodes', value: String(episodesCount) },
|
||||
{ title: 'Space Taken', value: formatSize(spaceOccupied) },
|
||||
{ title: 'Space Left', value: formatSize(spaceLeft) }
|
||||
]}
|
||||
fillPercentage={((spaceTotal - spaceLeft) / spaceTotal) * 100}
|
||||
color="#8aacfd21"
|
||||
>
|
||||
<SonarrIcon slot="icon" class="absolute opacity-20 p-4 h-full inset-y-0 right-2" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_JELLYFIN_URL } from '$env/static/public';
|
||||
import {
|
||||
getJellyfinItem,
|
||||
getJellyfinPlaybackInfo,
|
||||
@@ -6,17 +7,22 @@
|
||||
reportJellyfinPlaybackStarted,
|
||||
reportJellyfinPlaybackStopped
|
||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import Hls from 'hls.js';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { Cross2 } from 'radix-icons-svelte';
|
||||
import classNames from 'classnames';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { PUBLIC_JELLYFIN_URL } from '$env/static/public';
|
||||
import getDeviceProfile from '$lib/apis/jellyfin/playback-profiles';
|
||||
import type { PlayerState, PlayerStateValue } from './VideoPlayer';
|
||||
import classNames from 'classnames';
|
||||
import Hls from 'hls.js';
|
||||
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';
|
||||
|
||||
const { playerState, close } = getContext<PlayerState>('player');
|
||||
let modalProps = createModalProps(() => {
|
||||
playerState.close();
|
||||
video?.pause();
|
||||
clearInterval(progressInterval);
|
||||
stopCallback?.();
|
||||
});
|
||||
|
||||
let video: HTMLVideoElement;
|
||||
|
||||
@@ -61,14 +67,6 @@
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
close();
|
||||
video?.pause();
|
||||
clearInterval(progressInterval);
|
||||
stopCallback?.();
|
||||
playerState.set({ visible: false, jellyfinId: '' });
|
||||
}
|
||||
|
||||
let uiVisible = false;
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
function handleMouseMove() {
|
||||
@@ -93,18 +91,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal visible={$playerState.visible} close={handleClose}>
|
||||
<div class="bg-black w-screen h-screen relative" on:mousemove={handleMouseMove}>
|
||||
<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 />
|
||||
</IconButton>
|
||||
{#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 />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const initialValue = { visible: false, jellyfinId: '' };
|
||||
export const playerState = writable(initialValue);
|
||||
|
||||
export const initialPlayerState = {
|
||||
playerState,
|
||||
close: () => {
|
||||
playerState.set({ visible: false, jellyfinId: '' });
|
||||
},
|
||||
streamJellyfinId: (id: string) => {
|
||||
playerState.set({ visible: true, jellyfinId: id });
|
||||
}
|
||||
};
|
||||
|
||||
export type PlayerState = typeof initialPlayerState;
|
||||
export type PlayerStateValue = typeof initialValue;
|
||||
|
||||
function createPlayerState() {
|
||||
const store = writable<PlayerStateValue>(initialValue);
|
||||
|
||||
return {
|
||||
...store,
|
||||
streamJellyfinId: (id: string) => {
|
||||
store.set({ visible: true, jellyfinId: id });
|
||||
},
|
||||
close: () => {
|
||||
store.set({ visible: false, jellyfinId: '' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const playerState = createPlayerState();
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import ModalBackground from '$lib/components/Modal/ModalBackground.svelte';
|
||||
import Navbar from '$lib/components/Navbar/Navbar.svelte';
|
||||
import VideoPlayer from '$lib/components/VideoPlayer/VideoPlayer.svelte';
|
||||
import { setContext } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { initialPlayerState } from '$lib/components/VideoPlayer/VideoPlayer';
|
||||
import SetupRequired from '$lib/components/SetupRequired/SetupRequired.svelte';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
|
||||
setContext('player', initialPlayerState);
|
||||
|
||||
settings.set({ autoplayTrailers: false });
|
||||
import VideoPlayer from '$lib/components/VideoPlayer/VideoPlayer.svelte';
|
||||
import '../app.css';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
export let data: LayoutData;
|
||||
</script>
|
||||
@@ -22,6 +16,7 @@
|
||||
<slot />
|
||||
</main>
|
||||
<VideoPlayer />
|
||||
<ModalBackground />
|
||||
|
||||
<!-- <footer>-->
|
||||
<!-- <p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>-->
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<!-- Does not contain any of the titles in library.-->
|
||||
|
||||
<div class="pt-24 bg-black">
|
||||
<Carousel>
|
||||
<Carousel gradientFromColor="from-black">
|
||||
<div slot="title" class={headerStyle}>For You</div>
|
||||
{#await discoverPromise}
|
||||
<CarouselPlaceholderItems size="lg" />
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<div class="flex flex-col gap-4 max-w-3xl flex-1">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<RadarrStats large />
|
||||
<div
|
||||
<SonarrStats large />
|
||||
<!-- <div
|
||||
class="border-zinc-800 border-2 border-dashed relative w-full p-3 px-4 rounded-xl overflow-hidden text-zinc-500 text-center flex flex-col gap-1"
|
||||
>
|
||||
<h2 class="font-medium">Sonarr is not set up</h2>
|
||||
@@ -18,8 +19,7 @@
|
||||
To set up Sonarr, define the <code>PUBLIC_SONARR_API_KEY</code> and <code>SONARR_URL</code> environment
|
||||
variables.
|
||||
</p>
|
||||
</div>
|
||||
<SonarrStats large />
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- <div>Sources</div>-->
|
||||
<!-- <div>Streaming services</div>-->
|
||||
|
||||
Reference in New Issue
Block a user