forked from akai/readest
Responsive layout for sidebar and settings dialog on mobile devices (#184)
This commit is contained in:
@@ -11,6 +11,7 @@ import Dropdown from '@/components/Dropdown';
|
||||
import SidebarToggler from './SidebarToggler';
|
||||
import BookmarkToggler from './BookmarkToggler';
|
||||
import NotebookToggler from './NotebookToggler';
|
||||
import SettingsToggler from './SettingsToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
|
||||
interface HeaderBarProps {
|
||||
@@ -57,16 +58,17 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='sidebar-bookmark-toggler flex h-full items-center'>
|
||||
<div className='sidebar-bookmark-toggler flex h-full items-center space-x-2 sm:space-x-4'>
|
||||
<SidebarToggler bookKey={bookKey} />
|
||||
<BookmarkToggler bookKey={bookKey} />
|
||||
</div>
|
||||
|
||||
<div className='header-title flex flex-1 items-center justify-center'>
|
||||
<div className='header-title pointer-events-none absolute inset-0 flex items-center justify-center'>
|
||||
<h2 className='line-clamp-1 max-w-[80%] text-center text-xs font-semibold'>{bookTitle}</h2>
|
||||
</div>
|
||||
|
||||
<div className='flex h-full items-center space-x-2'>
|
||||
<div className='ml-auto flex h-full items-center space-x-2 sm:space-x-4'>
|
||||
<SettingsToggler />
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { TbLayoutSidebarRight, TbLayoutSidebarRightFilled } from 'react-icons/tb';
|
||||
import { LuNotebookPen } from 'react-icons/lu';
|
||||
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
@@ -26,9 +26,9 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
sideBarBookKey == bookKey && isNotebookVisible ? (
|
||||
<TbLayoutSidebarRightFilled size={20} className='text-base-content' />
|
||||
<LuNotebookPen size={16} className='text-base-content' />
|
||||
) : (
|
||||
<TbLayoutSidebarRight size={20} className='text-base-content' />
|
||||
<LuNotebookPen size={16} className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { RiFontSize } from 'react-icons/ri';
|
||||
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
const SettingsToggler = () => {
|
||||
const _ = useTranslation();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const handleToggleSettings = () => {
|
||||
setFontLayoutSettingsDialogOpen(!isFontLayoutSettingsDialogOpen);
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
icon={<RiFontSize size={16} className='text-base-content' />}
|
||||
onClick={handleToggleSettings}
|
||||
tooltip={_('Font & Layout')}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsToggler;
|
||||
@@ -92,7 +92,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomLevel <= MIN_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
@@ -100,7 +100,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
|
||||
'hover:bg-base-300 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
|
||||
)}
|
||||
onClick={resetZoom}
|
||||
>
|
||||
@@ -109,7 +109,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomLevel >= MAX_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { LuNotebookPen } from 'react-icons/lu';
|
||||
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const NotebookHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
handleClose: () => void;
|
||||
handleTogglePin: () => void;
|
||||
}> = ({ isPinned, handleTogglePin }) => {
|
||||
}> = ({ isPinned, handleClose, handleTogglePin }) => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div className='notebook-header relative flex h-11 items-center px-3'>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<div className='absolute inset-0 flex items-center justify-center space-x-2'>
|
||||
<LuNotebookPen size={14} />
|
||||
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center space-x-3'>
|
||||
<div className='z-10 flex items-center gap-x-3'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
className={clsx(
|
||||
'btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
|
||||
isPinned ? 'bg-base-300' : 'bg-base-300/65',
|
||||
)}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
</button>
|
||||
|
||||
@@ -8,13 +8,14 @@ import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
|
||||
const MIN_NOTEBOOK_WIDTH = 0.15;
|
||||
const MAX_NOTEBOOK_WIDTH = 0.45;
|
||||
@@ -31,10 +32,23 @@ const Notebook: React.FC = ({}) => {
|
||||
const { setNotebookWidth, setNotebookVisible, toggleNotebookPin } = useNotebookStore();
|
||||
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
|
||||
|
||||
const onNavigateEvent = async () => {
|
||||
const pinButton = document.querySelector('.sidebar-pin-btn');
|
||||
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
|
||||
if (isPinButtonHidden) {
|
||||
setNotebookVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNotebookWidth(settings.globalReadSettings.notebookWidth);
|
||||
setNotebookPin(settings.globalReadSettings.isNotebookPinned);
|
||||
setNotebookVisible(settings.globalReadSettings.isNotebookPinned);
|
||||
|
||||
eventDispatcher.on('navigate', onNavigateEvent);
|
||||
return () => {
|
||||
eventDispatcher.off('navigate', onNavigateEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -135,11 +149,23 @@ const Notebook: React.FC = ({}) => {
|
||||
position: isNotebookPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
@media (max-width: 640px) {
|
||||
.notebook-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<NotebookHeader isPinned={isNotebookPinned} handleTogglePin={handleTogglePin} />
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
handleClose={() => setNotebookVisible(false)}
|
||||
handleTogglePin={handleTogglePin}
|
||||
/>
|
||||
<div className='max-h-[calc(100vh-44px)] overflow-y-auto px-3'>
|
||||
<div>{excerptNotes.length > 0 && <p className='pt-1 text-sm'>{_('Excerpts')}</p>}</div>
|
||||
<ul className=''>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
@@ -18,6 +19,12 @@ import MiscPanel from './MiscPanel';
|
||||
|
||||
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Misc';
|
||||
|
||||
type TabConfig = {
|
||||
tab: SettingsPanelType;
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
|
||||
@@ -37,39 +44,47 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
tab: 'Font',
|
||||
icon: RiFontSize,
|
||||
label: _('Font'),
|
||||
},
|
||||
{
|
||||
tab: 'Layout',
|
||||
icon: RiDashboardLine,
|
||||
label: _('Layout'),
|
||||
},
|
||||
{
|
||||
tab: 'Color',
|
||||
icon: VscSymbolColor,
|
||||
label: _('Color'),
|
||||
},
|
||||
{
|
||||
tab: 'Misc',
|
||||
icon: IoAccessibilityOutline,
|
||||
label: _('Misc'),
|
||||
},
|
||||
] as TabConfig[];
|
||||
|
||||
return (
|
||||
<dialog className='modal modal-open min-w-90 w-full !bg-[rgba(0,0,0,0.2)]'>
|
||||
<div className='modal-box settings-content flex h-[65%] w-1/2 min-w-[480px] max-w-full flex-col p-0'>
|
||||
<dialog className='modal modal-open sm:min-w-90 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full'>
|
||||
<div className='modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:h-[65%] sm:w-1/2 sm:min-w-[480px] sm:max-w-full sm:rounded-2xl'>
|
||||
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
|
||||
<div className='dialog-tabs flex h-10 max-w-[80%] flex-grow items-center justify-around'>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Font' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Font')}
|
||||
>
|
||||
<RiFontSize size={20} className='mr-0' />
|
||||
{_('Font')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Layout')}
|
||||
>
|
||||
<RiDashboardLine size={20} className='mr-0' />
|
||||
{_('Layout')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Color' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Color')}
|
||||
>
|
||||
<VscSymbolColor size={20} className='mr-0' />
|
||||
{_('Color')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Misc' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Misc')}
|
||||
>
|
||||
<IoAccessibilityOutline size={20} className='mr-0' />
|
||||
{_('Misc')}
|
||||
</button>
|
||||
<div className='dialog-tabs flex h-10 max-w-[75%] flex-grow items-center justify-around sm:max-w-[80%]'>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={clsx(
|
||||
'btn btn-ghost text-base-content h-8 min-h-8',
|
||||
activePanel === tab ? 'btn-active' : '',
|
||||
)}
|
||||
onClick={() => setActivePanel(tab)}
|
||||
>
|
||||
<Icon size={20} className='mr-0' />
|
||||
{window.innerWidth >= 500 ? label : ''}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end'>
|
||||
<Dropdown
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -10,10 +11,11 @@ import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import useBooksManager from '../../hooks/useBooksManager';
|
||||
|
||||
interface BookMenuProps {
|
||||
menuClassName?: string;
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const { library } = useLibraryStore();
|
||||
const { openParallelView } = useBooksManager();
|
||||
@@ -39,7 +41,10 @@ const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='book-menu dropdown-content dropdown-center border-base-100 z-20 mt-3 w-60 shadow-2xl'
|
||||
className={clsx(
|
||||
'book-menu dropdown-content border-base-100 z-20 w-60 shadow-2xl',
|
||||
menuClassName,
|
||||
)}
|
||||
>
|
||||
<MenuItem label={_('Parallel Read')} noIcon>
|
||||
<ul className='max-h-60 overflow-y-auto'>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import useScrollToItem from '../../hooks/useScrollToItem';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
@@ -29,6 +30,8 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
eventDispatcher.dispatch('navigate', { bookKey, cfi });
|
||||
|
||||
getView(bookKey)?.goTo(cfi);
|
||||
if (note) {
|
||||
setNotebookVisible(true);
|
||||
|
||||
@@ -12,9 +12,10 @@ const SidebarHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
isSearchBarVisible: boolean;
|
||||
onGoToLibrary: () => void;
|
||||
onClose: () => void;
|
||||
onTogglePin: () => void;
|
||||
onToggleSearchBar: () => void;
|
||||
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onTogglePin, onToggleSearchBar }) => {
|
||||
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
return (
|
||||
<div
|
||||
@@ -39,7 +40,11 @@ const SidebarHeader: React.FC<{
|
||||
<FiSearch size={18} className='text-base-content' />
|
||||
</button>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
className={clsx(
|
||||
window.innerWidth < 640 && 'dropdown-end',
|
||||
'dropdown-bottom flex justify-center',
|
||||
)}
|
||||
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3'}
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={20} className='fill-base-content' />}
|
||||
>
|
||||
@@ -48,10 +53,24 @@ const SidebarHeader: React.FC<{
|
||||
<div className='right-0 flex h-8 w-8 items-center justify-center'>
|
||||
<button
|
||||
onClick={onTogglePin}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
className={clsx(
|
||||
'sidebar-pin-btn btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
|
||||
isPinned ? 'bg-base-300' : 'bg-base-300/65',
|
||||
)}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
|
||||
/>
|
||||
|
||||
<div className='flex h-8 w-8 items-center rounded-r-lg bg-gray-300'>
|
||||
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg'
|
||||
|
||||
@@ -9,100 +9,68 @@ interface SearchOptionsProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
interface OptionProps {
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const Option: React.FC<OptionProps> = ({ label, isActive, onClick }) => (
|
||||
<button
|
||||
className='hover:bg-base-300 flex w-full items-center justify-between rounded-md p-2'
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isActive && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{label}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
searchConfig,
|
||||
onSearchConfigChanged,
|
||||
setIsDropdownOpen,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const handleSetScope = () => {
|
||||
onSearchConfigChanged({
|
||||
...searchConfig,
|
||||
scope: searchConfig.scope === 'book' ? 'section' : 'book',
|
||||
});
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handleSetMatchCase = () => {
|
||||
onSearchConfigChanged({ ...searchConfig, matchCase: !searchConfig.matchCase });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handleSetMatchWholeWords = () => {
|
||||
onSearchConfigChanged({ ...searchConfig, matchWholeWords: !searchConfig.matchWholeWords });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handleSetMatchDiacritics = () => {
|
||||
onSearchConfigChanged({ ...searchConfig, matchDiacritics: !searchConfig.matchDiacritics });
|
||||
const updateConfig = (key: keyof BookSearchConfig, value: boolean | string) => {
|
||||
onSearchConfigChanged({ ...searchConfig, [key]: value });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='book-menu dropdown-content dropdown-center z-20 mt-3 w-56 border bg-white shadow-2xl'
|
||||
className='book-menu dropdown-content dropdown-center border-base-200 z-20 mt-3 w-56 border shadow-2xl'
|
||||
>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetScope}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.scope === 'book' && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Book')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetScope}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.scope === 'section' && (
|
||||
<MdCheck size={20} className='text-base-content' />
|
||||
)}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Chapter')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<hr className='my-1' />
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetMatchCase}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchCase && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Match Case')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetMatchWholeWords}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchWholeWords && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Match Whole Words')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetMatchDiacritics}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchDiacritics && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Match Diacritics')}</span>
|
||||
</div>
|
||||
</button>
|
||||
<Option
|
||||
label={_('Book')}
|
||||
isActive={searchConfig.scope === 'book'}
|
||||
onClick={() => updateConfig('scope', 'book')}
|
||||
/>
|
||||
<Option
|
||||
label={_('Chapter')}
|
||||
isActive={searchConfig.scope === 'section'}
|
||||
onClick={() => updateConfig('scope', 'section')}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
<Option
|
||||
label={_('Match Case')}
|
||||
isActive={searchConfig.matchCase}
|
||||
onClick={() => updateConfig('matchCase', !searchConfig.matchCase)}
|
||||
/>
|
||||
<Option
|
||||
label={_('Match Whole Words')}
|
||||
isActive={searchConfig.matchWholeWords}
|
||||
onClick={() => updateConfig('matchWholeWords', !searchConfig.matchWholeWords)}
|
||||
/>
|
||||
<Option
|
||||
label={_('Match Diacritics')}
|
||||
isActive={searchConfig.matchDiacritics}
|
||||
onClick={() => updateConfig('matchDiacritics', !searchConfig.matchDiacritics)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const SideBar: React.FC<{
|
||||
handleSideBarTogglePin,
|
||||
} = useSidebar(
|
||||
settings.globalReadSettings.sideBarWidth,
|
||||
settings.globalReadSettings.isSideBarPinned,
|
||||
window.innerWidth >= 640 ? settings.globalReadSettings.isSideBarPinned : false,
|
||||
);
|
||||
|
||||
const onSearchEvent = async (event: CustomEvent) => {
|
||||
@@ -48,10 +48,20 @@ const SideBar: React.FC<{
|
||||
setSearchTerm(term);
|
||||
};
|
||||
|
||||
const onNavigateEvent = async () => {
|
||||
const pinButton = document.querySelector('.sidebar-pin-btn');
|
||||
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
|
||||
if (isPinButtonHidden) {
|
||||
setSideBarVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('search', onSearchEvent);
|
||||
eventDispatcher.on('navigate', onNavigateEvent);
|
||||
return () => {
|
||||
eventDispatcher.off('search', onSearchEvent);
|
||||
eventDispatcher.off('navigate', onNavigateEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -79,6 +89,7 @@ const SideBar: React.FC<{
|
||||
useShortcuts({ onToggleSearchBar: handleToggleSearchBar }, [sideBarBookKey]);
|
||||
|
||||
const handleSearchResultClick = (cfi: string) => {
|
||||
onNavigateEvent();
|
||||
getView(sideBarBookKey)?.goTo(cfi);
|
||||
};
|
||||
|
||||
@@ -104,11 +115,20 @@ const SideBar: React.FC<{
|
||||
position: isSideBarPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
@media (max-width: 640px) {
|
||||
.sidebar-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className='flex-shrink-0'>
|
||||
<SidebarHeader
|
||||
isPinned={isSideBarPinned}
|
||||
isSearchBarVisible={isSearchBarVisible}
|
||||
onGoToLibrary={onGoToLibrary}
|
||||
onClose={() => setSideBarVisible(false)}
|
||||
onTogglePin={handleSideBarTogglePin}
|
||||
onToggleSearchBar={handleToggleSearchBar}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { TOCItem } from '@/libs/document';
|
||||
@@ -5,7 +6,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { findParentPath } from '@/utils/toc';
|
||||
import { getContentMd5 } from '@/utils/misc';
|
||||
import clsx from 'clsx';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
const createExpanderIcon = (isExpanded: boolean) => {
|
||||
return (
|
||||
@@ -43,6 +44,7 @@ const TOCItemView: React.FC<{
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
eventDispatcher.dispatch('navigate', { bookKey, href: item.href });
|
||||
if (item.href) {
|
||||
getView(bookKey)?.goTo(item.href);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ const Button: React.FC<ButtonProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('tooltip z-50', tooltip && `tooltip-${tooltipDirection}`, {
|
||||
className={clsx('tooltip z-50 h-8 min-h-8 w-8', tooltip && `tooltip-${tooltipDirection}`, {
|
||||
'tooltip-hidden': !tooltip,
|
||||
})}
|
||||
data-tip={tooltip}
|
||||
>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost mx-2 h-8 min-h-8 w-8 p-0',
|
||||
'btn btn-ghost h-8 min-h-8 w-8 p-0',
|
||||
disabled && 'btn-disabled !bg-transparent',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -3,14 +3,16 @@ import React, { useState, isValidElement, ReactElement } from 'react';
|
||||
|
||||
interface DropdownProps {
|
||||
className?: string;
|
||||
menuClassName?: string;
|
||||
buttonClassName?: string;
|
||||
toggleButton: React.ReactNode;
|
||||
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void }>;
|
||||
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void; menuClassName?: string }>;
|
||||
onToggle?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
className,
|
||||
menuClassName,
|
||||
buttonClassName,
|
||||
toggleButton,
|
||||
children,
|
||||
@@ -30,7 +32,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
};
|
||||
|
||||
const childrenWithToggle = isValidElement(children)
|
||||
? React.cloneElement(children, { setIsDropdownOpen })
|
||||
? React.cloneElement(children, { setIsDropdownOpen, menuClassName })
|
||||
: children;
|
||||
|
||||
return (
|
||||
@@ -42,7 +44,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onClick={toggleDropdown}
|
||||
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-200')}
|
||||
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-300/50')}
|
||||
>
|
||||
{toggleButton}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const menuButton = (
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-neutral text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
|
||||
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
|
||||
disabled && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -44,7 +44,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
<ul className='menu rounded-box m-0 p-0'>
|
||||
<li>
|
||||
<details>
|
||||
<summary className='hover:bg-neutral p-0 pr-3'>{menuButton}</summary>
|
||||
<summary className='hover:bg-base-300 p-0 pr-3'>{menuButton}</summary>
|
||||
{children}
|
||||
</details>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user