feat(settings): add an option to avoid overriding paragraph layout, closes #3824 (#3858)

This commit is contained in:
Huang Xin
2026-04-13 15:42:53 +08:00
committed by GitHub
parent ec32614539
commit e9d71b2936
38 changed files with 311 additions and 156 deletions
+25
View File
@@ -167,6 +167,30 @@ const handleReset = () => {
The hook resolves the default by reading from `getDefaultViewSettings(ctx)` and calls each provided setter with that value, which then fires your `useEffect` and persists the change.
### Step 5 — Register in the Command Palette
If your setting has a visible row in a panel, register it in the matching `*PanelItems` array in `src/services/commandRegistry.ts`. This wires it into the command-palette fuzzy search so users can jump straight to it.
```ts
// src/services/commandRegistry.ts
const layoutPanelItems = [
// ...existing entries
{
id: 'settings.layout.myNewToggle',
labelKey: _('My New Toggle'),
keywords: ['search', 'terms', 'for', 'discoverability'],
section: 'Paragraph',
},
];
```
- `id` must match the `data-setting-id` attribute on the panel row. The palette uses it to scroll/highlight the target control.
- `labelKey` uses `stubTranslation` (imported as `_`) so the extractor picks it up — the same string that appears in the panel.
- `keywords` broadens fuzzy-search hits beyond the label; include synonyms, related jargon, and the panel section name.
- `section` groups the entry in the palette results (matches the panel's sub-header: `Layout`, `Paragraph`, `Page`, `Header & Footer`, etc.).
Skip this step only for settings that don't surface as a user-visible row (hidden toggles, flags used by other settings).
### Don'ts
- **Don't make the field optional** just to skip providing a default. Add a default in Step 2 instead.
@@ -182,3 +206,4 @@ The hook resolves the default by reading from `getDefaultViewSettings(ctx)` and
- [ ] Read via `getViewSettings(bookKey) || settings.globalViewSettings`
- [ ] Write via `saveViewSettings(envConfig, bookKey, 'key', value)`
- [ ] Reset setter wired into `useResetViewSettings` if the panel has a reset menu
- [ ] Command-palette entry added to the matching `*PanelItems` array in `src/services/commandRegistry.ts`, with an `id` that matches the panel row's `data-setting-id`
@@ -1242,5 +1242,6 @@
"Unavailable": "غير متاح",
"Remove": "إزالة",
"Edit OPDS Catalog": "تعديل كتالوج OPDS",
"Save Changes": "حفظ التغييرات"
"Save Changes": "حفظ التغييرات",
"Use Book Layout": "استخدام تخطيط الكتاب"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "অনুপলব্ধ",
"Remove": "সরান",
"Edit OPDS Catalog": "OPDS ক্যাটালগ সম্পাদনা",
"Save Changes": "পরিবর্তন সংরক্ষণ"
"Save Changes": "পরিবর্তন সংরক্ষণ",
"Use Book Layout": "বইয়ের লেআউট ব্যবহার করুন"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "རག་མི་ཐུབ",
"Remove": "བསུབ།",
"Edit OPDS Catalog": "OPDS དཀར་ཆག་རྩོམ་སྒྲིག",
"Save Changes": "བསྒྱུར་བཅོས་ཉར་ཚགས།"
"Save Changes": "བསྒྱུར་བཅོས་ཉར་ཚགས།",
"Use Book Layout": "དཔེ་ཆའི་བཀོད་སྒྲིག་སྤྱོད་པ"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "Nicht verfügbar",
"Remove": "Entfernen",
"Edit OPDS Catalog": "OPDS-Katalog bearbeiten",
"Save Changes": "Änderungen speichern"
"Save Changes": "Änderungen speichern",
"Use Book Layout": "Buchlayout verwenden"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "Μη διαθέσιμο",
"Remove": "Αφαίρεση",
"Edit OPDS Catalog": "Επεξεργασία καταλόγου OPDS",
"Save Changes": "Αποθήκευση αλλαγών"
"Save Changes": "Αποθήκευση αλλαγών",
"Use Book Layout": "Χρήση διάταξης βιβλίου"
}
@@ -1203,5 +1203,6 @@
"Unavailable": "No disponible",
"Remove": "Eliminar",
"Edit OPDS Catalog": "Editar catálogo OPDS",
"Save Changes": "Guardar cambios"
"Save Changes": "Guardar cambios",
"Use Book Layout": "Usar el diseño del libro"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "ناموجود",
"Remove": "حذف",
"Edit OPDS Catalog": "ویرایش کاتالوگ OPDS",
"Save Changes": "ذخیره تغییرات"
"Save Changes": "ذخیره تغییرات",
"Use Book Layout": "استفاده از چیدمان کتاب"
}
@@ -1203,5 +1203,6 @@
"Unavailable": "Indisponible",
"Remove": "Supprimer",
"Edit OPDS Catalog": "Modifier le catalogue OPDS",
"Save Changes": "Enregistrer les modifications"
"Save Changes": "Enregistrer les modifications",
"Use Book Layout": "Utiliser la mise en page du livre"
}
@@ -1203,5 +1203,6 @@
"Unavailable": "לא זמין",
"Remove": "הסר",
"Edit OPDS Catalog": "עריכת קטלוג OPDS",
"Save Changes": "שמור שינויים"
"Save Changes": "שמור שינויים",
"Use Book Layout": "השתמש בפריסת הספר"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "अनुपलब्ध",
"Remove": "हटाएँ",
"Edit OPDS Catalog": "OPDS कैटलॉग संपादित करें",
"Save Changes": "परिवर्तन सहेजें"
"Save Changes": "परिवर्तन सहेजें",
"Use Book Layout": "पुस्तक का लेआउट उपयोग करें"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "Nem elérhető",
"Remove": "Eltávolítás",
"Edit OPDS Catalog": "OPDS katalógus szerkesztése",
"Save Changes": "Módosítások mentése"
"Save Changes": "Módosítások mentése",
"Use Book Layout": "Könyv elrendezésének használata"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "Tidak Tersedia",
"Remove": "Hapus",
"Edit OPDS Catalog": "Edit Katalog OPDS",
"Save Changes": "Simpan Perubahan"
"Save Changes": "Simpan Perubahan",
"Use Book Layout": "Gunakan tata letak buku"
}
@@ -1203,5 +1203,6 @@
"Unavailable": "Non disponibile",
"Remove": "Rimuovi",
"Edit OPDS Catalog": "Modifica catalogo OPDS",
"Save Changes": "Salva modifiche"
"Save Changes": "Salva modifiche",
"Use Book Layout": "Usa layout del libro"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "利用不可",
"Remove": "削除",
"Edit OPDS Catalog": "OPDSカタログを編集",
"Save Changes": "変更を保存"
"Save Changes": "変更を保存",
"Use Book Layout": "書籍のレイアウトを使用"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "사용 불가",
"Remove": "삭제",
"Edit OPDS Catalog": "OPDS 카탈로그 편집",
"Save Changes": "변경 사항 저장"
"Save Changes": "변경 사항 저장",
"Use Book Layout": "책 레이아웃 사용"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "Tidak Tersedia",
"Remove": "Alih keluar",
"Edit OPDS Catalog": "Edit Katalog OPDS",
"Save Changes": "Simpan Perubahan"
"Save Changes": "Simpan Perubahan",
"Use Book Layout": "Guna susun atur buku"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "Niet beschikbaar",
"Remove": "Verwijderen",
"Edit OPDS Catalog": "OPDS-catalogus bewerken",
"Save Changes": "Wijzigingen opslaan"
"Save Changes": "Wijzigingen opslaan",
"Use Book Layout": "Boekindeling gebruiken"
}
@@ -1216,5 +1216,6 @@
"Unavailable": "Niedostępne",
"Remove": "Usuń",
"Edit OPDS Catalog": "Edytuj katalog OPDS",
"Save Changes": "Zapisz zmiany"
"Save Changes": "Zapisz zmiany",
"Use Book Layout": "Użyj układu książki"
}
@@ -1203,5 +1203,6 @@
"Unavailable": "Indisponível",
"Remove": "Remover",
"Edit OPDS Catalog": "Editar catálogo OPDS",
"Save Changes": "Salvar alterações"
"Save Changes": "Salvar alterações",
"Use Book Layout": "Usar layout do livro"
}
@@ -1203,5 +1203,6 @@
"Unavailable": "Indisponibil",
"Remove": "Elimină",
"Edit OPDS Catalog": "Editare catalog OPDS",
"Save Changes": "Salvare modificări"
"Save Changes": "Salvare modificări",
"Use Book Layout": "Folosește aspectul cărții"
}
@@ -1216,5 +1216,6 @@
"Unavailable": "Недоступно",
"Remove": "Удалить",
"Edit OPDS Catalog": "Редактировать каталог OPDS",
"Save Changes": "Сохранить изменения"
"Save Changes": "Сохранить изменения",
"Use Book Layout": "Использовать макет книги"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "ලබාගත නොහැක",
"Remove": "ඉවත් කරන්න",
"Edit OPDS Catalog": "OPDS නාමාවලිය සංස්කරණය",
"Save Changes": "වෙනස්කම් සුරකින්න"
"Save Changes": "වෙනස්කම් සුරකින්න",
"Use Book Layout": "පොතේ සැකැස්ම භාවිත කරන්න"
}
@@ -1216,5 +1216,6 @@
"Unavailable": "Nedosegljivo",
"Remove": "Odstrani",
"Edit OPDS Catalog": "Uredi katalog OPDS",
"Save Changes": "Shrani spremembe"
"Save Changes": "Shrani spremembe",
"Use Book Layout": "Uporabi postavitev knjige"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "Inte tillgängligt",
"Remove": "Ta bort",
"Edit OPDS Catalog": "Redigera OPDS-katalog",
"Save Changes": "Spara ändringar"
"Save Changes": "Spara ändringar",
"Use Book Layout": "Använd bokens layout"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "கிடைக்கவில்லை",
"Remove": "நீக்கு",
"Edit OPDS Catalog": "OPDS பட்டியலைத் திருத்து",
"Save Changes": "மாற்றங்களைச் சேமி"
"Save Changes": "மாற்றங்களைச் சேமி",
"Use Book Layout": "புத்தகத்தின் அமைப்பைப் பயன்படுத்து"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "ไม่พร้อมใช้งาน",
"Remove": "ลบ",
"Edit OPDS Catalog": "แก้ไขแคตตาล็อก OPDS",
"Save Changes": "บันทึกการเปลี่ยนแปลง"
"Save Changes": "บันทึกการเปลี่ยนแปลง",
"Use Book Layout": "ใช้เลย์เอาต์ของหนังสือ"
}
@@ -1190,5 +1190,6 @@
"Unavailable": "Kullanılamaz",
"Remove": "Kaldır",
"Edit OPDS Catalog": "OPDS Kataloğunu Düzenle",
"Save Changes": "Değişiklikleri Kaydet"
"Save Changes": "Değişiklikleri Kaydet",
"Use Book Layout": "Kitap düzenini kullan"
}
@@ -1216,5 +1216,6 @@
"Unavailable": "Недоступно",
"Remove": "Видалити",
"Edit OPDS Catalog": "Редагувати каталог OPDS",
"Save Changes": "Зберегти зміни"
"Save Changes": "Зберегти зміни",
"Use Book Layout": "Використовувати макет книги"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "Không khả dụng",
"Remove": "Xóa",
"Edit OPDS Catalog": "Chỉnh sửa danh mục OPDS",
"Save Changes": "Lưu thay đổi"
"Save Changes": "Lưu thay đổi",
"Use Book Layout": "Sử dụng bố cục sách"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "不可用",
"Remove": "移除",
"Edit OPDS Catalog": "编辑 OPDS 目录",
"Save Changes": "保存更改"
"Save Changes": "保存更改",
"Use Book Layout": "使用书籍排版"
}
@@ -1177,5 +1177,6 @@
"Unavailable": "無法使用",
"Remove": "移除",
"Edit OPDS Catalog": "編輯 OPDS 目錄",
"Save Changes": "儲存變更"
"Save Changes": "儲存變更",
"Use Book Layout": "使用書籍排版"
}
@@ -368,6 +368,58 @@ describe('getLayoutStyles branches (via getStyles)', () => {
// getStyles passes 1.0 as zoomLevel to getLayoutStyles
expect(css).toContain('zoom: 1');
});
it('omits only paragraph-related layout rules when useBookLayout is true', () => {
const vs = makeViewSettings({
useBookLayout: true,
lineHeight: 1.7,
wordSpacing: 3,
letterSpacing: 2,
textIndent: 2,
paragraphMargin: 1.25,
fullJustification: true,
hyphenation: true,
marginTopPx: 50,
});
const css = getStyles(vs, theme);
// paragraph-specific tokens driven by the Paragraph section must be absent
expect(css).not.toContain('--default-text-align:');
expect(css).not.toContain('line-height: 1.7');
expect(css).not.toContain('word-spacing: 3px');
expect(css).not.toContain('letter-spacing: 2px');
expect(css).not.toContain('text-indent: 2em');
expect(css).not.toContain('hyphens: auto');
expect(css).not.toContain('-webkit-hyphens: auto');
// non-paragraph layout rules must still be emitted
expect(css).toContain('@namespace epub');
expect(css).toContain('--margin-top: 50px');
expect(css).toContain('--margin-right:');
expect(css).toContain('--margin-bottom:');
expect(css).toContain('--margin-left:');
// font/color/translation sections must still be present
expect(css).toContain('--serif:');
expect(css).toContain('--theme-bg-color');
expect(css).toContain('.translation-source');
});
it('includes paragraph layout rules when useBookLayout is false', () => {
const vs = makeViewSettings({
useBookLayout: false,
lineHeight: 1.7,
wordSpacing: 3,
letterSpacing: 2,
textIndent: 2,
fullJustification: true,
hyphenation: true,
});
const css = getStyles(vs, theme);
expect(css).toContain('--default-text-align: justify');
expect(css).toContain('line-height: 1.7');
expect(css).toContain('word-spacing: 3px');
expect(css).toContain('letter-spacing: 2px');
expect(css).toContain('text-indent: 2em');
expect(css).toContain('hyphens: auto');
});
});
// ---------------------------------------------------------------------------
@@ -61,6 +61,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize);
const [writingMode, setWritingMode] = useState(viewSettings.writingMode);
const [overrideLayout, setOverrideLayout] = useState(viewSettings.overrideLayout);
const [useBookLayout, setUseBookLayout] = useState(viewSettings.useBookLayout);
const [doubleBorder, setDoubleBorder] = useState(viewSettings.doubleBorder);
const [borderColor, setBorderColor] = useState(viewSettings.borderColor);
const [showHeader, setShowHeader] = useState(viewSettings.showHeader);
@@ -106,6 +107,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
maxInlineSize: setMaxInlineSize,
maxBlockSize: setMaxBlockSize,
overrideLayout: setOverrideLayout,
useBookLayout: setUseBookLayout,
doubleBorder: setDoubleBorder,
borderColor: setBorderColor,
showHeader: setShowHeader,
@@ -315,6 +317,12 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrideLayout]);
useEffect(() => {
if (useBookLayout === viewSettings.useBookLayout) return;
saveViewSettings(envConfig, bookKey, 'useBookLayout', useBookLayout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useBookLayout]);
useEffect(() => {
if (doubleBorder === viewSettings.doubleBorder) return;
saveViewSettings(envConfig, bookKey, 'doubleBorder', doubleBorder, false, false);
@@ -527,10 +535,20 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
<h2 className='mb-2 font-medium'>{_('Paragraph')}</h2>
<div className='card bg-base-100 border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item' data-setting-id='settings.layout.useBookLayout'>
<span className=''>{_('Use Book Layout')}</span>
<input
type='checkbox'
className='toggle'
checked={useBookLayout}
onChange={() => setUseBookLayout(!useBookLayout)}
/>
</div>
<NumberInput
label={_('Paragraph Margin')}
value={paragraphMargin}
onChange={setParagraphMargin}
disabled={useBookLayout}
min={0}
max={4}
step={0.1}
@@ -540,6 +558,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
label={_('Line Spacing')}
value={lineHeight}
onChange={setLineHeight}
disabled={useBookLayout}
min={1.0}
max={3.0}
step={0.1}
@@ -550,6 +569,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
label={_('Word Spacing')}
value={wordSpacing}
onChange={setWordSpacing}
disabled={useBookLayout}
min={-4}
max={8}
step={0.5}
@@ -560,6 +580,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
label={_('Letter Spacing')}
value={letterSpacing}
onChange={setLetterSpacing}
disabled={useBookLayout}
min={-2}
max={4}
step={0.5}
@@ -569,6 +590,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
label={_('Text Indent')}
value={textIndent}
onChange={setTextIndent}
disabled={useBookLayout}
min={-2}
max={4}
step={1}
@@ -580,6 +602,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
type='checkbox'
className='toggle'
checked={fullJustification}
disabled={useBookLayout}
onChange={() => setFullJustification(!fullJustification)}
/>
</div>
@@ -589,6 +612,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
type='checkbox'
className='toggle'
checked={hyphenation}
disabled={useBookLayout}
onChange={() => setHyphenation(!hyphenation)}
/>
</div>
@@ -235,6 +235,12 @@ const layoutPanelItems = [
keywords: ['border', 'frame', 'vertical', 'mode'],
section: 'Layout',
},
{
id: 'settings.layout.useBookLayout',
labelKey: _('Use Book Layout'),
keywords: ['paragraph', 'book', 'layout', 'default', 'original', 'preserve'],
section: 'Paragraph',
},
{
id: 'settings.layout.paragraphMargin',
labelKey: _('Paragraph Margin'),
+5 -3
View File
@@ -221,9 +221,6 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
fullJustification: true,
hyphenation: true,
theme: 'light',
overrideFont: false,
overrideLayout: false,
overrideColor: false,
backgroundTextureId: 'none',
backgroundOpacity: 0.6,
backgroundSize: 'cover',
@@ -233,6 +230,11 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
userStylesheet: '',
userUIStylesheet: '',
overrideFont: false,
overrideLayout: false,
overrideColor: false,
useBookLayout: false,
zoomMode: 'fit-page',
spreadMode: 'auto',
keepCoverSpread: true,
+5 -3
View File
@@ -185,9 +185,6 @@ export interface BookStyle {
fullJustification: boolean;
hyphenation: boolean;
theme: string;
overrideFont: boolean;
overrideLayout: boolean;
overrideColor: boolean;
backgroundTextureId: string;
backgroundOpacity: number;
backgroundSize: string;
@@ -197,6 +194,11 @@ export interface BookStyle {
userStylesheet: string;
userUIStylesheet: string;
overrideFont: boolean;
overrideLayout: boolean;
overrideColor: boolean;
useBookLayout: boolean;
// fixed-layout specific
zoomMode: 'fit-page' | 'fit-width' | 'original-size' | 'custom';
spreadMode: 'auto' | 'none';
+132 -119
View File
@@ -238,46 +238,24 @@ const getColorStyles = (
return colorStyles;
};
const getLayoutStyles = (
overrideLayout: boolean,
const getPageLayoutStyles = (
marginTop: number,
marginRight: number,
marginBottom: number,
marginLeft: number,
paragraphMargin: number,
lineSpacing: number,
wordSpacing: number,
letterSpacing: number,
textIndent: number,
justify: boolean,
hyphenate: boolean,
zoomLevel: number,
writingMode: string,
vertical: boolean,
) => {
const layoutStyle = `
) => `
@namespace epub "http://www.idpf.org/2007/ops";
html {
--default-text-align: ${justify ? 'justify' : 'start'};
--margin-top: ${marginTop}px;
--margin-right: ${marginRight}px;
--margin-bottom: ${marginBottom}px;
--margin-left: ${marginLeft}px;
hanging-punctuation: allow-end last;
orphans: 2;
widows: 2;
}
[align="left"] { text-align: left; }
[align="right"] { text-align: right; }
[align="center"] { text-align: center; }
[align="justify"] { text-align: justify; }
:is(hgroup, header) p {
text-align: unset;
hyphens: unset;
}
html, body {
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode} !important;`}
text-align: var(--default-text-align);
max-height: unset;
-webkit-touch-callout: none;
-webkit-user-select: text;
@@ -306,6 +284,116 @@ const getLayoutStyles = (
position: absolute;
inset: -10px;
}
pre {
white-space: pre-wrap !important;
}
.epubtype-footnote,
aside[epub|type~="endnote"],
aside[epub|type~="footnote"],
aside[epub|type~="note"],
aside[epub|type~="rearnote"] {
display: none;
}
/* Now begins really dirty hacks to fix some badly designed epubs */
body {
line-height: unset;
}
.duokan-footnote-content,
.duokan-footnote-item {
display: none;
}
div:has(> img, > svg) {
max-width: 100% !important;
}
body.paginated-mode td:has(img), body.paginated-mode td :has(img) {
max-height: calc(var(--available-height) * 0.8 * 1px);
}
/* some epubs set insane inline-block for p */
p {
display: block;
}
/* inline images without dimension */
.ie6 img {
width: unset;
height: unset;
}
sup img {
height: 1em;
}
img.has-text-siblings {
${vertical ? 'width: 1em;' : 'height: 1em;'}
vertical-align: baseline;
}
:is(div) > img.has-text-siblings[style*="object-fit"] {
display: block;
height: auto;
vertical-align: unset;
}
.duokan-footnote img:not([class]) {
width: 0.8em;
height: 0.8em;
}
div:has(img.singlepage) {
position: relative;
width: auto;
height: auto;
}
/* page break */
body.paginated-mode div[style*="page-break-after: always"],
body.paginated-mode div[style*="page-break-after:always"],
body.paginated-mode p[style*="page-break-after: always"],
body.paginated-mode p[style*="page-break-after:always"] {
margin-bottom: calc(var(--available-height) * 1px);
}
.br {
display: flow-root;
}
.h5_mainbody {
overflow: unset !important;
}
`;
// Paragraph-scoped CSS controlled by the Paragraph section of the Layout
// panel. Gated by BookStyle.useBookLayout: when true, this chunk is skipped
// and the book's own paragraph formatting takes over.
const getParagraphLayoutStyles = (
overrideLayout: boolean,
paragraphMargin: number,
lineSpacing: number,
wordSpacing: number,
letterSpacing: number,
textIndent: number,
justify: boolean,
hyphenate: boolean,
vertical: boolean,
) => `
html {
--default-text-align: ${justify ? 'justify' : 'start'};
hanging-punctuation: allow-end last;
orphans: 2;
widows: 2;
}
html, body {
text-align: var(--default-text-align);
}
[align="left"] { text-align: left; }
[align="right"] { text-align: right; }
[align="center"] { text-align: center; }
[align="justify"] { text-align: justify; }
:is(hgroup, header) p {
text-align: unset;
hyphens: unset;
}
p, blockquote, dd, div:not(:has(*:not(b, a, em, i, strong, u, span))) {
line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''};
word-spacing: ${wordSpacing}px ${overrideLayout ? '!important' : ''};
@@ -374,22 +462,11 @@ const getLayoutStyles = (
orphans: 1;
}
pre {
white-space: pre-wrap !important;
}
.epubtype-footnote,
aside[epub|type~="endnote"],
aside[epub|type~="footnote"],
aside[epub|type~="note"],
aside[epub|type~="rearnote"] {
display: none;
}
/* Now begins really dirty hacks to fix some badly designed epubs */
body {
line-height: unset;
}
/* workaround for some badly designed epubs */
div.left *, p.left * { text-align: left; }
div.right *, p.right * { text-align: right; }
div.center *, p.center * { text-align: center; }
div.justify *, p.justify * { text-align: justify; }
img.pi {
${vertical ? 'transform: rotate(90deg);' : ''}
@@ -399,79 +476,10 @@ const getLayoutStyles = (
${vertical ? `vertical-align: unset;` : ''}
}
.duokan-footnote-content,
.duokan-footnote-item {
display: none;
}
div:has(> img, > svg) {
max-width: 100% !important;
}
body.paginated-mode td:has(img), body.paginated-mode td :has(img) {
max-height: calc(var(--available-height) * 0.8 * 1px);
}
/* some epubs set insane inline-block for p */
p {
display: block;
}
/* inline images without dimension */
.ie6 img {
width: unset;
height: unset;
}
sup img {
height: 1em;
}
img.has-text-siblings {
${vertical ? 'width: 1em;' : 'height: 1em;'}
vertical-align: baseline;
}
:is(div) > img.has-text-siblings[style*="object-fit"] {
display: block;
height: auto;
vertical-align: unset;
}
.duokan-footnote img:not([class]) {
width: 0.8em;
height: 0.8em;
}
div:has(img.singlepage) {
position: relative;
width: auto;
height: auto;
}
/* page break */
body.paginated-mode div[style*="page-break-after: always"],
body.paginated-mode div[style*="page-break-after:always"],
body.paginated-mode p[style*="page-break-after: always"],
body.paginated-mode p[style*="page-break-after:always"] {
margin-bottom: calc(var(--available-height) * 1px);
}
/* workaround for some badly designed epubs */
div.left *, p.left * { text-align: left; }
div.right *, p.right * { text-align: right; }
div.center *, p.center * { text-align: center; }
div.justify *, p.justify * { text-align: justify; }
.br {
display: flow-root;
}
.h5_mainbody {
overflow: unset !important;
}
.nonindent, .noindent {
text-indent: unset !important;
}
`;
return layoutStyle;
};
export const getFootnoteStyles = () => `
.duokan-footnote-content,
@@ -587,23 +595,28 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
if (!themeCode) {
themeCode = getThemeCode();
}
const layoutStyles = getLayoutStyles(
viewSettings.overrideLayout!,
const pageLayoutStyles = getPageLayoutStyles(
viewSettings.marginTopPx,
viewSettings.marginRightPx,
viewSettings.marginBottomPx,
viewSettings.marginLeftPx,
viewSettings.paragraphMargin!,
viewSettings.lineHeight!,
viewSettings.wordSpacing!,
viewSettings.letterSpacing!,
viewSettings.textIndent!,
viewSettings.fullJustification!,
viewSettings.hyphenation!,
1.0,
viewSettings.writingMode!,
viewSettings.vertical!,
);
const paragraphLayoutStyles = viewSettings.useBookLayout
? ''
: getParagraphLayoutStyles(
viewSettings.overrideLayout!,
viewSettings.paragraphMargin!,
viewSettings.lineHeight!,
viewSettings.wordSpacing!,
viewSettings.letterSpacing!,
viewSettings.textIndent!,
viewSettings.fullJustification!,
viewSettings.hyphenation!,
viewSettings.vertical!,
);
// scale the font size on-the-fly so that we can sync the same font size on different devices
const isMobile = ['ios', 'android'].includes(getOSPlatform());
const fontScale = isMobile ? 1.25 : 1;
@@ -629,7 +642,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
);
const translationStyles = getTranslationStyles(viewSettings.showTranslateSource!);
const userStylesheet = viewSettings.userStylesheet!;
return `${layoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${userStylesheet}`;
return `${pageLayoutStyles}\n${paragraphLayoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${userStylesheet}`;
};
export const applyTranslationStyle = (viewSettings: ViewSettings) => {