format files

This commit is contained in:
maxDorninger
2025-08-30 21:53:00 +02:00
parent 18c0b38c8d
commit e4b8596468
31 changed files with 2055 additions and 2114 deletions

View File

@@ -1,12 +1,9 @@
import { env } from '$env/dynamic/public';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import client from "$lib/api";
const apiUrl = env.PUBLIC_API_URL;
import client from '$lib/api';
export const load: LayoutLoad = async ({ fetch }) => {
const { data, response } = await client.GET('/api/v1/users/me', { fetch: fetch });

View File

@@ -1,79 +1,75 @@
<script lang="ts">
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 RecommendedMediaCarousel from '$lib/components/recommended-media-carousel.svelte';
import {base} from '$app/paths';
import {onMount} from 'svelte';
import {env} from '$env/dynamic/public';
import type {MetaDataProviderSearchResult} from '$lib/types';
import client from "$lib/api";
import type {operations, components} from '$lib/api/api.d.ts';
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 RecommendedMediaCarousel from '$lib/components/recommended-media-carousel.svelte';
import { base } from '$app/paths';
import { onMount } from 'svelte';
import client from '$lib/api';
import type { components } from '$lib/api/api.d.ts';
const apiUrl = env.PUBLIC_API_URL;
let recommendedShows: components['schemas']['MetaDataProviderSearchResult'][] = [];
let showsLoading = true;
let recommendedShows: components['schemas']['MetaDataProviderSearchResult'][] = [];
let showsLoading = true;
let recommendedMovies: components['schemas']['MetaDataProviderSearchResult'][] = [];
let moviesLoading = true;
let recommendedMovies: components['schemas']['MetaDataProviderSearchResult'][] = [];
let moviesLoading = true;
onMount(async () => {
client.GET('/api/v1/tv/recommended').then((res) => {
recommendedShows = res.data as components['schemas']['MetaDataProviderSearchResult'][];
showsLoading = false;
})
client.GET('/api/v1/movies/recommended').then((res) => {
recommendedMovies = res.data as components['schemas']['MetaDataProviderSearchResult'][];
moviesLoading = false;
})
});
onMount(async () => {
client.GET('/api/v1/tv/recommended').then((res) => {
recommendedShows = res.data as components['schemas']['MetaDataProviderSearchResult'][];
showsLoading = false;
});
client.GET('/api/v1/movies/recommended').then((res) => {
recommendedMovies = res.data as components['schemas']['MetaDataProviderSearchResult'][];
moviesLoading = false;
});
});
</script>
<svelte:head>
<title>Dashboard - MediaManager</title>
<meta
content="MediaManager Dashboard - View your recommended movies and TV shows"
name="description"
/>
<title>Dashboard - MediaManager</title>
<meta
content="MediaManager Dashboard - View your recommended movies and TV shows"
name="description"
/>
</svelte:head>
<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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Home</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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Page>Home</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<div class="flex 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">
Dashboard
</h1>
<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}/>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Dashboard
</h1>
<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} />
<h3 class="my-4 text-center text-2xl font-semibold">Trending Movies</h3>
<RecommendedMediaCarousel
isLoading={moviesLoading}
isShow={false}
media={recommendedMovies}
/>
</div>
</main>
<h3 class="my-4 text-center text-2xl font-semibold">Trending Movies</h3>
<RecommendedMediaCarousel
isLoading={moviesLoading}
isShow={false}
media={recommendedMovies}
/>
</div>
</main>
<!---
<!---
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50"></div>
<div class="aspect-video rounded-xl bg-muted/50"></div>

View File

@@ -1,93 +1,90 @@
<script lang="ts">
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 {getFullyQualifiedMediaName} from '$lib/utils';
import MediaPicture from '$lib/components/media-picture.svelte';
import {onMount} from 'svelte';
import {toast} from 'svelte-sonner';
import {env} from '$env/dynamic/public';
import {Skeleton} from '$lib/components/ui/skeleton';
import {base} from '$app/paths';
import client from "$lib/api";
import type { operations, components } from '$lib/api/api.d.ts';
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 { getFullyQualifiedMediaName } from '$lib/utils';
import MediaPicture from '$lib/components/media-picture.svelte';
import { onMount } from 'svelte';
import { Skeleton } from '$lib/components/ui/skeleton';
import { base } from '$app/paths';
import client from '$lib/api';
import type { components } from '$lib/api/api.d.ts';
const apiUrl = env.PUBLIC_API_URL;
let movies: components['schemas']['PublicMovie'][] = [];
let loading = false;
onMount(async () => {
loading = true;
const {data} = await client.GET('/api/v1/movies', {fetch: fetch});
let movies: components['schemas']['PublicMovie'][] = [];
let loading = false;
onMount(async () => {
loading = true;
const { data } = await client.GET('/api/v1/movies', { fetch: fetch });
movies = data as components['schemas']['PublicMovie'][];
console.log('got movies: ', movies);
loading = false;
});
movies = data as components['schemas']['PublicMovie'][];
console.log('got movies: ', movies);
loading = false;
});
</script>
<svelte:head>
<title>Movies - MediaManager</title>
<meta content="Browse and manage your movie collection in MediaManager" name="description"/>
<title>Movies - MediaManager</title>
<meta content="Browse and manage your movie collection in MediaManager" name="description" />
</svelte:head>
<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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Movies</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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Page>Movies</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
{#snippet loadingbar()}
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full "/>
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
<Skeleton class="h-[50vh] w-full " />
{/snippet}
<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"
>
{#if loading}
{@render loadingbar()}
{:else}
{#each movies as movie (movie.id)}
<a href={base + '/dashboard/movies/' + movie.id}>
<Card.Root class="col-span-full max-w-[90vw] ">
<Card.Header>
<Card.Title class="h-6 truncate">{getFullyQualifiedMediaName(movie)}</Card.Title>
<Card.Description class="truncate">{movie.overview}</Card.Description>
</Card.Header>
<Card.Content>
<MediaPicture media={movie}/>
</Card.Content>
</Card.Root>
</a>
{:else}
<div class="col-span-full text-center text-muted-foreground">No movies added yet.</div>
{/each}
{/if}
</div>
<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"
>
{#if loading}
{@render loadingbar()}
{:else}
{#each movies as movie (movie.id)}
<a href={base + '/dashboard/movies/' + movie.id}>
<Card.Root class="col-span-full max-w-[90vw] ">
<Card.Header>
<Card.Title class="h-6 truncate">{getFullyQualifiedMediaName(movie)}</Card.Title>
<Card.Description class="truncate">{movie.overview}</Card.Description>
</Card.Header>
<Card.Content>
<MediaPicture media={movie} />
</Card.Content>
</Card.Root>
</a>
{:else}
<div class="col-span-full text-center text-muted-foreground">No movies added yet.</div>
{/each}
{/if}
</div>
</main>

View File

@@ -1,7 +1,5 @@
import type { PageLoad } from './$types';
import { env } from '$env/dynamic/public';
import { error } from '@sveltejs/kit';
import client from "$lib/api";
import client from '$lib/api';
export const load: PageLoad = async ({ params, fetch }) => {
const { data } = await client.GET('/api/v1/movies/{movie_id}', {
@@ -9,7 +7,7 @@ export const load: PageLoad = async ({ params, fetch }) => {
params: {
path: {
movie_id: params.movieId
},
}
}
});

View File

@@ -1,5 +1,4 @@
<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';
@@ -8,19 +7,16 @@
import { Button } from '$lib/components/ui/button';
import { ChevronDown } from 'lucide-svelte';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import type { MetaDataProviderSearchResult } from '$lib/types.js';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import AddMediaCard from '$lib/components/add-media-card.svelte';
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import client from "$lib/api";
import type {components} from "$lib/api/api";
import client from '$lib/api';
import type { components } from '$lib/api/api';
const apiUrl = env.PUBLIC_API_URL;
let searchTerm: string = $state('');
let metadataProvider: "tmdb" | "tvdb" = $state('tmdb');
let metadataProvider: 'tmdb' | 'tvdb' = $state('tmdb');
let results: components['schemas']['MetaDataProviderSearchResult'][] | null = $state(null);
onMount(() => {
@@ -28,14 +24,17 @@
});
async function search(query: string) {
const {data} = query.length > 0 ? await client.GET('/api/v1/movies/search', {
params: {
query: {
query: query,
metadata_provider: metadataProvider
}
}
}) : await client.GET('/api/v1/tv/recommended');
const { data } =
query.length > 0
? await client.GET('/api/v1/movies/search', {
params: {
query: {
query: query,
metadata_provider: metadataProvider
}
}
})
: await client.GET('/api/v1/tv/recommended');
if (data && data.length > 0) {
toast.success(`Found ${data.length} result(s) for "${query}".`);
results = data as components['schemas']['MetaDataProviderSearchResult'][];

View File

@@ -1,12 +1,10 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import client from "$lib/api";
import type { PageLoad } from './$types';
import client from '$lib/api';
const apiUrl = env.PUBLIC_API_URL;
export const load: PageLoad = async ({fetch}) => {
const {data} = await client.GET('/api/v1/movies/requests', {fetch: fetch});
export const load: PageLoad = async ({ fetch }) => {
const { data } = await client.GET('/api/v1/movies/requests', { fetch: fetch });
return {
requestsData: data
};
return {
requestsData: data
};
};

View File

@@ -2,23 +2,19 @@
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 { RichMovieTorrent } from '$lib/types';
import { getFullyQualifiedMediaName } from '$lib/utils';
import * as Accordion from '$lib/components/ui/accordion/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import TorrentTable from '$lib/components/torrent-table.svelte';
import { onMount } from 'svelte';
import { env } from '$env/dynamic/public';
import { toast } from 'svelte-sonner';
import { base } from '$app/paths';
import client from "$lib/api";
import type {components} from "$lib/api/api";
import client from '$lib/api';
import type { components } from '$lib/api/api';
const apiUrl = env.PUBLIC_API_URL;
let torrents: components["schemas"]["RichMovieTorrent"][] = [];
let torrents: components['schemas']['RichMovieTorrent'][] = [];
onMount(async () => {
const { data } = await client.GET('/api/v1/movies/torrents');
torrents = data as components["schemas"]["RichMovieTorrent"][];
torrents = data as components['schemas']['RichMovieTorrent'][];
});
</script>

View File

@@ -1,260 +1,264 @@
<script lang="ts">
import {onMount} from 'svelte';
import {env} from '$env/dynamic/public';
import {Button} from '$lib/components/ui/button/index.js';
import {Separator} from '$lib/components/ui/separator';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button/index.js';
import { Separator } from '$lib/components/ui/separator';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
const apiUrl = env.PUBLIC_API_URL;
import {base} from '$app/paths';
import client from "$lib/api";
import type {components} from "$lib/api/api";
import { base } from '$app/paths';
import client from '$lib/api';
import type { components } from '$lib/api/api';
let unreadNotifications: components["schemas"]["Notification"][] = [];
let readNotifications: components["schemas"]["Notification"][] = [];
let loading = true;
let showRead = false;
let markingAllAsRead = false;
let unreadNotifications: components['schemas']['Notification'][] = [];
let readNotifications: components['schemas']['Notification'][] = [];
let loading = true;
let showRead = false;
let markingAllAsRead = false;
async function fetchNotifications() {
loading = true;
const unread = await client.GET('/api/v1/notification/unread');
const all = await client.GET('/api/v1/notification');
unreadNotifications = unread.data!;
readNotifications = all.data!.filter((n) => n.read);
loading = false;
}
async function fetchNotifications() {
loading = true;
const unread = await client.GET('/api/v1/notification/unread');
const all = await client.GET('/api/v1/notification');
unreadNotifications = unread.data!;
readNotifications = all.data!.filter((n) => n.read);
loading = false;
}
async function markAsRead(notificationId: string) {
const {response} = await client.PATCH('/api/v1/notification/{notification_id}/read', {params: {path: {notification_id: notificationId}}});
async function markAsRead(notificationId: string) {
const { response } = await client.PATCH('/api/v1/notification/{notification_id}/read', {
params: { path: { notification_id: notificationId } }
});
if (response.ok) {
const notification = unreadNotifications.find((n) => n.id === notificationId);
if (notification) {
notification.read = true;
readNotifications = [notification, ...readNotifications];
unreadNotifications = unreadNotifications.filter((n) => n.id !== notificationId);
}
}
}
if (response.ok) {
const notification = unreadNotifications.find((n) => n.id === notificationId);
if (notification) {
notification.read = true;
readNotifications = [notification, ...readNotifications];
unreadNotifications = unreadNotifications.filter((n) => n.id !== notificationId);
}
}
}
async function markAsUnread(notificationId: string) {
const {response} = await client.PATCH('/api/v1/notification/{notification_id}/unread', {params: {path: {notification_id: notificationId}}});
async function markAsUnread(notificationId: string) {
const { response } = await client.PATCH('/api/v1/notification/{notification_id}/unread', {
params: { path: { notification_id: notificationId } }
});
if (response.ok) {
const notification = readNotifications.find((n) => n.id === notificationId);
if (notification) {
notification.read = false;
unreadNotifications = [notification, ...unreadNotifications];
readNotifications = readNotifications.filter((n) => n.id !== notificationId);
}
}
}
if (response.ok) {
const notification = readNotifications.find((n) => n.id === notificationId);
if (notification) {
notification.read = false;
unreadNotifications = [notification, ...unreadNotifications];
readNotifications = readNotifications.filter((n) => n.id !== notificationId);
}
}
}
async function markAllAsRead() {
if (unreadNotifications.length === 0) return;
async function markAllAsRead() {
if (unreadNotifications.length === 0) return;
try {
markingAllAsRead = true;
const promises = unreadNotifications.map((notification) =>
client.PATCH('/api/v1/notification/{notification_id}/read', {params: {path: {notification_id: notification.id!}}})
);
try {
markingAllAsRead = true;
const promises = unreadNotifications.map((notification) =>
client.PATCH('/api/v1/notification/{notification_id}/read', {
params: { path: { notification_id: notification.id! } }
})
);
await Promise.all(promises);
await Promise.all(promises);
// Move all unread to read
readNotifications = [
...unreadNotifications.map((n) => ({...n, read: true})),
...readNotifications
];
unreadNotifications = [];
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
} finally {
markingAllAsRead = false;
}
}
// Move all unread to read
readNotifications = [
...unreadNotifications.map((n) => ({ ...n, read: true })),
...readNotifications
];
unreadNotifications = [];
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
} finally {
markingAllAsRead = false;
}
}
onMount(() => {
fetchNotifications();
onMount(() => {
fetchNotifications();
const interval = setInterval(fetchNotifications, 30000);
return () => clearInterval(interval);
});
const interval = setInterval(fetchNotifications, 30000);
return () => clearInterval(interval);
});
</script>
<svelte:head>
<title>Notifications - MediaManager</title>
<title>Notifications - MediaManager</title>
</svelte:head>
<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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Notifications</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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Page>Notifications</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<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}
<Button onclick={() => markAllAsRead()} disabled={markingAllAsRead} class="flex items-center">
{#if markingAllAsRead}
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>
{/if}
Mark All as Read
</Button>
{/if}
</div>
<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}
<Button onclick={() => markAllAsRead()} disabled={markingAllAsRead} class="flex items-center">
{#if markingAllAsRead}
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>
{/if}
Mark All as Read
</Button>
{/if}
</div>
{#if loading}
<div class="flex items-center justify-center py-12">
<div class="h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
</div>
{:else}
<!-- Unread Notifications -->
<div class="mb-8">
<div class="mb-4 flex items-center gap-2">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
Unread Notifications
{#if unreadNotifications.length > 0}:
{unreadNotifications.length}
{/if}
</h2>
</div>
{#if loading}
<div class="flex items-center justify-center py-12">
<div class="h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
</div>
{:else}
<!-- Unread Notifications -->
<div class="mb-8">
<div class="mb-4 flex items-center gap-2">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
Unread Notifications
{#if unreadNotifications.length > 0}:
{unreadNotifications.length}
{/if}
</h2>
</div>
{#if unreadNotifications.length === 0}
<div
class="rounded-lg border border-green-200 bg-green-50 p-6 text-center dark:border-green-800 dark:bg-green-900/20"
>
<p class="font-medium text-green-800 dark:text-green-200">All caught up!</p>
<p class="text-sm text-green-600 dark:text-green-400">No unread notifications</p>
</div>
{:else}
<div class="space-y-3">
{#each unreadNotifications as notification (notification.id)}
<div
class="rounded-lg border border-blue-200 bg-blue-50 p-4 shadow-sm dark:border-blue-800 dark:bg-blue-900/20"
>
<div class="flex items-start justify-between gap-4">
<div class="flex flex-1 items-start gap-3">
<div class="flex-1">
<p class="font-medium text-gray-900 dark:text-white">
{notification.message}
</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{new Date(notification.timestamp ?? 0).toLocaleDateString()}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<Button
onclick={() => markAsRead(notification.id ?? '')}
class="rounded-lg p-2 text-blue-600 transition-colors hover:bg-blue-100 dark:hover:bg-blue-800"
title="Mark as read"
variant="outline"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
</Button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{#if unreadNotifications.length === 0}
<div
class="rounded-lg border border-green-200 bg-green-50 p-6 text-center dark:border-green-800 dark:bg-green-900/20"
>
<p class="font-medium text-green-800 dark:text-green-200">All caught up!</p>
<p class="text-sm text-green-600 dark:text-green-400">No unread notifications</p>
</div>
{:else}
<div class="space-y-3">
{#each unreadNotifications as notification (notification.id)}
<div
class="rounded-lg border border-blue-200 bg-blue-50 p-4 shadow-sm dark:border-blue-800 dark:bg-blue-900/20"
>
<div class="flex items-start justify-between gap-4">
<div class="flex flex-1 items-start gap-3">
<div class="flex-1">
<p class="font-medium text-gray-900 dark:text-white">
{notification.message}
</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{new Date(notification.timestamp ?? 0).toLocaleDateString()}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<Button
onclick={() => markAsRead(notification.id ?? '')}
class="rounded-lg p-2 text-blue-600 transition-colors hover:bg-blue-100 dark:hover:bg-blue-800"
title="Mark as read"
variant="outline"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
</Button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Read Notifications Toggle -->
<div class="mb-4">
<button
on:click={() => (showRead = !showRead)}
class="flex items-center gap-2 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<svg
class="h-4 w-4 transition-transform {showRead ? 'rotate-90' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"
></path>
</svg>
<span>Read Notifications ({readNotifications.length})</span>
</button>
</div>
<!-- Read Notifications Toggle -->
<div class="mb-4">
<button
on:click={() => (showRead = !showRead)}
class="flex items-center gap-2 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<svg
class="h-4 w-4 transition-transform {showRead ? 'rotate-90' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"
></path>
</svg>
<span>Read Notifications ({readNotifications.length})</span>
</button>
</div>
<!-- Read Notifications -->
{#if showRead}
<div>
{#if readNotifications.length === 0}
<div
class="rounded-lg border border-gray-200 bg-gray-50 p-6 text-center dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-gray-500 dark:text-gray-400">No read notifications</p>
</div>
{:else}
<div class="space-y-3">
{#each readNotifications as notification (notification.id)}
<div
class="rounded-lg border border-gray-200 bg-white p-4 opacity-75 shadow-sm dark:border-gray-700 dark:bg-gray-800"
>
<div class="flex items-start justify-between gap-4">
<div class="flex flex-1 items-start gap-3">
<div class="flex-1">
<p class="text-gray-700 dark:text-gray-300">
{notification.message}
</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{new Date(notification.timestamp ?? 0).toLocaleDateString()}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<Button
onclick={() => markAsUnread(notification.id ?? '')}
class="rounded-lg p-2 text-blue-600 transition-colors hover:bg-blue-100 dark:hover:bg-blue-800"
title="Mark as unread"
variant="outline"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 7.89a2 2 0 002.83 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
</Button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}
{/if}
<!-- Read Notifications -->
{#if showRead}
<div>
{#if readNotifications.length === 0}
<div
class="rounded-lg border border-gray-200 bg-gray-50 p-6 text-center dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-gray-500 dark:text-gray-400">No read notifications</p>
</div>
{:else}
<div class="space-y-3">
{#each readNotifications as notification (notification.id)}
<div
class="rounded-lg border border-gray-200 bg-white p-4 opacity-75 shadow-sm dark:border-gray-700 dark:bg-gray-800"
>
<div class="flex items-start justify-between gap-4">
<div class="flex flex-1 items-start gap-3">
<div class="flex-1">
<p class="text-gray-700 dark:text-gray-300">
{notification.message}
</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{new Date(notification.timestamp ?? 0).toLocaleDateString()}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<Button
onclick={() => markAsUnread(notification.id ?? '')}
class="rounded-lg p-2 text-blue-600 transition-colors hover:bg-blue-100 dark:hover:bg-blue-800"
title="Mark as unread"
variant="outline"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 7.89a2 2 0 002.83 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
</Button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}
{/if}
</main>

View File

@@ -1,12 +1,10 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import client from "$lib/api";
import type { PageLoad } from './$types';
import client from '$lib/api';
const apiUrl = env.PUBLIC_API_URL;
export const load: PageLoad = async ({fetch}) => {
const {data} = await client.GET('/api/v1/users/all', {fetch: fetch});
export const load: PageLoad = async ({ fetch }) => {
const { data } = await client.GET('/api/v1/users/all', { fetch: fetch });
return {
users: data
};
return {
users: data
};
};

View File

@@ -1,64 +1,64 @@
<script lang="ts">
import {page} from '$app/state';
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 {getFullyQualifiedMediaName} from '$lib/utils';
import MediaPicture from '$lib/components/media-picture.svelte';
import {Skeleton} from '$lib/components/ui/skeleton';
import {base} from '$app/paths';
import { page } from '$app/state';
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 { getFullyQualifiedMediaName } from '$lib/utils';
import MediaPicture from '$lib/components/media-picture.svelte';
import { base } from '$app/paths';
let tvShows = page.data.tvShows;
let tvShows = page.data.tvShows;
</script>
<svelte:head>
<title>TV Shows - MediaManager</title>
<meta content="Browse and manage your TV show collection in MediaManager" name="description"/>
<title>TV Shows - MediaManager</title>
<meta content="Browse and manage your TV show collection in MediaManager" name="description" />
</svelte:head>
<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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Shows</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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Page>Shows</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<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>
<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"
>)}
{#each tvShows as show (show.id)}
<a href={base + '/dashboard/tv/' + show.id}>
<Card.Root class="col-span-full max-w-[90vw] ">
<Card.Header>
<Card.Title class="h-6 truncate">{getFullyQualifiedMediaName(show)}</Card.Title>
<Card.Description class="truncate">{show.overview}</Card.Description>
</Card.Header>
<Card.Content>
<MediaPicture media={show}/>
</Card.Content>
</Card.Root>
</a>
{:else}
<div class="col-span-full text-center text-muted-foreground">No TV shows added yet.</div>
{/each}
</div>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
TV Shows
</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"
>
)}
{#each tvShows as show (show.id)}
<a href={base + '/dashboard/tv/' + show.id}>
<Card.Root class="col-span-full max-w-[90vw] ">
<Card.Header>
<Card.Title class="h-6 truncate">{getFullyQualifiedMediaName(show)}</Card.Title>
<Card.Description class="truncate">{show.overview}</Card.Description>
</Card.Header>
<Card.Content>
<MediaPicture media={show} />
</Card.Content>
</Card.Root>
</a>
{:else}
<div class="col-span-full text-center text-muted-foreground">No TV shows added yet.</div>
{/each}
</div>
</main>

View File

@@ -1,9 +1,7 @@
import {env} from '$env/dynamic/public';
import client from '$lib/api';
import type {PageLoad} from './$types';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({fetch}) => {
const {data} = await client.GET('/api/v1/tv/shows', {fetch: fetch});
return {tvShows: data};
export const load: PageLoad = async ({ fetch }) => {
const { data } = await client.GET('/api/v1/tv/shows', { fetch: fetch });
return { tvShows: data };
};

View File

@@ -1,20 +1,18 @@
import {env} from '$env/dynamic/public';
import type {LayoutLoad} from './$types';
import client from "$lib/api";
import type { LayoutLoad } from './$types';
import client from '$lib/api';
const apiUrl = env.PUBLIC_API_URL;
export const load: LayoutLoad = async ({params, fetch}) => {
const show = await client.GET('/api/v1/tv/shows/{show_id}', {
fetch: fetch,
params: {path: {show_id: params.showId}}
});
const torrents = await client.GET('/api/v1/tv/shows/{show_id}/torrents', {
fetch: fetch,
params: {path: {show_id: params.showId}}
});
export const load: LayoutLoad = async ({ params, fetch }) => {
const show = await client.GET('/api/v1/tv/shows/{show_id}', {
fetch: fetch,
params: { path: { show_id: params.showId } }
});
const torrents = await client.GET('/api/v1/tv/shows/{show_id}/torrents', {
fetch: fetch,
params: { path: { show_id: params.showId } }
});
return {
showData: show.data,
torrentsData: torrents.data
};
return {
showData: show.data,
torrentsData: torrents.data
};
};

View File

@@ -1,211 +1,209 @@
<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 {PublicShow, RichShowTorrent, User} from '$lib/types.js';
import {getFullyQualifiedMediaName} from '$lib/utils';
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 MediaPicture from '$lib/components/media-picture.svelte';
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';
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 { PublicShow, RichShowTorrent, User } from '$lib/types.js';
import { getFullyQualifiedMediaName } from '$lib/utils';
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 MediaPicture from '$lib/components/media-picture.svelte';
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';
import client from "$lib/api";
let show: () => PublicShow = getContext('show');
let user: () => User = getContext('user');
let torrents: RichShowTorrent = page.data.torrentsData;
import { base } from '$app/paths';
import client from '$lib/api';
let continuousDownloadEnabled = $state(show().continuous_download);
let continuousDownloadEnabled = $state(show().continuous_download);
async function toggle_continuous_download() {
const {response} = await client.POST('/api/v1/tv/shows/{show_id}/continuousDownload', {
params: {
path: {show_id: show().id},
query: {continuous_download: !continuousDownloadEnabled}
}
});
console.log(
'Toggling continuous download for show',
show().name,
'to',
!continuousDownloadEnabled
);
if (!response.ok) {
const errorText = await response.text();
toast.error('Failed to toggle continuous download: ' + errorText);
} else {
continuousDownloadEnabled = !continuousDownloadEnabled;
toast.success('Continuous download toggled successfully.');
}
}
async function toggle_continuous_download() {
const { response } = await client.POST('/api/v1/tv/shows/{show_id}/continuousDownload', {
params: {
path: { show_id: show().id },
query: { continuous_download: !continuousDownloadEnabled }
}
});
console.log(
'Toggling continuous download for show',
show().name,
'to',
!continuousDownloadEnabled
);
if (!response.ok) {
const errorText = await response.text();
toast.error('Failed to toggle continuous download: ' + errorText);
} else {
continuousDownloadEnabled = !continuousDownloadEnabled;
toast.success('Continuous download toggled successfully.');
}
}
</script>
<svelte:head>
<title>{getFullyQualifiedMediaName(show())} - MediaManager</title>
<meta
content="View details and manage downloads for {getFullyQualifiedMediaName(
<title>{getFullyQualifiedMediaName(show())} - MediaManager</title>
<meta
content="View details and manage downloads for {getFullyQualifiedMediaName(
show()
)} in MediaManager"
name="description"
/>
name="description"
/>
</svelte:head>
<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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>{getFullyQualifiedMediaName(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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Page>{getFullyQualifiedMediaName(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">
{getFullyQualifiedMediaName(show())}
{getFullyQualifiedMediaName(show())}
</h1>
<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="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="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="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="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}
<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}
<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}
<RequestSeasonDialog show={show()}/>
</Card.Content>
</Card.Root>
</div>
</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 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>
<div class="flex flex-col gap-4 md:flex-row md:items-stretch">
<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="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="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="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}
<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}
<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}
<RequestSeasonDialog show={show()} />
</Card.Content>
</Card.Root>
</div>
</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 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>
<Card.Content class="w-full overflow-x-auto">
<TorrentTable torrents={torrents.torrents} />
</Card.Content>
</Card.Root>
</div>
</main>

View File

@@ -1,8 +1,5 @@
import { env } from '$env/dynamic/public';
import type { PageLoad } from './$types';
import client from "$lib/api";
const apiUrl = env.PUBLIC_API_URL;
import client from '$lib/api';
export const load: PageLoad = async ({ fetch, params }) => {
const season = await client.GET('/api/v1/tv/seasons/{season_id}', {
@@ -10,7 +7,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
params: {
path: {
season_id: params.SeasonId
},
}
}
});
const seasonFiles = await client.GET('/api/v1/tv/seasons/{season_id}/files', {
@@ -18,7 +15,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
params: {
path: {
season_id: params.SeasonId
},
}
}
});
return {

View File

@@ -1,134 +1,134 @@
<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 {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {Button} from '$lib/components/ui/button';
import {ChevronDown} 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 AddMediaCard from '$lib/components/add-media-card.svelte';
import {toast} from 'svelte-sonner';
import {onMount} from 'svelte';
import {SvelteURLSearchParams} from 'svelte/reactivity';
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 { Button } from '$lib/components/ui/button';
import { ChevronDown } 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 AddMediaCard from '$lib/components/add-media-card.svelte';
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
const apiUrl = env.PUBLIC_API_URL;
let searchTerm: string = $state('');
let metadataProvider: "tmdb" | "tvdb" = $state('tmdb');
let data: components['schemas']['MetaDataProviderSearchResult'][] | null = $state(null);
import {base} from '$app/paths';
import client from "$lib/api";
import type {components} from "$lib/api/api";
let searchTerm: string = $state('');
let metadataProvider: 'tmdb' | 'tvdb' = $state('tmdb');
let data: components['schemas']['MetaDataProviderSearchResult'][] | null = $state(null);
import { base } from '$app/paths';
import client from '$lib/api';
import type { components } from '$lib/api/api';
onMount(() => {
search('');
});
onMount(() => {
search('');
});
async function search(query: string) {
const results = query.length > 0 ? await client.GET('/api/v1/tv/search', {
params: {
query: {
query: query,
metadata_provider: metadataProvider
}
}
}) : await client.GET('/api/v1/tv/recommended');
if (results.data && results.data.length > 0) {
toast.success(`Found ${results.data.length} result(s) for "${query}".`);
data = results.data as components['schemas']['MetaDataProviderSearchResult'][];
} else {
toast.info(`No results found for "${query}".`);
data = null;
}
}
async function search(query: string) {
const results =
query.length > 0
? await client.GET('/api/v1/tv/search', {
params: {
query: {
query: query,
metadata_provider: metadataProvider
}
}
})
: await client.GET('/api/v1/tv/recommended');
if (results.data && results.data.length > 0) {
toast.success(`Found ${results.data.length} result(s) for "${query}".`);
data = results.data as components['schemas']['MetaDataProviderSearchResult'][];
} else {
toast.info(`No results found for "${query}".`);
data = null;
}
}
</script>
<svelte:head>
<title>Add TV Show - MediaManager</title>
<meta content="Add a new TV show to your MediaManager collection" name="description"/>
<title>Add TV Show - MediaManager</title>
<meta content="Add a new TV show to your MediaManager collection" name="description" />
</svelte:head>
<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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Add a 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="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Link href="{base}/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
<Breadcrumb.Item>
<Breadcrumb.Page>Add a Show</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<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
</h1>
<section>
<Label for="search-box">Show Name</Label>
<Input bind:value={searchTerm} id="search-box" placeholder="Show Name" type="text"/>
<p class="text-muted-foreground text-sm">Search for a Show to add.</p>
</section>
<section>
<Collapsible.Root class="w-full space-y-1">
<Collapsible.Trigger>
<div class="flex items-center justify-between space-x-4 px-4">
<h4 class="text-sm font-semibold">Advanced Settings</h4>
<Button class="w-9 p-0" size="sm" variant="ghost">
<ChevronDown/>
<span class="sr-only">Toggle</span>
</Button>
</div>
</Collapsible.Trigger>
<Collapsible.Content class="space-y-1">
<Label for="metadata-provider-selector">Choose which Metadata Provider to query.</Label>
<RadioGroup.Root bind:value={metadataProvider} id="metadata-provider-selector">
<div class="flex items-center space-x-2">
<RadioGroup.Item id="option-one" value="tmdb"/>
<Label for="option-one">TMDB (Recommended)</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item id="option-two" value="tvdb"/>
<Label for="option-two">TVDB</Label>
</div>
</RadioGroup.Root>
</Collapsible.Content>
</Collapsible.Root>
</section>
<section>
<Button onclick={() => search(searchTerm)} type="submit">Search</Button>
</section>
</div>
<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
</h1>
<section>
<Label for="search-box">Show Name</Label>
<Input bind:value={searchTerm} id="search-box" placeholder="Show Name" type="text" />
<p class="text-muted-foreground text-sm">Search for a Show to add.</p>
</section>
<section>
<Collapsible.Root class="w-full space-y-1">
<Collapsible.Trigger>
<div class="flex items-center justify-between space-x-4 px-4">
<h4 class="text-sm font-semibold">Advanced Settings</h4>
<Button class="w-9 p-0" size="sm" variant="ghost">
<ChevronDown />
<span class="sr-only">Toggle</span>
</Button>
</div>
</Collapsible.Trigger>
<Collapsible.Content class="space-y-1">
<Label for="metadata-provider-selector">Choose which Metadata Provider to query.</Label>
<RadioGroup.Root bind:value={metadataProvider} id="metadata-provider-selector">
<div class="flex items-center space-x-2">
<RadioGroup.Item id="option-one" value="tmdb" />
<Label for="option-one">TMDB (Recommended)</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item id="option-two" value="tvdb" />
<Label for="option-two">TVDB</Label>
</div>
</RadioGroup.Root>
</Collapsible.Content>
</Collapsible.Root>
</section>
<section>
<Button onclick={() => search(searchTerm)} type="submit">Search</Button>
</section>
</div>
<Separator class="my-8"/>
<Separator class="my-8" />
{#if data && data.length === 0}
<h3 class="mx-auto">No Shows found.</h3>
{:else if data}
<div
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1
{#if data && data.length === 0}
<h3 class="mx-auto">No Shows found.</h3>
{:else if data}
<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"
>
{#each data as dataItem (dataItem.external_id)}
<AddMediaCard result={dataItem} isShow={true}/>
{/each}
</div>
{/if}
>
{#each data as dataItem (dataItem.external_id)}
<AddMediaCard result={dataItem} isShow={true} />
{/each}
</div>
{/if}
</main>

View File

@@ -1,10 +1,8 @@
import { env } from '$env/dynamic/public';
import type { LayoutLoad } from './$types';
import client from "$lib/api";
import client from '$lib/api';
const apiUrl = env.PUBLIC_API_URL;
export const load: LayoutLoad = async ({ fetch }) => {
const { data, error } = await client.GET('/api/v1/tv/seasons/requests', { fetch: fetch });
const { data } = await client.GET('/api/v1/tv/seasons/requests', { fetch: fetch });
return {
requestsData: data
};

View File

@@ -1,7 +1,5 @@
import { env } from '$env/dynamic/public';
import type { PageLoad } from './$types';
import client from "$lib/api";
import client from '$lib/api';
export const load: PageLoad = async ({ fetch }) => {
const { data } = await client.GET('/api/v1/tv/shows/torrents', { fetch: fetch });