forked from akai/readest
feat: add options to adjust paragraph margin, word spacing, letter spacing, text indent and font weight (#310)
This commit is contained in:
@@ -53,7 +53,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-2',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center justify-between space-x-12'>
|
||||
<div className='flex items-center justify-between space-x-6 sm:space-x-12'>
|
||||
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
|
||||
<span className='absolute left-8 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
@@ -68,7 +68,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
'border-none focus:outline-none focus:ring-0',
|
||||
)}
|
||||
/>
|
||||
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
|
||||
<div className='absolute right-4 flex items-center space-x-2 text-gray-500 sm:space-x-4'>
|
||||
<span className='mx-2 h-6 w-[1px] bg-gray-400'></span>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
|
||||
@@ -98,7 +98,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-full items-center gap-x-4'>
|
||||
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize
|
||||
interface DropdownProps {
|
||||
family?: string;
|
||||
selected: string;
|
||||
options: { option: string; label: string }[];
|
||||
moreOptions?: string[];
|
||||
options: { option: string; label?: string }[];
|
||||
moreOptions?: { option: string; label?: string }[];
|
||||
onSelect: (option: string) => void;
|
||||
onGetFontFamily: (option: string, family: string) => string;
|
||||
}
|
||||
@@ -25,7 +25,8 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
const _ = useTranslation();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const defaultIconSize = useDefaultIconSize();
|
||||
const selectedOption = options.find((option) => option.option === selected) || options[0]!;
|
||||
const allOptions = [...options, ...(moreOptions ?? [])];
|
||||
const selectedOption = allOptions.find((option) => option.option === selected) ?? allOptions[0]!;
|
||||
return (
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
@@ -68,14 +69,14 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
'!mr-5 mb-[-46px] inline max-h-80 w-[200px] overflow-y-scroll',
|
||||
)}
|
||||
>
|
||||
{moreOptions.map((option) => (
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
{moreOptions.map((option, index) => (
|
||||
<li key={`${index}-${option.option}`} onClick={() => onSelect(option.option)}>
|
||||
<div className='flex items-center px-2'>
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
{selected === option && <MdCheck className='text-base-content' />}
|
||||
{selected === option.option && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
|
||||
{option}
|
||||
<span style={{ fontFamily: onGetFontFamily(option.option, family ?? '') }}>
|
||||
{option.label || option.option}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -50,7 +50,7 @@ const FontFace = ({
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options.map((option) => ({ option, label: option }))}
|
||||
moreOptions={moreOptions}
|
||||
moreOptions={moreOptions?.map((option) => ({ option, label: option })) ?? []}
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
onGetFontFamily={handleFontFaceFont}
|
||||
@@ -106,6 +106,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [serifFont, setSerifFont] = useState(viewSettings.serifFont!);
|
||||
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
|
||||
const [monospaceFont, setMonospaceFont] = useState(viewSettings.monospaceFont!);
|
||||
const [fontWeight, setFontWeight] = useState(viewSettings.fontWeight!);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTauriAppPlatform() && FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
|
||||
@@ -149,6 +150,17 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minFontSize]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.fontWeight = fontWeight;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.fontWeight = fontWeight;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fontWeight]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.serifFont = serifFont;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
@@ -232,6 +244,23 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Font Weight')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Font Weight')}
|
||||
value={fontWeight}
|
||||
onChange={setFontWeight}
|
||||
min={100}
|
||||
max={900}
|
||||
step={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Font Family')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
|
||||
@@ -22,7 +22,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
|
||||
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
|
||||
const [wordSpacing, setWordSpacing] = useState(viewSettings.wordSpacing!);
|
||||
const [letterSpacing, setLetterSpacing] = useState(viewSettings.letterSpacing!);
|
||||
const [textIndent, setTextIndent] = useState(viewSettings.textIndent!);
|
||||
const [fullJustification, setFullJustification] = useState(viewSettings.fullJustification!);
|
||||
const [hyphenation, setHyphenation] = useState(viewSettings.hyphenation!);
|
||||
const [marginPx, setMarginPx] = useState(viewSettings.marginPx!);
|
||||
@@ -32,6 +36,17 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!);
|
||||
const [writingMode, setWritingMode] = useState(viewSettings.writingMode!);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.paragraphMargin = paragraphMargin;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.paragraphMargin = paragraphMargin;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paragraphMargin]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.lineHeight = lineHeight;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
@@ -43,6 +58,39 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lineHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.wordSpacing = wordSpacing;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.wordSpacing = wordSpacing;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wordSpacing]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.letterSpacing = letterSpacing;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.letterSpacing = letterSpacing;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [letterSpacing]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.textIndent = textIndent;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.textIndent = textIndent;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [textIndent]);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.fullJustification = fullJustification;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
@@ -188,13 +236,49 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Line Height')}
|
||||
label={_('Paragraph Margin')}
|
||||
value={paragraphMargin}
|
||||
onChange={setParagraphMargin}
|
||||
min={0}
|
||||
max={4}
|
||||
step={0.5}
|
||||
/>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Line Spacing')}
|
||||
value={lineHeight}
|
||||
onChange={setLineHeight}
|
||||
min={1.0}
|
||||
max={3.0}
|
||||
step={0.1}
|
||||
/>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Word Spacing')}
|
||||
value={wordSpacing}
|
||||
onChange={setWordSpacing}
|
||||
min={-4}
|
||||
max={8}
|
||||
step={0.5}
|
||||
/>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Letter Spacing')}
|
||||
value={letterSpacing}
|
||||
onChange={setLetterSpacing}
|
||||
min={-2}
|
||||
max={4}
|
||||
step={0.1}
|
||||
/>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Text Indent')}
|
||||
value={textIndent}
|
||||
onChange={setTextIndent}
|
||||
min={-2}
|
||||
max={4}
|
||||
step={1}
|
||||
/>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className=''>{_('Full Justification')}</span>
|
||||
<input
|
||||
|
||||
@@ -67,7 +67,11 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
|
||||
export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
zoomLevel: 100,
|
||||
paragraphMargin: 1,
|
||||
lineHeight: 1.6,
|
||||
wordSpacing: 0,
|
||||
letterSpacing: 0,
|
||||
textIndent: 0,
|
||||
fullJustification: true,
|
||||
hyphenation: true,
|
||||
invert: false,
|
||||
|
||||
@@ -63,7 +63,11 @@ export interface BookLayout {
|
||||
|
||||
export interface BookStyle {
|
||||
zoomLevel: number;
|
||||
paragraphMargin: number;
|
||||
lineHeight: number;
|
||||
wordSpacing: number;
|
||||
letterSpacing: number;
|
||||
textIndent: number;
|
||||
fullJustification: boolean;
|
||||
hyphenation: boolean;
|
||||
invert: boolean;
|
||||
|
||||
@@ -16,6 +16,7 @@ const getFontStyles = (
|
||||
monospace: string,
|
||||
defaultFont: string,
|
||||
fontSize: number,
|
||||
fontWeight: number,
|
||||
overrideFont: boolean,
|
||||
) => {
|
||||
const serifFonts = [serif, ...SERIF_FONTS.filter((font) => font !== serif), ...FALLBACK_FONTS];
|
||||
@@ -34,6 +35,7 @@ const getFontStyles = (
|
||||
html, body {
|
||||
font-family: var(${defaultFont.toLowerCase() === 'serif' ? '--serif' : '--sans-serif'}) ${overrideFont ? '!important' : ''};
|
||||
font-size: ${fontSize}px !important;
|
||||
font-weight: ${fontWeight};
|
||||
}
|
||||
body * {
|
||||
font-family: revert ${overrideFont ? '!important' : ''};
|
||||
@@ -92,7 +94,11 @@ const getAdditionalFontFaces = () => `
|
||||
`;
|
||||
|
||||
const getLayoutStyles = (
|
||||
spacing: number,
|
||||
paragraphMargin: number,
|
||||
lineSpacing: number,
|
||||
wordSpacing: number,
|
||||
letterSpacing: number,
|
||||
textIndent: number,
|
||||
justify: boolean,
|
||||
hyphenate: boolean,
|
||||
zoomLevel: number,
|
||||
@@ -144,6 +150,7 @@ const getLayoutStyles = (
|
||||
html, body {
|
||||
color: ${fg};
|
||||
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode};`}
|
||||
text-indent: ${textIndent}em;
|
||||
text-align: var(--default-text-align);
|
||||
background-color: var(--theme-bg-color, transparent);
|
||||
background: var(--background-set, none);
|
||||
@@ -160,7 +167,10 @@ const getLayoutStyles = (
|
||||
background-color: transparent !important;
|
||||
}
|
||||
p, li, blockquote, dd {
|
||||
line-height: ${spacing} !important;
|
||||
margin: ${paragraphMargin}em 0 !important;
|
||||
line-height: ${lineSpacing} !important;
|
||||
word-spacing: ${wordSpacing}px !important;
|
||||
letter-spacing: ${letterSpacing}px !important;
|
||||
text-align: inherit;
|
||||
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
@@ -201,7 +211,11 @@ export interface ThemeCode {
|
||||
|
||||
export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
|
||||
const layoutStyles = getLayoutStyles(
|
||||
viewSettings.paragraphMargin!,
|
||||
viewSettings.lineHeight!,
|
||||
viewSettings.wordSpacing!,
|
||||
viewSettings.letterSpacing!,
|
||||
viewSettings.textIndent!,
|
||||
viewSettings.fullJustification!,
|
||||
viewSettings.hyphenation!,
|
||||
viewSettings.zoomLevel! / 100.0,
|
||||
@@ -219,6 +233,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
|
||||
viewSettings.monospaceFont!,
|
||||
viewSettings.defaultFont!,
|
||||
viewSettings.defaultFontSize! * fontScale,
|
||||
viewSettings.fontWeight!,
|
||||
viewSettings.overrideFont!,
|
||||
);
|
||||
const userStylesheet = viewSettings.userStylesheet!;
|
||||
|
||||
Reference in New Issue
Block a user