forked from akai/readest
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05e9549410 | |||
| 35fbce3506 | |||
| c1d6825793 | |||
| ae6c6970b4 | |||
| f8fef57cf3 | |||
| 4dc191735d | |||
| 4f11f437e1 | |||
| 5773e31990 | |||
| 9dda5d4c88 | |||
| 7f5b6959e1 | |||
| 126981d085 | |||
| 854a578929 | |||
| 6dd0daa597 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.86",
|
||||
"version": "0.9.88",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
|
||||
|
||||
@@ -622,5 +622,8 @@
|
||||
"Opacity": "شفافية",
|
||||
"Size": "حجم",
|
||||
"Cover": "غطاء",
|
||||
"Contain": "احتواء"
|
||||
"Contain": "احتواء",
|
||||
"{{number}} pages left in chapter": "تبقّت {{number}} صفحات في هذا الفصل",
|
||||
"Device": "الجهاز",
|
||||
"E-Ink Mode": "وضع الحبر الإلكتروني"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "স্বচ্ছতা",
|
||||
"Size": "আকার",
|
||||
"Cover": "ঢাকা",
|
||||
"Contain": "ধারণ"
|
||||
"Contain": "ধারণ",
|
||||
"{{number}} pages left in chapter": "অধ্যায়ে {{number}} পৃষ্ঠা বাকি",
|
||||
"Device": "যন্ত্র",
|
||||
"E-Ink Mode": "ই-ইঙ্ক মোড"
|
||||
}
|
||||
|
||||
@@ -602,5 +602,8 @@
|
||||
"Opacity": "སྤོ་སྐོར་གྱི་འོད་ཟེར།",
|
||||
"Size": "ཨང་",
|
||||
"Cover": "དེབ་གྱི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Contain": "འབྱོར་བ།"
|
||||
"Contain": "འབྱོར་བ།",
|
||||
"{{number}} pages left in chapter": "དོན་ཚན་ནང་ཤོག་ཨང་ {{number}} ལོག་གི་འདུག།",
|
||||
"Device": "རྐྱབ་སྐོར",
|
||||
"E-Ink Mode": "ཡིག་སྒྱུར་སྤོ་བ"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "Opazität",
|
||||
"Size": "Größe",
|
||||
"Cover": "Cover",
|
||||
"Contain": "Inhalt"
|
||||
"Contain": "Inhalt",
|
||||
"{{number}} pages left in chapter": "{{number}} Seiten verbleibend im Kapitel",
|
||||
"Device": "Gerät",
|
||||
"E-Ink Mode": "E-Ink-Modus"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "Αδιαφάνεια",
|
||||
"Size": "Μέγεθος",
|
||||
"Cover": "Εξώφυλλο",
|
||||
"Contain": "Περιέχει"
|
||||
"Contain": "Περιέχει",
|
||||
"{{number}} pages left in chapter": "Μένουν {{number}} σελίδες στο κεφάλαιο",
|
||||
"Device": "Συσκευή",
|
||||
"E-Ink Mode": "Λειτουργία E-Ink"
|
||||
}
|
||||
|
||||
@@ -610,5 +610,8 @@
|
||||
"Opacity": "Opacidad",
|
||||
"Size": "Tamaño",
|
||||
"Cover": "Cubierta",
|
||||
"Contain": "Contener"
|
||||
"Contain": "Contener",
|
||||
"{{number}} pages left in chapter": "{{number}} páginas restantes en el capítulo",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modo E-Ink"
|
||||
}
|
||||
|
||||
@@ -610,5 +610,8 @@
|
||||
"Opacity": "Opacité",
|
||||
"Size": "Taille",
|
||||
"Cover": "Couverture",
|
||||
"Contain": "Contenir"
|
||||
"Contain": "Contenir",
|
||||
"{{number}} pages left in chapter": "{{number}} pages restantes dans le chapitre",
|
||||
"Device": "Appareil",
|
||||
"E-Ink Mode": "Mode E-Ink"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "अस्पष्टता",
|
||||
"Size": "आकार",
|
||||
"Cover": "कवर",
|
||||
"Contain": "समाहित करें"
|
||||
"Contain": "समाहित करें",
|
||||
"{{number}} pages left in chapter": "इस अध्याय में {{number}} पृष्ठ शेष हैं",
|
||||
"Device": "डिवाइस",
|
||||
"E-Ink Mode": "ई-इंक मोड"
|
||||
}
|
||||
|
||||
@@ -602,5 +602,8 @@
|
||||
"Opacity": "Keburaman",
|
||||
"Size": "Ukuran",
|
||||
"Cover": "Sampul",
|
||||
"Contain": "Mengandung"
|
||||
"Contain": "Mengandung",
|
||||
"{{number}} pages left in chapter": "{{number}} halaman tersisa di bab",
|
||||
"Device": "Perangkat",
|
||||
"E-Ink Mode": "Mode E-Ink"
|
||||
}
|
||||
|
||||
@@ -610,5 +610,8 @@
|
||||
"Opacity": "Opacità",
|
||||
"Size": "Dimensione",
|
||||
"Cover": "Copertura",
|
||||
"Contain": "Contenere"
|
||||
"Contain": "Contenere",
|
||||
"{{number}} pages left in chapter": "{{number}} pagine rimaste nel capitolo",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modalità E-Ink"
|
||||
}
|
||||
|
||||
@@ -602,5 +602,8 @@
|
||||
"Opacity": "不透明度",
|
||||
"Size": "サイズ",
|
||||
"Cover": "カバー",
|
||||
"Contain": "含む"
|
||||
"Contain": "含む",
|
||||
"{{number}} pages left in chapter": "章に{{number}}ページ残り",
|
||||
"Device": "デバイス",
|
||||
"E-Ink Mode": "E-Inkモード"
|
||||
}
|
||||
|
||||
@@ -602,5 +602,8 @@
|
||||
"Opacity": "불투명도",
|
||||
"Size": "크기",
|
||||
"Cover": "표지",
|
||||
"Contain": "포함"
|
||||
"Contain": "포함",
|
||||
"{{number}} pages left in chapter": "남은 페이지 {{number}}장",
|
||||
"Device": "장치",
|
||||
"E-Ink Mode": "E-Ink 모드"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "Doorzichtigheid",
|
||||
"Size": "Grootte",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Bevatten"
|
||||
"Contain": "Bevatten",
|
||||
"{{number}} pages left in chapter": "Nog {{number}} pagina’s in hoofdstuk",
|
||||
"Device": "Apparaat",
|
||||
"E-Ink Mode": "E-Ink Modus"
|
||||
}
|
||||
|
||||
@@ -614,5 +614,8 @@
|
||||
"Opacity": "Przezroczystość",
|
||||
"Size": "Rozmiar",
|
||||
"Cover": "Okładka",
|
||||
"Contain": "Zawierać"
|
||||
"Contain": "Zawierać",
|
||||
"{{number}} pages left in chapter": "Pozostało {{number}} stron w rozdziale",
|
||||
"Device": "Urządzenie",
|
||||
"E-Ink Mode": "Tryb E-Ink"
|
||||
}
|
||||
|
||||
@@ -610,5 +610,8 @@
|
||||
"Opacity": "Opacidade",
|
||||
"Size": "Tamanho",
|
||||
"Cover": "Capa",
|
||||
"Contain": "Contém"
|
||||
"Contain": "Contém",
|
||||
"{{number}} pages left in chapter": "Faltam {{number}} páginas neste capítulo",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modo E-Ink"
|
||||
}
|
||||
|
||||
@@ -614,5 +614,8 @@
|
||||
"Opacity": "Непрозрачность",
|
||||
"Size": "Размер",
|
||||
"Cover": "Обложка",
|
||||
"Contain": "Содержать"
|
||||
"Contain": "Содержать",
|
||||
"{{number}} pages left in chapter": "Осталось {{number}} страниц в главе",
|
||||
"Device": "Устройство",
|
||||
"E-Ink Mode": "E-Ink режим"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "පැහැදිලිත්වය",
|
||||
"Size": "ප්රමාණය",
|
||||
"Cover": "ආවරණය",
|
||||
"Contain": "අඩංගු වන්න"
|
||||
"Contain": "අඩංගු වන්න",
|
||||
"{{number}} pages left in chapter": "පරිච්ඡේදයේ පිටු {{number}} ඉතිරිව ඇත",
|
||||
"Device": "උපකරණය",
|
||||
"E-Ink Mode": "E-Ink ආකාරය"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "Opacitet",
|
||||
"Size": "Storlek",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Innehålla"
|
||||
"Contain": "Innehålla",
|
||||
"{{number}} pages left in chapter": "{{number}} sidor kvar i kapitlet",
|
||||
"Device": "Enhet",
|
||||
"E-Ink Mode": "E-Ink-läge"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "பார்வை",
|
||||
"Size": "அளவு",
|
||||
"Cover": "மூடு",
|
||||
"Contain": "அடங்கும்"
|
||||
"Contain": "அடங்கும்",
|
||||
"{{number}} pages left in chapter": "அத்தியாயத்தில் {{number}} பக்கங்கள் மீதமுள்ளன",
|
||||
"Device": "சாதனம்",
|
||||
"E-Ink Mode": "E-Ink முறை"
|
||||
}
|
||||
|
||||
@@ -602,5 +602,8 @@
|
||||
"Opacity": "ความโปร่งใส",
|
||||
"Size": "ขนาด",
|
||||
"Cover": "ปก",
|
||||
"Contain": "บรรจุ"
|
||||
"Contain": "บรรจุ",
|
||||
"{{number}} pages left in chapter": "เหลือ {{number}} หน้าในบท",
|
||||
"Device": "อุปกรณ์",
|
||||
"E-Ink Mode": "โหมด E-Ink"
|
||||
}
|
||||
|
||||
@@ -606,5 +606,8 @@
|
||||
"Opacity": "Opaklık",
|
||||
"Size": "Boyut",
|
||||
"Cover": "Kapak",
|
||||
"Contain": "İçerir"
|
||||
"Contain": "İçerir",
|
||||
"{{number}} pages left in chapter": "Bu bölümde {{number}} sayfa kaldı",
|
||||
"Device": "Cihaz",
|
||||
"E-Ink Mode": "E-Ink Modu"
|
||||
}
|
||||
|
||||
@@ -614,5 +614,8 @@
|
||||
"Opacity": "Непрозорість",
|
||||
"Size": "Розмір",
|
||||
"Cover": "Обкладинка",
|
||||
"Contain": "Вміщати"
|
||||
"Contain": "Вміщати",
|
||||
"{{number}} pages left in chapter": "Залишилось {{number}} сторінок у розділі",
|
||||
"Device": "Пристрій",
|
||||
"E-Ink Mode": "E-Ink режим"
|
||||
}
|
||||
|
||||
@@ -602,5 +602,8 @@
|
||||
"Opacity": "Độ mờ",
|
||||
"Size": "Kích thước",
|
||||
"Cover": "Bìa",
|
||||
"Contain": "Chứa"
|
||||
"Contain": "Chứa",
|
||||
"{{number}} pages left in chapter": "Còn {{number}} trang trong chương",
|
||||
"Device": "Thiết bị",
|
||||
"E-Ink Mode": "Chế độ E-Ink"
|
||||
}
|
||||
|
||||
@@ -601,6 +601,9 @@
|
||||
"Import Image": "导入图片",
|
||||
"Opacity": "不透明度",
|
||||
"Size": "大小",
|
||||
"Cover": "封面",
|
||||
"Contain": "包含"
|
||||
"Cover": "覆盖",
|
||||
"Contain": "适应",
|
||||
"{{number}} pages left in chapter": "本章剩余{{number}}页",
|
||||
"Device": "设备",
|
||||
"E-Ink Mode": "E-Ink 模式"
|
||||
}
|
||||
|
||||
@@ -601,6 +601,9 @@
|
||||
"Import Image": "匯入圖片",
|
||||
"Opacity": "不透明度",
|
||||
"Size": "大小",
|
||||
"Cover": "封面",
|
||||
"Contain": "包含"
|
||||
"Cover": "覆蓋",
|
||||
"Contain": "適應",
|
||||
"{{number}} pages left in chapter": "本章剩餘{{number}}頁",
|
||||
"Device": "設備",
|
||||
"E-Ink Mode": "E-Ink 模式"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.88": {
|
||||
"date": "2025-10-18",
|
||||
"notes": [
|
||||
"TTS: Fixed issue where the read-aloud indicator disappeared too quickly",
|
||||
"Layout: Overriding book layout no longer affects explicitly set text alignment",
|
||||
"Reader: Added localized number display for reading progress in vertical layout",
|
||||
"Reader: Enhanced swipe responsiveness for smoother page transitions",
|
||||
"Settings: Added an option to disable animations when using E-Ink mode"
|
||||
]
|
||||
},
|
||||
"0.9.87": {
|
||||
"date": "2025-10-16",
|
||||
"notes": [
|
||||
"Reader: Improved chapter detection with better chapter title recognition",
|
||||
"Reader: Enhanced compatibility for parsing more footnote formats",
|
||||
"Theme: Fixed auto theme switching in new reader windows",
|
||||
"TTS: Improved scrolling accuracy to highlighted text during read-aloud",
|
||||
"TTS: Fixed crash issues on certain Android systems"
|
||||
]
|
||||
},
|
||||
"0.9.86": {
|
||||
"date": "2025-10-14",
|
||||
"notes": [
|
||||
|
||||
+38
-1
@@ -161,10 +161,47 @@ class MediaPlaybackService : MediaBrowserServiceCompat() {
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
|
||||
addAction(
|
||||
android.R.drawable.ic_media_previous,
|
||||
"Previous",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
)
|
||||
)
|
||||
if (playbackState == PlaybackStateCompat.STATE_PLAYING) {
|
||||
addAction(
|
||||
android.R.drawable.ic_media_pause,
|
||||
"Pause",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_PAUSE
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addAction(
|
||||
android.R.drawable.ic_media_play,
|
||||
"Play",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_PLAY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addAction(
|
||||
android.R.drawable.ic_media_next,
|
||||
"Next",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||
)
|
||||
)
|
||||
|
||||
setStyle(
|
||||
androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSession?.sessionToken)
|
||||
.setShowActionsInCompactView(0)
|
||||
.setShowActionsInCompactView(0, 1, 2)
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
|
||||
"frame-src": "'self' blob: asset: http://asset.localhost https://*.stripe.com",
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
|
||||
},
|
||||
|
||||
@@ -18,12 +18,14 @@ import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
|
||||
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
|
||||
import { useAutoFocus } from '@/hooks/useAutoFocus';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useEinkMode } from '@/hooks/useEinkMode';
|
||||
import { useKOSync } from '../hooks/useKOSync';
|
||||
import {
|
||||
applyFixedlayoutStyles,
|
||||
applyImageStyle,
|
||||
applyTranslationStyle,
|
||||
getStyles,
|
||||
keepTextAlignment,
|
||||
transformStylesheet,
|
||||
} from '@/utils/style';
|
||||
import { mountAdditionalFonts, mountCustomFont } from '@/styles/fonts';
|
||||
@@ -76,6 +78,7 @@ const FoliateViewer: React.FC<{
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { applyBackgroundTexture } = useBackgroundTexture();
|
||||
const { applyEinkMode } = useEinkMode();
|
||||
const viewState = getViewState(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
@@ -176,6 +179,8 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
applyImageStyle(detail.doc);
|
||||
|
||||
keepTextAlignment(detail.doc);
|
||||
|
||||
removeTabIndex(detail.doc);
|
||||
|
||||
// Inline scripts in tauri platforms are not executed by default
|
||||
@@ -310,6 +315,7 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
doubleClickDisabled.current = viewSettings.disableDoubleClick!;
|
||||
const animated = viewSettings.animated!;
|
||||
const eink = viewSettings.isEink!;
|
||||
const maxColumnCount = viewSettings.maxColumnCount!;
|
||||
const maxInlineSize = getMaxInlineSize(viewSettings);
|
||||
const maxBlockSize = viewSettings.maxBlockSize!;
|
||||
@@ -322,6 +328,14 @@ const FoliateViewer: React.FC<{
|
||||
} else {
|
||||
view.renderer.removeAttribute('animated');
|
||||
}
|
||||
if (appService?.isAndroidApp) {
|
||||
if (eink) {
|
||||
view.renderer.setAttribute('eink', '');
|
||||
} else {
|
||||
view.renderer.removeAttribute('eink');
|
||||
}
|
||||
applyEinkMode(eink);
|
||||
}
|
||||
if (bookDoc?.rendition?.layout === 'pre-paginated') {
|
||||
view.renderer.setAttribute('zoom', viewSettings.zoomMode);
|
||||
view.renderer.setAttribute('spread', viewSettings.spreadMode);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { formatReadingProgress } from '@/utils/progress';
|
||||
import { formatNumber, formatProgress } from '@/utils/progress';
|
||||
|
||||
interface PageInfoProps {
|
||||
bookKey: string;
|
||||
@@ -40,24 +40,34 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
const isVertical = viewSettings.vertical;
|
||||
const { progressStyle: readingProgressStyle } = viewSettings;
|
||||
|
||||
const formatTemplate =
|
||||
const template =
|
||||
readingProgressStyle === 'fraction'
|
||||
? isVertical
|
||||
? '{current} · {total}'
|
||||
: '{current} / {total}'
|
||||
: '{percent}%';
|
||||
|
||||
const pageProgress = bookData?.isFixedLayout ? section : pageinfo;
|
||||
const progressInfo = bookData?.isFixedLayout
|
||||
? formatReadingProgress(pageProgress?.current, pageProgress?.total, formatTemplate)
|
||||
: formatReadingProgress(pageProgress?.current, pageProgress?.total, formatTemplate);
|
||||
const lang = localStorage?.getItem('i18nextLng') || '';
|
||||
const localize = isVertical && lang.toLowerCase().startsWith('zh');
|
||||
const progress = bookData?.isFixedLayout ? section : pageinfo;
|
||||
const progressInfo = formatProgress(progress?.current, progress?.total, template, localize, lang);
|
||||
|
||||
const timeLeft = timeinfo
|
||||
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
|
||||
? _('{{time}} min left in chapter', {
|
||||
time: formatNumber(Math.round(timeinfo.section), localize, lang),
|
||||
})
|
||||
: '';
|
||||
const { page = 0, pages = 0 } = view?.renderer || {};
|
||||
const pageLeft =
|
||||
pages - 1 > page ? _('{{count}} pages left in chapter', { count: pages - 1 - page }) : '';
|
||||
pages - 1 > page
|
||||
? localize
|
||||
? _('{{number}} pages left in chapter', {
|
||||
number: formatNumber(pages - page - 1, localize, lang),
|
||||
})
|
||||
: _('{{count}} pages left in chapter', {
|
||||
count: pages - page - 1,
|
||||
})
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -68,10 +78,10 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
aria-label={[
|
||||
pageProgress
|
||||
progress
|
||||
? _('On {{current}} of {{total}} page', {
|
||||
current: pageProgress.current + 1,
|
||||
total: pageProgress.total,
|
||||
current: progress.current + 1,
|
||||
total: progress.total,
|
||||
})
|
||||
: '',
|
||||
timeLeft,
|
||||
@@ -99,7 +109,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={clsx(
|
||||
'flex items-center justify-center',
|
||||
'flex items-center justify-between',
|
||||
isVertical ? 'h-full' : 'h-[52px] w-full',
|
||||
)}
|
||||
>
|
||||
@@ -108,7 +118,11 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
) : viewSettings.showRemainingPages ? (
|
||||
<span className='text-start'>{pageLeft}</span>
|
||||
) : null}
|
||||
{viewSettings.showProgressInfo && <span className='ms-auto text-end'>{progressInfo}</span>}
|
||||
{viewSettings.showProgressInfo && (
|
||||
<span className={clsx('text-end', isVertical ? 'mt-auto' : 'ms-auto')}>
|
||||
{progressInfo}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -68,8 +68,8 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
}
|
||||
: {
|
||||
top: `${topInset}px`,
|
||||
insetInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
|
||||
width: `calc(100% - ${contentInsets.left + contentInsets.right}px)`,
|
||||
paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
|
||||
width: '100%',
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { isCJKStr } from '@/utils/lang';
|
||||
import { createRejecttFilter } from '@/utils/node';
|
||||
import { createRejectFilter } from '@/utils/node';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SearchOptions from './SearchOptions';
|
||||
|
||||
@@ -127,7 +127,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
...searchConfig,
|
||||
index,
|
||||
query: term,
|
||||
acceptNode: createRejecttFilter({
|
||||
acceptNode: createRejectFilter({
|
||||
tags: primaryLang.startsWith('ja') ? ['rt'] : [],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -54,6 +54,9 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
const [timeoutTimestamp, setTimeoutTimestamp] = useState(0);
|
||||
const [timeoutFunc, setTimeoutFunc] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [showIndicatorWithinTimeout, setShowIndicatorWithinTimeout] = useState(true);
|
||||
|
||||
const popupPadding = useResponsiveSize(POPUP_PADDING);
|
||||
const maxWidth = window.innerWidth - 2 * popupPadding;
|
||||
const popupWidth = Math.min(maxWidth, useResponsiveSize(POPUP_WIDTH));
|
||||
@@ -208,6 +211,30 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
viewSettings.ttsLocation = cfi || '';
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
}
|
||||
|
||||
if (!view.renderer.scrolled) {
|
||||
view.renderer.scrollToAnchor(range);
|
||||
} else {
|
||||
const rect = range.getBoundingClientRect();
|
||||
const { start, size, viewSize, sideProp } = view.renderer;
|
||||
const positionStart = rect[sideProp === 'height' ? 'y' : 'x'] + viewSettings.marginTopPx;
|
||||
const positionEnd = rect[sideProp === 'height' ? 'height' : 'width'] + positionStart;
|
||||
const offsetStart = view.book.dir === 'rtl' ? viewSize - positionStart : positionStart;
|
||||
const offsetEnd = view.book.dir === 'rtl' ? viewSize - positionEnd : positionEnd;
|
||||
|
||||
const showHeader = viewSettings.showHeader;
|
||||
const showFooter = viewSettings.showFooter;
|
||||
const showBarsOnScroll = viewSettings.showBarsOnScroll;
|
||||
const headerScrollOverlap = showHeader && showBarsOnScroll ? 44 : 0;
|
||||
const footerScrollOverlap = showFooter && showBarsOnScroll ? 44 : 0;
|
||||
const scrollingOverlap = viewSettings.scrollingOverlap;
|
||||
const endInNextView = offsetEnd > start + size - footerScrollOverlap - scrollingOverlap;
|
||||
const startInPrevView = offsetStart < start + headerScrollOverlap + scrollingOverlap;
|
||||
if (endInNextView || startInPrevView) {
|
||||
const scrollTo = offsetStart - headerScrollOverlap - scrollingOverlap;
|
||||
view.renderer.scrollToAnchor(scrollTo / viewSize);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ttsController.addEventListener('tts-speak-mark', handleSpeakMark);
|
||||
@@ -547,10 +574,38 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showPanel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredBookKey) {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
const showTimeout = setTimeout(() => {
|
||||
setShowIndicatorWithinTimeout(true);
|
||||
}, 100);
|
||||
hoverTimeoutRef.current = showTimeout;
|
||||
} else {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
const hideTimeout = setTimeout(() => {
|
||||
setShowIndicatorWithinTimeout(false);
|
||||
}, 5000);
|
||||
hoverTimeoutRef.current = hideTimeout;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [hoveredBookKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showPanel && <Overlay onDismiss={handleDismissPopup} />}
|
||||
{showIndicator && hoveredBookKey === bookKey && (
|
||||
{(showPanel || (showIndicator && showIndicatorWithinTimeout)) && (
|
||||
<div
|
||||
ref={iconRef}
|
||||
className={clsx(
|
||||
|
||||
@@ -112,7 +112,7 @@ export const useTouchEvent = (
|
||||
// also check for deltaX to prevent swipe page turn from triggering the toggle
|
||||
if (
|
||||
deltaY < -10 &&
|
||||
Math.abs(deltaY) > Math.abs(deltaX) &&
|
||||
Math.abs(deltaY) > Math.abs(deltaX) * 2 &&
|
||||
Math.abs(deltaX) < windowWidth * 0.3
|
||||
) {
|
||||
// swipe up to toggle the header bar and the footer bar, only for horizontal page mode
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
|
||||
import { useEinkMode } from '@/hooks/useEinkMode';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -20,6 +21,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const { applyUILanguage } = useSettingsStore();
|
||||
const { setScreenBrightness } = useDeviceControlStore();
|
||||
const { applyBackgroundTexture } = useBackgroundTexture();
|
||||
const { applyEinkMode } = useEinkMode();
|
||||
const iconSize = useDefaultIconSize();
|
||||
useSafeAreaInsets(); // Initialize safe area insets
|
||||
|
||||
@@ -48,6 +50,9 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
setScreenBrightness(brightness / 100);
|
||||
}
|
||||
applyBackgroundTexture(envConfig, globalViewSettings);
|
||||
if (appService.isAndroidApp) {
|
||||
applyEinkMode(globalViewSettings.isEink);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [envConfig, appService, applyUILanguage, setScreenBrightness, applyBackgroundTexture]);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResetViewSettings } from '@/hooks/useResetSettings';
|
||||
import { useEinkMode } from '@/hooks/useEinkMode';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
@@ -19,6 +20,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { applyEinkMode } = useEinkMode();
|
||||
const { acquireVolumeKeyInterception, releaseVolumeKeyInterception } = useDeviceControlStore();
|
||||
const bookData = getBookData(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
|
||||
@@ -31,6 +33,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
const [swapClickArea, setSwapClickArea] = useState(viewSettings.swapClickArea);
|
||||
const [isDisableDoubleClick, setIsDisableDoubleClick] = useState(viewSettings.disableDoubleClick);
|
||||
const [animated, setAnimated] = useState(viewSettings.animated);
|
||||
const [isEink, setIsEink] = useState(viewSettings.isEink);
|
||||
const [allowScript, setAllowScript] = useState(viewSettings.allowScript);
|
||||
|
||||
const resetToDefaults = useResetViewSettings();
|
||||
@@ -44,6 +47,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
disableClick: setIsDisableClick,
|
||||
swapClickArea: setSwapClickArea,
|
||||
animated: setAnimated,
|
||||
isEink: setIsEink,
|
||||
allowScript: setAllowScript,
|
||||
});
|
||||
};
|
||||
@@ -113,6 +117,17 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [animated]);
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'isEink', isEink, false, false);
|
||||
if (isEink) {
|
||||
getView(bookKey)?.renderer.setAttribute('eink', '');
|
||||
} else {
|
||||
getView(bookKey)?.renderer.removeAttribute('eink');
|
||||
}
|
||||
applyEinkMode(isEink);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEink]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewSettings.allowScript === allowScript) return;
|
||||
saveViewSettings(envConfig, bookKey, 'allowScript', allowScript, true, false);
|
||||
@@ -221,6 +236,25 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{appService?.isAndroidApp && (
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Device')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('E-Ink Mode')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={isEink}
|
||||
onChange={() => setIsEink(!isEink)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Security')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
|
||||
@@ -462,7 +462,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
onChange={setParagraphMargin}
|
||||
min={0}
|
||||
max={4}
|
||||
step={0.2}
|
||||
step={0.1}
|
||||
/>
|
||||
<NumberInput
|
||||
label={_('Line Spacing')}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useEinkMode = () => {
|
||||
const applyEinkMode = useCallback((isEink: boolean) => {
|
||||
if (isEink) {
|
||||
document.body.classList.add('no-transitions');
|
||||
} else {
|
||||
document.body.classList.remove('no-transitions');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { applyEinkMode };
|
||||
};
|
||||
@@ -132,6 +132,7 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
maxInlineSize: getDefaultMaxInlineSize(),
|
||||
maxBlockSize: getDefaultMaxBlockSize(),
|
||||
animated: false,
|
||||
isEink: false,
|
||||
writingMode: 'auto',
|
||||
vertical: false,
|
||||
rtl: false,
|
||||
@@ -141,8 +142,8 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
|
||||
export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
zoomLevel: 100,
|
||||
paragraphMargin: 1,
|
||||
lineHeight: 1.6,
|
||||
paragraphMargin: 0.6,
|
||||
lineHeight: 1.4,
|
||||
wordSpacing: 0,
|
||||
letterSpacing: 0,
|
||||
textIndent: 0,
|
||||
@@ -177,6 +178,8 @@ export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
fullJustification: true,
|
||||
textIndent: 2,
|
||||
paragraphMargin: 1,
|
||||
lineHeight: 1.6,
|
||||
};
|
||||
|
||||
export const DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
@@ -231,6 +234,7 @@ export const SERIF_FONTS = [
|
||||
'Bitter',
|
||||
'Literata',
|
||||
'Merriweather',
|
||||
'Roboto Slab',
|
||||
'Vollkorn',
|
||||
'Georgia',
|
||||
'Times New Roman',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AppService } from '@/types/system';
|
||||
import { parseSSMLMarks } from '@/utils/ssml';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { TTSGranularity, TTSHighlightOptions, TTSMark, TTSVoice } from './types';
|
||||
import { createRejecttFilter } from '@/utils/node';
|
||||
import { createRejectFilter } from '@/utils/node';
|
||||
import { WebSpeechClient } from './WebSpeechClient';
|
||||
import { NativeTTSClient } from './NativeTTSClient';
|
||||
import { EdgeTTSClient } from './EdgeTTSClient';
|
||||
@@ -85,13 +85,6 @@ export class TTSController extends EventTarget {
|
||||
const { style, color } = options;
|
||||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
overlayer?.add(HIGHLIGHT_KEY, range, Overlayer[style], { color });
|
||||
const rect = range.getBoundingClientRect();
|
||||
const { start, size, viewSize, sideProp } = this.view.renderer;
|
||||
const position = rect[sideProp === 'height' ? 'y' : 'x'] + 88;
|
||||
const offset = this.view.book.dir === 'rtl' ? viewSize - position : position;
|
||||
if (!this.view.renderer.scrolled || offset < start || offset > start + size) {
|
||||
this.view.renderer.scrollToAnchor(range);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,7 +102,7 @@ export class TTSController extends EventTarget {
|
||||
const highlightOptions: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
|
||||
await this.view.initTTS(
|
||||
granularity,
|
||||
createRejecttFilter({
|
||||
createRejectFilter({
|
||||
tags: ['rt', 'sup'],
|
||||
contents: [{ tag: 'a', content: /^\d+$/ }],
|
||||
}),
|
||||
|
||||
@@ -141,7 +141,12 @@ export const loadDataTheme = () => {
|
||||
const themeMode = localStorage.getItem('themeMode');
|
||||
const themeColor = localStorage.getItem('themeColor');
|
||||
if (themeMode && themeColor) {
|
||||
document.documentElement.setAttribute('data-theme', `${themeColor}-${themeMode}`);
|
||||
const systemIsDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme',
|
||||
`${themeColor}-${isDarkMode ? 'dark' : 'light'}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const basicGoogleFonts = [
|
||||
{ family: 'Noto Sans', weights: 'ital,wght@0,100..900;1,100..900' },
|
||||
{ family: 'Open Sans', weights: 'ital,wght@0,300..800;1,300..800' },
|
||||
{ family: 'Roboto', weights: 'ital,wght@0,100..900;1,100..900' },
|
||||
{ family: 'Roboto Slab', weights: 'ital,wght@0,100..900;1,100..900' },
|
||||
{ family: 'Vollkorn', weights: 'ital,wght@0,400..900;1,400..900' },
|
||||
];
|
||||
|
||||
@@ -33,7 +34,7 @@ const getAdditionalBasicFontLinks = () => `
|
||||
|
||||
const getAdditionalCJKFontLinks = () => `
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans-webfont@1.0.4/misans-l3/misans-l3/result.min.css" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cn-fontsource-lxgw-wen-kai-gb-screen@1.0.6/font.min.css" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lxgw-wenkai-screen-web/1.520.0/lxgwwenkaigbscreen/result.css" crossorigin="anonymous" />
|
||||
<link rel='stylesheet' href='https://chinese-fonts-cdn.netlify.app/packages/hwmct/dist/%E6%B1%87%E6%96%87%E6%98%8E%E6%9C%9D%E4%BD%93/result.css' crossorigin="anonymous" />
|
||||
<link rel='stylesheet' href='https://chinese-fonts-cdn.netlify.app/packages/jhlst/dist/%E4%BA%AC%E8%8F%AF%E8%80%81%E5%AE%8B%E4%BD%93v2_002/result.css' crossorigin="anonymous" />
|
||||
<link rel='stylesheet' href='https://chinese-fonts-cdn.netlify.app/packages/syst/dist/SourceHanSerifCN/result.css' crossorigin="anonymous" />
|
||||
|
||||
@@ -381,4 +381,8 @@ foliate-view {
|
||||
|
||||
.animate-reverse-spin {
|
||||
animation: spin-counterclockwise 1s linear;
|
||||
}
|
||||
|
||||
.no-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
@@ -105,6 +105,7 @@ export interface BookLayout {
|
||||
maxInlineSize: number;
|
||||
maxBlockSize: number;
|
||||
animated: boolean;
|
||||
isEink: boolean;
|
||||
writingMode: WritingMode;
|
||||
vertical: boolean;
|
||||
rtl: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const createRejecttFilter = ({
|
||||
export const createRejectFilter = ({
|
||||
tags = [],
|
||||
classes = [],
|
||||
attributes = [],
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
export const localizeNumber = (num: number, language: string, isIndex = false): string => {
|
||||
const lang = language.toLowerCase();
|
||||
const isChinese = lang.startsWith('zh');
|
||||
if (!isChinese) {
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
const isTraditional = lang.includes('tw') || lang.includes('hk');
|
||||
|
||||
const baseDigits = isTraditional ? '零壹貳叁肆伍陸柒捌玖' : '零一二三四五六七八九';
|
||||
const zeroChar = isIndex ? '〇' : '零';
|
||||
const digits = baseDigits.replace('零', zeroChar);
|
||||
const units = isTraditional
|
||||
? ['', '拾', '佰', '仟', '萬', '億']
|
||||
: ['', '十', '百', '千', '万', '亿'];
|
||||
|
||||
const str = String(num);
|
||||
const len = str.length;
|
||||
let result = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
const n = Number(str[i]);
|
||||
const pos = len - i - 1;
|
||||
if (n !== 0) {
|
||||
result += digits[n]! + units[pos % 4]!;
|
||||
} else if (!result.endsWith(zeroChar) && i < len - 1) {
|
||||
result += zeroChar;
|
||||
}
|
||||
if (pos === 4) result += units[4];
|
||||
if (pos === 8) result += units[5];
|
||||
}
|
||||
result = result
|
||||
.replace(new RegExp(`${zeroChar}+$`), '')
|
||||
.replace(`${zeroChar}萬`, '萬')
|
||||
.replace(`${zeroChar}万`, '万')
|
||||
.replace(new RegExp(`${zeroChar}+`, 'g'), zeroChar);
|
||||
|
||||
if (!isIndex) {
|
||||
if (isTraditional) {
|
||||
result = result.replace(
|
||||
/^貳$|(?<![壹貳叁肆伍陸柒捌玖])貳(?![佰仟萬億壹貳叁肆伍陸柒捌玖])/g,
|
||||
'兩',
|
||||
);
|
||||
} else {
|
||||
result = result.replace(
|
||||
/^二$|(?<![一二三四五六七八九])二(?![百千万亿一二三四五六七八九])/g,
|
||||
'两',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,14 +1,31 @@
|
||||
export function formatReadingProgress(
|
||||
import { localizeNumber } from './number';
|
||||
|
||||
export function formatProgress(
|
||||
current: number | undefined,
|
||||
total: number | undefined,
|
||||
template: string,
|
||||
localize: boolean = false,
|
||||
language: string = 'en',
|
||||
): string {
|
||||
if (current !== undefined && total !== undefined && total > 0 && current >= 0) {
|
||||
const currentStr = localize ? localizeNumber(current + 1, language, true) : String(current + 1);
|
||||
const totalStr = localize ? localizeNumber(total, language, true) : String(total);
|
||||
return template
|
||||
.replace('{current}', String(current + 1))
|
||||
.replace('{total}', String(total))
|
||||
.replace('{current}', currentStr)
|
||||
.replace('{total}', totalStr)
|
||||
.replace('{percent}', (((current + 1) / total) * 100).toFixed(1));
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function formatNumber(
|
||||
number: number | undefined,
|
||||
localize: boolean = false,
|
||||
language: string = 'en',
|
||||
): string {
|
||||
if (number === undefined || number < 0) {
|
||||
return '';
|
||||
}
|
||||
return localize ? localizeNumber(number, language) : String(number);
|
||||
}
|
||||
|
||||
@@ -249,9 +249,7 @@ const getLayoutStyles = (
|
||||
line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''};
|
||||
word-spacing: ${wordSpacing}px ${overrideLayout ? '!important' : ''};
|
||||
letter-spacing: ${letterSpacing}px ${overrideLayout ? '!important' : ''};
|
||||
text-indent: ${vertical ? textIndent * 1.2 : textIndent}em ${overrideLayout ? '!important' : ''};
|
||||
${justify ? `text-align: justify ${overrideLayout ? '!important' : ''};` : ''}
|
||||
${!justify && overrideLayout ? 'text-align: initial !important;' : ''};
|
||||
text-indent: ${textIndent}em ${overrideLayout ? '!important' : ''};
|
||||
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
-webkit-hyphenate-limit-before: 3;
|
||||
@@ -260,6 +258,18 @@ const getLayoutStyles = (
|
||||
hanging-punctuation: allow-end last;
|
||||
widows: 2;
|
||||
}
|
||||
p.aligned-center, blockquote.aligned-center,
|
||||
dd.aligned-center, div.aligned-center {
|
||||
text-align: center ${overrideLayout ? '!important' : ''};
|
||||
}
|
||||
p.aligned-right, blockquote.aligned-right,
|
||||
dd.aligned-right, div.aligned-right {
|
||||
text-align: right ${overrideLayout ? '!important' : ''};
|
||||
}
|
||||
p.aligned-justify, blockquote.aligned-justify,
|
||||
dd.aligned-justify, div.aligned-justify {
|
||||
${!justify && overrideLayout ? 'text-align: initial !important;' : ''};
|
||||
}
|
||||
p:has(> img:only-child), p:has(> span:only-child > img:only-child),
|
||||
p:has(> img:not(.has-text-siblings)),
|
||||
p:has(> a:first-child + img:last-child) {
|
||||
@@ -267,6 +277,8 @@ const getLayoutStyles = (
|
||||
}
|
||||
blockquote[align="center"], div[align="center"],
|
||||
p[align="center"], dd[align="center"],
|
||||
p.aligned-center, blockquote.aligned-center,
|
||||
dd.aligned-center, div.aligned-center,
|
||||
li p, ol p, ul p {
|
||||
text-indent: initial !important;
|
||||
}
|
||||
@@ -581,6 +593,19 @@ export const applyImageStyle = (document: Document) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const keepTextAlignment = (document: Document) => {
|
||||
document.querySelectorAll('div, p, blockquote, dd').forEach((el) => {
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
if (computedStyle.textAlign === 'center') {
|
||||
el.classList.add('aligned-center');
|
||||
} else if (computedStyle.textAlign === 'right') {
|
||||
el.classList.add('aligned-right');
|
||||
} else if (computedStyle.textAlign === 'justify') {
|
||||
el.classList.add('aligned-justify');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const applyFixedlayoutStyles = (
|
||||
document: Document,
|
||||
viewSettings: ViewSettings,
|
||||
|
||||
@@ -115,9 +115,10 @@ export class TxtToEpubConverter {
|
||||
[
|
||||
String.raw`第[零〇一二三四五六七八九十0-9][零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封本册部话])(?:[::、 \(\)0-9]*[^\n-]{0,24})(?!\S)`,
|
||||
String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论|后记)(?:[:: ][^\n-]{0,24})?(?!\S)`,
|
||||
String.raw`chapter[\s.]*[0-9]+(?:[::. ]+[^\n-]{0,50})?(?!\S)`,
|
||||
].join('|') +
|
||||
')',
|
||||
'gu',
|
||||
'gui',
|
||||
),
|
||||
);
|
||||
chapterRegexps.push(
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: e900daa533...7d1693ddd1
Reference in New Issue
Block a user