style: apply consistent formatting and spacing across multiple files

This commit is contained in:
maxDorninger
2025-05-24 21:57:18 +02:00
parent 64eace0c74
commit ecf3fe1f45
42 changed files with 906 additions and 911 deletions

View File

@@ -7,8 +7,6 @@
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
%sveltekit.body%
</div>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,7 +1,6 @@
import {createImageOptimizer} from 'sveltekit-image-optimize';
import type {Handle} from '@sveltejs/kit';
import {createFileSystemCache} from 'sveltekit-image-optimize/cache-adapters';
import type {HandleServerError} from '@sveltejs/kit';
const cache = createFileSystemCache('./cache');
const imageHandler = createImageOptimizer({

View File

@@ -1,20 +1,13 @@
<script>
import {Button} from "$lib/components/ui/button/index.js";
import {env} from "$env/dynamic/public";
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import * as Card from '$lib/components/ui/card/index.js';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {ChevronDown, ImageOff} from 'lucide-svelte';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import {ImageOff} from 'lucide-svelte';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
let loading = $state(false)
let errorMessage = $state(null)
let loading = $state(false);
let errorMessage = $state(null);
let {result} = $props();
async function addShow() {
@@ -26,16 +19,17 @@
method: 'POST',
credentials: 'include'
});
let responseData = await response.json()
let responseData = await response.json();
console.log('Added Show: Response Data: ', responseData);
if (response.ok) {
await goto(base + '/dashboard/tv/' + responseData.id);
} else {
errorMessage = "Error occurred: " + responseData;
errorMessage = 'Error occurred: ' + responseData;
}
loading = false;
}
</script>
<Card.Root class="h-full max-w-sm">
<Card.Header>
<Card.Title>
@@ -69,4 +63,4 @@
<p class="text-sm text-red-500">{errorMessage}</p>
{/if}
</Card.Footer>
</Card.Root>
</Card.Root>

View File

@@ -3,7 +3,6 @@
import Send from '@lucide/svelte/icons/send';
import TvIcon from '@lucide/svelte/icons/tv';
import LayoutPanelLeft from '@lucide/svelte/icons/layout-panel-left';
import DownloadIcon from '@lucide/svelte/icons/download';
const data = {
navMain: [
@@ -18,16 +17,15 @@
url: '/dashboard/tv/add-show'
},
{
title: 'Torrents',
url: '/dashboard/tv/torrents'
},
{
title: 'Torrents',
url: '/dashboard/tv/torrents'
},
{
title: 'Requests',
url: '/dashboard/tv/requests'
}
]
},
]
}
],
navSecondary: [
{
@@ -57,10 +55,9 @@
import NavSecondary from '$lib/components/nav-secondary.svelte';
import NavUser from '$lib/components/nav-user.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import Command from '@lucide/svelte/icons/command';
import type {ComponentProps} from 'svelte';
import logo from '$lib/images/logo.svg';
import {base} from "$app/paths";
import {base} from '$app/paths';
let {ref = $bindable(null), ...restProps}: ComponentProps<typeof Sidebar.Root> = $props();
</script>

View File

@@ -4,8 +4,9 @@
let {state} = $props();
</script>
{#if state}
<Check class="stroke-green-500"/>
{:else}
<X class="stroke-rose-600"/>
{/if}
{/if}

View File

@@ -1,318 +1,309 @@
<script lang="ts">
import {env} from '$env/dynamic/public';
import {Button, buttonVariants} from '$lib/components/ui/button/index.js';
import {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {toast} from 'svelte-sonner';
import {env} from '$env/dynamic/public';
import {Button, buttonVariants} from '$lib/components/ui/button/index.js';
import {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {toast} from 'svelte-sonner';
import type {PublicIndexerQueryResult} from '$lib/types.js';
import {convertTorrentSeasonRangeToIntegerRange, getFullyQualifiedShowName} 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 type {PublicIndexerQueryResult} from '$lib/types.js';
import {convertTorrentSeasonRangeToIntegerRange, getFullyQualifiedShowName} 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';
let {show} = $props();
let dialogueState = $state(false);
let selectedSeasonNumber: number = $state(1);
let torrents: PublicIndexerQueryResult[] = $state([]);
let isLoadingTorrents: boolean = $state(false);
let torrentsError: string | null = $state(null);
let queryOverride: string = $state('');
let filePathSuffix: string = $state('');
let {show} = $props();
let dialogueState = $state(false);
let selectedSeasonNumber: number = $state(1);
let torrents: PublicIndexerQueryResult[] = $state([]);
let isLoadingTorrents: boolean = $state(false);
let torrentsError: string | null = $state(null);
let queryOverride: string = $state('');
let filePathSuffix: string = $state('');
async function downloadTorrent(result_id: string) {
let url = new URL(env.PUBLIC_API_URL + '/tv/torrents');
url.searchParams.append('public_indexer_result_id', result_id);
url.searchParams.append('show_id', show.id);
if (filePathSuffix !== '') {
url.searchParams.append('file_path_suffix', filePathSuffix);
}
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
async function downloadTorrent(result_id: string) {
let url = new URL(env.PUBLIC_API_URL + '/tv/torrents');
url.searchParams.append('public_indexer_result_id', result_id);
url.searchParams.append('show_id', show.id);
if (filePathSuffix !== '') {
url.searchParams.append('file_path_suffix', filePathSuffix);
}
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!response.ok) {
const errorMessage = `Failed to download torrent for show ${show.id} and season ${selectedSeasonNumber}: ${response.statusText}`;
console.error(errorMessage);
torrentsError = errorMessage;
toast.error(errorMessage);
return false;
}
if (!response.ok) {
const errorMessage = `Failed to download torrent for show ${show.id} and season ${selectedSeasonNumber}: ${response.statusText}`;
console.error(errorMessage);
torrentsError = errorMessage;
toast.error(errorMessage);
return false;
}
const data: PublicIndexerQueryResult[] = await response.json();
console.log('Downloading torrent:', data);
toast.success('Torrent download started successfully!');
const data: PublicIndexerQueryResult[] = await response.json();
console.log('Downloading torrent:', data);
toast.success('Torrent download started successfully!');
return true;
} catch (err) {
const errorMessage = `Error downloading torrent: ${err instanceof Error ? err.message : 'An unknown error occurred'}`;
console.error(errorMessage);
toast.error(errorMessage);
return false;
}
}
return true;
} catch (err) {
const errorMessage = `Error downloading torrent: ${err instanceof Error ? err.message : 'An unknown error occurred'}`;
console.error(errorMessage);
toast.error(errorMessage);
return false;
}
}
async function getTorrents(
season_number: number,
override: boolean = false
): Promise<PublicIndexerQueryResult[]> {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
async function getTorrents(
season_number: number,
override: boolean = false
): Promise<PublicIndexerQueryResult[]> {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
let url = new URL(env.PUBLIC_API_URL + '/tv/torrents');
url.searchParams.append('show_id', show.id);
if (override) {
url.searchParams.append('search_query_override', queryOverride);
} else {
url.searchParams.append('season_number', season_number.toString());
}
let url = new URL(env.PUBLIC_API_URL + '/tv/torrents');
url.searchParams.append('show_id', show.id);
if (override) {
url.searchParams.append('search_query_override', queryOverride);
} else {
url.searchParams.append('season_number', season_number.toString());
}
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!response.ok) {
const errorMessage = `Failed to fetch torrents for show ${show.id} and season ${selectedSeasonNumber}: ${response.statusText}`;
console.error(errorMessage);
torrentsError = errorMessage;
if (dialogueState)
toast.error(errorMessage);
return [];
}
if (!response.ok) {
const errorMessage = `Failed to fetch torrents for show ${show.id} and season ${selectedSeasonNumber}: ${response.statusText}`;
console.error(errorMessage);
torrentsError = errorMessage;
if (dialogueState) toast.error(errorMessage);
return [];
}
const data: PublicIndexerQueryResult[] = await response.json();
console.log('Fetched torrents:', data);
if (dialogueState) {
if (data.length > 0) {
toast.success(`Found ${data.length} torrents.`);
} else {
toast.info('No torrents found for your query.');
}
}
return data;
} catch (err) {
const errorMessage = `Error fetching torrents: ${err instanceof Error ? err.message : 'An unknown error occurred'}`;
console.error(errorMessage);
torrentsError = errorMessage;
if (dialogueState)
toast.error(errorMessage);
return [];
} finally {
isLoadingTorrents = false;
}
}
const data: PublicIndexerQueryResult[] = await response.json();
console.log('Fetched torrents:', data);
if (dialogueState) {
if (data.length > 0) {
toast.success(`Found ${data.length} torrents.`);
} else {
toast.info('No torrents found for your query.');
}
}
return data;
} catch (err) {
const errorMessage = `Error fetching torrents: ${err instanceof Error ? err.message : 'An unknown error occurred'}`;
console.error(errorMessage);
torrentsError = errorMessage;
if (dialogueState) toast.error(errorMessage);
return [];
} finally {
isLoadingTorrents = false;
}
}
$effect(() => {
if (show?.id) {
console.log('selectedSeasonNumber changed:', selectedSeasonNumber);
getTorrents(selectedSeasonNumber).then((fetchedTorrents) => {
if (!isLoadingTorrents) {
torrents = fetchedTorrents;
} else if (fetchedTorrents.length > 0 || torrentsError) {
torrents = fetchedTorrents;
}
});
}
});
$effect(() => {
if (show?.id) {
console.log('selectedSeasonNumber changed:', selectedSeasonNumber);
getTorrents(selectedSeasonNumber).then((fetchedTorrents) => {
if (!isLoadingTorrents) {
torrents = fetchedTorrents;
} else if (fetchedTorrents.length > 0 || torrentsError) {
torrents = fetchedTorrents;
}
});
}
});
</script>
{#snippet saveDirectoryPreview(show, filePathSuffix)}
/{getFullyQualifiedShowName(show)} [{show.metadata_provider}id-{show.external_id}]/
Season XX/{show.name} SXXEXX {filePathSuffix === ''
? ''
: ' - ' + filePathSuffix}.mkv
/{getFullyQualifiedShowName(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">
<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" 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">
{#if show?.seasons?.length > 0}
<Label for="season-number"
>Enter a season number from 1 to {show.seasons.at(-1).number}</Label
>
<Input
type="number"
class="max-w-sm"
id="season-number"
bind:value={selectedSeasonNumber}
max={show.seasons.at(-1).number}
/>
<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>
<Label for="file-suffix">Filepath suffix</Label>
<Select.Root type="single" bind:value={filePathSuffix} id="file-suffix">
<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-sm text-muted-foreground">
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-sm text-muted-foreground" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
</p>
{:else}
<p class="text-sm text-muted-foreground">
No season information available for this show.
</p>
{/if}
</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-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>
<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-sm text-muted-foreground">
This is necessary to differentiate between versions of the same season/show, for
example a 1080p and a 4K version of a season.
</p>
<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" 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">
{#if show?.seasons?.length > 0}
<Label for="season-number"
>Enter a season number from 1 to {show.seasons.at(-1).number}</Label
>
<Input
type="number"
class="max-w-sm"
id="season-number"
bind:value={selectedSeasonNumber}
max={show.seasons.at(-1).number}
/>
<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>
<Label for="file-suffix">Filepath suffix</Label>
<Select.Root type="single" bind:value={filePathSuffix} id="file-suffix">
<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-sm text-muted-foreground">
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-sm text-muted-foreground" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
</p>
{:else}
<p class="text-sm text-muted-foreground">
No season information available for this show.
</p>
{/if}
</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-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>
<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-sm text-muted-foreground">
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-sm text-muted-foreground" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
</p>
{:else}
<p class="text-sm text-muted-foreground">
No season information available for this show.
</p>
{/if}
</div>
</Tabs.Content>
</Tabs.Root>
<div class="mt-4 items-center">
{#if isLoadingTorrents}
<div class="flex w-full max-w-sm items-center space-x-2">
<LoaderCircle class="animate-spin"/>
<p>Loading torrents...</p>
</div>
{:else if torrentsError}
<p class="text-red-500">Error: {torrentsError}</p>
{:else if torrents.length > 0}
<h3 class="mb-2 text-lg font-semibold">Found Torrents:</h3>
<div class="max-h-[200px] overflow-y-auto rounded-md border p-2">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Title</Table.Head>
<Table.Head>Size</Table.Head>
<Table.Head>Seeders</Table.Head>
<Table.Head>Indexer Flags</Table.Head>
<Table.Head>Seasons</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each torrents 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>
{#each torrent.flags as flag}
{flag},&nbsp;
{/each}
</Table.Cell>
<Table.Cell>
{torrent.seasons}
{convertTorrentSeasonRangeToIntegerRange(torrent)}
</Table.Cell>
<Table.Cell class="text-right">
<Button
size="sm"
variant="outline"
onclick={() => {
downloadTorrent(torrent.id);
}}
>
Download
</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{:else if show?.seasons?.length > 0}
<p>No torrents found for season {selectedSeasonNumber}. Try a different season.</p>
{/if}
</div>
</Dialog.Content>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
>
<p class="text-sm text-muted-foreground" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
</p>
{:else}
<p class="text-sm text-muted-foreground">
No season information available for this show.
</p>
{/if}
</div>
</Tabs.Content>
</Tabs.Root>
<div class="mt-4 items-center">
{#if isLoadingTorrents}
<div class="flex w-full max-w-sm items-center space-x-2">
<LoaderCircle class="animate-spin"/>
<p>Loading torrents...</p>
</div>
{:else if torrentsError}
<p class="text-red-500">Error: {torrentsError}</p>
{:else if torrents.length > 0}
<h3 class="mb-2 text-lg font-semibold">Found Torrents:</h3>
<div class="max-h-[200px] overflow-y-auto rounded-md border p-2">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Title</Table.Head>
<Table.Head>Size</Table.Head>
<Table.Head>Seeders</Table.Head>
<Table.Head>Indexer Flags</Table.Head>
<Table.Head>Seasons</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each torrents 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>
{#each torrent.flags as flag}
{flag},&nbsp;
{/each}
</Table.Cell>
<Table.Cell>
{torrent.seasons}
{convertTorrentSeasonRangeToIntegerRange(torrent)}
</Table.Cell>
<Table.Cell class="text-right">
<Button
size="sm"
variant="outline"
onclick={() => {
downloadTorrent(torrent.id);
}}
>
Download
</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{:else if show?.seasons?.length > 0}
<p>No torrents found for season {selectedSeasonNumber}. Try a different season.</p>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,208 +1,222 @@
<script lang="ts">
import {Button} from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import {Input} from '$lib/components/ui/input/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import {goto} from '$app/navigation';
import {env} from '$env/dynamic/public';
import * as Tabs from "$lib/components/ui/tabs/index.js";
import {toast} from 'svelte-sonner';
import {Button} from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import {Input} from '$lib/components/ui/input/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import {goto} from '$app/navigation';
import {env} from '$env/dynamic/public';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import {toast} from 'svelte-sonner';
let apiUrl = env.PUBLIC_API_URL;
let apiUrl = env.PUBLIC_API_URL;
let email = '';
let password = '';
let errorMessage = '';
let isLoading = false;
let tabValue = "login";
let email = $state('');
let password = $state('');
let errorMessage = $state('');
let isLoading = $state(false);
let tabValue = $state('login');
async function handleLogin(event: Event) {
event.preventDefault();
async function handleLogin(event: Event) {
event.preventDefault();
isLoading = true;
errorMessage = '';
isLoading = true;
errorMessage = '';
const formData = new URLSearchParams();
formData.append('username', email);
formData.append('password', password);
try {
const response = await fetch(apiUrl + '/auth/cookie/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString(),
credentials: 'include'
});
const formData = new URLSearchParams();
formData.append('username', email);
formData.append('password', password);
try {
const response = await fetch(apiUrl + '/auth/cookie/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString(),
credentials: 'include'
});
if (response.ok) {
console.log('Login successful!');
console.log('Received User Data: ', response);
errorMessage = 'Login successful! Redirecting...';
toast.success(errorMessage);
goto('/dashboard');
} else {
let errorText = await response.text();
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.message || 'Login failed. Please check your credentials.';
} catch {
errorMessage = errorText || 'Login failed. Please check your credentials.';
}
toast.error(errorMessage);
console.error('Login failed:', response.status, errorText);
}
} catch (error) {
console.error('Login request failed:', error);
errorMessage = 'An error occurred during the login request.';
toast.error(errorMessage);
} finally {
isLoading = false;
}
}
if (response.ok) {
console.log('Login successful!');
console.log('Received User Data: ', response);
errorMessage = 'Login successful! Redirecting...';
toast.success(errorMessage);
goto('/dashboard');
} else {
let errorText = await response.text();
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.message || 'Login failed. Please check your credentials.';
} catch {
errorMessage = errorText || 'Login failed. Please check your credentials.';
}
toast.error(errorMessage);
console.error('Login failed:', response.status, errorText);
}
} catch (error) {
console.error('Login request failed:', error);
errorMessage = 'An error occurred during the login request.';
toast.error(errorMessage);
} finally {
isLoading = false;
}
}
async function handleSignup(event: Event) {
event.preventDefault();
async function handleSignup(event: Event) {
event.preventDefault();
isLoading = true;
errorMessage = '';
isLoading = true;
errorMessage = '';
try {
const response = await fetch(apiUrl + '/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password
try {
const response = await fetch(apiUrl + '/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password
}),
credentials: 'include'
});
}),
credentials: 'include'
});
if (response.ok) {
console.log('Registration successful!');
console.log('Received User Data: ', response);
tabValue = "login"; // Switch to login tab after successful registration
errorMessage = 'Registration successful! Please login.';
toast.success(errorMessage);
} else {
let errorText = await response.text();
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.message || 'Registration failed. Please check your credentials.';
} catch {
errorMessage = errorText || 'Registration failed. Please check your credentials.';
}
toast.error(errorMessage);
console.error('Registration failed:', response.status, errorText);
}
} catch (error) {
console.error('Registration request failed:', error);
errorMessage = 'An error occurred during the Registration request.';
toast.error(errorMessage);
} finally {
isLoading = false;
}
}
if (response.ok) {
console.log('Registration successful!');
console.log('Received User Data: ', response);
tabValue = 'login'; // Switch to login tab after successful registration
errorMessage = 'Registration successful! Please login.';
toast.success(errorMessage);
} else {
let errorText = await response.text();
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.message || 'Registration failed. Please check your credentials.';
} catch {
errorMessage = errorText || 'Registration failed. Please check your credentials.';
}
toast.error(errorMessage);
console.error('Registration failed:', response.status, errorText);
}
} catch (error) {
console.error('Registration request failed:', error);
errorMessage = 'An error occurred during the Registration request.';
toast.error(errorMessage);
} finally {
isLoading = false;
}
}
</script>
{#snippet tabSwitcher()}
<!-- <Tabs.List>-->
<!-- <Tabs.Trigger value="login">Login</Tabs.Trigger>-->
<!-- <Tabs.Trigger value="register">Sign up</Tabs.Trigger>-->
<!-- </Tabs.List>-->
<!-- <Tabs.List>-->
<!-- <Tabs.Trigger value="login">Login</Tabs.Trigger>-->
<!-- <Tabs.Trigger value="register">Sign up</Tabs.Trigger>-->
<!-- </Tabs.List>-->
{/snippet}
<Tabs.Root class="w-[400px]" value={tabValue}>
<Tabs.Content value="login">
<Card.Root class="mx-auto max-w-sm">
<Card.Header>
{@render tabSwitcher()}
<Tabs.Content value="login">
<Card.Root class="mx-auto max-w-sm">
<Card.Header>
{@render tabSwitcher()}
<Card.Title class="text-2xl">Login</Card.Title>
<Card.Description>Enter your email below to login to your account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleLogin}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input bind:value={email} id="email" placeholder="m@example.com" required type="email"/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<!-- TODO: add link to relevant documentation -->
<a class="ml-auto inline-block text-sm underline" href="##"> Forgot your password? </a>
</div>
<Input bind:value={password} id="password" required type="password"/>
</div>
<Card.Title class="text-2xl">Login</Card.Title>
<Card.Description>Enter your email below to login to your account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleLogin}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
bind:value={email}
id="email"
placeholder="m@example.com"
required
type="email"
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<!-- TODO: add link to relevant documentation -->
<a class="ml-auto inline-block text-sm underline" href="##">
Forgot your password?
</a>
</div>
<Input bind:value={password} id="password" required type="password"/>
</div>
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{/if}
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{/if}
<Button class="w-full" disabled={isLoading} type="submit">
{#if isLoading}
Logging in...
{:else}
Login
{/if}
</Button>
</form>
<Button class="w-full" disabled={isLoading} type="submit">
{#if isLoading}
Logging in...
{:else}
Login
{/if}
</Button>
</form>
<Button class="mt-2 w-full" variant="outline">Login with Google</Button>
<Button class="mt-2 w-full" variant="outline">Login with Google</Button>
<div class="mt-4 text-center text-sm">
Don't have an account?
<span class="underline" onclick={tabValue="register"}> Sign up </span>
</div>
</Card.Content>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="register">
<Card.Root class="mx-auto max-w-sm">
<div class="mt-4 text-center text-sm">
<Button onclick={() => (tabValue = 'register')} variant="link">
Don't have an account? Sign up
</Button
>
</div>
</Card.Content>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="register">
<Card.Root class="mx-auto max-w-sm">
<Card.Header>
{@render tabSwitcher()}
<Card.Title class="text-2xl">Sign up</Card.Title>
<Card.Description>Enter your email and password below to sign up.</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleSignup}>
<div class="grid gap-2">
<Label for="email2">Email</Label>
<Input
bind:value={email}
id="email2"
placeholder="m@example.com"
required
type="email"
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password2">Password</Label>
</div>
<Input bind:value={password} id="password2" required type="password"/>
</div>
<Card.Header>
{@render tabSwitcher()}
<Card.Title class="text-2xl">Sign up</Card.Title>
<Card.Description>Enter your email and password below to sign up.</Card.Description>
</Card.Header>
<Card.Content>
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{/if}
<form class="grid gap-4" onsubmit={handleSignup}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input bind:value={email} id="email" placeholder="m@example.com" required type="email"/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
</div>
<Input bind:value={password} id="password" required type="password"/>
</div>
<Button class="w-full" disabled={isLoading} type="submit">
{#if isLoading}
Signing up...
{:else}
Sign up
{/if}
</Button>
</form>
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{/if}
<Button class="mt-2 w-full" variant="outline">Login with Google</Button>
<Button class="w-full" disabled={isLoading} type="submit">
{#if isLoading}
Signing up...
{:else}
Sign up
{/if}
</Button>
</form>
<Button class="mt-2 w-full" variant="outline">Login with Google</Button>
<div class="mt-4 text-center text-sm">
Already have an account?
<span class="underline" onclick={tabValue="login"}>Login </span>
</div>
</Card.Content>
</Card.Root>
</Tabs.Content>
<div class="mt-4 text-center text-sm">
<Button onclick={() => (tabValue = 'login')} variant="link"
>Already have an account? Login
</Button
>
</div>
</Card.Content>
</Card.Root>
</Tabs.Content>
</Tabs.Root>

View File

@@ -5,6 +5,6 @@
</script>
<div {...props} class="flex items-center">
<img alt="Logo" class="h-12 w-12 mr-2" src={logo}/>
<img alt="Logo" class="mr-2 h-12 w-12" src={logo}/>
<span class="text-3xl font-bold">Media Manager</span>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import type {ComponentProps} from 'svelte';
import Sun from "@lucide/svelte/icons/sun";
import Moon from "@lucide/svelte/icons/moon";
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import {toggleMode} from "mode-watcher";
import {toggleMode} from 'mode-watcher';
let {
ref = $bindable(null),
@@ -27,10 +27,9 @@
<Sidebar.MenuItem>
<Sidebar.MenuButton size="sm">
{#snippet child({props})}
<div onclick={()=>toggleMode()} {...props}>
<div onclick={() => toggleMode()} {...props}>
<Sun class="dark:hidden "/>
<span class="dark:hidden ">Switch to dark mode</span>
<span class="dark:hidden">Switch to dark mode</span>
<Moon class="hidden dark:inline"/>
<span class="hidden dark:inline">Switch to light mode</span>
</div>

View File

@@ -10,17 +10,10 @@
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import {useSidebar} from '$lib/components/ui/sidebar/index.js';
import {getContext} from 'svelte';
import UserDetails from './user-details.svelte';
import type {User} from '$lib/types';
import UserRound from '@lucide/svelte/icons/user-round';
import {base} from '$app/paths';
import {env} from "$env/dynamic/public";
import {goto} from '$app/navigation';
import {handleLogout} from '$lib/utils.ts';
const user: () => User = getContext('user');
const sidebar = useSidebar();
</script>
<Sidebar.Menu>

View File

@@ -5,7 +5,7 @@
import {Label} from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select/index.js';
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
import type {PublicShow, Quality, CreateSeasonRequest} from '$lib/types.js';
import type {CreateSeasonRequest, PublicShow, Quality} from '$lib/types.js';
import {getFullyQualifiedShowName, getTorrentQualityString} from '$lib/utils.js';
import {toast} from 'svelte-sonner';
@@ -20,19 +20,21 @@
const qualityValues: Quality[] = [1, 2, 3, 4];
let qualityOptions = $derived(
qualityValues.map(q => ({value: q, label: getTorrentQualityString(q)}))
qualityValues.map((q) => ({value: q, label: getTorrentQualityString(q)}))
);
let isFormInvalid = $derived(
!selectedSeasonsIds || selectedSeasonsIds.length === 0 ||
!minQuality || !wantedQuality ||
(wantedQuality > minQuality)
!selectedSeasonsIds ||
selectedSeasonsIds.length === 0 ||
!minQuality ||
!wantedQuality ||
wantedQuality > minQuality
);
async function handleRequestSeason() {
isSubmittingRequest = true;
submitRequestError = null;
const payloads: CreateSeasonRequest = selectedSeasonsIds.map(seasonId => ({
const payloads: CreateSeasonRequest = selectedSeasonsIds.map((seasonId) => ({
season_id: seasonId,
min_quality: minQuality,
wanted_quality: wantedQuality
@@ -42,13 +44,14 @@
const response = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (response.status === 204) { // Success, no content
if (response.status === 204) {
// Success, no content
dialogOpen = false; // Close the dialog
// Reset form fields
selectedSeasonsIds = undefined;
@@ -70,15 +73,14 @@
}
}
}
</script>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger
class={buttonVariants({ variant: 'default' })}
on:click={() => {
dialogOpen = true;
}}
dialogOpen = true;
}}
>
Request Season
</Dialog.Trigger>
@@ -96,8 +98,8 @@
<Select.Root bind:value={selectedSeasonsIds}>
<Select.Trigger class="w-full" id="season">
{#each selectedSeasonsIds as seasonId (seasonId)}
{#if show.seasons.find(season => season.id === seasonId)}
{show.seasons.find(season => season.id === seasonId).number},&nbsp;
{#if show.seasons.find((season) => season.id === seasonId)}
{show.seasons.find((season) => season.id === seasonId).number},&nbsp;
{/if}
{:else}
Select one or more seasons
@@ -118,7 +120,7 @@
<Label class="text-right" for="min-quality">Min Quality</Label>
<Select.Root bind:value={minQuality} type="single">
<Select.Trigger class="w-full" id="min-quality">
{minQuality ? getTorrentQualityString(minQuality) : "Select Minimum Quality"}
{minQuality ? getTorrentQualityString(minQuality) : 'Select Minimum Quality'}
</Select.Trigger>
<Select.Content>
{#each qualityOptions as option (option.value)}
@@ -133,7 +135,7 @@
<Label class="text-right" for="wanted-quality">Wanted Quality</Label>
<Select.Root bind:value={wantedQuality} type="single">
<Select.Trigger class="w-full" id="wanted-quality">
{wantedQuality ? getTorrentQualityString(wantedQuality) : "Select Wanted Quality"}
{wantedQuality ? getTorrentQualityString(wantedQuality) : 'Select Wanted Quality'}
</Select.Trigger>
<Select.Content>
{#each qualityOptions as option (option.value)}
@@ -144,15 +146,15 @@
</div>
{#if submitRequestError}
<p class="col-span-full text-sm text-red-500 text-center">{submitRequestError}</p>
<p class="col-span-full text-center text-sm text-red-500">{submitRequestError}</p>
{/if}
</div>
<Dialog.Footer>
<Button disabled={isSubmittingRequest} onclick={() => dialogOpen = false} variant="outline">Cancel</Button>
<Button
disabled={isFormInvalid || isSubmittingRequest}
onclick={handleRequestSeason}
<Button disabled={isSubmittingRequest} onclick={() => (dialogOpen = false)} variant="outline"
>Cancel
</Button
>
<Button disabled={isFormInvalid || isSubmittingRequest} onclick={handleRequestSeason}>
{#if isSubmittingRequest}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin"/>
Submitting...
@@ -163,4 +165,3 @@
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,41 +1,43 @@
<script lang="ts">
import {
getFullyQualifiedShowName,
getTorrentQualityString,
} from "$lib/utils.js";
import type {SeasonRequest, User} from "$lib/types.js";
import CheckmarkX from "$lib/components/checkmark-x.svelte";
import * as Table from "$lib/components/ui/table/index.js";
import {getContext} from "svelte";
import {Button} from "$lib/components/ui/button/index.js";
import {getFullyQualifiedShowName, getTorrentQualityString} from '$lib/utils.js';
import type {SeasonRequest, User} from '$lib/types.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import * as Table from '$lib/components/ui/table/index.js';
import {getContext} from 'svelte';
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import {toast} from "svelte-sonner";
import {toast} from 'svelte-sonner';
let {
requests, filter = () => {
return true
requests,
filter = () => {
return true;
}
}: { requests: SeasonRequest[], filter: (request: SeasonRequest) => boolean } = $props();
const user: () => User = getContext("user");
}: { requests: SeasonRequest[]; filter: (request: SeasonRequest) => boolean } = $props();
const user: () => User = getContext('user');
async function approveRequest(requestId: string, currentAuthorizedStatus: boolean) {
try {
const response = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests/${requestId}?authorized_status=${!currentAuthorizedStatus}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include'
});
const response = await fetch(
`${env.PUBLIC_API_URL}/tv/seasons/requests/${requestId}?authorized_status=${!currentAuthorizedStatus}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
}
);
if (response.ok) {
const requestIndex = requests.findIndex(r => r.id === requestId);
const requestIndex = requests.findIndex((r) => r.id === requestId);
if (requestIndex !== -1) {
requests[requestIndex].authorized = !currentAuthorizedStatus;
requests[requestIndex].authorized_by = user();
}
toast.success(`Request ${!currentAuthorizedStatus ? 'approved' : 'unapproved'} successfully.`);
toast.success(
`Request ${!currentAuthorizedStatus ? 'approved' : 'unapproved'} successfully.`
);
} else {
const errorText = await response.text();
console.error(`Failed to update request status ${response.statusText}`, errorText);
@@ -43,7 +45,9 @@
}
} catch (error) {
console.error('Error updating request status:', error);
toast.error('Error updating request status: ' + (error instanceof Error ? error.message : String(error)));
toast.error(
'Error updating request status: ' + (error instanceof Error ? error.message : String(error))
);
}
}
@@ -52,13 +56,13 @@
const response = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests/${requestId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (response.ok) {
const index = requests.findIndex(r => r.id === requestId);
const index = requests.findIndex((r) => r.id === requestId);
if (index > -1) {
requests.splice(index, 1); // Remove the request from the list
}
@@ -69,10 +73,11 @@
}
} catch (error) {
console.error('Error deleting request:', error);
toast.error('Error deleting request: ' + (error instanceof Error ? error.message : String(error)));
toast.error(
'Error deleting request: ' + (error instanceof Error ? error.message : String(error))
);
}
}
</script>
<Table.Root>
@@ -116,13 +121,17 @@
</Table.Cell>
<Table.Cell class="space-x-1">
{#if user().is_superuser}
<Button class="mb-1" size="sm"
onclick={() => approveRequest(request.id, request.authorized)}>
<Button
class="mb-1"
size="sm"
onclick={() => approveRequest(request.id, request.authorized)}
>
{request.authorized ? 'Unapprove' : 'Approve'}
</Button>
{/if}
{#if user().is_superuser || user().id === request.requested_by?.id}
<Button variant="destructive" size="sm" onclick={() => deleteRequest(request.id)}>Delete
<Button variant="destructive" size="sm" onclick={() => deleteRequest(request.id)}
>Delete
</Button>
{/if}
</Table.Cell>

View File

@@ -3,12 +3,11 @@
convertTorrentSeasonRangeToIntegerRange,
getTorrentQualityString,
getTorrentStatusString
} from "$lib/utils.js";
import CheckmarkX from "$lib/components/checkmark-x.svelte";
import * as Table from "$lib/components/ui/table/index.js";
} from '$lib/utils.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import * as Table from '$lib/components/ui/table/index.js';
let {torrents} = $props();
</script>
<Table.Root>
@@ -59,4 +58,4 @@
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Table.Root>

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import Ellipsis from '@lucide/svelte/icons/ellipsis';
import type {WithElementRef, WithoutChildren} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
import {cn} from '$lib/utils.js';
import type {WithElementRef, WithoutChildren} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -12,11 +12,11 @@
</script>
<span
{...restProps}
aria-hidden="true"
bind:this={ref}
class={cn('flex size-9 items-center justify-center', className)}
role="presentation"
{...restProps}
aria-hidden="true"
bind:this={ref}
class={cn('flex size-9 items-center justify-center', className)}
role="presentation"
>
<Ellipsis class="size-4"/>
<span class="sr-only">More</span>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type {WithElementRef} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
import {cn} from '$lib/utils.js';
import type {WithElementRef} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -12,12 +12,12 @@
</script>
<span
{...restProps}
aria-current="page"
aria-disabled="true"
bind:this={ref}
class={cn('font-normal text-foreground', className)}
role="link"
{...restProps}
aria-current="page"
aria-disabled="true"
bind:this={ref}
class={cn('font-normal text-foreground', className)}
role="link"
>
{@render children?.()}
</span>

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
import {DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
import Check from '@lucide/svelte/icons/check';
import Minus from '@lucide/svelte/icons/minus';
import {cn} from '$lib/utils.js';
import type {Snippet} from 'svelte';
import {cn} from '$lib/utils.js';
import type {Snippet} from 'svelte';
let {
ref = $bindable(null),
@@ -18,16 +18,16 @@
</script>
<DropdownMenuPrimitive.CheckboxItem
{...restProps}
bind:checked
bind:indeterminate
bind:ref
class={cn(
{...restProps}
bind:checked
bind:indeterminate
bind:ref
class={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
className
)}
>
{#snippet children({checked, indeterminate})}
{#snippet children({checked, indeterminate})}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4"/>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type {HTMLAttributes} from 'svelte/elements';
import {type WithElementRef} from 'bits-ui';
import {cn} from '$lib/utils.js';
import type {HTMLAttributes} from 'svelte/elements';
import {type WithElementRef} from 'bits-ui';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -12,9 +12,9 @@
</script>
<span
{...restProps}
bind:this={ref}
class={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...restProps}
bind:this={ref}
class={cn('ml-auto text-xs tracking-widest opacity-60', className)}
>
{@render children?.()}
</span>

View File

@@ -1 +1 @@
export {default as Toaster} from "./sonner.svelte";
export {default as Toaster} from './sonner.svelte';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {Toaster as Sonner, type ToasterProps as SonnerProps} from "svelte-sonner";
import {mode} from "mode-watcher";
import {Toaster as Sonner, type ToasterProps as SonnerProps} from 'svelte-sonner';
import {mode} from 'mode-watcher';
let {...restProps}: SonnerProps = $props();
</script>
@@ -11,10 +11,11 @@
theme={mode.current}
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
}
}}
/>

View File

@@ -188,4 +188,4 @@ export interface SeasonRequest extends SeasonRequestBase {
authorized: boolean;
authorized_by?: User;
show: Show;
}
}

View File

@@ -1,6 +1,6 @@
import {type ClassValue, clsx} from 'clsx';
import {twMerge} from 'tailwind-merge';
import {env} from "$env/dynamic/public";
import {env} from '$env/dynamic/public';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {toast} from 'svelte-sonner';
@@ -40,16 +40,20 @@ export function getFullyQualifiedShowName(show: { name: string; year: number }):
return name;
}
export function convertTorrentSeasonRangeToIntegerRange(torrent: any): string {
export function convertTorrentSeasonRangeToIntegerRange(torrent: {
season?: number[];
seasons?: number[];
}): string {
if (torrent?.season?.length === 1) return torrent.season[0]?.toString();
if (torrent?.season?.length >= 2) return torrent.season[0]?.toString() + "-" + torrent.season.at(-1).toString();
if (torrent?.season?.length >= 2)
return torrent.season[0]?.toString() + '-' + torrent.season.at(-1).toString();
if (torrent?.seasons?.length === 1) return torrent.seasons[0]?.toString();
if (torrent?.seasons?.length >= 2) return torrent.seasons[0]?.toString() + "-" + torrent.seasons.at(-1).toString();
if (torrent?.seasons?.length >= 2)
return torrent.seasons[0]?.toString() + '-' + torrent.seasons.at(-1).toString();
else {
console.log("Error parsing season range: " + torrent?.seasons + torrent?.season);
return "Error parsing season range: " + torrent?.seasons + torrent?.season;
console.log('Error parsing season range: ' + torrent?.seasons + torrent?.season);
return 'Error parsing season range: ' + torrent?.seasons + torrent?.season;
}
}
export async function handleLogout() {
@@ -65,4 +69,4 @@ export async function handleLogout() {
console.error('Logout failed:', response.status);
toast.error('Logout failed: ' + response.status);
}
}
}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import '../app.css';
import {ModeWatcher} from "mode-watcher";
import {Toaster} from "$lib/components/ui/sonner/index.js";
import {ModeWatcher} from 'mode-watcher';
import {Toaster} from '$lib/components/ui/sonner/index.js';
let {children} = $props();
</script>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {goto} from "$app/navigation";
import {base} from "$app/paths";
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {onMount} from 'svelte';
onMount(() => {

View File

@@ -1,7 +1,7 @@
import {env} from '$env/dynamic/public';
import type {LayoutServerLoad} from './$types';
import {redirect} from '@sveltejs/kit';
import {base} from "$app/paths";
import {base} from '$app/paths';
const apiUrl = env.PUBLIC_API_URL;

View File

@@ -4,14 +4,14 @@
import type {LayoutProps} from './$types';
import {setContext} from 'svelte';
import {goto} from '$app/navigation';
import {base} from "$app/paths";
import {toast} from "svelte-sonner";
import {base} from '$app/paths';
import {toast} from 'svelte-sonner';
let {data, children}: LayoutProps = $props();
console.log('Received User Data: ', data.user);
if (!data.user.is_verified) {
toast.info("Your account requires verification. Redirecting...");
goto(base + '/login/verify')
toast.info('Your account requires verification. Redirecting...');
goto(base + '/login/verify');
}
setContext('user', () => data.user);
</script>

View File

@@ -2,7 +2,7 @@
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {base} from "$app/paths";
import {base} from '$app/paths';
</script>
<header class="flex h-16 shrink-0 items-center gap-2">

View File

@@ -8,8 +8,6 @@
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import {getFullyQualifiedShowName} from '$lib/utils';
import logo from '$lib/images/svelte-logo.svg';
import {Button} from "$lib/components/ui/button";
import {goto} from "$app/navigation";
let tvShowsPromise = page.data.tvShows;
</script>
@@ -53,7 +51,7 @@
<a href={'/dashboard/tv/' + show.id}>
<Card.Root class="h-full ">
<Card.Header>
<Card.Title class="truncate h-6">{getFullyQualifiedShowName(show)}</Card.Title>
<Card.Title class="h-6 truncate">{getFullyQualifiedShowName(show)}</Card.Title>
<Card.Description class="truncate">{show.overview}</Card.Description>
</Card.Header>
<Card.Content>
@@ -62,8 +60,8 @@
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show.id}.jpg`)}
alt="{getFullyQualifiedShowName(show)}'s Poster Image"
on:error={(e) => {
e.target.src = logo;
}}
e.target.src = logo;
}}
/>
</Card.Content>
</Card.Root>

View File

@@ -7,7 +7,7 @@ export const load: LayoutLoad = async ({params, fetch}) => {
if (!showId) {
return {
showData: null,
torrentsData: null,
torrentsData: null
};
}
@@ -32,7 +32,7 @@ export const load: LayoutLoad = async ({params, fetch}) => {
console.error(`Failed to fetch show ${showId}: ${show.statusText}`);
return {
showData: null,
torrentsData: null,
torrentsData: null
};
}
@@ -43,13 +43,13 @@ export const load: LayoutLoad = async ({params, fetch}) => {
return {
showData: showData,
torrentsData: torrentsData,
torrentsData: torrentsData
};
} catch (error) {
console.error('Error fetching show:', error);
return {
showData: null,
torrentsData: null,
torrentsData: null
};
}
};

View File

@@ -1,122 +1,124 @@
<script lang="ts">
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {goto} from "$app/navigation";
import {ImageOff} from 'lucide-svelte';
import * as Table from '$lib/components/ui/table/index.js';
import {getContext} from 'svelte';
import type {RichShowTorrent, Show, User} from '$lib/types.js';
import {getFullyQualifiedShowName} from '$lib/utils';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import DownloadSeasonDialog from '$lib/components/download-season-dialog.svelte';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import {page} from '$app/state';
import TorrentTable from '$lib/components/torrent-table.svelte';
import RequestSeasonDialog from '$lib/components/request-season-dialog.svelte';
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {goto} from '$app/navigation';
import {ImageOff} from 'lucide-svelte';
import * as Table from '$lib/components/ui/table/index.js';
import {getContext} from 'svelte';
import type {RichShowTorrent, Show, User} from '$lib/types.js';
import {getFullyQualifiedShowName} from '$lib/utils';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import DownloadSeasonDialog from '$lib/components/download-season-dialog.svelte';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import {page} from '$app/state';
import TorrentTable from '$lib/components/torrent-table.svelte';
import RequestSeasonDialog from '$lib/components/request-season-dialog.svelte';
let show: Show = getContext('show');
let user: User = getContext('user');
let torrents: RichShowTorrent = page.data.torrentsData
let show: Show = getContext('show');
let user: User = getContext('user');
let torrents: RichShowTorrent = page.data.torrentsData;
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>{getFullyQualifiedShowName(show())}</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>{getFullyQualifiedShowName(show())}</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
{getFullyQualifiedShowName(show())}
{getFullyQualifiedShowName(show())}
</h1>
<div class="flex flex-1 flex-col gap-4 p-4">
<div class="flex items-center gap-2">
<div class="max-h-50% w-1/3 max-w-sm rounded-xl bg-muted/50">
{#if show().id}
<img
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
alt="{show().name}'s Poster Image"
/>
{:else}
<div
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
>
<ImageOff size={48}/>
</div>
{/if}
</div>
<div class="h-full flex-auto rounded-xl bg-muted/50 p-4">
<p class="leading-7 [&:not(:first-child)]:mt-6">
{show().overview}
</p>
</div>
<div class="h-full flex-auto rounded-xl bg-muted/50 p-4 flex flex-col items-center justify-center gap-2">
{#if user().is_superuser}
<DownloadSeasonDialog show={show()}/>
{/if}
<RequestSeasonDialog show={show()}/>
</div>
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
<div class="w-full overflow-x-auto">
<Table.Root>
<Table.Caption>A list of all seasons.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Number</Table.Head>
<Table.Head>Exists on file</Table.Head>
<Table.Head>Title</Table.Head>
<Table.Head>Overview</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if show().seasons.length > 0}
{#each show().seasons as season (season.id)}
<Table.Row
link={true}
onclick={() => goto('/dashboard/tv/' + show().id + '/' + season.number)}
>
<Table.Cell class="min-w-[10px] font-medium">{season.number}</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium">
<CheckmarkX state={season.downloaded}/>
</Table.Cell>
<Table.Cell class="min-w-[50px]">{season.name}</Table.Cell>
<Table.Cell class="max-w-[300px] truncate">{season.overview}</Table.Cell>
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan="3" class="text-center">No season data available.</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
<div class="w-full overflow-x-auto">
<TorrentTable torrents={torrents.torrents}/>
</div>
</div>
<div class="flex items-center gap-2">
<div class="max-h-50% w-1/3 max-w-sm rounded-xl bg-muted/50">
{#if show().id}
<img
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
alt="{show().name}'s Poster Image"
/>
{:else}
<div
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
>
<ImageOff size={48}/>
</div>
{/if}
</div>
<div class="h-full flex-auto rounded-xl bg-muted/50 p-4">
<p class="leading-7 [&:not(:first-child)]:mt-6">
{show().overview}
</p>
</div>
<div
class="flex h-full flex-auto flex-col items-center justify-center gap-2 rounded-xl bg-muted/50 p-4"
>
{#if user().is_superuser}
<DownloadSeasonDialog show={show()}/>
{/if}
<RequestSeasonDialog show={show()}/>
</div>
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
<div class="w-full overflow-x-auto">
<Table.Root>
<Table.Caption>A list of all seasons.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Number</Table.Head>
<Table.Head>Exists on file</Table.Head>
<Table.Head>Title</Table.Head>
<Table.Head>Overview</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if show().seasons.length > 0}
{#each show().seasons as season (season.id)}
<Table.Row
link={true}
onclick={() => goto('/dashboard/tv/' + show().id + '/' + season.number)}
>
<Table.Cell class="min-w-[10px] font-medium">{season.number}</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium">
<CheckmarkX state={season.downloaded}/>
</Table.Cell>
<Table.Cell class="min-w-[50px]">{season.name}</Table.Cell>
<Table.Cell class="max-w-[300px] truncate">{season.overview}</Table.Cell>
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan="3" class="text-center">No season data available.</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
<div class="w-full overflow-x-auto">
<TorrentTable torrents={torrents.torrents}/>
</div>
</div>
</div>

View File

@@ -6,19 +6,19 @@
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import {getContext} from 'svelte';
import type {PublicSeasonFile, RichShowTorrent, Season, Show} from '$lib/types';
import type {PublicSeasonFile, Season, Show} from '$lib/types';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import {getTorrentQualityString, getFullyQualifiedShowName} from "$lib/utils";
import {getFullyQualifiedShowName, getTorrentQualityString} from '$lib/utils';
const SeasonNumber = page.params.SeasonNumber;
let seasonFiles: PublicSeasonFile[] = $state(page.data.files);
let show: Show = getContext('show');
let season: Season;
let season: Season = $state();
show.seasons.forEach((item) => {
if (item.number === parseInt(SeasonNumber)) season = item;
});
console.log('loaded files', seasonFiles);
console.log('loaded files', seasonFiles);
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
@@ -54,7 +54,7 @@
</div>
</header>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
{getFullyQualifiedShowName(show)} Season {SeasonNumber}
{getFullyQualifiedShowName(show)} Season {SeasonNumber}
</h1>
<div class="flex flex-1 flex-col gap-4 p-4">
<div class="flex items-center gap-2">
@@ -65,12 +65,12 @@
alt="{show.name}'s Poster Image"
/>
</div>
<div class="h-full flex-auto rounded-xl bg-muted/50 p-4 w-1/4 ">
<div class="h-full w-1/4 flex-auto rounded-xl bg-muted/50 p-4">
<p class="leading-7 [&:not(:first-child)]:mt-6">
{show.overview}
</p>
</div>
<div class="h-full flex-auto rounded-xl bg-muted/50 p-4 w-1/3">
<div class="h-full w-1/3 flex-auto rounded-xl bg-muted/50 p-4">
<Table.Root>
<Table.Caption>A list of all downloaded/downloading versions of this season.</Table.Caption>
<Table.Header>
@@ -90,10 +90,10 @@
{file.file_path_suffix}
</Table.Cell>
<Table.Cell class="w-[10px] font-medium">
<CheckmarkX state={file.downloaded}/>
<CheckmarkX state={file.downloaded}/>
</Table.Cell>
</Table.Row>
{:else }
{:else}
<span class="font-semibold">You haven't downloaded this season yet.</span>
{/each}
</Table.Body>

View File

@@ -4,35 +4,34 @@ import type {PageLoad} from './$types';
const apiUrl = env.PUBLIC_API_URL;
export const load: PageLoad = async ({fetch, params}) => {
const url = `${apiUrl}/tv/shows/${params.showId}/${params.SeasonNumber}/files`;
const url = `${apiUrl}/tv/shows/${params.showId}/${params.SeasonNumber}/files`;
try {
console.log(`Fetching data from: ${url}`);
const response = await fetch(url, {
method: 'GET',
credentials: 'include'
});
try {
console.log(`Fetching data from: ${url}`);
const response = await fetch(url, {
method: 'GET',
credentials: 'include'
});
if (!response.ok) {
const errorText = await response.text();
console.error(`API request failed with status ${response.status}: ${errorText}`);
return {
error: `Failed to load TV show files. Status: ${response.status}`,
files: []
};
}
if (!response.ok) {
const errorText = await response.text();
console.error(`API request failed with status ${response.status}: ${errorText}`);
return {
error: `Failed to load TV show files. Status: ${response.status}`,
files: []
};
}
const filesData = await response.json();
console.log("received season_files data: ", filesData);
return {
files: filesData
};
} catch (error) {
console.error('An error occurred while fetching TV show files:', error);
return {
error: `An unexpected error occurred: ${error.message || 'Unknown error'}`,
files: []
};
}
};
const filesData = await response.json();
console.log('received season_files data: ', filesData);
return {
files: filesData
};
} catch (error) {
console.error('An error occurred while fetching TV show files:', error);
return {
error: `An unexpected error occurred: ${error.message || 'Unknown error'}`,
files: []
};
}
};

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card/index.js';
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
@@ -7,20 +6,16 @@
import {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {Button} from '$lib/components/ui/button';
import {ChevronDown, ImageOff} from 'lucide-svelte';
import {ChevronDown} from 'lucide-svelte';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import type {MetaDataProviderShowSearchResult} from '$lib/types.js';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import AddShowCard from '$lib/components/add-show-card.svelte';
import {toast} from 'svelte-sonner';
let searchTerm: string = $state('');
let metadataProvider: string = $state('tmdb');
let results:
| MetaDataProviderShowSearchResult[]
| null = $state(null);
let results: MetaDataProviderShowSearchResult[] | null = $state(null);
async function search() {
if (searchTerm.length > 0) {
@@ -44,7 +39,8 @@
toast.info(`No results found for "${searchTerm}".`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred during search.';
const errorMessage =
error instanceof Error ? error.message : 'An unknown error occurred during search.';
console.error('Search error:', error);
toast.error(errorMessage);
results = null; // Clear previous results on error
@@ -54,7 +50,6 @@
results = null;
}
}
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
@@ -83,7 +78,7 @@
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<div class="grid w-full max-w-sm items-center gap-12">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Add a show
@@ -135,7 +130,7 @@
md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
>
{#each results as result (result.external_id)}
<AddShowCard result={result}/>
<AddShowCard {result}/>
{/each}
</div>
{/if}

View File

@@ -1,33 +1,33 @@
import {env} from '$env/dynamic/public';
import type {LayoutLoad} from './$types';
export const load: LayoutLoad = async ({params, fetch}) => {
try {
const requests = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
export const load: LayoutLoad = async ({fetch}) => {
try {
const requests = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!requests.ok) {
console.error(`Failed to fetch season requests ${requests.statusText}`);
return {
requestsData: null,
};
}
if (!requests.ok) {
console.error(`Failed to fetch season requests ${requests.statusText}`);
return {
requestsData: null
};
}
const requestsData = await requests.json();
console.log('Fetched season requests:', requestsData);
const requestsData = await requests.json();
console.log('Fetched season requests:', requestsData);
return {
requestsData: requestsData,
};
} catch (error) {
console.error('Error fetching season requests:', error);
return {
requestsData: null,
};
}
return {
requestsData: requestsData
};
} catch (error) {
console.error('Error fetching season requests:', error);
return {
requestsData: null
};
}
};

View File

@@ -1,49 +1,44 @@
<script lang="ts">
import {page} from '$app/state';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import {getTorrentQualityString, getTorrentStatusString} from '$lib/utils';
import type {SeasonRequest} from '$lib/types';
import {getFullyQualifiedShowName} from '$lib/utils';
import * as Accordion from '$lib/components/ui/accordion/index.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import * as Card from "$lib/components/ui/card/index.js";
import RequestsTable from "$lib/components/season-requests-table.svelte";
import {page} from '$app/state';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import type {SeasonRequest} from '$lib/types';
import RequestsTable from '$lib/components/season-requests-table.svelte';
let requests: SeasonRequest[] = $state(page.data.requestsData);
let requests: SeasonRequest[] = $state(page.data.requestsData);
</script>
<!-- TODO: ADD DIALOGUE TO MODIFY REQUEST -->
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>TV Torrents</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>TV Torrents</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Season Requests
</h1>
<RequestsTable bind:requests={requests}/>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Season Requests
</h1>
<RequestsTable bind:requests/>
</div>

View File

@@ -3,14 +3,11 @@
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import {getTorrentQualityString, getTorrentStatusString} from '$lib/utils';
import type {RichShowTorrent} from '$lib/types';
import {getFullyQualifiedShowName} from '$lib/utils';
import * as Accordion from '$lib/components/ui/accordion/index.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import * as Card from "$lib/components/ui/card/index.js";
import TorrentTable from "$lib/components/torrent-table.svelte";
import * as Card from '$lib/components/ui/card/index.js';
import TorrentTable from '$lib/components/torrent-table.svelte';
let showsPromise: Promise<RichShowTorrent[]> = $state(page.data.shows);
</script>

View File

@@ -12,4 +12,4 @@
<LoginForm/>
</div>
</div>
</div>

View File

@@ -5,13 +5,14 @@
import {handleLogout} from '$lib/utils.ts';
</script>
<div class="flex min-h-screen flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8">
<div class="absolute top-4 left-4">
<div
class="flex min-h-screen flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8"
>
<div class="absolute left-4 top-4">
<Logo/>
</div>
<div class="absolute top-4 right-4">
<Button onclick={()=>handleLogout()} variant="outline">Logout</Button>
<div class="absolute right-4 top-4">
<Button onclick={() => handleLogout()} variant="outline">Logout</Button>
</div>
<div class="mx-auto w-full max-w-md text-center">
<div class="mb-6">
@@ -21,8 +22,7 @@
Account Pending Activation
</h1>
<p class="mt-4 text-lg text-muted-foreground">
Your account has been successfully created, but activation by an
administrator is required.
Your account has been successfully created, but activation by an administrator is required.
</p>
<div class="mt-8">
<Button href="/dashboard">Go to Dashboard</Button>
@@ -30,8 +30,8 @@
<p class="mt-10 text-sm text-muted-foreground">
The above button will only work once your account is verified.
</p>
<p class="mt-10 text-sm text-muted-foreground end">
<p class="end mt-10 text-sm text-muted-foreground">
If you have any questions, please contact an administrator.
</p>
</div>
</div>
</div>