feat: merge books from the duplicates screen and the toolbar

A workbench with the folded records in a rail, the survivor's fields live and
editable beside them, and per-field actions chosen by what kind of field it is.
Fields the records agree on stay out of the way. Reachable from a duplicate
group and from the selection toolbar, which is the only way to merge a pair the
detector never proposed.
This commit is contained in:
2026-08-16 20:21:19 -04:00
parent 22539644a9
commit 699a1a7fa2
8 changed files with 781 additions and 32 deletions
+14
View File
@@ -30,6 +30,19 @@ export type PossibleDuplicate = components['schemas']['PossibleDuplicateRead'];
/** Books the library holds that all look like copies of one book. */
export type DuplicateBookGroup = components['schemas']['DuplicateBookGroupRead'];
/** The metadata a reader resolved while merging, or while reviewing a provider. */
export type BookMetadataUpdate = components['schemas']['BookMetadataUpdate'];
/** Mirrors BookMerge in backend/src/chitai/schemas/book.py */
export const bookMergeSchema = z.object({
library_id: z.coerce.number(),
survivor_id: z.coerce.number(),
merged_ids: z.array(z.coerce.number()).min(1, 'Pick at least one book to fold in'),
// Passed through untouched — the backend validates it as BookMetadataUpdate, and
// duplicating that shape here would be two places to keep in step for no gain.
metadata: z.record(z.string(), z.unknown()).optional()
});
/** Mirrors DuplicateDismissal in backend/src/chitai/schemas/book.py */
export const duplicateDismissalSchema = z.object({
book_a_id: z.coerce.number(),
@@ -128,4 +141,5 @@ export type BookEditMetadata = z.infer<typeof editBookMetadataSchema>;
export type DeleteBook = z.infer<typeof deleteBooksSchema>;
export type DeleteBookFiles = z.infer<typeof deleteBookFilesSchema>;
export type DuplicateDismissal = z.infer<typeof duplicateDismissalSchema>;
export type BookMerge = z.infer<typeof bookMergeSchema>;
export type UpdateBookProgress = z.infer<typeof updateBookProgressSchema>;