mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-21 16:25:36 +02:00
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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
62
web/src/lib/components/file-path-suffix-selector.svelte
Normal file
62
web/src/lib/components/file-path-suffix-selector.svelte
Normal 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>
|
||||
41
web/src/lib/components/select-file-path-suffix-dialog.svelte
Normal file
41
web/src/lib/components/select-file-path-suffix-dialog.svelte
Normal 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>
|
||||
@@ -4,6 +4,7 @@ import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import client from '$lib/api';
|
||||
import type { components } from '$lib/api/api';
|
||||
|
||||
export const qualityMap: { [key: number]: string } = {
|
||||
1: '4K/UHD',
|
||||
@@ -97,3 +98,23 @@ export function handleQueryNotificationToast(count: number = 0, query: string =
|
||||
toast.success(`Found ${count} ${count > 1 ? 'result' : 'results'} for search term "${query}".`);
|
||||
else if (count == 0) toast.info(`No results found for "${query}".`);
|
||||
}
|
||||
|
||||
export function saveDirectoryPreview(
|
||||
media: components['schemas']['Show'] | components['schemas']['Movie'],
|
||||
filePathSuffix: string = ''
|
||||
) {
|
||||
let path =
|
||||
'/' +
|
||||
getFullyQualifiedMediaName(media) +
|
||||
' [' +
|
||||
media.metadata_provider +
|
||||
'id-' +
|
||||
media.external_id +
|
||||
']/';
|
||||
if ('seasons' in media) {
|
||||
path += ' Season XX/SXXEXX' + (filePathSuffix === '' ? '' : ' - ' + filePathSuffix) + '.mkv';
|
||||
} else {
|
||||
path += media.name + (filePathSuffix === '' ? '' : ' - ' + filePathSuffix) + '.mkv';
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user