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

@@ -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'
}
}}
/>