162 lines
5.0 KiB
Svelte
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>
|