Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/app-expo/src/components/library/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -67,6 +68,7 @@ export const BookCard = memo(function BookCard({
book,
onOpen,
onDelete,
onResetReadingData,
onManageTags,
onVectorize,
isVectorizing,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -405,6 +405,7 @@ export const BookCard = memo(function BookCard({
onManageTags={onManageTags}
onVectorize={onVectorize}
onDelete={onDelete}
onResetReadingData={onResetReadingData}
/>
</>
);
Expand Down
62 changes: 60 additions & 2 deletions packages/app-expo/src/components/library/BookCardActionSheet.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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({
Expand All @@ -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);
Expand Down Expand Up @@ -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) },
Expand All @@ -103,6 +110,18 @@ export function BookCardActionSheet({
},
}
: null,
onResetReadingData
? {
key: "reset-reading-data",
icon: <RotateCcwIcon size={18} color={colors.destructive} />,
label: t("library.resetReadingData", "重置阅读数据"),
destructive: true,
onPress: () => {
onClose();
setShowResetConfirm(true);
},
}
: null,
{
key: "delete",
icon: <Trash2Icon size={18} color={colors.destructive} />,
Expand Down Expand Up @@ -229,6 +248,45 @@ export function BookCardActionSheet({
</Pressable>
</Pressable>
</Modal>

<Modal
visible={showResetConfirm}
transparent
animationType="fade"
onRequestClose={() => setShowResetConfirm(false)}
>
<Pressable style={styles.confirmOverlay} onPress={() => setShowResetConfirm(false)}>
<Pressable style={styles.confirmCard} onPress={() => {}}>
<Text style={styles.confirmTitle}>
{t("library.resetReadingDataTitle", "重置阅读数据?")}
</Text>
<Text style={styles.confirmDescription}>
{t(
"library.resetReadingDataDescription",
"这会清空本书的阅读进度、当前位置和阅读字数统计,但不会删除书籍、笔记、高亮或书签。",
)}
</Text>

<View style={styles.confirmActions}>
<TouchableOpacity
style={styles.confirmSecondary}
onPress={() => setShowResetConfirm(false)}
>
<Text style={styles.confirmSecondaryText}>{t("common.cancel", "取消")}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.confirmDanger}
onPress={() => {
setShowResetConfirm(false);
onResetReadingData?.(book.id);
}}
>
<Text style={styles.confirmDangerText}>{t("common.reset", "重置")}</Text>
</TouchableOpacity>
</View>
</Pressable>
</Pressable>
</Modal>
<GroupPickerSheet
visible={showGroupPicker}
groups={groups}
Expand Down
3 changes: 3 additions & 0 deletions packages/app-expo/src/screens/LibraryScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export function LibraryScreen() {
loadBooks,
importBooks,
removeBook,
resetBookReadingData,
setFilter,
setGroupView,
setActiveGroupId,
Expand Down Expand Up @@ -685,6 +686,7 @@ export function LibraryScreen() {
cardWidth={gridItemWidth}
onOpen={handleOpen}
onDelete={removeBook}
onResetReadingData={resetBookReadingData}
onManageTags={handleManageTags}
onVectorize={handleVectorize}
isVectorizing={vectorizingBookId === item.book.id}
Expand All @@ -707,6 +709,7 @@ export function LibraryScreen() {
handleOpen,
handleVectorize,
removeBook,
resetBookReadingData,
s.gridItem,
selectedBookIds,
selectionMode,
Expand Down
18 changes: 18 additions & 0 deletions packages/app-expo/src/stores/library-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface LibraryState {
setActiveGroupId: (groupId: string) => void;
addBook: (book: Book) => Promise<void>;
removeBook: (bookId: string, options?: RemoveBookOptions) => Promise<void>;
resetBookReadingData: (bookId: string) => Promise<void>;
updateBook: (bookId: string, updates: Partial<Book>) => void;
setFilter: (filter: Partial<LibraryFilter>) => void;
setViewMode: (mode: LibraryViewMode) => void;
Expand Down Expand Up @@ -806,6 +807,23 @@ export const useLibraryStore = create<LibraryState>((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)),
Expand Down
104 changes: 87 additions & 17 deletions packages/app/src/components/home/BookCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GroupPickerPopover } from "@/components/home/GroupPickerPopover";
import { ConfigGuideDialog, type ConfigGuideType } from "@/components/shared/ConfigGuideDialog";
import {
Dialog,
Expand All @@ -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";
/**
Expand All @@ -30,6 +30,7 @@ import {
Loader2,
MoreVertical,
Plus,
RotateCcw,
Trash2,
} from "lucide-react";
import { memo, useCallback, useEffect, useRef, useState } from "react";
Expand All @@ -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);
Expand All @@ -71,6 +73,7 @@ export const BookCard = memo(function BookCard({
const [vectorProgress, setVectorProgress] = useState<VectorizeProgress | null>(null);
const [configGuide, setConfigGuide] = useState<ConfigGuideType>(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showResetReadingDataDialog, setShowResetReadingDataDialog] = useState(false);
const [showReindexConfirm, setShowReindexConfirm] = useState(false);
const [preserveDataOnDelete, setPreserveDataOnDelete] = useState(true);
const coverRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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 });
Expand All @@ -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 {
Expand Down Expand Up @@ -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<HTMLImageElement>) => {
setImageLoaded(event.currentTarget.naturalWidth > 0);
Expand Down Expand Up @@ -486,7 +500,15 @@ export const BookCard = memo(function BookCard({
</div>
)}
</div>
{/* Delete button */}
{/* Destructive actions */}
<button
type="button"
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs text-destructive hover:bg-destructive/10"
onClick={handleResetReadingData}
>
<RotateCcw className="h-3.5 w-3.5" />
{t("library.resetReadingData", "重置阅读数据")}
</button>
<button
type="button"
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs text-destructive hover:bg-destructive/10"
Expand Down Expand Up @@ -618,20 +640,68 @@ export const BookCard = memo(function BookCard({
</DialogContent>
</Dialog>

<Dialog open={showResetReadingDataDialog} onOpenChange={setShowResetReadingDataDialog}>
<DialogContent className="max-w-md" onClick={(e) => e.stopPropagation()}>
<DialogHeader>
<DialogTitle>{t("library.resetReadingDataTitle", "重置阅读数据?")}</DialogTitle>
<DialogDescription>
{t(
"library.resetReadingDataDescription",
"这会清空本书的阅读进度、当前位置和阅读字数统计,但不会删除书籍、笔记、高亮或书签。",
)}
</DialogDescription>
</DialogHeader>

<DialogFooter>
<button
type="button"
className="inline-flex h-9 items-center justify-center rounded-md border border-border bg-background px-4 text-sm font-medium text-foreground transition-colors hover:bg-muted"
onClick={(e) => {
e.stopPropagation();
setShowResetReadingDataDialog(false);
}}
>
{t("common.cancel", "取消")}
</button>
<button
type="button"
className="inline-flex h-9 items-center justify-center rounded-md bg-destructive px-4 text-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive/90"
onClick={async (e) => {
e.stopPropagation();
suppressOpenUntilRef.current = Date.now() + 600;
setShowResetReadingDataDialog(false);
const matchingTabIds = useAppStore
.getState()
.tabs.filter((tab) => tab.bookId === book.id)
.map((tab) => tab.id);
for (const tabId of matchingTabIds) {
closeAppTab(tabId);
closeReaderTab(tabId);
}
await resetBookReadingData(book.id);
}}
>
{t("common.reset", "重置")}
</button>
</DialogFooter>
</DialogContent>
</Dialog>

{/* Re-index confirmation dialog */}
<Dialog open={showReindexConfirm} onOpenChange={setShowReindexConfirm}>
<DialogContent className="max-w-sm" onClick={(e) => e.stopPropagation()}>
<DialogHeader>
<DialogTitle>{t("home.vec_reindex")}</DialogTitle>
<DialogDescription>
{t("home.vec_reindexConfirm")}
</DialogDescription>
<DialogDescription>{t("home.vec_reindexConfirm")}</DialogDescription>
</DialogHeader>
<DialogFooter>
<button
type="button"
className="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium transition-colors hover:bg-muted"
onClick={(e) => { e.stopPropagation(); setShowReindexConfirm(false); }}
onClick={(e) => {
e.stopPropagation();
setShowReindexConfirm(false);
}}
>
{t("common.cancel")}
</button>
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/lib/db/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
getDeletedBookByTitle,
insertBook,
updateBook,
resetBookReadingData,
deleteBook,
getGroups,
insertGroup,
Expand Down
Loading