Add information modal for book details (#27)

* Add BookDetailModal component and implement details view in Bookshelf

* Enhance BookDetailModal to fetch and display detailed book metadata

* Refactor Bookshelf and BookDetailModal for improved click handling and styling consistency

* Enhance BookDetailModal with additional metadata fields and improved layout

* Refactor BookDetailModal to use appService for fetching book details and remove deprecated bookService
This commit is contained in:
Jiucheng(Oliver)
2024-12-26 10:59:54 -05:00
committed by GitHub
parent acfce9da67
commit 329c907f80
4 changed files with 187 additions and 3 deletions
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import { PiPlus } from 'react-icons/pi';
import { MdDelete, MdOpenInNew } from 'react-icons/md';
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
import { CiCircleMore } from "react-icons/ci";
import { useRouter, useSearchParams } from 'next/navigation';
import Image from 'next/image';
@@ -22,6 +23,7 @@ import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
import { isTauriAppPlatform } from '@/services/environment';
import BookDetailModal from '@/components/BookDetailModal';
type BookshelfItem = Book | BooksGroup;
@@ -67,6 +69,17 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
const [clickedImage, setClickedImage] = useState<string | null>(null);
const [importBookUrl] = useState(searchParams?.get('url') || '');
const isImportingBook = useRef(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
const showMoreDetails = (book: Book) => {
setIsModalOpen(true);
setSelectedBook(book);
};
const closeModal = () => {
setIsModalOpen(false);
};
const { setLibrary } = useLibraryStore();
@@ -171,9 +184,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
<div
className='book-item cursor-pointer'
onContextMenu={bookContextMenuHandler.bind(null, item as Book)}
onClick={() => handleBookClick(item.hash)}
>
<div key={(item as Book).hash} className='bg-base-100 shadow-md'>
<div key={(item as Book).hash}
className='bg-base-100 shadow-md'
onClick={() => handleBookClick(item.hash)}
>
<div className='relative aspect-[28/41]'>
<Image
src={item.coverImageUrl!}
@@ -212,10 +227,17 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
)}
</div>
</div>
<div className='card-body p-0 pt-2'>
<div className='card-body 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>
<div
className='card-detail'
role='button'
onClick={showMoreDetails.bind(null, item as Book)}
>
<CiCircleMore size={15} />
</div>
</div>
</div>
) : (
@@ -275,6 +297,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
onClickConfirm={confirmDelete}
/>
)}
{/* Modal Component */}
{selectedBook && (
<BookDetailModal isOpen={isModalOpen} onClose={closeModal} book={selectedBook} envConfig={envConfig} />
)}
</div>
);
};
@@ -0,0 +1,149 @@
import React, { useEffect, useState } from 'react';
import { Book } from '@/types/book';
import { EnvConfigType } from '@/services/environment';
import { useSettingsStore } from '@/store/settingsStore';
import WindowButtons from '@/components/WindowButtons';
const BookDetailModal = ({
isOpen,
onClose,
book,
envConfig,
}: {
isOpen: boolean;
onClose: () => void;
book: Book;
envConfig: EnvConfigType;
}) => {
if (!isOpen) return null;
const [bookMeta, setBookMeta] = useState<null | {
title: string;
language: string | string[];
editor?: string;
publisher?: string;
published?: string;
description?: string;
subject?: string[];
identifier?: string;
}>(null);
const { settings } = useSettingsStore();
useEffect(() => {
const fetchBookDetails = async () => {
const appService = await envConfig.getAppService();
const details = await appService.fetchBookDetails(book, settings);
setBookMeta(details);
};
fetchBookDetails();
}, [book]);
if (!bookMeta)
return (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<div className='fixed inset-0 bg-gray-800 bg-opacity-70' onClick={onClose} />
<div className='bg-base-200 relative z-50 w-full max-w-md rounded-lg p-6 shadow-xl'>
<div className='absolute right-4 top-4 flex space-x-2'>
<WindowButtons
className='window-buttons flex'
showMinimize={false}
showMaximize={false}
onClose={onClose}
/>
</div>
<h2 className='text-base-content text-center text-2xl font-semibold'>
Loading Book Details...
</h2>
</div>
</div>
);
return (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<div className='fixed inset-0 bg-gray-800 bg-opacity-70' onClick={onClose} />
<div className='bg-base-200 relative z-50 w-full max-w-md rounded-lg p-6 shadow-xl'>
<div className='absolute right-4 top-4 flex space-x-2'>
<WindowButtons
className='window-buttons flex'
showMinimize={false}
showMaximize={false}
onClose={onClose}
/>
</div>
<div className='mb-6 flex items-start'>
{book.coverImageUrl ? (
<img
src={book.coverImageUrl}
alt={book.title}
className='mr-4 h-40 w-30 rounded-lg object-contain shadow-md'
/>
) : (
<div className='mr-4 flex h-40 w-30 items-center justify-center rounded-lg bg-gray-300'>
<span className='text-gray-500'>No Image</span>
</div>
)}
<div className='h-40 flex flex-col justify-between'>
<h2 className='text-base-content mb-2 text-2xl font-bold'>
{bookMeta.title || 'Untitled'}
</h2>
<p className='text-neutral-content mb-4'>{book.author || 'Unknown Author'}</p>
<button className='mt-4 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600'>
More Info
</button>
</div>
</div>
<div className='text-base-content mb-4'>
<div className='mb-4 grid grid-cols-3 gap-4'>
<div>
<span className='font-bold'>Publisher:</span>
<p className='text-neutral-content'>{bookMeta.publisher || 'Unknown'}</p>
</div>
<div>
<span className='font-bold'>Published:</span>
<p className='text-neutral-content'>{bookMeta.published || 'Unknown Date'}</p>
</div>
<div>
<span className='font-bold'>Updated:</span>
<p className='text-neutral-content'>
{book.lastUpdated
? new Date(book.lastUpdated).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: 'Unknown Date'}
</p>
</div>
</div>
<div className='grid grid-cols-3 gap-4'>
<div>
<span className='font-bold'>Language:</span>
<p className='text-neutral-content'>{bookMeta.language || 'Unknown'}</p>
</div>
<div>
<span className='font-bold'>Identifier:</span>
<p className='text-neutral-content'>
{bookMeta.identifier ? bookMeta.identifier.slice(-8) : 'N/A'}
</p>{' '}
{/* Show last 8 characters */}
</div>
<div>
<span className='font-bold'>Subjects:</span>
<p className='text-neutral-content'>{bookMeta.subject?.join(', ') || 'None'}</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default BookDetailModal;
@@ -219,6 +219,12 @@ export abstract class BaseAppService implements AppService {
}
}
async fetchBookDetails(book: Book, settings: SystemSettings) {
const { file } = await this.loadBookContent(book, settings) as BookContent;
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
return bookDoc.metadata;
}
async saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings) {
let serializedConfig: string;
if (settings) {
+2
View File
@@ -1,5 +1,6 @@
import { SystemSettings } from './settings';
import { Book, BookConfig, BookContent } from './book';
import { BookDoc } from '@/libs/document';
export type AppPlatform = 'web' | 'tauri';
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Log' | 'Cache' | 'None';
@@ -38,6 +39,7 @@ export interface AppService {
): Promise<Book | null>;
deleteBook(book: Book): Promise<void>;
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
fetchBookDetails(book: Book, settings: SystemSettings): Promise<BookDoc['metadata']>;
saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings): Promise<void>;
loadBookContent(book: Book, settings: SystemSettings): Promise<BookContent>;
loadLibraryBooks(): Promise<Book[]>;