mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 00:35:12 +02:00
Implemented season pack download requests
This commit is contained in:
@@ -33,7 +33,7 @@ export const modalStack = createModalStack();
|
||||
|
||||
export type ModalProps = ReturnType<typeof createModalProps>;
|
||||
|
||||
export function createModalProps(onClose: () => void) {
|
||||
export function createModalProps(onClose: () => void, onBack?: () => void) {
|
||||
const id = Symbol();
|
||||
|
||||
function close() {
|
||||
@@ -41,8 +41,14 @@ export function createModalProps(onClose: () => void) {
|
||||
modalStack.remove(id);
|
||||
}
|
||||
|
||||
function back() {
|
||||
onBack?.();
|
||||
modalStack.remove(id);
|
||||
}
|
||||
|
||||
return {
|
||||
close,
|
||||
back,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { Cross2 } from 'radix-icons-svelte';
|
||||
import { ChevronLeft, Cross2 } from 'radix-icons-svelte';
|
||||
|
||||
export let text = '';
|
||||
export let close = () => {};
|
||||
export let back: (() => void) | undefined = undefined;
|
||||
export let text = back ? 'Back' : '';
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex text-zinc-200 items-center p-3 px-5 gap-4 border-b border-zinc-700">
|
||||
<slot />
|
||||
{#if text}
|
||||
<p class="font flex-1">{text}</p>
|
||||
{/if}
|
||||
<slot>
|
||||
{#if text}
|
||||
<button
|
||||
class={classNames('flex-1 flex items-center gap-1', {
|
||||
'cursor-pointer text-zinc-300 hover:text-zinc-200': !!back,
|
||||
'cursor-default': !back
|
||||
})}
|
||||
on:click={() => back?.()}
|
||||
tabindex={back ? 0 : -1}
|
||||
>
|
||||
{#if !!back}
|
||||
<ChevronLeft size={20} />
|
||||
{/if}
|
||||
<h1>{text}</h1>
|
||||
</button>
|
||||
{/if}
|
||||
</slot>
|
||||
<IconButton on:click={close}>
|
||||
<Cross2 size={20} />
|
||||
</IconButton>
|
||||
|
||||
60
src/lib/components/RequestModal/EpisodeSelectModal.svelte
Normal file
60
src/lib/components/RequestModal/EpisodeSelectModal.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<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 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 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)
|
||||
);
|
||||
}
|
||||
</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>
|
||||
<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>
|
||||
</Modal>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fetchRadarrReleases } from '$lib/apis/radarr/radarrApi';
|
||||
import { fetchSonarrReleases } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { fetchSonarrReleases, fetchSonarrSeasonReleases } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { formatMinutesToTime, formatSize } from '$lib/utils';
|
||||
import { DotFilled, Download, Plus } from 'radix-icons-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@@ -18,6 +18,7 @@
|
||||
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;
|
||||
|
||||
let showAllReleases = false;
|
||||
let showDetailsId: string | null = null;
|
||||
@@ -25,7 +26,7 @@
|
||||
let downloadingGuid: string | undefined;
|
||||
|
||||
async function fetchReleases() {
|
||||
if (!radarrId && !sonarrEpisodeId) {
|
||||
if (!radarrId && !sonarrEpisodeId && !seasonPack) {
|
||||
return {
|
||||
releases: [],
|
||||
filtered: [],
|
||||
@@ -35,7 +36,12 @@
|
||||
|
||||
const releases = radarrId
|
||||
? await fetchRadarrReleases(radarrId)
|
||||
: await fetchSonarrReleases(sonarrEpisodeId as number);
|
||||
: sonarrEpisodeId
|
||||
? await fetchSonarrReleases(sonarrEpisodeId as number)
|
||||
: await fetchSonarrSeasonReleases(
|
||||
seasonPack?.sonarrId as number,
|
||||
seasonPack?.seasonNumber as number
|
||||
);
|
||||
|
||||
let filtered = releases.slice();
|
||||
|
||||
|
||||
62
src/lib/components/RequestModal/SeasonSelectModal.svelte
Normal file
62
src/lib/components/RequestModal/SeasonSelectModal.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<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,96 @@
|
||||
<script lang="ts">
|
||||
import { fetchSonarrEpisodes, type SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { ChevronUp } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
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';
|
||||
import RoundedButton from '../RoundedButton.svelte';
|
||||
import EpisodeSelectModal from './EpisodeSelectModal.svelte';
|
||||
import RequestModal from './RequestModal.svelte';
|
||||
import type { SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let sonarrId: number;
|
||||
export let seasons: number;
|
||||
|
||||
let selectedEpisode: SonarrEpisode | undefined;
|
||||
let requestModalProps = createModalProps(() => {
|
||||
modalProps.close();
|
||||
selectedEpisode = undefined;
|
||||
});
|
||||
let episodeSelectProps: Omit<ComponentProps<EpisodeSelectModal>, 'modalProps'> | undefined =
|
||||
undefined;
|
||||
let episodeSelectModalProps = createModalProps(
|
||||
() => {
|
||||
episodeSelectProps = undefined;
|
||||
modalProps.close();
|
||||
},
|
||||
() => {
|
||||
episodeSelectProps = 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;
|
||||
}
|
||||
let requestProps: Omit<ComponentProps<RequestModal>, 'modalProps'> | undefined = undefined;
|
||||
let requestModalProps = createModalProps(
|
||||
() => {
|
||||
requestProps = undefined;
|
||||
episodeSelectModalProps.close();
|
||||
},
|
||||
() => {
|
||||
requestProps = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
if (!seasons[episode.seasonNumber - 1]) {
|
||||
seasons[episode.seasonNumber - 1] = [];
|
||||
}
|
||||
seasons[episode.seasonNumber - 1].push(episode);
|
||||
});
|
||||
function selectSeasonPack(seasonNumber: number) {
|
||||
requestProps = {
|
||||
seasonPack: {
|
||||
sonarrId,
|
||||
seasonNumber
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return seasons;
|
||||
});
|
||||
function selectSeason(seasonNumber: number) {
|
||||
episodeSelectProps = {
|
||||
seasonNumber,
|
||||
selectEpisode,
|
||||
sonarrId
|
||||
};
|
||||
}
|
||||
|
||||
function selectEpisode(episode: SonarrEpisode) {
|
||||
requestProps = {
|
||||
sonarrEpisodeId: episode.id,
|
||||
title: episode.title || 'Episode'
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} text="Seasons" />
|
||||
<ModalHeader {...modalProps} back={undefined} 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-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>
|
||||
{#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="uppercase font-bold text-sm">
|
||||
Season {seasonNumber}
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
<div class="flex gap-2">
|
||||
<RoundedButton on:click={() => selectSeasonPack(seasonNumber)}
|
||||
>Season Packs</RoundedButton
|
||||
>
|
||||
<RoundedButton on:click={() => selectSeason(seasonNumber)}>Episodes</RoundedButton>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
|
||||
{#if selectedEpisode?.id}
|
||||
<RequestModal
|
||||
modalProps={requestModalProps}
|
||||
sonarrEpisodeId={selectedEpisode.id}
|
||||
title={selectedEpisode.title || undefined}
|
||||
/>
|
||||
{#if episodeSelectProps}
|
||||
<EpisodeSelectModal modalProps={episodeSelectModalProps} {...episodeSelectProps} />
|
||||
{/if}
|
||||
|
||||
{#if requestProps}
|
||||
<RequestModal modalProps={requestModalProps} {...requestProps} />
|
||||
{/if}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
cancelDownloadSonarrEpisode,
|
||||
deleteSonarrEpisode,
|
||||
getSonarrEpisodes,
|
||||
removeFromSonarr
|
||||
removeFromSonarr,
|
||||
type SonarrSeries
|
||||
} from '$lib/apis/sonarr/sonarrApi';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
@@ -23,6 +24,7 @@
|
||||
import SeriesRequestModal from '../RequestModal/SeriesRequestModal.svelte';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
import LibraryDetailsFile from './LibraryDetailsFile.svelte';
|
||||
import SeasonSelectModal from '../RequestModal/SeasonSelectModal.svelte';
|
||||
|
||||
export let tmdbId: number;
|
||||
export let type: 'movie' | 'tv';
|
||||
@@ -31,6 +33,7 @@
|
||||
let isAdded = false;
|
||||
let isRequestModalVisible = false;
|
||||
const requestModalProps = createModalProps(() => (isRequestModalVisible = false));
|
||||
let series: SonarrSeries | undefined = undefined;
|
||||
|
||||
let downloadProps: ComponentProps<LibraryDetailsFile>[] = [];
|
||||
let movieFileProps: ComponentProps<LibraryDetailsFile>[] = [];
|
||||
@@ -148,6 +151,7 @@
|
||||
|
||||
isAdded = !!radarrMovie || !!sonarrSeries;
|
||||
servarrId = radarrMovie?.id || sonarrSeries?.id;
|
||||
series = sonarrSeries;
|
||||
});
|
||||
|
||||
let addToServarrLoading = false;
|
||||
@@ -333,8 +337,14 @@
|
||||
radarrId={servarrId}
|
||||
on:download={() => setTimeout(refetch, 5000)}
|
||||
/>
|
||||
{:else if isAdded && servarrId && type === 'tv'}
|
||||
<SeriesRequestModal modalProps={requestModalProps} sonarrId={servarrId} />
|
||||
{:else if isAdded && servarrId && type === 'tv' && series?.statistics?.seasonCount}
|
||||
{console.log(series)}
|
||||
<SeriesRequestModal
|
||||
modalProps={requestModalProps}
|
||||
sonarrId={servarrId}
|
||||
seasons={series?.statistics.seasonCount}
|
||||
/>
|
||||
<!-- <SeasonSelectModal modalProps={requestModalProps} sonarrId={servarrId} sonarrSeries={series} /> -->
|
||||
{:else}
|
||||
<div>NO CONTENT</div>
|
||||
{console.log('NO CONTENT')}
|
||||
|
||||
23
src/lib/components/RoundedButton.svelte
Normal file
23
src/lib/components/RoundedButton.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
|
||||
export let disableIcon = false;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<button
|
||||
class={classNames(
|
||||
'text-sm py-1 px-3 rounded-full flex items-center select-none cursor-pointer transition-colors selectable border',
|
||||
'border-zinc-600 text-zinc-300 hover:bg-zinc-100 hover:border-zinc-100 hover:text-zinc-800 focus-visible:bg-zinc-100 focus-visible:border-zinc-100 focus-visible:text-zinc-800'
|
||||
)}
|
||||
on:click
|
||||
>
|
||||
<span><slot /></span>
|
||||
{#if !disableIcon}
|
||||
<slot name="icon">
|
||||
<ChevronRight size={20} />
|
||||
</slot>
|
||||
{/if}
|
||||
</button>
|
||||
Reference in New Issue
Block a user