refactor: Media Manager Modals

This commit is contained in:
Aleksi Lassila
2024-04-24 10:59:15 +03:00
parent 5ece8dd6f5
commit 165f793a43
20 changed files with 318 additions and 82 deletions

View File

@@ -1,11 +1,9 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import HeroShowcaseBackground from './HeroBackground.svelte';
import { scrollIntoView, Selectable } from '../../selectable';
import IconButton from '../IconButton.svelte';
import { ChevronRight } from 'radix-icons-svelte';
import PageDots from '../HeroShowcase/PageDots.svelte';
import SidebarMargin from '../SidebarMargin.svelte';
import type { Readable, Writable } from 'svelte/store';
import { createEventDispatcher } from 'svelte';

View File

@@ -4,45 +4,48 @@
import { formatSize } from '../../utils';
import { ChevronRight } from 'radix-icons-svelte';
import type { Download } from '../../apis/combined-types';
import Container from '../../../Container.svelte';
export let downloads: Promise<Download[]>;
export let cancelDownload: (downloadId: number) => Promise<any>;
</script>
<div class="-my-1">
<Container class="flex flex-col -my-2">
{#await downloads}
{#each new Array(5) as _, index}
<div class="flex-1 my-1">
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{/each}
{:then downloads}
{#each downloads as download, index}
<Button on:clickOrSelect={() => cancelDownload(download.id || -1)} let:hasFocus>
<div class="flex w-full">
<h1 class="flex-1 line-clamp-1">
{download.title}
</h1>
<div>
{#if !hasFocus}
{#if download.status === 'downloading'}
{formatSize((download.size || 0) - (download.sizeleft || 0))}/{formatSize(
download.size || 0
)}
<div class="my-2">
<Button on:clickOrSelect={() => cancelDownload(download.id || -1)} let:hasFocus>
<div class="flex w-full">
<h1 class="flex-1 line-clamp-1">
{download.title}
</h1>
<div>
{#if !hasFocus}
{#if download.status === 'downloading'}
{formatSize((download.size || 0) - (download.sizeleft || 0))}/{formatSize(
download.size || 0
)}
{:else}
{download.status}
{/if}
{:else}
{download.status}
<div class="flex items-center">
Cancel
<ChevronRight size={19} class="ml-1" />
</div>
{/if}
{:else}
<div class="flex items-center">
Cancel
<ChevronRight size={19} class="ml-1" />
</div>
{/if}
</div>
</div>
</div>
</Button>
</Button>
</div>
{:else}
<h1 class="text-sm text-zinc-400">No downloads found</h1>
{/each}
{/await}
</div>
</Container>

View File

@@ -5,21 +5,22 @@
import { formatSize } from '../../utils.js';
import type { FileResource } from '../../apis/combined-types';
import { scrollIntoView } from '../../selectable';
import Container from '../../../Container.svelte';
export let files: Promise<FileResource[]>;
export let handleSelectFile: (file: FileResource) => void;
</script>
<div class="-my-1">
<Container class="flex flex-col -my-2">
{#await files}
{#each new Array(5) as _, index}
<div class="flex-1 my-1">
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{/each}
{:then files}
{#each files as file, index}
<div class="flex-1 my-1">
<div class="flex-1 my-2">
<Button
on:clickOrSelect={() => handleSelectFile(file)}
let:hasFocus
@@ -46,4 +47,4 @@
<div class="text-sm text-zinc-400">No local files found</div>
{/each}
{/await}
</div>
</Container>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import { modalStack } from '../Modal/modal.store';
export let modalId: symbol;
export let hidden: boolean = false;
</script>
<Container
on:navigate={({ detail }) => {
if (detail.direction === 'left' && detail.willLeaveContainer) {
modalStack.close(modalId);
detail.preventNavigation();
}
}}
focusOnMount
trapFocus
class={classNames('fixed inset-0 bg-stone-950/80 overflow-auto', {
'opacity-0': hidden
})}
canFocusEmpty
>
<div class="mx-20 py-16">
<slot />
</div>
</Container>

View File

@@ -8,6 +8,7 @@
import { derived } from 'svelte/store';
import ButtonGhost from '../Ghosts/ButtonGhost.svelte';
import type { SonarrRelease } from '../../apis/sonarr/sonarr-api';
import Container from '../../../Container.svelte';
type Release = RadarrRelease | SonarrRelease;
@@ -33,16 +34,16 @@
});
</script>
<div class="flex flex-col -my-1">
<Container class="flex flex-col -my-2">
{#if $isLoading}
{#each new Array(5) as _, index}
<div class="flex-1 my-1">
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{/each}
{:else}
{#each (showAll ? $releases : $filteredReleases)?.filter((r) => r.guid && r.indexerId) || [] as release, index}
<div class="flex-1 my-1">
<div class="flex-1 my-2">
<Button
on:clickOrSelect={() => selectRelease(release)}
let:hasFocus
@@ -118,4 +119,4 @@
</div>
{/if}
{/if}
</div>
</Container>

View File

@@ -22,7 +22,7 @@
promise: downloads,
data: downloadsData,
refresh: refreshDownloads
} = useRequest(sonarrApi.getSonarrDownloadsById, id);
} = useRequest(sonarrApi.getDownloadsBySeriesId, id);
const handleGrabRelease = (guid: string, indexerId: number) =>
sonarrApi

View File

@@ -36,7 +36,7 @@
modalStack.create(
ReleaseListModal,
{
getReleases: () => sonarrApi.fetchSonarrReleases(id),
getReleases: () => sonarrApi.getEpisodeReleases(id),
selectRelease: handleSelectRelease
},
groupId
@@ -47,7 +47,7 @@
modalStack.create(
ReleaseListModal,
{
getReleases: () => sonarrApi.fetchSonarrSeasonReleases(seriesId, seasonNumber),
getReleases: () => sonarrApi.getSeasonReleases(seriesId, seasonNumber),
selectRelease: handleSelectRelease
},
groupId

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
import MMMainLayout from './MMMainLayout.svelte';
import MMAddToSonarr from './MMAddToSonarr.svelte';
import MMModal from '../MediaManager/MMModal.svelte';
import ReleaseList from '../MediaManager/ReleaseList.svelte';
import DownloadList from '../MediaManager/DownloadList.svelte';
import FileList from '../MediaManager/FileList.svelte';
import { log } from '../../utils';
export let id: number; // Tmdb ID
export let season: number;
export let episode: number;
export let modalId: symbol;
export let hidden: boolean;
const sonarrItem = sonarrApi.getSeriesByTmdbId(id);
const downloads = sonarrItem.then((si) => sonarrApi.getDownloadsBySeriesId(si?.id || -1));
const files = sonarrItem.then((si) => sonarrApi.getFilesBySeriesId(si?.id || -1));
const sonarrEpisode = sonarrItem.then((si) =>
sonarrApi
.getEpisodes(si?.id || -1, season)
.then(log)
.then((episodes) => episodes.find((e) => e.episodeNumber === episode))
);
sonarrItem.then((si) => console.log('sonarrItem', si));
sonarrEpisode.then((se) => console.log('sonarrEpisode', se));
console.log(id, season, episode);
const getReleases = () => sonarrEpisode.then((se) => sonarrApi.getEpisodeReleases(se?.id || -1));
const selectRelease = () => {};
const cancelDownload = sonarrApi.cancelDownloadSonarrEpisode;
const handleSelectFile = () => {};
</script>
<MMModal {modalId} {hidden}>
{#await sonarrEpisode then sonarrEpisode}
{#if !sonarrEpisode}
<MMAddToSonarr />
{:else}
<MMMainLayout>
<h1 slot="title">{sonarrEpisode?.title}</h1>
<h2 slot="subtitle">Season {season} Episode {episode}</h2>
<ReleaseList slot="releases" {getReleases} {selectRelease} />
<DownloadList slot="downloads" {downloads} {cancelDownload} />
<FileList slot="local-files" {files} {handleSelectFile} />
</MMMainLayout>
{/if}
{/await}
</MMModal>

View File

@@ -0,0 +1,31 @@
<script>
import Container from '../../../Container.svelte';
</script>
<div class="flex flex-col">
<div class="mb-16">
<div class="text-4xl font-semibold">
<slot name="title" />
</div>
<div class="text-zinc-300 font-medium text-lg mt-2">
<slot name="subtitle" />
</div>
</div>
<Container direction="horizontal" class="grid grid-cols-2 gap-16">
<div class="flex flex-col">
<h1 class="text-2xl font-semibold mb-4">Releases</h1>
<slot name="releases" />
</div>
<div class="flex flex-col">
<div class="flex flex-col mb-8">
<h1 class="text-2xl font-semibold mb-4">Local Files</h1>
<slot name="local-files" />
</div>
<div class="flex flex-col mb-8">
<h1 class="text-2xl font-semibold mb-4">Downloads</h1>
<slot name="downloads" />
</div>
</div>
</Container>
</div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import MMMainLayout from './MMMainLayout.svelte';
import MMAddToSonarr from './MMAddToSonarr.svelte';
import MMModal from '../MediaManager/MMModal.svelte';
import ReleaseList from '../MediaManager/ReleaseList.svelte';
import DownloadList from '../MediaManager/DownloadList.svelte';
import FileList from '../MediaManager/FileList.svelte';
import { radarrApi } from '../../apis/radarr/radarr-api';
export let id: number; // Tmdb ID
export let modalId: symbol;
export let hidden: boolean;
const radarrItem = radarrApi.getMovieByTmdbId(id);
const downloads = radarrItem.then((i) => radarrApi.getDownloadsById(i?.id || -1));
const files = radarrItem.then((i) => radarrApi.getFilesByMovieId(i?.id || -1));
const getReleases = () => radarrItem.then((si) => radarrApi.getReleases(si?.id || -1));
const selectRelease = () => {};
const cancelDownload = radarrApi.cancelDownloadRadarrMovie;
const handleSelectFile = () => {};
</script>
<MMModal {modalId} {hidden}>
{#await radarrItem then movie}
{#if !movie}
<MMAddToSonarr />
{:else}
<MMMainLayout>
<h1 slot="title">{movie?.title}</h1>
<ReleaseList slot="releases" {getReleases} {selectRelease} />
<DownloadList slot="downloads" {downloads} {cancelDownload} />
<FileList slot="local-files" {files} {handleSelectFile} />
</MMMainLayout>
{/if}
{/await}
</MMModal>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
import MMMainLayout from './MMMainLayout.svelte';
import MMAddToSonarr from './MMAddToSonarr.svelte';
import MMModal from '../MediaManager/MMModal.svelte';
import ReleaseList from '../MediaManager/ReleaseList.svelte';
import DownloadList from '../MediaManager/DownloadList.svelte';
import FileList from '../MediaManager/FileList.svelte';
export let id: number; // Tmdb ID
export let season: number;
export let modalId: symbol;
export let hidden: boolean;
const sonarrItem = sonarrApi.getSeriesByTmdbId(id);
const downloads = sonarrItem.then((si) => sonarrApi.getDownloadsBySeriesId(si?.id || -1));
const files = sonarrItem.then((si) => sonarrApi.getFilesBySeriesId(si?.id || -1));
const getReleases = () =>
sonarrItem.then((si) => sonarrApi.getSeasonReleases(si?.id || -1, season));
const selectRelease = () => {};
const cancelDownload = sonarrApi.cancelDownloadSonarrEpisode;
const handleSelectFile = () => {};
</script>
<MMModal {modalId} {hidden}>
{#await sonarrItem then series}
{#if !series}
<MMAddToSonarr />
{:else}
<MMMainLayout>
<h1 slot="title">{series?.title}</h1>
<h2 slot="subtitle">Season {season} Packs</h2>
<ReleaseList slot="releases" {getReleases} {selectRelease} />
<DownloadList slot="downloads" {downloads} {cancelDownload} />
<FileList slot="local-files" {files} {handleSelectFile} />
</MMMainLayout>
{/if}
{/await}
</MMModal>

View File

@@ -1,5 +1,8 @@
import type { ComponentType, SvelteComponentTyped } from 'svelte';
import { derived, writable } from 'svelte/store';
import SeasonMediaManagerModal from '../MediaManagerModal/SeasonMediaManagerModal.svelte';
import EpisodeMediaManagerModal from '../MediaManagerModal/EpisodeMediaManagerModal.svelte';
import MovieMediaManagerModal from '../MediaManagerModal/MovieMediaManagerModal.svelte';
type ModalItem = {
id: symbol;
@@ -49,6 +52,15 @@ function createModalStack() {
export const modalStack = createModalStack();
export const modalStackTop = modalStack.top;
export const openSeasonMediaManager = (tmdbId: number, season: number) =>
modalStack.create(SeasonMediaManagerModal, { id: tmdbId, season });
export const openEpisodeMediaManager = (tmdbId: number, season: number, episode: number) =>
modalStack.create(EpisodeMediaManagerModal, { id: tmdbId, season, episode });
export const openMovieMediaManager = (tmdbId: number) =>
modalStack.create(MovieMediaManagerModal, { id: tmdbId });
// let lastTitleModal: symbol | undefined = undefined;
// export function openTitleModal(titleId: TitleId) {
// if (lastTitleModal) {

View File

@@ -12,6 +12,9 @@
import classNames from 'classnames';
import ScrollHelper from '../ScrollHelper.svelte';
import { useNavigate } from 'svelte-navigator';
import ManageSeasonCard from './ManageSeasonCard.svelte';
import { TMDB_BACKDROP_SMALL } from '../../constants';
import { modalStack, openSeasonMediaManager } from '../Modal/modal.store';
const navigate = useNavigate();
@@ -107,5 +110,9 @@
on:clickOrSelect={() => handleOpenEpisodePage(episode)}
/>
{/each}
<ManageSeasonCard
backdropUrl={TMDB_BACKDROP_SMALL + $tmdbSeries?.backdrop_path}
on:clickOrSelect={() => openSeasonMediaManager(id, seasonIndex + 1)}
/>
</CardGrid>
</Container>

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import type { Readable } from 'svelte/store';
import AnimateScale from '../AnimateScale.svelte';
import classNames from 'classnames';
import { Plus, PlusCircled } from 'radix-icons-svelte';
export let backdropUrl: string;
let hasFocus: Readable<boolean>;
</script>
<AnimateScale hasFocus={$hasFocus}>
<Container
class={classNames(
'w-full h-64',
'flex flex-col shrink-0',
'overflow-hidden rounded-2xl cursor-pointer group relative selectable transition-opacity'
)}
on:clickOrSelect
bind:hasFocus
>
<div
class="bg-cover bg-center absolute inset-0"
style={`background-image: url('${backdropUrl}')`}
/>
<div class="absolute inset-0 bg-secondary-800/75 flex items-center justify-center">
<div class="rounded-full p-2.5 bg-secondary-800/75">
<Plus size={32} />
</div>
</div>
</Container>
</AnimateScale>

View File

@@ -15,13 +15,13 @@
import { derived } from 'svelte/store';
import { scrollIntoView, useRegistrar } from '../../selectable';
import ScrollHelper from '../ScrollHelper.svelte';
import SonarrMediaMangerModal from '../MediaManager/sonarr/SonarrMediaMangerModal.svelte';
import Carousel from '../Carousel/Carousel.svelte';
import TmdbPersonCard from '../PersonCard/TmdbPersonCard.svelte';
import TmdbCard from '../Card/TmdbCard.svelte';
import EpisodeGrid from './EpisodeGrid.svelte';
import { Route } from 'svelte-navigator';
import EpisodePage from '../../pages/EpisodePage.svelte';
import SeriesMediaManagerModal from '../MediaManagerModal/SeasonMediaManagerModal.svelte';
export let id: string;
@@ -168,29 +168,18 @@
<Play size={19} slot="icon" />
</Button>
{/if}
{#if sonarrItem}
<Button
class="mr-4"
on:clickOrSelect={() =>
modalStack.create(SonarrMediaMangerModal, { id: sonarrItem.id || -1 })}
>
{#if jellyfinItem}
Manage Files
{:else}
Request
{/if}
<svelte:component this={jellyfinItem ? File : Download} size={19} slot="icon" />
</Button>
{:else}
<Button
class="mr-4"
on:clickOrSelect={() => addSeriesToSonarr(Number(id))}
inactive={$addSeriesToSonarrFetching}
>
Add to Sonarr
<Plus slot="icon" size={19} />
</Button>
{/if}
<Button
class="mr-4"
on:clickOrSelect={() =>
modalStack.create(SeriesMediaManagerModal, { id: Number(id) })}
>
{#if jellyfinItem}
Manage Media
{:else}
Request
{/if}
<svelte:component this={jellyfinItem ? File : Download} size={19} slot="icon" />
</Button>
{#if PLATFORM_WEB}
<Button class="mr-4">
Open In TMDB