forked from akai/readest
Refactor ReaderStore
This commit is contained in:
@@ -1,25 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BookState, useReaderStore } from '@/store/readerStore';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FooterBar from './FooterBar';
|
||||
import PageInfo from './PageInfo';
|
||||
import PageInfoView from './PageInfo';
|
||||
import Ribbon from './Ribbon';
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
import Annotator from './annotator/Annotator';
|
||||
|
||||
interface BookGridProps {
|
||||
bookKeys: string[];
|
||||
bookStates: BookState[];
|
||||
onCloseBook: (bookKey: string) => void;
|
||||
}
|
||||
|
||||
const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }) => {
|
||||
const { isSideBarPinned, isSideBarVisible, sideBarWidth, bookmarkRibbons } = useReaderStore();
|
||||
const BookGrid: React.FC<BookGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { isSideBarPinned, isSideBarVisible, sideBarWidth } = useReaderStore();
|
||||
const { getConfig, getProgress, getBookData, getViewState } = useReaderStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useReaderStore();
|
||||
const gridWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
@@ -33,12 +32,15 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
|
||||
gridTemplateRows: gridTemplate.rows,
|
||||
}}
|
||||
>
|
||||
{bookStates.map((bookState, index) => {
|
||||
const bookKey = bookKeys[index]!;
|
||||
const isBookmarked = bookmarkRibbons[bookKey];
|
||||
const { book, config, progress, bookDoc } = bookState;
|
||||
if (!book || !config || !progress || !bookDoc) return null;
|
||||
const { section, pageinfo, tocLabel: chapter } = progress;
|
||||
{bookKeys.map((bookKey) => {
|
||||
const bookData = getBookData(bookKey);
|
||||
const config = getConfig(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const { book, bookDoc } = bookData || {};
|
||||
if (!book || !config || !bookDoc) return null;
|
||||
|
||||
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
|
||||
const { section, pageinfo, tocLabel: chapter } = progress || {};
|
||||
const marginGap = `${config.viewSettings!.gapPercent}%`;
|
||||
|
||||
return (
|
||||
@@ -51,18 +53,18 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
|
||||
<HeaderBar
|
||||
bookKey={bookKey}
|
||||
bookTitle={book.title}
|
||||
isHoveredAnim={bookStates.length > 2}
|
||||
isHoveredAnim={bookKeys.length > 2}
|
||||
onCloseBook={onCloseBook}
|
||||
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
|
||||
/>
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookState.bookDoc!} bookConfig={config} />
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
|
||||
{config.viewSettings!.scrolled ? null : (
|
||||
<>
|
||||
<SectionInfo chapter={chapter} gapLeft={marginGap} />
|
||||
<PageInfo
|
||||
<PageInfoView
|
||||
bookFormat={book.format}
|
||||
section={section}
|
||||
pageinfo={pageinfo}
|
||||
section={section ?? null}
|
||||
pageinfo={pageinfo ?? null}
|
||||
gapRight={marginGap}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -12,11 +12,10 @@ interface BookmarkTogglerProps {
|
||||
|
||||
const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { books, settings, saveConfig, updateBooknotes, setBookmarkRibbonVisibility } =
|
||||
useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const progress = bookState.progress!;
|
||||
const { settings, saveConfig, updateBooknotes } = useReaderStore();
|
||||
const { getConfig, getProgress, setBookmarkRibbonVisibility } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
|
||||
@@ -56,7 +55,8 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { location: cfi, booknotes = [] } = config;
|
||||
const { booknotes = [] } = config || {};
|
||||
const { location: cfi } = progress || {};
|
||||
if (!cfi) return;
|
||||
|
||||
const start = CFI.collapse(cfi);
|
||||
@@ -66,7 +66,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
.some((item) => CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) <= 0);
|
||||
setIsBookmarked(locationBookmarked);
|
||||
setBookmarkRibbonVisibility(bookKey, locationBookmarked);
|
||||
}, [config]);
|
||||
}, [config, progress]);
|
||||
|
||||
return (
|
||||
<button onClick={toggleBookmark} className='p-2'>
|
||||
|
||||
@@ -41,13 +41,13 @@ const wrappedFoliateView = (originalView: FoliateView): FoliateView => {
|
||||
const FoliateViewer: React.FC<{
|
||||
bookKey: string;
|
||||
bookDoc: BookDoc;
|
||||
bookConfig: BookConfig;
|
||||
}> = ({ bookKey, bookDoc, bookConfig }) => {
|
||||
config: BookConfig;
|
||||
}> = ({ bookKey, bookDoc, config }) => {
|
||||
const viewRef = useRef<HTMLDivElement>(null);
|
||||
const [view, setView] = useState<FoliateView | null>(null);
|
||||
const [viewInited, setViewInited] = useState(false);
|
||||
const isViewCreated = useRef(false);
|
||||
const { setFoliateView } = useReaderStore();
|
||||
const { setView: setFoliateView } = useReaderStore();
|
||||
const setProgress = useReaderStore((state) => state.setProgress);
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
@@ -115,8 +115,8 @@ const FoliateViewer: React.FC<{
|
||||
setFoliateView(bookKey, view);
|
||||
|
||||
await view.open(bookDoc);
|
||||
const viewSettings = bookConfig.viewSettings!;
|
||||
view.renderer.setStyles?.(getStyles(bookConfig));
|
||||
const viewSettings = config.viewSettings!;
|
||||
view.renderer.setStyles?.(getStyles(config));
|
||||
const isScrolled = viewSettings.scrolled!;
|
||||
const marginPx = viewSettings.marginPx!;
|
||||
const gapPercent = viewSettings.gapPercent!;
|
||||
@@ -130,7 +130,6 @@ const FoliateViewer: React.FC<{
|
||||
} else {
|
||||
view.renderer.removeAttribute('animated');
|
||||
}
|
||||
view.renderer.setAttribute('animated', animated ? 'animated' : '');
|
||||
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
|
||||
view.renderer.setAttribute('margin', `${marginPx}px`);
|
||||
view.renderer.setAttribute('gap', `${gapPercent}%`);
|
||||
@@ -138,7 +137,7 @@ const FoliateViewer: React.FC<{
|
||||
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
|
||||
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
|
||||
|
||||
const lastLocation = bookConfig.location;
|
||||
const lastLocation = config.location;
|
||||
if (lastLocation) {
|
||||
await view.init({ lastLocation });
|
||||
} else {
|
||||
@@ -158,7 +157,7 @@ const FoliateViewer: React.FC<{
|
||||
}, []);
|
||||
|
||||
const initAnnotations = () => {
|
||||
const { booknotes = [] } = bookConfig;
|
||||
const { booknotes = [] } = config;
|
||||
const annotations = booknotes.filter((item) => item.type === 'annotation');
|
||||
try {
|
||||
Promise.all(annotations.map((annotation) => view?.addAnnotation(annotation)));
|
||||
|
||||
@@ -6,29 +6,27 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface FooterBarProps {
|
||||
bookKey: string;
|
||||
pageinfo?: { current: number; total: number } | undefined;
|
||||
pageinfo: { current: number; total: number } | undefined;
|
||||
isHoveredAnim: boolean;
|
||||
}
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
|
||||
const { isSideBarVisible, hoveredBookKey, setHoveredBookKey, getFoliateView } = useReaderStore();
|
||||
const { isSideBarVisible, hoveredBookKey, setHoveredBookKey, getView } = useReaderStore();
|
||||
|
||||
const handleProgressChange = (event: React.ChangeEvent) => {
|
||||
const newProgress = parseInt((event.target as HTMLInputElement).value, 10);
|
||||
const foliateView = getFoliateView(bookKey);
|
||||
foliateView?.goToFraction(newProgress / 100.0);
|
||||
getView(bookKey)?.goToFraction(newProgress / 100.0);
|
||||
};
|
||||
|
||||
const handleGoPrev = () => {
|
||||
const foliateView = getFoliateView(bookKey);
|
||||
foliateView?.goLeft();
|
||||
getView(bookKey)?.goLeft();
|
||||
};
|
||||
|
||||
const handleGoNext = () => {
|
||||
const foliateView = getFoliateView(bookKey);
|
||||
foliateView?.goRight();
|
||||
getView(bookKey)?.goRight();
|
||||
};
|
||||
const progressFraction = pageinfo ? pageinfo.current / pageinfo.total : 0;
|
||||
const pageinfoValid = pageinfo && pageinfo.total > 0 && pageinfo.current >= 0;
|
||||
const progressFraction = pageinfoValid ? pageinfo.current / pageinfo.total : 0;
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -45,14 +43,14 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
<RiArrowLeftWideLine size={20} />
|
||||
</button>
|
||||
<span className='mx-2 text-center text-sm text-black'>
|
||||
{pageinfo ? `${Math.round(progressFraction * 100)}%` : ''}
|
||||
{pageinfoValid ? `${Math.round(progressFraction * 100)}%` : ''}
|
||||
</span>
|
||||
<input
|
||||
type='range'
|
||||
className='mx-2 w-full'
|
||||
min={0}
|
||||
max={100}
|
||||
value={pageinfo ? progressFraction * 100 : 0}
|
||||
value={pageinfoValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
<button className='btn btn-ghost mx-2 h-8 min-h-8 w-8 p-0' onClick={handleGoNext}>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { PageInfo } from '@/types/book';
|
||||
import React from 'react';
|
||||
|
||||
interface PageInfoProps {
|
||||
bookFormat: string;
|
||||
section?: { current: number; total: number };
|
||||
pageinfo?: { current: number; total: number };
|
||||
section: PageInfo | null;
|
||||
pageinfo: PageInfo | null;
|
||||
gapRight: string;
|
||||
}
|
||||
|
||||
const PageInfo: React.FC<PageInfoProps> = ({ bookFormat, section, pageinfo, gapRight }) => {
|
||||
const PageInfoView: React.FC<PageInfoProps> = ({ bookFormat, section, pageinfo, gapRight }) => {
|
||||
const pageInfo =
|
||||
bookFormat === 'PDF'
|
||||
? section
|
||||
@@ -27,4 +28,4 @@ const PageInfo: React.FC<PageInfoProps> = ({ bookFormat, section, pageinfo, gapR
|
||||
);
|
||||
};
|
||||
|
||||
export default PageInfo;
|
||||
export default PageInfoView;
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { DEFAULT_BOOK_STATE, useReaderStore } from '@/store/readerStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
@@ -17,23 +17,22 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
const router = useRouter();
|
||||
const { envConfig } = useEnv();
|
||||
const { bookKeys, dismissBook, getNextBookKey, openSplitView } = useBooks();
|
||||
const { sideBarBookKey, setSideBarBookKey } = useReaderStore();
|
||||
|
||||
const { books, getFoliateView, clearBookState, saveConfig, saveSettings } = useReaderStore();
|
||||
const bookStates = bookKeys.map((key) => books[key] || DEFAULT_BOOK_STATE);
|
||||
const { sideBarBookKey, getConfig, setSideBarBookKey } = useReaderStore();
|
||||
const { getView, getBookData, getViewState, clearViewState, saveConfig, saveSettings } =
|
||||
useReaderStore();
|
||||
|
||||
useBookShortcuts({ sideBarBookKey, bookKeys, openSplitView, getNextBookKey });
|
||||
|
||||
const saveConfigAndCloseBook = (bookKey: string) => {
|
||||
getFoliateView(bookKey)?.close();
|
||||
getFoliateView(bookKey)?.remove();
|
||||
const bookState = books[bookKey];
|
||||
if (!bookState) return;
|
||||
const { book, config, isPrimary } = bookState;
|
||||
getView(bookKey)?.close();
|
||||
getView(bookKey)?.remove();
|
||||
const config = getConfig(bookKey);
|
||||
const { book } = getBookData(bookKey) || {};
|
||||
const { isPrimary } = getViewState(bookKey) || {};
|
||||
if (isPrimary && book && config) {
|
||||
saveConfig(envConfig, bookKey, config, settings);
|
||||
}
|
||||
clearBookState(bookKey);
|
||||
clearViewState(bookKey);
|
||||
};
|
||||
|
||||
const saveSettingsAndGoToLibrary = () => {
|
||||
@@ -59,21 +58,12 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
}
|
||||
};
|
||||
|
||||
const bookState = bookStates[0];
|
||||
if (
|
||||
bookStates.length !== bookKeys.length ||
|
||||
!bookState ||
|
||||
!bookState.book ||
|
||||
!bookState.bookDoc
|
||||
) {
|
||||
if (!bookKeys || bookKeys.length === 0) return null;
|
||||
const bookData = getBookData(bookKeys[0]!);
|
||||
if (!bookData || !bookData.book || !bookData.bookDoc) {
|
||||
return (
|
||||
<div className={'hero hero-content min-h-screen'}>
|
||||
<Spinner loading={true} />
|
||||
{bookState?.error && (
|
||||
<div className='text-center'>
|
||||
<h2 className='text-red-500'>{bookState.error}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +77,7 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
onOpenSplitView={openSplitView}
|
||||
/>
|
||||
|
||||
<BookGrid bookKeys={bookKeys} bookStates={bookStates} onCloseBook={handleCloseBook} />
|
||||
<BookGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,9 +18,8 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
setIsDropdownOpen,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const { books, setConfig, getFoliateView } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const { getConfig, setConfig, getView } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
|
||||
const [isDarkMode, setDarkMode] = useState(config.viewSettings!.theme === 'dark');
|
||||
const [isScrolledMode, setScrolledMode] = useState(config.viewSettings!.scrolled);
|
||||
@@ -40,8 +39,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const view = getFoliateView(bookKey);
|
||||
view?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
|
||||
getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
|
||||
config.viewSettings!.scrolled = isScrolledMode;
|
||||
setConfig(bookKey, config);
|
||||
}, [isScrolledMode]);
|
||||
@@ -51,7 +49,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
}, [isInvertedColors]);
|
||||
|
||||
useEffect(() => {
|
||||
const view = getFoliateView(bookKey);
|
||||
const view = getView(bookKey);
|
||||
if (!view) return;
|
||||
// FIXME: zoom level is not working in paginated mode
|
||||
if (config.viewSettings?.scrolled) {
|
||||
|
||||
@@ -25,12 +25,12 @@ interface TextSelection {
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { books, settings, saveConfig, updateBooknotes, getFoliateView } = useReaderStore();
|
||||
const { settings, getConfig, saveConfig, getProgress, updateBooknotes, getView } =
|
||||
useReaderStore();
|
||||
const globalReadSettings = settings.globalReadSettings;
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const progress = bookState.progress!;
|
||||
const view = getFoliateView(bookKey);
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
const view = getView(bookKey);
|
||||
|
||||
const [selection, setSelection] = useState<TextSelection | null>();
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
@@ -90,7 +90,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelection(selection as TextSelection);
|
||||
};
|
||||
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation }, [bookState]);
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation }, [config]);
|
||||
|
||||
const popupRef = useOutsideClick<HTMLDivElement>(() => {
|
||||
setShowPopup(false);
|
||||
|
||||
@@ -36,11 +36,10 @@ const FontFace = ({ className, family, label, options, selected, onSelect }: Fon
|
||||
);
|
||||
|
||||
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { books, settings, setSettings, setConfig, getFoliateView } = useReaderStore();
|
||||
const { isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const view = getFoliateView(bookKey);
|
||||
const { settings, isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const { setSettings, getConfig, setConfig, getView } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const view = getView(bookKey);
|
||||
|
||||
const [defaultFontSize, setDefaultFontSize] = useState(config.viewSettings!.defaultFontSize!);
|
||||
const [minFontSize, setMinFontSize] = useState(config.viewSettings!.minimumFontSize!);
|
||||
|
||||
@@ -5,11 +5,10 @@ import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { books, settings, setSettings, setConfig, getFoliateView } = useReaderStore();
|
||||
const { isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const view = getFoliateView(bookKey);
|
||||
const { settings, isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const { setSettings, getConfig, setConfig, getView } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const view = getView(bookKey);
|
||||
|
||||
const [lineHeight, setLineHeight] = useState(config.viewSettings!.lineHeight!);
|
||||
const [fullJustification, setFullJustification] = useState(
|
||||
|
||||
@@ -7,10 +7,9 @@ const cssRegex =
|
||||
/((?:\s*)([\w#.@*,:\-.:>+~\[\]\"=(),*\s]+)\s*{(?:[\s]*)((?:[A-Za-z\- \s]+[:]\s*['"0-9\w .,\/\()\-!#%]+;?)*)*\s*}(?:\s*))/gim;
|
||||
|
||||
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { books, settings, setSettings, setConfig, getFoliateView } = useReaderStore();
|
||||
const { isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const { settings, isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const { setSettings, getConfig, setConfig, getView } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
|
||||
const [animated, setAnimated] = useState(config.viewSettings!.animated!);
|
||||
const [userStylesheet, setUserStylesheet] = useState(config.viewSettings!.userStylesheet!);
|
||||
@@ -44,7 +43,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
}
|
||||
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(config));
|
||||
} catch (err) {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
console.log('CSS Error:', err);
|
||||
@@ -64,9 +63,9 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSettings(settings);
|
||||
}
|
||||
if (animated) {
|
||||
getFoliateView(bookKey)?.renderer.setAttribute('animated', '');
|
||||
getView(bookKey)?.renderer.setAttribute('animated', '');
|
||||
} else {
|
||||
getFoliateView(bookKey)?.renderer.removeAttribute('animated');
|
||||
getView(bookKey)?.renderer.removeAttribute('animated');
|
||||
}
|
||||
}, [animated]);
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ const BookMenu: React.FC<BookMenuProps> = ({ openSplitView, setIsDropdownOpen })
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleOpenSplitView}
|
||||
>
|
||||
<span className='ml-2'>Split View</span>
|
||||
<span className='text-sm text-gray-400'>Shift+S</span>
|
||||
<span className='ml-2'>Parallel Read</span>
|
||||
<span className='text-sm text-gray-400'>Shift+P</span>
|
||||
</button>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
|
||||
@@ -20,13 +20,11 @@ interface BooknoteItemProps {
|
||||
}
|
||||
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
const { getFoliateView } = useReaderStore();
|
||||
const { books } = useReaderStore();
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const [isCurrentBookmark, setIsCurrentBookmark] = useState(false);
|
||||
|
||||
const { text, cfi } = item;
|
||||
const bookState = books[bookKey]!;
|
||||
const progress = bookState.progress!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
|
||||
useEffect(() => {
|
||||
const { location } = progress;
|
||||
@@ -37,7 +35,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
getFoliateView(bookKey)?.goTo(cfi);
|
||||
getView(bookKey)?.goTo(cfi);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -74,9 +72,8 @@ const BooknoteView: React.FC<{
|
||||
bookKey: string;
|
||||
toc: TOCItem[];
|
||||
}> = ({ type, bookKey, toc }) => {
|
||||
const { books } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
const { getConfig } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const { booknotes: allNotes = [] } = config;
|
||||
const booknotes = allNotes.filter((note) => note.type === type);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { BookState, DEFAULT_BOOK_STATE, useReaderStore } from '@/store/readerStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
import TabNavigation from './TabNavigation';
|
||||
@@ -18,9 +18,8 @@ const SideBar: React.FC<{
|
||||
onGoToLibrary: () => void;
|
||||
onOpenSplitView: () => void;
|
||||
}> = ({ width, isPinned, onGoToLibrary, onOpenSplitView }) => {
|
||||
const { getBookData, getProgress } = useReaderStore();
|
||||
const [activeTab, setActiveTab] = useState('toc');
|
||||
const { books } = useReaderStore();
|
||||
const [bookState, setBookState] = useState<BookState | null>(null);
|
||||
const [currentHref, setCurrentHref] = useState<string | null>(null);
|
||||
const { sideBarBookKey } = useReaderStore();
|
||||
const {
|
||||
@@ -33,22 +32,22 @@ const SideBar: React.FC<{
|
||||
const { handleMouseDown } = useDragBar(handleSideBarResize, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
|
||||
|
||||
useEffect(() => {
|
||||
if (!books || !sideBarBookKey) return;
|
||||
const bookState = books[sideBarBookKey] || DEFAULT_BOOK_STATE;
|
||||
const { progress } = bookState;
|
||||
setBookState(bookState);
|
||||
if (!sideBarBookKey) return;
|
||||
const progress = getProgress(sideBarBookKey);
|
||||
setCurrentHref(progress?.tocHref || null);
|
||||
}, [books, sideBarBookKey]);
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
setSideBarVisibility(false);
|
||||
};
|
||||
|
||||
if (!sideBarBookKey || !bookState || !bookState.book || !bookState.bookDoc) {
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
const bookData = getBookData(sideBarBookKey);
|
||||
if (!bookData || !bookData.book || !bookData.bookDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { book, bookDoc } = bookState;
|
||||
const { book, bookDoc } = bookData;
|
||||
|
||||
return isSideBarVisible ? (
|
||||
<>
|
||||
|
||||
@@ -31,7 +31,7 @@ const TOCItemView: React.FC<{
|
||||
expandedItems: string[];
|
||||
}> = ({ bookKey, item, depth, setCurrentHref, currentHref, expandedItems }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(expandedItems.includes(item.href || ''));
|
||||
const { getFoliateView } = useReaderStore();
|
||||
const { getView } = useReaderStore();
|
||||
|
||||
const handleToggleExpand = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -42,7 +42,7 @@ const TOCItemView: React.FC<{
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (item.href) {
|
||||
getFoliateView(bookKey)?.goTo(item.href);
|
||||
getView(bookKey)?.goTo(item.href);
|
||||
setCurrentHref(item.href);
|
||||
}
|
||||
};
|
||||
@@ -109,7 +109,7 @@ const TOCView: React.FC<{
|
||||
}> = ({ bookKey, toc, currentHref: href }) => {
|
||||
const [currentHref, setCurrentHref] = useState<string | null>(href);
|
||||
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||
const { getFoliateView } = useReaderStore();
|
||||
const { getView } = useReaderStore();
|
||||
const tocRef = useRef<HTMLUListElement | null>(null);
|
||||
|
||||
const tocRelocateHandler = (event: Event) => {
|
||||
@@ -120,7 +120,7 @@ const TOCView: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
useFoliateEvents(getFoliateView(bookKey), { onRelocate: tocRelocateHandler });
|
||||
useFoliateEvents(getView(bookKey), { onRelocate: tocRelocateHandler });
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentHref(href);
|
||||
|
||||
@@ -14,7 +14,7 @@ const useBookShortcuts = ({
|
||||
openSplitView,
|
||||
getNextBookKey,
|
||||
}: UseBookShortcutsProps) => {
|
||||
const { getFoliateView, setSideBarBookKey } = useReaderStore();
|
||||
const { getView, setSideBarBookKey } = useReaderStore();
|
||||
const { toggleSideBar } = useReaderStore();
|
||||
|
||||
const switchSideBar = () => {
|
||||
@@ -22,11 +22,11 @@ const useBookShortcuts = ({
|
||||
};
|
||||
|
||||
const goLeft = () => {
|
||||
getFoliateView(sideBarBookKey)?.goLeft();
|
||||
getView(sideBarBookKey)?.goLeft();
|
||||
};
|
||||
|
||||
const goRight = () => {
|
||||
getFoliateView(sideBarBookKey)?.goRight();
|
||||
getView(sideBarBookKey)?.goRight();
|
||||
};
|
||||
|
||||
const reloadPage = () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const useBooks = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { books, initBookState, sideBarBookKey, setSideBarBookKey } = useReaderStore();
|
||||
const { sideBarBookKey, initViewState, getViewState, setSideBarBookKey } = useReaderStore();
|
||||
|
||||
const initialIds = (searchParams.get('ids') || '').split(',').filter(Boolean);
|
||||
const [bookKeys, setBookKeys] = useState<string[]>(() => {
|
||||
@@ -32,7 +32,7 @@ const useBooks = () => {
|
||||
// Append a new book and sync with bookKeys and URL
|
||||
const appendBook = (id: string, isPrimary: boolean) => {
|
||||
const newKey = `${id}-${uniqueId()}`;
|
||||
initBookState(envConfig, id, newKey, isPrimary);
|
||||
initViewState(envConfig, id, newKey, isPrimary);
|
||||
setBookKeys((prevKeys) => {
|
||||
if (!prevKeys.includes(newKey)) {
|
||||
prevKeys.push(newKey);
|
||||
@@ -71,8 +71,8 @@ const useBooks = () => {
|
||||
const id = key.split('-')[0]!;
|
||||
const isPrimary = !uniqueIds.has(id);
|
||||
uniqueIds.add(id);
|
||||
if (!books[key]) {
|
||||
initBookState(envConfig, id, key, isPrimary);
|
||||
if (!getViewState(key)) {
|
||||
initViewState(envConfig, id, key, isPrimary);
|
||||
if (index === 0) setSideBarBookKey(key);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface ShortcutConfig {
|
||||
const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
switchSidebar: ['ctrl+Tab', 'opt+Tab', 'alt+Tab'],
|
||||
toggleSidebar: ['t'],
|
||||
openSplitView: ['shift+s'],
|
||||
openSplitView: ['shift+p'],
|
||||
reloadPage: ['shift+r'],
|
||||
goLeft: ['ArrowLeft', 'PageUp', 'h'],
|
||||
goRight: ['ArrowRight', 'PageDown', 'l'],
|
||||
|
||||
@@ -190,6 +190,7 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
async saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings) {
|
||||
if (settings) {
|
||||
config = JSON.parse(JSON.stringify(config));
|
||||
const globalViewSettings = settings.globalViewSettings as ViewSettings;
|
||||
const viewSettings = config.viewSettings as Partial<ViewSettings>;
|
||||
config.viewSettings = Object.entries(viewSettings).reduce(
|
||||
|
||||
@@ -5,27 +5,34 @@ import { EnvConfigType } from '@/services/environment';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { FoliateView } from '@/app/reader/components/FoliateViewer';
|
||||
import { BookDoc, DocumentLoader, TOCItem } from '@/libs/document';
|
||||
import { updateTocID } from '@/utils/toc';
|
||||
|
||||
export interface BookState {
|
||||
export interface BookData {
|
||||
/* Persistent data shared with different views of the same book */
|
||||
id: string;
|
||||
book: Book | null;
|
||||
file: File | null;
|
||||
config: BookConfig | null;
|
||||
bookDoc: BookDoc | null;
|
||||
}
|
||||
|
||||
export interface ViewState {
|
||||
/* Unique key for each book view */
|
||||
key: string;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
book?: Book | null;
|
||||
file?: File | null;
|
||||
config?: BookConfig | null;
|
||||
progress?: BookProgress | null;
|
||||
bookDoc?: BookDoc | null;
|
||||
isPrimary?: boolean;
|
||||
view: FoliateView | null;
|
||||
isPrimary: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
progress: BookProgress | null;
|
||||
ribbonVisible: boolean;
|
||||
}
|
||||
|
||||
interface ReaderStore {
|
||||
library: Book[];
|
||||
settings: SystemSettings;
|
||||
|
||||
books: Record<string, BookState>;
|
||||
foliateViews: Record<string, FoliateView>;
|
||||
bookDocCache: Record<string, BookDoc>;
|
||||
bookmarkRibbons: Record<string, boolean>;
|
||||
booksData: { [id: string]: BookData };
|
||||
viewStates: { [key: string]: ViewState };
|
||||
|
||||
hoveredBookKey: string | null;
|
||||
sideBarBookKey: string | null;
|
||||
@@ -58,9 +65,11 @@ interface ReaderStore {
|
||||
pageinfo: PageInfo,
|
||||
range: Range,
|
||||
) => void;
|
||||
getProgress: (key: string) => BookProgress | null;
|
||||
setConfig: (key: string, config: BookConfig) => void;
|
||||
setFoliateView: (key: string, view: FoliateView) => void;
|
||||
getFoliateView: (key: string | null) => FoliateView | null;
|
||||
getConfig: (key: string) => BookConfig | null;
|
||||
setView: (key: string, view: FoliateView) => void;
|
||||
getView: (key: string | null) => FoliateView | null;
|
||||
|
||||
deleteBook: (envConfig: EnvConfigType, book: Book) => void;
|
||||
saveConfig: (
|
||||
@@ -70,32 +79,19 @@ interface ReaderStore {
|
||||
settings: SystemSettings,
|
||||
) => void;
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
initBookState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void;
|
||||
|
||||
clearBookState: (key: string) => void;
|
||||
initViewState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void;
|
||||
clearViewState: (key: string) => void;
|
||||
getBookData: (key: string) => BookData | null;
|
||||
getViewState: (key: string) => ViewState | null;
|
||||
updateBooknotes: (key: string, booknotes: BookNote[]) => BookConfig | undefined;
|
||||
}
|
||||
|
||||
export const DEFAULT_BOOK_STATE = {
|
||||
key: '',
|
||||
loading: true,
|
||||
error: null,
|
||||
file: null,
|
||||
book: null,
|
||||
config: null,
|
||||
progress: null,
|
||||
bookDoc: null,
|
||||
isPrimary: true,
|
||||
};
|
||||
|
||||
export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
library: [],
|
||||
settings: {} as SystemSettings,
|
||||
|
||||
books: {},
|
||||
foliateViews: {},
|
||||
bookDocCache: {},
|
||||
bookmarkRibbons: {},
|
||||
booksData: {},
|
||||
viewStates: {},
|
||||
|
||||
hoveredBookKey: null,
|
||||
sideBarBookKey: null,
|
||||
@@ -120,15 +116,15 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
setSettings: (settings: SystemSettings) => set({ settings }),
|
||||
setConfig: (key: string, config: BookConfig) => {
|
||||
set((state) => {
|
||||
const book = state.books[key];
|
||||
if (!book) return state;
|
||||
const bookData = state.booksData[key];
|
||||
if (!bookData) return state;
|
||||
return {
|
||||
books: {
|
||||
...state.books,
|
||||
booksData: {
|
||||
...state.booksData,
|
||||
[key]: {
|
||||
...book,
|
||||
...bookData,
|
||||
config: {
|
||||
...book.config,
|
||||
...bookData.config,
|
||||
...config,
|
||||
lastUpdated: Date.now(),
|
||||
},
|
||||
@@ -138,10 +134,17 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
setFoliateView: (key: string, view) =>
|
||||
set((state) => ({ foliateViews: { ...state.foliateViews, [key]: view } })),
|
||||
getConfig: (key: string) => {
|
||||
const id = key.split('-')[0]!;
|
||||
return get().booksData[id]?.config || null;
|
||||
},
|
||||
|
||||
getFoliateView: (key: string | null) => (key && get().foliateViews[key]) || null,
|
||||
setView: (key: string, view) =>
|
||||
set((state) => ({
|
||||
viewStates: { ...state.viewStates, [key]: { ...state.viewStates[key]!, view } },
|
||||
})),
|
||||
|
||||
getView: (key: string | null) => (key && get().viewStates[key]?.view) || null,
|
||||
|
||||
deleteBook: async (envConfig: EnvConfigType, book: Book) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
@@ -177,89 +180,89 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
await appService.saveSettings(settings);
|
||||
},
|
||||
|
||||
clearBookState: (key: string) => {
|
||||
clearViewState: (key: string) => {
|
||||
set((state) => {
|
||||
const books = { ...state.books };
|
||||
delete books[key];
|
||||
return { books };
|
||||
const viewStates = { ...state.viewStates };
|
||||
delete viewStates[key];
|
||||
return { viewStates };
|
||||
});
|
||||
},
|
||||
initBookState: async (envConfig: EnvConfigType, id: string, key: string, isPrimary = true) => {
|
||||
const cache = get().bookDocCache || {};
|
||||
|
||||
initViewState: async (envConfig: EnvConfigType, id: string, key: string, isPrimary = true) => {
|
||||
const bookData = get().booksData[id];
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
[key]: DEFAULT_BOOK_STATE,
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
key: '',
|
||||
view: null,
|
||||
isPrimary: false,
|
||||
loading: true,
|
||||
error: null,
|
||||
progress: null,
|
||||
ribbonVisible: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
try {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library, settings } = get();
|
||||
const book = library.find((b) => b.hash === id);
|
||||
if (!book) {
|
||||
throw new Error('Book not found');
|
||||
}
|
||||
const content = (await appService.loadBookContent(book, settings)) as BookContent;
|
||||
const { file, config } = content;
|
||||
let bookDoc: BookDoc;
|
||||
if (cache[id]) {
|
||||
console.log('Using cached bookDoc for book', key);
|
||||
bookDoc = cache[id];
|
||||
} else {
|
||||
if (!bookData) {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library, settings } = get();
|
||||
const book = library.find((b) => b.hash === id);
|
||||
if (!book) {
|
||||
throw new Error('Book not found');
|
||||
}
|
||||
const content = (await appService.loadBookContent(book, settings)) as BookContent;
|
||||
const { file, config } = content;
|
||||
console.log('Loading book', key);
|
||||
const { book: loadedBookDoc } = await new DocumentLoader(file).open();
|
||||
bookDoc = loadedBookDoc as BookDoc;
|
||||
const updateTocID = (items: TOCItem[], index = 0): number => {
|
||||
items.forEach((item) => {
|
||||
if (item.id === undefined) {
|
||||
item.id = index++;
|
||||
}
|
||||
if (item.subitems) {
|
||||
index = updateTocID(item.subitems, index);
|
||||
}
|
||||
});
|
||||
return index;
|
||||
};
|
||||
const bookDoc = loadedBookDoc as BookDoc;
|
||||
updateTocID(bookDoc.toc);
|
||||
set((state) => ({
|
||||
bookDocCache: {
|
||||
...state.bookDocCache,
|
||||
[id]: bookDoc,
|
||||
booksData: {
|
||||
...state.booksData,
|
||||
[id]: { id, book, file, config, bookDoc },
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
...state.books[key],
|
||||
loading: false,
|
||||
...state.viewStates[key],
|
||||
key,
|
||||
book,
|
||||
file,
|
||||
config,
|
||||
progress: {} as BookProgress,
|
||||
bookDoc,
|
||||
view: null,
|
||||
isPrimary,
|
||||
loading: false,
|
||||
error: null,
|
||||
progress: null,
|
||||
ribbonVisible: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
[key]: { ...state.books[key], key: '', loading: false, error: 'Failed to load book.' },
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
...state.viewStates[key],
|
||||
key: '',
|
||||
view: null,
|
||||
isPrimary: false,
|
||||
loading: false,
|
||||
error: 'Failed to load book.',
|
||||
progress: null,
|
||||
ribbonVisible: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getBookData: (key: string) => {
|
||||
const id = key.split('-')[0]!;
|
||||
return get().booksData[id] || null;
|
||||
},
|
||||
getViewState: (key: string) => get().viewStates[key] || null,
|
||||
setProgress: (
|
||||
key: string,
|
||||
location: string,
|
||||
@@ -269,26 +272,31 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
range: Range,
|
||||
) =>
|
||||
set((state) => {
|
||||
const book = state.books[key];
|
||||
if (!book) return state;
|
||||
const id = key.split('-')[0]!;
|
||||
const bookData = state.booksData[id];
|
||||
const viewState = state.viewStates[key];
|
||||
if (!viewState || !bookData) return state;
|
||||
const oldConfig = bookData.config;
|
||||
const newConfig = {
|
||||
...bookData.config,
|
||||
lastUpdated: Date.now(),
|
||||
progress: [pageinfo.current, pageinfo.total] as [number, number],
|
||||
location,
|
||||
};
|
||||
return {
|
||||
books: {
|
||||
...state.books,
|
||||
booksData: {
|
||||
...state.booksData,
|
||||
[id]: {
|
||||
...bookData,
|
||||
config: viewState.isPrimary ? newConfig : oldConfig,
|
||||
},
|
||||
},
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
...book,
|
||||
config: {
|
||||
...book.config,
|
||||
lastUpdated: Date.now(),
|
||||
href: tocItem?.href,
|
||||
chapter: tocItem?.label,
|
||||
progress: [pageinfo.current, pageinfo.total],
|
||||
location,
|
||||
section,
|
||||
pageinfo,
|
||||
},
|
||||
...viewState,
|
||||
progress: {
|
||||
...book.progress,
|
||||
progress: [pageinfo.current, pageinfo.total],
|
||||
...viewState.progress,
|
||||
location,
|
||||
tocHref: tocItem?.href,
|
||||
tocLabel: tocItem?.label,
|
||||
@@ -302,18 +310,24 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
};
|
||||
}),
|
||||
|
||||
getProgress: (key: string) => get().viewStates[key]?.progress || null,
|
||||
|
||||
setBookmarkRibbonVisibility: (key: string, visible: boolean) =>
|
||||
set((state) => ({
|
||||
bookmarkRibbons: {
|
||||
...state.bookmarkRibbons,
|
||||
[key]: visible,
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
...state.viewStates[key]!,
|
||||
ribbonVisible: visible,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
updateBooknotes: (key: string, booknotes: BookNote[]) => {
|
||||
let updatedConfig: BookConfig | undefined;
|
||||
set((state) => {
|
||||
const book = state.books[key];
|
||||
const id = key.split('-')[0]!;
|
||||
const book = state.booksData[id];
|
||||
if (!book) return state;
|
||||
updatedConfig = {
|
||||
...book.config,
|
||||
@@ -321,9 +335,9 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
booknotes,
|
||||
};
|
||||
return {
|
||||
books: {
|
||||
...state.books,
|
||||
[key]: {
|
||||
booksData: {
|
||||
...state.booksData,
|
||||
[id]: {
|
||||
...book,
|
||||
config: {
|
||||
...book.config,
|
||||
|
||||
@@ -14,3 +14,15 @@ export const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => {
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const updateTocID = (items: TOCItem[], index = 0): number => {
|
||||
items.forEach((item) => {
|
||||
if (item.id === undefined) {
|
||||
item.id = index++;
|
||||
}
|
||||
if (item.subitems) {
|
||||
index = updateTocID(item.subitems, index);
|
||||
}
|
||||
});
|
||||
return index;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user