forked from akai/readest
feat(pdf): support setting zoom level, zoom mode and spread mode for PDF (#1969)
This should close #1717, close #1276, close #669.
This commit is contained in:
@@ -537,5 +537,6 @@
|
||||
"Pause": "إيقاف مؤقت",
|
||||
"Play": "تشغيل",
|
||||
"Next Sentence": "الجملة التالية",
|
||||
"Next Paragraph": "الفقرة التالية"
|
||||
"Next Paragraph": "الفقرة التالية",
|
||||
"Separate Cover Page": "صفحة غلاف منفصلة"
|
||||
}
|
||||
|
||||
@@ -521,5 +521,6 @@
|
||||
"Pause": "বিরতি",
|
||||
"Play": "চালান",
|
||||
"Next Sentence": "পরবর্তী বাক্য",
|
||||
"Next Paragraph": "পরবর্তী অনুচ্ছেদ"
|
||||
"Next Paragraph": "পরবর্তী অনুচ্ছেদ",
|
||||
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা"
|
||||
}
|
||||
|
||||
@@ -517,5 +517,6 @@
|
||||
"Pause": "སྐར་མ་འཇོག",
|
||||
"Play": "གཏོང་བ།",
|
||||
"Next Sentence": "རྗེས་ཀྱི་ཚིག་གྲུབ།",
|
||||
"Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།"
|
||||
"Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།",
|
||||
"Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -521,5 +521,6 @@
|
||||
"Pause": "Παύση",
|
||||
"Play": "Αναπαραγωγή",
|
||||
"Next Sentence": "Επόμενη πρόταση",
|
||||
"Next Paragraph": "Επόμενη παράγραφος"
|
||||
"Next Paragraph": "Επόμενη παράγραφος",
|
||||
"Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -521,5 +521,6 @@
|
||||
"Pause": "रोकें",
|
||||
"Play": "चलाएं",
|
||||
"Next Sentence": "अगला वाक्य",
|
||||
"Next Paragraph": "अगला पैराग्राफ"
|
||||
"Next Paragraph": "अगला पैराग्राफ",
|
||||
"Separate Cover Page": "अलग कवर पृष्ठ"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -517,5 +517,6 @@
|
||||
"Pause": "一時停止",
|
||||
"Play": "再生",
|
||||
"Next Sentence": "次の文",
|
||||
"Next Paragraph": "次の段落"
|
||||
"Next Paragraph": "次の段落",
|
||||
"Separate Cover Page": "別の表紙ページ"
|
||||
}
|
||||
|
||||
@@ -517,5 +517,6 @@
|
||||
"Pause": "일시정지",
|
||||
"Play": "재생",
|
||||
"Next Sentence": "다음 문장",
|
||||
"Next Paragraph": "다음 단락"
|
||||
"Next Paragraph": "다음 단락",
|
||||
"Separate Cover Page": "별도의 표지 페이지"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -529,5 +529,6 @@
|
||||
"Pause": "Пауза",
|
||||
"Play": "Воспроизвести",
|
||||
"Next Sentence": "Следующее предложение",
|
||||
"Next Paragraph": "Следующий абзац"
|
||||
"Next Paragraph": "Следующий абзац",
|
||||
"Separate Cover Page": "Отдельная страница обложки"
|
||||
}
|
||||
|
||||
@@ -521,5 +521,6 @@
|
||||
"Pause": "විරාමය",
|
||||
"Play": "වාදනය කරන්න",
|
||||
"Next Sentence": "මීළඟ වාක්යය",
|
||||
"Next Paragraph": "මීළඟ ඡේදය"
|
||||
"Next Paragraph": "මීළඟ ඡේදය",
|
||||
"Separate Cover Page": "වෙන් වූ ආවරණ පිටුව"
|
||||
}
|
||||
|
||||
@@ -521,5 +521,6 @@
|
||||
"Pause": "இடைநிறுத்தம்",
|
||||
"Play": "இயக்கு",
|
||||
"Next Sentence": "அடுத்த வாக்கியம்",
|
||||
"Next Paragraph": "அடுத்த பத்தி"
|
||||
"Next Paragraph": "அடுத்த பத்தி",
|
||||
"Separate Cover Page": "தனித்த அட்டை பக்கம்"
|
||||
}
|
||||
|
||||
@@ -517,5 +517,6 @@
|
||||
"Pause": "หยุดชั่วคราว",
|
||||
"Play": "เล่น",
|
||||
"Next Sentence": "ประโยคถัดไป",
|
||||
"Next Paragraph": "ย่อหน้าถัดไป"
|
||||
"Next Paragraph": "ย่อหน้าถัดไป",
|
||||
"Separate Cover Page": "หน้าปกแยกต่างหาก"
|
||||
}
|
||||
|
||||
@@ -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ı"
|
||||
}
|
||||
|
||||
@@ -529,5 +529,6 @@
|
||||
"Pause": "Пауза",
|
||||
"Play": "Відтворити",
|
||||
"Next Sentence": "Наступне речення",
|
||||
"Next Paragraph": "Наступний абзац"
|
||||
"Next Paragraph": "Наступний абзац",
|
||||
"Separate Cover Page": "Окрема сторінка обкладинки"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -517,5 +517,6 @@
|
||||
"Pause": "暂停",
|
||||
"Play": "播放",
|
||||
"Next Sentence": "下一句",
|
||||
"Next Paragraph": "下一段"
|
||||
"Next Paragraph": "下一段",
|
||||
"Separate Cover Page": "单独封面页"
|
||||
}
|
||||
|
||||
@@ -517,5 +517,6 @@
|
||||
"Pause": "暫停",
|
||||
"Play": "播放",
|
||||
"Next Sentence": "下一句",
|
||||
"Next Paragraph": "下一段"
|
||||
"Next Paragraph": "下一段",
|
||||
"Separate Cover Page": "單獨封面頁"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ViewMenuProps> = ({
|
||||
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<ViewMenuProps> = ({
|
||||
|
||||
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<ViewMenuProps> = ({
|
||||
// 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<ViewMenuProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{bookData.bookDoc?.rendition?.layout === 'pre-paginated' && (
|
||||
<>
|
||||
<div className={clsx('my-2 flex items-center justify-between rounded-md')}>
|
||||
<button
|
||||
onClick={setSpreadMode.bind(null, 'none')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
spreadMode === 'none' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<TbColumns1 />
|
||||
</button>
|
||||
<button
|
||||
onClick={setSpreadMode.bind(null, 'auto')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
spreadMode === 'auto' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<TbColumns2 />
|
||||
</button>
|
||||
<div className='bg-base-300 mx-2 h-6 w-[1px]' />
|
||||
<button
|
||||
onClick={setZoomMode.bind(null, 'fit-page')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomMode === 'fit-page' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<IoMdExpand />
|
||||
</button>
|
||||
<button
|
||||
onClick={setZoomMode.bind(null, 'fit-width')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomMode === 'fit-width' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<TbArrowAutofitWidth />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MenuItem
|
||||
label={_('Separate Cover Page')}
|
||||
Icon={keepCoverSpread ? MdCheck : undefined}
|
||||
onClick={() => setKeepCoverSpread(!keepCoverSpread)}
|
||||
disabled={spreadMode === 'none'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<hr className='border-base-300 my-1' />
|
||||
|
||||
<MenuItem label={_('Font & Layout')} shortcut='Shift+F' onClick={openFontLayoutMenu} />
|
||||
@@ -154,6 +244,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
shortcut='Shift+J'
|
||||
Icon={isScrolledMode ? MdCheck : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
disabled={bookData.bookDoc?.rendition?.layout === 'pre-paginated'}
|
||||
/>
|
||||
|
||||
<hr className='border-base-300 my-1' />
|
||||
|
||||
@@ -57,7 +57,10 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
{!noIcon && (
|
||||
<span style={{ minWidth: `${iconSize}px` }}>
|
||||
{typeof Icon === 'function' ? (
|
||||
<Icon className='text-base-content' size={iconSize} />
|
||||
<Icon
|
||||
className={disabled ? 'text-gray-400' : 'text-base-content'}
|
||||
size={iconSize}
|
||||
/>
|
||||
) : (
|
||||
Icon
|
||||
)}
|
||||
@@ -73,8 +76,9 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
{shortcut && (
|
||||
<kbd
|
||||
className={clsx(
|
||||
'border-base-300/40 bg-base-300/75 text-neutral-content hidden rounded-md border shadow-sm sm:flex',
|
||||
'border-base-300/40 bg-base-300/75 hidden rounded-md border shadow-sm sm:flex',
|
||||
'shrink-0 px-1.5 py-0.5 text-xs font-medium',
|
||||
disabled ? 'text-gray-400' : 'text-neutral-content',
|
||||
)}
|
||||
>
|
||||
{shortcut}
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface SectionItem {
|
||||
size: number;
|
||||
linear: string;
|
||||
location?: Location;
|
||||
pageSpread?: 'left' | 'right' | 'center' | '';
|
||||
|
||||
createDocument: () => Promise<Document>;
|
||||
}
|
||||
@@ -87,6 +88,7 @@ export interface BookDoc {
|
||||
metadata: BookMetadata;
|
||||
rendition?: {
|
||||
layout?: 'pre-paginated' | 'reflowable';
|
||||
spread?: 'auto' | 'none';
|
||||
viewport?: { width: number; height: number };
|
||||
};
|
||||
dir: string;
|
||||
|
||||
@@ -151,6 +151,10 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
codeLanguage: 'auto-detect',
|
||||
userStylesheet: '',
|
||||
userUIStylesheet: '',
|
||||
|
||||
zoomMode: 'fit-page',
|
||||
spreadMode: 'auto',
|
||||
keepCoverSpread: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
|
||||
@@ -126,6 +126,11 @@ export interface BookStyle {
|
||||
codeLanguage: string;
|
||||
userStylesheet: string;
|
||||
userUIStylesheet: string;
|
||||
|
||||
// fixed-layout specific
|
||||
zoomMode: 'fit-page' | 'fit-width' | 'original-size' | 'custom';
|
||||
spreadMode: 'auto' | 'none';
|
||||
keepCoverSpread: boolean;
|
||||
}
|
||||
|
||||
export interface BookFont {
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: f952e8ccb2...a98e6cb83c
Reference in New Issue
Block a user