feat: add book cover component and detail-card list view

This commit is contained in:
2026-08-11 20:08:53 -04:00
parent 87fff20d72
commit 75360ff603
3 changed files with 414 additions and 0 deletions
@@ -0,0 +1,99 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { getBookOperationsState } from '$lib/state/bookOperations.svelte';
import { getLibraryState } from '$lib/state/library.svelte';
import { getBookshelfState } from '$lib/state/bookshelf.svelte';
import { getFileType } from '$lib/utils';
import {
BookOpenCheck,
BookOpenText,
Download,
EllipsisVertical,
Pencil,
Trash2
} from '@lucide/svelte';
import type { Book, BookFile } from '$lib/schema';
let { book, class: className = '' }: { book: Book; class?: string } = $props();
const bookOps = getBookOperationsState();
const libraryState = getLibraryState();
const bookshelfState = getBookshelfState();
function openInReader(file: BookFile) {
const type = getFileType(file.filename);
if (type === 'EPUB' || type === 'PDF')
window.open(`/book/${book.id}/read/${type.toLowerCase()}/${file.id}`, '_blank', 'noopener');
}
</script>
<!--
The per-book overflow menu, shared so the grid, list and table cannot drift
apart. data-row-control keeps a click here from toggling row selection.
-->
<DropdownMenu.Root>
<DropdownMenu.Trigger
data-row-control
aria-label="More actions for {book.title}"
class="{buttonVariants({ variant: 'ghost', size: 'icon' })} {className}"
>
<EllipsisVertical class="size-4" />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" data-row-control>
<DropdownMenu.Group>
{#if book.files.length > 0}
<DropdownMenu.Item onclick={() => openInReader(book.files[0])}>
<BookOpenText class="size-4" />
Read
</DropdownMenu.Item>
<DropdownMenu.Item
onclick={() => bookOps.downloadBookFile(book.id, book.files[0].id, book.files[0].filename)}
>
<Download class="size-4" />
Download
</DropdownMenu.Item>
<DropdownMenu.Separator />
{/if}
<DropdownMenu.Item
onclick={() => {
bookOps.bookToEdit = book;
bookOps.editDialogOpen = true;
}}
>
<Pencil class="size-4" />
Edit
</DropdownMenu.Item>
<DropdownMenu.Item
onclick={async () => {
if (!book.progress?.completed) await bookOps.markBooksAsComplete([book.id]);
else await bookOps.markBooksAsIncomplete([book.id]);
}}
>
<BookOpenCheck class="size-4" />
{book.progress?.completed ? 'Mark as unfinished' : 'Mark as finished'}
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
class="text-destructive"
onclick={() => {
bookOps.deleteDialogTitle = `Delete "${book.title}"?`;
bookOps.deleteFn = async (deleteFiles: boolean) => {
await bookOps.deleteBooks([book.id], deleteFiles);
libraryState.activeLibrary!.total!--;
bookshelfState.deletedBooks([book]);
};
bookOps.deleteDialogOpen = true;
}}
>
<Trash2 class="size-4" />
Delete
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>