From 77c43039385f5d66e27fed2dbe926550e435c9dc Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 6 Sep 2025 00:20:15 +0800 Subject: [PATCH] feat(pdf): support setting zoom level, zoom mode and spread mode for PDF (#1969) This should close #1717, close #1276, close #669. --- .../public/locales/ar/translation.json | 3 +- .../public/locales/bn/translation.json | 3 +- .../public/locales/bo/translation.json | 3 +- .../public/locales/de/translation.json | 3 +- .../public/locales/el/translation.json | 3 +- .../public/locales/es/translation.json | 3 +- .../public/locales/fr/translation.json | 3 +- .../public/locales/hi/translation.json | 3 +- .../public/locales/id/translation.json | 3 +- .../public/locales/it/translation.json | 3 +- .../public/locales/ja/translation.json | 3 +- .../public/locales/ko/translation.json | 3 +- .../public/locales/nl/translation.json | 3 +- .../public/locales/pl/translation.json | 3 +- .../public/locales/pt/translation.json | 3 +- .../public/locales/ru/translation.json | 3 +- .../public/locales/si/translation.json | 3 +- .../public/locales/ta/translation.json | 3 +- .../public/locales/th/translation.json | 3 +- .../public/locales/tr/translation.json | 3 +- .../public/locales/uk/translation.json | 3 +- .../public/locales/vi/translation.json | 3 +- .../public/locales/zh-CN/translation.json | 3 +- .../public/locales/zh-TW/translation.json | 3 +- .../app/reader/components/FoliateViewer.tsx | 17 +++- .../src/app/reader/components/ViewMenu.tsx | 93 ++++++++++++++++++- apps/readest-app/src/components/MenuItem.tsx | 8 +- apps/readest-app/src/libs/document.ts | 2 + apps/readest-app/src/services/constants.ts | 4 + apps/readest-app/src/types/book.ts | 5 + packages/foliate-js | 2 +- 31 files changed, 172 insertions(+), 31 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index d40fbf11..c4f82a24 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -537,5 +537,6 @@ "Pause": "إيقاف مؤقت", "Play": "تشغيل", "Next Sentence": "الجملة التالية", - "Next Paragraph": "الفقرة التالية" + "Next Paragraph": "الفقرة التالية", + "Separate Cover Page": "صفحة غلاف منفصلة" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 51ce480d..8dbd04d6 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -521,5 +521,6 @@ "Pause": "বিরতি", "Play": "চালান", "Next Sentence": "পরবর্তী বাক্য", - "Next Paragraph": "পরবর্তী অনুচ্ছেদ" + "Next Paragraph": "পরবর্তী অনুচ্ছেদ", + "Separate Cover Page": "আলাদা কভার পৃষ্ঠা" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 37a7a6f4..d32cdc43 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -517,5 +517,6 @@ "Pause": "སྐར་མ་འཇོག", "Play": "གཏོང་བ།", "Next Sentence": "རྗེས་ཀྱི་ཚིག་གྲུབ།", - "Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།" + "Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།", + "Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index e7fd24c3..a679e79b 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -521,5 +521,6 @@ "Pause": "Pause", "Play": "Wiedergabe", "Next Sentence": "Nächster Satz", - "Next Paragraph": "Nächster Absatz" + "Next Paragraph": "Nächster Absatz", + "Separate Cover Page": "Titelseite separat anzeigen" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 350a1a9f..369a6e99 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -521,5 +521,6 @@ "Pause": "Παύση", "Play": "Αναπαραγωγή", "Next Sentence": "Επόμενη πρόταση", - "Next Paragraph": "Επόμενη παράγραφος" + "Next Paragraph": "Επόμενη παράγραφος", + "Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index cb493211..871c680d 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -525,5 +525,6 @@ "Pause": "Pausar", "Play": "Reproducir", "Next Sentence": "Siguiente oración", - "Next Paragraph": "Siguiente párrafo" + "Next Paragraph": "Siguiente párrafo", + "Separate Cover Page": "Portada separada" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 8cefadf1..c095077c 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -525,5 +525,6 @@ "Pause": "Pause", "Play": "Lecture", "Next Sentence": "Phrase suivante", - "Next Paragraph": "Paragraphe suivant" + "Next Paragraph": "Paragraphe suivant", + "Separate Cover Page": "Page de couverture séparée" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 2c215666..ac0596d0 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -521,5 +521,6 @@ "Pause": "रोकें", "Play": "चलाएं", "Next Sentence": "अगला वाक्य", - "Next Paragraph": "अगला पैराग्राफ" + "Next Paragraph": "अगला पैराग्राफ", + "Separate Cover Page": "अलग कवर पृष्ठ" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 165b689f..8086a516 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -517,5 +517,6 @@ "Pause": "Jeda", "Play": "Putar", "Next Sentence": "Kalimat Berikutnya", - "Next Paragraph": "Paragraf Berikutnya" + "Next Paragraph": "Paragraf Berikutnya", + "Separate Cover Page": "Halaman Sampul Terpisah" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index a9d1ebb4..ff5d8d75 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -525,5 +525,6 @@ "Pause": "Pausa", "Play": "Riproduci", "Next Sentence": "Frase successiva", - "Next Paragraph": "Paragrafo successivo" + "Next Paragraph": "Paragrafo successivo", + "Separate Cover Page": "Pagina di copertina separata" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index eddee451..61e8265a 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -517,5 +517,6 @@ "Pause": "一時停止", "Play": "再生", "Next Sentence": "次の文", - "Next Paragraph": "次の段落" + "Next Paragraph": "次の段落", + "Separate Cover Page": "別の表紙ページ" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 9f8561ca..6b922328 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -517,5 +517,6 @@ "Pause": "일시정지", "Play": "재생", "Next Sentence": "다음 문장", - "Next Paragraph": "다음 단락" + "Next Paragraph": "다음 단락", + "Separate Cover Page": "별도의 표지 페이지" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 6de6970f..694dea78 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -521,5 +521,6 @@ "Pause": "Pauzeren", "Play": "Afspelen", "Next Sentence": "Volgende zin", - "Next Paragraph": "Volgende alinea" + "Next Paragraph": "Volgende alinea", + "Separate Cover Page": "Afzonderlijke omslagpagina" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 74d35104..50bca5b7 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -529,5 +529,6 @@ "Pause": "Pauza", "Play": "Odtwórz", "Next Sentence": "Następne zdanie", - "Next Paragraph": "Następny akapit" + "Next Paragraph": "Następny akapit", + "Separate Cover Page": "Osobna strona okładki" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 6297d89a..df22bb61 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -525,5 +525,6 @@ "Pause": "Pausar", "Play": "Reproduzir", "Next Sentence": "Próxima frase", - "Next Paragraph": "Próximo parágrafo" + "Next Paragraph": "Próximo parágrafo", + "Separate Cover Page": "Páginas de Capa Separadas" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 1126555c..4e6dfa44 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -529,5 +529,6 @@ "Pause": "Пауза", "Play": "Воспроизвести", "Next Sentence": "Следующее предложение", - "Next Paragraph": "Следующий абзац" + "Next Paragraph": "Следующий абзац", + "Separate Cover Page": "Отдельная страница обложки" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 07f48fb7..8f39e54f 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -521,5 +521,6 @@ "Pause": "විරාමය", "Play": "වාදනය කරන්න", "Next Sentence": "මීළඟ වාක්‍යය", - "Next Paragraph": "මීළඟ ඡේදය" + "Next Paragraph": "මීළඟ ඡේදය", + "Separate Cover Page": "වෙන් වූ ආවරණ පිටුව" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 69306c2f..ca944b92 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -521,5 +521,6 @@ "Pause": "இடைநிறுத்தம்", "Play": "இயக்கு", "Next Sentence": "அடுத்த வாக்கியம்", - "Next Paragraph": "அடுத்த பத்தி" + "Next Paragraph": "அடுத்த பத்தி", + "Separate Cover Page": "தனித்த அட்டை பக்கம்" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 8931fea5..1b65cbcf 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -517,5 +517,6 @@ "Pause": "หยุดชั่วคราว", "Play": "เล่น", "Next Sentence": "ประโยคถัดไป", - "Next Paragraph": "ย่อหน้าถัดไป" + "Next Paragraph": "ย่อหน้าถัดไป", + "Separate Cover Page": "หน้าปกแยกต่างหาก" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index adc645b7..7eddc344 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -521,5 +521,6 @@ "Pause": "Duraklat", "Play": "Oynat", "Next Sentence": "Sonraki Cümle", - "Next Paragraph": "Sonraki Paragraf" + "Next Paragraph": "Sonraki Paragraf", + "Separate Cover Page": "Ayrı Kapak Sayfası" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index bf8d348d..c5cf2e66 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -529,5 +529,6 @@ "Pause": "Пауза", "Play": "Відтворити", "Next Sentence": "Наступне речення", - "Next Paragraph": "Наступний абзац" + "Next Paragraph": "Наступний абзац", + "Separate Cover Page": "Окрема сторінка обкладинки" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 97b0867b..8beafcd5 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -517,5 +517,6 @@ "Pause": "Tạm dừng", "Play": "Phát", "Next Sentence": "Câu tiếp theo", - "Next Paragraph": "Đoạn tiếp theo" + "Next Paragraph": "Đoạn tiếp theo", + "Separate Cover Page": "Trang Bìa Riêng" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index fd22da1f..20a6f821 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -517,5 +517,6 @@ "Pause": "暂停", "Play": "播放", "Next Sentence": "下一句", - "Next Paragraph": "下一段" + "Next Paragraph": "下一段", + "Separate Cover Page": "单独封面页" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 1208ea49..8f008b63 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -517,5 +517,6 @@ "Pause": "暫停", "Play": "播放", "Next Sentence": "下一句", - "Next Paragraph": "下一段" + "Next Paragraph": "下一段", + "Separate Cover Page": "單獨封面頁" } diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 9c489328..d8ab3104 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -256,6 +256,12 @@ const FoliateViewer: React.FC<{ } } + if (bookDoc.rendition?.layout === 'pre-paginated' && bookDoc.sections) { + bookDoc.rendition.spread = viewSettings.spreadMode; + const coverSide = bookDoc.dir === 'rtl' ? 'right' : 'left'; + bookDoc.sections[0]!.pageSpread = viewSettings.keepCoverSpread ? '' : coverSide; + } + await view.open(bookDoc); // make sure we can listen renderer events after opening book viewRef.current = view; @@ -291,9 +297,14 @@ const FoliateViewer: React.FC<{ } else { view.renderer.removeAttribute('animated'); } - view.renderer.setAttribute('max-column-count', maxColumnCount); - view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`); - view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`); + if (bookDoc?.rendition?.layout === 'pre-paginated') { + view.renderer.setAttribute('zoom', viewSettings.zoomMode); + view.renderer.setAttribute('spread', viewSettings.spreadMode); + } else { + view.renderer.setAttribute('max-column-count', maxColumnCount); + view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`); + view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`); + } applyMarginAndGap(); const lastLocation = config.location; diff --git a/apps/readest-app/src/app/reader/components/ViewMenu.tsx b/apps/readest-app/src/app/reader/components/ViewMenu.tsx index 3eccf7bf..a3a5be3d 100644 --- a/apps/readest-app/src/app/reader/components/ViewMenu.tsx +++ b/apps/readest-app/src/app/reader/components/ViewMenu.tsx @@ -6,6 +6,9 @@ import { BiMoon, BiSun } from 'react-icons/bi'; import { TbSunMoon } from 'react-icons/tb'; import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md'; import { MdSync, MdSyncProblem } from 'react-icons/md'; +import { IoMdExpand } from 'react-icons/io'; +import { TbArrowAutofitWidth } from 'react-icons/tb'; +import { TbColumns1, TbColumns2 } from 'react-icons/tb'; import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants'; import { useEnv } from '@/context/EnvContext'; @@ -37,14 +40,18 @@ const ViewMenu: React.FC = ({ const router = useRouter(); const { user } = useAuth(); const { envConfig, appService } = useEnv(); - const { getConfig } = useBookDataStore(); + const { getConfig, getBookData } = useBookDataStore(); const { getView, getViewSettings, setViewSettings } = useReaderStore(); const config = getConfig(bookKey)!; + const bookData = getBookData(bookKey)!; const viewSettings = getViewSettings(bookKey)!; const { themeMode, isDarkMode, setThemeMode } = useThemeStore(); const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled); const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!); + const [zoomMode, setZoomMode] = useState(viewSettings!.zoomMode!); + const [spreadMode, setSpreadMode] = useState(viewSettings!.spreadMode!); + const [keepCoverSpread, setKeepCoverSpread] = useState(viewSettings!.keepCoverSpread!); const [invertImgColorInDark, setInvertImgColorInDark] = useState( viewSettings!.invertImgColorInDark, ); @@ -93,6 +100,9 @@ const ViewMenu: React.FC = ({ useEffect(() => { saveViewSettings(envConfig, bookKey, 'zoomLevel', zoomLevel, true, true); + if (bookData.bookDoc?.rendition?.layout === 'pre-paginated') { + getView(bookKey)?.renderer.setAttribute('scale-factor', zoomLevel); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [zoomLevel]); @@ -102,6 +112,36 @@ const ViewMenu: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [invertImgColorInDark]); + useEffect(() => { + if (zoomMode === viewSettings.zoomMode) return; + viewSettings.zoomMode = zoomMode; + getView(bookKey)?.renderer.setAttribute('zoom', zoomMode); + setViewSettings(bookKey, viewSettings); + saveViewSettings(envConfig, bookKey, 'zoomMode', zoomMode, true, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [zoomMode]); + + useEffect(() => { + if (spreadMode === viewSettings.spreadMode) return; + viewSettings.spreadMode = spreadMode; + getView(bookKey)?.renderer.setAttribute('spread', spreadMode); + setViewSettings(bookKey, viewSettings); + saveViewSettings(envConfig, bookKey, 'spreadMode', spreadMode, true, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [spreadMode]); + + useEffect(() => { + if (keepCoverSpread === viewSettings.keepCoverSpread) return; + if (!bookData?.bookDoc?.sections?.length) return; + viewSettings.keepCoverSpread = keepCoverSpread; + const coverSide = bookData.bookDoc.dir === 'rtl' ? 'right' : 'left'; + bookData.bookDoc.sections[0]!.pageSpread = keepCoverSpread ? '' : coverSide; + getView(bookKey)?.renderer.setAttribute('spread', spreadMode); + setViewSettings(bookKey, viewSettings); + saveViewSettings(envConfig, bookKey, 'keepCoverSpread', keepCoverSpread, true, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [keepCoverSpread]); + const lastSyncTime = Math.max(config?.lastSyncedAtConfig || 0, config?.lastSyncedAtNotes || 0); return ( @@ -145,6 +185,56 @@ const ViewMenu: React.FC = ({ + {bookData.bookDoc?.rendition?.layout === 'pre-paginated' && ( + <> +
+ + +
+ + +
+ + setKeepCoverSpread(!keepCoverSpread)} + disabled={spreadMode === 'none'} + /> + + )}
@@ -154,6 +244,7 @@ const ViewMenu: React.FC = ({ shortcut='Shift+J' Icon={isScrolledMode ? MdCheck : undefined} onClick={toggleScrolledMode} + disabled={bookData.bookDoc?.rendition?.layout === 'pre-paginated'} />
diff --git a/apps/readest-app/src/components/MenuItem.tsx b/apps/readest-app/src/components/MenuItem.tsx index 67f65929..8bfeb81f 100644 --- a/apps/readest-app/src/components/MenuItem.tsx +++ b/apps/readest-app/src/components/MenuItem.tsx @@ -57,7 +57,10 @@ const MenuItem: React.FC = ({ {!noIcon && ( {typeof Icon === 'function' ? ( - + ) : ( Icon )} @@ -73,8 +76,9 @@ const MenuItem: React.FC = ({ {shortcut && (