mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 16:55:13 +02:00
Added "Dynamic modals"; fixed title modals not allowing child modals
This commit is contained in:
@@ -1,60 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { fetchSonarrEpisodes, type SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import type { ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import RequestModal from './RequestModal.svelte';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let modalId: Symbol;
|
||||
export let groupId: Symbol;
|
||||
|
||||
export let sonarrId: number;
|
||||
export let seasonNumber: number;
|
||||
export let selectEpisode: (episode: SonarrEpisode) => void;
|
||||
|
||||
async function fetchEpisodes(sonarrId: number, seasonNumber: number) {
|
||||
return fetchSonarrEpisodes(sonarrId).then((episodes) =>
|
||||
episodes.filter((episode) => episode.seasonNumber === seasonNumber)
|
||||
);
|
||||
}
|
||||
|
||||
function selectEpisode(episode: SonarrEpisode) {
|
||||
modalStack.create(
|
||||
RequestModal,
|
||||
{
|
||||
episode,
|
||||
sonarrId,
|
||||
groupId
|
||||
},
|
||||
groupId
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} text="Seasons" />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#await fetchEpisodes(sonarrId, seasonNumber)}
|
||||
Loading...
|
||||
{:then episodes}
|
||||
{#if episodes.length === 0}
|
||||
<div class="px-4 py-1 text-xs text-gray-400">No episodes</div>
|
||||
{:else}
|
||||
{#each episodes as episode}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="px-4 py-1 flex flex-row items-center justify-between cursor-pointer hover:bg-lighten"
|
||||
on:click={() => selectEpisode(episode)}
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium">{episode.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{episode.episodeNumber ? `Episode ${episode.episodeNumber}` : 'Special'}
|
||||
</div>
|
||||
</div>
|
||||
<ModalContainer>
|
||||
<ModalHeader
|
||||
close={() => modalStack.closeGroup(groupId)}
|
||||
back={() => modalStack.close(modalId)}
|
||||
text="Seasons"
|
||||
/>
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#await fetchEpisodes(sonarrId, seasonNumber)}
|
||||
Loading...
|
||||
{:then episodes}
|
||||
{#if episodes.length === 0}
|
||||
<div class="px-4 py-1 text-xs text-gray-400">No episodes</div>
|
||||
{:else}
|
||||
{#each episodes as episode}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="px-4 py-1 flex flex-row items-center justify-between cursor-pointer hover:bg-lighten"
|
||||
on:click={() => selectEpisode(episode)}
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium">{episode.title}</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{new Date(episode.airDate || Date.now()).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
{episode.episodeNumber ? `Episode ${episode.episodeNumber}` : 'Special'}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
<div class="text-xs text-gray-400">
|
||||
{new Date(episode.airDate || Date.now()).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import HeightHider from '../HeightHider.svelte';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContent from '../Modal/ModalContainer.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import type { ModalProps } from '../Modal/Modal';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// TODO: Switch to grid
|
||||
export let modalId: Symbol;
|
||||
export let groupId: Symbol | undefined = undefined;
|
||||
|
||||
export let title = 'Releases';
|
||||
export let modalProps: ModalProps;
|
||||
export let radarrId: number | undefined = undefined;
|
||||
export let sonarrEpisodeId: number | undefined = undefined;
|
||||
export let seasonPack: { sonarrId: number; seasonNumber: number } | undefined = undefined;
|
||||
@@ -100,76 +100,78 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContent>
|
||||
<ModalHeader {...modalProps} text={title} />
|
||||
{#await fetchReleases()}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">Loading...</div>
|
||||
{:then { releases, filtered, releasesSkipped }}
|
||||
{#if showAllReleases ? releases?.length : filtered?.length}
|
||||
<div class="flex flex-col py-2 divide-y divide-zinc-700 max-h-[60vh] overflow-y-scroll">
|
||||
{#each showAllReleases ? releases : filtered as release}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten items-center justify-between cursor-pointer text-sm"
|
||||
on:click={() => toggleShowDetails(release.guid || null)}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="tracking-wide font-medium">{release.indexer}</div>
|
||||
<div class="text-zinc-400">{release?.quality?.quality?.name}</div>
|
||||
<div class="text-zinc-400">{release.seeders} seeders</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-zinc-400">{formatSize(release?.size || 0)}</div>
|
||||
{#if release.guid !== downloadingGuid}
|
||||
<IconButton
|
||||
on:click={() => release.guid && handleDownload(release.guid)}
|
||||
disabled={downloadFetchingGuid === release.guid}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<div class="p-1">
|
||||
<Download size={20} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ModalContent>
|
||||
<ModalHeader
|
||||
back={groupId ? () => modalStack.close(modalId) : undefined}
|
||||
close={() => (groupId ? modalStack.closeGroup(groupId) : modalStack.close(modalId))}
|
||||
text={title}
|
||||
/>
|
||||
{#await fetchReleases()}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">Loading...</div>
|
||||
{:then { releases, filtered, releasesSkipped }}
|
||||
{#if showAllReleases ? releases?.length : filtered?.length}
|
||||
<div class="flex flex-col divide-y divide-zinc-700 max-h-[60vh] overflow-y-scroll">
|
||||
{#each showAllReleases ? releases : filtered as release}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex px-4 py-2 gap-4 hover:bg-lighten items-center justify-between cursor-pointer text-sm"
|
||||
on:click={() => toggleShowDetails(release.guid || null)}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="tracking-wide font-medium">{release.indexer}</div>
|
||||
<div class="text-zinc-400">{release?.quality?.quality?.name}</div>
|
||||
<div class="text-zinc-400">{release.seeders} seeders</div>
|
||||
</div>
|
||||
<HeightHider visible={showDetailsId === release.guid}>
|
||||
<div class="flex gap-1 text-xs text-zinc-400 px-4 py-2 items-center flex-wrap">
|
||||
<div>
|
||||
{release.title}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-zinc-400">{formatSize(release?.size || 0)}</div>
|
||||
{#if release.guid !== downloadingGuid}
|
||||
<IconButton
|
||||
on:click={() => release.guid && handleDownload(release.guid)}
|
||||
disabled={downloadFetchingGuid === release.guid}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<div class="p-1">
|
||||
<Download size={20} />
|
||||
</div>
|
||||
<DotFilled size={15} />
|
||||
<div>{formatMinutesToTime(release.ageMinutes || 0)} old</div>
|
||||
<DotFilled size={15} />
|
||||
<div><b>{release.seeders} seeders</b> / {release.leechers} leechers</div>
|
||||
<DotFilled size={15} />
|
||||
{#if release.seeders}
|
||||
<div>
|
||||
{formatSize((release.size || 0) / release.seeders)} per seeder
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</HeightHider>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if releasesSkipped > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="text-sm text-zinc-200 opacity-50 font-light px-4 py-2 hover:underline cursor-pointer"
|
||||
on:click={toggleShowAll}
|
||||
>
|
||||
{showAllReleases ? 'Show less' : `Show all ${releasesSkipped} releases`}
|
||||
<HeightHider visible={showDetailsId === release.guid}>
|
||||
<div class="flex gap-1 text-xs text-zinc-400 px-4 py-2 items-center flex-wrap">
|
||||
<div>
|
||||
{release.title}
|
||||
</div>
|
||||
<DotFilled size={15} />
|
||||
<div>{formatMinutesToTime(release.ageMinutes || 0)} old</div>
|
||||
<DotFilled size={15} />
|
||||
<div><b>{release.seeders} seeders</b> / {release.leechers} leechers</div>
|
||||
<DotFilled size={15} />
|
||||
{#if release.seeders}
|
||||
<div>
|
||||
{formatSize((release.size || 0) / release.seeders)} per seeder
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</HeightHider>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No releases found.</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if releasesSkipped > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="text-sm text-zinc-200 opacity-50 font-light px-4 py-2 hover:underline cursor-pointer"
|
||||
on:click={toggleShowAll}
|
||||
>
|
||||
{showAllReleases ? 'Show less' : `Show all ${releasesSkipped} releases`}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{:else}
|
||||
<div class="text-sm text-zinc-200 opacity-50 font-light p-4">No releases found.</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { SonarrSeries } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { createModalProps, type ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let sonarrId: number;
|
||||
export let sonarrSeries: SonarrSeries;
|
||||
|
||||
// let selectedEpisode: SonarrEpisode | undefined;
|
||||
let requestModalProps = createModalProps(() => {
|
||||
modalProps.close();
|
||||
// selectedEpisode = undefined;
|
||||
});
|
||||
|
||||
// async function fetchEpisodes(sonarrId: number) {
|
||||
// return fetchSonarrEpisodes(sonarrId).then((episodes) => {
|
||||
// // Group episodes by season
|
||||
// const seasons: SonarrEpisode[][] = [];
|
||||
// episodes.forEach((episode) => {
|
||||
// if (!episode.seasonNumber) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!seasons[episode.seasonNumber - 1]) {
|
||||
// seasons[episode.seasonNumber - 1] = [];
|
||||
// }
|
||||
// seasons[episode.seasonNumber - 1].push(episode);
|
||||
// });
|
||||
|
||||
// return seasons;
|
||||
// });
|
||||
// }
|
||||
|
||||
async function fetchSeasons() {}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} text="Seasons" />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#each sonarrSeries.seasons || [] as season}
|
||||
<div>{season.seasonNumber}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
|
||||
{console.log(sonarrSeries)}
|
||||
|
||||
<!-- {#if selectedEpisode?.id}
|
||||
<RequestModal
|
||||
modalProps={requestModalProps}
|
||||
sonarrEpisodeId={selectedEpisode.id}
|
||||
title={selectedEpisode.title || undefined}
|
||||
/>
|
||||
{/if} -->
|
||||
@@ -1,100 +1,65 @@
|
||||
<script lang="ts">
|
||||
import type { SonarrEpisode } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { createModalProps, type ModalProps } from '../Modal/Modal';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { modalStack } from '../Modal/Modal';
|
||||
import ModalContainer from '../Modal/ModalContainer.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import ModalHeader from '../Modal/ModalHeader.svelte';
|
||||
import EpisodeSelectModal from './EpisodeSelectModal.svelte';
|
||||
import RequestModal from './RequestModal.svelte';
|
||||
|
||||
export let modalProps: ModalProps;
|
||||
export let modalId: Symbol;
|
||||
export let sonarrId: number;
|
||||
export let seasons: number;
|
||||
export let heading = 'Seasons';
|
||||
|
||||
let episodeSelectProps: Omit<ComponentProps<EpisodeSelectModal>, 'modalProps'> | undefined =
|
||||
undefined;
|
||||
let episodeSelectModalProps = createModalProps(
|
||||
() => {
|
||||
episodeSelectProps = undefined;
|
||||
modalProps.close();
|
||||
},
|
||||
() => {
|
||||
episodeSelectProps = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
let requestProps: Omit<ComponentProps<RequestModal>, 'modalProps'> | undefined = undefined;
|
||||
let requestModalProps = createModalProps(
|
||||
() => {
|
||||
requestProps = undefined;
|
||||
episodeSelectModalProps.close();
|
||||
},
|
||||
() => {
|
||||
requestProps = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
function selectSeasonPack(seasonNumber: number) {
|
||||
requestProps = {
|
||||
seasonPack: {
|
||||
sonarrId,
|
||||
seasonNumber
|
||||
}
|
||||
};
|
||||
modalStack.create(
|
||||
RequestModal,
|
||||
{
|
||||
seasonPack: {
|
||||
sonarrId,
|
||||
seasonNumber
|
||||
}
|
||||
},
|
||||
modalId
|
||||
);
|
||||
}
|
||||
|
||||
function selectSeason(seasonNumber: number) {
|
||||
episodeSelectProps = {
|
||||
seasonNumber,
|
||||
selectEpisode,
|
||||
sonarrId
|
||||
};
|
||||
}
|
||||
|
||||
function selectEpisode(episode: SonarrEpisode) {
|
||||
requestProps = {
|
||||
sonarrEpisodeId: episode.id,
|
||||
title: episode.title || 'Episode'
|
||||
};
|
||||
modalStack.create(
|
||||
EpisodeSelectModal,
|
||||
{
|
||||
seasonNumber,
|
||||
sonarrId,
|
||||
groupId: modalId
|
||||
},
|
||||
modalId
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {...modalProps}>
|
||||
<ModalContainer>
|
||||
<ModalHeader {...modalProps} back={undefined} text={heading} />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#each [...Array(seasons).keys()].map((i) => i + 1) as seasonNumber}
|
||||
<div
|
||||
class="px-4 py-3 flex justify-between items-center text-zinc-300 group-hover:text-zinc-300"
|
||||
>
|
||||
<div class="font-medium">
|
||||
Season {seasonNumber}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeasonPack(seasonNumber)}>
|
||||
<span>Season Packs</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeason(seasonNumber)}>
|
||||
<span>Episodes</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<ModalContainer>
|
||||
<ModalHeader close={() => modalStack.close(modalId)} back={undefined} text={heading} />
|
||||
<ModalContent>
|
||||
<div class="flex flex-col divide-y divide-zinc-700">
|
||||
{#each [...Array(seasons).keys()].map((i) => i + 1) as seasonNumber}
|
||||
<div
|
||||
class="px-4 py-3 flex justify-between items-center text-zinc-300 group-hover:text-zinc-300"
|
||||
>
|
||||
<div class="font-medium">
|
||||
Season {seasonNumber}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
</Modal>
|
||||
|
||||
{#if episodeSelectProps}
|
||||
<EpisodeSelectModal modalProps={episodeSelectModalProps} {...episodeSelectProps} />
|
||||
{/if}
|
||||
|
||||
{#if requestProps}
|
||||
<RequestModal modalProps={requestModalProps} {...requestProps} />
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeasonPack(seasonNumber)}>
|
||||
<span>Season Packs</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
<Button size="sm" type="tertiary" on:click={() => selectSeason(seasonNumber)}>
|
||||
<span>Episodes</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalContainer>
|
||||
|
||||
Reference in New Issue
Block a user