Apply styles in the settings dialog

This commit is contained in:
chrox
2024-11-02 23:07:25 +01:00
parent e0f468e30c
commit 9617f6f273
18 changed files with 487 additions and 180 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ const nextConfig = {
unoptimized: true,
},
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: isProd ? null : `http://${internalHost}:3000`,
assetPrefix: isProd ? '' : `http://${internalHost}:3000`,
reactStrictMode: true,
};
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Readest",
"version": "0.3.5",
"version": "0.4.2",
"identifier": "com.bilingify.digest",
"build": {
"frontendDist": "../out",
+3 -3
View File
@@ -107,7 +107,7 @@ foliate-view {
.dropdown-left::before,
.dropdown-left::after {
left: 16px;
left: 20px;
transform: translateX(0);
}
@@ -119,7 +119,7 @@ foliate-view {
.dropdown-right::before,
.dropdown-right::after {
right: 16px;
right: 20px;
transform: translateX(0);
}
@@ -128,7 +128,7 @@ foliate-view {
display: none;
}
.modal, .dropdown-content {
.dropdown-content, .settings-content {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
font-weight: 400;
@@ -35,7 +35,7 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
const { book, config, bookDoc } = bookState;
if (!book || !config || !bookDoc) return null;
const { section, pageinfo, progress, chapter } = config;
const marginGap = config.viewSettings ? `${config.viewSettings!.gap! * 100}%` : '';
const marginGap = config.viewSettings ? `${config.viewSettings!.gapPercent!}%` : '';
return (
<div key={bookKey} className='relative h-full w-full overflow-hidden'>
@@ -3,45 +3,7 @@ import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { BookDoc } from '@/libs/document';
import { BookConfig } from '@/types/book';
import { useReaderStore } from '@/store/readerStore';
const getCSS = (spacing: number, justify: boolean, hyphenate: boolean) => `
@namespace epub "http://www.idpf.org/2007/ops";
html {
color-scheme: light dark;
}
/* https://github.com/whatwg/html/issues/5426 */
@media (prefers-color-scheme: dark) {
a:link {
color: lightblue;
}
}
p, li, blockquote, dd {
line-height: ${spacing};
text-align: ${justify ? 'justify' : 'start'};
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
hyphens: ${hyphenate ? 'auto' : 'manual'};
-webkit-hyphenate-limit-before: 3;
-webkit-hyphenate-limit-after: 2;
-webkit-hyphenate-limit-lines: 2;
hanging-punctuation: allow-end last;
widows: 2;
}
/* prevent the above from overriding the align attribute */
[align="left"] { text-align: left; }
[align="right"] { text-align: right; }
[align="center"] { text-align: center; }
[align="justify"] { text-align: justify; }
pre {
white-space: pre-wrap !important;
}
aside[epub|type~="endnote"],
aside[epub|type~="footnote"],
aside[epub|type~="note"],
aside[epub|type~="rearnote"] {
display: none;
}
`;
import { getStyles } from '@/utils/style';
export interface FoliateView extends HTMLElement {
open: (book: BookDoc) => Promise<void>;
@@ -52,8 +14,9 @@ export interface FoliateView extends HTMLElement {
goLeft: () => void;
goRight: () => void;
renderer: {
setStyles: (css: string) => void;
setStyles?: (css: string) => void;
setAttribute: (name: string, value: string | number) => void;
removeAttribute: (name: string) => void;
next: () => Promise<void>;
prev: () => Promise<void>;
};
@@ -141,22 +104,27 @@ const FoliateViewer: React.FC<{
await view.open(bookDoc);
const viewSettings = bookConfig.viewSettings!;
if ('setStyles' in view.renderer) {
const lineHeight = viewSettings.lineHeight!;
const justify = viewSettings.justify!;
const hyphenate = viewSettings.hyphenate!;
view.renderer.setStyles(getCSS(lineHeight, justify, hyphenate));
// FIXME: zoom level is not working in paginated mode
if (viewSettings.scrolled) {
const zoomLevel = viewSettings.zoomLevel!;
view.renderer.setStyles(`body { zoom: ${zoomLevel}%; }`);
}
}
view.renderer.setStyles?.(getStyles(bookConfig));
const isScrolled = viewSettings.scrolled!;
const gap = viewSettings.gap!;
const marginPx = viewSettings.marginPx!;
const gapPercent = viewSettings.gapPercent!;
const animated = viewSettings.animated!;
const maxColumnCount = viewSettings.maxColumnCount!;
const maxInlineSize = viewSettings.maxInlineSize!;
const maxBlockSize = viewSettings.maxBlockSize!;
if (animated) {
view.renderer.setAttribute('animated', '');
} else {
view.renderer.removeAttribute('animated');
}
view.renderer.setAttribute('animated', animated ? 'animated' : '');
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
view.renderer.setAttribute('margin', '44px');
view.renderer.setAttribute('gap', `${gap * 100}%`);
view.renderer.setAttribute('margin', `${marginPx}px`);
view.renderer.setAttribute('gap', `${gapPercent}%`);
view.renderer.setAttribute('max-column-count', maxColumnCount);
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
const lastLocation = bookConfig.location;
if (lastLocation) {
view.init({ lastLocation });
@@ -37,6 +37,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}
};
const handleToggleDropdown = (isOpen: boolean) => {
setIsDropdownOpen(isOpen);
if (!isOpen) setHoveredBookKey('');
};
return (
<div
ref={headerRef}
@@ -68,7 +73,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
className='dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeVerticalBold size={16} />}
onToggle={setIsDropdownOpen}
onToggle={handleToggleDropdown}
>
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
</Dropdown>
@@ -5,16 +5,17 @@ import { BiMoon, BiSun } from 'react-icons/bi';
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
import { useReaderStore } from '@/store/readerStore';
import { getStyles } from '@/utils/style';
interface ViewMenuProps {
bookKey: string;
toggleDropdown?: () => void;
setIsDropdownOpen?: (open: boolean) => void;
onSetSettingsDialogOpen: (open: boolean) => void;
}
const ViewMenu: React.FC<ViewMenuProps> = ({
bookKey,
toggleDropdown,
setIsDropdownOpen,
onSetSettingsDialogOpen,
}) => {
const { books, setConfig, getFoliateView } = useReaderStore();
@@ -34,7 +35,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
const toggleInvertedColors = () => setInvertedColors(!isInvertedColors);
const openFontLayoutMenu = () => {
toggleDropdown?.();
setIsDropdownOpen?.(false);
onSetSettingsDialogOpen(true);
};
@@ -52,13 +53,12 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
useEffect(() => {
const view = getFoliateView(bookKey);
if (!view) return;
if ('setStyles' in view.renderer) {
// FIXME: zoom level is not working in paginated mode
if (!config.viewSettings?.scrolled) return;
view.renderer.setStyles(`body { zoom: ${zoomLevel}%; }`);
config.viewSettings!.zoomLevel = zoomLevel;
setConfig(bookKey, config);
// FIXME: zoom level is not working in paginated mode
if (config.viewSettings?.scrolled) {
view.renderer.setStyles?.(getStyles(config));
}
config.viewSettings!.zoomLevel = zoomLevel;
setConfig(bookKey, config);
}, [zoomLevel]);
return (
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
const ColorPanel: React.FC = () => {
const ColorPanel: React.FC<{ bookKey: string }> = () => {
const [selectedTheme, setSelectedTheme] = useState('Default');
const themes = [
'Default',
@@ -16,8 +16,7 @@ const ColorPanel: React.FC = () => {
return (
<div>
<h2 className='text-lg font-bold'>Color Settings</h2>
{/* Theme Selection */}
<h2 className='mb-2 font-medium'>Color Settings</h2>
<div className='mt-4 grid grid-cols-3 gap-2'>
{themes.map((theme) => (
<button
@@ -1,8 +1,11 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import NumberInput from './NumberInput';
import FontDropdown from './FontDropDown';
import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS } from '@/services/constants';
import { useReaderStore } from '@/store/readerStore';
import { getStyles } from '@/utils/style';
interface FontFaceProps {
className?: string;
@@ -14,26 +17,107 @@ interface FontFaceProps {
}
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 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>
);
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { books, settings, setSettings, setConfig, getFoliateView } = useReaderStore();
const { isFontLayoutSettingsGlobal } = useReaderStore();
const bookState = books[bookKey]!;
const config = bookState.config!;
const [defaultFontSize, setDefaultFontSize] = useState(config.viewSettings!.defaultFontSize!);
const [minFontSize, setMinFontSize] = useState(config.viewSettings!.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(config.viewSettings!.overrideFont!);
const [defaultFont, setDefaultFont] = useState(config.viewSettings!.defaultFont!);
const [serifFont, setSerifFont] = useState(config.viewSettings!.serifFont!);
const [sansSerifFont, setSansSerifFont] = useState(config.viewSettings!.sansSerifFont!);
const [monospaceFont, setMonospaceFont] = useState(config.viewSettings!.monospaceFont!);
useEffect(() => {
config.viewSettings!.defaultFont = defaultFont;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.defaultFont = defaultFont;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [defaultFont]);
useEffect(() => {
config.viewSettings!.defaultFontSize = defaultFontSize;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.defaultFontSize = defaultFontSize;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [defaultFontSize]);
useEffect(() => {
config.viewSettings!.minimumFontSize = minFontSize;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.minimumFontSize = minFontSize;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [minFontSize]);
useEffect(() => {
config.viewSettings!.serifFont = serifFont;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.serifFont = serifFont;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [serifFont]);
useEffect(() => {
config.viewSettings!.sansSerifFont = sansSerifFont;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.sansSerifFont = sansSerifFont;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [sansSerifFont]);
useEffect(() => {
config.viewSettings!.monospaceFont = monospaceFont;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.monospaceFont = monospaceFont;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [monospaceFont]);
useEffect(() => {
config.viewSettings!.overrideFont = overrideFont;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.overrideFont = overrideFont;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [overrideFont]);
const handleFontFamilyFont = (option: string) => {
switch (option) {
@@ -48,23 +132,6 @@ const FontPanel: React.FC = () => {
}
};
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'>
@@ -110,8 +177,8 @@ const FontPanel: React.FC = () => {
<input
type='checkbox'
className='toggle'
checked={overridePublisherFont}
onChange={() => setOverridePublisherFont(!overridePublisherFont)}
checked={overrideFont}
onChange={() => setOverrideFont(!overrideFont)}
/>
</div>
</div>
@@ -126,14 +193,14 @@ const FontPanel: React.FC = () => {
className='config-item-top'
family='serif'
label='Serif Font'
options={serifFonts}
options={SERIF_FONTS}
selected={serifFont}
onSelect={setSerifFont}
/>
<FontFace
family='sans-serif'
label='Sans-Serif Font'
options={sansSerifFonts}
options={SANS_SERIF_FONTS}
selected={sansSerifFont}
onSelect={setSansSerifFont}
/>
@@ -141,7 +208,7 @@ const FontPanel: React.FC = () => {
className='config-item-bottom'
family='monospace'
label='Monospace Font'
options={monospaceFonts}
options={MONOSPACE_FONTS}
selected={monospaceFont}
onSelect={setMonospaceFont}
/>
@@ -1,14 +1,94 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import NumberInput from './NumberInput';
import { useReaderStore } from '@/store/readerStore';
import { getStyles } from '@/utils/style';
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);
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { books, settings, setSettings, setConfig, getFoliateView } = useReaderStore();
const { isFontLayoutSettingsGlobal } = useReaderStore();
const bookState = books[bookKey]!;
const config = bookState.config!;
const [lineHeight, setLineHeight] = useState(config.viewSettings!.lineHeight!);
const [fullJustification, setFullJustification] = useState(
config.viewSettings!.fullJustification!,
);
const [hyphenation, setHyphenation] = useState(config.viewSettings!.hyphenation!);
const [marginPx, setMarginPx] = useState(config.viewSettings!.marginPx!);
const [gapPercent, setGapPercent] = useState(config.viewSettings!.gapPercent!);
const [maxColumnCount, setMaxColumnCount] = useState(config.viewSettings!.maxColumnCount!);
const [maxInlineSize, setMaxInlineSize] = useState(config.viewSettings!.maxInlineSize!);
const [maxBlockSize, setMaxBlockSize] = useState(config.viewSettings!.maxBlockSize!);
useEffect(() => {
config.viewSettings!.lineHeight = lineHeight;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.lineHeight = lineHeight;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [lineHeight]);
useEffect(() => {
config.viewSettings!.fullJustification = fullJustification;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.fullJustification = fullJustification;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [fullJustification]);
useEffect(() => {
config.viewSettings!.hyphenation = hyphenation;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.hyphenation = hyphenation;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setStyles?.(getStyles(config));
}, [hyphenation]);
useEffect(() => {
config.viewSettings!.marginPx = marginPx;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.marginPx = marginPx;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setAttribute('margin', `${marginPx}px`);
}, [marginPx]);
useEffect(() => {
config.viewSettings!.gapPercent = gapPercent;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.gapPercent = gapPercent;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setAttribute('gap', `${gapPercent}%`);
}, [gapPercent]);
useEffect(() => {
config.viewSettings!.maxColumnCount = maxColumnCount;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.maxColumnCount = maxColumnCount;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setAttribute('max-column-count', maxColumnCount);
}, [maxColumnCount]);
useEffect(() => {
config.viewSettings!.maxInlineSize = maxInlineSize;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.maxInlineSize = maxInlineSize;
setSettings(settings);
}
getFoliateView(bookKey)?.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
}, [maxInlineSize]);
return (
<div className='my-4 w-full space-y-6'>
@@ -21,8 +101,9 @@ const LayoutPanel: React.FC = () => {
label='Line Height'
value={lineHeight}
onChange={setLineHeight}
min={1}
max={120}
min={1.0}
max={3.0}
step={0.1}
/>
<div className='config-item config-item-bottom'>
<span className='text-gray-700'>Full Justification</span>
@@ -52,17 +133,24 @@ const LayoutPanel: React.FC = () => {
<div className='divide-y'>
<NumberInput
className='config-item-top'
label='Margins'
value={margins}
onChange={setMargins}
label='Margins (px)'
value={marginPx}
onChange={setMarginPx}
min={0}
max={0.3}
step={0.01}
max={88}
step={4}
/>
<NumberInput
label='Gaps (%)'
value={gapPercent}
onChange={setGapPercent}
min={0}
max={30}
/>
<NumberInput
label='Maximum Number of Columns'
value={maxNumberOfColumns}
onChange={setMaxNumberOfColumns}
value={maxColumnCount}
onChange={setMaxColumnCount}
min={1}
max={2}
/>
@@ -0,0 +1,48 @@
import React, { useEffect, useState } from 'react';
import { useReaderStore } from '@/store/readerStore';
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { books, settings, setSettings, setConfig, getFoliateView } = useReaderStore();
const { isFontLayoutSettingsGlobal } = useReaderStore();
const bookState = books[bookKey]!;
const config = bookState.config!;
const [animated, setAnimated] = useState(config.viewSettings!.animated!);
useEffect(() => {
config.viewSettings!.animated = animated;
setConfig(bookKey, config);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.animated = animated;
setSettings(settings);
}
if (animated) {
getFoliateView(bookKey)?.renderer.setAttribute('animated', '');
} else {
getFoliateView(bookKey)?.renderer.removeAttribute('animated');
}
}, [animated]);
return (
<div className='my-4 w-full space-y-6'>
<div className='cell-font-size w-full'>
<h2 className='mb-2 font-medium'>Animation</h2>
<div className='card bg-base-100 border shadow'>
<div className='divide-y'>
<div className='config-item config-item-top config-item-bottom'>
<span className='text-gray-700'>Paging Animation</span>
<input
type='checkbox'
className='toggle'
checked={animated}
onChange={() => setAnimated(!animated)}
/>
</div>
</div>
</div>
</div>
</div>
);
};
export default MiscPanel;
@@ -32,21 +32,24 @@ const NumberInput: React.FC<NumberInputProps> = ({
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 roundedValue = Math.round(newValue * 10) / 10;
setLocalValue(roundedValue);
onChange(Math.max(min, Math.min(max, roundedValue)));
}
};
const increment = () => {
const newValue = Math.min(max, localValue + numberStep);
setLocalValue(newValue);
onChange(newValue);
const roundedValue = Math.round(newValue * 10) / 10;
setLocalValue(roundedValue);
onChange(roundedValue);
};
const decrement = () => {
const newValue = Math.max(min, localValue - numberStep);
setLocalValue(newValue);
onChange(newValue);
const roundedValue = Math.round(newValue * 10) / 10;
setLocalValue(roundedValue);
onChange(roundedValue);
};
const handleOnBlur = () => {
@@ -67,7 +70,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
onKeyDown={handleInput}
onKeyUp={handleInput}
onBlur={handleOnBlur}
className='input input-ghost w-20 max-w-xs rounded px-3 py-1 text-right'
className='input input-ghost settings-content w-20 max-w-xs rounded px-3 py-1 text-right'
onFocus={(e) => e.target.select()}
/>
<button
@@ -5,6 +5,7 @@ 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 { IoAccessibilityOutline } from 'react-icons/io5';
import FontPanel from './FontPanel';
import LayoutPanel from './LayoutPanel';
@@ -12,41 +13,49 @@ import ColorPanel from './ColorPanel';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import DialogMenu from './DialogMenu';
import MiscPanel from './MiscPanel';
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({}) => {
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
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='modal-box settings-content flex h-[60%] w-1/2 min-w-[480px] 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' : ''}`}
className={`btn btn-ghost 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 h-8 min-h-8 px-6 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
className={`btn btn-ghost 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 h-8 min-h-8 px-6 ${activePanel === 'Color' ? 'btn-active' : ''}`}
className={`btn btn-ghost 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 h-8 min-h-8 px-4 ${activePanel === 'Misc' ? 'btn-active' : ''}`}
onClick={() => setActivePanel('Misc')}
>
<IoAccessibilityOutline size={20} className='mr-0' />
Misc
</button>
</div>
<div className='flex h-full items-center justify-end'>
<Dropdown
className='dropdown-bottom dropdown-end absolute right-12'
className='dropdown-bottom dropdown-end absolute right-[7%]'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeVerticalBold size={16} />}
>
@@ -61,10 +70,11 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({}) =
</div>
</div>
<div className='mt-2 flex-grow overflow-y-auto px-16'>
{activePanel === 'Font' && <FontPanel />}
{activePanel === 'Layout' && <LayoutPanel />}
{activePanel === 'Color' && <ColorPanel />}
<div className='mt-2 flex-grow overflow-y-auto px-[10%]'>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
</div>
</div>
</dialog>
@@ -2,20 +2,20 @@ import React from 'react';
interface BookMenuProps {
openSplitView: () => void;
toggleDropdown?: () => void;
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const BookMenu: React.FC<BookMenuProps> = ({ openSplitView, toggleDropdown }) => {
const BookMenu: React.FC<BookMenuProps> = ({ openSplitView, setIsDropdownOpen }) => {
const handleOpenSplitView = () => {
openSplitView();
toggleDropdown?.();
setIsDropdownOpen?.(false);
};
const handleReloadPage = () => {
window.location.reload();
toggleDropdown?.();
setIsDropdownOpen?.(false);
};
const showAboutReadest = () => {
toggleDropdown?.();
setIsDropdownOpen?.(false);
};
return (
+8 -8
View File
@@ -5,7 +5,7 @@ interface DropdownProps {
className?: string;
buttonClassName?: string;
toggleButton: React.ReactNode;
children: ReactElement<{ toggleDropdown: () => void }>;
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void }>;
onToggle?: (isOpen: boolean) => void;
}
@@ -25,25 +25,25 @@ const Dropdown: React.FC<DropdownProps> = ({
onToggle?.(newIsOpen);
};
const closeDropdown = () => {
setIsOpen(false);
onToggle?.(false);
const setIsDropdownOpen = (isOpen: boolean) => {
setIsOpen(isOpen);
onToggle?.(isOpen);
};
const handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {
if (!dropdownRef.current?.contains(event.relatedTarget as Node)) {
closeDropdown();
setIsDropdownOpen(false);
}
};
const handleClickOutside = (event: MouseEvent | Event) => {
if (event instanceof MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
closeDropdown();
setIsDropdownOpen(false);
}
} else if (event instanceof MessageEvent) {
if (event.data && event.data.type === 'iframe-mousedown') {
closeDropdown();
setIsDropdownOpen(false);
}
}
};
@@ -58,7 +58,7 @@ const Dropdown: React.FC<DropdownProps> = ({
}, []);
const childrenWithToggle = isValidElement(children)
? React.cloneElement(children, { toggleDropdown })
? React.cloneElement(children, { setIsDropdownOpen })
: children;
return (
+31 -9
View File
@@ -11,17 +11,18 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
};
export const DEFAULT_BOOK_FONT: BookFont = {
serif: 'Serif 12',
sansSerif: 'Sans 12',
monospace: 'Monospace 12',
defaultFont: 'serif',
defaultSize: 16,
minimumSize: 8,
serifFont: 'Literata',
sansSerifFont: 'Roboto',
monospaceFont: 'Consolas',
defaultFont: 'Serif',
defaultFontSize: 16,
minimumFontSize: 8,
fontWeight: 400,
};
export const DEFAULT_BOOK_LAYOUT: BookLayout = {
gap: 0.05,
marginPx: 44,
gapPercent: 5,
scrolled: false,
maxColumnCount: 2,
maxInlineSize: 720,
@@ -32,8 +33,8 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
export const DEFAULT_BOOK_STYLE: BookStyle = {
zoomLevel: 100,
lineHeight: 1.6,
justify: true,
hyphenate: true,
fullJustification: true,
hyphenation: true,
invert: false,
theme: 'light',
overrideFont: false,
@@ -41,3 +42,24 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
};
export const SYSTEM_SETTINGS_VERSION = 1;
export const SERIF_FONTS = [
'Literata',
'Vollkorn',
'Aleo',
'Crimson Text',
'Merriweather',
'Georgia',
'Times New Roman',
];
export const SANS_SERIF_FONTS = [
'Roboto',
'Open Sans',
'Noto Sans',
'Poppins',
'Helvetica',
'Arial',
];
export const MONOSPACE_FONTS = ['Fira Code', 'Lucida Console', 'Consolas', 'Courier New'];
+9 -8
View File
@@ -30,7 +30,8 @@ export interface BookNote {
}
export interface BookLayout {
gap: number;
marginPx: number;
gapPercent: number;
scrolled: boolean;
maxColumnCount: number;
maxInlineSize: number;
@@ -41,8 +42,8 @@ export interface BookLayout {
export interface BookStyle {
zoomLevel: number;
lineHeight: number;
justify: boolean;
hyphenate: boolean;
fullJustification: boolean;
hyphenation: boolean;
invert: boolean;
theme: string;
overrideFont: boolean;
@@ -50,12 +51,12 @@ export interface BookStyle {
}
export interface BookFont {
serif: string;
sansSerif: string;
monospace: string;
serifFont: string;
sansSerifFont: string;
monospaceFont: string;
defaultFont: string;
defaultSize: number;
minimumSize: number;
defaultFontSize: number;
minimumFontSize: number;
fontWeight: number;
}
+96
View File
@@ -0,0 +1,96 @@
import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS } from '@/services/constants';
import { BookConfig } from '@/types/book';
const getFontStyles = (
serif: string,
sansSerif: string,
monospace: string,
defaultFont: string,
fontSize: number,
overrideFont: boolean,
) => {
const serifFonts = [serif, ...SERIF_FONTS.filter((font) => font !== serif)];
const sansSerifFonts = [sansSerif, ...SANS_SERIF_FONTS.filter((font) => font !== sansSerif)];
const monospaceFonts = [monospace, ...MONOSPACE_FONTS.filter((font) => font !== monospace)];
const fontStyles = `
html {
--serif: ${serifFonts.map((font) => `"${font}"`).join(', ')}, serif;
--sans-serif: ${sansSerifFonts.map((font) => `"${font}"`).join(', ')}, sans-serif;
--monospace: ${monospaceFonts.map((font) => `"${font}"`).join(', ')}, monospace;
}
body * {
font-size: ${fontSize}px ${overrideFont ? '!important' : ''};
font-family: revert ${overrideFont ? '!important' : ''};
font-family: var(${defaultFont.toLowerCase() === 'serif' ? '--serif' : '--sans-serif'}) ${overrideFont ? '!important' : ''};
}
`;
return fontStyles;
};
const getLayoutStyles = (
spacing: number,
justify: boolean,
hyphenate: boolean,
zoomLevel: number,
) => `
@namespace epub "http://www.idpf.org/2007/ops";
html {
color-scheme: light dark;
}
/* https://github.com/whatwg/html/issues/5426 */
@media (prefers-color-scheme: dark) {
a:link {
color: lightblue;
}
}
body {
zoom: ${zoomLevel}%;
}
p, li, blockquote, dd {
line-height: ${spacing};
text-align: ${justify ? 'justify' : 'start'};
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
hyphens: ${hyphenate ? 'auto' : 'manual'};
-webkit-hyphenate-limit-before: 3;
-webkit-hyphenate-limit-after: 2;
-webkit-hyphenate-limit-lines: 2;
hanging-punctuation: allow-end last;
widows: 2;
}
/* prevent the above from overriding the align attribute */
[align="left"] { text-align: left; }
[align="right"] { text-align: right; }
[align="center"] { text-align: center; }
[align="justify"] { text-align: justify; }
pre {
white-space: pre-wrap !important;
}
aside[epub|type~="endnote"],
aside[epub|type~="footnote"],
aside[epub|type~="note"],
aside[epub|type~="rearnote"] {
display: none;
}
`;
export const getStyles = (config: BookConfig) => {
const viewSettings = config.viewSettings!;
const layoutStyles = getLayoutStyles(
viewSettings.lineHeight!,
viewSettings.fullJustification!,
viewSettings.hyphenation!,
// FIXME: zoom level is not working in paginated mode
viewSettings.scrolled ? viewSettings.zoomLevel! : 100,
);
const fontStyles = getFontStyles(
viewSettings.serifFont!,
viewSettings.sansSerifFont!,
viewSettings.monospaceFont!,
viewSettings.defaultFont!,
viewSettings.defaultFontSize!,
viewSettings.overrideFont!,
);
const userStylesheet = viewSettings.userStylesheet!;
return `${layoutStyles}\n${fontStyles}\n${userStylesheet}`;
};