feat: Modals & release downloading
This commit is contained in:
@@ -14,14 +14,15 @@
|
||||
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
|
||||
import { appState } from './lib/stores/app-state.store';
|
||||
import MoviePage from './lib/pages/MoviePage.svelte';
|
||||
import DetatchedPage from './lib/components/DetatchedPage/DetatchedPage.svelte';
|
||||
import Button from './lib/components/Button.svelte';
|
||||
import ModalStack from './lib/components/Modal/ModalStack.svelte';
|
||||
|
||||
getReiverrApiClient()
|
||||
.GET('/user', {})
|
||||
.then((res) => res.data)
|
||||
.then((user) => appState.setUser(user || null))
|
||||
.catch(() => appState.setUser(null));
|
||||
|
||||
appState.subscribe((s) => console.log('appState', s));
|
||||
</script>
|
||||
|
||||
<I18n />
|
||||
@@ -66,6 +67,8 @@
|
||||
<Router>
|
||||
<Route path="movies/movie/:id" component={MoviePage} />
|
||||
</Router>
|
||||
|
||||
<ModalStack />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
})}
|
||||
use:registerer
|
||||
>
|
||||
<slot />
|
||||
<slot hasFocus={$hasFocus} hasFocusWithin={$hasFocusWithin} focusIndex={$focusIndex} />
|
||||
</svelte:element>
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Api } from '../api.interface';
|
||||
|
||||
export type RadarrMovie = components['schemas']['MovieResource'];
|
||||
export type MovieFileResource = components['schemas']['MovieFileResource'];
|
||||
export type ReleaseResource = components['schemas']['ReleaseResource'];
|
||||
export type RadarrRelease = components['schemas']['ReleaseResource'];
|
||||
export type RadarrDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
|
||||
export type DiskSpaceInfo = components['schemas']['DiskSpaceResource'];
|
||||
|
||||
@@ -116,7 +116,7 @@ export class RadarrApi implements Api<paths> {
|
||||
return !!deleteResponse?.response.ok;
|
||||
};
|
||||
|
||||
fetchRadarrReleases = (movieId: number) =>
|
||||
fetchRadarrReleases = (movieId: number): Promise<RadarrRelease[]> =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/release', { params: { query: { movieId: movieId } } })
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { settings } from '../../stores/settings.store';
|
||||
import { log } from '../../utils';
|
||||
|
||||
export type SonarrSeries = components['schemas']['SeriesResource'];
|
||||
export type SonarrReleaseResource = components['schemas']['ReleaseResource'];
|
||||
export type SonarrRelease = components['schemas']['ReleaseResource'];
|
||||
export type SonarrDownload = components['schemas']['QueueResource'] & { series: SonarrSeries };
|
||||
export type DiskSpaceInfo = components['schemas']['DiskSpaceResource'];
|
||||
export type SonarrEpisode = components['schemas']['EpisodeResource'];
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { formatMinutesToTime } from '../../../lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { Clock, Star } from 'radix-icons-svelte';
|
||||
import { openTitleModal } from '../../stores/modal.store';
|
||||
import { openTitleModal } from '../../components/Modal/modal.store';
|
||||
import ContextMenu from '../ContextMenu/ContextMenu.svelte';
|
||||
import LibraryItemContextItems from '../ContextMenu/LibraryItemContextItems.svelte';
|
||||
import ProgressBar from '../ProgressBar.svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import TitleSearchModal from './TitleSearchModal.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
let y = 0;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { searchTmdbTitles } from '$lib/apis/tmdb/tmdbApi';
|
||||
import { TMDB_POSTER_SMALL } from '$lib/constants';
|
||||
import { MagnifyingGlass } from 'radix-icons-svelte';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import ModalContent from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { TitleType } from '../../../lib/types';
|
||||
import { TMDB_PROFILE_SMALL } from '../../../lib/constants';
|
||||
import { openTitleModal } from '../../../lib/stores/modal.store';
|
||||
import { openTitleModal } from '../../components/Modal/modal.store';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let tmdbId: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fetchSonarrEpisodes, type SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import HeightHider from '../HeightHider.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import ModalContent from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
createSonarrDownloadStore,
|
||||
createSonarrSeriesStore
|
||||
} from '../../lib/stores/data.store';
|
||||
import { modalStack } from '../../lib/stores/modal.store';
|
||||
import { modalStack } from '../components/Modal/modal.store';
|
||||
import { settings } from '../../lib/stores/settings.store';
|
||||
import type { TitleId } from '../../lib/types';
|
||||
import { capitalize, formatMinutesToTime, formatSize } from '../../lib/utils';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { fly } from 'svelte/transition';
|
||||
import MoviePage from '../MoviePage.svelte';
|
||||
import SeriesPage from '../SeriesPage.svelte';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import PersonPage from '../PersonPage.svelte';
|
||||
|
||||
export let titleId: TitleId;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import { contextMenu } from '../ContextMenu/ContextMenu';
|
||||
import SelectableContextMenuItem from '../ContextMenu/SelectableContextMenuItem.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import Slider from './Slider.svelte';
|
||||
import { playerState } from './VideoPlayer';
|
||||
import { linear } from 'svelte/easing';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
import VideoPlayer from './VideoPlayer.svelte';
|
||||
import { jellyfinItemsStore } from '../../stores/data.store';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let inactive: boolean = false;
|
||||
export let focusOnMount: boolean = false;
|
||||
|
||||
let hasFoucus: Readable<boolean>;
|
||||
</script>
|
||||
@@ -18,13 +19,15 @@
|
||||
'cursor-not-allowed pointer-events-none opacity-40': inactive
|
||||
})}
|
||||
on:click
|
||||
let:hasFocus
|
||||
{focusOnMount}
|
||||
>
|
||||
{#if $$slots.icon}
|
||||
<div class="mr-2">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
<slot {hasFocus} />
|
||||
{#if $$slots['icon-after']}
|
||||
<div class="ml-2">
|
||||
<slot name="icon-after" />
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
$: console.log('cols', cols);
|
||||
|
||||
onMount(() => {
|
||||
calculateRows();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { getTmdbPopularMovies } from '../../apis/tmdb/tmdb-api';
|
||||
import { formatMinutesToTime } from '../../utils';
|
||||
import type { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
||||
|
||||
export type RatingSource = 'tmdb'; // TODO: Add more rating sources & move elsewhere
|
||||
|
||||
@@ -20,9 +20,8 @@ export type ShowcaseItemProps = {
|
||||
};
|
||||
|
||||
export async function getShowcasePropsFromTmdb(
|
||||
response: Awaited<ReturnType<typeof getTmdbPopularMovies>>
|
||||
response: Awaited<ReturnType<typeof tmdbApi.getPopularMovies>>
|
||||
): Promise<ShowcaseItemProps[]> {
|
||||
console.log(response);
|
||||
return response.slice(0, 10).map((movie) => ({
|
||||
title: movie.title || '',
|
||||
posterUrl: movie.poster_path || '',
|
||||
|
||||
20
src/lib/components/Modal/FullScreenModal.svelte
Normal file
20
src/lib/components/Modal/FullScreenModal.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import { modalStack } from './modal.store';
|
||||
|
||||
export let modalId: symbol;
|
||||
</script>
|
||||
|
||||
<Container
|
||||
navigationActions={{
|
||||
left: () => {
|
||||
modalStack.close(modalId);
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
focusOnMount
|
||||
trapFocus
|
||||
class="fixed inset-0 bg-stone-950/80"
|
||||
>
|
||||
<slot />
|
||||
</Container>
|
||||
35
src/lib/components/Modal/ModalStack.svelte
Normal file
35
src/lib/components/Modal/ModalStack.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { modalStack, modalStackTop } from './modal.store';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
const top = $modalStackTop;
|
||||
if (event.key === 'Escape' && top) {
|
||||
modalStack.close(top.id);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
modalStack.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleShortcuts} />
|
||||
|
||||
<svelte:head>
|
||||
{#if $modalStackTop}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#each $modalStack as modal (modal.id)}
|
||||
{@const hidden = $modalStackTop?.group === modal.group && $modalStackTop?.id !== modal.id}
|
||||
|
||||
<div class="fixed inset-0 z-30">
|
||||
<svelte:component this={modal.component} {...modal.props} modalId={modal.id} {hidden} />
|
||||
</div>
|
||||
{/each}
|
||||
59
src/lib/components/Modal/modal.store.ts
Normal file
59
src/lib/components/Modal/modal.store.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
type ModalItem = {
|
||||
id: symbol;
|
||||
group: symbol;
|
||||
component: ConstructorOfATypedSvelteComponent;
|
||||
props: Record<string, any>;
|
||||
};
|
||||
function createModalStack() {
|
||||
const items = writable<ModalItem[]>([]);
|
||||
const top = derived(items, ($items) => $items[$items.length - 1]);
|
||||
|
||||
function close(symbol: symbol) {
|
||||
items.update((prev) => prev.filter((i) => i.id !== symbol));
|
||||
}
|
||||
|
||||
function closeGroup(group: symbol) {
|
||||
items.update((prev) => prev.filter((i) => i.group !== group));
|
||||
}
|
||||
|
||||
function create(
|
||||
component: ConstructorOfATypedSvelteComponent,
|
||||
props: Record<string, any>,
|
||||
group: symbol | undefined = undefined
|
||||
) {
|
||||
const id = Symbol();
|
||||
const item = { id, component, props, group: group || id };
|
||||
items.update((prev) => [...prev, item]);
|
||||
return id;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
items.set([]);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: items.subscribe,
|
||||
top: {
|
||||
subscribe: top.subscribe
|
||||
},
|
||||
create,
|
||||
close,
|
||||
closeGroup,
|
||||
reset
|
||||
};
|
||||
}
|
||||
|
||||
export const modalStack = createModalStack();
|
||||
export const modalStackTop = modalStack.top;
|
||||
|
||||
// let lastTitleModal: symbol | undefined = undefined;
|
||||
// export function openTitleModal(titleId: TitleId) {
|
||||
// if (lastTitleModal) {
|
||||
// modalStack.close(lastTitleModal);
|
||||
// }
|
||||
// lastTitleModal = modalStack.create(TitlePageModal, {
|
||||
// titleId
|
||||
// });
|
||||
// }
|
||||
21
src/lib/components/RequestModal/RadarrRequestModal.svelte
Normal file
21
src/lib/components/RequestModal/RadarrRequestModal.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import FullScreenModal from '../Modal/FullScreenModal.svelte';
|
||||
import { radarrApi } from '../../apis/radarr/radarr-api';
|
||||
import ReleaseList from './ReleaseList.svelte';
|
||||
|
||||
export let id: number;
|
||||
export let modalId: symbol;
|
||||
</script>
|
||||
|
||||
<FullScreenModal {modalId}>
|
||||
<div class="m-auto flex flex-col items-center my-16">
|
||||
<div>
|
||||
<h1 class="tracking-wide text-2xl font-semibold mb-4">Download</h1>
|
||||
<ReleaseList
|
||||
{id}
|
||||
grabRelease={radarrApi.downloadRadarrMovie}
|
||||
getReleases={radarrApi.fetchRadarrReleases}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
127
src/lib/components/RequestModal/ReleaseList.svelte
Normal file
127
src/lib/components/RequestModal/ReleaseList.svelte
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { type RadarrRelease } from '../../apis/radarr/radarr-api';
|
||||
import type { SonarrRelease } from '../../apis/sonarr/sonarrApi';
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from '../../stores/data.store';
|
||||
import Button from '../Button.svelte';
|
||||
import { DotFilled, Download, Plus } from 'radix-icons-svelte';
|
||||
import { formatMinutesToTime, formatSize } from '../../utils';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export let id: number;
|
||||
export let getReleases: (id: number) => Promise<(RadarrRelease | SonarrRelease)[]>;
|
||||
export let grabRelease: (guid: string, indexerId: number) => Promise<boolean>;
|
||||
|
||||
let showAll = false;
|
||||
|
||||
const { data: releases } = useRequest(getReleases, id);
|
||||
|
||||
const filteredReleases = derived(releases, ($releases) => {
|
||||
if (!$releases) return [];
|
||||
let filtered = $releases.slice();
|
||||
|
||||
filtered.sort((a, b) => (b.seeders || 0) - (a.seeders || 0));
|
||||
filtered = (filtered as any)
|
||||
.filter((release: any) => release?.quality?.quality?.resolution > 720)
|
||||
.slice(0, 5);
|
||||
|
||||
filtered.sort((a, b) => (b.size || 0) - (a.size || 0));
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const isFetchingGrab: Record<string, boolean> = {};
|
||||
const grabbedReleases: Record<string, boolean> = {};
|
||||
function handleGrabRelease(guid: string, indexerId: number) {
|
||||
isFetchingGrab[guid] = true;
|
||||
grabRelease(guid, indexerId).then((ok) => {
|
||||
isFetchingGrab[guid] = false;
|
||||
if (ok) {
|
||||
grabbedReleases[guid] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col -my-1 max-w-2xl">
|
||||
{#each (showAll ? $releases : $filteredReleases)?.filter((r) => r.guid && r.indexerId) || [] as release, index}
|
||||
{@const isFetching = isFetchingGrab[release.guid || ''] || false}
|
||||
{@const isGrabbed = grabbedReleases[release.guid || ''] || false}
|
||||
<div class="flex-1 my-1">
|
||||
<Button
|
||||
on:click={() =>
|
||||
!isFetching &&
|
||||
!isGrabbed &&
|
||||
handleGrabRelease(release.guid || '', release.indexerId || 0)}
|
||||
inactive={isFetching || isGrabbed}
|
||||
let:hasFocus
|
||||
focusOnMount={index === 0}
|
||||
>
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="flex-1 flex items-center">
|
||||
{#if !isGrabbed}
|
||||
<Plus size={19} class="mr-2" />
|
||||
{:else}
|
||||
<Download size={19} class="mr-2" />
|
||||
{/if}
|
||||
<div class="flex-1 flex mr-2">
|
||||
<div class="tracking-wide mr-2">{release.indexer}</div>
|
||||
<div
|
||||
class={classNames('mr-2', {
|
||||
'text-zinc-400': !hasFocus,
|
||||
'text-zinc-700': hasFocus
|
||||
})}
|
||||
>
|
||||
{release?.quality?.quality?.name}
|
||||
</div>
|
||||
<div
|
||||
class={classNames('mr-2', {
|
||||
'text-zinc-400': !hasFocus,
|
||||
'text-zinc-700': hasFocus
|
||||
})}
|
||||
>
|
||||
{release.seeders} seeders
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class={classNames({
|
||||
'text-zinc-400': !hasFocus,
|
||||
'text-zinc-700': hasFocus
|
||||
})}
|
||||
>
|
||||
{formatSize(release?.size || 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if hasFocus}
|
||||
<div class="flex text-xs text-zinc-700 items-center flex-wrap mt-2">
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{#if !showAll && $releases?.length}
|
||||
<div class="my-1 w-full">
|
||||
<Button on:click={() => (showAll = true)}>Show all {$releases?.length} releases</Button>
|
||||
</div>
|
||||
{:else if showAll}
|
||||
<div class="my-1 w-full">
|
||||
<Button on:click={() => (showAll = false)}>Show less</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -9,8 +9,6 @@
|
||||
let focusIndex: Readable<number>;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const asd = '';
|
||||
const asd2 = '';
|
||||
const itemContainer = (index: number, _focusIndex: number) =>
|
||||
classNames('h-12 flex items-center', {
|
||||
'text-amber-300': _focusIndex === index,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import VideoPlayer from './VideoPlayer.svelte';
|
||||
import { jellyfinItemsStore } from '../../stores/data.store';
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import classNames from 'classnames';
|
||||
import { DotFilled, ExternalLink, Plus } from 'radix-icons-svelte';
|
||||
import { DotFilled, Download, ExternalLink, Play, Plus } from 'radix-icons-svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import VideoPlayer from '../components/VideoPlayer/VideoPlayer.svelte';
|
||||
import { radarrApi } from '../apis/radarr/radarr-api';
|
||||
import { useActionRequests, useRequest } from '../stores/data.store';
|
||||
import DetatchedPage from '../components/DetatchedPage/DetatchedPage.svelte';
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import { modalStack } from '../components/Modal/modal.store';
|
||||
import RequestModal from '../components/RequestModal/RadarrRequestModal.svelte';
|
||||
|
||||
export let id: string;
|
||||
|
||||
@@ -34,7 +36,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<DetatchedPage>
|
||||
<DetachedPage>
|
||||
<div class="h-screen flex flex-col">
|
||||
<HeroCarousel
|
||||
bind:index={heroIndex}
|
||||
@@ -83,9 +85,15 @@
|
||||
{#await Promise.all([$jellyfinItemP, $radarrItemP]) then [jellyfinItem, radarrItem]}
|
||||
<Container direction="horizontal" class="flex mt-8 gap-2">
|
||||
{#if jellyfinItem}
|
||||
<Button on:click={() => (playbackId = jellyfinItem.Id || '')}>Play</Button>
|
||||
<Button on:click={() => (playbackId = jellyfinItem.Id || '')}>
|
||||
Play
|
||||
<Play size={19} slot="icon" />
|
||||
</Button>
|
||||
{:else if radarrItem}
|
||||
<Button>Request</Button>
|
||||
<Button on:click={() => modalStack.create(RequestModal, { id: radarrItem.id })}>
|
||||
Request
|
||||
<Download size={19} slot="icon" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={() => requests.handleAddToRadarr(Number(id))}
|
||||
@@ -95,6 +103,12 @@
|
||||
<Plus slot="icon" size={19} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if jellyfinItem && radarrItem}
|
||||
<Button on:click={() => modalStack.create(RequestModal, { id: radarrItem.id })}>
|
||||
Manage Files
|
||||
<Download size={19} slot="icon" />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if PLATFORM_WEB}
|
||||
<Button>
|
||||
Open In TMDB
|
||||
@@ -115,4 +129,4 @@
|
||||
<VideoPlayer jellyfinId={playbackId} />
|
||||
{/if}
|
||||
</div>
|
||||
</DetatchedPage>
|
||||
</DetachedPage>
|
||||
|
||||
@@ -265,6 +265,7 @@ export const useRequest = <P extends (...args: A) => Promise<any>, A extends any
|
||||
|
||||
function refresh(...args: A): ReturnType<P> {
|
||||
isFetching.set(true);
|
||||
// @ts-ignore
|
||||
const p: ReturnType<P> = fn(...args)
|
||||
.then((res) => {
|
||||
data.set(res);
|
||||
@@ -278,7 +279,7 @@ export const useRequest = <P extends (...args: A) => Promise<any>, A extends any
|
||||
return p;
|
||||
}
|
||||
|
||||
refresh(...initialArgs);
|
||||
refresh(...initialArgs).finally(() => isLoading.set(false));
|
||||
|
||||
return {
|
||||
promise: {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { TitleId } from '$lib/types';
|
||||
import { writable } from 'svelte/store';
|
||||
import TitlePageModal from '../components/TitlePageLayout/TitlePageModal.svelte';
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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,
|
||||
create,
|
||||
close,
|
||||
closeGroup,
|
||||
reset
|
||||
};
|
||||
}
|
||||
|
||||
export const modalStack = createDynamicModalStack();
|
||||
|
||||
let lastTitleModal: symbol | undefined = undefined;
|
||||
export function openTitleModal(titleId: TitleId) {
|
||||
if (lastTitleModal) {
|
||||
modalStack.close(lastTitleModal);
|
||||
}
|
||||
lastTitleModal = modalStack.create(TitlePageModal, {
|
||||
titleId
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user