diff --git a/src/App.svelte b/src/App.svelte index 4ee6956..41074f6 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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)); @@ -66,6 +67,8 @@ + + {/if} diff --git a/src/Container.svelte b/src/Container.svelte index 0c94d91..b3f0044 100644 --- a/src/Container.svelte +++ b/src/Container.svelte @@ -57,5 +57,5 @@ })} use:registerer > - + diff --git a/src/lib/apis/radarr/radarr-api.ts b/src/lib/apis/radarr/radarr-api.ts index 188e413..e9ae10c 100644 --- a/src/lib/apis/radarr/radarr-api.ts +++ b/src/lib/apis/radarr/radarr-api.ts @@ -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 { return !!deleteResponse?.response.ok; }; - fetchRadarrReleases = (movieId: number) => + fetchRadarrReleases = (movieId: number): Promise => this.getClient() ?.GET('/api/v3/release', { params: { query: { movieId: movieId } } }) .then((r) => r.data || []) || Promise.resolve([]); diff --git a/src/lib/apis/sonarr/sonarrApi.ts b/src/lib/apis/sonarr/sonarrApi.ts index 4cbc5de..fe70653 100644 --- a/src/lib/apis/sonarr/sonarrApi.ts +++ b/src/lib/apis/sonarr/sonarrApi.ts @@ -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']; diff --git a/src/lib/components-old/Card/Card.svelte b/src/lib/components-old/Card/Card.svelte index 62243a2..f2d1a84 100644 --- a/src/lib/components-old/Card/Card.svelte +++ b/src/lib/components-old/Card/Card.svelte @@ -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'; diff --git a/src/lib/components-old/Modal/DynamicModal.svelte b/src/lib/components-old/Modal/DynamicModal.svelte index 705f7fa..0bec6ed 100644 --- a/src/lib/components-old/Modal/DynamicModal.svelte +++ b/src/lib/components-old/Modal/DynamicModal.svelte @@ -1,6 +1,6 @@ @@ -18,13 +19,15 @@ 'cursor-not-allowed pointer-events-none opacity-40': inactive })} on:click + let:hasFocus + {focusOnMount} > {#if $$slots.icon} {/if} - + {#if $$slots['icon-after']} diff --git a/src/lib/components/CardGrid.svelte b/src/lib/components/CardGrid.svelte index 1e7c7b2..0a6928e 100644 --- a/src/lib/components/CardGrid.svelte +++ b/src/lib/components/CardGrid.svelte @@ -17,8 +17,6 @@ } }; - $: console.log('cols', cols); - onMount(() => { calculateRows(); }); diff --git a/src/lib/components/DetatchedPage/DetatchedPage.svelte b/src/lib/components/DetachedPage/DetachedPage.svelte similarity index 100% rename from src/lib/components/DetatchedPage/DetatchedPage.svelte rename to src/lib/components/DetachedPage/DetachedPage.svelte diff --git a/src/lib/components/HeroShowcase/HeroShowcase.ts b/src/lib/components/HeroShowcase/HeroShowcase.ts index 129a4f1..63947e5 100644 --- a/src/lib/components/HeroShowcase/HeroShowcase.ts +++ b/src/lib/components/HeroShowcase/HeroShowcase.ts @@ -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> + response: Awaited> ): Promise { - console.log(response); return response.slice(0, 10).map((movie) => ({ title: movie.title || '', posterUrl: movie.poster_path || '', diff --git a/src/lib/components/Modal/FullScreenModal.svelte b/src/lib/components/Modal/FullScreenModal.svelte new file mode 100644 index 0000000..c25b3d9 --- /dev/null +++ b/src/lib/components/Modal/FullScreenModal.svelte @@ -0,0 +1,20 @@ + + + { + modalStack.close(modalId); + return true; + } + }} + focusOnMount + trapFocus + class="fixed inset-0 bg-stone-950/80" +> + + diff --git a/src/lib/components/Modal/ModalStack.svelte b/src/lib/components/Modal/ModalStack.svelte new file mode 100644 index 0000000..76751fa --- /dev/null +++ b/src/lib/components/Modal/ModalStack.svelte @@ -0,0 +1,35 @@ + + + + + + {#if $modalStackTop} + + {/if} + + +{#each $modalStack as modal (modal.id)} + {@const hidden = $modalStackTop?.group === modal.group && $modalStackTop?.id !== modal.id} + + + + +{/each} diff --git a/src/lib/components/Modal/modal.store.ts b/src/lib/components/Modal/modal.store.ts new file mode 100644 index 0000000..37881eb --- /dev/null +++ b/src/lib/components/Modal/modal.store.ts @@ -0,0 +1,59 @@ +import { derived, writable } from 'svelte/store'; + +type ModalItem = { + id: symbol; + group: symbol; + component: ConstructorOfATypedSvelteComponent; + props: Record; +}; +function createModalStack() { + const items = writable([]); + 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, + 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 +// }); +// } diff --git a/src/lib/components/RequestModal/RadarrRequestModal.svelte b/src/lib/components/RequestModal/RadarrRequestModal.svelte new file mode 100644 index 0000000..044eb34 --- /dev/null +++ b/src/lib/components/RequestModal/RadarrRequestModal.svelte @@ -0,0 +1,21 @@ + + + + + + Download + + + + diff --git a/src/lib/components/RequestModal/ReleaseList.svelte b/src/lib/components/RequestModal/ReleaseList.svelte new file mode 100644 index 0000000..d90f84a --- /dev/null +++ b/src/lib/components/RequestModal/ReleaseList.svelte @@ -0,0 +1,127 @@ + + + + {#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} + + + !isFetching && + !isGrabbed && + handleGrabRelease(release.guid || '', release.indexerId || 0)} + inactive={isFetching || isGrabbed} + let:hasFocus + focusOnMount={index === 0} + > + + + {#if !isGrabbed} + + {:else} + + {/if} + + {release.indexer} + + {release?.quality?.quality?.name} + + + {release.seeders} seeders + + + + + {formatSize(release?.size || 0)} + + + + {#if hasFocus} + + + {release.title} + + + {formatMinutesToTime(release.ageMinutes || 0)} old + + {release.seeders} seeders / {release.leechers} leechers + + {#if release.seeders} + + {formatSize((release.size || 0) / release.seeders)} per seeder + + {/if} + + {/if} + + + + {/each} + {#if !showAll && $releases?.length} + + (showAll = true)}>Show all {$releases?.length} releases + + {:else if showAll} + + (showAll = false)}>Show less + + {/if} + diff --git a/src/lib/components/Sidebar/Sidebar.svelte b/src/lib/components/Sidebar/Sidebar.svelte index 879be1c..50d95e7 100644 --- a/src/lib/components/Sidebar/Sidebar.svelte +++ b/src/lib/components/Sidebar/Sidebar.svelte @@ -9,8 +9,6 @@ let focusIndex: Readable; const navigate = useNavigate(); - const asd = ''; - const asd2 = ''; const itemContainer = (index: number, _focusIndex: number) => classNames('h-12 flex items-center', { 'text-amber-300': _focusIndex === index, diff --git a/src/lib/components/VideoPlayer/VideoPlayer.ts b/src/lib/components/VideoPlayer/VideoPlayer.ts index 96e2cb6..11fa099 100644 --- a/src/lib/components/VideoPlayer/VideoPlayer.ts +++ b/src/lib/components/VideoPlayer/VideoPlayer.ts @@ -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'; diff --git a/src/lib/pages/MoviePage.svelte b/src/lib/pages/MoviePage.svelte index e748938..3e80f14 100644 --- a/src/lib/pages/MoviePage.svelte +++ b/src/lib/pages/MoviePage.svelte @@ -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 @@ }); - + {#if jellyfinItem} - (playbackId = jellyfinItem.Id || '')}>Play + (playbackId = jellyfinItem.Id || '')}> + Play + + {:else if radarrItem} - Request + modalStack.create(RequestModal, { id: radarrItem.id })}> + Request + + {:else} requests.handleAddToRadarr(Number(id))} @@ -95,6 +103,12 @@ {/if} + {#if jellyfinItem && radarrItem} + modalStack.create(RequestModal, { id: radarrItem.id })}> + Manage Files + + + {/if} {#if PLATFORM_WEB} Open In TMDB @@ -115,4 +129,4 @@ {/if} - + diff --git a/src/lib/stores/data.store.ts b/src/lib/stores/data.store.ts index 4054de8..e46ecd9 100644 --- a/src/lib/stores/data.store.ts +++ b/src/lib/stores/data.store.ts @@ -265,6 +265,7 @@ export const useRequest = Promise, A extends any function refresh(...args: A): ReturnType { isFetching.set(true); + // @ts-ignore const p: ReturnType = fn(...args) .then((res) => { data.set(res); @@ -278,7 +279,7 @@ export const useRequest = Promise, A extends any return p; } - refresh(...initialArgs); + refresh(...initialArgs).finally(() => isLoading.set(false)); return { promise: { diff --git a/src/lib/stores/modal.store.ts b/src/lib/stores/modal.store.ts deleted file mode 100644 index ee390c7..0000000 --- a/src/lib/stores/modal.store.ts +++ /dev/null @@ -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; -}; -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, - 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 - }); -}
Promise, A extends any function refresh(...args: A): ReturnType { isFetching.set(true); + // @ts-ignore const p: ReturnType = fn(...args) .then((res) => { data.set(res); @@ -278,7 +279,7 @@ export const useRequest = Promise, A extends any return p; } - refresh(...initialArgs); + refresh(...initialArgs).finally(() => isLoading.set(false)); return { promise: { diff --git a/src/lib/stores/modal.store.ts b/src/lib/stores/modal.store.ts deleted file mode 100644 index ee390c7..0000000 --- a/src/lib/stores/modal.store.ts +++ /dev/null @@ -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; -}; -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, - 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 - }); -}
{ isFetching.set(true); + // @ts-ignore const p: ReturnType
= fn(...args) .then((res) => { data.set(res); @@ -278,7 +279,7 @@ export const useRequest =
Promise, A extends any return p; } - refresh(...initialArgs); + refresh(...initialArgs).finally(() => isLoading.set(false)); return { promise: { diff --git a/src/lib/stores/modal.store.ts b/src/lib/stores/modal.store.ts deleted file mode 100644 index ee390c7..0000000 --- a/src/lib/stores/modal.store.ts +++ /dev/null @@ -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; -}; -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, - 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 - }); -}