mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-19 17:53:25 +02:00
feat: Managing movie files, requesting movies
This commit is contained in:
279
src/lib/components/MediaManagerModal/MMAddToRadarrDialog.svelte
Normal file
279
src/lib/components/MediaManagerModal/MMAddToRadarrDialog.svelte
Normal file
@@ -0,0 +1,279 @@
|
||||
<script lang="ts">
|
||||
import Dialog from '../Dialog/Dialog.svelte';
|
||||
import { TMDB_BACKDROP_SMALL } from '../../constants';
|
||||
import { type BackEvent, scrollIntoView, type Selectable } from '../../selectable';
|
||||
import { createLocalStorageStore } from '../../stores/localstorage.store';
|
||||
import {
|
||||
movieAvailabilities,
|
||||
type MovieAvailability,
|
||||
radarrApi
|
||||
} from '../../apis/radarr/radarr-api';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import classNames from 'classnames';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Container from '../../../Container.svelte';
|
||||
import { capitalize, formatSize } from '../../utils';
|
||||
import { ArrowRight, Check, Plus } from 'radix-icons-svelte';
|
||||
import Button from '../Button.svelte';
|
||||
|
||||
type AddOptionsStore = {
|
||||
rootFolderPath: string | null;
|
||||
qualityProfileId: number | null;
|
||||
minimumAvailability: MovieAvailability | null;
|
||||
};
|
||||
|
||||
export let backdropUri: string;
|
||||
export let tmdbId: number;
|
||||
export let title: string;
|
||||
export let onComplete: () => void = () => {};
|
||||
|
||||
export let modalId: symbol;
|
||||
$: backgroundUrl = TMDB_BACKDROP_SMALL + backdropUri;
|
||||
|
||||
let tab: 'add-to-radarr' | 'root-folders' | 'quality-profiles' | 'monitor-settings' =
|
||||
'add-to-radarr';
|
||||
let addToSonarrTab: Selectable;
|
||||
let rootFoldersTab: Selectable;
|
||||
let qualityProfilesTab: Selectable;
|
||||
let monitorSettingsTab: Selectable;
|
||||
$: {
|
||||
if (tab === 'add-to-radarr' && addToSonarrTab) addToSonarrTab.focus();
|
||||
if (tab === 'root-folders' && rootFoldersTab) rootFoldersTab.focus();
|
||||
if (tab === 'quality-profiles' && qualityProfilesTab) qualityProfilesTab.focus();
|
||||
if (tab === 'monitor-settings' && monitorSettingsTab) monitorSettingsTab.focus();
|
||||
}
|
||||
const addOptionsStore = createLocalStorageStore<AddOptionsStore>('add-to-radarr-options', {
|
||||
rootFolderPath: null,
|
||||
qualityProfileId: null,
|
||||
minimumAvailability: null
|
||||
});
|
||||
|
||||
const sonarrOptions = Promise.all([
|
||||
radarrApi.getRootFolders(),
|
||||
radarrApi.getQualityProfiles()
|
||||
]).then(([rootFolders, qualityProfiles]) => ({ rootFolders, qualityProfiles }));
|
||||
|
||||
sonarrOptions.then((s) => {
|
||||
addOptionsStore.update((prev) => ({
|
||||
rootFolderPath: prev.rootFolderPath || s.rootFolders[0]?.path || null,
|
||||
qualityProfileId: prev.qualityProfileId || s.qualityProfiles[0]?.id || null,
|
||||
minimumAvailability: prev.minimumAvailability || 'released'
|
||||
}));
|
||||
});
|
||||
addOptionsStore.subscribe(() => (tab = 'add-to-radarr'));
|
||||
|
||||
function handleAddToSonarr() {
|
||||
return radarrApi
|
||||
.addMovieToRadarr(tmdbId, {
|
||||
rootFolderPath: $addOptionsStore.rootFolderPath || undefined,
|
||||
qualityProfileId: $addOptionsStore.qualityProfileId || undefined,
|
||||
minimumAvailability: $addOptionsStore.minimumAvailability || undefined
|
||||
})
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
modalStack.close(modalId);
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleBack(e: BackEvent) {
|
||||
if (tab !== 'add-to-radarr') {
|
||||
tab = 'add-to-radarr';
|
||||
e.detail.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
const tabClasses = (active: boolean, secondary: boolean = false) =>
|
||||
classNames('flex flex-col transition-all', {
|
||||
'opacity-0 pointer-events-none': !active,
|
||||
'-translate-x-10': !active && !secondary,
|
||||
'translate-x-10': !active && secondary,
|
||||
'absolute inset-0': secondary
|
||||
});
|
||||
|
||||
const listItemClass = `flex items-center justify-between bg-primary-900 rounded-xl px-6 py-2.5 mb-4 font-medium
|
||||
border-2 border-transparent focus:border-primary-500 hover:border-primary-500 cursor-pointer group`;
|
||||
|
||||
const scaledArrowClas = (hasFocus: boolean) =>
|
||||
classNames('transition-transform', {
|
||||
'text-primary-500 translate-x-0.5 scale-110': hasFocus,
|
||||
'group-hover:text-primary-500 group-hover:translate-x-0.5 group-hover:scale-110': true
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog>
|
||||
{#if backgroundUrl && tab === 'add-to-radarr'}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="absolute inset-0 bg-cover bg-center h-52"
|
||||
style="background-image: url({backgroundUrl}); -webkit-mask-image: radial-gradient(at 90% 10%, hsla(0,0%,0%,1) 0px, transparent 70%);"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#await sonarrOptions then { qualityProfiles, rootFolders }}
|
||||
{@const selectedRootFolder = rootFolders.find(
|
||||
(f) => f.path === $addOptionsStore.rootFolderPath
|
||||
)}
|
||||
{@const selectedQualityProfile = qualityProfiles.find(
|
||||
(f) => f.id === $addOptionsStore.qualityProfileId
|
||||
)}
|
||||
<Container on:back={handleBack} class="relative">
|
||||
<Container
|
||||
trapFocus
|
||||
bind:selectable={addToSonarrTab}
|
||||
class={tabClasses(tab === 'add-to-radarr')}
|
||||
>
|
||||
<div class="z-10 mb-8">
|
||||
<div class="h-24" />
|
||||
<h1 class="header2">Add {title} to Sonarr?</h1>
|
||||
<div class="font-medium text-secondary-300 mb-8">
|
||||
Before you can fetch episodes, you need to add this series to Sonarr.
|
||||
</div>
|
||||
<Container
|
||||
class={listItemClass}
|
||||
on:clickOrSelect={() => (tab = 'root-folders')}
|
||||
let:hasFocus
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-secondary-300 font-semibold tracking-wide text-sm">Root Folder</h1>
|
||||
{selectedRootFolder?.path}
|
||||
({formatSize(selectedRootFolder?.freeSpace || 0)} left)
|
||||
</div>
|
||||
<ArrowRight class={scaledArrowClas(hasFocus)} size={24} />
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
class={listItemClass}
|
||||
on:clickOrSelect={() => (tab = 'quality-profiles')}
|
||||
let:hasFocus
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-secondary-300 font-semibold tracking-wide text-sm">
|
||||
Quality Profile
|
||||
</h1>
|
||||
<span>
|
||||
{selectedQualityProfile?.name}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight class={scaledArrowClas(hasFocus)} size={24} />
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
class={listItemClass}
|
||||
on:clickOrSelect={() => (tab = 'monitor-settings')}
|
||||
let:hasFocus
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-secondary-300 font-semibold tracking-wide text-sm">
|
||||
Minimum Availability
|
||||
</h1>
|
||||
<span>
|
||||
{capitalize($addOptionsStore.minimumAvailability || 'released')}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight class={scaledArrowClas(hasFocus)} size={24} />
|
||||
</Container>
|
||||
|
||||
<!-- <Container class="flex items-center" on:clickOrSelect={() => (tab = 'quality-profiles')}>-->
|
||||
<!-- {qualityProfile?.name}-->
|
||||
<!-- <ArrowRight size={19} />-->
|
||||
<!-- </Container>-->
|
||||
<!-- <Container class="flex items-center" on:clickOrSelect={() => (tab = 'monitor-settings')}>-->
|
||||
<!-- Monitor {$addOptionsStore.monitorSettings}-->
|
||||
<!-- <ArrowRight size={19} />-->
|
||||
<!-- </Container>-->
|
||||
</div>
|
||||
<Container class="flex flex-col space-y-4">
|
||||
<Button type="primary-dark" action={handleAddToSonarr} focusOnMount>
|
||||
<Plus size={19} slot="icon" />
|
||||
Add to Radarr
|
||||
</Button>
|
||||
<Button type="primary-dark" on:clickOrSelect={() => modalStack.close(modalId)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
trapFocus
|
||||
class={tabClasses(tab === 'root-folders', true)}
|
||||
bind:selectable={rootFoldersTab}
|
||||
>
|
||||
<h1 class="text-xl text-secondary-100 font-medium mb-4">Root Folder</h1>
|
||||
<div class="min-h-0 overflow-y-auto scrollbar-hide">
|
||||
{#each rootFolders as rootFolder}
|
||||
<Container
|
||||
class={listItemClass}
|
||||
on:enter={scrollIntoView({ vertical: 64 })}
|
||||
on:clickOrSelect={() =>
|
||||
addOptionsStore.update((prev) => ({ ...prev, rootFolderId: rootFolder.id || 0 }))}
|
||||
focusOnClick
|
||||
focusOnMount={$addOptionsStore.rootFolderPath === rootFolder.path}
|
||||
>
|
||||
<div>
|
||||
{rootFolder.path} ({formatSize(rootFolder.freeSpace || 0)} left)
|
||||
</div>
|
||||
{#if selectedRootFolder?.id === rootFolder.id}
|
||||
<Check size={24} />
|
||||
{/if}
|
||||
</Container>
|
||||
{/each}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
trapFocus
|
||||
class={tabClasses(tab === 'quality-profiles', true)}
|
||||
bind:selectable={qualityProfilesTab}
|
||||
>
|
||||
<h1 class="text-xl text-secondary-100 font-medium mb-4">Quality Profile</h1>
|
||||
<div class="min-h-0 overflow-y-auto scrollbar-hide">
|
||||
{#each qualityProfiles as qualityProfile}
|
||||
<Container
|
||||
class={listItemClass}
|
||||
on:enter={scrollIntoView({ vertical: 64 })}
|
||||
on:clickOrSelect={() =>
|
||||
addOptionsStore.update((prev) => ({
|
||||
...prev,
|
||||
qualityProfileId: qualityProfile.id || 0
|
||||
}))}
|
||||
focusOnClick
|
||||
focusOnMount={$addOptionsStore.qualityProfileId === qualityProfile.id}
|
||||
>
|
||||
<div>{qualityProfile.name}</div>
|
||||
{#if selectedQualityProfile?.id === qualityProfile.id}
|
||||
<Check size={24} />
|
||||
{/if}
|
||||
</Container>
|
||||
{/each}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
trapFocus
|
||||
class={tabClasses(tab === 'monitor-settings', true)}
|
||||
bind:selectable={monitorSettingsTab}
|
||||
>
|
||||
<h1 class="text-xl text-secondary-100 font-medium mb-4">Monitor Episodes</h1>
|
||||
<div class="min-h-0 overflow-y-auto scrollbar-hide">
|
||||
{#each movieAvailabilities as availibility}
|
||||
<Container
|
||||
class={listItemClass}
|
||||
on:enter={scrollIntoView({ vertical: 64 })}
|
||||
on:clickOrSelect={() =>
|
||||
addOptionsStore.update((prev) => ({ ...prev, monitorOptions: availibility }))}
|
||||
focusOnClick
|
||||
focusOnMount={$addOptionsStore.minimumAvailability === availibility}
|
||||
>
|
||||
<div>{capitalize(availibility)}</div>
|
||||
{#if $addOptionsStore.minimumAvailability === availibility}
|
||||
<Check size={24} />
|
||||
{/if}
|
||||
</Container>
|
||||
{/each}
|
||||
</div>
|
||||
</Container>
|
||||
</Container>
|
||||
{/await}
|
||||
</Dialog>
|
||||
@@ -5,34 +5,52 @@
|
||||
import ReleaseList from './Releases/MMReleasesTab.svelte';
|
||||
import DownloadList from '../MediaManager/DownloadList.svelte';
|
||||
import FileList from './LocalFiles/MMLocalFilesTab.svelte';
|
||||
import { radarrApi } from '../../apis/radarr/radarr-api';
|
||||
import { radarrApi, type RadarrMovie } from '../../apis/radarr/radarr-api';
|
||||
import type { GrabReleaseFn } from './MediaManagerModal';
|
||||
import type { Release } from '../../apis/combined-types';
|
||||
import Dialog from '../Dialog/Dialog.svelte';
|
||||
import MMReleasesTab from './Releases/MMReleasesTab.svelte';
|
||||
|
||||
export let radarrItem: RadarrMovie;
|
||||
export let onGrabRelease: (release: Release) => void = () => {};
|
||||
|
||||
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));
|
||||
$: releases = radarrApi.getReleases(radarrItem.id || -1);
|
||||
|
||||
const getReleases = () => radarrItem.then((si) => radarrApi.getReleases(si?.id || -1));
|
||||
const selectRelease = () => {};
|
||||
|
||||
const cancelDownload = radarrApi.cancelDownloadRadarrMovie;
|
||||
const handleSelectFile = () => {};
|
||||
const grabRelease: GrabReleaseFn = (release) =>
|
||||
radarrApi.downloadMovie(release.guid || '', release.indexerId || -1).then((r) => {
|
||||
onGrabRelease(release);
|
||||
return r;
|
||||
});
|
||||
</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>
|
||||
<Dialog size="full" {modalId} {hidden}>
|
||||
<MMReleasesTab {releases} {grabRelease}>
|
||||
<h1 slot="title">{radarrItem?.title}</h1>
|
||||
<h2 slot="subtitle">
|
||||
Releases
|
||||
<!--{#if season}-->
|
||||
<!-- Season {season} Releases-->
|
||||
<!--{:else if 'episodeNumber' in sonarrItem}-->
|
||||
<!-- Episode {sonarrItem.episodeNumber} Releases-->
|
||||
<!--{/if}-->
|
||||
</h2>
|
||||
</MMReleasesTab>
|
||||
</Dialog>
|
||||
|
||||
<!--<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>-->
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
|
||||
export let season: number | undefined = undefined;
|
||||
export let sonarrItem: SonarrSeries | SonarrEpisode;
|
||||
export let onGrabRelease: (release: Release) => void = () => {};
|
||||
|
||||
export let modalId: symbol;
|
||||
export let hidden: boolean;
|
||||
export let onGrabRelease: (release: Release) => void = () => {};
|
||||
|
||||
$: releases = getReleases(season);
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
import { formatSize } from '../../utils';
|
||||
import { Cross1 } from 'radix-icons-svelte';
|
||||
import { capitalize } from '../../utils.js';
|
||||
import type { Download } from '../../apis/combined-types';
|
||||
|
||||
export let download: EpisodeDownload;
|
||||
export let episode: SonarrEpisode | undefined;
|
||||
export let download: Download;
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
export let backgroundUrl: string;
|
||||
export let onCancel: () => void;
|
||||
$: backgroundUrl = episode?.images?.[0]?.remoteUrl;
|
||||
console.log(download);
|
||||
|
||||
function handleCancelDownload() {
|
||||
return sonarrApi.cancelDownload(download.id || -1).then(() => onCancel());
|
||||
@@ -33,8 +34,8 @@
|
||||
{#if backgroundUrl}
|
||||
<div class="h-24" />
|
||||
{/if}
|
||||
<h1 class="header2">{episode?.title}</h1>
|
||||
<h2 class="header1 mb-4">Season {episode?.seasonNumber} Episode {episode?.episodeNumber}</h2>
|
||||
<h1 class="header2">{title}</h1>
|
||||
<h2 class="header1 mb-4">{subtitle}</h2>
|
||||
<div
|
||||
class="grid grid-cols-[1fr_max-content] font-medium mb-16
|
||||
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Dialog from '../Dialog/Dialog.svelte';
|
||||
import {
|
||||
type EpisodeFileResource,
|
||||
sonarrApi,
|
||||
type SonarrEpisode
|
||||
} from '../../apis/sonarr/sonarr-api';
|
||||
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||
import Button from '../Button.svelte';
|
||||
import Container from '../../../Container.svelte';
|
||||
import { formatSize } from '../../utils';
|
||||
import { Trash } from 'radix-icons-svelte';
|
||||
import type { FileResource } from '../../apis/combined-types';
|
||||
|
||||
export let file: EpisodeFileResource;
|
||||
export let episode: SonarrEpisode | undefined;
|
||||
export let file: FileResource;
|
||||
export let title = '';
|
||||
export let subtitle = '';
|
||||
export let backgroundUrl: string;
|
||||
export let onDelete: () => void;
|
||||
$: backgroundUrl = episode?.images?.[0]?.remoteUrl;
|
||||
|
||||
function handleDeleteFile() {
|
||||
return sonarrApi.deleteSonarrEpisode(file.id || -1).then(() => onDelete());
|
||||
@@ -31,8 +29,8 @@
|
||||
{#if backgroundUrl}
|
||||
<div class="h-24" />
|
||||
{/if}
|
||||
<h1 class="header2">{episode?.title}</h1>
|
||||
<h2 class="header1 mb-4">Season {episode?.seasonNumber} Episode {episode?.episodeNumber}</h2>
|
||||
<h1 class="header2">{title}</h1>
|
||||
<h2 class="header1 mb-4">{subtitle}</h2>
|
||||
<div
|
||||
class="grid grid-cols-[1fr_max-content] font-medium mb-16
|
||||
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
|
||||
|
||||
@@ -347,13 +347,17 @@
|
||||
if (file)
|
||||
modalStack.create(FileDetailsDialog, {
|
||||
file,
|
||||
episode,
|
||||
title: episode?.title || '',
|
||||
subtitle: `Season ${episode?.seasonNumber} Episode ${episode?.episodeNumber}`,
|
||||
backgroundUrl: episode?.images?.[0]?.remoteUrl || '',
|
||||
onDelete: () => (sonarrFiles = getFiles(sonarrItem))
|
||||
});
|
||||
else if (download)
|
||||
modalStack.create(DownloadDetailsDialog, {
|
||||
download,
|
||||
episode,
|
||||
title: episode?.title || '',
|
||||
subtitle: `Season ${episode?.seasonNumber} Episode ${episode?.episodeNumber}`,
|
||||
backgroundUrl: episode?.images?.[0]?.remoteUrl || '',
|
||||
onCancel: () => (sonarrDownloads = getDownloads(sonarrItem))
|
||||
});
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user