Refactor ReaderContent and SideBar
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BookState, useReaderStore } from '@/store/readerStore';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import PageInfo from './PageInfo';
|
||||
import FooterBar from './FooterBar';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
|
||||
interface BookGridProps {
|
||||
bookKeys: string[];
|
||||
bookStates: BookState[];
|
||||
onCloseBook: (bookKey: string) => void;
|
||||
}
|
||||
|
||||
const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }) => {
|
||||
const { isSideBarPinned, isSideBarVisible, sideBarWidth } = useReaderStore();
|
||||
const gridWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
|
||||
return (
|
||||
<div
|
||||
className='grid h-full'
|
||||
style={{
|
||||
width: gridWidth,
|
||||
gridTemplateColumns: gridTemplate.columns,
|
||||
gridTemplateRows: gridTemplate.rows,
|
||||
}}
|
||||
>
|
||||
{bookStates.map((bookState, index) => {
|
||||
const bookKey = bookKeys[index]!;
|
||||
const { book, config, bookDoc } = bookState;
|
||||
if (!book || !config || !bookDoc) return null;
|
||||
const { section, pageinfo, progress, chapter } = config;
|
||||
|
||||
return (
|
||||
<div key={bookKey} className='relative h-full w-full overflow-hidden'>
|
||||
<HeaderBar
|
||||
bookKey={bookKey}
|
||||
bookTitle={book.title}
|
||||
isHoveredAnim={bookStates.length > 2}
|
||||
onCloseBook={onCloseBook}
|
||||
/>
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookState.bookDoc!} bookConfig={config} />
|
||||
{config.viewSettings?.scrolled ? null : (
|
||||
<>
|
||||
<SectionInfo chapter={chapter} />
|
||||
<PageInfo bookFormat={book.format} section={section} pageinfo={pageinfo} />
|
||||
</>
|
||||
)}
|
||||
<FooterBar bookKey={bookKey} progress={progress} isHoveredAnim={false} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookGrid;
|
||||
@@ -131,7 +131,7 @@ const FoliateViewer: React.FC<{
|
||||
useEffect(() => {
|
||||
if (isViewCreated.current) return;
|
||||
const openBook = async () => {
|
||||
console.log('opening book');
|
||||
console.log('Opening book', bookKey);
|
||||
await import('foliate-js/view.js');
|
||||
const view = document.createElement('foliate-view') as FoliateView;
|
||||
document.body.append(view);
|
||||
@@ -162,7 +162,13 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
openBook();
|
||||
isViewCreated.current = true;
|
||||
}, [bookDoc]);
|
||||
|
||||
return () => {
|
||||
console.log('Closing book', bookKey);
|
||||
view?.close();
|
||||
view?.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleTap = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { clientX } = event;
|
||||
|
||||
@@ -8,18 +8,10 @@ interface FooterBarProps {
|
||||
bookKey: string;
|
||||
progress: number | undefined;
|
||||
isHoveredAnim: boolean;
|
||||
hoveredBookKey: string;
|
||||
setHoveredBookKey: (key: string) => void;
|
||||
}
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({
|
||||
bookKey,
|
||||
progress,
|
||||
isHoveredAnim,
|
||||
hoveredBookKey,
|
||||
setHoveredBookKey,
|
||||
}) => {
|
||||
const { getFoliateView } = useReaderStore();
|
||||
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, progress, isHoveredAnim }) => {
|
||||
const { isSideBarVisible, hoveredBookKey, setHoveredBookKey, getFoliateView } = useReaderStore();
|
||||
|
||||
const handleProgressChange = (event: React.ChangeEvent) => {
|
||||
const newProgress = parseInt((event.target as HTMLInputElement).value, 10);
|
||||
@@ -42,6 +34,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
className={clsx(
|
||||
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center px-4',
|
||||
'shadow-xs bg-base-100 rounded-window-bottom-right transition-opacity duration-300',
|
||||
!isSideBarVisible && 'rounded-window-bottom-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
|
||||
)}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { VscLayoutSidebarLeft, VscLayoutSidebarLeftOff } from 'react-icons/vsc';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SidebarToggler from './SidebarToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface HeaderBarProps {
|
||||
bookKey: string;
|
||||
bookTitle: string;
|
||||
isHoveredAnim: boolean;
|
||||
hoveredBookKey: string;
|
||||
isSideBarVisible: boolean;
|
||||
sideBarBookKey: string | null;
|
||||
setSideBarVisibility: (visibility: boolean) => void;
|
||||
setSideBarBookKey: (key: string) => void;
|
||||
setHoveredBookKey: (key: string) => void;
|
||||
onCloseBook: (bookKey: string) => void;
|
||||
}
|
||||
|
||||
@@ -24,24 +19,20 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
bookKey,
|
||||
bookTitle,
|
||||
isHoveredAnim,
|
||||
hoveredBookKey,
|
||||
isSideBarVisible,
|
||||
sideBarBookKey,
|
||||
setSideBarVisibility,
|
||||
setSideBarBookKey,
|
||||
setHoveredBookKey,
|
||||
onCloseBook,
|
||||
}) => {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { hoveredBookKey, setHoveredBookKey, sideBarBookKey, setSideBarBookKey } = useReaderStore();
|
||||
const { isSideBarVisible, toggleSideBar } = useReaderStore();
|
||||
|
||||
const toggleSideBar = () => {
|
||||
if (!isSideBarVisible) {
|
||||
setSideBarVisibility(true);
|
||||
} else if (sideBarBookKey === bookKey) {
|
||||
setSideBarVisibility(false);
|
||||
const toggleSidebarForBook = (bookKey: string) => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
} else {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isSideBarVisible) toggleSideBar();
|
||||
}
|
||||
setSideBarBookKey(bookKey);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -50,6 +41,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
className={clsx(
|
||||
`header-bar absolute top-0 z-10 flex h-11 w-full items-center px-4`,
|
||||
`shadow-xs bg-base-100 rounded-window-top-right transition-opacity duration-300`,
|
||||
!isSideBarVisible && 'rounded-window-top-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey || isDropdownOpen ? `opacity-100` : `opacity-0`,
|
||||
isDropdownOpen && 'header-bar-pinned',
|
||||
@@ -58,13 +50,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='sidebar-toggler mr-auto flex h-full items-center'>
|
||||
<button onClick={toggleSideBar} className='p-2'>
|
||||
{sideBarBookKey === bookKey && isSideBarVisible ? (
|
||||
<VscLayoutSidebarLeft size={16} />
|
||||
) : (
|
||||
<VscLayoutSidebarLeftOff size={16} />
|
||||
)}
|
||||
</button>
|
||||
<SidebarToggler
|
||||
isSidebarVisible={isSideBarVisible}
|
||||
isCurrentBook={sideBarBookKey === bookKey}
|
||||
toggleSidebar={() => toggleSidebarForBook(bookKey)}
|
||||
></SidebarToggler>
|
||||
</div>
|
||||
|
||||
<div className='header-title flex flex-1 items-center justify-center'>
|
||||
|
||||
@@ -1,116 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore, DEFAULT_BOOK_STATE } from '@/store/readerStore';
|
||||
import { DEFAULT_BOOK_STATE, useReaderStore } from '@/store/readerStore';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import SideBar from './SideBar';
|
||||
import PageInfo from './PageInfo';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FooterBar from './FooterBar';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import SideBar from './sidebar/SideBar';
|
||||
import useBooks from '../hooks/useBooks';
|
||||
import BookGrid from './BookGrid';
|
||||
import useBookShortcuts from '../hooks/useBookShortcuts';
|
||||
|
||||
const ReaderContent = () => {
|
||||
const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const ids = (searchParams.get('ids') || '').split(',');
|
||||
|
||||
const { envConfig } = useEnv();
|
||||
const { books, settings } = useReaderStore();
|
||||
const { initBookState, closeBook, saveConfig, saveSettings, getFoliateView } = useReaderStore();
|
||||
const { bookKeys, dismissBook, getNextBookKey, openSplitView } = useBooks();
|
||||
const { sideBarBookKey, setSideBarBookKey } = useReaderStore();
|
||||
|
||||
const [sideBarWidth, setSideBarWidth] = useState(settings.globalReadSettings.sideBarWidth);
|
||||
const [isSideBarPinned, setIsSideBarPinned] = useState(
|
||||
settings.globalReadSettings.isSideBarPinned,
|
||||
);
|
||||
const [isSideBarVisible, setSideBarVisibility] = useState(isSideBarPinned);
|
||||
const { books, getFoliateView, clearBookState, saveConfig, saveSettings } = useReaderStore();
|
||||
const bookStates = bookKeys.map((key) => books[key] || DEFAULT_BOOK_STATE);
|
||||
|
||||
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));
|
||||
const [hoveredBookKey, setHoveredBookKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (ids.length === 0) return;
|
||||
const uniqueIds = new Set<string>();
|
||||
console.log('fetching books', ids);
|
||||
ids.forEach((id, index) => {
|
||||
const isPrimary = !uniqueIds.has(id);
|
||||
uniqueIds.add(id);
|
||||
const key = getKey(id, index);
|
||||
if (books[key]) return;
|
||||
console.log('initing state', key);
|
||||
initBookState(envConfig, id, key, isPrimary);
|
||||
});
|
||||
}, [searchParams]);
|
||||
|
||||
const getNextBookKey = (bookKey: string) => {
|
||||
const bookKeys = ids.map((id, index) => getKey(id, index));
|
||||
const index = bookKeys.findIndex((key) => key === bookKey);
|
||||
const nextIndex = (index + 1) % bookKeys.length;
|
||||
return bookKeys[nextIndex]!;
|
||||
};
|
||||
|
||||
const switchSidebar = () => {
|
||||
setSideBarBookKey((prevKey: string) => {
|
||||
return getNextBookKey(prevKey);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSideBarVisibility((prev) => !prev);
|
||||
};
|
||||
|
||||
const openSplitView = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
const sideBarBookId = sideBarBookKey.split('-')[0];
|
||||
const updatedIds = [...ids, sideBarBookId].join(',');
|
||||
params.set('ids', updatedIds);
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
const goLeft = () => {
|
||||
getFoliateView(sideBarBookKey)?.goLeft();
|
||||
};
|
||||
|
||||
const goRight = () => {
|
||||
getFoliateView(sideBarBookKey)?.goRight();
|
||||
};
|
||||
|
||||
const reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
useShortcuts(
|
||||
{
|
||||
onOpenSplitView: openSplitView,
|
||||
onSwitchSidebar: switchSidebar,
|
||||
onToggleSidebar: toggleSidebar,
|
||||
onReloadPage: reloadPage,
|
||||
onGoLeft: goLeft,
|
||||
onGoRight: goRight,
|
||||
},
|
||||
[sideBarBookKey, searchParams],
|
||||
);
|
||||
|
||||
const handleSideBarResize = (newWidth: string) => {
|
||||
setSideBarWidth(newWidth);
|
||||
settings.globalReadSettings.sideBarWidth = newWidth;
|
||||
};
|
||||
|
||||
const handleSideBarTogglePin = () => {
|
||||
if (isSideBarPinned && isSideBarVisible) {
|
||||
setSideBarVisibility(false);
|
||||
}
|
||||
setIsSideBarPinned(!isSideBarPinned);
|
||||
settings.globalReadSettings.isSideBarPinned = !isSideBarPinned;
|
||||
};
|
||||
useBookShortcuts({ sideBarBookKey, bookKeys, openSplitView, getNextBookKey });
|
||||
|
||||
const saveConfigAndCloseBook = (bookKey: string) => {
|
||||
getFoliateView(bookKey)?.close();
|
||||
@@ -121,7 +33,7 @@ const ReaderContent = () => {
|
||||
if (isPrimary && book && config) {
|
||||
saveConfig(envConfig, book, config, settings);
|
||||
}
|
||||
closeBook(bookKey);
|
||||
clearBookState(bookKey);
|
||||
};
|
||||
|
||||
const saveSettingsAndGoToLibrary = () => {
|
||||
@@ -130,8 +42,7 @@ const ReaderContent = () => {
|
||||
};
|
||||
|
||||
const handleCloseBooks = () => {
|
||||
ids.forEach((id, index) => {
|
||||
const key = getKey(id, index);
|
||||
bookKeys.forEach((key) => {
|
||||
saveConfigAndCloseBook(key);
|
||||
});
|
||||
saveSettingsAndGoToLibrary();
|
||||
@@ -139,43 +50,22 @@ const ReaderContent = () => {
|
||||
|
||||
const handleCloseBook = (bookKey: string) => {
|
||||
saveConfigAndCloseBook(bookKey);
|
||||
setSideBarBookKey((prevKey: string) => {
|
||||
return prevKey === bookKey ? getNextBookKey(prevKey) : prevKey;
|
||||
});
|
||||
const newIds = ids.filter((id, index) => getKey(id, index) !== bookKey);
|
||||
if (newIds.length > 0) {
|
||||
router.push(`/reader?ids=${newIds.join(',')}`);
|
||||
} else {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
setSideBarBookKey(getNextBookKey(sideBarBookKey));
|
||||
}
|
||||
dismissBook(bookKey);
|
||||
if (bookKeys.filter((key) => key !== bookKey).length == 0) {
|
||||
saveSettingsAndGoToLibrary();
|
||||
}
|
||||
};
|
||||
|
||||
const getGridTemplate = () => {
|
||||
const count = ids.length;
|
||||
const aspectRatio = window.innerWidth / window.innerHeight;
|
||||
|
||||
if (count <= 1) {
|
||||
// One book, full screen
|
||||
return { columns: '1fr', rows: '1fr' };
|
||||
} else if (count === 2) {
|
||||
if (aspectRatio < 1) {
|
||||
// portrait mode: horizontal split
|
||||
return { columns: '1fr', rows: '1fr 1fr' };
|
||||
} else {
|
||||
// landscape mode: vertical 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) {
|
||||
if (
|
||||
bookStates.length !== bookKeys.length ||
|
||||
!bookState ||
|
||||
!bookState.book ||
|
||||
!bookState.bookDoc
|
||||
) {
|
||||
return (
|
||||
<div className={'hero hero-content min-h-screen'}>
|
||||
<Spinner loading={true} />
|
||||
@@ -188,66 +78,16 @@ const ReaderContent = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const gridWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
|
||||
return (
|
||||
<div className='flex h-screen'>
|
||||
<SideBar
|
||||
bookKey={sideBarBookKey}
|
||||
width={sideBarWidth}
|
||||
isVisible={isSideBarVisible}
|
||||
isPinned={isSideBarPinned}
|
||||
onResize={handleSideBarResize}
|
||||
onTogglePin={handleSideBarTogglePin}
|
||||
width={settings.globalReadSettings.sideBarWidth}
|
||||
isPinned={settings.globalReadSettings.isSideBarPinned}
|
||||
onGoToLibrary={handleCloseBooks}
|
||||
onOpenSplitView={openSplitView}
|
||||
onSetVisibility={(visibility: boolean) => setSideBarVisibility(visibility)}
|
||||
/>
|
||||
|
||||
<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;
|
||||
const { section, pageinfo, progress, chapter } = config;
|
||||
const scrolled = config.viewSettings!.scrolled;
|
||||
return (
|
||||
<div key={key} className='relative h-full w-full overflow-hidden'>
|
||||
<HeaderBar
|
||||
bookKey={key}
|
||||
bookTitle={book?.title}
|
||||
isHoveredAnim={ids.length > 2}
|
||||
hoveredBookKey={hoveredBookKey}
|
||||
isSideBarVisible={isSideBarVisible}
|
||||
sideBarBookKey={sideBarBookKey}
|
||||
setSideBarVisibility={setSideBarVisibility}
|
||||
setSideBarBookKey={setSideBarBookKey}
|
||||
setHoveredBookKey={setHoveredBookKey}
|
||||
onCloseBook={handleCloseBook}
|
||||
/>
|
||||
<FoliateViewer bookKey={key} bookDoc={bookDoc!} bookConfig={config!} />
|
||||
{!scrolled && <SectionInfo chapter={chapter} />}
|
||||
{!scrolled && (
|
||||
<PageInfo bookFormat={book.format} section={section} pageinfo={pageinfo} />
|
||||
)}
|
||||
<FooterBar
|
||||
bookKey={key}
|
||||
progress={progress}
|
||||
isHoveredAnim={false}
|
||||
hoveredBookKey={hoveredBookKey}
|
||||
setHoveredBookKey={setHoveredBookKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<BookGrid bookKeys={bookKeys} bookStates={bookStates} onCloseBook={handleCloseBook} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
MdOutlinePushPin,
|
||||
MdPushPin,
|
||||
MdToc,
|
||||
MdEditNote,
|
||||
MdBookmarkBorder,
|
||||
MdOutlineMenu,
|
||||
} from 'react-icons/md';
|
||||
import { GiBookshelf } from 'react-icons/gi';
|
||||
import { CiSearch } from 'react-icons/ci';
|
||||
|
||||
import { BookState, DEFAULT_BOOK_STATE, useReaderStore } from '@/store/readerStore';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import BookCard from './BookCard';
|
||||
import TOCView from './TOCView';
|
||||
import BookMenu from './BookMenu';
|
||||
|
||||
const MIN_SIDEBAR_WIDTH = 0.15;
|
||||
const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
|
||||
const SideBar: React.FC<{
|
||||
bookKey: string;
|
||||
width: string;
|
||||
isVisible: boolean;
|
||||
isPinned: boolean;
|
||||
onSetVisibility: (visibility: boolean) => void;
|
||||
onTogglePin: () => void;
|
||||
onResize: (newWidth: string) => void;
|
||||
onGoToLibrary: () => void;
|
||||
onOpenSplitView: () => void;
|
||||
}> = ({
|
||||
bookKey,
|
||||
width,
|
||||
isPinned,
|
||||
isVisible,
|
||||
onTogglePin,
|
||||
onSetVisibility,
|
||||
onResize,
|
||||
onGoToLibrary,
|
||||
onOpenSplitView,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('toc');
|
||||
const { books } = useReaderStore();
|
||||
const [bookState, setBookState] = useState<BookState | null>(null);
|
||||
const [currentHref, setCurrentHref] = useState<string | null>(null);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const newWidthPx = e.clientX;
|
||||
const width = `${Math.round((newWidthPx / window.innerWidth) * 10000) / 100}%`;
|
||||
const minWidthPx = MIN_SIDEBAR_WIDTH * window.innerWidth;
|
||||
const maxWidthPx = MAX_SIDEBAR_WIDTH * window.innerWidth;
|
||||
if (newWidthPx >= minWidthPx && newWidthPx <= maxWidthPx) {
|
||||
onResize(width);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
if (!bookState || !bookState.book || !bookState.bookDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { book, bookDoc } = bookState;
|
||||
|
||||
return isVisible ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 h-full select-none',
|
||||
'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
width: `${width}`,
|
||||
minWidth: `${MIN_SIDEBAR_WIDTH * 100}%`,
|
||||
maxWidth: `${MAX_SIDEBAR_WIDTH * 100}%`,
|
||||
position: isPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<div className={'sidebar h-full'}>
|
||||
<div className='sidebar-header flex h-11 items-center justify-between pl-1.5 pr-3'>
|
||||
<div className='flex items-center'>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
<GiBookshelf size={20} className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex size-[50%] min-w-20 items-center justify-between'>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<CiSearch size={20} className='fill-base-content' />
|
||||
</button>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={20} />}
|
||||
>
|
||||
<BookMenu openSplitView={onOpenSplitView} />
|
||||
</Dropdown>
|
||||
<button
|
||||
onClick={onTogglePin}
|
||||
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle right-0 h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-b px-3'>
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
</div>
|
||||
<div className='sidebar-content overflow-y-auto shadow-inner'>
|
||||
{activeTab === 'toc' && bookDoc!.toc && (
|
||||
<TOCView toc={bookDoc!.toc} bookKey={bookKey} currentHref={currentHref} />
|
||||
)}
|
||||
{activeTab === 'annotations' && <div>Annotations</div>}
|
||||
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
|
||||
</div>
|
||||
<div className='bottom-tab absolute bottom-0 flex w-full border-t'>
|
||||
{['toc', 'annotations', 'bookmarks'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === tab ? 'bg-base-300' : ''}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
{tab === 'toc' && <MdToc size={20} className='mx-auto' />}
|
||||
{tab === 'annotations' && <MdEditNote size={20} className='mx-auto' />}
|
||||
{tab === 'bookmarks' && <MdBookmarkBorder size={20} className='mx-auto' />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
></div>
|
||||
</div>
|
||||
{!isPinned && (
|
||||
<div
|
||||
className='overlay fixed top-0 h-full bg-black/20'
|
||||
style={{
|
||||
left: width,
|
||||
width: `calc(100% - ${width})`,
|
||||
}}
|
||||
onClick={() => handleClickOverlay()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { VscLayoutSidebarLeft, VscLayoutSidebarLeftOff } from 'react-icons/vsc';
|
||||
|
||||
interface SidebarTogglerProps {
|
||||
isSidebarVisible: boolean;
|
||||
isCurrentBook: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({
|
||||
isSidebarVisible,
|
||||
isCurrentBook,
|
||||
toggleSidebar,
|
||||
}) => (
|
||||
<button onClick={toggleSidebar} className='p-2'>
|
||||
{isCurrentBook && isSidebarVisible ? (
|
||||
<VscLayoutSidebarLeft size={16} />
|
||||
) : (
|
||||
<VscLayoutSidebarLeftOff size={16} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default SidebarToggler;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import TOCView from './TOCView';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
|
||||
const SidebarContent: React.FC<{
|
||||
activeTab: string;
|
||||
bookDoc: BookDoc;
|
||||
currentHref: string | null;
|
||||
sideBarBookKey: string;
|
||||
}> = ({ activeTab, bookDoc, currentHref, sideBarBookKey }) => (
|
||||
<div className='sidebar-content overflow-y-auto shadow-inner'>
|
||||
{activeTab === 'toc' && bookDoc.toc && (
|
||||
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} currentHref={currentHref} />
|
||||
)}
|
||||
{activeTab === 'annotations' && <div>Annotations</div>}
|
||||
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SidebarContent;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { GiBookshelf } from 'react-icons/gi';
|
||||
import { CiSearch } from 'react-icons/ci';
|
||||
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import BookMenu from './BookMenu';
|
||||
|
||||
const SidebarHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
onGoToLibrary: () => void;
|
||||
onOpenSplitView: () => void;
|
||||
handleSideBarTogglePin: () => void;
|
||||
}> = ({ isPinned, onGoToLibrary, onOpenSplitView, handleSideBarTogglePin }) => (
|
||||
<div className='sidebar-header flex h-11 items-center justify-between pl-1.5 pr-3'>
|
||||
<div className='flex items-center'>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
<GiBookshelf size={20} className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex size-[50%] min-w-20 items-center justify-between'>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<CiSearch size={20} className='fill-base-content' />
|
||||
</button>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={20} />}
|
||||
>
|
||||
<BookMenu openSplitView={onOpenSplitView} />
|
||||
</Dropdown>
|
||||
<button
|
||||
onClick={handleSideBarTogglePin}
|
||||
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle right-0 h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SidebarHeader;
|
||||
@@ -0,0 +1,98 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { BookState, DEFAULT_BOOK_STATE, useReaderStore } from '@/store/readerStore';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
import TabNavigation from './TabNavigation';
|
||||
import BookCard from './BookCard';
|
||||
import useSidebar from '../../hooks/useSidebar';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
|
||||
const MIN_SIDEBAR_WIDTH = 0.15;
|
||||
const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
|
||||
const SideBar: React.FC<{
|
||||
width: string;
|
||||
isPinned: boolean;
|
||||
onGoToLibrary: () => void;
|
||||
onOpenSplitView: () => void;
|
||||
}> = ({ width, isPinned, onGoToLibrary, onOpenSplitView }) => {
|
||||
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 {
|
||||
sideBarWidth,
|
||||
isSideBarVisible,
|
||||
handleSideBarResize,
|
||||
handleSideBarTogglePin,
|
||||
setSideBarVisibility,
|
||||
} = useSidebar(width, isPinned);
|
||||
const { handleMouseDown } = useDragBar(handleSideBarResize, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
|
||||
|
||||
useEffect(() => {
|
||||
if (!books || !sideBarBookKey) return;
|
||||
const bookState = books[sideBarBookKey] || DEFAULT_BOOK_STATE;
|
||||
const { config } = bookState;
|
||||
setBookState(bookState);
|
||||
setCurrentHref(config?.href || null);
|
||||
}, [books, sideBarBookKey]);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
setSideBarVisibility(false);
|
||||
};
|
||||
|
||||
if (!sideBarBookKey || !bookState || !bookState.book || !bookState.bookDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { book, bookDoc } = bookState;
|
||||
|
||||
return isSideBarVisible ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 h-full select-none',
|
||||
'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
width: `${sideBarWidth}`,
|
||||
minWidth: `${MIN_SIDEBAR_WIDTH * 100}%`,
|
||||
maxWidth: `${MAX_SIDEBAR_WIDTH * 100}%`,
|
||||
position: isPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<SidebarHeader
|
||||
isPinned={isPinned}
|
||||
onGoToLibrary={onGoToLibrary}
|
||||
onOpenSplitView={onOpenSplitView}
|
||||
handleSideBarTogglePin={handleSideBarTogglePin}
|
||||
/>
|
||||
<div className='border-b px-3'>
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
</div>
|
||||
<SidebarContent
|
||||
activeTab={activeTab}
|
||||
bookDoc={bookDoc}
|
||||
currentHref={currentHref}
|
||||
sideBarBookKey={sideBarBookKey!}
|
||||
/>
|
||||
<TabNavigation activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
></div>
|
||||
{!isPinned && (
|
||||
<div
|
||||
className='overlay fixed top-0 h-full bg-black/20'
|
||||
style={{ left: sideBarWidth, width: `calc(100% - ${sideBarWidth})` }}
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
+1
-3
@@ -3,7 +3,7 @@ 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';
|
||||
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
|
||||
|
||||
const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => {
|
||||
for (const item of toc) {
|
||||
@@ -61,8 +61,6 @@ const TOCItemView: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {}, [currentHref]);
|
||||
|
||||
const isActive = currentHref === item.href;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { MdToc, MdEditNote, MdBookmarkBorder } from 'react-icons/md';
|
||||
|
||||
const TabNavigation: React.FC<{
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
}> = ({ activeTab, onTabChange }) => (
|
||||
<div className='bottom-tab absolute bottom-0 flex w-full border-t'>
|
||||
{['toc', 'annotations', 'bookmarks'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === tab ? 'bg-base-300' : ''}`}
|
||||
onClick={() => onTabChange(tab)}
|
||||
>
|
||||
{tab === 'toc' && <MdToc size={20} className='mx-auto' />}
|
||||
{tab === 'annotations' && <MdEditNote size={20} className='mx-auto' />}
|
||||
{tab === 'bookmarks' && <MdBookmarkBorder size={20} className='mx-auto' />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TabNavigation;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
|
||||
interface UseBookShortcutsProps {
|
||||
sideBarBookKey: string | null;
|
||||
bookKeys: string[];
|
||||
openSplitView: () => void;
|
||||
getNextBookKey: (bookKey: string) => string;
|
||||
}
|
||||
|
||||
const useBookShortcuts = ({
|
||||
sideBarBookKey,
|
||||
bookKeys,
|
||||
openSplitView,
|
||||
getNextBookKey,
|
||||
}: UseBookShortcutsProps) => {
|
||||
const { getFoliateView, setSideBarBookKey } = useReaderStore();
|
||||
const { toggleSideBar } = useReaderStore();
|
||||
|
||||
const switchSideBar = () => {
|
||||
if (sideBarBookKey) setSideBarBookKey(getNextBookKey(sideBarBookKey));
|
||||
};
|
||||
|
||||
const goLeft = () => {
|
||||
getFoliateView(sideBarBookKey)?.goLeft();
|
||||
};
|
||||
|
||||
const goRight = () => {
|
||||
getFoliateView(sideBarBookKey)?.goRight();
|
||||
};
|
||||
|
||||
const reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
useShortcuts(
|
||||
{
|
||||
onOpenSplitView: openSplitView,
|
||||
onSwitchSideBar: switchSideBar,
|
||||
onToggleSideBar: toggleSideBar,
|
||||
onReloadPage: reloadPage,
|
||||
onGoLeft: goLeft,
|
||||
onGoRight: goRight,
|
||||
},
|
||||
[sideBarBookKey, bookKeys],
|
||||
);
|
||||
};
|
||||
|
||||
export default useBookShortcuts;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
const uniqueId = () => Math.random().toString(36).substring(2, 9);
|
||||
|
||||
const useBooks = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { books, initBookState, sideBarBookKey, setSideBarBookKey } = useReaderStore();
|
||||
|
||||
const initialIds = (searchParams.get('ids') || '').split(',').filter(Boolean);
|
||||
const [bookKeys, setBookKeys] = useState<string[]>(() => {
|
||||
return initialIds.map((id) => `${id}-${uniqueId()}`);
|
||||
});
|
||||
|
||||
const [shouldUpdateSearchParams, setShouldUpdateSearchParams] = useState(false);
|
||||
useEffect(() => {
|
||||
if (shouldUpdateSearchParams) {
|
||||
const ids = bookKeys.map((key) => key.split('-')[0]).join(',');
|
||||
if (ids) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('ids', ids);
|
||||
router.replace(`?${params.toString()}`, { scroll: false });
|
||||
}
|
||||
setShouldUpdateSearchParams(false);
|
||||
}
|
||||
}, [bookKeys, shouldUpdateSearchParams]);
|
||||
|
||||
// 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);
|
||||
setBookKeys((prevKeys) => {
|
||||
if (!prevKeys.includes(newKey)) {
|
||||
prevKeys.push(newKey);
|
||||
}
|
||||
return prevKeys;
|
||||
});
|
||||
setSideBarBookKey(newKey);
|
||||
setShouldUpdateSearchParams(true);
|
||||
};
|
||||
|
||||
// Close a book and sync with bookKeys and URL
|
||||
const dismissBook = (bookKey: string) => {
|
||||
setBookKeys((prevKeys) => {
|
||||
const updatedKeys = prevKeys.filter((key) => key !== bookKey);
|
||||
return updatedKeys;
|
||||
});
|
||||
setShouldUpdateSearchParams(true);
|
||||
};
|
||||
|
||||
const getNextBookKey = (bookKey: string) => {
|
||||
const index = bookKeys.findIndex((key) => key === bookKey);
|
||||
const nextIndex = (index + 1) % bookKeys.length;
|
||||
return bookKeys[nextIndex]!;
|
||||
};
|
||||
|
||||
const openSplitView = () => {
|
||||
const sideBarBookId = sideBarBookKey?.split('-')[0];
|
||||
if (sideBarBookId) appendBook(sideBarBookId, false);
|
||||
};
|
||||
|
||||
// Initialize all book states on first load
|
||||
useEffect(() => {
|
||||
const uniqueIds = new Set<string>();
|
||||
console.log('Initialize books', bookKeys);
|
||||
bookKeys.forEach((key, index) => {
|
||||
const id = key.split('-')[0]!;
|
||||
const isPrimary = !uniqueIds.has(id);
|
||||
uniqueIds.add(id);
|
||||
if (!books[key]) {
|
||||
initBookState(envConfig, id, key, isPrimary);
|
||||
if (index === 0) setSideBarBookKey(key);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
bookKeys,
|
||||
appendBook,
|
||||
dismissBook,
|
||||
getNextBookKey,
|
||||
openSplitView,
|
||||
};
|
||||
};
|
||||
|
||||
export default useBooks;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useDragBar = (
|
||||
handleSideBarResize: (width: string) => void,
|
||||
minWidth: number,
|
||||
maxWidth: number,
|
||||
) => {
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
const newWidthPx = e.clientX;
|
||||
const width = `${Math.round((newWidthPx / window.innerWidth) * 10000) / 100}%`;
|
||||
const minWidthPx = minWidth * window.innerWidth;
|
||||
const maxWidthPx = maxWidth * window.innerWidth;
|
||||
if (newWidthPx >= minWidthPx && newWidthPx <= maxWidthPx) {
|
||||
handleSideBarResize(width);
|
||||
}
|
||||
},
|
||||
[handleSideBarResize, minWidth, maxWidth],
|
||||
);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
};
|
||||
|
||||
return { handleMouseDown };
|
||||
};
|
||||
|
||||
export default useDragBar;
|
||||
@@ -20,5 +20,5 @@ export const useFoliateEvents = (view: FoliateView | null, handlers?: FoliateEve
|
||||
if (onLoad) view.removeEventListener('load', onLoad);
|
||||
if (onRelocate) view.removeEventListener('relocate', onRelocate);
|
||||
};
|
||||
}, [view, onLoad, onRelocate, handlers]);
|
||||
}, [view, onLoad, onRelocate]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useSidebar = (initialWidth: string, isPinned: boolean) => {
|
||||
const { settings } = useReaderStore();
|
||||
const {
|
||||
sideBarWidth,
|
||||
setSideBarWidth,
|
||||
isSideBarVisible,
|
||||
setSideBarVisibility,
|
||||
toggleSideBar,
|
||||
isSideBarPinned,
|
||||
setSideBarPin,
|
||||
toggleSideBarPin,
|
||||
} = useReaderStore();
|
||||
|
||||
useEffect(() => {
|
||||
setSideBarWidth(initialWidth);
|
||||
setSideBarPin(isPinned);
|
||||
setSideBarVisibility(isPinned);
|
||||
}, []);
|
||||
|
||||
const handleSideBarResize = (newWidth: string) => {
|
||||
setSideBarWidth(newWidth);
|
||||
settings.globalReadSettings.sideBarWidth = newWidth;
|
||||
};
|
||||
|
||||
const handleSideBarTogglePin = () => {
|
||||
toggleSideBarPin();
|
||||
settings.globalReadSettings.isSideBarPinned = !isSideBarPinned;
|
||||
if (isSideBarPinned && isSideBarVisible) setSideBarVisibility(false);
|
||||
};
|
||||
|
||||
return {
|
||||
sideBarWidth,
|
||||
isSideBarPinned,
|
||||
isSideBarVisible,
|
||||
handleSideBarResize,
|
||||
handleSideBarTogglePin,
|
||||
setSideBarVisibility,
|
||||
toggleSideBar,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSidebar;
|
||||
@@ -32,7 +32,7 @@ const ReaderPage = () => {
|
||||
settings.globalReadSettings && (
|
||||
<div className='min-h-screen select-none'>
|
||||
<Suspense>
|
||||
<ReaderContent />
|
||||
<ReaderContent settings={settings} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useEffect, useState } from 'react';
|
||||
import { loadShortcuts, ShortcutConfig } from '../helpers/shortcuts';
|
||||
|
||||
interface KeyActionHandlers {
|
||||
onSwitchSidebar?: () => void;
|
||||
onToggleSidebar?: () => void;
|
||||
onSwitchSideBar?: () => void;
|
||||
onToggleSideBar?: () => void;
|
||||
onOpenSplitView?: () => void;
|
||||
onReloadPage?: () => void;
|
||||
onGoLeft?: () => void;
|
||||
@@ -63,7 +63,7 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
isShortcutMatch(shortcut, key, ctrlKey, altKey, metaKey, shiftKey),
|
||||
)
|
||||
) {
|
||||
actions.onSwitchSidebar?.();
|
||||
actions.onSwitchSideBar?.();
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
@@ -71,7 +71,7 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
isShortcutMatch(shortcut, key, ctrlKey, altKey, metaKey, shiftKey),
|
||||
)
|
||||
) {
|
||||
actions.onToggleSidebar?.();
|
||||
actions.onToggleSideBar?.();
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -19,9 +19,25 @@ export interface BookState {
|
||||
|
||||
interface ReaderStore {
|
||||
library: Book[];
|
||||
books: Record<string, BookState>;
|
||||
settings: SystemSettings;
|
||||
|
||||
books: Record<string, BookState>;
|
||||
foliateViews: Record<string, FoliateView>;
|
||||
bookDocCache: Record<string, BookDoc>;
|
||||
|
||||
hoveredBookKey: string | null;
|
||||
sideBarBookKey: string | null;
|
||||
setHoveredBookKey: (key: string) => void;
|
||||
setSideBarBookKey: (key: string) => void;
|
||||
|
||||
sideBarWidth: string;
|
||||
isSideBarVisible: boolean;
|
||||
isSideBarPinned: boolean;
|
||||
setSideBarWidth: (width: string) => void;
|
||||
toggleSideBar: () => void;
|
||||
toggleSideBarPin: () => void;
|
||||
setSideBarVisibility: (visible: boolean) => void;
|
||||
setSideBarPin: (pinned: boolean) => void;
|
||||
|
||||
setLibrary: (books: Book[]) => void;
|
||||
setSettings: (settings: SystemSettings) => void;
|
||||
@@ -36,7 +52,7 @@ interface ReaderStore {
|
||||
) => void;
|
||||
setConfig: (key: string, config: BookConfig) => void;
|
||||
setFoliateView: (key: string, view: FoliateView) => void;
|
||||
getFoliateView: (key: string) => FoliateView | null;
|
||||
getFoliateView: (key: string | null) => FoliateView | null;
|
||||
|
||||
deleteBook: (envConfig: EnvConfigType, book: Book) => void;
|
||||
saveConfig: (
|
||||
@@ -48,7 +64,7 @@ interface ReaderStore {
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
initBookState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void;
|
||||
|
||||
closeBook: (key: string) => void;
|
||||
clearBookState: (key: string) => void;
|
||||
addBookmark: (key: string, bookmark: BookNote) => void;
|
||||
}
|
||||
|
||||
@@ -65,9 +81,25 @@ export const DEFAULT_BOOK_STATE = {
|
||||
|
||||
export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
library: [],
|
||||
books: {},
|
||||
settings: {} as SystemSettings,
|
||||
|
||||
books: {},
|
||||
foliateViews: {},
|
||||
bookDocCache: {},
|
||||
|
||||
hoveredBookKey: null,
|
||||
sideBarBookKey: null,
|
||||
setHoveredBookKey: (key: string) => set({ hoveredBookKey: key }),
|
||||
setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }),
|
||||
|
||||
sideBarWidth: '',
|
||||
isSideBarVisible: false,
|
||||
isSideBarPinned: false,
|
||||
setSideBarWidth: (width: string) => set({ sideBarWidth: width }),
|
||||
toggleSideBar: () => set((state) => ({ isSideBarVisible: !state.isSideBarVisible })),
|
||||
toggleSideBarPin: () => set((state) => ({ isSideBarPinned: !state.isSideBarPinned })),
|
||||
setSideBarVisibility: (visible: boolean) => set({ isSideBarVisible: visible }),
|
||||
setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }),
|
||||
|
||||
setLibrary: (books: Book[]) => set({ library: books }),
|
||||
setSettings: (settings: SystemSettings) => set({ settings }),
|
||||
@@ -94,7 +126,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
setFoliateView: (key: string, view) =>
|
||||
set((state) => ({ foliateViews: { ...state.foliateViews, [key]: view } })),
|
||||
|
||||
getFoliateView: (key: string) => get().foliateViews[key] || null,
|
||||
getFoliateView: (key: string | null) => (key && get().foliateViews[key]) || null,
|
||||
|
||||
deleteBook: async (envConfig: EnvConfigType, book: Book) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
@@ -130,7 +162,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
await appService.saveSettings(settings);
|
||||
},
|
||||
|
||||
closeBook: (key: string) => {
|
||||
clearBookState: (key: string) => {
|
||||
set((state) => {
|
||||
const books = { ...state.books };
|
||||
delete books[key];
|
||||
@@ -138,6 +170,8 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
initBookState: async (envConfig: EnvConfigType, id: string, key: string, isPrimary = true) => {
|
||||
const cache = get().bookDocCache || {};
|
||||
|
||||
set((state) => ({
|
||||
books: {
|
||||
...state.books,
|
||||
@@ -154,7 +188,21 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
}
|
||||
const content = (await appService.loadBookContent(book, settings)) as BookContent;
|
||||
const { file, config } = content;
|
||||
const { book: bookDoc } = await new DocumentLoader(file).open();
|
||||
let bookDoc: BookDoc;
|
||||
if (cache[id]) {
|
||||
console.log('Using cached bookDoc for book', key);
|
||||
bookDoc = cache[id];
|
||||
} else {
|
||||
console.log('Loading book', key);
|
||||
const { book: loadedBookDoc } = await new DocumentLoader(file).open();
|
||||
bookDoc = loadedBookDoc as BookDoc;
|
||||
set((state) => ({
|
||||
bookDocCache: {
|
||||
...state.bookDocCache,
|
||||
[id]: bookDoc,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
books: {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const getGridTemplate = (count: number, aspectRatio: number) => {
|
||||
if (count <= 1) {
|
||||
return { columns: '1fr', rows: '1fr' };
|
||||
} else if (count === 2) {
|
||||
return aspectRatio < 1
|
||||
? { columns: '1fr', rows: '1fr 1fr' }
|
||||
: { columns: '1fr 1fr', rows: '1fr' };
|
||||
} else if (count === 3 || count === 4) {
|
||||
return { columns: '1fr 1fr', rows: '1fr 1fr' };
|
||||
} else {
|
||||
return { columns: '1fr 1fr 1fr', rows: '1fr 1fr 1fr' };
|
||||
}
|
||||
};
|
||||
|
||||
export default getGridTemplate;
|
||||
Reference in New Issue
Block a user