Merge pull request #129 from maxdorninger/enhance-frontend

Enhance frontend
This commit is contained in:
Maximilian Dorninger
2025-08-11 22:04:10 +02:00
committed by GitHub
239 changed files with 7075 additions and 3184 deletions

View File

@@ -70,7 +70,7 @@
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Dashboard
</h1>
<div class="min-h-[100vh] flex-1 items-center justify-center rounded-xl p-4 md:min-h-min">
<main class="min-h-screen flex-1 items-center justify-center rounded-xl p-4 md:min-h-min">
<div class="mx-auto max-w-[70vw] md:max-w-[80vw]">
<h3 class="my-4 text-center text-2xl font-semibold">Trending Shows</h3>
<RecommendedMediaCarousel isLoading={showsLoading} isShow={true} media={recommendedShows} />
@@ -82,7 +82,7 @@
media={recommendedMovies}
/>
</div>
</div>
</main>
<!---
<div class="grid auto-rows-min gap-4 md:grid-cols-3">

View File

@@ -33,15 +33,15 @@
</div>
</header>
<div class="flex w-full flex-col items-center justify-center px-4 py-12">
<main class="mx-auto flex w-full flex-1 flex-col items-center gap-4 p-4 md:max-w-[80em]">
<img alt="Media Manager Logo" class="mb-4 h-24 w-24" src={logo} />
<h1 class="mb-2 text-4xl font-bold">About Media Manager</h1>
<p class="mb-6 mt-10 max-w-2xl text-center text-lg">
<p class="mt-10 mb-6 max-w-2xl text-center text-lg">
<strong>Media Manager</strong> is an all-in-one solution for organizing and building your media library.
Built for simplicity and modernity, it helps you keep track of your favorite shows and movies and
explore trending content—all in one place.
</p>
<p class="mb-2 text-sm text-muted-foreground">
<p class="text-muted-foreground mb-2 text-sm">
Version: v{PUBLIC_VERSION}
</p>
<h2
@@ -83,7 +83,7 @@
Metadata sources of MediaManager
</h2>
<div class="my-6 mb-6 flex items-center gap-2 text-sm text-muted-foreground sm:w-1/2 lg:w-1/3">
<div class="text-muted-foreground my-6 mb-6 flex items-center gap-2 text-sm sm:w-1/2 lg:w-1/3">
<a class="flex items-center gap-2" href="https://www.themoviedb.org/" target="_blank">
<img
alt="TMDB Logo"
@@ -95,7 +95,7 @@
>
</a>
</div>
<div class="my-6 mb-6 flex items-center gap-2 text-sm text-muted-foreground sm:w-1/2 lg:w-1/3">
<div class="text-muted-foreground my-6 mb-6 flex items-center gap-2 text-sm sm:w-1/2 lg:w-1/3">
<a class="flex items-center gap-2" href="https://thetvdb.com/subscribe" target="_blank">
<img
alt="TheTVDB Logo"
@@ -107,4 +107,4 @@
>
</a>
</div>
</div>
</main>

View File

@@ -73,7 +73,7 @@
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
{/snippet}
<div class="flex w-full flex-1 flex-col gap-4 p-4 pt-0">
<main class="flex w-full flex-1 flex-col gap-4 p-4 pt-0">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">Movies</h1>
<div
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
@@ -81,7 +81,7 @@
{#if loading}
{@render loadingbar()}
{:else}
{#each movies as movie}
{#each movies as movie (movie.id)}
<a href={base + '/dashboard/movies/' + movie.id}>
<Card.Root class="col-span-full max-w-[90vw] ">
<Card.Header>
@@ -98,4 +98,4 @@
{/each}
{/if}
</div>
</div>
</main>

View File

@@ -4,7 +4,7 @@
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { ImageOff } from 'lucide-svelte';
import { getContext } from 'svelte';
import type { PublicMovie, RichShowTorrent, User } from '$lib/types.js';
import type { PublicMovie, User } from '$lib/types.js';
import { getFullyQualifiedMediaName } from '$lib/utils';
import { page } from '$app/state';
import TorrentTable from '$lib/components/torrent-table.svelte';
@@ -12,12 +12,11 @@
import DownloadMovieDialog from '$lib/components/download-movie-dialog.svelte';
import RequestMovieDialog from '$lib/components/request-movie-dialog.svelte';
import LibraryCombobox from '$lib/components/library-combobox.svelte';
import { Label } from '$lib/components/ui/label';
import { base } from '$app/paths';
import * as Card from '$lib/components/ui/card/index.js';
let movie: PublicMovie = page.data.movie;
let user: () => User = getContext('user');
let torrents: RichShowTorrent = page.data.torrents;
</script>
<svelte:head>
@@ -58,45 +57,66 @@
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
{getFullyQualifiedMediaName(movie)}
</h1>
<div class="flex w-full flex-1 flex-col gap-4 p-4">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<div class="flex flex-col gap-4 md:flex-row md:items-stretch">
<div class="w-full overflow-hidden rounded-xl bg-muted/50 md:w-1/3 md:max-w-sm">
<div class="bg-muted/50 w-full overflow-hidden rounded-xl md:w-1/3 md:max-w-sm">
{#if movie.id}
<MediaPicture media={movie} />
{:else}
<div
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
class="flex aspect-9/16 h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
>
<ImageOff size={48} />
</div>
{/if}
</div>
<div class="w-full flex-auto rounded-xl bg-muted/50 p-4 md:w-1/4">
<p class="leading-7 [&:not(:first-child)]:mt-6">
{movie.overview}
</p>
<div class="h-full w-full flex-auto rounded-xl md:w-1/4">
<Card.Root class="h-full w-full">
<Card.Header>
<Card.Title>Overview</Card.Title>
</Card.Header>
<Card.Content>
<p class="leading-7 not-first:mt-6">
{movie.overview}
</p>
</Card.Content>
</Card.Root>
</div>
<div class="w-full flex-auto rounded-xl bg-muted/50 p-4 md:w-1/3">
<div
class="flex h-full w-full flex-auto flex-col items-center justify-start gap-4 rounded-xl md:w-1/3 md:max-w-[40em]"
>
{#if user().is_superuser}
<div class="mx-1 my-2 block">
<LibraryCombobox media={movie} mediaType="movie" />
<Label for="library-combobox">Select Library for this movie</Label>
<hr />
</div>
<DownloadMovieDialog {movie} />
<div class="my-4"></div>
<Card.Root class="w-full flex-1">
<Card.Header>
<Card.Title>Administrator Controls</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-4">
<LibraryCombobox media={movie} mediaType="movie" />
</Card.Content>
</Card.Root>
{/if}
<RequestMovieDialog {movie} />
<Card.Root class="w-full flex-1">
<Card.Header>
<Card.Title>Download Options</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-4">
{#if user().is_superuser}
<DownloadMovieDialog {movie} />
{/if}
<RequestMovieDialog {movie} />
</Card.Content>
</Card.Root>
</div>
</div>
<!-- <div class="flex-1 rounded-xl bg-muted/50 p-4">
<div class="w-full overflow-x-auto">
</div>
</div> -->
<div class="flex-1 rounded-xl bg-muted/50 p-4">
<div class="w-full overflow-x-auto">
<TorrentTable isShow={false} torrents={torrents.torrents} />
</div>
<div class="flex-1 rounded-xl">
<Card.Root class="h-full w-full">
<Card.Header>
<Card.Title>Torrent Information</Card.Title>
<Card.Description>A list of all torrents associated with this movie.</Card.Description>
</Card.Header>
<Card.Content class="flex flex-col gap-4">
<TorrentTable isShow={false} torrents={movie.torrents} />
</Card.Content>
</Card.Root>
</div>
</div>
</main>

View File

@@ -8,5 +8,6 @@ export const load: PageLoad = async ({ params, fetch }) => {
});
if (!res.ok) throw error(res.status, `Failed to load movie`);
const movieData = await res.json();
return { movie: movieData, torrents: [] };
console.log('got movie data', movieData);
return { movie: movieData };
};

View File

@@ -14,6 +14,7 @@
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { SvelteURLSearchParams } from 'svelte/reactivity';
const apiUrl = env.PUBLIC_API_URL;
let searchTerm: string = $state('');
@@ -26,7 +27,7 @@
async function search(query: string) {
let urlString = apiUrl + '/movies/recommended';
const urlParams = new URLSearchParams();
const urlParams = new SvelteURLSearchParams();
if (query.length > 0) {
urlString = apiUrl + '/movies/search';
@@ -96,7 +97,7 @@
</div>
</header>
<div class="flex w-full max-w-[90vw] flex-1 flex-col items-center gap-4 p-4 pt-0">
<main class="flex w-full max-w-[90vw] 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 Movie
@@ -104,7 +105,7 @@
<section>
<Label for="search-box">Movie Name</Label>
<Input bind:value={searchTerm} id="search-box" placeholder="Show Name" type="text" />
<p class="text-sm text-muted-foreground">Search for a Movie to add.</p>
<p class="text-muted-foreground text-sm">Search for a Movie to add.</p>
</section>
<section>
<Collapsible.Root class="w-full space-y-1">
@@ -146,9 +147,9 @@
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1
md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
>
{#each results as result}
{#each results as result (result.external_id)}
<AddMediaCard {result} isShow={false} />
{/each}
</div>
{/if}
</div>
</main>

View File

@@ -40,9 +40,9 @@
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Movie Requests
</h1>
<RequestsTable {requests} isShow={false} />
</div>
</main>

View File

@@ -59,12 +59,12 @@
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Movie Torrents
</h1>
<Accordion.Root class="w-full" type="single">
{#each torrents as movie}
{#each torrents as movie (movie.movie_id)}
<div class="p-6">
<Card.Root>
<Card.Header>
@@ -81,4 +81,4 @@
<div class="col-span-full text-center text-muted-foreground">No Torrents added yet.</div>
{/each}
</Accordion.Root>
</div>
</main>

View File

@@ -183,7 +183,7 @@
</div>
</header>
<div class="container mx-auto px-4 py-8">
<main class="container mx-auto px-4 py-8">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Notifications</h1>
{#if unreadNotifications.length > 0}
@@ -329,4 +329,4 @@
</div>
{/if}
{/if}
</div>
</main>

View File

@@ -40,7 +40,7 @@
</div>
</header>
<div class="mx-auto flex w-full max-w-[1000px] flex-1 flex-col gap-4 p-4 pt-0">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<h1 class="my-6 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Settings
</h1>
@@ -64,4 +64,4 @@
</Card.Content>
</Card.Root>
{/if}
</div>
</main>

View File

@@ -51,7 +51,7 @@
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
{/snippet}
<div class="flex w-full flex-1 flex-col gap-4 p-4 pt-0">
<main class="flex w-full flex-1 flex-col gap-4 p-4 pt-0">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
TV Shows
</h1>
@@ -64,7 +64,7 @@
{#await tvShowsJson.json()}
{@render loadingbar()}
{:then tvShows}
{#each tvShows as show}
{#each tvShows as show (show.id)}
<a href={base + '/dashboard/tv/' + show.id}>
<Card.Root class="col-span-full max-w-[90vw] ">
<Card.Header>
@@ -82,4 +82,4 @@
{/await}
{/await}
</div>
</div>
</main>

View File

@@ -15,23 +15,27 @@
import TorrentTable from '$lib/components/torrent-table.svelte';
import RequestSeasonDialog from '$lib/components/request-season-dialog.svelte';
import MediaPicture from '$lib/components/media-picture.svelte';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Switch } from '$lib/components/ui/switch/index.js';
import { toast } from 'svelte-sonner';
import { Label } from '$lib/components/ui/label';
import LibraryCombobox from '$lib/components/library-combobox.svelte';
import * as Card from '$lib/components/ui/card/index.js';
const apiUrl = env.PUBLIC_API_URL;
let show: () => PublicShow = getContext('show');
let user: () => User = getContext('user');
let torrents: RichShowTorrent = page.data.torrentsData;
import { base } from '$app/paths';
let continuousDownloadEnabled = $state(show().continuous_download);
async function toggle_continuous_download() {
const urlString = `${apiUrl}/tv/shows/${show().id}/continuousDownload?continuous_download=${!show().continuous_download}`;
const urlString = `${apiUrl}/tv/shows/${show().id}/continuousDownload?continuous_download=${!continuousDownloadEnabled}`;
console.log(
'Toggling continuous download for show',
show().name,
'to',
!show().continuous_download
!continuousDownloadEnabled
);
const response = await fetch(urlString, {
method: 'POST',
@@ -41,10 +45,15 @@
const errorText = await response.text();
toast.error('Failed to toggle continuous download: ' + errorText);
} else {
show().continuous_download = !show().continuous_download;
continuousDownloadEnabled = !continuousDownloadEnabled;
toast.success('Continuous download toggled successfully.');
}
}
/* $effect(()=>{
continuousDownloadEnabled;
toggle_continuous_download();
});*/
</script>
<svelte:head>
@@ -85,91 +94,121 @@
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
{getFullyQualifiedMediaName(show())}
</h1>
<div class="flex w-full flex-1 flex-col gap-4 p-4">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<div class="flex flex-col gap-4 md:flex-row md:items-stretch">
<div class="w-full overflow-hidden rounded-xl bg-muted/50 md:w-1/3 md:max-w-sm">
<div class="bg-muted/50 w-full overflow-hidden rounded-xl md:w-1/3 md:max-w-sm">
{#if show().id}
<MediaPicture media={show()} />
{:else}
<div
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
class="flex aspect-9/16 h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
>
<ImageOff size={48} />
</div>
{/if}
</div>
<div class="w-full flex-auto rounded-xl bg-muted/50 p-4 md:w-1/4">
<p class="leading-7 [&:not(:first-child)]:mt-6">
{show().overview}
</p>
<div class="h-full w-full flex-auto rounded-xl md:w-1/4">
<Card.Root class="h-full w-full">
<Card.Header>
<Card.Title>Overview</Card.Title>
</Card.Header>
<Card.Content>
<p class="leading-7 not-first:mt-6">
{show().overview}
</p>
</Card.Content>
</Card.Root>
</div>
<div class="w-full flex-auto rounded-xl bg-muted/50 p-4 md:w-1/3">
<div
class="flex h-full w-full flex-auto flex-col items-center justify-start gap-4 rounded-xl md:w-1/3 md:max-w-[40em]"
>
{#if user().is_superuser}
{#if !show().ended}
<div class="mx-1 my-2 block">
<Checkbox
checked={show().continuous_download}
onCheckedChange={() => {
toggle_continuous_download();
}}
id="continuous-download-checkbox"
/>
<Label for="continuous-download-checkbox">
Enable automatic download of future seasons
</Label>
<hr />
</div>
{/if}
<div class="mx-1 my-2 block">
<LibraryCombobox media={show()} mediaType="tv" />
<Label for="library-combobox">Select Library for this show</Label>
<hr />
</div>
<DownloadSeasonDialog show={show()} />
<div class="my-2"></div>
<Card.Root class="w-full flex-1">
<Card.Header>
<Card.Title>Administrator Controls</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-4">
{#if !show().ended}
<div class="flex items-center gap-3">
<Switch
bind:checked={() => continuousDownloadEnabled, toggle_continuous_download}
id="continuous-download-checkbox"
/>
<Label for="continuous-download-checkbox">
Enable automatic download of future seasons
</Label>
</div>
{/if}
<LibraryCombobox media={show()} mediaType="tv" />
</Card.Content>
</Card.Root>
{/if}
<RequestSeasonDialog show={show()} />
</div>
</div>
<div class="flex-1 rounded-xl bg-muted/50 p-4">
<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(base + '/dashboard/tv/' + show().id + '/' + season.id)}
>
<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>
<Card.Root class="w-full flex-1">
<Card.Header>
<Card.Title>Download Options</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-4">
{#if user().is_superuser}
<DownloadSeasonDialog show={show()} />
{/if}
</Table.Body>
</Table.Root>
<RequestSeasonDialog show={show()} />
</Card.Content>
</Card.Root>
</div>
</div>
<div class="flex-1 rounded-xl bg-muted/50 p-4">
<div class="w-full overflow-x-auto">
<TorrentTable torrents={torrents.torrents} />
</div>
<div class="flex-1 rounded-xl">
<Card.Root class="w-full">
<Card.Header>
<Card.Title>Season Details</Card.Title>
<Card.Description>
A list of all seasons for {getFullyQualifiedMediaName(show())}.
</Card.Description>
</Card.Header>
<Card.Content 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
onclick={() => goto(base + '/dashboard/tv/' + show().id + '/' + season.id)}
>
<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>
</Card.Content>
</Card.Root>
</div>
</div>
<div class="flex-1 rounded-xl">
<Card.Root>
<Card.Header>
<Card.Title>Torrent Information</Card.Title>
<Card.Description>A list of all torrents associated with this show.</Card.Description>
</Card.Header>
<Card.Content class="w-full overflow-x-auto">
<TorrentTable torrents={torrents.torrents} />
</Card.Content>
</Card.Root>
</div>
</main>

View File

@@ -10,6 +10,7 @@
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils';
import MediaPicture from '$lib/components/media-picture.svelte';
import { base } from '$app/paths';
import * as Card from '$lib/components/ui/card/index.js';
let seasonFiles: PublicSeasonFile[] = $state(page.data.files);
let season: Season = $state(page.data.season);
@@ -63,65 +64,95 @@
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
{getFullyQualifiedMediaName(show())} Season {season.number}
</h1>
<div class="flex flex-1 flex-col gap-4 p-4">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<div class="flex flex-col gap-4 md:flex-row md:items-stretch">
<div class="w-full overflow-hidden rounded-xl bg-muted/50 md:w-1/3 md:max-w-sm">
<div class="bg-muted/50 w-full overflow-hidden rounded-xl md:w-1/3 md:max-w-sm">
<MediaPicture media={show()} />
</div>
<div class="w-full flex-auto rounded-xl bg-muted/50 p-4 md:w-1/4">
<p class="leading-7 [&:not(:first-child)]:mt-6">
{show().overview}
</p>
<div class="h-full w-full flex-auto rounded-xl md:w-1/4">
<Card.Root class="h-full w-full">
<Card.Header>
<Card.Title>Overview</Card.Title>
</Card.Header>
<Card.Content>
<p class="leading-7 not-first:mt-6">
{show().overview}
</p>
</Card.Content>
</Card.Root>
</div>
<div class="w-full flex-auto rounded-xl bg-muted/50 p-4 md:w-1/3">
<Table.Root>
<Table.Caption>A list of all downloaded/downloading versions of this season.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Quality</Table.Head>
<Table.Head>File Path Suffix</Table.Head>
<Table.Head>Imported</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each seasonFiles as file}
<Table.Row>
<Table.Cell class="w-[50px]">
{getTorrentQualityString(file.quality)}
</Table.Cell>
<Table.Cell class="w-[100px]">
{file.file_path_suffix}
</Table.Cell>
<Table.Cell class="w-[10px] font-medium">
<CheckmarkX state={file.downloaded} />
</Table.Cell>
</Table.Row>
{:else}
<span class="font-semibold">You haven't downloaded this season yet.</span>
{/each}
</Table.Body>
</Table.Root>
<div
class="flex h-full w-full flex-auto flex-col items-center justify-start gap-4 rounded-xl md:w-1/3 md:max-w-[40em]"
>
<Card.Root class="h-full w-full">
<Card.Header>
<Card.Title>Season Details</Card.Title>
<Card.Description>
A list of all downloaded/downloading versions of this season.
</Card.Description>
</Card.Header>
<Card.Content>
<Table.Root>
<Table.Caption
>A list of all downloaded/downloading versions of this season.</Table.Caption
>
<Table.Header>
<Table.Row>
<Table.Head>Quality</Table.Head>
<Table.Head>File Path Suffix</Table.Head>
<Table.Head>Imported</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each seasonFiles as file (file)}
<Table.Row>
<Table.Cell class="w-[50px]">
{getTorrentQualityString(file.quality)}
</Table.Cell>
<Table.Cell class="w-[100px]">
{file.file_path_suffix}
</Table.Cell>
<Table.Cell class="w-[10px] font-medium">
<CheckmarkX state={file.downloaded} />
</Table.Cell>
</Table.Row>
{:else}
<span class="font-semibold">You haven't downloaded this season yet.</span>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
</div>
</div>
<div class="flex-1 rounded-xl bg-muted/50 p-4">
<div class="w-full overflow-x-auto">
<Table.Root>
<Table.Caption>A list of all episodes.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Number</Table.Head>
<Table.Head class="min-w-[50px]">Title</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each season.episodes as episode (episode.id)}
<div class="flex-1 rounded-xl">
<Card.Root class="w-full">
<Card.Header>
<Card.Title>Episodes</Card.Title>
<Card.Description
>A list of all episodes for {getFullyQualifiedMediaName(show())} Season {season.number}
.
</Card.Description>
</Card.Header>
<Card.Content class="w-full overflow-x-auto">
<Table.Root>
<Table.Caption>A list of all episodes.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Cell class="w-[100px] font-medium">{episode.number}</Table.Cell>
<Table.Cell class="min-w-[50px]">{episode.title}</Table.Cell>
<Table.Head class="w-[100px]">Number</Table.Head>
<Table.Head class="min-w-[50px]">Title</Table.Head>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
</Table.Header>
<Table.Body>
{#each season.episodes as episode (episode.id)}
<Table.Row>
<Table.Cell class="w-[100px] font-medium">{episode.number}</Table.Cell>
<Table.Cell class="min-w-[50px]">{episode.title}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
</div>
</div>
</main>

View File

@@ -13,6 +13,7 @@
import AddMediaCard from '$lib/components/add-media-card.svelte';
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { SvelteURLSearchParams } from 'svelte/reactivity';
const apiUrl = env.PUBLIC_API_URL;
let searchTerm: string = $state('');
@@ -26,7 +27,7 @@
async function search(query: string) {
let urlString = apiUrl + '/tv/recommended';
const urlParams = new URLSearchParams();
const urlParams = new SvelteURLSearchParams();
if (query.length > 0) {
urlString = apiUrl + '/tv/search';
@@ -96,7 +97,7 @@
</div>
</header>
<div class="flex w-full max-w-[90vw] flex-1 flex-col items-center gap-4 p-4 pt-0">
<main class="flex w-full max-w-[90vw] 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
@@ -104,7 +105,7 @@
<section>
<Label for="search-box">Show Name</Label>
<Input bind:value={searchTerm} id="search-box" placeholder="Show Name" type="text" />
<p class="text-sm text-muted-foreground">Search for a Show to add.</p>
<p class="text-muted-foreground text-sm">Search for a Show to add.</p>
</section>
<section>
<Collapsible.Root class="w-full space-y-1">
@@ -146,9 +147,9 @@
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1
md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
>
{#each results as result}
{#each results as result (result.external_id)}
<AddMediaCard {result} isShow={true} />
{/each}
</div>
{/if}
</div>
</main>

View File

@@ -41,9 +41,9 @@
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Season Requests
</h1>
<RequestsTable {requests} isShow={true} />
</div>
</main>

View File

@@ -44,7 +44,7 @@
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<div class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
TV Torrents
</h1>
@@ -52,7 +52,7 @@
Loading...
{:then shows}
<Accordion.Root type="single" class="w-full">
{#each shows as show}
{#each shows as show (show.show_id)}
<div class="p-6">
<Card.Root>
<Card.Header>