From b67468927317f899b48c168965fd8fe4bcd531cc Mon Sep 17 00:00:00 2001 From: bealqiu Date: Wed, 3 Jun 2026 19:11:37 +0800 Subject: [PATCH] feat: add per-book reading data reset --- .../src/components/library/BookCard.tsx | 5 +- .../library/BookCardActionSheet.tsx | 62 ++++++++++- .../app-expo/src/screens/LibraryScreen.tsx | 3 + packages/app-expo/src/stores/library-store.ts | 18 +++ packages/app/src/components/home/BookCard.tsx | 104 +++++++++++++++--- packages/app/src/lib/db/database.ts | 1 + packages/app/src/stores/library-store.ts | 17 +++ .../src/db/__tests__/book-queries.test.ts | 96 +++++++++++++--- .../src/db/__tests__/session-queries.test.ts | 35 +++++- packages/core/src/db/book-queries.ts | 8 +- packages/core/src/db/database.ts | 2 + packages/core/src/db/index.ts | 2 + packages/core/src/db/session-queries.ts | 16 ++- 13 files changed, 326 insertions(+), 43 deletions(-) diff --git a/packages/app-expo/src/components/library/BookCard.tsx b/packages/app-expo/src/components/library/BookCard.tsx index 9beaa9d7..f6a06c77 100644 --- a/packages/app-expo/src/components/library/BookCard.tsx +++ b/packages/app-expo/src/components/library/BookCard.tsx @@ -50,6 +50,7 @@ interface BookCardProps { book: Book; onOpen: (book: Book) => void; onDelete: (bookId: string, options?: { preserveData?: boolean }) => void; + onResetReadingData?: (bookId: string) => void; onManageTags?: (book: Book) => void; onVectorize?: (book: Book) => void; isVectorizing?: boolean; @@ -67,6 +68,7 @@ export const BookCard = memo(function BookCard({ book, onOpen, onDelete, + onResetReadingData, onManageTags, onVectorize, isVectorizing, @@ -115,8 +117,6 @@ export const BookCard = memo(function BookCard({ }, [book.meta.coverUrl]); const progressPct = Math.round(book.progress * 100); - const hasCover = resolvedCoverUrl && !imageError; - const vecPct = vectorProgress ? vectorProgress.totalChunks > 0 ? Math.round((vectorProgress.processedChunks / vectorProgress.totalChunks) * 100) @@ -405,6 +405,7 @@ export const BookCard = memo(function BookCard({ onManageTags={onManageTags} onVectorize={onVectorize} onDelete={onDelete} + onResetReadingData={onResetReadingData} /> ); diff --git a/packages/app-expo/src/components/library/BookCardActionSheet.tsx b/packages/app-expo/src/components/library/BookCardActionSheet.tsx index 6cdbbdf3..77f32bba 100644 --- a/packages/app-expo/src/components/library/BookCardActionSheet.tsx +++ b/packages/app-expo/src/components/library/BookCardActionSheet.tsx @@ -1,11 +1,12 @@ +import { GroupPickerSheet } from "@/components/library/GroupPickerSheet"; import { CheckIcon, DatabaseIcon, FolderInputIcon, HashIcon, + RotateCcwIcon, Trash2Icon, } from "@/components/ui/Icon"; -import { GroupPickerSheet } from "@/components/library/GroupPickerSheet"; import { useLibraryStore } from "@/stores/library-store"; import { type ThemeColors, fontSize, fontWeight, radius, spacing, useColors } from "@/styles/theme"; import type { Book } from "@readany/core/types"; @@ -31,6 +32,7 @@ interface BookCardActionSheetProps { onManageTags?: (book: Book) => void; onVectorize?: (book: Book) => void; onDelete: (bookId: string, options?: { preserveData?: boolean }) => void; + onResetReadingData?: (bookId: string) => void; } export function BookCardActionSheet({ @@ -41,12 +43,14 @@ export function BookCardActionSheet({ onManageTags, onVectorize, onDelete, + onResetReadingData, }: BookCardActionSheetProps) { const colors = useColors(); const styles = useMemo(() => makeStyles(colors), [colors]); const { width: screenWidth, height: screenHeight } = useWindowDimensions(); const { t } = useTranslation(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showResetConfirm, setShowResetConfirm] = useState(false); const [preserveDataOnDelete, setPreserveDataOnDelete] = useState(true); const [showGroupPicker, setShowGroupPicker] = useState(false); const groups = useLibraryStore((state) => state.groups); @@ -91,7 +95,10 @@ export function BookCardActionSheet({ if (book.isVectorized) { Alert.alert( t("home.vec_reindex", "重新索引"), - t("home.vec_reindexConfirm", "该书已完成索引,重新索引将重置现有数据,确定继续吗?"), + t( + "home.vec_reindexConfirm", + "该书已完成索引,重新索引将重置现有数据,确定继续吗?", + ), [ { text: t("common.cancel"), style: "cancel" }, { text: t("common.confirm"), onPress: () => onVectorize(book) }, @@ -103,6 +110,18 @@ export function BookCardActionSheet({ }, } : null, + onResetReadingData + ? { + key: "reset-reading-data", + icon: , + label: t("library.resetReadingData", "重置阅读数据"), + destructive: true, + onPress: () => { + onClose(); + setShowResetConfirm(true); + }, + } + : null, { key: "delete", icon: , @@ -229,6 +248,45 @@ export function BookCardActionSheet({ + + setShowResetConfirm(false)} + > + setShowResetConfirm(false)}> + {}}> + + {t("library.resetReadingDataTitle", "重置阅读数据?")} + + + {t( + "library.resetReadingDataDescription", + "这会清空本书的阅读进度、当前位置和阅读字数统计,但不会删除书籍、笔记、高亮或书签。", + )} + + + + setShowResetConfirm(false)} + > + {t("common.cancel", "取消")} + + { + setShowResetConfirm(false); + onResetReadingData?.(book.id); + }} + > + {t("common.reset", "重置")} + + + + + void; addBook: (book: Book) => Promise; removeBook: (bookId: string, options?: RemoveBookOptions) => Promise; + resetBookReadingData: (bookId: string) => Promise; updateBook: (bookId: string, updates: Partial) => void; setFilter: (filter: Partial) => void; setViewMode: (mode: LibraryViewMode) => void; @@ -806,6 +807,23 @@ export const useLibraryStore = create((set, get) => ({ debouncedSave("library-books", get().books); }, + resetBookReadingData: async (bookId) => { + set((state) => ({ + books: state.books.map((book) => + book.id === bookId ? { ...book, progress: 0, currentCfi: undefined } : book, + ), + })); + try { + await db.initDatabase(); + await db.resetBookReadingData(bookId); + } catch (err) { + console.error("Failed to reset book reading data:", err); + await get().loadBooks(); + return; + } + debouncedSave("library-books", get().books); + }, + updateBook: (bookId, updates) => { set((state) => ({ books: state.books.map((b) => (b.id === bookId ? { ...b, ...updates } : b)), diff --git a/packages/app/src/components/home/BookCard.tsx b/packages/app/src/components/home/BookCard.tsx index 4adca963..67f7419a 100644 --- a/packages/app/src/components/home/BookCard.tsx +++ b/packages/app/src/components/home/BookCard.tsx @@ -1,3 +1,4 @@ +import { GroupPickerPopover } from "@/components/home/GroupPickerPopover"; import { ConfigGuideDialog, type ConfigGuideType } from "@/components/shared/ConfigGuideDialog"; import { Dialog, @@ -7,7 +8,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { GroupPickerPopover } from "@/components/home/GroupPickerPopover"; import { useResolvedSrc, useSyncVersion } from "@/hooks/use-resolved-src"; import { openDesktopBook } from "@/lib/library/open-book"; /** @@ -30,6 +30,7 @@ import { Loader2, MoreVertical, Plus, + RotateCcw, Trash2, } from "lucide-react"; import { memo, useCallback, useEffect, useRef, useState } from "react"; @@ -50,6 +51,7 @@ export const BookCard = memo(function BookCard({ }: BookCardProps) { const { t } = useTranslation(); const removeBook = useLibraryStore((s) => s.removeBook); + const resetBookReadingData = useLibraryStore((s) => s.resetBookReadingData); const closeAppTab = useAppStore((s) => s.removeTab); const closeReaderTab = useReaderStore((s) => s.removeTab); const allTags = useLibraryStore((s) => s.allTags); @@ -71,6 +73,7 @@ export const BookCard = memo(function BookCard({ const [vectorProgress, setVectorProgress] = useState(null); const [configGuide, setConfigGuide] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [showResetReadingDataDialog, setShowResetReadingDataDialog] = useState(false); const [showReindexConfirm, setShowReindexConfirm] = useState(false); const [preserveDataOnDelete, setPreserveDataOnDelete] = useState(true); const coverRef = useRef(null); @@ -100,7 +103,13 @@ export const BookCard = memo(function BookCard({ onSelect?.(book.id); return; } - if (showMenu || showDeleteDialog || showReindexConfirm || Date.now() < suppressOpenUntilRef.current) { + if ( + showMenu || + showDeleteDialog || + showResetReadingDataDialog || + showReindexConfirm || + Date.now() < suppressOpenUntilRef.current + ) { return; } await openDesktopBook({ book, t }); @@ -115,6 +124,14 @@ export const BookCard = memo(function BookCard({ setShowDeleteDialog(true); }, []); + const handleResetReadingData = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + suppressOpenUntilRef.current = Date.now() + 600; + setShowMenu(false); + setMenuPos(null); + setShowResetReadingDataDialog(true); + }, []); + const doVectorize = useCallback(async () => { setVectorizing(true); try { @@ -153,16 +170,13 @@ export const BookCard = memo(function BookCard({ [book.isVectorized, hasVectorCapability, vectorizing, doVectorize], ); - const handleMoveGroup = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - suppressOpenUntilRef.current = Date.now() + 300; - setShowMenu(false); - setMenuPos(null); - setShowGroupPicker(true); - }, - [], - ); + const handleMoveGroup = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + suppressOpenUntilRef.current = Date.now() + 300; + setShowMenu(false); + setMenuPos(null); + setShowGroupPicker(true); + }, []); const handleImageLoad = (event: React.SyntheticEvent) => { setImageLoaded(event.currentTarget.naturalWidth > 0); @@ -486,7 +500,15 @@ export const BookCard = memo(function BookCard({ )} - {/* Delete button */} + {/* Destructive actions */} + + + + + + {/* Re-index confirmation dialog */} e.stopPropagation()}> {t("home.vec_reindex")} - - {t("home.vec_reindexConfirm")} - + {t("home.vec_reindexConfirm")} diff --git a/packages/app/src/lib/db/database.ts b/packages/app/src/lib/db/database.ts index f70eee80..a92dbfa7 100644 --- a/packages/app/src/lib/db/database.ts +++ b/packages/app/src/lib/db/database.ts @@ -11,6 +11,7 @@ export { getDeletedBookByTitle, insertBook, updateBook, + resetBookReadingData, deleteBook, getGroups, insertGroup, diff --git a/packages/app/src/stores/library-store.ts b/packages/app/src/stores/library-store.ts index f33a4c37..4ee035c4 100644 --- a/packages/app/src/stores/library-store.ts +++ b/packages/app/src/stores/library-store.ts @@ -348,6 +348,7 @@ export interface LibraryState { setActiveGroupId: (groupId: string) => void; addBook: (book: Book) => void; removeBook: (bookId: string, options?: RemoveBookOptions) => Promise; + resetBookReadingData: (bookId: string) => Promise; updateBook: (bookId: string, updates: Partial) => void; setFilter: (filter: Partial) => void; setViewMode: (mode: LibraryViewMode) => void; @@ -823,6 +824,22 @@ export const useLibraryStore = create((set, get) => ({ } }, + resetBookReadingData: async (bookId) => { + set((state) => ({ + books: state.books.map((book) => + book.id === bookId ? { ...book, progress: 0, currentCfi: undefined } : book, + ), + })); + try { + await db.resetBookReadingData(bookId); + } catch (err) { + console.error("Failed to reset book reading data:", err); + await get().loadBooks(); + return; + } + debouncedSave("library-books", get().books); + }, + updateBook: (bookId, updates) => { set((state) => ({ books: state.books.map((b) => (b.id === bookId ? { ...b, ...updates } : b)), diff --git a/packages/core/src/db/__tests__/book-queries.test.ts b/packages/core/src/db/__tests__/book-queries.test.ts index 2ad76c3e..41d16948 100644 --- a/packages/core/src/db/__tests__/book-queries.test.ts +++ b/packages/core/src/db/__tests__/book-queries.test.ts @@ -1,5 +1,5 @@ -import type { Book } from "../../types"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Book } from "../../types"; // --- Mock db-core --- const mockExecute = vi.fn(); @@ -16,7 +16,11 @@ const coreMocks = vi.hoisted(() => ({ insertTombstone: vi.fn(), parseJSON: vi.fn((str: string | null | undefined, fallback: unknown) => { if (!str) return fallback; - try { return JSON.parse(str); } catch { return fallback; } + try { + return JSON.parse(str); + } catch { + return fallback; + } }), })); @@ -26,7 +30,9 @@ const dependencyMocks = vi.hoisted(() => ({ })); vi.mock("../db-core", () => coreMocks); -vi.mock("../thread-queries", () => ({ deleteThreadsByBookId: dependencyMocks.deleteThreadsByBookId })); +vi.mock("../thread-queries", () => ({ + deleteThreadsByBookId: dependencyMocks.deleteThreadsByBookId, +})); vi.mock("../chunk-queries", () => ({ deleteChunks: dependencyMocks.deleteChunks })); const { @@ -35,6 +41,7 @@ const { getDeletedBookByFileHash, insertBook, updateBook, + resetBookReadingData, deleteBook, } = await import("../book-queries"); @@ -166,7 +173,7 @@ describe("book-queries", () => { const book = await getBook("book-1"); expect(book).not.toBeNull(); - expect(book!.id).toBe("book-1"); + expect(book?.id).toBe("book-1"); expect(mockSelect).toHaveBeenCalledWith( "SELECT * FROM books WHERE id = ? AND deleted_at IS NULL", ["book-1"], @@ -256,24 +263,67 @@ describe("book-queries", () => { }); }); + describe("resetBookReadingData", () => { + it("resets book progress and deletes reading sessions", async () => { + mockExecute.mockResolvedValue(undefined); + mockSelect.mockResolvedValueOnce([{ id: "session-1" }]); + + await resetBookReadingData("book-1"); + + expect(mockExecute).toHaveBeenCalledWith(expect.stringContaining("UPDATE books SET"), [ + 0, + "", + 3000, + 1, + "device-1", + "book-1", + ]); + expect(mockSelect).toHaveBeenCalledWith("SELECT id FROM reading_sessions WHERE book_id = ?", [ + "book-1", + ]); + expect(coreMocks.insertTombstone).toHaveBeenCalledWith( + mockDb, + "session-1", + "reading_sessions", + ); + expect(mockExecute).toHaveBeenCalledWith("DELETE FROM reading_sessions WHERE book_id = ?", [ + "book-1", + ]); + }); + }); + describe("deleteBook", () => { it("soft-deletes book and preserves notes + reading stats when requested", async () => { mockExecute.mockResolvedValue(undefined); await deleteBook("book-1", { preserveData: true }); - expect(mockSelect).not.toHaveBeenCalledWith("SELECT id FROM highlights WHERE book_id = ?", ["book-1"]); - expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM highlights WHERE book_id = ?", ["book-1"]); - expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM notes WHERE book_id = ?", ["book-1"]); - expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM bookmarks WHERE book_id = ?", ["book-1"]); - expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM reading_sessions WHERE book_id = ?", ["book-1"]); + expect(mockSelect).not.toHaveBeenCalledWith("SELECT id FROM highlights WHERE book_id = ?", [ + "book-1", + ]); + expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM highlights WHERE book_id = ?", [ + "book-1", + ]); + expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM notes WHERE book_id = ?", [ + "book-1", + ]); + expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM bookmarks WHERE book_id = ?", [ + "book-1", + ]); + expect(mockExecute).not.toHaveBeenCalledWith( + "DELETE FROM reading_sessions WHERE book_id = ?", + ["book-1"], + ); expect(mockExecute).not.toHaveBeenCalledWith("DELETE FROM books WHERE id = ?", ["book-1"]); expect(dependencyMocks.deleteThreadsByBookId).toHaveBeenCalledWith("book-1"); expect(dependencyMocks.deleteChunks).toHaveBeenCalledWith("book-1"); - expect(mockExecute).toHaveBeenCalledWith( - expect.stringContaining("UPDATE books"), - [expect.any(Number), 3000, 1, "device-1", "book-1"], - ); + expect(mockExecute).toHaveBeenCalledWith(expect.stringContaining("UPDATE books"), [ + expect.any(Number), + 3000, + 1, + "device-1", + "book-1", + ]); }); it("hard-deletes everything when preserveData is not requested", async () => { @@ -281,18 +331,30 @@ describe("book-queries", () => { mockSelect .mockResolvedValueOnce([{ id: "hl-1" }]) .mockResolvedValueOnce([{ id: "note-1" }]) - .mockResolvedValueOnce([{ id: "bm-1" }]); + .mockResolvedValueOnce([{ id: "bm-1" }]) + .mockResolvedValueOnce([{ id: "session-1" }]); await deleteBook("book-1"); - expect(mockExecute).toHaveBeenCalledWith("DELETE FROM highlights WHERE book_id = ?", ["book-1"]); + expect(mockExecute).toHaveBeenCalledWith("DELETE FROM highlights WHERE book_id = ?", [ + "book-1", + ]); expect(mockExecute).toHaveBeenCalledWith("DELETE FROM notes WHERE book_id = ?", ["book-1"]); - expect(mockExecute).toHaveBeenCalledWith("DELETE FROM bookmarks WHERE book_id = ?", ["book-1"]); - expect(mockExecute).toHaveBeenCalledWith("DELETE FROM reading_sessions WHERE book_id = ?", ["book-1"]); + expect(mockExecute).toHaveBeenCalledWith("DELETE FROM bookmarks WHERE book_id = ?", [ + "book-1", + ]); + expect(mockExecute).toHaveBeenCalledWith("DELETE FROM reading_sessions WHERE book_id = ?", [ + "book-1", + ]); expect(mockExecute).toHaveBeenCalledWith("DELETE FROM books WHERE id = ?", ["book-1"]); expect(coreMocks.insertTombstone).toHaveBeenCalledWith(mockDb, "hl-1", "highlights"); expect(coreMocks.insertTombstone).toHaveBeenCalledWith(mockDb, "note-1", "notes"); expect(coreMocks.insertTombstone).toHaveBeenCalledWith(mockDb, "bm-1", "bookmarks"); + expect(coreMocks.insertTombstone).toHaveBeenCalledWith( + mockDb, + "session-1", + "reading_sessions", + ); expect(coreMocks.insertTombstone).toHaveBeenCalledWith(mockDb, "book-1", "books"); }); }); diff --git a/packages/core/src/db/__tests__/session-queries.test.ts b/packages/core/src/db/__tests__/session-queries.test.ts index 44f9cf4c..999b72ba 100644 --- a/packages/core/src/db/__tests__/session-queries.test.ts +++ b/packages/core/src/db/__tests__/session-queries.test.ts @@ -1,5 +1,5 @@ -import type { ReadingSession } from "../../types/reading"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ReadingSession } from "../../types/reading"; const mockExecute = vi.fn(); const mockSelect = vi.fn(); @@ -8,6 +8,7 @@ const mockDb = { execute: mockExecute, select: mockSelect, close: vi.fn() }; const coreMocks = vi.hoisted(() => ({ getDB: vi.fn(), getDeviceId: vi.fn(), + insertTombstone: vi.fn(), nextSyncVersion: vi.fn(), nextUpdatedAt: vi.fn(), })); @@ -20,6 +21,7 @@ const { getReadingSessionsByDateRange, insertReadingSession, updateReadingSession, + deleteReadingSessionsForBook, } = await import("../session-queries"); const sampleSession: ReadingSession = { @@ -38,6 +40,7 @@ describe("session-queries", () => { vi.clearAllMocks(); coreMocks.getDB.mockResolvedValue(mockDb); coreMocks.getDeviceId.mockResolvedValue("device-1"); + coreMocks.insertTombstone.mockResolvedValue(undefined); coreMocks.nextSyncVersion.mockResolvedValue(1); coreMocks.nextUpdatedAt.mockResolvedValue(3000); }); @@ -162,8 +165,8 @@ describe("session-queries", () => { expect(params[1]).toBe("book-1"); expect(params[2]).toBe(1000); // startedAt expect(params[3]).toBe(2000); // endedAt - expect(params[4]).toBe(900); // totalActiveTime - expect(params[5]).toBe(10); // pagesRead + expect(params[4]).toBe(900); // totalActiveTime + expect(params[5]).toBe(10); // pagesRead expect(params[6]).toBe(15000); // charactersRead expect(params[7]).toBe("STOPPED"); // state }); @@ -214,4 +217,30 @@ describe("session-queries", () => { expect(sql).toContain("last_modified_by = ?"); }); }); + + describe("deleteReadingSessionsForBook", () => { + it("creates tombstones and deletes sessions for a book", async () => { + mockSelect.mockResolvedValue([{ id: "session-1" }, { id: "session-2" }]); + mockExecute.mockResolvedValue(undefined); + + await deleteReadingSessionsForBook("book-1"); + + expect(mockSelect).toHaveBeenCalledWith("SELECT id FROM reading_sessions WHERE book_id = ?", [ + "book-1", + ]); + expect(coreMocks.insertTombstone).toHaveBeenCalledWith( + mockDb, + "session-1", + "reading_sessions", + ); + expect(coreMocks.insertTombstone).toHaveBeenCalledWith( + mockDb, + "session-2", + "reading_sessions", + ); + expect(mockExecute).toHaveBeenCalledWith("DELETE FROM reading_sessions WHERE book_id = ?", [ + "book-1", + ]); + }); + }); }); diff --git a/packages/core/src/db/book-queries.ts b/packages/core/src/db/book-queries.ts index 198e0925..b013936c 100644 --- a/packages/core/src/db/book-queries.ts +++ b/packages/core/src/db/book-queries.ts @@ -8,6 +8,7 @@ import { nextUpdatedAt, parseJSON, } from "./db-core"; +import { deleteReadingSessionsForBook } from "./session-queries"; import { deleteThreadsByBookId } from "./thread-queries"; interface BookRow { @@ -304,6 +305,11 @@ export async function setBookSyncStatus(id: string, syncStatus: Book["syncStatus await database.execute("UPDATE books SET sync_status = ? WHERE id = ?", [syncStatus, id]); } +export async function resetBookReadingData(id: string): Promise { + await updateBook(id, { progress: 0, currentCfi: "" }); + await deleteReadingSessionsForBook(id); +} + export async function deleteBook(id: string, options: DeleteBookOptions = {}): Promise { const database = await getDB(); const preserveData = options.preserveData ?? false; @@ -348,7 +354,7 @@ export async function deleteBook(id: string, options: DeleteBookOptions = {}): P await database.execute("DELETE FROM highlights WHERE book_id = ?", [id]); await database.execute("DELETE FROM notes WHERE book_id = ?", [id]); await database.execute("DELETE FROM bookmarks WHERE book_id = ?", [id]); - await database.execute("DELETE FROM reading_sessions WHERE book_id = ?", [id]); + await deleteReadingSessionsForBook(id); await deleteThreadsByBookId(id); await deleteChunks(id); await insertTombstone(database, id, "books"); diff --git a/packages/core/src/db/database.ts b/packages/core/src/db/database.ts index 6fbeed91..bbc8a802 100644 --- a/packages/core/src/db/database.ts +++ b/packages/core/src/db/database.ts @@ -41,6 +41,7 @@ export { insertBook, updateBook, setBookSyncStatus, + resetBookReadingData, deleteBook, } from "./book-queries"; @@ -96,6 +97,7 @@ export { getReadingSessionsByDateRange, insertReadingSession, updateReadingSession, + deleteReadingSessionsForBook, } from "./session-queries"; export { diff --git a/packages/core/src/db/index.ts b/packages/core/src/db/index.ts index d4a11882..c38997ec 100644 --- a/packages/core/src/db/index.ts +++ b/packages/core/src/db/index.ts @@ -27,6 +27,7 @@ export { insertBook, updateBook, setBookSyncStatus, + resetBookReadingData, deleteBook, // Group queries getGroups, @@ -67,6 +68,7 @@ export { getReadingSessionsByDateRange, insertReadingSession, updateReadingSession, + deleteReadingSessionsForBook, // Chunk queries getChunks, insertChunks, diff --git a/packages/core/src/db/session-queries.ts b/packages/core/src/db/session-queries.ts index c5ba9934..6bd5fd82 100644 --- a/packages/core/src/db/session-queries.ts +++ b/packages/core/src/db/session-queries.ts @@ -1,5 +1,5 @@ import type { ReadingSession } from "../types/reading"; -import { getDB, getDeviceId, nextSyncVersion, nextUpdatedAt } from "./db-core"; +import { getDB, getDeviceId, insertTombstone, nextSyncVersion, nextUpdatedAt } from "./db-core"; type ReadingSessionRow = { id: string; @@ -121,3 +121,17 @@ export async function updateReadingSession( values.push(id); await database.execute(`UPDATE reading_sessions SET ${sets.join(", ")} WHERE id = ?`, values); } + +export async function deleteReadingSessionsForBook(bookId: string): Promise { + const database = await getDB(); + const sessionRows = await database.select<{ id: string }>( + "SELECT id FROM reading_sessions WHERE book_id = ?", + [bookId], + ); + + for (const row of sessionRows) { + await insertTombstone(database, row.id, "reading_sessions"); + } + + await database.execute("DELETE FROM reading_sessions WHERE book_id = ?", [bookId]); +}