mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-02-19 15:35:18 -05:00
Merge pull request #339 from maxdorninger/deduplicate-frontend-code
deduplicate code in the download movie/season dialogs
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { buttonVariants } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
triggerText,
|
||||
title,
|
||||
description,
|
||||
children
|
||||
}: {
|
||||
open: boolean;
|
||||
triggerText: string;
|
||||
title: string;
|
||||
description: string;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}>{triggerText}</Dialog.Trigger>
|
||||
<Dialog.Content class="max-h-[90vh] w-fit min-w-[80vw] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{description}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
{@render children()}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,19 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
|
||||
import { ArrowDown, ArrowUp, LoaderCircle } from 'lucide-svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import client from '$lib/api';
|
||||
import type { components } from '$lib/api/api';
|
||||
import SelectFilePathSuffixDialog from '$lib/components/download-dialogs/select-file-path-suffix-dialog.svelte';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import TorrentTable from '$lib/components/download-dialogs/torrent-table.svelte';
|
||||
import SearchTabs from '$lib/components/download-dialogs/search-tabs.svelte';
|
||||
import DownloadDialogWrapper from '$lib/components/download-dialogs/download-dialog-wrapper.svelte';
|
||||
|
||||
let { movie } = $props();
|
||||
let { movie }: { movie: components['schemas']['Movie'] } = $props();
|
||||
let dialogueState = $state(false);
|
||||
let torrentsError: string | null = $state(null);
|
||||
let queryOverride: string = $state('');
|
||||
@@ -23,7 +22,6 @@
|
||||
let torrentsData: any[] | null = $state(null);
|
||||
let tabState: string = $state('basic');
|
||||
let isLoading: boolean = $state(false);
|
||||
let sortBy = $state({ col: 'score', ascending: false });
|
||||
|
||||
let advancedMode: boolean = $derived(tabState === 'advanced');
|
||||
|
||||
@@ -35,32 +33,12 @@
|
||||
{ name: 'Indexer Flags', id: 'flags' }
|
||||
];
|
||||
|
||||
function getSortedColumnState(column: string | undefined): boolean | null {
|
||||
if (sortBy.col !== column) return null;
|
||||
return sortBy.ascending;
|
||||
}
|
||||
|
||||
function sortData(column?: string | undefined) {
|
||||
if (column !== undefined) {
|
||||
if (column === sortBy.col) {
|
||||
sortBy.ascending = !sortBy.ascending;
|
||||
} else {
|
||||
sortBy = { col: column, ascending: true };
|
||||
}
|
||||
}
|
||||
|
||||
let modifier = sortBy.ascending ? 1 : -1;
|
||||
torrentsData?.sort((a, b) =>
|
||||
a[sortBy.col] < b[sortBy.col] ? -1 * modifier : a[sortBy.col] > b[sortBy.col] ? modifier : 0
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadTorrent(result_id: string) {
|
||||
torrentsError = null;
|
||||
const { data, response } = await client.POST(`/api/v1/movies/{movie_id}/torrents`, {
|
||||
params: {
|
||||
path: {
|
||||
movie_id: movie.id
|
||||
movie_id: movie.id!
|
||||
},
|
||||
query: {
|
||||
public_indexer_result_id: result_id,
|
||||
@@ -96,7 +74,7 @@
|
||||
search_query_override: advancedMode ? queryOverride : undefined
|
||||
},
|
||||
path: {
|
||||
movie_id: movie.id
|
||||
movie_id: movie.id!
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -105,135 +83,49 @@
|
||||
toast.info('Searching for torrents...');
|
||||
|
||||
torrentsData = await torrentsPromise;
|
||||
sortData();
|
||||
toast.info('Found ' + torrentsData?.length + ' torrents.');
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Download a Movie</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Search and download torrents for a specific season or season packs.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Tabs.Root class="w-full" bind:value={tabState}>
|
||||
<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">
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
class="w-fit"
|
||||
onclick={() => {
|
||||
search();
|
||||
}}
|
||||
>
|
||||
Search for Torrents
|
||||
</Button>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="advanced">
|
||||
<div class="grid w-full items-center gap-1.5">
|
||||
<Label for="query-override">Enter a custom query</Label>
|
||||
<div class="flex w-full max-w-sm items-center space-x-2">
|
||||
<Input bind:value={queryOverride} id="query-override" type="text" />
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
class="w-fit"
|
||||
onclick={() => {
|
||||
search();
|
||||
}}
|
||||
>
|
||||
Search for Torrents
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
The custom query will override the default search string like "A Minecraft Movie
|
||||
(2025)".
|
||||
</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
{#if torrentsError}
|
||||
<div class="my-2 w-full text-center text-red-500">An error occurred: {torrentsError}</div>
|
||||
{/if}
|
||||
<div class="mt-4 items-center">
|
||||
{#await torrentsPromise}
|
||||
<div class="flex w-full max-w-sm items-center space-x-2">
|
||||
<LoaderCircle class="animate-spin" />
|
||||
<p>Loading torrents...</p>
|
||||
</div>
|
||||
{:then}
|
||||
<h3 class="mb-2 text-lg font-semibold">Found Torrents:</h3>
|
||||
<div class="overflow-y-auto rounded-md border p-2">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Title</Table.Head>
|
||||
{#each tableColumnHeadings as { name, id } (id)}
|
||||
<Table.Head onclick={() => sortData(id)} class="cursor-pointer">
|
||||
<div class="inline-flex items-center">
|
||||
{name}
|
||||
{#if getSortedColumnState(id) === true}
|
||||
<ArrowUp />
|
||||
{:else if getSortedColumnState(id) === false}
|
||||
<ArrowDown />
|
||||
{:else}
|
||||
<!-- Preserve layout (column width) when no sort is applied -->
|
||||
<ArrowUp class="invisible"></ArrowUp>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Head>
|
||||
{/each}
|
||||
<Table.Head class="text-right">Actions</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each torrentsData as torrent (torrent.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="max-w-[300px] font-medium">{torrent.title}</Table.Cell>
|
||||
<Table.Cell>{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB</Table.Cell>
|
||||
<Table.Cell>{torrent.seeders}</Table.Cell>
|
||||
<Table.Cell>{torrent.score}</Table.Cell>
|
||||
<Table.Cell>{torrent.indexer ?? 'Unknown'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#each torrent.flags as flag (flag)}
|
||||
<Badge variant="outline">{flag}</Badge>
|
||||
{/each}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<SelectFilePathSuffixDialog
|
||||
media={movie}
|
||||
bind:filePathSuffix
|
||||
callback={() => downloadTorrent(torrent.id!)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#if torrentsData === null}
|
||||
<Table.Cell colspan={7}>
|
||||
<div class="font-light text-center w-full">
|
||||
Start searching by clicking the search button!
|
||||
</div>
|
||||
</Table.Cell>
|
||||
{:else}
|
||||
<Table.Cell colspan={7}>
|
||||
<div class="font-light text-center w-full">No torrents found.</div>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="w-full text-center text-red-500">Failed to load torrents.</div>
|
||||
<div class="w-full text-center text-red-500">Error: {error.message}</div>
|
||||
{/await}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<DownloadDialogWrapper
|
||||
bind:open={dialogueState}
|
||||
triggerText="Download Movie"
|
||||
title="Download a Movie"
|
||||
description="Search and download torrents for a specific season or season packs."
|
||||
>
|
||||
<SearchTabs
|
||||
bind:tabState
|
||||
{isLoading}
|
||||
bind:queryOverride
|
||||
onSearch={search}
|
||||
advancedModeHelpText="The custom query will override the default search string like 'A Minecraft Movie (2025)'."
|
||||
>
|
||||
{#snippet basicModeContent()}
|
||||
<Button disabled={isLoading} class="w-fit" onclick={search}>Search for Torrents</Button>
|
||||
{/snippet}
|
||||
</SearchTabs>
|
||||
{#if torrentsError}
|
||||
<div class="my-2 w-full text-center text-red-500">An error occurred: {torrentsError}</div>
|
||||
{/if}
|
||||
<TorrentTable {torrentsPromise} columns={tableColumnHeadings}>
|
||||
{#snippet rowSnippet(torrent)}
|
||||
<Table.Cell class="font-medium">{torrent.title}</Table.Cell>
|
||||
<Table.Cell>{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB</Table.Cell>
|
||||
<Table.Cell>{torrent.seeders}</Table.Cell>
|
||||
<Table.Cell>{torrent.score}</Table.Cell>
|
||||
<Table.Cell>{torrent.indexer ?? 'Unknown'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#each torrent.flags as flag (flag)}
|
||||
<Badge variant="outline">{flag}</Badge>
|
||||
{/each}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<SelectFilePathSuffixDialog
|
||||
media={movie}
|
||||
bind:filePathSuffix
|
||||
callback={() => downloadTorrent(torrent.id)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</TorrentTable>
|
||||
</DownloadDialogWrapper>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -7,15 +7,15 @@
|
||||
convertTorrentSeasonRangeToIntegerRange,
|
||||
formatSecondsToOptimalUnit
|
||||
} from '$lib/utils.ts';
|
||||
import { ArrowDown, ArrowUp, LoaderCircle } from 'lucide-svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import client from '$lib/api';
|
||||
import type { components } from '$lib/api/api';
|
||||
import SelectFilePathSuffixDialog from '$lib/components/download-dialogs/select-file-path-suffix-dialog.svelte';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import TorrentTable from '$lib/components/download-dialogs/torrent-table.svelte';
|
||||
import SearchTabs from '$lib/components/download-dialogs/search-tabs.svelte';
|
||||
import DownloadDialogWrapper from '$lib/components/download-dialogs/download-dialog-wrapper.svelte';
|
||||
|
||||
let { show }: { show: components['schemas']['Show'] } = $props();
|
||||
let dialogueState = $state(false);
|
||||
@@ -28,7 +28,6 @@
|
||||
let torrentsData: any[] | null = $state(null);
|
||||
let tabState: string = $state('basic');
|
||||
let isLoading: boolean = $state(false);
|
||||
let sortBy = $state({ col: 'score', ascending: false });
|
||||
|
||||
let advancedMode: boolean = $derived(tabState === 'advanced');
|
||||
|
||||
@@ -43,26 +42,6 @@
|
||||
{ name: 'Seasons', id: 'season' }
|
||||
];
|
||||
|
||||
function getSortedColumnState(column: string | undefined): boolean | null {
|
||||
if (sortBy.col !== column) return null;
|
||||
return sortBy.ascending;
|
||||
}
|
||||
|
||||
function sortData(column?: string | undefined) {
|
||||
if (column !== undefined) {
|
||||
if (column === sortBy.col) {
|
||||
sortBy.ascending = !sortBy.ascending;
|
||||
} else {
|
||||
sortBy = { col: column, ascending: true };
|
||||
}
|
||||
}
|
||||
|
||||
let modifier = sortBy.ascending ? 1 : -1;
|
||||
torrentsData?.sort((a, b) =>
|
||||
a[sortBy.col] < b[sortBy.col] ? -1 * modifier : a[sortBy.col] > b[sortBy.col] ? modifier : 0
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadTorrent(result_id: string) {
|
||||
torrentsError = null;
|
||||
const { response } = await client.POST('/api/v1/tv/torrents', {
|
||||
@@ -110,166 +89,80 @@
|
||||
toast.info('Searching for torrents...');
|
||||
|
||||
torrentsData = await torrentsPromise;
|
||||
sortData();
|
||||
toast.info('Found ' + torrentsData?.length + ' torrents.');
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Download a Season</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Search and download torrents for a specific season or season packs.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Tabs.Root class="w-full" bind:value={tabState}>
|
||||
<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="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
|
||||
disabled={isLoading}
|
||||
class="w-fit"
|
||||
onclick={() => {
|
||||
search();
|
||||
}}
|
||||
>
|
||||
Search for Torrents
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
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">
|
||||
<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
|
||||
disabled={isLoading}
|
||||
class="w-fit"
|
||||
onclick={() => {
|
||||
search();
|
||||
}}
|
||||
>
|
||||
Search for Torrents
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
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>
|
||||
{#if torrentsError}
|
||||
<div class="my-2 w-full text-center text-red-500">An error occurred: {torrentsError}</div>
|
||||
{/if}
|
||||
<div class="mt-4 items-center">
|
||||
{#await torrentsPromise}
|
||||
<div class="flex w-full max-w-sm items-center space-x-2">
|
||||
<LoaderCircle class="animate-spin" />
|
||||
<p>Loading torrents...</p>
|
||||
</div>
|
||||
{:then}
|
||||
<h3 class="mb-2 text-lg font-semibold">Found Torrents:</h3>
|
||||
<div class="overflow-y-auto rounded-md border p-2">
|
||||
<Table.Root class="torrentResult">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Title</Table.Head>
|
||||
{#each tableColumnHeadings as { name, id } (id)}
|
||||
<Table.Head onclick={() => sortData(id)} class="cursor-pointer">
|
||||
<div class="inline-flex items-center">
|
||||
{name}
|
||||
{#if getSortedColumnState(id) === true}
|
||||
<ArrowUp />
|
||||
{:else if getSortedColumnState(id) === false}
|
||||
<ArrowDown />
|
||||
{:else}
|
||||
<!-- Preserve layout (column width) when no sort is applied -->
|
||||
<ArrowUp class="invisible"></ArrowUp>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Head>
|
||||
{/each}
|
||||
<Table.Head class="text-right">Actions</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each torrentsData as torrent (torrent.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="max-w-[300px] font-medium">{torrent.title}</Table.Cell>
|
||||
<Table.Cell>{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB</Table.Cell>
|
||||
<Table.Cell>{torrent.usenet}</Table.Cell>
|
||||
<Table.Cell>{torrent.usenet ? 'N/A' : torrent.seeders}</Table.Cell>
|
||||
<Table.Cell
|
||||
>{torrent.age
|
||||
? formatSecondsToOptimalUnit(torrent.age)
|
||||
: torrent.usenet
|
||||
? 'N/A'
|
||||
: ''}</Table.Cell
|
||||
>
|
||||
<Table.Cell>{torrent.score}</Table.Cell>
|
||||
<Table.Cell>{torrent.indexer ?? 'unknown'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if torrent.flags}
|
||||
{#each torrent.flags as flag (flag)}
|
||||
<Badge variant="outline">{flag}</Badge>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if torrent.season}
|
||||
{convertTorrentSeasonRangeToIntegerRange(torrent.season)}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<SelectFilePathSuffixDialog
|
||||
bind:filePathSuffix
|
||||
media={show}
|
||||
callback={() => downloadTorrent(torrent.id!)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#if torrentsData === null}
|
||||
<Table.Cell colspan={7}>
|
||||
<div class="font-light text-center w-full">
|
||||
Start searching by clicking the search button!
|
||||
</div>
|
||||
</Table.Cell>
|
||||
{:else}
|
||||
<Table.Cell colspan={7}>
|
||||
<div class="font-light text-center w-full">No torrents found.</div>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="w-full text-center text-red-500">Failed to load torrents.</div>
|
||||
<div class="w-full text-center text-red-500">Error: {error.message}</div>
|
||||
{/await}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<DownloadDialogWrapper
|
||||
bind:open={dialogueState}
|
||||
triggerText="Download Seasons"
|
||||
title="Download a Season"
|
||||
description="Search and download torrents for a specific season or season packs."
|
||||
>
|
||||
<SearchTabs
|
||||
bind:tabState
|
||||
{isLoading}
|
||||
bind:queryOverride
|
||||
onSearch={search}
|
||||
advancedModeHelpText="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!"
|
||||
>
|
||||
{#snippet basicModeContent()}
|
||||
<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 disabled={isLoading} class="w-fit" onclick={search}>Search for Torrents</Button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
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>
|
||||
{/snippet}
|
||||
</SearchTabs>
|
||||
{#if torrentsError}
|
||||
<div class="my-2 w-full text-center text-red-500">An error occurred: {torrentsError}</div>
|
||||
{/if}
|
||||
<TorrentTable {torrentsPromise} columns={tableColumnHeadings}>
|
||||
{#snippet rowSnippet(torrent)}
|
||||
<Table.Cell class="font-medium">{torrent.title}</Table.Cell>
|
||||
<Table.Cell>{(torrent.size / 1024 / 1024 / 1024).toFixed(2)}GB</Table.Cell>
|
||||
<Table.Cell>{torrent.usenet}</Table.Cell>
|
||||
<Table.Cell>{torrent.usenet ? 'N/A' : torrent.seeders}</Table.Cell>
|
||||
<Table.Cell
|
||||
>{torrent.age
|
||||
? formatSecondsToOptimalUnit(torrent.age)
|
||||
: torrent.usenet
|
||||
? 'N/A'
|
||||
: ''}</Table.Cell
|
||||
>
|
||||
<Table.Cell>{torrent.score}</Table.Cell>
|
||||
<Table.Cell>{torrent.indexer ?? 'unknown'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if torrent.flags}
|
||||
{#each torrent.flags as flag (flag)}
|
||||
<Badge variant="outline">{flag}</Badge>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if torrent.season}
|
||||
{convertTorrentSeasonRangeToIntegerRange(torrent.season)}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<SelectFilePathSuffixDialog
|
||||
bind:filePathSuffix
|
||||
media={show}
|
||||
callback={() => downloadTorrent(torrent.id)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</TorrentTable>
|
||||
</DownloadDialogWrapper>
|
||||
|
||||
47
web/src/lib/components/download-dialogs/search-tabs.svelte
Normal file
47
web/src/lib/components/download-dialogs/search-tabs.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
tabState = $bindable(),
|
||||
isLoading,
|
||||
queryOverride = $bindable(),
|
||||
onSearch,
|
||||
basicModeContent,
|
||||
advancedModeHelpText
|
||||
}: {
|
||||
tabState: string;
|
||||
isLoading: boolean;
|
||||
queryOverride: string;
|
||||
onSearch: () => void;
|
||||
basicModeContent: Snippet;
|
||||
advancedModeHelpText: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Tabs.Root class="w-full" bind:value={tabState}>
|
||||
<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">
|
||||
{@render basicModeContent()}
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="advanced">
|
||||
<div class="grid w-full items-center gap-1.5">
|
||||
<Label for="query-override">Enter a custom query</Label>
|
||||
<div class="flex w-full max-w-sm items-center space-x-2">
|
||||
<Input bind:value={queryOverride} id="query-override" type="text" />
|
||||
<Button disabled={isLoading} class="w-fit" onclick={onSearch}>Search for Torrents</Button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{advancedModeHelpText}
|
||||
</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
101
web/src/lib/components/download-dialogs/torrent-table.svelte
Normal file
101
web/src/lib/components/download-dialogs/torrent-table.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { ArrowDown, ArrowUp, LoaderCircle } from 'lucide-svelte';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
torrentsPromise,
|
||||
columns,
|
||||
rowSnippet
|
||||
}: {
|
||||
torrentsPromise: Promise<any>;
|
||||
columns: { name: string; id: string }[];
|
||||
rowSnippet: Snippet<[any]>;
|
||||
} = $props();
|
||||
|
||||
let sortBy = $state({ col: 'score', ascending: false });
|
||||
|
||||
function getSortedColumnState(column: string | undefined): boolean | null {
|
||||
if (sortBy.col !== column) return null;
|
||||
return sortBy.ascending;
|
||||
}
|
||||
|
||||
function toggleSort(column: string) {
|
||||
if (column === sortBy.col) {
|
||||
sortBy.ascending = !sortBy.ascending;
|
||||
} else {
|
||||
sortBy = { col: column, ascending: true };
|
||||
}
|
||||
}
|
||||
function sort(data: any[], column: string, ascending: boolean): any[] {
|
||||
let modifier = ascending ? 1 : -1;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[column] < b[column]) {
|
||||
return -1 * modifier;
|
||||
} else if (a[column] > b[column]) {
|
||||
return modifier;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-4 items-center">
|
||||
{#await torrentsPromise}
|
||||
<div class="flex w-full max-w-sm items-center space-x-2">
|
||||
<LoaderCircle class="animate-spin" />
|
||||
<p>Loading torrents...</p>
|
||||
</div>
|
||||
{:then data}
|
||||
<h3 class="mb-2 text-lg font-semibold">Found Torrents:</h3>
|
||||
<div class="overflow-y-auto rounded-md border p-2">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Title</Table.Head>
|
||||
{#each columns as column (column.id)}
|
||||
<Table.Head onclick={() => toggleSort(column.id)} class="cursor-pointer">
|
||||
<div class="inline-flex items-center">
|
||||
{column.name}
|
||||
{#if getSortedColumnState(column.id) === true}
|
||||
<ArrowUp />
|
||||
{:else if getSortedColumnState(column.id) === false}
|
||||
<ArrowDown />
|
||||
{:else}
|
||||
<!-- Preserve layout (column width) when no sort is applied -->
|
||||
<ArrowUp class="invisible"></ArrowUp>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Head>
|
||||
{/each}
|
||||
<Table.Head class="text-right">Actions</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if data}
|
||||
{@const sortedData = sort(data, sortBy.col, sortBy.ascending)}
|
||||
{#each sortedData as torrent (torrent.id)}
|
||||
<Table.Row>
|
||||
{@render rowSnippet(torrent)}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Cell colspan={columns.length + 2}>
|
||||
<div class="font-light text-center w-full">No torrents found.</div>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Cell colspan={columns.length + 2}>
|
||||
<div class="w-full text-center font-light">
|
||||
Start searching by clicking the search button!
|
||||
</div>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="w-full text-center text-red-500">Failed to load torrents.</div>
|
||||
<div class="w-full text-center text-red-500">Error: {error.message}</div>
|
||||
{/await}
|
||||
</div>
|
||||
Reference in New Issue
Block a user