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:
committed by
GitHub
parent
ba82203629
commit
46fdea3522
@@ -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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user