Files
chitai/frontend/src/lib/components/forms/books-upload.svelte
T
2025-12-04 00:33:37 -05:00

162 lines
5.0 KiB
Svelte

<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Field from '$lib/components/ui/field/index.js';
import { Button } from '$lib/components/ui/button';
import * as NativeSelect from '$lib/components/ui/native-select/index.js';
import {
displaySize,
FileDropZone,
type FileDropZoneProps
} from '$lib/components/ui/file-drop-zone';
import { X } from '@lucide/svelte';
import { toast } from 'svelte-sonner';
import { Switch } from '$lib/components/ui/switch/index';
import { tick } from 'svelte';
import { Spinner } from '$lib/components/ui/spinner/index';
import { getLibraryState } from '$lib/state/library.svelte';
import { uploadBooks } from '$lib/api';
import type { Book, PaginatedResponse } from '$lib/schema';
import { goto } from '$app/navigation';
let { open = $bindable() }: { open?: boolean } = $props();
let libraryState = getLibraryState();
$effect(() => {
uploadBooks.fields.library_id.set(libraryState.activeLibrary!.id);
});
let files = $derived(uploadBooks.fields.files.value() ?? []);
let autoUploadOnDrop = $state(true);
let navigateOnUpload = $state(true);
let formEl = $state<HTMLFormElement>();
const onUpload: FileDropZoneProps['onUpload'] = async (uploadedFiles) => {
uploadBooks.fields.files.set([...Array.from(files), ...uploadedFiles]);
if (autoUploadOnDrop && files.length > 0) {
await tick();
formEl?.requestSubmit();
}
};
const onFileRejected: FileDropZoneProps['onFileRejected'] = async ({ reason, file }) => {
toast.error(`${file.name} failed to upload!`, { description: reason });
};
function navigateToBooks(books: PaginatedResponse<Book>) {
open = false;
let libraryId = books.items[0].library_id;
libraryState.setActive(libraryId);
if (books.items.length === 1) {
goto(`/book/${books.items[0].id}`);
} else {
goto(`/library/${libraryId}/view?orderBy=created_at&sortOrder=desc`);
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content>
{#if uploadBooks.pending}
<div class="flex flex-col items-center gap-4">
<span class="text-lg font-semibold"
>Uploading {uploadBooks.fields.files.value().length} files...</span
>
<Spinner class="scale-150" />
</div>
{:else}
<Dialog.Header>
<Dialog.Title>Upload Books</Dialog.Title>
</Dialog.Header>
<form
{...uploadBooks.enhance(async ({ submit, form }) => {
try {
await submit();
// Check if there are any validation issues
const issues = uploadBooks.fields.allIssues();
if (issues && issues.length > 0) {
return;
}
// Update library book count
const count = uploadBooks.result.total
libraryState.libraries.find(lib => uploadBooks.fields.library_id.value() == lib.id.toString())!.total += count
// Reset the files field
uploadBooks.fields.files.set([]);
toast.success('Books successfully uploaded!');
if (navigateOnUpload) {
navigateToBooks(uploadBooks.result);
}
} catch (error) {
console.error('Failed to upload book: ', error);
toast.error('Failed to upload books');
}
})}
bind:this={formEl}
enctype="multipart/form-data"
class="flex w-full flex-col gap-2 p-4"
>
<!-- Library select field -->
<Field.Label for="library_id">Select Library</Field.Label>
<NativeSelect.Root {...uploadBooks.fields.library_id.as('select')} class="w-36">
{#each libraryState.libraries as library}
<NativeSelect.Option value={library.id}>
{library.name}
</NativeSelect.Option>
{/each}
</NativeSelect.Root>
<FileDropZone
{onUpload}
{onFileRejected}
directory={true}
accept=".pdf,.epub,.mobi,application/pdf,application/epub+zip,application/x-mobipocket-ebook"
sublabel="Only PDF, EPUB, and MOBI files supported"
/>
<input class="hidden" {...uploadBooks.fields.files.as('file multiple')} />
<div class="flex max-h-[300px] flex-col gap-2 overflow-y-auto">
{#each files as file, idx}
<div class="flex place-items-center justify-between gap-2">
<div class="flex flex-col">
<span>{file.name}</span>
<span class="text-xs text-muted-foreground">{displaySize(file.size)}</span>
</div>
<Button
variant="outline"
size="icon"
onclick={() => {
uploadBooks.fields.files.set([
...Array.from(files).slice(0, idx),
...Array.from(files).slice(idx + 1)
]);
}}
>
<X />
</Button>
</div>
{/each}
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<Switch bind:checked={autoUploadOnDrop} />
<Field.Label for="auto-upload-on-drop">Auto upload on file drop</Field.Label>
<Button type="submit" class="ml-auto w-fit">Upload</Button>
</div>
<div class="flex items-center gap-2">
<Switch bind:checked={navigateOnUpload} />
<Field.Label for="navigate-to-book">Navigate to book on upload</Field.Label>
</div>
</div>
</form>
{/if}
</Dialog.Content>
</Dialog.Root>