forked from akai/readest
263 lines
7.9 KiB
TypeScript
263 lines
7.9 KiB
TypeScript
import clsx from 'clsx';
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
import { Book } from '@/types/book';
|
|
import { BookMetadata } from '@/libs/document';
|
|
import { useEnv } from '@/context/EnvContext';
|
|
import { useThemeStore } from '@/store/themeStore';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { useMetadataEdit } from './useMetadataEdit';
|
|
import { DeleteAction } from '@/types/system';
|
|
import Alert from '@/components/Alert';
|
|
import Dialog from '@/components/Dialog';
|
|
import Spinner from '@/components/Spinner';
|
|
import BookDetailView from './BookDetailView';
|
|
import BookDetailEdit from './BookDetailEdit';
|
|
import SourceSelector from './SourceSelector';
|
|
|
|
interface BookDetailModalProps {
|
|
book: Book;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
handleBookDownload?: (book: Book, redownload?: boolean) => void;
|
|
handleBookUpload?: (book: Book) => void;
|
|
handleBookDelete?: (book: Book) => void;
|
|
handleBookDeleteCloudBackup?: (book: Book) => void;
|
|
handleBookDeleteLocalCopy?: (book: Book) => void;
|
|
handleBookMetadataUpdate?: (book: Book, updatedMetadata: BookMetadata) => void;
|
|
}
|
|
|
|
interface DeleteConfig {
|
|
title: string;
|
|
message: string;
|
|
handler?: (book: Book) => void;
|
|
}
|
|
|
|
const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
|
book,
|
|
isOpen,
|
|
onClose,
|
|
handleBookDownload,
|
|
handleBookUpload,
|
|
handleBookDelete,
|
|
handleBookDeleteCloudBackup,
|
|
handleBookDeleteLocalCopy,
|
|
handleBookMetadataUpdate,
|
|
}) => {
|
|
const _ = useTranslation();
|
|
const { envConfig } = useEnv();
|
|
const { safeAreaInsets } = useThemeStore();
|
|
const [loading, setLoading] = useState(false);
|
|
const [activeDeleteAction, setActiveDeleteAction] = useState<DeleteAction | null>(null);
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [bookMeta, setBookMeta] = useState<BookMetadata | null>(null);
|
|
const [fileSize, setFileSize] = useState<number | null>(null);
|
|
|
|
// Initialize metadata edit hook
|
|
const {
|
|
editedMeta,
|
|
fieldSources,
|
|
lockedFields,
|
|
fieldErrors,
|
|
searchLoading,
|
|
showSourceSelection,
|
|
availableSources,
|
|
handleFieldChange,
|
|
handleToggleFieldLock,
|
|
handleLockAll,
|
|
handleUnlockAll,
|
|
handleAutoRetrieve,
|
|
handleSourceSelection,
|
|
handleCloseSourceSelection,
|
|
resetToOriginal,
|
|
} = useMetadataEdit(bookMeta);
|
|
|
|
const deleteConfigs: Record<DeleteAction, DeleteConfig> = {
|
|
both: {
|
|
title: _('Confirm Deletion'),
|
|
message: _('Are you sure to delete the selected book?'),
|
|
handler: handleBookDelete,
|
|
},
|
|
cloud: {
|
|
title: _('Confirm Deletion'),
|
|
message: _('Are you sure to delete the cloud backup of the selected book?'),
|
|
handler: handleBookDeleteCloudBackup,
|
|
},
|
|
local: {
|
|
title: _('Confirm Deletion'),
|
|
message: _('Are you sure to delete the local copy of the selected book?'),
|
|
handler: handleBookDeleteLocalCopy,
|
|
},
|
|
};
|
|
|
|
useEffect(() => {
|
|
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
|
const fetchBookDetails = async () => {
|
|
const appService = await envConfig.getAppService();
|
|
try {
|
|
const details = book.metadata || (await appService.fetchBookDetails(book));
|
|
setBookMeta(details);
|
|
const size = await appService.getBookFileSize(book);
|
|
setFileSize(size);
|
|
} finally {
|
|
if (loadingTimeout) clearTimeout(loadingTimeout);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchBookDetails();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [book]);
|
|
|
|
const handleClose = () => {
|
|
setBookMeta(null);
|
|
setEditMode(false);
|
|
setActiveDeleteAction(null);
|
|
onClose();
|
|
};
|
|
|
|
const handleEditMetadata = () => {
|
|
setEditMode(true);
|
|
};
|
|
|
|
const handleCancelEdit = () => {
|
|
resetToOriginal();
|
|
setEditMode(false);
|
|
};
|
|
|
|
const handleSaveMetadata = () => {
|
|
if (editedMeta && handleBookMetadataUpdate) {
|
|
setBookMeta({ ...editedMeta });
|
|
handleBookMetadataUpdate(book, editedMeta);
|
|
setEditMode(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteAction = (action: DeleteAction) => {
|
|
setActiveDeleteAction(action);
|
|
};
|
|
|
|
const confirmDeleteAction = async () => {
|
|
if (!activeDeleteAction) return;
|
|
|
|
const config = deleteConfigs[activeDeleteAction];
|
|
handleClose();
|
|
|
|
if (config.handler) {
|
|
config.handler(book);
|
|
}
|
|
};
|
|
|
|
const cancelDeleteAction = () => {
|
|
setActiveDeleteAction(null);
|
|
};
|
|
|
|
const handleDelete = () => handleDeleteAction('both');
|
|
const handleDeleteCloudBackup = () => handleDeleteAction('cloud');
|
|
const handleDeleteLocalCopy = () => handleDeleteAction('local');
|
|
|
|
const handleRedownload = async () => {
|
|
handleClose();
|
|
if (handleBookDownload) {
|
|
handleBookDownload(book, true);
|
|
}
|
|
};
|
|
|
|
const handleReupload = async () => {
|
|
handleClose();
|
|
if (handleBookUpload) {
|
|
handleBookUpload(book);
|
|
}
|
|
};
|
|
|
|
const currentDeleteConfig = activeDeleteAction ? deleteConfigs[activeDeleteAction] : null;
|
|
|
|
if (!bookMeta)
|
|
return (
|
|
loading && (
|
|
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
|
<Spinner loading />
|
|
</div>
|
|
)
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
|
<Dialog
|
|
title={editMode ? _('Edit Metadata') : _('Book Details')}
|
|
isOpen={isOpen}
|
|
onClose={handleClose}
|
|
boxClassName={clsx(
|
|
editMode ? 'sm:min-w-[600px] sm:max-w-[600px]' : 'sm:min-w-[480px] sm:max-w-[480px]',
|
|
'sm:h-auto sm:max-h-[90%]',
|
|
)}
|
|
contentClassName='!px-6 !py-4'
|
|
>
|
|
<div className='flex w-full select-text items-start justify-center'>
|
|
{editMode ? (
|
|
<BookDetailEdit
|
|
book={book}
|
|
metadata={editedMeta}
|
|
fieldSources={fieldSources}
|
|
lockedFields={lockedFields}
|
|
fieldErrors={fieldErrors}
|
|
searchLoading={searchLoading}
|
|
onFieldChange={handleFieldChange}
|
|
onToggleFieldLock={handleToggleFieldLock}
|
|
onAutoRetrieve={handleAutoRetrieve}
|
|
onLockAll={handleLockAll}
|
|
onUnlockAll={handleUnlockAll}
|
|
onCancel={handleCancelEdit}
|
|
onReset={resetToOriginal}
|
|
onSave={handleSaveMetadata}
|
|
/>
|
|
) : (
|
|
<BookDetailView
|
|
book={book}
|
|
metadata={bookMeta}
|
|
fileSize={fileSize}
|
|
onEdit={handleBookMetadataUpdate ? handleEditMetadata : undefined}
|
|
onDelete={handleBookDelete ? handleDelete : undefined}
|
|
onDeleteCloudBackup={
|
|
handleBookDeleteCloudBackup ? handleDeleteCloudBackup : undefined
|
|
}
|
|
onDeleteLocalCopy={handleBookDeleteLocalCopy ? handleDeleteLocalCopy : undefined}
|
|
onDownload={handleBookDownload ? handleRedownload : undefined}
|
|
onUpload={handleBookUpload ? handleReupload : undefined}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Source Selection Modal */}
|
|
{showSourceSelection && (
|
|
<SourceSelector
|
|
sources={availableSources}
|
|
isOpen={showSourceSelection}
|
|
onSelect={handleSourceSelection}
|
|
onClose={handleCloseSourceSelection}
|
|
/>
|
|
)}
|
|
|
|
{activeDeleteAction && currentDeleteConfig && (
|
|
<div
|
|
className={clsx('fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
|
|
style={{
|
|
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
|
|
}}
|
|
>
|
|
<Alert
|
|
title={currentDeleteConfig.title}
|
|
message={currentDeleteConfig.message}
|
|
onCancel={cancelDeleteAction}
|
|
onConfirm={confirmDeleteAction}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default BookDetailModal;
|