improve UX of downloading torrents by splitting the dialogs into two steps; deduplicate code; fix bug which causes the directory preview to be incorrect

This commit is contained in:
maxDorninger
2025-10-29 15:56:00 +01:00
parent 49f8886db1
commit 82700abeb6
5 changed files with 221 additions and 187 deletions

View File

@@ -5,15 +5,13 @@
import { toast } from 'svelte-sonner';
import { Badge } from '$lib/components/ui/badge/index.js';
import { getFullyQualifiedMediaName } from '$lib/utils';
import { LoaderCircle } from 'lucide-svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import client from '$lib/api';
import type { components } from '$lib/api/api';
import SelectFilePathSuffixDialog from '$lib/components/select-file-path-suffix-dialog.svelte';
let { movie } = $props();
let dialogueState = $state(false);
let torrents: components['schemas']['IndexerQueryResult'][] = $state([]);
@@ -103,11 +101,6 @@
});
</script>
{#snippet saveDirectoryPreview(movie: components['schemas']['Movie'], filePathSuffix: string)}
/{getFullyQualifiedMediaName(movie)} [{movie.metadata_provider}id-{movie.external_id}
]/{movie.name}{filePathSuffix === '' ? '' : ' - ' + filePathSuffix}.mkv
{/snippet}
<Dialog.Root bind:open={dialogueState}>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}>Download Movie</Dialog.Trigger>
<Dialog.Content class="max-h-[90vh] w-fit min-w-[80vw] overflow-y-auto">
@@ -124,28 +117,24 @@
</Tabs.List>
<Tabs.Content value="basic">
<div class="grid w-full items-center gap-1.5">
<Label for="file-suffix">Filepath suffix</Label>
<Select.Root bind:value={filePathSuffix} type="single">
<Select.Trigger class="w-[180px]">{filePathSuffix}</Select.Trigger>
<Select.Content>
<Select.Item value="">None</Select.Item>
<Select.Item value="2160P">2160p</Select.Item>
<Select.Item value="1080P">1080p</Select.Item>
<Select.Item value="720P">720p</Select.Item>
<Select.Item value="480P">480p</Select.Item>
<Select.Item value="360P">360p</Select.Item>
</Select.Content>
</Select.Root>
<p class="text-muted-foreground text-sm">
This is necessary to differentiate between versions of the same movie, for example a
1080p and a 4K version.
</p>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
<Button
class="w-fit"
variant="secondary"
onclick={async () => {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
try {
torrents = await getTorrents();
} catch (error) {
console.log(error);
} finally {
isLoadingTorrents = false;
}
}}
>
<p class="text-muted-foreground text-sm" id="file-suffix-display">
{@render saveDirectoryPreview(movie, filePathSuffix)}
</p>
Search for Torrents
</Button>
</div>
</Tabs.Content>
<Tabs.Content value="advanced">
@@ -175,25 +164,6 @@
The custom query will override the default search string like "A Minecraft Movie
(2025)".
</p>
<Label for="file-suffix">Filepath suffix</Label>
<Input
bind:value={filePathSuffix}
class="max-w-sm"
id="file-suffix"
placeholder="1080P"
type="text"
/>
<p class="text-muted-foreground text-sm">
This is necessary to differentiate between versions of the same movie, for example a
1080p and a 4K version.
</p>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
>
<p class="text-muted-foreground text-sm" id="file-suffix-display">
{@render saveDirectoryPreview(movie, filePathSuffix)}
</p>
</div>
</Tabs.Content>
</Tabs.Root>
@@ -234,15 +204,11 @@
{/each}
</Table.Cell>
<Table.Cell class="text-right">
<Button
size="sm"
variant="outline"
onclick={() => {
downloadTorrent(torrent.id!);
}}
>
Download
</Button>
<SelectFilePathSuffixDialog
media={movie}
bind:filePathSuffix
callback={() => downloadTorrent(torrent.id!)}
/>
</Table.Cell>
</Table.Row>
{/each}

View File

@@ -3,19 +3,15 @@
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { toast } from 'svelte-sonner';
import {
convertTorrentSeasonRangeToIntegerRange,
formatSecondsToOptimalUnit,
getFullyQualifiedMediaName
} from '$lib/utils';
import { convertTorrentSeasonRangeToIntegerRange, formatSecondsToOptimalUnit } from '$lib/utils';
import { LoaderCircle } from 'lucide-svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import client from '$lib/api';
import type { components } from '$lib/api/api';
import SelectFilePathSuffixDialog from '$lib/components/select-file-path-suffix-dialog.svelte';
let { show }: { show: components['schemas']['Show'] } = $props();
let dialogueState = $state(false);
@@ -90,13 +86,19 @@
}
return data;
}
$effect(() => {
if (show?.id) {
getTorrents(selectedSeasonNumber).then((fetchedTorrents) => {
if (!isLoadingTorrents) {
torrents = fetchedTorrents;
} else if (fetchedTorrents.length > 0 || torrentsError) {
torrents = fetchedTorrents;
}
});
}
});
</script>
{#snippet saveDirectoryPreview(show: components['schemas']['Show'], filePathSuffix: string)}
/{getFullyQualifiedMediaName(show)} [{show.metadata_provider}id-{show.external_id}]/ Season XX/{show.name}
SXXEXX {filePathSuffix === '' ? '' : ' - ' + filePathSuffix}.mkv
{/snippet}
<Dialog.Root bind:open={dialogueState}>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}>Download Seasons</Dialog.Trigger>
<Dialog.Content class="max-h-[90vh] w-fit min-w-[80vw] overflow-y-auto">
@@ -113,122 +115,68 @@
</Tabs.List>
<Tabs.Content value="basic">
<div class="grid w-full items-center gap-1.5">
{#if show?.seasons?.length > 0}
<Label for="season-number"
>Enter a season number from 1 to {show.seasons.at(-1)?.number}</Label
<Label for="season-number">
Enter a season number from 1 to {show.seasons.at(-1)?.number}
</Label>
<div class="flex w-full max-w-sm items-center space-x-2">
<Input
type="number"
id="season-number"
bind:value={selectedSeasonNumber}
max={show.seasons.at(-1)?.number}
/>
<Button
variant="secondary"
onclick={async () => {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
try {
torrents = await getTorrents(selectedSeasonNumber, false);
} catch (error) {
console.log(error);
} finally {
isLoadingTorrents = false;
}
}}
>
<div class="flex w-full max-w-sm items-center space-x-2">
<Input
type="number"
class="max-w-sm"
id="season-number"
bind:value={selectedSeasonNumber}
max={show.seasons.at(-1)?.number}
/>
<Button
variant="secondary"
onclick={async () => {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
try {
torrents = await getTorrents(selectedSeasonNumber, false);
} catch (error) {
console.log(error);
} finally {
isLoadingTorrents = false;
}
}}
>
Search
</Button>
</div>
<p class="text-muted-foreground text-sm">
Enter the season's number you want to search for. The first, usually 1, or the last
season number usually yield the most season packs. Note that only Seasons which are
listed in the "Seasons" cell will be imported!
</p>
<Label for="file-suffix">Filepath suffix</Label>
<Select.Root type="single" bind:value={filePathSuffix}>
<Select.Trigger class="w-[180px]">{filePathSuffix}</Select.Trigger>
<Select.Content>
<Select.Item value="">None</Select.Item>
<Select.Item value="2160P">2160p</Select.Item>
<Select.Item value="1080P">1080p</Select.Item>
<Select.Item value="720P">720p</Select.Item>
<Select.Item value="480P">480p</Select.Item>
<Select.Item value="360P">360p</Select.Item>
</Select.Content>
</Select.Root>
<p class="text-muted-foreground text-sm">
This is necessary to differentiate between versions of the same season/show, for
example a 1080p and a 4K version of a season.
</p>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
>
<p class="text-muted-foreground text-sm" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
</p>
{:else}
<p class="text-muted-foreground text-sm">
No season information available for this show.
</p>
{/if}
Search
</Button>
</div>
<p class="text-muted-foreground text-sm">
Enter the season's number you want to search for. The first, usually 1, or the last
season number usually yield the most season packs. Note that only Seasons which are
listed in the "Seasons" cell will be imported!
</p>
</div>
</Tabs.Content>
<Tabs.Content value="advanced">
<div class="grid w-full items-center gap-1.5">
{#if show?.seasons?.length > 0}
<Label for="query-override">Enter a custom query</Label>
<div class="flex w-full max-w-sm items-center space-x-2">
<Input type="text" id="query-override" bind:value={queryOverride} />
<Button
variant="secondary"
onclick={async () => {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
try {
torrents = await getTorrents(selectedSeasonNumber, true);
} catch (error) {
console.log(error);
} finally {
isLoadingTorrents = false;
}
}}
>
Search
</Button>
</div>
<p class="text-muted-foreground text-sm">
The custom query will override the default search string like "The Simpsons Season 3".
Note that only Seasons which are listed in the "Seasons" cell will be imported!
</p>
<Label for="file-suffix">Filepath suffix</Label>
<Input
type="text"
class="max-w-sm"
id="file-suffix"
bind:value={filePathSuffix}
placeholder="1080P"
/>
<p class="text-muted-foreground text-sm">
This is necessary to differentiate between versions of the same season/show, for
example a 1080p and a 4K version of a season.
</p>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
<Label for="query-override">Enter a custom query</Label>
<div class="flex w-full max-w-sm items-center space-x-2">
<Input type="text" id="query-override" bind:value={queryOverride} />
<Button
variant="secondary"
onclick={async () => {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
try {
torrents = await getTorrents(selectedSeasonNumber, true);
} catch (error) {
console.log(error);
} finally {
isLoadingTorrents = false;
}
}}
>
<p class="text-muted-foreground text-sm" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
</p>
{:else}
<p class="text-muted-foreground text-sm">
No season information available for this show.
</p>
{/if}
Search
</Button>
</div>
<p class="text-muted-foreground text-sm">
The custom query will override the default search string like "The Simpsons Season 3".
Note that only Seasons which are listed in the "Seasons" cell will be imported!
</p>
</div>
</Tabs.Content>
</Tabs.Root>
@@ -287,15 +235,11 @@
{/if}
</Table.Cell>
<Table.Cell class="text-right">
<Button
size="sm"
variant="outline"
onclick={() => {
downloadTorrent(torrent.id!);
}}
>
Download
</Button>
<SelectFilePathSuffixDialog
bind:filePathSuffix
media={show}
callback={() => downloadTorrent(torrent.id!)}
/>
</Table.Cell>
</Table.Row>
{/each}

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import { saveDirectoryPreview } from '$lib/utils.js';
import type { components } from '$lib/api/api';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import { Input } from '$lib/components/ui/input';
let {
media,
filePathSuffix = $bindable()
}: {
media: components['schemas']['Movie'] | components['schemas']['Show'];
filePathSuffix: string;
} = $props();
</script>
{#snippet filePathPreview()}
<p class="text-muted-foreground text-sm">
This is necessary to differentiate between versions of the same movie or show, for example a
1080p and a 4K version.
</p>
<Label for="file-suffix-display">The files will be saved in the following directory:</Label>
<p class="text-muted-foreground text-sm" id="file-suffix-display">
{saveDirectoryPreview(media, filePathSuffix)}
</p>
{/snippet}
<Tabs.Root value="basic">
<Tabs.List>
<Tabs.Trigger value="basic">Standard Mode</Tabs.Trigger>
<Tabs.Trigger value="advanced">Advanced Mode</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="basic">
<div class="grid w-full items-center gap-1.5">
<Label for="file-suffix">Filepath suffix</Label>
<Select.Root bind:value={filePathSuffix} type="single">
<Select.Trigger class="w-[180px]">{filePathSuffix}</Select.Trigger>
<Select.Content>
<Select.Item value="">None</Select.Item>
<Select.Item value="2160P">2160p</Select.Item>
<Select.Item value="1080P">1080p</Select.Item>
<Select.Item value="720P">720p</Select.Item>
<Select.Item value="480P">480p</Select.Item>
<Select.Item value="360P">360p</Select.Item>
</Select.Content>
</Select.Root>
{@render filePathPreview()}
</div>
</Tabs.Content>
<Tabs.Content value="advanced">
<Label for="file-suffix">Filepath suffix</Label>
<Input
type="text"
class="max-w-sm"
id="file-suffix"
bind:value={filePathSuffix}
placeholder="1080P"
/>
{@render filePathPreview()}
</Tabs.Content>
</Tabs.Root>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import FilePathSuffixSelector from '$lib/components/file-path-suffix-selector.svelte';
import type { components } from '$lib/api/api';
let {
filePathSuffix = $bindable(),
media,
callback
}: {
filePathSuffix: string;
media: components['schemas']['Movie'] | components['schemas']['Show'];
callback: () => void;
} = $props();
let dialogOpen = $state(false);
function onDownload() {
callback();
dialogOpen = false;
}
</script>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger>
<Button class="w-full" onclick={() => (dialogOpen = true)}>Download</Button>
</Dialog.Trigger>
<Dialog.Content class="w-full max-w-[600px] rounded-lg p-6 shadow-lg">
<Dialog.Header>
<Dialog.Title class="mb-1 text-xl font-semibold">Set File Path Suffix</Dialog.Title>
<Dialog.Description class="mb-4 text-sm">
Set the filepath suffix for downloaded files of the torrent.
</Dialog.Description>
</Dialog.Header>
<FilePathSuffixSelector bind:filePathSuffix {media} />
<div class="mt-8 flex justify-between gap-2">
<Button onclick={() => (dialogOpen = false)} variant="secondary">Cancel</Button>
<Button onclick={() => onDownload()}>Download Torrent</Button>
</div>
</Dialog.Content>
</Dialog.Root>