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 */}
+