Display multiple books in grid split
This commit is contained in:
@@ -29,6 +29,14 @@ body {
|
||||
foliate-view {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.foliate-viewer {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.header-bar-anim:hover + .foliate-viewer {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const LibraryPage = () => {
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
setLoading(false);
|
||||
});
|
||||
}, [envConfig, libraryBooks, setLibrary, getAppService]);
|
||||
}, []);
|
||||
|
||||
const importBooks = async (files: [string | File]) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
type FoliateViewerProps = {
|
||||
bookId: string;
|
||||
bookConfig: BookConfig;
|
||||
bookDoc: BookDoc;
|
||||
};
|
||||
|
||||
const getCSS = (spacing: number, justify: boolean, hyphenate: boolean) => `
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
html {
|
||||
@@ -63,12 +55,18 @@ export interface FoliateView extends HTMLElement {
|
||||
};
|
||||
}
|
||||
|
||||
const FoliateViewer: React.FC<FoliateViewerProps> = ({ bookId, bookConfig, bookDoc }) => {
|
||||
const FoliateViewer: React.FC<{
|
||||
bookKey: string;
|
||||
bookDoc: BookDoc;
|
||||
bookConfig: BookConfig;
|
||||
}> = ({ bookKey, bookDoc, bookConfig }) => {
|
||||
const viewRef = useRef<HTMLDivElement>(null);
|
||||
const [view, setView] = useState<FoliateView | null>(null);
|
||||
const isViewCreated = useRef(false);
|
||||
const { setFoliateView } = useReaderStore();
|
||||
|
||||
useFoliateEvents(bookKey, view);
|
||||
|
||||
useEffect(() => {
|
||||
if (isViewCreated.current) return;
|
||||
const openBook = async () => {
|
||||
@@ -78,7 +76,8 @@ const FoliateViewer: React.FC<FoliateViewerProps> = ({ bookId, bookConfig, bookD
|
||||
document.body.append(view);
|
||||
viewRef.current?.appendChild(view);
|
||||
setView(view);
|
||||
setFoliateView(view);
|
||||
setFoliateView(bookKey, view);
|
||||
|
||||
await view.open(bookDoc);
|
||||
if ('setStyles' in view.renderer) {
|
||||
view.renderer.setStyles(getCSS(2.4, true, true));
|
||||
@@ -95,8 +94,6 @@ const FoliateViewer: React.FC<FoliateViewerProps> = ({ bookId, bookConfig, bookD
|
||||
isViewCreated.current = true;
|
||||
}, [bookDoc]);
|
||||
|
||||
useFoliateEvents(view, bookId);
|
||||
|
||||
const handleTap = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { clientX } = event;
|
||||
const width = window.innerWidth;
|
||||
@@ -110,7 +107,7 @@ const FoliateViewer: React.FC<FoliateViewerProps> = ({ bookId, bookConfig, bookD
|
||||
}
|
||||
};
|
||||
|
||||
return <div ref={viewRef} onClick={handleTap} />;
|
||||
return <div className='foliate-viewer h-[100%] w-[100%]' ref={viewRef} onClick={handleTap} />;
|
||||
};
|
||||
|
||||
export default FoliateViewer;
|
||||
|
||||
@@ -4,10 +4,10 @@ import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { BsLayoutSidebar } from 'react-icons/bs';
|
||||
import { VscLayoutSidebarLeft, VscLayoutSidebarLeftOff } from 'react-icons/vsc';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
import { useReaderStore, DEFAULT_BOOK_STATE } from '@/store/readerStore';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
@@ -16,13 +16,10 @@ import SideBar from './SideBar';
|
||||
const ReaderContent = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams.get('id');
|
||||
const ids = searchParams.getAll('id') || [];
|
||||
|
||||
const [bookDoc, setBookDoc] = React.useState<BookDoc>();
|
||||
const { envConfig } = useEnv();
|
||||
const { books, settings, fetchBook, saveConfig, saveSettings } = useReaderStore();
|
||||
const defaultBookState = { loading: true, error: null, file: null, book: null, config: null };
|
||||
const bookState = id ? books[id] || defaultBookState : defaultBookState;
|
||||
const { books, settings, initBookState, saveConfig, saveSettings } = useReaderStore();
|
||||
|
||||
const [sideBarWidth, setSideBarWidth] = useState(
|
||||
settings.globalReadSettings.sideBarWidth ?? '25%',
|
||||
@@ -31,26 +28,23 @@ const ReaderContent = () => {
|
||||
settings.globalReadSettings.isSideBarPinned ?? true,
|
||||
);
|
||||
const [isSideBarVisible, setSideBarVisibility] = useState(isSideBarPinned);
|
||||
const [isTopBarVisible, setTopBarVisibility] = useState(false);
|
||||
|
||||
const getKey = (id: string, index: number) => `${id}-${index}`;
|
||||
const bookStates = ids.map((id, index) => books[getKey(id, index)] || DEFAULT_BOOK_STATE);
|
||||
const [sideBarBookKey, setSideBarBookKey] = useState(getKey(ids[0] ?? '', 0));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const { file } = bookState;
|
||||
if (id && !file) {
|
||||
fetchBook(envConfig, id);
|
||||
}
|
||||
if (!bookDoc && file) {
|
||||
const loadDocument = async () => {
|
||||
if (file) {
|
||||
const { book } = await new DocumentLoader(file).open();
|
||||
setBookDoc(book);
|
||||
}
|
||||
};
|
||||
loadDocument();
|
||||
}
|
||||
}, [bookState.file, envConfig, fetchBook, id]);
|
||||
if (ids.length === 0) return;
|
||||
const uniqueIds = new Set<string>();
|
||||
ids.forEach((id, index) => {
|
||||
const isPrimary = !uniqueIds.has(id);
|
||||
uniqueIds.add(id);
|
||||
const key = getKey(id, index);
|
||||
if (books[key]) return;
|
||||
console.log('fetching book', key);
|
||||
initBookState(envConfig, id, key, isPrimary);
|
||||
});
|
||||
}, [ids, settings]);
|
||||
|
||||
const handleResize = (newWidth: string) => {
|
||||
setSideBarWidth(newWidth);
|
||||
@@ -66,21 +60,46 @@ const ReaderContent = () => {
|
||||
};
|
||||
|
||||
const handleCloseBook = () => {
|
||||
const { book, config } = bookState;
|
||||
if (book && config) {
|
||||
saveConfig(envConfig, book, config);
|
||||
saveSettings(envConfig, settings);
|
||||
}
|
||||
bookStates.forEach((bookState) => {
|
||||
const { book, config, isPrimary } = bookState;
|
||||
if (isPrimary && book && config) {
|
||||
saveConfig(envConfig, book, config);
|
||||
}
|
||||
});
|
||||
saveSettings(envConfig, settings);
|
||||
router.back();
|
||||
};
|
||||
|
||||
const topBarWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
const getGridTemplate = () => {
|
||||
const count = ids.length;
|
||||
const aspectRatio = window.innerWidth / window.innerHeight;
|
||||
|
||||
if (!id || !bookDoc || !bookState.config || !bookState.book) {
|
||||
if (count === 1) {
|
||||
// One book, full screen
|
||||
return { columns: '1fr', rows: '1fr' };
|
||||
} else if (count === 2) {
|
||||
if (aspectRatio < 1) {
|
||||
// portrait mode: vertical split
|
||||
return { columns: '1fr', rows: '1fr 1fr' };
|
||||
} else {
|
||||
// landscape mode: horizontal split
|
||||
return { columns: '1fr 1fr', rows: '1fr' };
|
||||
}
|
||||
} else if (count === 3 || count === 4) {
|
||||
// Three or four books, 2x2 grid
|
||||
return { columns: '1fr 1fr', rows: '1fr 1fr' };
|
||||
} else {
|
||||
// Five or more books, a 3x3 grid
|
||||
return { columns: '1fr 1fr 1fr', rows: '1fr 1fr 1fr' };
|
||||
}
|
||||
};
|
||||
|
||||
const bookState = bookStates[0];
|
||||
if (bookStates.length !== ids.length || !bookState || !bookState.book || !bookState.bookDoc) {
|
||||
return (
|
||||
<div className={'flex-1 overflow-hidden'}>
|
||||
{bookState.loading && <Spinner loading={bookState.loading} />}
|
||||
{bookState.error && (
|
||||
<div className={'flex-1'}>
|
||||
<Spinner loading={true} />
|
||||
{bookState?.error && (
|
||||
<div className='text-center'>
|
||||
<h2 className='text-red-500'>{bookState.error}</h2>
|
||||
</div>
|
||||
@@ -89,12 +108,12 @@ const ReaderContent = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const gridWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
|
||||
return (
|
||||
<div className='flex h-screen'>
|
||||
<SideBar
|
||||
book={bookState.book}
|
||||
tocData={bookDoc.toc}
|
||||
currentHref={bookState.config.href ?? null}
|
||||
bookKey={sideBarBookKey}
|
||||
width={sideBarWidth}
|
||||
isVisible={isSideBarVisible}
|
||||
isPinned={isSideBarPinned}
|
||||
@@ -104,25 +123,53 @@ const ReaderContent = () => {
|
||||
onSetVisibility={(visibility: boolean) => setSideBarVisibility(visibility)}
|
||||
/>
|
||||
|
||||
<div className={'flex-1 overflow-hidden'}>
|
||||
<div
|
||||
className={`topbar absolute top-0 z-10 h-10 border-b ${
|
||||
isTopBarVisible ? 'opacity-100' : 'opacity-0'
|
||||
} flex items-center px-4`}
|
||||
style={{ width: topBarWidth }}
|
||||
onMouseEnter={() => setTopBarVisibility(true)}
|
||||
onMouseLeave={() => setTopBarVisibility(false)}
|
||||
>
|
||||
<div className='absolute left-4 flex h-full items-center p-2'>
|
||||
<button onClick={() => setSideBarVisibility(!isSideBarVisible)}>
|
||||
<BsLayoutSidebar size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className='absolute left-1/2 -translate-x-1/2 transform'>
|
||||
<h2 className='text-center text-sm font-semibold'>{bookState.book.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<FoliateViewer bookId={id} bookDoc={bookDoc} bookConfig={bookState.config} />
|
||||
<div
|
||||
className='grid h-full'
|
||||
style={{
|
||||
width: gridWidth,
|
||||
gridTemplateColumns: getGridTemplate().columns,
|
||||
gridTemplateRows: getGridTemplate().rows,
|
||||
}}
|
||||
>
|
||||
{bookStates.map((bookState, index) => {
|
||||
const key = getKey(ids[index]!, index);
|
||||
const { book, config, bookDoc } = bookState;
|
||||
if (!book || !config || !bookDoc) return null;
|
||||
return (
|
||||
<div key={key} className='relative h-full w-full overflow-hidden'>
|
||||
<div
|
||||
className={`absolute top-0 z-10 flex h-10 w-full items-center px-4 ${
|
||||
ids.length > 1 ? 'header-bar-anim' : 'header-bar'
|
||||
} shadow-xs opacity-0 transition-opacity duration-300 hover:opacity-100`}
|
||||
>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<h2 className='line-clamp-1 max-w-[90%] px-2 text-center text-xs font-semibold'>
|
||||
{book?.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className='absolute left-4 flex h-full items-center p-2'>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isSideBarVisible) {
|
||||
setSideBarVisibility(true);
|
||||
} else if (sideBarBookKey === key) {
|
||||
setSideBarVisibility(false);
|
||||
}
|
||||
setSideBarBookKey(key);
|
||||
}}
|
||||
>
|
||||
{sideBarBookKey == key && isSideBarVisible ? (
|
||||
<VscLayoutSidebarLeft size={16} />
|
||||
) : (
|
||||
<VscLayoutSidebarLeftOff size={16} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<FoliateViewer bookKey={key} bookDoc={bookDoc!} bookConfig={config!} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,17 +10,15 @@ import {
|
||||
} from 'react-icons/md';
|
||||
import { GiBookshelf } from 'react-icons/gi';
|
||||
|
||||
import { TOCItem } from '@/libs/document';
|
||||
import BookCard from './BookCard';
|
||||
import TOCView from './TOCView';
|
||||
import { BookState, DEFAULT_BOOK_STATE, useReaderStore } from '@/store/readerStore';
|
||||
|
||||
const MIN_SIDEBAR_WIDTH = 0.15;
|
||||
const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
|
||||
const SideBar: React.FC<{
|
||||
book: Book;
|
||||
tocData: TOCItem[];
|
||||
currentHref: string | null;
|
||||
bookKey: string;
|
||||
width: string;
|
||||
isVisible: boolean;
|
||||
isPinned: boolean;
|
||||
@@ -29,9 +27,7 @@ const SideBar: React.FC<{
|
||||
onResize: (newWidth: string) => void;
|
||||
onGoToLibrary: () => void;
|
||||
}> = ({
|
||||
book,
|
||||
tocData,
|
||||
currentHref,
|
||||
bookKey,
|
||||
width,
|
||||
isPinned,
|
||||
isVisible,
|
||||
@@ -41,8 +37,17 @@ const SideBar: React.FC<{
|
||||
onGoToLibrary,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('toc');
|
||||
const { books } = useReaderStore();
|
||||
const [bookState, setBookState] = useState<BookState | null>(null);
|
||||
const [currentHref, setCurrentHref] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {}, [isPinned, isVisible]);
|
||||
useEffect(() => {
|
||||
if (!books) return;
|
||||
const bookState = books[bookKey] || DEFAULT_BOOK_STATE;
|
||||
const { config } = bookState;
|
||||
setBookState(bookState);
|
||||
setCurrentHref(config?.href || null);
|
||||
}, [bookKey]);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
onSetVisibility(false);
|
||||
@@ -69,6 +74,12 @@ const SideBar: React.FC<{
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
if (!bookState || !bookState.book || !bookState.bookDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { book, bookDoc } = bookState;
|
||||
|
||||
return (
|
||||
isVisible && (
|
||||
<div
|
||||
@@ -103,8 +114,8 @@ const SideBar: React.FC<{
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
</div>
|
||||
<div className='sidebar-content overflow-y-auto'>
|
||||
{activeTab === 'toc' && (
|
||||
<TOCView toc={tocData} bookId={book.hash} currentHref={currentHref} />
|
||||
{activeTab === 'toc' && bookDoc!.toc && (
|
||||
<TOCView toc={bookDoc!.toc} bookKey={bookKey} currentHref={currentHref} />
|
||||
)}
|
||||
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
|
||||
{activeTab === 'annotations' && <div>Annotations</div>}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { md5 } from 'js-md5';
|
||||
import { TOCItem } from '@/libs/document';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
@@ -19,6 +20,8 @@ const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const getHrefMd5 = (href: string) => md5(JSON.stringify(href));
|
||||
|
||||
const createExpanderIcon = (isExpanded: boolean) => {
|
||||
return (
|
||||
<svg
|
||||
@@ -34,15 +37,15 @@ const createExpanderIcon = (isExpanded: boolean) => {
|
||||
};
|
||||
|
||||
const TOCItemView: React.FC<{
|
||||
bookId: string;
|
||||
bookKey: string;
|
||||
item: TOCItem;
|
||||
depth: number;
|
||||
setCurrentHref: (href: string) => void;
|
||||
currentHref: string | null;
|
||||
expandedItems: string[];
|
||||
}> = ({ bookId, item, depth, setCurrentHref, currentHref, expandedItems }) => {
|
||||
}> = ({ bookKey, item, depth, setCurrentHref, currentHref, expandedItems }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(expandedItems.includes(item.href || ''));
|
||||
const { foliateView } = useReaderStore();
|
||||
const { getFoliateView } = useReaderStore();
|
||||
|
||||
const handleToggleExpand = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -53,7 +56,7 @@ const TOCItemView: React.FC<{
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (item.href) {
|
||||
foliateView?.goTo(item.href);
|
||||
getFoliateView(bookKey)?.goTo(item.href);
|
||||
setCurrentHref(item.href);
|
||||
}
|
||||
};
|
||||
@@ -73,7 +76,7 @@ const TOCItemView: React.FC<{
|
||||
style={{ paddingInlineStart: `${(depth + 1) * 12}px` }}
|
||||
aria-expanded={isExpanded ? 'true' : 'false'}
|
||||
aria-selected={isActive ? 'true' : 'false'}
|
||||
data-href={item.href}
|
||||
data-href={item.href ? getHrefMd5(item.href) : undefined}
|
||||
className={`flex w-full cursor-pointer items-center rounded-md py-2 ${
|
||||
isActive ? 'bg-gray-300 hover:bg-gray-400' : 'hover:bg-gray-300'
|
||||
}`}
|
||||
@@ -98,7 +101,7 @@ const TOCItemView: React.FC<{
|
||||
<ol role='group'>
|
||||
{item.subitems.map((subitem) => (
|
||||
<TOCItemView
|
||||
bookId={bookId}
|
||||
bookKey={bookKey}
|
||||
key={subitem.label}
|
||||
item={subitem}
|
||||
depth={depth + 1}
|
||||
@@ -114,13 +117,13 @@ const TOCItemView: React.FC<{
|
||||
};
|
||||
|
||||
const TOCView: React.FC<{
|
||||
bookId: string;
|
||||
bookKey: string;
|
||||
toc: TOCItem[];
|
||||
currentHref: string | null;
|
||||
}> = ({ bookId, toc, currentHref: href }) => {
|
||||
}> = ({ bookKey, toc, currentHref: href }) => {
|
||||
const [currentHref, setCurrentHref] = useState<string | null>(href);
|
||||
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||
const { foliateView } = useReaderStore();
|
||||
const { getFoliateView } = useReaderStore();
|
||||
const tocRef = useRef<HTMLUListElement | null>(null);
|
||||
|
||||
const tocRelocateHandler = (event: Event) => {
|
||||
@@ -131,7 +134,12 @@ const TOCView: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
useFoliateEvents(foliateView, bookId, { onRelocate: tocRelocateHandler });
|
||||
const foliateView = getFoliateView(bookKey);
|
||||
useFoliateEvents(bookKey, foliateView, { onRelocate: tocRelocateHandler });
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentHref(href);
|
||||
}, [href]);
|
||||
|
||||
const expandParents = (toc: TOCItem[], href: string) => {
|
||||
const parentPath = findParentPath(toc, href).map((item) => item.href);
|
||||
@@ -139,7 +147,8 @@ const TOCView: React.FC<{
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const currentItem = tocRef.current?.querySelector(`[data-href="${currentHref}"]`);
|
||||
const hrefMd5 = currentHref ? getHrefMd5(currentHref) : '';
|
||||
const currentItem = tocRef.current?.querySelector(`[data-href="${hrefMd5}"]`);
|
||||
if (currentItem) {
|
||||
const rect = currentItem.getBoundingClientRect();
|
||||
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
||||
@@ -160,7 +169,7 @@ const TOCView: React.FC<{
|
||||
{toc &&
|
||||
toc.map((item) => (
|
||||
<TOCItemView
|
||||
bookId={bookId}
|
||||
bookKey={bookKey}
|
||||
key={item.label}
|
||||
item={item}
|
||||
depth={0}
|
||||
|
||||
@@ -8,8 +8,8 @@ type FoliateEventHandler = {
|
||||
};
|
||||
|
||||
export const useFoliateEvents = (
|
||||
bookKey: string,
|
||||
view: FoliateView | null,
|
||||
bookId: string,
|
||||
handlers?: FoliateEventHandler,
|
||||
) => {
|
||||
const setProgress = useReaderStore((state) => state.setProgress);
|
||||
@@ -22,7 +22,7 @@ export const useFoliateEvents = (
|
||||
const defaultRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
// console.log('relocate:', detail);
|
||||
setProgress(bookId, detail.fraction, detail.cfi, detail.tocItem?.href, detail.location);
|
||||
setProgress(bookKey, detail.fraction, detail.cfi, detail.tocItem?.href, detail.location);
|
||||
};
|
||||
|
||||
const onLoad = handlers?.onLoad || defaultLoadHandler;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense } from 'react';
|
||||
import { useEffect, Suspense, useRef } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
@@ -12,8 +12,11 @@ import { DEFAULT_READSETTINGS } from '@/services/constants';
|
||||
const ReaderPage = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setLibrary, setSettings } = useReaderStore();
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
const initLibrary = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const settings = await appService.loadSettings();
|
||||
@@ -21,11 +24,12 @@ const ReaderPage = () => {
|
||||
settings.globalReadSettings = DEFAULT_READSETTINGS;
|
||||
}
|
||||
setSettings(settings);
|
||||
console.log('initializing library in reader');
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
}, [envConfig, setSettings, setLibrary]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
settings.globalReadSettings && (
|
||||
|
||||
@@ -4,49 +4,69 @@ import { BookNote, BookContent, Book, BookConfig, PageInfo } from '@/types/book'
|
||||
import { EnvConfigType } from '@/services/environment';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { FoliateView } from '@/app/reader/components/FoliateViewer';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
|
||||
interface BookState {
|
||||
export interface BookState {
|
||||
key: string;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
book?: Book | null;
|
||||
file?: File | null;
|
||||
config?: BookConfig | null;
|
||||
bookDoc?: BookDoc | null;
|
||||
isPrimary?: boolean;
|
||||
}
|
||||
|
||||
interface ReaderStore {
|
||||
library: Book[];
|
||||
books: Record<string, BookState>;
|
||||
settings: SystemSettings;
|
||||
foliateView: FoliateView | null;
|
||||
foliateViews: Record<string, FoliateView>;
|
||||
|
||||
setLibrary: (books: Book[]) => void;
|
||||
setSettings: (settings: SystemSettings) => void;
|
||||
setProgress: (
|
||||
id: string,
|
||||
key: string,
|
||||
progress: number,
|
||||
location: string,
|
||||
href: string,
|
||||
pageinfo: PageInfo,
|
||||
) => void;
|
||||
setFoliateView: (view: FoliateView | null) => void;
|
||||
setFoliateView: (key: string, view: FoliateView) => void;
|
||||
getFoliateView: (key: string) => FoliateView | null;
|
||||
|
||||
saveConfig: (envConfig: EnvConfigType, book: Book, config: BookConfig) => void;
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
|
||||
fetchBook: (envConfig: EnvConfigType, id: string) => Promise<Book | null>;
|
||||
addBookmark: (id: string, bookmark: BookNote) => void;
|
||||
initBookState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void;
|
||||
addBookmark: (key: string, bookmark: BookNote) => void;
|
||||
}
|
||||
|
||||
export const DEFAULT_BOOK_STATE = {
|
||||
key: '',
|
||||
loading: true,
|
||||
error: null,
|
||||
file: null,
|
||||
book: null,
|
||||
config: null,
|
||||
bookDoc: null,
|
||||
isPrimary: true,
|
||||
};
|
||||
|
||||
export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
library: [],
|
||||
books: {},
|
||||
settings: {} as SystemSettings,
|
||||
foliateView: null,
|
||||
foliateViews: {},
|
||||
|
||||
setFoliateView: (view) => set({ foliateView: view }),
|
||||
setLibrary: (books: Book[]) => set({ library: books }),
|
||||
setSettings: (settings: SystemSettings) => set({ settings }),
|
||||
|
||||
setFoliateView: (key: string, view) =>
|
||||
set((state) => ({ foliateViews: { ...state.foliateViews, [key]: view } })),
|
||||
|
||||
getFoliateView: (key: string) => get().foliateViews[key] || null,
|
||||
|
||||
saveConfig: async (envConfig: EnvConfigType, book: Book, config: BookConfig) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library } = get();
|
||||
@@ -65,11 +85,11 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
await appService.saveSettings(settings);
|
||||
},
|
||||
|
||||
fetchBook: async (envConfig: EnvConfigType, id: string) => {
|
||||
initBookState: async (envConfig: EnvConfigType, id: string, key: string, isPrimary = true) => {
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
[id]: { loading: true, file: null, book: null, config: null, error: null, notes: [] },
|
||||
[key]: DEFAULT_BOOK_STATE,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -82,34 +102,50 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
}
|
||||
const content = (await appService.loadBookContent(book)) as BookContent;
|
||||
const { file, config } = content;
|
||||
const { book: bookDoc } = await new DocumentLoader(file).open();
|
||||
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
[id]: { ...state.books[id], loading: false, book, file, config },
|
||||
[key]: {
|
||||
...state.books[key],
|
||||
loading: false,
|
||||
key,
|
||||
book,
|
||||
file,
|
||||
config,
|
||||
bookDoc,
|
||||
isPrimary,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return book;
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
[id]: { ...state.books[id], loading: false, error: 'Failed to load book.' },
|
||||
[key]: { ...state.books[key], key: '', loading: false, error: 'Failed to load book.' },
|
||||
},
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setProgress: (id: string, progress: number, location: string, href: string, pageinfo: PageInfo) =>
|
||||
setProgress: (
|
||||
key: string,
|
||||
progress: number,
|
||||
location: string,
|
||||
href: string,
|
||||
pageinfo: PageInfo,
|
||||
) =>
|
||||
set((state) => {
|
||||
const book = state.books[id];
|
||||
const book = state.books[key];
|
||||
if (!book) return state;
|
||||
return {
|
||||
books: {
|
||||
...state.books,
|
||||
[id]: {
|
||||
[key]: {
|
||||
...book,
|
||||
config: {
|
||||
...book.config,
|
||||
@@ -124,14 +160,14 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
};
|
||||
}),
|
||||
|
||||
addBookmark: (id: string, bookmark: BookNote) =>
|
||||
addBookmark: (key: string, bookmark: BookNote) =>
|
||||
set((state) => {
|
||||
const book = state.books[id];
|
||||
const book = state.books[key];
|
||||
if (!book) return state;
|
||||
return {
|
||||
books: {
|
||||
...state.books,
|
||||
[id]: {
|
||||
[key]: {
|
||||
...book,
|
||||
config: {
|
||||
...book.config,
|
||||
|
||||
Reference in New Issue
Block a user