Dead imports and locals removed, each blocks keyed, `any` narrowed to unknown. Two context setters kept their calls and lost only the unused binding; the settings redirect no longer awaits a parent whose data it discards. A leading underscore now marks a binding that only holds a position.
102 lines
3.5 KiB
Svelte
102 lines
3.5 KiB
Svelte
<script lang="ts">
|
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
|
import { Circle, Pencil, EllipsisVertical } from '@lucide/svelte';
|
|
|
|
import type { Book } from '$lib/schema';
|
|
import BookThumbnail from './book-thumbnail.svelte';
|
|
import { getBookSelectionState } from '$lib/state/bookSelection.svelte';
|
|
import { getBookOperationsState } from '$lib/state/bookOperations.svelte';
|
|
import { getLibraryState } from '$lib/state/library.svelte';
|
|
import { getBookshelfState } from '$lib/state/bookshelf.svelte';
|
|
|
|
let { books }: { books: Book[] } = $props();
|
|
|
|
const libraryState = getLibraryState();
|
|
const selectedState = getBookSelectionState();
|
|
const bookOps = getBookOperationsState();
|
|
const bookshelfState = getBookshelfState();
|
|
</script>
|
|
|
|
<div class="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4 p-2">
|
|
{#each books as book (book.id)}
|
|
<div class="group relative">
|
|
<div
|
|
class="absolute top-2 right-2 z-50 {selectedState.isSelected(book.id)
|
|
? 'opacity-100'
|
|
: 'opacity-0'} transition-opacity group-hover:opacity-100"
|
|
>
|
|
<Circle
|
|
onclick={() => {
|
|
selectedState.toggleSelection(book);
|
|
}}
|
|
class="{selectedState.isSelected(book.id)
|
|
? 'scale-110 text-star'
|
|
: 'scale-75 text-white'} transition-all hover:scale-110 hover:cursor-pointer hover:text-star"
|
|
/>
|
|
</div>
|
|
{#if selectedState.isSelected(book.id)}
|
|
<div
|
|
class="absolute top-2 right-2 z-40 {selectedState.isSelected(book.id)
|
|
? 'opacity-100'
|
|
: 'opacity-0'} transition-opacity group-hover:opacity-100"
|
|
>
|
|
<Circle
|
|
class="scale-50 fill-star text-star transition-all hover:scale-75 hover:cursor-pointer hover:text-star"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
<BookThumbnail {book} />
|
|
{#if !selectedState.selectionModeActive}
|
|
<div
|
|
class="absolute bottom-20 left-1 z-50 inline-flex items-center opacity-0 transition-opacity group-hover:opacity-100"
|
|
>
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
<EllipsisVertical
|
|
class="scale-75 text-white transition-all hover:scale-110 hover:cursor-pointer hover:text-star"
|
|
/>
|
|
</DropdownMenu.Trigger>
|
|
<DropdownMenu.Content>
|
|
<DropdownMenu.Group>
|
|
<DropdownMenu.Item
|
|
onclick={async () => {
|
|
if (!book.progress?.completed) await bookOps.markBooksAsComplete([book.id]);
|
|
else await bookOps.markBooksAsIncomplete([book.id]);
|
|
}}
|
|
>{!book?.progress?.completed
|
|
? 'Mark as finished'
|
|
: 'Mark as unfinished'}</DropdownMenu.Item
|
|
>
|
|
<DropdownMenu.Item
|
|
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;
|
|
}}
|
|
>
|
|
Delete
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Group>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
</div>
|
|
<div
|
|
class="absolute right-2 bottom-20 z-50 inline-flex items-center opacity-0 transition-opacity group-hover:opacity-100"
|
|
>
|
|
<Pencil
|
|
class="scale-75 text-white transition-all hover:scale-110 hover:cursor-pointer hover:text-star"
|
|
onclick={() => {
|
|
bookOps.bookToEdit = book;
|
|
bookOps.editDialogOpen = true;
|
|
}}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|