From 75360ff603cb8d784a8cfdf778f76b8a8d6ec41e Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 11 Aug 2026 20:06:23 -0400 Subject: [PATCH] feat: add book cover component and detail-card list view --- .../components/view/book-actions-menu.svelte | 99 ++++++++ .../src/lib/components/view/book-cover.svelte | 84 +++++++ .../src/lib/components/view/book-rows.svelte | 231 ++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 frontend/src/lib/components/view/book-actions-menu.svelte create mode 100644 frontend/src/lib/components/view/book-cover.svelte create mode 100644 frontend/src/lib/components/view/book-rows.svelte diff --git a/frontend/src/lib/components/view/book-actions-menu.svelte b/frontend/src/lib/components/view/book-actions-menu.svelte new file mode 100644 index 0000000..ebdd3ff --- /dev/null +++ b/frontend/src/lib/components/view/book-actions-menu.svelte @@ -0,0 +1,99 @@ + + + + + + + + + + + {#if book.files.length > 0} + openInReader(book.files[0])}> + + Read + + bookOps.downloadBookFile(book.id, book.files[0].id, book.files[0].filename)} + > + + Download + + + {/if} + + { + bookOps.bookToEdit = book; + bookOps.editDialogOpen = true; + }} + > + + Edit + + + { + if (!book.progress?.completed) await bookOps.markBooksAsComplete([book.id]); + else await bookOps.markBooksAsIncomplete([book.id]); + }} + > + + {book.progress?.completed ? 'Mark as unfinished' : 'Mark as finished'} + + + + + { + 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 + + + + diff --git a/frontend/src/lib/components/view/book-cover.svelte b/frontend/src/lib/components/view/book-cover.svelte new file mode 100644 index 0000000..903a4b9 --- /dev/null +++ b/frontend/src/lib/components/view/book-cover.svelte @@ -0,0 +1,84 @@ + + + + + {#if src && !failed} + + + Cover of {book.title} (failed = true)} + class="h-full w-auto object-contain" + style="max-width: {Math.round(height * 0.95)}px;" + /> + + {#if book.progress?.percentage} + + + + {/if} + + {:else} + + + {book.title} + {#if authors} + {authors} + {/if} + + {/if} + diff --git a/frontend/src/lib/components/view/book-rows.svelte b/frontend/src/lib/components/view/book-rows.svelte new file mode 100644 index 0000000..a30bcce --- /dev/null +++ b/frontend/src/lib/components/view/book-rows.svelte @@ -0,0 +1,231 @@ + + + +
+ {#each books as book (book.id)} + {@const selected = selectionState.isSelected(book.id)} + {@const state = readState(book)} + + +
handleCardClick(event, book)} + class="group flex gap-3.5 rounded-lg border bg-card p-3 transition-colors {selected + ? 'border-primary bg-accent' + : 'border-border/60 hover:border-border'} {selectionState.selectionModeActive + ? 'cursor-pointer' + : ''}" + > + + + + +
+
+
+ + {book.title} + +

{authors(book)}

+
+ + + +
+ +
+ {#if year(book)} + + {year(book)} + + {/if} + {#if book.files.length > 0} + {formats(book)} + {/if} + {#each book.tags.slice(0, TAG_LIMIT) as tag (tag.id)} + {tag.name} + {/each} + {#if book.tags.length > TAG_LIMIT} + + +{book.tags.length - TAG_LIMIT} + + {/if} +
+ +
+ +
+ {#if book.files.length > 0} + + + openInReader(book, book.files[0])} + > + + +

Read

+
+
+ + + + + bookOps.downloadBookFile(book.id, book.files[0].id, book.files[0].filename)} + > + + +

Download

+
+
+ {/if} + + +
+ + {#if state.text} + + {state.text} + + {/if} +
+
+
+ {/each} +