feat: review and dismiss duplicate books

Adds the per-library review screen, a muted note in the upload tray for a book
that may already be held, and the remote functions behind them. The tray note
is deliberately weaker than the file-level one, which means something stronger.
This commit is contained in:
2026-08-15 21:55:20 -04:00
parent 5047277845
commit fc6b97bf38
8 changed files with 465 additions and 12 deletions
+43
View File
@@ -8,8 +8,10 @@ import {
deleteBooksSchema,
editBookMetadataSchema,
updateBookProgressSchema,
duplicateDismissalSchema,
type Book,
type BooksUploadResult,
type DuplicateBookGroup,
bookFilesUpload
} from '$lib/schema/index';
import { stringCoerce, type PaginatedResponse } from '$lib/schema/common';
@@ -153,6 +155,47 @@ export const deleteBookFiles = command(deleteBookFilesSchema, async ({ book_id,
}
});
/**
* Books already in the library that look like copies of one another.
*
* Metadata only, so every group is a question rather than a verdict — which is why
* the screen it feeds offers a way to disagree.
*/
export const listDuplicateBooks = query(
stringCoerce,
async (libraryId): Promise<DuplicateBookGroup[]> => {
const { locals } = getRequestEvent();
const response = await locals.api.get(`/books/duplicate-books?library_id=${libraryId}`);
if (!response.ok) error(response.status, detailOf(await response.text()));
return await response.json();
}
);
/** Record that two books are not the same book, so the pair stops being proposed. */
export const dismissDuplicateBooks = command(duplicateDismissalSchema, async (data) => {
const { locals } = getRequestEvent();
const response = await locals.api.post('/books/duplicate-books/dismissals', data);
if (!response.ok) error(response.status, detailOf(await response.text()));
});
/** Undo a dismissal, so the pair is proposed again. */
export const restoreDuplicateBooks = command(duplicateDismissalSchema, async (data) => {
const { locals } = getRequestEvent();
const params = createQueryParams(data);
const response = await locals.api.delete(
`/books/duplicate-books/dismissals?${params.toString()}`
);
if (!response.ok) error(response.status, detailOf(await response.text()));
});
export const updateBookProgress = command(
updateBookProgressSchema,
async ({ book_ids, ...data }) => {