feat: provide reading progress indicator for each books in the bookshelf (#153)

* feat: provide reading progress indicator for each books in the bookshelf

* refactor: simplify bookshelf component and remove unused hook; enhance reading progress calculation

- Removed the `useBookConfigLoader` hook as it was no longer needed.
- Simplified the bookshelf items generation by eliminating the use of `useMemo`.
- Improved the reading progress calculation in the `ReadingProgress` component to directly use the book's progress data.
- Updated the `Book` type to include a `progress` field for better tracking of reading status.

* fix formatting for readerStore
This commit is contained in:
Egan Gumiwang Pratama Bisma
2025-01-15 23:17:07 +07:00
committed by GitHub
parent ba82203629
commit 46fdea3522
4 changed files with 118 additions and 14 deletions
@@ -25,6 +25,7 @@ import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
import BookDetailModal from '@/components/BookDetailModal';
import ReadingProgress from './ReadingProgress';
type BookshelfItem = Book | BooksGroup;
@@ -242,17 +243,50 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
)}
</div>
</div>
<div className='flex flex-row items-center justify-between p-0 pt-2'>
<h4 className='card-title line-clamp-1 text-[0.6em] text-xs font-semibold'>
{(item as Book).title}
</h4>
{isWebAppPlatform() && (
<div
className='show-detail-button self-start opacity-0 group-hover:opacity-100'
role='button'
onClick={showBookDetailsModal.bind(null, item as Book)}
>
<CiCircleMore size={15} />
<div
className={clsx(
'flex w-full p-0 pt-2',
isWebAppPlatform() ? 'flex-col' : 'flex-row justify-between',
)}
>
<div className='min-w-0 flex-1'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-[0.6em] text-xs font-semibold'>
{(item as Book).title}
</h4>
</div>
{item.progress && (
<div className={'flex items-center justify-between'}>
<ReadingProgress book={item as Book} />
{isWebAppPlatform() && (
<button
type='button'
className='show-detail-button opacity-0 group-hover:opacity-100'
onClick={showBookDetailsModal.bind(null, item as Book)}
onKeyUp={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
showBookDetailsModal(item as Book);
}
}}
>
<CiCircleMore size={15} />
</button>
)}
</div>
)}
{!item.progress && isWebAppPlatform() && (
<div className={'flex items-center justify-end'}>
<button
type='button'
className='show-detail-button opacity-0 group-hover:opacity-100'
onClick={showBookDetailsModal.bind(null, item as Book)}
onKeyUp={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
showBookDetailsModal(item as Book);
}
}}
>
<CiCircleMore size={15} />
</button>
</div>
)}
</div>
@@ -270,6 +304,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
className='h-48 w-full object-cover'
/>
</figure>
<div className='card-body p-4'>
<h3 className='card-title line-clamp-2 text-sm'>{book.title}</h3>
<p className='text-neutral-content line-clamp-1 text-xs'>{book.author}</p>
<ReadingProgress book={book} />
</div>
</div>
))}
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
@@ -0,0 +1,43 @@
import type React from 'react';
import { memo, useMemo } from 'react';
import type { Book } from '@/types/book';
interface ReadingProgressProps {
book: Book;
}
const getProgressPercentage = (book: Book) => {
if (!book.progress || !book.progress[1]) {
return null;
}
if (book.progress && book.progress[1] === 1) {
return 100;
}
return Math.round((book.progress[0] / book.progress[1]) * 100);
};
const ReadingProgress: React.FC<ReadingProgressProps> = memo(
({ book }) => {
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
if (!progressPercentage) {
return null;
}
return (
<div className='text-neutral-content/70 flex justify-between text-xs'>
<span>{progressPercentage}%</span>
</div>
);
},
(prevProps, nextProps) => {
return (
prevProps.book.hash === nextProps.book.hash &&
prevProps.book.updatedAt === nextProps.book.updatedAt
);
},
);
ReadingProgress.displayName = 'ReadingProgress';
export default ReadingProgress;
+24 -3
View File
@@ -64,7 +64,10 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
getView: (key: string | null) => (key && get().viewStates[key]?.view) || null,
setView: (key: string, view) =>
set((state) => ({
viewStates: { ...state.viewStates, [key]: { ...state.viewStates[key]!, view } },
viewStates: {
...state.viewStates,
[key]: { ...state.viewStates[key]!, view },
},
})),
getViews: () => Object.values(get().viewStates).map((state) => state.view!),
getViewsById: (id: string) => {
@@ -213,13 +216,31 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
const bookData = useBookDataStore.getState().booksData[id];
const viewState = state.viewStates[key];
if (!viewState || !bookData) return state;
const progress: [number, number] = [pageinfo.current, pageinfo.total];
// Update library book progress
const { library, setLibrary } = useLibraryStore.getState();
const bookIndex = library.findIndex((b) => b.hash === id);
if (bookIndex !== -1) {
const updatedLibrary = [...library];
const existingBook = updatedLibrary[bookIndex]!;
updatedLibrary[bookIndex] = {
...existingBook,
progress,
updatedAt: Date.now(),
};
setLibrary(updatedLibrary);
}
const oldConfig = bookData.config;
const newConfig = {
...bookData.config,
updatedAt: Date.now(),
progress: [pageinfo.current, pageinfo.total] as [number, number],
progress,
location,
};
useBookDataStore.setState((state) => ({
booksData: {
...state.booksData,
@@ -229,6 +250,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
},
},
}));
return {
viewStates: {
...state.viewStates,
@@ -248,7 +270,6 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
},
};
}),
setBookmarkRibbonVisibility: (key: string, visible: boolean) =>
set((state) => ({
viewStates: {
+1
View File
@@ -19,6 +19,7 @@ export interface Book {
deletedAt?: number | null;
lastUpdated?: number; // deprecated in favor of updatedAt
progress?: [number, number]; // Add progress field: [current, total]
}
export interface PageInfo {