Add font and layout settings UI
This commit is contained in:
@@ -81,10 +81,6 @@ foliate-view {
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
@apply text-gray-700;
|
||||
}
|
||||
|
||||
.dropdown-content::before,
|
||||
@@ -111,7 +107,7 @@ foliate-view {
|
||||
|
||||
.dropdown-left::before,
|
||||
.dropdown-left::after {
|
||||
left: 20px;
|
||||
left: 16px;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@@ -123,7 +119,7 @@ foliate-view {
|
||||
|
||||
.dropdown-right::before,
|
||||
.dropdown-right::after {
|
||||
right: 20px;
|
||||
right: 16px;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@@ -131,3 +127,24 @@ foliate-view {
|
||||
.dropdown-content.no-triangle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal, .dropdown-content {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
@apply text-gray-700;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
@apply flex h-14 items-center justify-between p-4;
|
||||
@apply hover:bg-gray-50;
|
||||
}
|
||||
|
||||
.config-item-top {
|
||||
@apply rounded-t-2xl;
|
||||
}
|
||||
|
||||
.config-item-bottom {
|
||||
@apply rounded-b-2xl;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,19 +58,19 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
/>
|
||||
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
|
||||
<span className='mx-2 h-5 w-[1px] bg-gray-300'></span>
|
||||
<span className='dropdown dropdown-end h-5 cursor-pointer text-gray-500'>
|
||||
<div className='dropdown dropdown-bottom flex h-5 cursor-pointer justify-center text-gray-500'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip='Add books'>
|
||||
<PiPlus tabIndex={0} className='h-5 w-5' role='button' />
|
||||
<PiPlus tabIndex={-1} className='h-5 w-5' />
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
|
||||
tabIndex={-1}
|
||||
className='dropdown-content dropdown-center menu bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow'
|
||||
>
|
||||
<li>
|
||||
<button onClick={onImportBooks}>From Local File</button>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={onToggleSelectMode} aria-label='Select Multiple Books' className='h-6'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom cursor-pointer' data-tip='Select books'>
|
||||
<PiSelectionAllDuotone
|
||||
|
||||
@@ -7,6 +7,7 @@ import PageInfo from './PageInfo';
|
||||
import FooterBar from './FooterBar';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
|
||||
interface BookGridProps {
|
||||
bookKeys: string[];
|
||||
@@ -16,6 +17,7 @@ interface BookGridProps {
|
||||
|
||||
const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }) => {
|
||||
const { isSideBarPinned, isSideBarVisible, sideBarWidth } = useReaderStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useReaderStore();
|
||||
const gridWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
|
||||
@@ -42,6 +44,7 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
|
||||
bookTitle={book.title}
|
||||
isHoveredAnim={bookStates.length > 2}
|
||||
onCloseBook={onCloseBook}
|
||||
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
|
||||
/>
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookState.bookDoc!} bookConfig={config} />
|
||||
{config.viewSettings!.scrolled ? null : (
|
||||
@@ -56,6 +59,7 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
|
||||
</>
|
||||
)}
|
||||
<FooterBar bookKey={bookKey} progress={progress} isHoveredAnim={false} />
|
||||
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -13,6 +13,7 @@ interface HeaderBarProps {
|
||||
bookTitle: string;
|
||||
isHoveredAnim: boolean;
|
||||
onCloseBook: (bookKey: string) => void;
|
||||
onSetSettingsDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
@@ -20,6 +21,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
bookTitle,
|
||||
isHoveredAnim,
|
||||
onCloseBook,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
@@ -61,14 +63,14 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<h2 className='line-clamp-1 max-w-[80%] text-center text-xs font-semibold'>{bookTitle}</h2>
|
||||
</div>
|
||||
|
||||
<div className='ml-auto flex h-full items-center space-x-2'>
|
||||
<div className='flex h-full items-center space-x-2'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
onToggle={setIsDropdownOpen}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} />
|
||||
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
|
||||
</Dropdown>
|
||||
|
||||
<WindowButtons
|
||||
|
||||
@@ -9,9 +9,14 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
interface ViewMenuProps {
|
||||
bookKey: string;
|
||||
toggleDropdown?: () => void;
|
||||
onSetSettingsDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey }) => {
|
||||
const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
bookKey,
|
||||
toggleDropdown,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const { books, setConfig, getFoliateView } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
@@ -29,7 +34,8 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey }) => {
|
||||
const toggleInvertedColors = () => setInvertedColors(!isInvertedColors);
|
||||
|
||||
const openFontLayoutMenu = () => {
|
||||
// open font layout menu
|
||||
toggleDropdown?.();
|
||||
onSetSettingsDialogOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const ColorPanel: React.FC = () => {
|
||||
const [selectedTheme, setSelectedTheme] = useState('Default');
|
||||
const themes = [
|
||||
'Default',
|
||||
'Gray',
|
||||
'Sepia',
|
||||
'Grass',
|
||||
'Cherry',
|
||||
'Sky',
|
||||
'Nord',
|
||||
'Solarized',
|
||||
'Gruvbox',
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-lg font-bold'>Color Settings</h2>
|
||||
{/* Theme Selection */}
|
||||
<div className='mt-4 grid grid-cols-3 gap-2'>
|
||||
{themes.map((theme) => (
|
||||
<button
|
||||
key={theme}
|
||||
className={`btn ${selectedTheme === theme ? 'btn-active' : ''}`}
|
||||
onClick={() => setSelectedTheme(theme)}
|
||||
>
|
||||
{theme}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPanel;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
|
||||
interface DialogMenuProps {
|
||||
toggleDropdown?: () => void;
|
||||
}
|
||||
|
||||
const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useReaderStore();
|
||||
|
||||
const handleToggleGlobal = () => {
|
||||
setFontLayoutSettingsGlobal(!isFontLayoutSettingsGlobal);
|
||||
toggleDropdown?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='dropdown-content dropdown-right no-triangle z-20 mt-1 w-44 border bg-white shadow-2xl'
|
||||
>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleToggleGlobal}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isFontLayoutSettingsGlobal && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<div className='tooltip' data-tip='Uncheck for current book settings'>
|
||||
<span className='ml-2'>Global Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogMenu;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { FiChevronDown } from 'react-icons/fi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
|
||||
interface DropdownProps {
|
||||
family?: string;
|
||||
selected: string;
|
||||
options: string[];
|
||||
onSelect: (option: string) => void;
|
||||
onGetFontFamily: (option: string, family: string) => string;
|
||||
}
|
||||
|
||||
const FontDropdown: React.FC<DropdownProps> = ({
|
||||
family,
|
||||
selected,
|
||||
options,
|
||||
onSelect,
|
||||
onGetFontFamily,
|
||||
}) => {
|
||||
return (
|
||||
<div className='dropdown dropdown-end'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
|
||||
>
|
||||
<span style={{ fontFamily: onGetFontFamily(selected, family ?? '') }}>{selected}</span>
|
||||
<FiChevronDown className='h-4 w-4' />
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content dropdown-right menu bg-base-100 rounded-box z-[1] mt-4 w-44 shadow'
|
||||
>
|
||||
{options.map((option) => (
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selected === option && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>{option}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontDropdown;
|
||||
@@ -0,0 +1,155 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import NumberInput from './NumberInput';
|
||||
import FontDropdown from './FontDropDown';
|
||||
|
||||
interface FontFaceProps {
|
||||
className?: string;
|
||||
family: string;
|
||||
label: string;
|
||||
options: string[];
|
||||
selected: string;
|
||||
onSelect: (option: string) => void;
|
||||
}
|
||||
|
||||
const fontFamilyOptions = ['Serif', 'Sans-serif'];
|
||||
const serifFonts = [
|
||||
'Literata',
|
||||
'Vollkorn',
|
||||
'Aleo',
|
||||
'Crimson Text',
|
||||
'Merriweather',
|
||||
'Georgia',
|
||||
'Times New Roman',
|
||||
];
|
||||
const sansSerifFonts = ['Roboto', 'Open Sans', 'Noto Sans', 'Poppins', 'Helvetica', 'Arial'];
|
||||
const monospaceFonts = ['Fira Code', 'Lucida Console', 'Consolas', 'Courier New'];
|
||||
|
||||
const FontPanel: React.FC = () => {
|
||||
const [defaultFontSize, setDefaultFontSize] = useState(18);
|
||||
const [minFontSize, setMinFontSize] = useState(1);
|
||||
const [overridePublisherFont, setOverridePublisherFont] = useState(true);
|
||||
const [defaultFont, setDefaultFont] = useState('Serif');
|
||||
const [serifFont, setSerifFont] = useState('Georgia');
|
||||
const [sansSerifFont, setSansSerifFont] = useState('Roboto');
|
||||
const [monospaceFont, setMonospaceFont] = useState('Consolas');
|
||||
|
||||
const handleFontFamilyFont = (option: string) => {
|
||||
switch (option) {
|
||||
case 'Serif':
|
||||
return `'${serifFont}', serif`;
|
||||
case 'Sans-serif':
|
||||
return `'${sansSerifFont}', sans-serif`;
|
||||
case 'Monospace':
|
||||
return `'${monospaceFont}', monospace`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleFontFaceFont = (option: string, family: string) => {
|
||||
return `'${option}', ${family}`;
|
||||
};
|
||||
|
||||
const FontFace = ({ className, family, label, options, selected, onSelect }: FontFaceProps) => (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className='text-gray-700'>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options}
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
onGetFontFamily={handleFontFaceFont}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='cell-font-size w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Size</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Default Font Size'
|
||||
value={defaultFontSize}
|
||||
onChange={setDefaultFontSize}
|
||||
min={1}
|
||||
max={120}
|
||||
/>
|
||||
<NumberInput
|
||||
className='config-item-bottom'
|
||||
label='Minimum Font Size'
|
||||
value={minFontSize}
|
||||
onChange={setMinFontSize}
|
||||
min={1}
|
||||
max={120}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='cell-font-family w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Family</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='config-item config-item-top'>
|
||||
<span className='text-gray-700'>Default Font</span>
|
||||
<FontDropdown
|
||||
options={fontFamilyOptions}
|
||||
selected={defaultFont}
|
||||
onSelect={setDefaultFont}
|
||||
onGetFontFamily={handleFontFamilyFont}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className='text-gray-700'>Override Publisher Font</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={overridePublisherFont}
|
||||
onChange={() => setOverridePublisherFont(!overridePublisherFont)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='cell-font-family w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Face</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<FontFace
|
||||
className='config-item-top'
|
||||
family='serif'
|
||||
label='Serif Font'
|
||||
options={serifFonts}
|
||||
selected={serifFont}
|
||||
onSelect={setSerifFont}
|
||||
/>
|
||||
<FontFace
|
||||
family='sans-serif'
|
||||
label='Sans-Serif Font'
|
||||
options={sansSerifFonts}
|
||||
selected={sansSerifFont}
|
||||
onSelect={setSansSerifFont}
|
||||
/>
|
||||
<FontFace
|
||||
className='config-item-bottom'
|
||||
family='monospace'
|
||||
label='Monospace Font'
|
||||
options={monospaceFonts}
|
||||
selected={monospaceFont}
|
||||
onSelect={setMonospaceFont}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontPanel;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useState } from 'react';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
const LayoutPanel: React.FC = () => {
|
||||
const [lineHeight, setLineHeight] = useState(1.5);
|
||||
const [fullJustification, setFullJustification] = useState(true);
|
||||
const [hyphenation, setHyphenation] = useState(true);
|
||||
const [margins, setMargins] = useState(0.05);
|
||||
const [maxNumberOfColumns, setMaxNumberOfColumns] = useState(2);
|
||||
const [maxInlineSize, setMaxInlineSize] = useState(720);
|
||||
const [maxBlockSize, setMaxBlockSize] = useState(1440);
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='cell-font-size w-full'>
|
||||
<h2 className='mb-2 font-medium'>Paragraph</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Line Height'
|
||||
value={lineHeight}
|
||||
onChange={setLineHeight}
|
||||
min={1}
|
||||
max={120}
|
||||
/>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className='text-gray-700'>Full Justification</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={fullJustification}
|
||||
onChange={() => setFullJustification(!fullJustification)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className='text-gray-700'>Hyphenation</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={hyphenation}
|
||||
onChange={() => setHyphenation(!hyphenation)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='cell-font-family w-full'>
|
||||
<h2 className='mb-2 font-medium'>Page</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Margins'
|
||||
value={margins}
|
||||
onChange={setMargins}
|
||||
min={0}
|
||||
max={0.3}
|
||||
step={0.01}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Maximum Number of Columns'
|
||||
value={maxNumberOfColumns}
|
||||
onChange={setMaxNumberOfColumns}
|
||||
min={1}
|
||||
max={2}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Maximum Inline Size'
|
||||
value={maxInlineSize}
|
||||
onChange={setMaxInlineSize}
|
||||
min={500}
|
||||
max={9999}
|
||||
step={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Maximum Block Size'
|
||||
value={maxBlockSize}
|
||||
onChange={setMaxBlockSize}
|
||||
min={500}
|
||||
max={9999}
|
||||
step={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutPanel;
|
||||
@@ -0,0 +1,90 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { FiMinus, FiPlus } from 'react-icons/fi';
|
||||
|
||||
interface NumberInputProps {
|
||||
className?: string;
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
const NumberInput: React.FC<NumberInputProps> = ({
|
||||
className,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const numberStep = step || 1;
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value === '' ? 0 : parseInt(e.target.value);
|
||||
if (!isNaN(newValue)) {
|
||||
setLocalValue(newValue);
|
||||
onChange(Math.max(min, Math.min(max, newValue)));
|
||||
}
|
||||
};
|
||||
|
||||
const increment = () => {
|
||||
const newValue = Math.min(max, localValue + numberStep);
|
||||
setLocalValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const decrement = () => {
|
||||
const newValue = Math.max(min, localValue - numberStep);
|
||||
setLocalValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
const newValue = Math.max(min, Math.min(max, localValue));
|
||||
setLocalValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className='text-gray-700'>{label}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<input
|
||||
type='text'
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onBlur={handleOnBlur}
|
||||
className='input input-ghost w-20 max-w-xs rounded px-3 py-1 text-right'
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
<button
|
||||
onClick={decrement}
|
||||
className={`btn btn-circle btn-sm ${value === min ? 'btn-disabled !bg-opacity-5' : ''}`}
|
||||
>
|
||||
<FiMinus className='h-4 w-4' />
|
||||
</button>
|
||||
<button
|
||||
onClick={increment}
|
||||
className={`btn btn-circle btn-sm ${value === max ? 'btn-disabled !bg-opacity-5' : ''}`}
|
||||
>
|
||||
<FiPlus className='h-4 w-4' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberInput;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { RiFontSize } from 'react-icons/ri';
|
||||
import { RiDashboardLine } from 'react-icons/ri';
|
||||
import { VscSymbolColor } from 'react-icons/vsc';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import FontPanel from './FontPanel';
|
||||
import LayoutPanel from './LayoutPanel';
|
||||
import ColorPanel from './ColorPanel';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import DialogMenu from './DialogMenu';
|
||||
|
||||
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({}) => {
|
||||
const [activePanel, setActivePanel] = useState('Font');
|
||||
const { setFontLayoutSettingsDialogOpen } = useReaderStore();
|
||||
|
||||
return (
|
||||
<dialog className='modal modal-open min-w-90 w-full'>
|
||||
<div className='modal-box flex h-[60%] w-1/2 min-w-96 max-w-full flex-col p-0'>
|
||||
<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 h-8 min-h-8 px-6 ${activePanel === 'Font' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Font')}
|
||||
>
|
||||
<RiFontSize size={20} className='mr-0' />
|
||||
Font
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost h-8 min-h-8 px-6 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Layout')}
|
||||
>
|
||||
<RiDashboardLine size={20} className='mr-0' />
|
||||
Layout
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost h-8 min-h-8 px-6 ${activePanel === 'Color' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Color')}
|
||||
>
|
||||
<VscSymbolColor size={20} className='mr-0' />
|
||||
Color
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end absolute right-12'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
>
|
||||
<DialogMenu />
|
||||
</Dropdown>
|
||||
<WindowButtons
|
||||
className='window-buttons absolute right-4 !ml-2 flex h-full items-center'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={() => setFontLayoutSettingsDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 flex-grow overflow-y-auto px-16'>
|
||||
{activePanel === 'Font' && <FontPanel />}
|
||||
{activePanel === 'Layout' && <LayoutPanel />}
|
||||
{activePanel === 'Color' && <ColorPanel />}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsDialog;
|
||||
@@ -37,7 +37,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent | Event) => {
|
||||
if (event instanceof KeyboardEvent) {
|
||||
if (event instanceof MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
closeDropdown();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface WindowButtonsProps {
|
||||
className?: string;
|
||||
headerRef: React.RefObject<HTMLDivElement>;
|
||||
headerRef?: React.RefObject<HTMLDivElement>;
|
||||
showMinimize?: boolean;
|
||||
showMaximize?: boolean;
|
||||
showClose?: boolean;
|
||||
@@ -41,7 +41,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const headerElement = headerRef.current;
|
||||
const headerElement = headerRef?.current;
|
||||
headerElement?.addEventListener('mousedown', handleMouseDown);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
|
||||
export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
zoomLevel: 100,
|
||||
lineHeight: 1.5,
|
||||
lineHeight: 1.6,
|
||||
justify: true,
|
||||
hyphenate: true,
|
||||
invert: false,
|
||||
|
||||
@@ -39,6 +39,11 @@ interface ReaderStore {
|
||||
setSideBarVisibility: (visible: boolean) => void;
|
||||
setSideBarPin: (pinned: boolean) => void;
|
||||
|
||||
isFontLayoutSettingsDialogOpen: boolean;
|
||||
isFontLayoutSettingsGlobal: boolean;
|
||||
setFontLayoutSettingsDialogOpen: (open: boolean) => void;
|
||||
setFontLayoutSettingsGlobal: (global: boolean) => void;
|
||||
|
||||
setLibrary: (books: Book[]) => void;
|
||||
setSettings: (settings: SystemSettings) => void;
|
||||
setProgress: (
|
||||
@@ -101,6 +106,11 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
setSideBarVisibility: (visible: boolean) => set({ isSideBarVisible: visible }),
|
||||
setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }),
|
||||
|
||||
isFontLayoutSettingsDialogOpen: false,
|
||||
isFontLayoutSettingsGlobal: true,
|
||||
setFontLayoutSettingsDialogOpen: (open: boolean) => set({ isFontLayoutSettingsDialogOpen: open }),
|
||||
setFontLayoutSettingsGlobal: (global: boolean) => set({ isFontLayoutSettingsGlobal: global }),
|
||||
|
||||
setLibrary: (books: Book[]) => set({ library: books }),
|
||||
setSettings: (settings: SystemSettings) => set({ settings }),
|
||||
setConfig: (key: string, config: BookConfig) => {
|
||||
|
||||
Reference in New Issue
Block a user