mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-20 07:54:19 +02:00
fix formatting
This commit is contained in:
@@ -1,85 +1,89 @@
|
||||
<script lang="ts">
|
||||
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 {ImageOff} from 'lucide-svelte';
|
||||
import {goto} from '$app/navigation';
|
||||
import {base} from '$app/paths';
|
||||
import type {MetaDataProviderShowSearchResult} from '$lib/types.js';
|
||||
import {toOptimizedURL} from "sveltekit-image-optimize/components";
|
||||
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 {ImageOff} from 'lucide-svelte';
|
||||
import {goto} from '$app/navigation';
|
||||
import {base} from '$app/paths';
|
||||
import type {MetaDataProviderShowSearchResult} from '$lib/types.js';
|
||||
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
|
||||
|
||||
let loading = $state(false);
|
||||
let errorMessage = $state(null);
|
||||
let {result}: { result: MetaDataProviderShowSearchResult } = $props();
|
||||
console.log('Add Show Card Result: ', result);
|
||||
let loading = $state(false);
|
||||
let errorMessage = $state(null);
|
||||
let {result}: { result: MetaDataProviderShowSearchResult } = $props();
|
||||
console.log('Add Show Card Result: ', result);
|
||||
|
||||
async function addShow() {
|
||||
loading = true;
|
||||
let url = new URL(env.PUBLIC_API_URL + '/tv/shows');
|
||||
url.searchParams.append('show_id', String(result.external_id));
|
||||
url.searchParams.append('metadata_provider', result.metadata_provider);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
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;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
async function addShow() {
|
||||
loading = true;
|
||||
let url = new URL(env.PUBLIC_API_URL + '/tv/shows');
|
||||
url.searchParams.append('show_id', String(result.external_id));
|
||||
url.searchParams.append('metadata_provider', result.metadata_provider);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
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;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="h-full max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title class="h-12 overflow-hidden leading-tight flex items-center">
|
||||
{result.name}
|
||||
{#if result.year != null}
|
||||
({result.year})
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
class="truncate">{result.overview !== "" ? result.overview : "No overview available"}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="w-full h-96 flex items-center justify-center">
|
||||
{#if result.poster_path != null}
|
||||
<img
|
||||
class="max-h-full max-w-full object-contain rounded-lg"
|
||||
src={toOptimizedURL(result.poster_path)}
|
||||
alt="{result.name}'s Poster Image"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<ImageOff class="w-12 h-12 text-gray-400"/>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
<Card.Footer class="flex flex-col gap-2 items-start p-4 bg-card rounded-b-lg border-t">
|
||||
<Button
|
||||
class="w-full font-semibold"
|
||||
disabled={result.added || loading}
|
||||
onclick={() => addShow(result)}
|
||||
>
|
||||
{#if loading}
|
||||
<span class="animate-pulse">Loading...</span>
|
||||
{:else}
|
||||
{result.added ? 'Show already exists' : 'Add Show'}
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
{#if result.vote_average != null}
|
||||
<span class="text-sm text-yellow-600 font-medium flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path
|
||||
d="M10 15l-5.878 3.09 1.122-6.545L.488 6.91l6.561-.955L10 0l2.951 5.955 6.561.955-4.756 4.635 1.122 6.545z"/></svg>
|
||||
Rating: {Math.round(result.vote_average)}/10
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if errorMessage}
|
||||
<p class="text-xs text-red-500 bg-red-50 rounded px-2 py-1 w-full">{errorMessage}</p>
|
||||
{/if}
|
||||
</Card.Footer>
|
||||
<Card.Header>
|
||||
<Card.Title class="flex h-12 items-center overflow-hidden leading-tight">
|
||||
{result.name}
|
||||
{#if result.year != null}
|
||||
({result.year})
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description class="truncate"
|
||||
>{result.overview !== '' ? result.overview : 'No overview available'}</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex h-96 w-full items-center justify-center">
|
||||
{#if result.poster_path != null}
|
||||
<img
|
||||
class="max-h-full max-w-full rounded-lg object-contain"
|
||||
src={toOptimizedURL(result.poster_path)}
|
||||
alt="{result.name}'s Poster Image"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<ImageOff class="h-12 w-12 text-gray-400"/>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
<Card.Footer class="flex flex-col items-start gap-2 rounded-b-lg border-t bg-card p-4">
|
||||
<Button
|
||||
class="w-full font-semibold"
|
||||
disabled={result.added || loading}
|
||||
onclick={() => addShow(result)}
|
||||
>
|
||||
{#if loading}
|
||||
<span class="animate-pulse">Loading...</span>
|
||||
{:else}
|
||||
{result.added ? 'Show already exists' : 'Add Show'}
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="flex w-full items-center gap-2">
|
||||
{#if result.vote_average != null}
|
||||
<span class="flex items-center text-sm font-medium text-yellow-600">
|
||||
<svg class="mr-1 h-4 w-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"
|
||||
><path
|
||||
d="M10 15l-5.878 3.09 1.122-6.545L.488 6.91l6.561-.955L10 0l2.951 5.955 6.561.955-4.756 4.635 1.122 6.545z"
|
||||
/></svg
|
||||
>
|
||||
Rating: {Math.round(result.vote_average)}/10
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if errorMessage}
|
||||
<p class="w-full rounded bg-red-50 px-2 py-1 text-xs text-red-500">{errorMessage}</p>
|
||||
{/if}
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,98 +1,98 @@
|
||||
<script lang="ts" module>
|
||||
import {Home, Info, LifeBuoy, Send, Settings, TvIcon} from "lucide-svelte";
|
||||
import {PUBLIC_VERSION} from '$env/static/public';
|
||||
import {Home, Info, LifeBuoy, Send, Settings, TvIcon} from 'lucide-svelte';
|
||||
import {PUBLIC_VERSION} from '$env/static/public';
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/dashboard',
|
||||
icon: Home,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
title: 'TV',
|
||||
url: '/dashboard/tv',
|
||||
icon: TvIcon,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: 'Add a show',
|
||||
url: '/dashboard/tv/add-show'
|
||||
},
|
||||
{
|
||||
title: 'Torrents',
|
||||
url: '/dashboard/tv/torrents'
|
||||
},
|
||||
{
|
||||
title: 'Requests',
|
||||
url: '/dashboard/tv/requests'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
url: '/dashboard/settings',
|
||||
icon: Settings,
|
||||
isActive: true,
|
||||
}
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: 'Support',
|
||||
url: '#',
|
||||
icon: LifeBuoy
|
||||
},
|
||||
{
|
||||
title: 'Feedback',
|
||||
url: '#',
|
||||
icon: Send
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
url: '/dashboard/about',
|
||||
icon: Info
|
||||
}
|
||||
]
|
||||
};
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/dashboard',
|
||||
icon: Home,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
title: 'TV',
|
||||
url: '/dashboard/tv',
|
||||
icon: TvIcon,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: 'Add a show',
|
||||
url: '/dashboard/tv/add-show'
|
||||
},
|
||||
{
|
||||
title: 'Torrents',
|
||||
url: '/dashboard/tv/torrents'
|
||||
},
|
||||
{
|
||||
title: 'Requests',
|
||||
url: '/dashboard/tv/requests'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
url: '/dashboard/settings',
|
||||
icon: Settings,
|
||||
isActive: true
|
||||
}
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: 'Support',
|
||||
url: '#',
|
||||
icon: LifeBuoy
|
||||
},
|
||||
{
|
||||
title: 'Feedback',
|
||||
url: '#',
|
||||
icon: Send
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
url: '/dashboard/about',
|
||||
icon: Info
|
||||
}
|
||||
]
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import NavMain from '$lib/components/nav-main.svelte';
|
||||
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 type {ComponentProps} from 'svelte';
|
||||
import logo from '$lib/images/logo.svg';
|
||||
import {base} from '$app/paths';
|
||||
import NavMain from '$lib/components/nav-main.svelte';
|
||||
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 type {ComponentProps} from 'svelte';
|
||||
import logo from '$lib/images/logo.svg';
|
||||
import {base} from '$app/paths';
|
||||
|
||||
let {ref = $bindable(null), ...restProps}: ComponentProps<typeof Sidebar.Root> = $props();
|
||||
let {ref = $bindable(null), ...restProps}: ComponentProps<typeof Sidebar.Root> = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root {...restProps} bind:ref variant="inset">
|
||||
<Sidebar.Header>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="lg">
|
||||
{#snippet child({props})}
|
||||
<a href="{base}/dashboard" {...props}>
|
||||
<img class="size-12" src={logo} alt="Media Manager Logo"/>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">Media Manager</span>
|
||||
<span class="truncate text-xs">v{PUBLIC_VERSION}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Header>
|
||||
<Sidebar.Content>
|
||||
<NavMain items={data.navMain}/>
|
||||
<!-- <NavProjects projects={data.projects}/> -->
|
||||
<NavSecondary class="mt-auto" items={data.navSecondary}/>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Footer>
|
||||
<NavUser/>
|
||||
</Sidebar.Footer>
|
||||
<Sidebar.Header>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="lg">
|
||||
{#snippet child({props})}
|
||||
<a href="{base}/dashboard" {...props}>
|
||||
<img class="size-12" src={logo} alt="Media Manager Logo"/>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">Media Manager</span>
|
||||
<span class="truncate text-xs">v{PUBLIC_VERSION}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Header>
|
||||
<Sidebar.Content>
|
||||
<NavMain items={data.navMain}/>
|
||||
<!-- <NavProjects projects={data.projects}/> -->
|
||||
<NavSecondary class="mt-auto" items={data.navSecondary}/>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Footer>
|
||||
<NavUser/>
|
||||
</Sidebar.Footer>
|
||||
</Sidebar.Root>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<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 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 {show} = $props();
|
||||
let dialogueState = $state(false);
|
||||
let selectedSeasonNumber: number = $state(1);
|
||||
let torrents: PublicIndexerQueryResult[] = $state([]);
|
||||
@@ -60,8 +60,8 @@
|
||||
}
|
||||
|
||||
async function getTorrents(
|
||||
season_number: number,
|
||||
override: boolean = false
|
||||
season_number: number,
|
||||
override: boolean = false
|
||||
): Promise<PublicIndexerQueryResult[]> {
|
||||
isLoadingTorrents = true;
|
||||
torrentsError = null;
|
||||
@@ -150,14 +150,14 @@
|
||||
<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
|
||||
>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}
|
||||
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
|
||||
@@ -181,7 +181,7 @@
|
||||
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
|
||||
>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)}
|
||||
@@ -198,10 +198,10 @@
|
||||
{#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}/>
|
||||
<Input type="text" id="query-override" bind:value={queryOverride}/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={async () => {
|
||||
variant="secondary"
|
||||
onclick={async () => {
|
||||
isLoadingTorrents = true;
|
||||
torrentsError = null;
|
||||
torrents = [];
|
||||
@@ -223,11 +223,11 @@
|
||||
</p>
|
||||
<Label for="file-suffix">Filepath suffix</Label>
|
||||
<Input
|
||||
type="text"
|
||||
class="max-w-sm"
|
||||
id="file-suffix"
|
||||
bind:value={filePathSuffix}
|
||||
placeholder="1080P"
|
||||
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
|
||||
@@ -235,7 +235,7 @@
|
||||
</p>
|
||||
|
||||
<Label for="file-suffix-display"
|
||||
>The files will be saved in the following directory:</Label
|
||||
>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)}
|
||||
@@ -251,7 +251,7 @@
|
||||
<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"/>
|
||||
<LoaderCircle class="animate-spin"/>
|
||||
<p>Loading torrents...</p>
|
||||
</div>
|
||||
{:else if torrentsError}
|
||||
@@ -287,9 +287,9 @@
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
downloadTorrent(torrent.id);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Progress} from "$lib/components/ui/progress/index.js";
|
||||
import {onMount} from "svelte";
|
||||
import {Progress} from '$lib/components/ui/progress/index.js';
|
||||
import {onMount} from 'svelte';
|
||||
|
||||
let value = $state(0);
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Progress value={value}/>
|
||||
<Progress {value}/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
let apiUrl = env.PUBLIC_API_URL;
|
||||
|
||||
let {oauthProvider} = $props();
|
||||
let oauthProviderName = $derived(oauthProvider.oauth_name)
|
||||
let oauthProviderName = $derived(oauthProvider.oauth_name);
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
@@ -112,19 +112,25 @@
|
||||
|
||||
async function handleOauth() {
|
||||
try {
|
||||
const response = await fetch(apiUrl + "/auth/cookie/" + oauthProviderName + "/authorize?scopes=email", {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
apiUrl + '/auth/cookie/' + oauthProviderName + '/authorize?scopes=email',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
let result = await response.json();
|
||||
console.log('Redirecting to OAuth provider:', oauthProviderName, "url: ", result.authorization_url);
|
||||
toast.success("Redirecting to " + oauthProviderName + " for authentication...");
|
||||
console.log(
|
||||
'Redirecting to OAuth provider:',
|
||||
oauthProviderName,
|
||||
'url: ',
|
||||
result.authorization_url
|
||||
);
|
||||
toast.success('Redirecting to ' + oauthProviderName + ' for authentication...');
|
||||
window.location = result.authorization_url;
|
||||
|
||||
|
||||
} else {
|
||||
let errorText = await response.text();
|
||||
toast.error(errorMessage);
|
||||
@@ -143,8 +149,9 @@
|
||||
<LoadingBar/>
|
||||
{:then result}
|
||||
{#if result.oauth_name != null}
|
||||
<Button class="mt-2 w-full" onclick={() => handleOauth()} variant="outline">Login
|
||||
with {result.oauth_name}</Button>
|
||||
<Button class="mt-2 w-full" onclick={() => handleOauth()} variant="outline"
|
||||
>Login with {result.oauth_name}</Button
|
||||
>
|
||||
{/if}
|
||||
{/await}
|
||||
{/snippet}
|
||||
@@ -196,8 +203,7 @@
|
||||
<div class="mt-4 text-center text-sm">
|
||||
<Button onclick={() => (tabValue = 'register')} variant="link">
|
||||
Don't have an account? Sign up
|
||||
</Button
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -242,12 +248,10 @@
|
||||
<!-- TODO: dynamically display oauth providers based on config -->
|
||||
{@render oauthLogin()}
|
||||
|
||||
|
||||
<div class="mt-4 text-center text-sm">
|
||||
<Button onclick={() => (tabValue = 'login')} variant="link"
|
||||
>Already have an account? Login
|
||||
</Button
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||
<Sidebar.GroupLabel><!-- TODO: what to set this label to?? --> </Sidebar.GroupLabel>
|
||||
<Sidebar.GroupLabel><!-- TODO: what to set this label to?? --></Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each projects as item (item.name)}
|
||||
<Sidebar.MenuItem>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import UserDetails from './user-details.svelte';
|
||||
import UserRound from '@lucide/svelte/icons/user-round';
|
||||
import {handleLogout} from '$lib/utils.ts';
|
||||
import {goto} from "$app/navigation";
|
||||
import {base} from "$app/paths";
|
||||
import {goto} from '$app/navigation';
|
||||
import {base} from '$app/paths';
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator/>
|
||||
<DropdownMenu.Item onclick={() => goto(base+'/dashboard/settings#me')}>
|
||||
<DropdownMenu.Item onclick={() => goto(base + '/dashboard/settings#me')}>
|
||||
My Account
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator/>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<script lang="ts">
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import * as Carousel from "$lib/components/ui/carousel/index.js";
|
||||
import type {MetaDataProviderShowSearchResult} from "$lib/types";
|
||||
import AddShowCard from "$lib/components/add-show-card.svelte";
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import * as Carousel from '$lib/components/ui/carousel/index.js';
|
||||
import type {MetaDataProviderShowSearchResult} from '$lib/types';
|
||||
import AddShowCard from '$lib/components/add-show-card.svelte';
|
||||
|
||||
let {shows}: { shows: MetaDataProviderShowSearchResult } = $props();
|
||||
</script>
|
||||
|
||||
<Carousel.Root
|
||||
opts={{
|
||||
align: "start",
|
||||
loop: true,
|
||||
}}
|
||||
align: 'start',
|
||||
loop: true
|
||||
}}
|
||||
plugins={[
|
||||
Autoplay({
|
||||
delay: 2000,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
playOnInit: true,
|
||||
|
||||
}),
|
||||
]}
|
||||
Autoplay({
|
||||
delay: 2000,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
playOnInit: true
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Carousel.Content class="-ml-1">
|
||||
{#each shows as show}
|
||||
|
||||
@@ -152,8 +152,7 @@
|
||||
<Dialog.Footer>
|
||||
<Button disabled={isSubmittingRequest} onclick={() => (dialogOpen = false)} variant="outline"
|
||||
>Cancel
|
||||
</Button
|
||||
>
|
||||
</Button>
|
||||
<Button disabled={isFormInvalid || isSubmittingRequest} onclick={handleRequestSeason}>
|
||||
{#if isSubmittingRequest}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin"/>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import {Button} from '$lib/components/ui/button/index.js';
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {toast} from 'svelte-sonner';
|
||||
import {goto} from "$app/navigation";
|
||||
import {base} from "$app/paths";
|
||||
import {goto} from '$app/navigation';
|
||||
import {base} from '$app/paths';
|
||||
|
||||
let {
|
||||
requests,
|
||||
@@ -135,7 +135,7 @@
|
||||
class=""
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => goto(base+"/dashboard/tv/"+request.show.id)}
|
||||
onclick={() => goto(base + '/dashboard/tv/' + request.show.id)}
|
||||
>
|
||||
Download manually
|
||||
</Button>
|
||||
|
||||
@@ -1,75 +1,69 @@
|
||||
<script lang="ts" module>
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from "svelte/elements";
|
||||
import {type VariantProps, tv} from "tailwind-variants";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from 'svelte/elements';
|
||||
import {type VariantProps, tv} from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
|
||||
outline:
|
||||
"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
export const buttonVariants = tv({
|
||||
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
|
||||
outline:
|
||||
'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = 'button',
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{href}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
<a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
<button
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -2,8 +2,8 @@ import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
buttonVariants
|
||||
} from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -13,5 +13,5 @@ export {
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
type ButtonVariant
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import emblaCarouselSvelte from "embla-carousel-svelte";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {getEmblaContext} from "./context.js";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import emblaCarouselSvelte from 'embla-carousel-svelte';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {getEmblaContext} from './context.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,7 +12,7 @@
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
|
||||
const emblaCtx = getEmblaContext("<Carousel.Content/>");
|
||||
const emblaCtx = getEmblaContext('<Carousel.Content/>');
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore event_directive_deprecated -->
|
||||
@@ -21,20 +21,20 @@
|
||||
on:emblaInit={emblaCtx.onInit}
|
||||
use:emblaCarouselSvelte={{
|
||||
options: {
|
||||
container: "[data-embla-container]",
|
||||
slides: "[data-embla-slide]",
|
||||
container: '[data-embla-container]',
|
||||
slides: '[data-embla-slide]',
|
||||
...emblaCtx.options,
|
||||
axis: emblaCtx.orientation === "horizontal" ? "x" : "y",
|
||||
axis: emblaCtx.orientation === 'horizontal' ? 'x' : 'y'
|
||||
},
|
||||
plugins: emblaCtx.plugins,
|
||||
plugins: emblaCtx.plugins
|
||||
}}
|
||||
>
|
||||
<div
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"flex",
|
||||
emblaCtx.orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
'flex',
|
||||
emblaCtx.orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className
|
||||
)}
|
||||
data-embla-container=""
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import {getEmblaContext} from "./context.js";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
import {getEmblaContext} from './context.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,7 +11,7 @@
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
|
||||
const emblaCtx = getEmblaContext("<Carousel.Item/>");
|
||||
const emblaCtx = getEmblaContext('<Carousel.Item/>');
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -19,8 +19,8 @@
|
||||
aria-roledescription="slide"
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
emblaCtx.orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
emblaCtx.orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className
|
||||
)}
|
||||
data-embla-slide=""
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import ArrowRight from "@lucide/svelte/icons/arrow-right";
|
||||
import type {WithoutChildren} from "bits-ui";
|
||||
import {getEmblaContext} from "./context.js";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Button, type Props} from "$lib/components/ui/button/index.js";
|
||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
import type {WithoutChildren} from 'bits-ui';
|
||||
import {getEmblaContext} from './context.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
import {Button, type Props} from '$lib/components/ui/button/index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
variant = 'outline',
|
||||
size = 'icon',
|
||||
...restProps
|
||||
}: WithoutChildren<Props> = $props();
|
||||
|
||||
const emblaCtx = getEmblaContext("<Carousel.Next/>");
|
||||
const emblaCtx = getEmblaContext('<Carousel.Next/>');
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"absolute size-8 touch-manipulation rounded-full",
|
||||
emblaCtx.orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
'absolute size-8 touch-manipulation rounded-full',
|
||||
emblaCtx.orientation === 'horizontal'
|
||||
? '-right-12 top-1/2 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className
|
||||
)}
|
||||
disabled={!emblaCtx.canScrollNext}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import ArrowLeft from "@lucide/svelte/icons/arrow-left";
|
||||
import type {WithoutChildren} from "bits-ui";
|
||||
import {getEmblaContext} from "./context.js";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Button, type Props} from "$lib/components/ui/button/index.js";
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import type {WithoutChildren} from 'bits-ui';
|
||||
import {getEmblaContext} from './context.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
import {Button, type Props} from '$lib/components/ui/button/index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
variant = 'outline',
|
||||
size = 'icon',
|
||||
...restProps
|
||||
}: WithoutChildren<Props> = $props();
|
||||
|
||||
const emblaCtx = getEmblaContext("<Carousel.Previous/>");
|
||||
const emblaCtx = getEmblaContext('<Carousel.Previous/>');
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"absolute size-8 touch-manipulation rounded-full",
|
||||
emblaCtx.orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
'absolute size-8 touch-manipulation rounded-full',
|
||||
emblaCtx.orientation === 'horizontal'
|
||||
? '-left-12 top-1/2 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className
|
||||
)}
|
||||
disabled={!emblaCtx.canScrollPrev}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
type CarouselAPI,
|
||||
type CarouselProps,
|
||||
type EmblaContext,
|
||||
setEmblaContext,
|
||||
} from "./context.js";
|
||||
import {cn} from "$lib/utils.js";
|
||||
setEmblaContext
|
||||
} from './context.js';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
opts = {},
|
||||
plugins = [],
|
||||
setApi = () => {
|
||||
},
|
||||
orientation = "horizontal",
|
||||
orientation = 'horizontal',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
@@ -31,7 +31,7 @@
|
||||
onInit,
|
||||
scrollSnaps: [],
|
||||
selectedIndex: 0,
|
||||
scrollTo,
|
||||
scrollTo
|
||||
});
|
||||
|
||||
setEmblaContext(carouselState);
|
||||
@@ -58,16 +58,16 @@
|
||||
$effect(() => {
|
||||
if (carouselState.api) {
|
||||
onSelect(carouselState.api);
|
||||
carouselState.api.on("select", onSelect);
|
||||
carouselState.api.on("reInit", onSelect);
|
||||
carouselState.api.on('select', onSelect);
|
||||
carouselState.api.on('reInit', onSelect);
|
||||
}
|
||||
});
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "ArrowLeft") {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
@@ -85,11 +85,11 @@
|
||||
|
||||
$effect(() => {
|
||||
return () => {
|
||||
carouselState.api?.off("select", onSelect);
|
||||
carouselState.api?.off('select', onSelect);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div {...restProps} aria-roledescription="carousel" class={cn("relative", className)} role="region">
|
||||
<div {...restProps} aria-roledescription="carousel" class={cn('relative', className)} role="region">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type {EmblaCarouselSvelteType} from "embla-carousel-svelte";
|
||||
import type emblaCarouselSvelte from "embla-carousel-svelte";
|
||||
import {getContext, hasContext, setContext} from "svelte";
|
||||
import type {WithElementRef} from "bits-ui";
|
||||
import type {HTMLAttributes} from "svelte/elements";
|
||||
import type {EmblaCarouselSvelteType} from 'embla-carousel-svelte';
|
||||
import type emblaCarouselSvelte from 'embla-carousel-svelte';
|
||||
import {getContext, hasContext, setContext} from 'svelte';
|
||||
import type {WithElementRef} from 'bits-ui';
|
||||
import type {HTMLAttributes} from 'svelte/elements';
|
||||
|
||||
export type CarouselAPI =
|
||||
NonNullable<NonNullable<EmblaCarouselSvelteType["$$_attributes"]>["on:emblaInit"]> extends (
|
||||
NonNullable<NonNullable<EmblaCarouselSvelteType['$$_attributes']>['on:emblaInit']> extends (
|
||||
evt: CustomEvent<infer CarouselAPI>
|
||||
) => void
|
||||
? CarouselAPI
|
||||
@@ -13,8 +13,8 @@ export type CarouselAPI =
|
||||
|
||||
type EmblaCarouselConfig = NonNullable<Parameters<typeof emblaCarouselSvelte>[1]>;
|
||||
|
||||
export type CarouselOptions = EmblaCarouselConfig["options"];
|
||||
export type CarouselPlugins = EmblaCarouselConfig["plugins"];
|
||||
export type CarouselOptions = EmblaCarouselConfig['options'];
|
||||
export type CarouselPlugins = EmblaCarouselConfig['plugins'];
|
||||
|
||||
////
|
||||
|
||||
@@ -22,14 +22,14 @@ export type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugins;
|
||||
setApi?: (api: CarouselAPI | undefined) => void;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
|
||||
|
||||
const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT");
|
||||
const EMBLA_CAROUSEL_CONTEXT = Symbol('EMBLA_CAROUSEL_CONTEXT');
|
||||
|
||||
export type EmblaContext = {
|
||||
api: CarouselAPI | undefined;
|
||||
orientation: "horizontal" | "vertical";
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
scrollNext: () => void;
|
||||
scrollPrev: () => void;
|
||||
canScrollNext: boolean;
|
||||
@@ -48,7 +48,7 @@ export function setEmblaContext(config: EmblaContext): EmblaContext {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getEmblaContext(name = "This component") {
|
||||
export function getEmblaContext(name = 'This component') {
|
||||
if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) {
|
||||
throw new Error(`${name} must be used within a <Carousel.Root> component`);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Root from "./carousel.svelte";
|
||||
import Content from "./carousel-content.svelte";
|
||||
import Item from "./carousel-item.svelte";
|
||||
import Previous from "./carousel-previous.svelte";
|
||||
import Next from "./carousel-next.svelte";
|
||||
import Root from './carousel.svelte';
|
||||
import Content from './carousel-content.svelte';
|
||||
import Item from './carousel-item.svelte';
|
||||
import Previous from './carousel-previous.svelte';
|
||||
import Next from './carousel-next.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -15,5 +15,5 @@ export {
|
||||
Content as CarouselContent,
|
||||
Item as CarouselItem,
|
||||
Previous as CarouselPrevious,
|
||||
Next as CarouselNext,
|
||||
Next as CarouselNext
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./progress.svelte";
|
||||
import Root from './progress.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Progress,
|
||||
Root as Progress
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Progress as ProgressPrimitive, type WithoutChildrenOrChild} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Progress as ProgressPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,11 +14,11 @@
|
||||
<ProgressPrimitive.Root
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
|
||||
class={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
|
||||
{value}
|
||||
>
|
||||
<div
|
||||
class="bg-primary h-full w-full flex-1 transition-all"
|
||||
class="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={`transform: translateX(-${100 - (100 * (value ?? 0)) / (max ?? 1)}%)`}
|
||||
></div>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Tooltip as TooltipPrimitive} from "bits-ui";
|
||||
import Content from "./tooltip-content.svelte";
|
||||
import {Tooltip as TooltipPrimitive} from 'bits-ui';
|
||||
import Content from './tooltip-content.svelte';
|
||||
|
||||
const Root = TooltipPrimitive.Root;
|
||||
const Trigger = TooltipPrimitive.Trigger;
|
||||
@@ -14,5 +14,5 @@ export {
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Trigger as TooltipTrigger,
|
||||
Provider as TooltipProvider,
|
||||
Provider as TooltipProvider
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {Tooltip as TooltipPrimitive} from "bits-ui";
|
||||
import {cn} from "$lib/utils.js";
|
||||
import {Tooltip as TooltipPrimitive} from 'bits-ui';
|
||||
import {cn} from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,7 +14,7 @@
|
||||
{...restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs",
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{sideOffset}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import type {User} from '$lib/types.js';
|
||||
import CheckmarkX from '$lib/components/checkmark-x.svelte';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import {Button} from "$lib/components/ui/button/index.js";
|
||||
import {env} from "$env/dynamic/public";
|
||||
import {toast} from "svelte-sonner";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import {Label} from "$lib/components/ui/label/index.js";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group/index.js";
|
||||
import {Input} from "$lib/components/ui/input/index.js";
|
||||
import {Button} from '$lib/components/ui/button/index.js';
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {toast} from 'svelte-sonner';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import {Label} from '$lib/components/ui/label/index.js';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||
import {Input} from '$lib/components/ui/input/index.js';
|
||||
|
||||
let {users}: { users: User[] } = $props();
|
||||
let sortedUsers = $derived(users.sort((a, b) => a.email.localeCompare(b.email)));
|
||||
@@ -30,7 +30,7 @@
|
||||
is_verified: selectedUser.is_verified,
|
||||
is_active: selectedUser.is_active,
|
||||
is_superuser: selectedUser.is_superuser,
|
||||
...newPassword !== "" && {password: newPassword}
|
||||
...(newPassword !== '' && {password: newPassword})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
toast.success(`User ${selectedUser.email} updated successfully.`);
|
||||
dialogOpen = false;
|
||||
|
||||
const idx = sortedUsers.findIndex(u => u.id === selectedUser.id);
|
||||
const idx = sortedUsers.findIndex((u) => u.id === selectedUser.id);
|
||||
if (idx !== -1) {
|
||||
sortedUsers[idx] = selectedUser;
|
||||
}
|
||||
@@ -51,7 +51,9 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
toast.error('Error updating user: ' + (error instanceof Error ? error.message : String(error)));
|
||||
toast.error(
|
||||
'Error updating user: ' + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
newPassword = '';
|
||||
}
|
||||
@@ -84,7 +86,13 @@
|
||||
<CheckmarkX state={user.is_superuser}/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button variant="secondary" onclick={() => {selectedUser=user; dialogOpen=true}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
selectedUser = user;
|
||||
dialogOpen = true;
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
@@ -93,23 +101,24 @@
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<Dialog.Content class="max-w-[600px] w-full rounded-lg shadow-lg bg-white p-6">
|
||||
<Dialog.Content class="w-full max-w-[600px] rounded-lg bg-white p-6 shadow-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-xl font-semibold mb-1">
|
||||
Edit user
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-gray-500 mb-4">
|
||||
<Dialog.Title class="mb-1 text-xl font-semibold">Edit user</Dialog.Title>
|
||||
<Dialog.Description class="mb-4 text-sm text-gray-500">
|
||||
Edit {selectedUser?.email}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="space-y-6">
|
||||
<!-- Verified -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium mb-1" for="verified">Verified</Label>
|
||||
<Label class="mb-1 block text-sm font-medium" for="verified">Verified</Label>
|
||||
<RadioGroup.Root
|
||||
class="flex gap-4"
|
||||
onValueChange={(v) => { if (selectedUser) selectedUser.is_verified = v === 'true'; }}
|
||||
value={selectedUser?.is_verified ? 'true' : 'false'}>
|
||||
onValueChange={(v) => {
|
||||
if (selectedUser) selectedUser.is_verified = v === 'true';
|
||||
}}
|
||||
value={selectedUser?.is_verified ? 'true' : 'false'}
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<RadioGroup.Item class="accent-green-600" id="verified-true" value="true"/>
|
||||
<Label class="text-sm" for="verified-true">True</Label>
|
||||
@@ -123,11 +132,14 @@
|
||||
<hr/>
|
||||
<!-- Active -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium mb-1" for="active">Active</Label>
|
||||
<Label class="mb-1 block text-sm font-medium" for="active">Active</Label>
|
||||
<RadioGroup.Root
|
||||
class="flex gap-4"
|
||||
onValueChange={(v) => { if (selectedUser) selectedUser.is_active = v === 'true'; }}
|
||||
value={selectedUser?.is_active ? 'true' : 'false'}>
|
||||
onValueChange={(v) => {
|
||||
if (selectedUser) selectedUser.is_active = v === 'true';
|
||||
}}
|
||||
value={selectedUser?.is_active ? 'true' : 'false'}
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<RadioGroup.Item class="accent-green-600" id="active-true" value="true"/>
|
||||
<Label class="text-sm" for="active-true">True</Label>
|
||||
@@ -141,11 +153,14 @@
|
||||
<hr/>
|
||||
<!-- Super User -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium mb-1" for="superuser">Admin</Label>
|
||||
<Label class="mb-1 block text-sm font-medium" for="superuser">Admin</Label>
|
||||
<RadioGroup.Root
|
||||
class="flex gap-4"
|
||||
onValueChange={(v) => { if (selectedUser) selectedUser.is_superuser = v === 'true'; }}
|
||||
value={selectedUser?.is_superuser ? 'true' : 'false'}>
|
||||
onValueChange={(v) => {
|
||||
if (selectedUser) selectedUser.is_superuser = v === 'true';
|
||||
}}
|
||||
value={selectedUser?.is_superuser ? 'true' : 'false'}
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<RadioGroup.Item class="accent-green-600" id="superuser-true" value="true"/>
|
||||
<Label class="text-sm" for="superuser-true">True</Label>
|
||||
@@ -158,7 +173,7 @@
|
||||
</div>
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium mb-1" for="superuser">Password</Label>
|
||||
<Label class="mb-1 block text-sm font-medium" for="superuser">Password</Label>
|
||||
<Input
|
||||
bind:value={newPassword}
|
||||
class="w-full"
|
||||
@@ -169,8 +184,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end gap-2">
|
||||
<Button onclick={() => dialogOpen = false} variant="outline">Cancel</Button>
|
||||
<Button onclick={() => (dialogOpen = false)} variant="outline">Cancel</Button>
|
||||
<Button onclick={() => saveUser()} variant="destructive">Save</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,89 +1,88 @@
|
||||
<script lang="ts">
|
||||
import {Button} from "$lib/components/ui/button/index.js";
|
||||
import {env} from "$env/dynamic/public";
|
||||
import {toast} from "svelte-sonner";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import {Label} from "$lib/components/ui/label/index.js";
|
||||
import {Input} from "$lib/components/ui/input/index.js";
|
||||
import {Button} from '$lib/components/ui/button/index.js';
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {toast} from 'svelte-sonner';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import {Label} from '$lib/components/ui/label/index.js';
|
||||
import {Input} from '$lib/components/ui/input/index.js';
|
||||
|
||||
let newPassword: string = $state('');
|
||||
let newEmail: string = $state('');
|
||||
let dialogOpen = $state(false);
|
||||
let newPassword: string = $state('');
|
||||
let newEmail: string = $state('');
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
async function saveUser() {
|
||||
try {
|
||||
const response = await fetch(`${env.PUBLIC_API_URL}/users/me`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
...newPassword !== "" && {password: newPassword},
|
||||
...newEmail !== "" && {password: newEmail}
|
||||
async function saveUser() {
|
||||
try {
|
||||
const response = await fetch(`${env.PUBLIC_API_URL}/users/me`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
...(newPassword !== '' && {password: newPassword}),
|
||||
...(newEmail !== '' && {password: newEmail})
|
||||
})
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Updated details successfully.`);
|
||||
dialogOpen = false;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error(`Failed to update user: ${response.statusText}`, errorText);
|
||||
toast.error(`Failed to update user: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
toast.error('Error updating user: ' + (error instanceof Error ? error.message : String(error)));
|
||||
} finally {
|
||||
newPassword = '';
|
||||
newEmail = '';
|
||||
}
|
||||
}
|
||||
if (response.ok) {
|
||||
toast.success(`Updated details successfully.`);
|
||||
dialogOpen = false;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error(`Failed to update user: ${response.statusText}`, errorText);
|
||||
toast.error(`Failed to update user: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
toast.error(
|
||||
'Error updating user: ' + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
newPassword = '';
|
||||
newEmail = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<Dialog.Trigger>
|
||||
<Button class="w-full" onclick={() => dialogOpen = true} variant="outline">
|
||||
Edit my details
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="max-w-[600px] w-full rounded-lg shadow-lg bg-white p-6">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-xl font-semibold mb-1">
|
||||
Edit User Details
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-gray-500 mb-4">
|
||||
Change your email or password. Leave fields empty to not change them.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="space-y-6">
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium mb-1" for="email">Email</Label>
|
||||
<Input
|
||||
bind:value={newEmail}
|
||||
class="w-full"
|
||||
id="email"
|
||||
placeholder="Keep empty to not change the email"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium mb-1" for="password">Password</Label>
|
||||
<Input
|
||||
bind:value={newPassword}
|
||||
class="w-full"
|
||||
id="password"
|
||||
placeholder="Keep empty to not change the password"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end gap-2">
|
||||
<Button onclick={() => saveUser()} variant="destructive">Save</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button class="w-full" onclick={() => (dialogOpen = true)} variant="outline">
|
||||
Edit my details
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="w-full max-w-[600px] rounded-lg bg-white p-6 shadow-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="mb-1 text-xl font-semibold">Edit User Details</Dialog.Title>
|
||||
<Dialog.Description class="mb-4 text-sm text-gray-500">
|
||||
Change your email or password. Leave fields empty to not change them.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="space-y-6">
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<Label class="mb-1 block text-sm font-medium" for="email">Email</Label>
|
||||
<Input
|
||||
bind:value={newEmail}
|
||||
class="w-full"
|
||||
id="email"
|
||||
placeholder="Keep empty to not change the email"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<Label class="mb-1 block text-sm font-medium" for="password">Password</Label>
|
||||
<Input
|
||||
bind:value={newPassword}
|
||||
class="w-full"
|
||||
id="password"
|
||||
placeholder="Keep empty to not change the password"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end gap-2">
|
||||
<Button onclick={() => saveUser()} variant="destructive">Save</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import type {LayoutProps} from './$types';
|
||||
import {setContext} from 'svelte';
|
||||
import {goto} from '$app/navigation';
|
||||
import {base} from '$app/paths';
|
||||
import {toast} from 'svelte-sonner';
|
||||
import type {LayoutProps} from './$types';
|
||||
import {setContext} from 'svelte';
|
||||
import {goto} from '$app/navigation';
|
||||
import {base} from '$app/paths';
|
||||
import {toast} from 'svelte-sonner';
|
||||
|
||||
let {data, children}: LayoutProps = $props();
|
||||
let {data, children}: LayoutProps = $props();
|
||||
console.log('Received User Data: ', data.user);
|
||||
if (!data.user.is_verified) {
|
||||
toast.info('Your account requires verification. Redirecting...');
|
||||
@@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar/>
|
||||
<AppSidebar/>
|
||||
<Sidebar.Inset>
|
||||
{@render children()}
|
||||
</Sidebar.Inset>
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
<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 RecommendedShowsCarousel from '$lib/components/recommended-shows-carousel.svelte';
|
||||
import LoadingBar from '$lib/components/loading-bar.svelte';
|
||||
import {base} from '$app/paths';
|
||||
import {page} from '$app/state';
|
||||
import type {MetaDataProviderShowSearchResult} from "$lib/types";
|
||||
|
||||
let recommendedShows: Promise<MetaDataProviderShowSearchResult[]> = page.data.tvRecommendations;
|
||||
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 RecommendedShowsCarousel from '$lib/components/recommended-shows-carousel.svelte';
|
||||
import LoadingBar from '$lib/components/loading-bar.svelte';
|
||||
import {base} from '$app/paths';
|
||||
import {page} from '$app/state';
|
||||
import type {MetaDataProviderShowSearchResult} from '$lib/types';
|
||||
|
||||
let recommendedShows: Promise<MetaDataProviderShowSearchResult[]> = page.data.tvRecommendations;
|
||||
</script>
|
||||
|
||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="{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>
|
||||
<div class="min-h-[100vh] flex-1 rounded-xl md:min-h-min items-center justify-center p-4">
|
||||
<div class="xl:max-w-[1200px] lg:max-w-[750px] md:max-w-[500px] sm:max-w-[200px] mx-auto">
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight text-center my-4">Trending Shows</h3>
|
||||
{#await recommendedShows}
|
||||
<LoadingBar/>
|
||||
{:then recommendations}
|
||||
<RecommendedShowsCarousel shows={recommendations}/>
|
||||
<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">
|
||||
<div class="mx-auto sm:max-w-[200px] md:max-w-[500px] lg:max-w-[750px] xl:max-w-[1200px]">
|
||||
<h3 class="my-4 scroll-m-20 text-center text-2xl font-semibold tracking-tight">
|
||||
Trending Shows
|
||||
</h3>
|
||||
{#await recommendedShows}
|
||||
<LoadingBar/>
|
||||
{:then recommendations}
|
||||
<RecommendedShowsCarousel shows={recommendations}/>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/await}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---
|
||||
<!---
|
||||
<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>
|
||||
|
||||
@@ -3,15 +3,14 @@ import type {PageLoad} from './$types';
|
||||
|
||||
const apiUrl = env.PUBLIC_API_URL;
|
||||
|
||||
|
||||
export const load: PageLoad = async ({fetch}) => {
|
||||
const response = await fetch(apiUrl + '/tv/recommended', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
const response = await fetch(apiUrl + '/tv/recommended', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
return {tvRecommendations: await response.json()};
|
||||
};
|
||||
return {tvRecommendations: await response.json()};
|
||||
};
|
||||
|
||||
@@ -1,53 +1,63 @@
|
||||
<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 {base} from '$app/paths';
|
||||
import logo from '$lib/images/logo.svg';
|
||||
import {PUBLIC_VERSION} from '$env/static/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 {base} from '$app/paths';
|
||||
import logo from '$lib/images/logo.svg';
|
||||
import {PUBLIC_VERSION} from '$env/static/public';
|
||||
</script>
|
||||
|
||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>About</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>About</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col items-center justify-center w-full py-12 px-4">
|
||||
<img alt="Media Manager Logo" class="w-24 h-24 mb-4" src={logo}/>
|
||||
<h1 class="text-4xl font-bold mb-2">About Media Manager</h1>
|
||||
<p class="text-lg text-center max-w-2xl mb-6">
|
||||
<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="text-sm text-muted-foreground mb-2">
|
||||
Version: v{PUBLIC_VERSION}
|
||||
</p>
|
||||
<div class="text-sm text-muted-foreground mb-6 my-6 flex items-center gap-2 lg:w-1/3 sm:w-1/2">
|
||||
<a class="flex items-center gap-2" href="https://www.themoviedb.org/" target="_blank">
|
||||
<img alt="TMDB Logo"
|
||||
class="w-20 h-auto"
|
||||
src="https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_2-d537fb228cf3ded904ef09b136fe3fec72548ebc1fea3fbbd1ad9e36364db38b.svg"/>
|
||||
<span>Metadata provided by TMDB. Please consider adding missing information or subscribing.</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground mb-6 my-6 flex items-center gap-2 lg:w-1/3 sm:w-1/2">
|
||||
<a class="flex items-center gap-2" href="https://thetvdb.com/subscribe" target="_blank">
|
||||
<img alt="TheTVDB Logo" class="w-20 h-auto" src="https://www.thetvdb.com/images/attribution/logo2.png"/>
|
||||
<span>Metadata provided by TheTVDB. Please consider adding missing information or subscribing.</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center justify-center px-4 py-12">
|
||||
<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 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">
|
||||
Version: v{PUBLIC_VERSION}
|
||||
</p>
|
||||
<div class="my-6 mb-6 flex items-center gap-2 text-sm text-muted-foreground 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"
|
||||
class="h-auto w-20"
|
||||
src="https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_2-d537fb228cf3ded904ef09b136fe3fec72548ebc1fea3fbbd1ad9e36364db38b.svg"
|
||||
/>
|
||||
<span
|
||||
>Metadata provided by TMDB. Please consider adding missing information or subscribing.</span
|
||||
>
|
||||
</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">
|
||||
<a class="flex items-center gap-2" href="https://thetvdb.com/subscribe" target="_blank">
|
||||
<img
|
||||
alt="TheTVDB Logo"
|
||||
class="h-auto w-20"
|
||||
src="https://www.thetvdb.com/images/attribution/logo2.png"
|
||||
/>
|
||||
<span
|
||||
>Metadata provided by TheTVDB. Please consider adding missing information or subscribing.</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
<script lang="ts">
|
||||
import UserTable from '$lib/components/user-data-table.svelte';
|
||||
import {page} from '$app/state';
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import {getContext} from "svelte";
|
||||
import UserSettings from '$lib/components/user-settings.svelte';
|
||||
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 {base} from "$app/paths";
|
||||
let currentUser = getContext("user")
|
||||
let users = page.data.users;
|
||||
import UserTable from '$lib/components/user-data-table.svelte';
|
||||
import {page} from '$app/state';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import {getContext} from 'svelte';
|
||||
import UserSettings from '$lib/components/user-settings.svelte';
|
||||
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 {base} from '$app/paths';
|
||||
|
||||
let currentUser = getContext('user');
|
||||
let users = page.data.users;
|
||||
</script>
|
||||
|
||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>Settings</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>Settings</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex w-full flex-1 flex-col gap-4 p-4 pt-0 max-w-[1000px] mx-auto">
|
||||
<h1 class="scroll-m-20 my-6 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Settings
|
||||
</h1>
|
||||
<Card.Root id="me">
|
||||
<Card.Header>
|
||||
<Card.Title>You</Card.Title>
|
||||
<Card.Description>Change your email or password</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserSettings/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{#if currentUser().is_superuser}
|
||||
<Card.Root id="users">
|
||||
<Card.Header>
|
||||
<Card.Title>Users</Card.Title>
|
||||
<Card.Description>Edit or delete users</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserTable bind:users={users}/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
<div class="mx-auto flex w-full max-w-[1000px] flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<h1 class="my-6 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Settings
|
||||
</h1>
|
||||
<Card.Root id="me">
|
||||
<Card.Header>
|
||||
<Card.Title>You</Card.Title>
|
||||
<Card.Description>Change your email or password</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserSettings/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{#if currentUser().is_superuser}
|
||||
<Card.Root id="users">
|
||||
<Card.Header>
|
||||
<Card.Title>Users</Card.Title>
|
||||
<Card.Description>Edit or delete users</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserTable bind:users/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,32 +2,32 @@ import {env} from '$env/dynamic/public';
|
||||
import type {PageLoad} from './$types';
|
||||
|
||||
export const load: PageLoad = async ({fetch}) => {
|
||||
try {
|
||||
const users = await fetch(env.PUBLIC_API_URL + "/users/all", {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
try {
|
||||
const users = await fetch(env.PUBLIC_API_URL + '/users/all', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!users.ok) {
|
||||
console.error(`Failed to fetch users: ${users.statusText}`);
|
||||
return {
|
||||
users: null,
|
||||
};
|
||||
}
|
||||
if (!users.ok) {
|
||||
console.error(`Failed to fetch users: ${users.statusText}`);
|
||||
return {
|
||||
users: null
|
||||
};
|
||||
}
|
||||
|
||||
const usersData = await users.json();
|
||||
console.log('Fetched users:', usersData);
|
||||
const usersData = await users.json();
|
||||
console.log('Fetched users:', usersData);
|
||||
|
||||
return {
|
||||
users: usersData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
return {
|
||||
users: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
return {
|
||||
users: usersData
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
return {
|
||||
users: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,79 +1,80 @@
|
||||
<script lang="ts">
|
||||
import {page} from '$app/state';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
||||
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
|
||||
import {getFullyQualifiedShowName} from '$lib/utils';
|
||||
import logo from '$lib/images/svelte-logo.svg';
|
||||
import LoadingBar from '$lib/components/loading-bar.svelte';
|
||||
import {page} from '$app/state';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
||||
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
|
||||
import {getFullyQualifiedShowName} from '$lib/utils';
|
||||
import logo from '$lib/images/svelte-logo.svg';
|
||||
import LoadingBar from '$lib/components/loading-bar.svelte';
|
||||
|
||||
let tvShowsPromise = page.data.tvShows;
|
||||
let tvShowsPromise = page.data.tvShows;
|
||||
</script>
|
||||
|
||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.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="/dashboard">MediaManager</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>Shows</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
</div>
|
||||
</header>
|
||||
{#snippet loadingbar()}
|
||||
<div class="flex flex-col items-center justify-center w-full col-span-full py-16 animate-fade-in">
|
||||
<div class="w-1/2 max-w-xs">
|
||||
<LoadingBar/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animate-fade-in col-span-full flex w-full flex-col items-center justify-center py-16">
|
||||
<div class="w-1/2 max-w-xs">
|
||||
<LoadingBar/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
<div 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">
|
||||
{#await tvShowsPromise}
|
||||
{@render loadingbar()}
|
||||
{:then tvShowsJson}
|
||||
{#await tvShowsJson.json()}
|
||||
{@render loadingbar()}
|
||||
|
||||
{:then tvShows}
|
||||
{#each tvShows as show}
|
||||
<a href={'/dashboard/tv/' + show.id}>
|
||||
<Card.Root class="h-full ">
|
||||
<Card.Header>
|
||||
<Card.Title class="h-6 truncate">{getFullyQualifiedShowName(show)}</Card.Title>
|
||||
<Card.Description class="truncate">{show.overview}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<img
|
||||
class="aspect-9/16 center h-auto max-w-full rounded-lg object-cover"
|
||||
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show.id}.jpg`)}
|
||||
alt="{getFullyQualifiedShowName(show)}'s Poster Image"
|
||||
on:error={(e) => {
|
||||
<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"
|
||||
>
|
||||
{#await tvShowsPromise}
|
||||
{@render loadingbar()}
|
||||
{:then tvShowsJson}
|
||||
{#await tvShowsJson.json()}
|
||||
{@render loadingbar()}
|
||||
{:then tvShows}
|
||||
{#each tvShows as show}
|
||||
<a href={'/dashboard/tv/' + show.id}>
|
||||
<Card.Root class="h-full ">
|
||||
<Card.Header>
|
||||
<Card.Title class="h-6 truncate">{getFullyQualifiedShowName(show)}</Card.Title>
|
||||
<Card.Description class="truncate">{show.overview}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<img
|
||||
class="aspect-9/16 center h-auto max-w-full rounded-lg object-cover"
|
||||
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show.id}.jpg`)}
|
||||
alt="{getFullyQualifiedShowName(show)}'s Poster Image"
|
||||
on:error={(e) => {
|
||||
e.target.src = logo;
|
||||
}}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</a>
|
||||
{/each}
|
||||
{/await}
|
||||
{/await}
|
||||
</div>
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</a>
|
||||
{/each}
|
||||
{/await}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {setContext} from 'svelte';
|
||||
import type {LayoutProps} from './$types';
|
||||
import {setContext} from 'svelte';
|
||||
import type {LayoutProps} from './$types';
|
||||
|
||||
let {data, children}: LayoutProps = $props();
|
||||
let {data, children}: LayoutProps = $props();
|
||||
|
||||
const showData = $derived(data.showData);
|
||||
setContext('show', () => showData);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import {env} from '$env/dynamic/public';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
||||
import {goto} from '$app/navigation';
|
||||
import {ImageOff} from 'lucide-svelte';
|
||||
import {goto} from '$app/navigation';
|
||||
import {ImageOff} from 'lucide-svelte';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import {getContext} from 'svelte';
|
||||
import type {RichShowTorrent, Show, User} from '$lib/types.js';
|
||||
import {getFullyQualifiedShowName} from '$lib/utils';
|
||||
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
|
||||
import {getContext} from 'svelte';
|
||||
import type {RichShowTorrent, Show, User} from '$lib/types.js';
|
||||
import {getFullyQualifiedShowName} from '$lib/utils';
|
||||
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
|
||||
import DownloadSeasonDialog from '$lib/components/download-season-dialog.svelte';
|
||||
import CheckmarkX from '$lib/components/checkmark-x.svelte';
|
||||
import {page} from '$app/state';
|
||||
import {page} from '$app/state';
|
||||
import TorrentTable from '$lib/components/torrent-table.svelte';
|
||||
import RequestSeasonDialog from '$lib/components/request-season-dialog.svelte';
|
||||
|
||||
@@ -23,22 +23,22 @@
|
||||
|
||||
<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"/>
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>{getFullyQualifiedShowName(show())}</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
@@ -54,15 +54,15 @@
|
||||
<div class="max-h-50% w-1/3 max-w-sm rounded-xl bg-muted/50">
|
||||
{#if show().id}
|
||||
<img
|
||||
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
|
||||
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
|
||||
alt="{show().name}'s Poster Image"
|
||||
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
|
||||
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
|
||||
alt="{show().name}'s Poster Image"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
|
||||
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
|
||||
>
|
||||
<ImageOff size={48}/>
|
||||
<ImageOff size={48}/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -72,12 +72,12 @@
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-full flex-auto flex-col items-center justify-center gap-2 rounded-xl bg-muted/50 p-4"
|
||||
class="flex h-full flex-auto flex-col items-center justify-center gap-2 rounded-xl bg-muted/50 p-4"
|
||||
>
|
||||
{#if user().is_superuser}
|
||||
<DownloadSeasonDialog show={show()}/>
|
||||
<DownloadSeasonDialog show={show()}/>
|
||||
{/if}
|
||||
<RequestSeasonDialog show={show()}/>
|
||||
<RequestSeasonDialog show={show()}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
|
||||
@@ -96,12 +96,12 @@
|
||||
{#if show().seasons.length > 0}
|
||||
{#each show().seasons as season (season.id)}
|
||||
<Table.Row
|
||||
link={true}
|
||||
onclick={() => goto('/dashboard/tv/' + show().id + '/' + season.number)}
|
||||
link={true}
|
||||
onclick={() => goto('/dashboard/tv/' + show().id + '/' + season.number)}
|
||||
>
|
||||
<Table.Cell class="min-w-[10px] font-medium">{season.number}</Table.Cell>
|
||||
<Table.Cell class="min-w-[10px] font-medium">
|
||||
<CheckmarkX state={season.downloaded}/>
|
||||
<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>
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<TorrentTable torrents={torrents.torrents}/>
|
||||
<TorrentTable torrents={torrents.torrents}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
const SeasonNumber = page.params.SeasonNumber;
|
||||
let seasonFiles: PublicSeasonFile[] = $state(page.data.files);
|
||||
let show: Show = getContext('show');
|
||||
let season: Season = $derived(show().seasons.find((item) => item.number === parseInt(SeasonNumber)));
|
||||
let season: Season = $derived(
|
||||
show().seasons.find((item) => item.number === parseInt(SeasonNumber))
|
||||
);
|
||||
|
||||
console.log('loaded files', seasonFiles);
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {page} from '$app/state';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import {page} from '$app/state';
|
||||
import {Separator} from '$lib/components/ui/separator/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
||||
import type {SeasonRequest} from '$lib/types';
|
||||
import type {SeasonRequest} from '$lib/types';
|
||||
import RequestsTable from '$lib/components/season-requests-table.svelte';
|
||||
|
||||
let requests: SeasonRequest[] = $state(page.data.requestsData);
|
||||
@@ -12,22 +12,22 @@
|
||||
<!-- TODO: ADD DIALOGUE TO MODIFY REQUEST -->
|
||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Sidebar.Trigger class="-ml-1"/>
|
||||
<Separator class="mr-2 h-4" orientation="vertical"/>
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Separator class="hidden md:block"/>
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>TV Torrents</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
@@ -40,5 +40,5 @@
|
||||
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Season Requests
|
||||
</h1>
|
||||
<RequestsTable bind:requests/>
|
||||
<RequestsTable bind:requests/>
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
<script lang="ts">
|
||||
import LoginForm from '$lib/components/login-form.svelte';
|
||||
import logo from '$lib/images/logo.svg';
|
||||
import background from '$lib/images/pawel-czerwinski-NTYYL9Eb9y8-unsplash.jpg';
|
||||
import {toOptimizedURL} from "sveltekit-image-optimize/components";
|
||||
import {page} from '$app/state';
|
||||
import LoginForm from '$lib/components/login-form.svelte';
|
||||
import logo from '$lib/images/logo.svg';
|
||||
import background from '$lib/images/pawel-czerwinski-NTYYL9Eb9y8-unsplash.jpg';
|
||||
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
|
||||
import {page} from '$app/state';
|
||||
|
||||
let oauthProvider = page.data.oauthProvider;
|
||||
let oauthProvider = page.data.oauthProvider;
|
||||
</script>
|
||||
|
||||
<div class="grid min-h-svh lg:grid-cols-2">
|
||||
<div class="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div class="flex justify-center gap-2 md:justify-start">
|
||||
<a class="flex items-center gap-2 font-medium" href="##">
|
||||
<div
|
||||
class="text-primary-foreground flex size-16 items-center justify-center rounded-md"
|
||||
>
|
||||
<img alt="MediaManager Logo" class="size-12" src={logo}/>
|
||||
</div>
|
||||
<h1 class="scale-110">Media Manager</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<div class="w-full max-w-xs">
|
||||
<LoginForm oauthProvider={oauthProvider}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative hidden lg:block">
|
||||
<img
|
||||
alt="background"
|
||||
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.8] rounded-l-3xl "
|
||||
src="{toOptimizedURL(background)}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div class="flex justify-center gap-2 md:justify-start">
|
||||
<a class="flex items-center gap-2 font-medium" href="##">
|
||||
<div class="flex size-16 items-center justify-center rounded-md text-primary-foreground">
|
||||
<img alt="MediaManager Logo" class="size-12" src={logo}/>
|
||||
</div>
|
||||
<h1 class="scale-110">Media Manager</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<div class="w-full max-w-xs">
|
||||
<LoginForm {oauthProvider}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative hidden lg:block">
|
||||
<img
|
||||
alt="background"
|
||||
class="absolute inset-0 h-full w-full rounded-l-3xl object-cover dark:brightness-[0.8]"
|
||||
src={toOptimizedURL(background)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,14 @@ import type {PageLoad} from './$types';
|
||||
|
||||
const apiUrl = env.PUBLIC_API_URL;
|
||||
|
||||
|
||||
export const load: PageLoad = async ({fetch}) => {
|
||||
const response = await fetch(apiUrl + '/auth/metadata', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
const response = await fetch(apiUrl + '/auth/metadata', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
return {oauthProvider: await response.json()};
|
||||
};
|
||||
return {oauthProvider: await response.json()};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user