mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 08:45:13 +02:00
feat: List and delete local files
This commit is contained in:
20
src/lib/components/ManageMedia/FullScreenModal.svelte
Normal file
20
src/lib/components/ManageMedia/FullScreenModal.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
|
||||
export let modalId: symbol;
|
||||
</script>
|
||||
|
||||
<Container
|
||||
navigationActions={{
|
||||
left: () => {
|
||||
modalStack.close(modalId);
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
focusOnMount
|
||||
trapFocus
|
||||
class="fixed inset-0 bg-stone-950/80"
|
||||
>
|
||||
<slot />
|
||||
</Container>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="max-h-full mx-auto flex flex-col py-32 max-w-2xl">
|
||||
<h1 class="tracking-wide text-2xl font-semibold mb-4">
|
||||
<slot name="header">Header is missing</slot>
|
||||
</h1>
|
||||
<slot>Content is missing</slot>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { MovieFileResource } from '../../../apis/radarr/radarr-api';
|
||||
import Button from '../../Button.svelte';
|
||||
export let file: MovieFileResource;
|
||||
export let handleDeleteFile: (fileId: number) => Promise<boolean>;
|
||||
</script>
|
||||
|
||||
<div class="-my-1">
|
||||
<Button focusOnMount on:click={() => file.id && handleDeleteFile(file.id)}>Delete File</Button>
|
||||
</div>
|
||||
28
src/lib/components/ManageMedia/LocalFiles/FilesList.svelte
Normal file
28
src/lib/components/ManageMedia/LocalFiles/FilesList.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { useRequest } from '../../../stores/data.store';
|
||||
import type { MovieFileResource } from '../../../apis/radarr/radarr-api';
|
||||
import ButtonGhost from '../../Ghosts/ButtonGhost.svelte';
|
||||
import Button from '../../Button.svelte';
|
||||
|
||||
export let id: number;
|
||||
export let getFiles: (movieId: number) => Promise<MovieFileResource[]>;
|
||||
export let handleSelectFile: (file: MovieFileResource) => void;
|
||||
|
||||
const { data: files, isLoading } = useRequest(getFiles, id);
|
||||
</script>
|
||||
|
||||
<div class="-my-1">
|
||||
{#if $isLoading}
|
||||
{#each new Array(5) as _, index}
|
||||
<div class="flex-1 my-1">
|
||||
<ButtonGhost />
|
||||
</div>
|
||||
{/each}
|
||||
{:else if $files}
|
||||
{#each $files as file, index}
|
||||
<Button focusOnMount={index === 0} on:click={() => handleSelectFile(file)}>
|
||||
{file.relativePath}
|
||||
</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import FullScreenModal from '../FullScreenModal.svelte';
|
||||
import FullScreenModalContainer from '../FullScreenModalContainer.svelte';
|
||||
import Container from '../../../../Container.svelte';
|
||||
import FilesList from './FilesList.svelte';
|
||||
import { type MovieFileResource, radarrApi } from '../../../apis/radarr/radarr-api';
|
||||
import FileActionsList from './FileActionsList.svelte';
|
||||
import { formatSize } from '../../../utils';
|
||||
|
||||
export let id: number;
|
||||
export let modalId: symbol;
|
||||
|
||||
let selectedFile: MovieFileResource | undefined = undefined;
|
||||
|
||||
function handleSelectFile(file: MovieFileResource) {
|
||||
selectedFile = file;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FullScreenModal {modalId}>
|
||||
{#if !selectedFile}
|
||||
<FullScreenModalContainer>
|
||||
<h1 slot="header">Local Files</h1>
|
||||
<Container>
|
||||
<FilesList getFiles={radarrApi.getMovieFilesByMovieId} {handleSelectFile} {id} />
|
||||
</Container>
|
||||
</FullScreenModalContainer>
|
||||
{:else}
|
||||
<FullScreenModalContainer>
|
||||
<div slot="header" class="flex">
|
||||
<h1 class="line-clamp-1 flex-1 mr-4">
|
||||
{selectedFile.relativePath}
|
||||
</h1>
|
||||
<h1 class="text-zinc-300">{formatSize(selectedFile.size || 0)}</h1>
|
||||
</div>
|
||||
<Container>
|
||||
<FileActionsList file={selectedFile} handleDeleteFile={radarrApi.deleteRadarrMovieFile} />
|
||||
</Container>
|
||||
</FullScreenModalContainer>
|
||||
{/if}
|
||||
</FullScreenModal>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import FullScreenModal from '../FullScreenModal.svelte';
|
||||
import { radarrApi } from '../../../apis/radarr/radarr-api';
|
||||
import ReleaseList from './ReleaseList.svelte';
|
||||
import Container from '../../../../Container.svelte';
|
||||
import { scrollWithOffset } from '../../../selectable';
|
||||
import FullScreenModalContainer from '../FullScreenModalContainer.svelte';
|
||||
|
||||
export let id: number;
|
||||
export let modalId: symbol;
|
||||
</script>
|
||||
|
||||
<FullScreenModal {modalId}>
|
||||
<FullScreenModalContainer>
|
||||
<h1 slot="header">Download</h1>
|
||||
<Container
|
||||
childrenRevealStrategy={scrollWithOffset('all', 10)}
|
||||
class="flex-1 overflow-y-scroll"
|
||||
>
|
||||
<ReleaseList
|
||||
{id}
|
||||
grabRelease={radarrApi.downloadRadarrMovie}
|
||||
getReleases={radarrApi.fetchRadarrReleases}
|
||||
/>
|
||||
</Container>
|
||||
</FullScreenModalContainer>
|
||||
</FullScreenModal>
|
||||
136
src/lib/components/ManageMedia/RequestMedia/ReleaseList.svelte
Normal file
136
src/lib/components/ManageMedia/RequestMedia/ReleaseList.svelte
Normal file
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { type RadarrRelease } from '../../../apis/radarr/radarr-api';
|
||||
import type { SonarrRelease } from '../../../apis/sonarr/sonarrApi';
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from '../../../stores/data.store';
|
||||
import Button from '../../Button.svelte';
|
||||
import { DotFilled, Download, Plus } from 'radix-icons-svelte';
|
||||
import { formatMinutesToTime, formatSize } from '../../../utils';
|
||||
import { derived } from 'svelte/store';
|
||||
import ButtonGhost from '../../Ghosts/ButtonGhost.svelte';
|
||||
|
||||
export let id: number;
|
||||
export let getReleases: (id: number) => Promise<(RadarrRelease | SonarrRelease)[]>;
|
||||
export let grabRelease: (guid: string, indexerId: number) => Promise<boolean>;
|
||||
|
||||
let showAll = false;
|
||||
|
||||
const { data: releases, isLoading } = useRequest(getReleases, id);
|
||||
|
||||
const filteredReleases = derived(releases, ($releases) => {
|
||||
if (!$releases) return [];
|
||||
let filtered = $releases.slice();
|
||||
|
||||
filtered.sort((a, b) => (b.seeders || 0) - (a.seeders || 0));
|
||||
filtered = (filtered as any)
|
||||
.filter((release: any) => release?.quality?.quality?.resolution > 720)
|
||||
.slice(0, 5);
|
||||
|
||||
filtered.sort((a, b) => (b.size || 0) - (a.size || 0));
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const isFetchingGrab: Record<string, boolean> = {};
|
||||
const grabbedReleases: Record<string, boolean> = {};
|
||||
function handleGrabRelease(guid: string, indexerId: number) {
|
||||
isFetchingGrab[guid] = true;
|
||||
grabRelease(guid, indexerId).then((ok) => {
|
||||
isFetchingGrab[guid] = false;
|
||||
if (ok) {
|
||||
grabbedReleases[guid] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col -my-1">
|
||||
{#if $isLoading}
|
||||
{#each new Array(5) as _, index}
|
||||
<div class="flex-1 my-1">
|
||||
<ButtonGhost />
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#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}
|
||||
<div class="flex-1 my-1">
|
||||
<Button
|
||||
on:click={() =>
|
||||
!isFetching &&
|
||||
!isGrabbed &&
|
||||
handleGrabRelease(release.guid || '', release.indexerId || 0)}
|
||||
inactive={isFetching || isGrabbed}
|
||||
let:hasFocus
|
||||
focusOnMount={index === 0}
|
||||
>
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="flex-1 flex items-center">
|
||||
{#if !isGrabbed}
|
||||
<Plus size={19} class="mr-2" />
|
||||
{:else}
|
||||
<Download size={19} class="mr-2" />
|
||||
{/if}
|
||||
<div class="flex-1 flex mr-2">
|
||||
<div class="tracking-wide mr-2">{release.indexer}</div>
|
||||
<div
|
||||
class={classNames('mr-2', {
|
||||
'text-zinc-400': !hasFocus,
|
||||
'text-zinc-700': hasFocus
|
||||
})}
|
||||
>
|
||||
{release?.quality?.quality?.name}
|
||||
</div>
|
||||
<div
|
||||
class={classNames('mr-2', {
|
||||
'text-zinc-400': !hasFocus,
|
||||
'text-zinc-700': hasFocus
|
||||
})}
|
||||
>
|
||||
{release.seeders} seeders
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class={classNames({
|
||||
'text-zinc-400': !hasFocus,
|
||||
'text-zinc-700': hasFocus
|
||||
})}
|
||||
>
|
||||
{formatSize(release?.size || 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if hasFocus}
|
||||
<div class="flex text-xs text-zinc-700 items-center flex-wrap mt-2">
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{#if !showAll && $releases?.length}
|
||||
<div class="my-1 w-full">
|
||||
<Button on:click={() => (showAll = true)}>Show all {$releases?.length} releases</Button>
|
||||
</div>
|
||||
{:else if showAll}
|
||||
<div class="my-1 w-full">
|
||||
<Button on:click={() => (showAll = false)}>Show less</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user