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}
+
+
+
(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'
+ : ''}"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#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}
+