forked from akai/readest
674 lines
21 KiB
TypeScript
674 lines
21 KiB
TypeScript
'use client';
|
|
|
|
import clsx from 'clsx';
|
|
import * as React from 'react';
|
|
import { useState, useRef, useEffect, Suspense } from 'react';
|
|
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation';
|
|
|
|
import { Book } from '@/types/book';
|
|
import { AppService } from '@/types/system';
|
|
import { navigateToLogin, navigateToReader } from '@/utils/nav';
|
|
import { getFilename, listFormater } from '@/utils/book';
|
|
import { eventDispatcher } from '@/utils/event';
|
|
import { ProgressPayload } from '@/utils/transfer';
|
|
import { throttle } from '@/utils/throttle';
|
|
import { parseOpenWithFiles } from '@/helpers/openWith';
|
|
import { isTauriAppPlatform } from '@/services/environment';
|
|
import { checkForAppUpdates } from '@/helpers/updater';
|
|
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
|
|
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
|
import { getCurrentWebview } from '@tauri-apps/api/webview';
|
|
|
|
import { useEnv } from '@/context/EnvContext';
|
|
import { useAuth } from '@/context/AuthContext';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { useLibraryStore } from '@/store/libraryStore';
|
|
import { useSettingsStore } from '@/store/settingsStore';
|
|
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
|
|
import { useTheme } from '@/hooks/useTheme';
|
|
import { useDemoBooks } from './hooks/useDemoBooks';
|
|
import { useBooksSync } from './hooks/useBooksSync';
|
|
import { useThemeStore } from '@/store/themeStore';
|
|
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
|
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
|
|
import { lockScreenOrientation } from '@/utils/bridge';
|
|
import { mountAdditionalFonts } from '@/utils/style';
|
|
import {
|
|
tauriHandleSetAlwaysOnTop,
|
|
tauriHandleToggleFullScreen,
|
|
tauriQuitApp,
|
|
} from '@/utils/window';
|
|
|
|
import { AboutWindow } from '@/components/AboutWindow';
|
|
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
|
import { Toast } from '@/components/Toast';
|
|
import Spinner from '@/components/Spinner';
|
|
import LibraryHeader from './components/LibraryHeader';
|
|
import Bookshelf from './components/Bookshelf';
|
|
import BookDetailModal from '@/components/BookDetailModal';
|
|
import useShortcuts from '@/hooks/useShortcuts';
|
|
import DropIndicator from '@/components/DropIndicator';
|
|
|
|
const LibraryPageWithSearchParams = () => {
|
|
const searchParams = useSearchParams();
|
|
return <LibraryPageContent searchParams={searchParams} />;
|
|
};
|
|
|
|
const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchParams | null }) => {
|
|
const router = useRouter();
|
|
const { envConfig, appService } = useEnv();
|
|
const { token, user } = useAuth();
|
|
const {
|
|
library: libraryBooks,
|
|
updateBook,
|
|
setLibrary,
|
|
checkOpenWithBooks,
|
|
checkLastOpenBooks,
|
|
setCheckOpenWithBooks,
|
|
setCheckLastOpenBooks,
|
|
} = useLibraryStore();
|
|
const _ = useTranslation();
|
|
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
|
|
const { settings, setSettings, saveSettings } = useSettingsStore();
|
|
const { statusBarHeight } = useThemeStore();
|
|
const [loading, setLoading] = useState(false);
|
|
const isInitiating = useRef(false);
|
|
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
|
const [isSelectMode, setIsSelectMode] = useState(false);
|
|
const [isSelectAll, setIsSelectAll] = useState(false);
|
|
const [isSelectNone, setIsSelectNone] = useState(false);
|
|
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
|
|
const [booksTransferProgress, setBooksTransferProgress] = useState<{
|
|
[key: string]: number | null;
|
|
}>({});
|
|
const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState<string[] | null>(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const demoBooks = useDemoBooks();
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const pageRef = useRef<HTMLDivElement>(null);
|
|
|
|
useOpenWithBooks();
|
|
|
|
const { pullLibrary, pushLibrary } = useBooksSync({
|
|
onSyncStart: () => setLoading(true),
|
|
onSyncEnd: () => setLoading(false),
|
|
});
|
|
|
|
usePullToRefresh(containerRef, pullLibrary);
|
|
useScreenWakeLock(settings.screenWakeLock);
|
|
|
|
useShortcuts({
|
|
onToggleFullscreen: async () => {
|
|
if (isTauriAppPlatform()) {
|
|
await tauriHandleToggleFullScreen();
|
|
}
|
|
},
|
|
onQuitApp: async () => {
|
|
if (isTauriAppPlatform()) {
|
|
await tauriQuitApp();
|
|
}
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
mountAdditionalFonts(document);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const doCheckAppUpdates = async () => {
|
|
if (appService?.hasUpdater && settings.autoCheckUpdates) {
|
|
await checkForAppUpdates(_);
|
|
}
|
|
};
|
|
if (settings.alwaysOnTop) {
|
|
tauriHandleSetAlwaysOnTop(settings.alwaysOnTop);
|
|
}
|
|
doCheckAppUpdates();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [settings]);
|
|
|
|
useEffect(() => {
|
|
if (appService?.isMobileApp) {
|
|
lockScreenOrientation({ orientation: 'auto' });
|
|
}
|
|
}, [appService]);
|
|
|
|
const handleDropedFiles = async (files: File[] | string[]) => {
|
|
if (files.length === 0) return;
|
|
const supportedFiles = files.filter((file) => {
|
|
let fileExt;
|
|
if (typeof file === 'string') {
|
|
fileExt = file.split('.').pop()?.toLowerCase();
|
|
} else {
|
|
fileExt = file.name.split('.').pop()?.toLowerCase();
|
|
}
|
|
return FILE_ACCEPT_FORMATS.includes(`.${fileExt}`);
|
|
});
|
|
if (supportedFiles.length === 0) {
|
|
eventDispatcher.dispatch('toast', {
|
|
message: _('No supported files found. Supported formats: {{formats}}', {
|
|
formats: FILE_ACCEPT_FORMATS,
|
|
}),
|
|
type: 'error',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (appService?.hasHaptics) {
|
|
impactFeedback('medium');
|
|
}
|
|
|
|
await importBooks(supportedFiles);
|
|
};
|
|
|
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement> | DragEvent) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setIsDragging(true);
|
|
};
|
|
|
|
const handleDragLeave = (event: React.DragEvent<HTMLDivElement> | DragEvent) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setIsDragging(false);
|
|
};
|
|
|
|
const handleDrop = async (event: React.DragEvent<HTMLDivElement> | DragEvent) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setIsDragging(false);
|
|
|
|
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
|
|
const files = Array.from(event.dataTransfer.files);
|
|
handleDropedFiles(files);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const libraryPage = document.querySelector('.library-page');
|
|
if (!appService?.isMobile) {
|
|
libraryPage?.addEventListener('dragover', handleDragOver as unknown as EventListener);
|
|
libraryPage?.addEventListener('dragleave', handleDragLeave as unknown as EventListener);
|
|
libraryPage?.addEventListener('drop', handleDrop as unknown as EventListener);
|
|
}
|
|
|
|
if (isTauriAppPlatform()) {
|
|
const unlisten = getCurrentWebview().onDragDropEvent((event) => {
|
|
if (event.payload.type === 'over') {
|
|
setIsDragging(true);
|
|
} else if (event.payload.type === 'drop') {
|
|
setIsDragging(false);
|
|
handleDropedFiles(event.payload.paths);
|
|
} else {
|
|
setIsDragging(false);
|
|
}
|
|
});
|
|
return () => {
|
|
unlisten.then((fn) => fn());
|
|
};
|
|
}
|
|
|
|
return () => {
|
|
if (!appService?.isMobile) {
|
|
libraryPage?.removeEventListener('dragover', handleDragOver as unknown as EventListener);
|
|
libraryPage?.removeEventListener('dragleave', handleDragLeave as unknown as EventListener);
|
|
libraryPage?.removeEventListener('drop', handleDrop as unknown as EventListener);
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [pageRef.current]);
|
|
|
|
const processOpenWithFiles = React.useCallback(
|
|
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
|
|
const settings = await appService.loadSettings();
|
|
const bookIds: string[] = [];
|
|
for (const file of openWithFiles) {
|
|
console.log('Open with book:', file);
|
|
try {
|
|
const temp = appService.isMobile ? false : !settings.autoImportBooksOnOpen;
|
|
const book = await appService.importBook(file, libraryBooks, true, true, false, temp);
|
|
if (book) {
|
|
bookIds.push(book.hash);
|
|
}
|
|
} catch (error) {
|
|
console.log('Failed to import book:', file, error);
|
|
}
|
|
}
|
|
setLibrary(libraryBooks);
|
|
appService.saveLibraryBooks(libraryBooks);
|
|
|
|
console.log('Opening books:', bookIds);
|
|
if (bookIds.length > 0) {
|
|
setPendingNavigationBookIds(bookIds);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[],
|
|
);
|
|
|
|
const handleOpenLastBooks = async (
|
|
appService: AppService,
|
|
lastBookIds: string[],
|
|
libraryBooks: Book[],
|
|
) => {
|
|
if (lastBookIds.length === 0) return false;
|
|
const bookIds: string[] = [];
|
|
for (const bookId of lastBookIds) {
|
|
const book = libraryBooks.find((b) => b.hash === bookId);
|
|
if (book && (await appService.isBookAvailable(book))) {
|
|
bookIds.push(book.hash);
|
|
}
|
|
}
|
|
console.log('Opening last books:', bookIds);
|
|
if (bookIds.length > 0) {
|
|
setPendingNavigationBookIds(bookIds);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (pendingNavigationBookIds) {
|
|
const bookIds = pendingNavigationBookIds;
|
|
setPendingNavigationBookIds(null);
|
|
if (bookIds.length > 0) {
|
|
navigateToReader(router, bookIds);
|
|
}
|
|
}
|
|
}, [pendingNavigationBookIds, router]);
|
|
|
|
useEffect(() => {
|
|
if (isInitiating.current) return;
|
|
isInitiating.current = true;
|
|
|
|
const initLogin = async () => {
|
|
const appService = await envConfig.getAppService();
|
|
const settings = await appService.loadSettings();
|
|
if (token && user) {
|
|
if (!settings.keepLogin) {
|
|
settings.keepLogin = true;
|
|
setSettings(settings);
|
|
saveSettings(envConfig, settings);
|
|
}
|
|
} else if (settings.keepLogin) {
|
|
router.push('/auth');
|
|
}
|
|
};
|
|
|
|
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
|
const initLibrary = async () => {
|
|
const appService = await envConfig.getAppService();
|
|
const settings = await appService.loadSettings();
|
|
setSettings(settings);
|
|
|
|
// Reuse the library from the store when we return from the reader
|
|
const library = libraryBooks.length > 0 ? libraryBooks : await appService.loadLibraryBooks();
|
|
setCheckOpenWithBooks(checkOpenWithBooks && (await handleOpenWithBooks(appService, library)));
|
|
setCheckLastOpenBooks(
|
|
checkLastOpenBooks &&
|
|
settings.openLastBooks &&
|
|
(await handleOpenLastBooks(appService, settings.lastOpenBooks, library)),
|
|
);
|
|
|
|
setLibrary(library);
|
|
setLibraryLoaded(true);
|
|
if (loadingTimeout) clearTimeout(loadingTimeout);
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleOpenWithBooks = async (appService: AppService, library: Book[]) => {
|
|
const openWithFiles = (await parseOpenWithFiles()) || [];
|
|
|
|
if (openWithFiles.length > 0) {
|
|
return await processOpenWithFiles(appService, openWithFiles, library);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
initLogin();
|
|
initLibrary();
|
|
return () => {
|
|
setCheckOpenWithBooks(false);
|
|
setCheckLastOpenBooks(false);
|
|
isInitiating.current = false;
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [searchParams]);
|
|
|
|
useEffect(() => {
|
|
if (demoBooks.length > 0 && libraryLoaded) {
|
|
const newLibrary = [...libraryBooks];
|
|
for (const book of demoBooks) {
|
|
const idx = newLibrary.findIndex((b) => b.hash === book.hash);
|
|
if (idx === -1) {
|
|
newLibrary.push(book);
|
|
} else {
|
|
newLibrary[idx] = book;
|
|
}
|
|
}
|
|
setLibrary(newLibrary);
|
|
appService?.saveLibraryBooks(newLibrary);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [demoBooks, libraryLoaded]);
|
|
|
|
const importBooks = async (files: (string | File)[]) => {
|
|
setLoading(true);
|
|
const failedFiles = [];
|
|
const errorMap: [string, string][] = [
|
|
['No chapters detected.', _('No chapters detected.')],
|
|
['Failed to parse EPUB.', _('Failed to parse the EPUB file.')],
|
|
['Unsupported format.', _('This book format is not supported.')],
|
|
];
|
|
const { library } = useLibraryStore.getState();
|
|
for (const file of files) {
|
|
try {
|
|
const book = await appService?.importBook(file, library);
|
|
setLibrary(library);
|
|
if (user && book && !book.uploadedAt && settings.autoUpload) {
|
|
console.log('Uploading book:', book.title);
|
|
handleBookUpload(book);
|
|
}
|
|
} catch (error) {
|
|
const filename = typeof file === 'string' ? file : file.name;
|
|
const baseFilename = getFilename(filename);
|
|
failedFiles.push(baseFilename);
|
|
const errorMessage =
|
|
error instanceof Error
|
|
? errorMap.find(([substring]) => error.message.includes(substring))?.[1] || ''
|
|
: '';
|
|
eventDispatcher.dispatch('toast', {
|
|
message:
|
|
_('Failed to import book(s): {{filenames}}', {
|
|
filenames: listFormater(false).format(failedFiles),
|
|
}) + (errorMessage ? `\n${errorMessage}` : ''),
|
|
type: 'error',
|
|
});
|
|
console.error('Failed to import book:', filename, error);
|
|
}
|
|
}
|
|
appService?.saveLibraryBooks(library);
|
|
setLoading(false);
|
|
};
|
|
|
|
const selectFilesTauri = async () => {
|
|
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
|
|
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
|
|
// Cannot filter out files on Android since some content providers may not return the file name
|
|
return files;
|
|
};
|
|
|
|
const selectFilesWeb = () => {
|
|
return new Promise((resolve) => {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = FILE_ACCEPT_FORMATS;
|
|
fileInput.multiple = true;
|
|
fileInput.click();
|
|
|
|
fileInput.onchange = () => {
|
|
resolve(fileInput.files);
|
|
};
|
|
});
|
|
};
|
|
|
|
const updateBookTransferProgress = throttle((bookHash: string, progress: ProgressPayload) => {
|
|
if (progress.total === 0) return;
|
|
const progressPct = (progress.progress / progress.total) * 100;
|
|
setBooksTransferProgress((prev) => ({
|
|
...prev,
|
|
[bookHash]: progressPct,
|
|
}));
|
|
}, 500);
|
|
|
|
const handleBookUpload = async (book: Book) => {
|
|
try {
|
|
await appService?.uploadBook(book, (progress) => {
|
|
updateBookTransferProgress(book.hash, progress);
|
|
});
|
|
await updateBook(envConfig, book);
|
|
pushLibrary();
|
|
eventDispatcher.dispatch('toast', {
|
|
type: 'info',
|
|
timeout: 2000,
|
|
message: _('Book uploaded: {{title}}', {
|
|
title: book.title,
|
|
}),
|
|
});
|
|
return true;
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
if (err.message.includes('Not authenticated') && settings.keepLogin) {
|
|
settings.keepLogin = false;
|
|
setSettings(settings);
|
|
navigateToLogin(router);
|
|
return false;
|
|
} else if (err.message.includes('Insufficient storage quota')) {
|
|
eventDispatcher.dispatch('toast', {
|
|
type: 'error',
|
|
message: _('Insufficient storage quota'),
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
eventDispatcher.dispatch('toast', {
|
|
type: 'error',
|
|
message: _('Failed to upload book: {{title}}', {
|
|
title: book.title,
|
|
}),
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleBookDownload = async (book: Book) => {
|
|
try {
|
|
await appService?.downloadBook(book, false, (progress) => {
|
|
updateBookTransferProgress(book.hash, progress);
|
|
});
|
|
await updateBook(envConfig, book);
|
|
eventDispatcher.dispatch('toast', {
|
|
type: 'info',
|
|
timeout: 2000,
|
|
message: _('Book downloaded: {{title}}', {
|
|
title: book.title,
|
|
}),
|
|
});
|
|
return true;
|
|
} catch {
|
|
eventDispatcher.dispatch('toast', {
|
|
message: _('Failed to download book: {{title}}', {
|
|
title: book.title,
|
|
}),
|
|
type: 'error',
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleBookDelete = async (book: Book) => {
|
|
try {
|
|
await appService?.deleteBook(book, !!book.uploadedAt);
|
|
await updateBook(envConfig, book);
|
|
pushLibrary();
|
|
eventDispatcher.dispatch('toast', {
|
|
type: 'info',
|
|
timeout: 2000,
|
|
message: _('Book deleted: {{title}}', {
|
|
title: book.title,
|
|
}),
|
|
});
|
|
return true;
|
|
} catch {
|
|
eventDispatcher.dispatch('toast', {
|
|
message: _('Failed to delete book: {{title}}', {
|
|
title: book.title,
|
|
}),
|
|
type: 'error',
|
|
});
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleImportBooks = async () => {
|
|
setIsSelectMode(false);
|
|
console.log('Importing books...');
|
|
let files;
|
|
|
|
if (isTauriAppPlatform()) {
|
|
if (appService?.isIOSApp) {
|
|
files = (await selectFilesWeb()) as [File];
|
|
} else {
|
|
files = (await selectFilesTauri()) as [string];
|
|
}
|
|
} else {
|
|
files = (await selectFilesWeb()) as [File];
|
|
}
|
|
importBooks(files);
|
|
};
|
|
|
|
const handleSetSelectMode = (selectMode: boolean) => {
|
|
if (selectMode && appService?.hasHaptics) {
|
|
impactFeedback('medium');
|
|
}
|
|
setIsSelectMode(selectMode);
|
|
setIsSelectAll(false);
|
|
setIsSelectNone(false);
|
|
};
|
|
|
|
const handleSelectAll = () => {
|
|
setIsSelectAll(true);
|
|
setIsSelectNone(false);
|
|
};
|
|
|
|
const handleDeselectAll = () => {
|
|
setIsSelectNone(true);
|
|
setIsSelectAll(false);
|
|
};
|
|
|
|
const handleShowDetailsBook = (book: Book) => {
|
|
setShowDetailsBook(book);
|
|
};
|
|
|
|
if (!appService) {
|
|
return null;
|
|
}
|
|
|
|
if (checkOpenWithBooks || checkLastOpenBooks) {
|
|
return (
|
|
loading && (
|
|
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
|
<Spinner loading />
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={pageRef}
|
|
className={clsx(
|
|
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
|
|
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
|
appService?.hasRoundedWindow && 'rounded-window',
|
|
)}
|
|
>
|
|
<div className='fixed top-0 z-40 w-full'>
|
|
<LibraryHeader
|
|
isSelectMode={isSelectMode}
|
|
isSelectAll={isSelectAll}
|
|
onImportBooks={handleImportBooks}
|
|
onToggleSelectMode={() => handleSetSelectMode(!isSelectMode)}
|
|
onSelectAll={handleSelectAll}
|
|
onDeselectAll={handleDeselectAll}
|
|
/>
|
|
</div>
|
|
{loading && (
|
|
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
|
<Spinner loading />
|
|
</div>
|
|
)}
|
|
{libraryLoaded &&
|
|
(libraryBooks.some((book) => !book.deletedAt) ? (
|
|
<div
|
|
ref={containerRef}
|
|
className={clsx(
|
|
'scroll-container drop-zone flex-grow overflow-y-auto',
|
|
appService?.hasSafeAreaInset && 'pt-[52px]',
|
|
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
|
|
isDragging && 'drag-over',
|
|
)}
|
|
style={{
|
|
marginTop: appService?.hasSafeAreaInset
|
|
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
|
|
: '48px',
|
|
}}
|
|
>
|
|
<DropIndicator />
|
|
<Bookshelf
|
|
libraryBooks={libraryBooks}
|
|
isSelectMode={isSelectMode}
|
|
isSelectAll={isSelectAll}
|
|
isSelectNone={isSelectNone}
|
|
handleImportBooks={handleImportBooks}
|
|
handleBookUpload={handleBookUpload}
|
|
handleBookDownload={handleBookDownload}
|
|
handleBookDelete={handleBookDelete}
|
|
handleSetSelectMode={handleSetSelectMode}
|
|
handleShowDetailsBook={handleShowDetailsBook}
|
|
booksTransferProgress={booksTransferProgress}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className='hero drop-zone h-screen items-center justify-center'>
|
|
<DropIndicator />
|
|
<div className='hero-content text-neutral-content text-center'>
|
|
<div className='max-w-md'>
|
|
<h1 className='mb-5 text-5xl font-bold'>{_('Your Library')}</h1>
|
|
<p className='mb-5'>
|
|
{_(
|
|
'Welcome to your library. You can import your books here and read them anytime.',
|
|
)}
|
|
</p>
|
|
<button className='btn btn-primary rounded-xl' onClick={handleImportBooks}>
|
|
{_('Import Books')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{showDetailsBook && (
|
|
<BookDetailModal
|
|
isOpen={!!showDetailsBook}
|
|
book={showDetailsBook}
|
|
onClose={() => setShowDetailsBook(null)}
|
|
handleBookUpload={handleBookUpload}
|
|
handleBookDownload={handleBookDownload}
|
|
handleBookDelete={handleBookDelete}
|
|
/>
|
|
)}
|
|
<AboutWindow />
|
|
{appService?.isAndroidApp && <UpdaterWindow />}
|
|
<Toast />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const LibraryPage = () => {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
|
<Spinner loading />
|
|
</div>
|
|
}
|
|
>
|
|
<LibraryPageWithSearchParams />
|
|
</Suspense>
|
|
);
|
|
};
|
|
|
|
export default LibraryPage;
|