mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-21 08:15:12 +02:00
feat: Deleting episodes and seasons, work in mm redesign
This commit is contained in:
@@ -1,49 +1,88 @@
|
||||
<script lang="ts">
|
||||
import ButtonGhost from '../Ghosts/ButtonGhost.svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
import { formatSize } from '../../utils.js';
|
||||
import type { FileResource } from '../../apis/combined-types';
|
||||
import Table from '../Table/Table.svelte';
|
||||
import MMLocalFileRow from '../MediaManagerModal/MMLocalFileRow.svelte';
|
||||
import TableHeaderRow from '../Table/TableHeaderRow.svelte';
|
||||
import TableHeaderSortBy from '../Table/TableHeaderSortBy.svelte';
|
||||
import TableHeaderCell from '../Table/TableHeaderCell.svelte';
|
||||
import Container from '../../../Container.svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { Trash } from 'radix-icons-svelte';
|
||||
import { scrollIntoView } from '../../selectable';
|
||||
import type { DeleteFile } from '../MediaManagerModal/MediaManagerModal';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import MMConfirmDeleteFileDialog from '../MediaManagerModal/Dialogs/MMConfirmDeleteFileDialog.svelte';
|
||||
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||
|
||||
export let files: Promise<FileResource[]>;
|
||||
export let handleSelectFile: (file: FileResource) => void;
|
||||
export let deleteFile: DeleteFile;
|
||||
// export let handleSelectFile: (file: FileResource) => void;
|
||||
|
||||
let sortBy: 'size' | 'quality' | 'title' | 'runtime' | undefined = 'title';
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const toggleSortBy = (sort: typeof sortBy) => () => {
|
||||
if (sortBy === sort) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortBy = sort;
|
||||
sortDirection = 'desc';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col -my-2">
|
||||
{#await files}
|
||||
{#each new Array(5) as _, index}
|
||||
<div class="flex-1 my-2">
|
||||
<ButtonGhost />
|
||||
</div>
|
||||
{#await files}
|
||||
{#each new Array(5) as _, index}
|
||||
<div class="flex-1 my-2">
|
||||
<ButtonGhost />
|
||||
</div>
|
||||
{/each}
|
||||
{:then files}
|
||||
<div class="grid grid-cols-[1fr_max-content_max-content_max-content_max-content] gap-y-4">
|
||||
<TableHeaderRow>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'title' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('title')}>Title</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'runtime' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('runtime')}>Runtime</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'size' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('size')}>Size</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'quality' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('quality')}>Quality</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderCell />
|
||||
</TableHeaderRow>
|
||||
{#each files as file}
|
||||
<MMLocalFileRow {file} {deleteFile} />
|
||||
{/each}
|
||||
{:then files}
|
||||
{#each files as file, index}
|
||||
<div class="flex-1 my-2">
|
||||
<Button
|
||||
on:clickOrSelect={() => handleSelectFile(file)}
|
||||
let:hasFocus
|
||||
on:enter={scrollIntoView({ vertical: 64 })}
|
||||
>
|
||||
<div class="flex items-center w-full">
|
||||
<div class="flex-1">
|
||||
{file.relativePath}
|
||||
</div>
|
||||
{#if hasFocus}
|
||||
<div class="flex items-center">
|
||||
Details
|
||||
<ChevronRight size={19} class="ml-1" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center text-zinc-400">
|
||||
{formatSize(file.size || 0)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-zinc-400">No local files found</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
{#if files?.length}
|
||||
<Container
|
||||
direction="horizontal"
|
||||
class="flex mt-8 mx-12"
|
||||
on:enter={scrollIntoView({ vertical: 128 })}
|
||||
>
|
||||
<Button
|
||||
on:clickOrSelect={() =>
|
||||
modalStack.create(MMConfirmDeleteFileDialog, {
|
||||
deleteFile: () => sonarrApi.deleteSonarrEpisodes(files.map((f) => f.id || -1))
|
||||
})}
|
||||
>
|
||||
Delete all
|
||||
<Trash size={19} slot="icon" />
|
||||
</Button>
|
||||
</Container>
|
||||
{:else}
|
||||
<div class="text-zinc-400 font-medium mx-12 flex flex-col items-center justify-center h-full">
|
||||
<h1 class="text-xl text-zinc-300">No local files found</h1>
|
||||
<div>Your local files will appear here.</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { type RadarrRelease } from '../../apis/radarr/radarr-api';
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from '../../stores/data.store';
|
||||
import { derived } from 'svelte/store';
|
||||
import ButtonGhost from '../Ghosts/ButtonGhost.svelte';
|
||||
import type { SonarrRelease } from '../../apis/sonarr/sonarr-api';
|
||||
import Container from '../../../Container.svelte';
|
||||
import MMReleaseListRow from '../MediaManagerModal/MMReleaseListRow.svelte';
|
||||
import AnimateScale from '../AnimateScale.svelte';
|
||||
import Table from '../Table/Table.svelte';
|
||||
import TableHeaderRow from '../Table/TableHeaderRow.svelte';
|
||||
import TableHeaderSortBy from '../Table/TableHeaderSortBy.svelte';
|
||||
import type { GrabRelease } from '../MediaManagerModal/MediaManagerModal';
|
||||
import Container from '../../../Container.svelte';
|
||||
import TableHeaderCell from '../Table/TableHeaderCell.svelte';
|
||||
|
||||
type Release = RadarrRelease | SonarrRelease;
|
||||
|
||||
export let getReleases: () => Promise<Release[]>;
|
||||
export let selectRelease: (release: Release) => void;
|
||||
export let releases: Promise<Release[]>;
|
||||
export let grabRelease: GrabRelease;
|
||||
|
||||
let showAll = false;
|
||||
let sortBy: 'size' | 'quality' | 'seeders' | 'age' | undefined = 'seeders';
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const { data: releases, isLoading } = useRequest(getReleases);
|
||||
|
||||
const filteredReleases = derived(releases, ($releases) => {
|
||||
if (!$releases) return [];
|
||||
let filtered = $releases.slice();
|
||||
function getRecommendedReleases(releases: Release[]) {
|
||||
if (!releases) return [];
|
||||
let filtered = releases.slice();
|
||||
|
||||
const releaseIsEnough = (r: Release) => r?.quality?.quality?.resolution || 0 > 720;
|
||||
filtered.sort((a, b) => (b.seeders || 0) - (a.seeders || 0));
|
||||
@@ -30,85 +30,78 @@
|
||||
filtered.sort((a, b) => (b.size || 0) - (a.size || 0));
|
||||
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
const toggleSortBy = (sort: typeof sortBy) => () => {
|
||||
if (sortBy === sort) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortBy = sort;
|
||||
sortDirection = 'desc';
|
||||
}
|
||||
};
|
||||
|
||||
function getSortFn(sb: typeof sortBy, sd: typeof sortDirection) {
|
||||
return (a: Release, b: Release) => {
|
||||
if (sb === 'size') {
|
||||
return (sd === 'asc' ? 1 : -1) * ((a.size || 0) - (b.size || 0));
|
||||
}
|
||||
if (sb === 'quality') {
|
||||
return (
|
||||
(sd === 'asc' ? 1 : -1) *
|
||||
((a.quality?.quality?.resolution || 0) - (b.quality?.quality?.resolution || 0))
|
||||
);
|
||||
}
|
||||
if (sb === 'seeders') {
|
||||
return (sd === 'asc' ? 1 : -1) * ((a.seeders || 0) - (b.seeders || 0));
|
||||
}
|
||||
if (sb === 'age') {
|
||||
return (sd === 'asc' ? 1 : -1) * ((b.ageHours || 0) - (a.ageHours || 0));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="w-full border-spacing-y-2 border-spacing-x-2 border-separate -mx-8">
|
||||
{#if $isLoading}
|
||||
{#each new Array(5) as _, index}
|
||||
<div class="flex-1 my-2">
|
||||
<ButtonGhost />
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<thead>
|
||||
<Container tag="tr" direction="horizontal">
|
||||
<Container tag="th" let:hasFocus>
|
||||
<AnimateScale {hasFocus} class="ml-8">
|
||||
<div
|
||||
class={classNames('float-left rounded-full px-3 py-1', {
|
||||
'bg-primary-500 text-secondary-800': hasFocus
|
||||
})}
|
||||
>
|
||||
Title
|
||||
</div>
|
||||
</AnimateScale>
|
||||
</Container>
|
||||
<Container tag="th" let:hasFocus>
|
||||
<AnimateScale {hasFocus}>
|
||||
<div
|
||||
class={classNames('float-left rounded-full px-3 py-1', {
|
||||
'bg-primary-500 text-secondary-800': hasFocus
|
||||
})}
|
||||
>
|
||||
Size
|
||||
</div>
|
||||
</AnimateScale>
|
||||
</Container>
|
||||
<Container tag="th" let:hasFocus>
|
||||
<AnimateScale {hasFocus}>
|
||||
<div
|
||||
class={classNames('float-left rounded-full px-3 py-1', {
|
||||
'bg-primary-500 text-secondary-800': hasFocus
|
||||
})}
|
||||
>
|
||||
Peers
|
||||
</div>
|
||||
</AnimateScale>
|
||||
</Container>
|
||||
<Container tag="th" let:hasFocus>
|
||||
<AnimateScale {hasFocus}>
|
||||
<div
|
||||
class={classNames('float-left rounded-full px-3 py-1', {
|
||||
'bg-primary-500 text-secondary-800': hasFocus
|
||||
})}
|
||||
>
|
||||
Quality
|
||||
</div>
|
||||
</AnimateScale>
|
||||
</Container>
|
||||
<th />
|
||||
</Container>
|
||||
</thead>
|
||||
<Container focusOnMount tag="tbody" class="">
|
||||
{#each $filteredReleases?.filter((r) => r.guid && r.indexerId) || [] as release, index}
|
||||
<MMReleaseListRow {release} />
|
||||
{#await releases}
|
||||
{#each new Array(5) as _, index}
|
||||
<div class="flex-1 my-2">
|
||||
<ButtonGhost />
|
||||
</div>
|
||||
{/each}
|
||||
{:then releases}
|
||||
<div class="grid grid-cols-[1fr_max-content_max-content_max-content_max-content] gap-y-4">
|
||||
<TableHeaderRow>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'age' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('age')}>Age</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'size' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('size')}>Size</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'seeders' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('seeders')}>Peers</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderSortBy
|
||||
icon={sortBy === 'quality' ? sortDirection : undefined}
|
||||
on:clickOrSelect={toggleSortBy('quality')}>Quality</TableHeaderSortBy
|
||||
>
|
||||
<TableHeaderCell />
|
||||
</TableHeaderRow>
|
||||
<Container class="contents" focusOnMount>
|
||||
{#each getRecommendedReleases(releases).sort(getSortFn(sortBy, sortDirection)) as release, index}
|
||||
<MMReleaseListRow {release} {grabRelease} />
|
||||
{/each}
|
||||
</Container>
|
||||
<h1 class="text-2xl font-semibold mb-4 mt-8 mx-8">All Releases</h1>
|
||||
<tbody class="divide-y divide-zinc-500">
|
||||
{#each $releases?.filter((r) => r.guid && r.indexerId) || [] as release, index}
|
||||
<MMReleaseListRow {release} />
|
||||
{/each}
|
||||
</tbody>
|
||||
{/if}
|
||||
</table>
|
||||
<!--{#if !showAll && $releases?.length}-->
|
||||
<!-- <div class="my-1 w-full">-->
|
||||
<!-- <Button on:clickOrSelect={() => (showAll = true)}>Show all {$releases?.length} releases</Button>-->
|
||||
<!-- </div>-->
|
||||
<!--{:else if showAll}-->
|
||||
<!-- <div class="my-1 w-full">-->
|
||||
<!-- <Button on:clickOrSelect={() => (showAll = false)}>Show less</Button>-->
|
||||
<!-- </div>-->
|
||||
<!--{/if}-->
|
||||
|
||||
<h1 class="text-2xl font-semibold mb-4 mt-8 col-span-5 mx-12">All Releases</h1>
|
||||
|
||||
{#each releases
|
||||
.filter((r) => r.guid && r.indexerId)
|
||||
.sort(getSortFn(sortBy, sortDirection)) as release, index}
|
||||
<MMReleaseListRow {release} {grabRelease} />
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
const handleGrabRelease = (guid: string, indexerId: number) =>
|
||||
sonarrApi
|
||||
.downloadSonarrEpisode(guid, indexerId)
|
||||
.downloadSonarrRelease(guid, indexerId)
|
||||
.then((ok) => {
|
||||
if (!ok) {
|
||||
// TODO: Show error
|
||||
|
||||
Reference in New Issue
Block a user