Apply styles in the settings dialog
This commit is contained in:
@@ -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,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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
Reference in New Issue
Block a user