Compare commits

...

19 Commits

Author SHA1 Message Date
Huang Xin 98870d9f3e release: version 0.9.59 (#1429) 2025-06-19 17:10:14 +02:00
Huang Xin 31aeb2289c settings: more reasonable constraints for top and bottom margins (#1428) 2025-06-19 17:01:20 +02:00
Huang Xin 0f66ecbba1 layout: remove unnecessary inset for header on landscape screen in iOS (#1425) 2025-06-19 12:01:42 +02:00
Huang Xin f0d3c9dd4e fix: transform vw/vh to pixels correctly, closes #1420 (#1421)
* i18n: update translations

* fix: transform vw/vh to pixels correctly, closes #1420
2025-06-19 10:53:58 +02:00
Huang Xin 92aae340d9 settings: unset deprecated margin settings (#1419) 2025-06-18 18:56:41 +02:00
Huang Xin 3cf40039ee release: version 0.9.58 (#1418) 2025-06-18 18:23:09 +02:00
Huang Xin 7ed4937d9d settings: add an option to always show status bar (#1417) 2025-06-18 18:15:55 +02:00
Huang Xin b3593c0365 feat: support split-screen mode on iPad, closes #971 (#1416) 2025-06-18 15:54:46 +02:00
Huang Xin 6235202066 layout: fix issue where sidebars couldn’t be dragged to resize on iPad (#1415) 2025-06-18 15:43:51 +02:00
Huang Xin b3d1085ddf fix: now it's possible to have multiple columns in portrait mode, closes #1369 (#1413) 2025-06-18 14:25:32 +02:00
Huang Xin b2a4ddae60 feat: show annotation create time and various other UI fixes (#1412) 2025-06-18 13:17:18 +02:00
Huang Xin 42f50af27b tts: each book view now has its own tts controller (#1411) 2025-06-18 11:37:02 +02:00
Huang Xin 07f74d8858 settings: individually adjust top, bottom, left, and right margins on the reader page (#1410) 2025-06-18 10:55:24 +02:00
Huang Xin 9477789db1 layout: apply responsive safe area insets to the reader page, closes #1067 and closes #1212 (#1408)
This also fixes layout glitches when showing status bar on devices without display cutouts
2025-06-17 03:36:23 +02:00
Huang Xin 8a7d0e1b0d fix: annotation tools now works when TTS is enabled, closes #1403 (#1406) 2025-06-16 08:53:55 +02:00
Huang Xin 261ce95ed1 feat: add keyboard shortcut t to toggle TTS (#1405) 2025-06-16 08:50:10 +02:00
Huang Xin c5a3b44cbf chore: add NEXT_PUBLIC_API_BASE_URL in example env file (#1404) 2025-06-16 06:02:50 +02:00
Huang Xin ed68d25b15 fix: restore full view settings when reopening book (#1400) 2025-06-15 16:07:51 +02:00
Huang Xin f7f8872a13 tts: translation also when TTS is played in the background, closes #1383 (#1399) 2025-06-15 09:07:22 +02:00
79 changed files with 1147 additions and 631 deletions
+2
View File
@@ -6,6 +6,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824
NEXT_PUBLIC_API_BASE_URL=https://your-api-base-url.com
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.57",
"version": "0.9.59",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -63,6 +63,7 @@
"clsx": "^2.1.1",
"cors": "^2.8.5",
"cssbeautify": "^0.3.1",
"dayjs": "^1.11.13",
"foliate-js": "workspace:*",
"highlight.js": "^11.11.1",
"i18next": "^24.2.0",
@@ -114,8 +114,6 @@
"Your Library": "المكتبة الخاصة بك",
"TTS not supported for PDF": "القراءة الصوتية غير مدعومة لملفات PDF",
"Override Book Font": "تجاوز خط الكتاب",
"Vertical Margins (px)": "هوامش عمودية (بكسل)",
"Horizontal Margins (%)": "هوامش أفقية (%)",
"Apply to All Books": "تطبيق على جميع الكتب",
"Apply to This Book": "تطبيق على هذا الكتاب",
"Unable to fetch the translation. Try again later.": "تعذر جلب الترجمة. حاول مرة أخرى لاحقًا.",
@@ -266,7 +264,7 @@
"Clear Search": "مسح البحث",
"Header & Footer": "الترويسة والتذييل",
"Apply also in Scrolled Mode": "تطبيق أيضًا في وضع التمرير",
"A new version of Readest is Available!": "يتوفر إصدار جديد من Readest!",
"A new version of Readest is available!": "يتوفر إصدار جديد من Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "إصدار {{newVersion}} من ريديست متاح للتنزيل (الإصدار المثبت حاليًا {{currentVersion}}).",
"Download and install now?": "هل ترغب في تنزيله وتثبيته الآن؟",
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
@@ -371,5 +369,11 @@
"Contact Support": "اتصل بالدعم الفني",
"Code Highlighting": "تمييز الكود",
"Enable Highlighting": "تمكين التمييز",
"Code Language": "لغة الكود"
"Code Language": "لغة الكود",
"Top Margin (px)": "الهامش العلوي (بكسل)",
"Bottom Margin (px)": "الهامش السفلي (بكسل)",
"Right Margin (px)": "الهامش الأيمن (بكسل)",
"Left Margin (px)": "الهامش الأيسر (بكسل)",
"Column Gap (%)": "تباعد الأعمدة (%)",
"Always Show Status Bar": "دائمًا إظهار شريط الحالة"
}
@@ -114,8 +114,6 @@
"Your Library": "Ihre Bibliothek",
"TTS not supported for PDF": "TTS wird für PDF nicht unterstützt",
"Override Book Font": "Buch-Schriftart überschreiben",
"Vertical Margins (px)": "Vertikale Ränder (px)",
"Horizontal Margins (%)": "Horizontale Ränder (%)",
"Apply to All Books": "Auf alle Bücher anwenden",
"Apply to This Book": "Auf dieses Buch anwenden",
"Unable to fetch the translation. Try again later.": "Übersetzung konnte nicht abgerufen werden. Versuchen Sie es später erneut.",
@@ -266,7 +264,7 @@
"Clear Search": "Suche löschen",
"Header & Footer": "Kopf- & Fußzeile",
"Apply also in Scrolled Mode": "Auch im Scroll-Modus anwenden",
"A new version of Readest is Available!": "Eine neue Version von Readest ist verfügbar!",
"A new version of Readest is available!": "Eine neue Version von Readest ist verfügbar!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} ist verfügbar (installierte Version {{currentVersion}}).",
"Download and install now?": "Jetzt herunterladen und installieren?",
"Downloading {{downloaded}} of {{contentLength}}": "Herunterladen {{downloaded}} von {{contentLength}}",
@@ -359,5 +357,11 @@
"Contact Support": "Support kontaktieren",
"Code Highlighting": "Code-Hervorhebung",
"Enable Highlighting": "Code-Hervorhebung aktivieren",
"Code Language": "Programmiersprache"
"Code Language": "Programmiersprache",
"Top Margin (px)": "Oberer Rand (px)",
"Bottom Margin (px)": "Unterer Rand (px)",
"Right Margin (px)": "Rechter Rand (px)",
"Left Margin (px)": "Linker Rand (px)",
"Column Gap (%)": "Spaltenabstand (%)",
"Always Show Status Bar": "Statusleiste immer anzeigen"
}
@@ -114,8 +114,6 @@
"Your Library": "Η βιβλιοθήκη σας",
"TTS not supported for PDF": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται για αρχεία PDF",
"Override Book Font": "Παράκαμψη γραμματοσειράς βιβλίου",
"Vertical Margins (px)": "Κάθετα περιθώρια (px)",
"Horizontal Margins (%)": "Οριζόντια περιθώρια (%)",
"Apply to All Books": "Εφαρμογή σε όλα τα βιβλία",
"Apply to This Book": "Εφαρμογή σε αυτό το βιβλίο",
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
@@ -266,7 +264,7 @@
"Clear Search": "Καθαρισμός αναζήτησης",
"Header & Footer": "Κεφαλίδα & Υποσέλιδο",
"Apply also in Scrolled Mode": "Εφαρμογή και σε λειτουργία κύλισης",
"A new version of Readest is Available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
"A new version of Readest is available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Η έκδοση Readest {{newVersion}} είναι διαθέσιμη (έκδοση που είναι εγκατεστημένη {{currentVersion}}).",
"Download and install now?": "Θέλετε να την κατεβάσετε και να την εγκαταστήσετε τώρα;",
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
@@ -359,5 +357,11 @@
"Contact Support": "Επικοινωνήστε με την υποστήριξη",
"Code Highlighting": "Επισήμανση κώδικα",
"Enable Highlighting": "Ενεργοποίηση επισήμανσης",
"Code Language": "Γλώσσα κώδικα"
"Code Language": "Γλώσσα κώδικα",
"Top Margin (px)": "Πάνω περιθώριο (px)",
"Bottom Margin (px)": "Κάτω περιθώριο (px)",
"Right Margin (px)": "Δεξί περιθώριο (px)",
"Left Margin (px)": "Αριστερό περιθώριο (px)",
"Column Gap (%)": "Απόσταση στηλών (%)",
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης"
}
@@ -114,8 +114,6 @@
"Your Library": "Tu biblioteca",
"TTS not supported for PDF": "TTS no es compatible con PDF",
"Override Book Font": "Sobrescribir fuente del libro",
"Vertical Margins (px)": "Márgenes verticales (px)",
"Horizontal Margins (%)": "Márgenes horizontales (%)",
"Apply to All Books": "Aplicar a todos los libros",
"Apply to This Book": "Aplicar a este libro",
"Unable to fetch the translation. Try again later.": "No se puede obtener la traducción. Inténtalo de nuevo más tarde.",
@@ -266,7 +264,7 @@
"Clear Search": "Limpiar búsqueda",
"Header & Footer": "Encabezado y pie de página",
"Apply also in Scrolled Mode": "Aplicar también en modo desplazamiento",
"A new version of Readest is Available!": "¡Una nueva versión de Readest está disponible!",
"A new version of Readest is available!": "¡Una nueva versión de Readest está disponible!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponible (versión instalada {{currentVersion}}).",
"Download and install now?": "Descargar e instalar ahora?",
"Downloading {{downloaded}} of {{contentLength}}": "Descargando {{downloaded}} de {{contentLength}}",
@@ -362,5 +360,11 @@
"Contact Support": "Contactar con soporte",
"Code Highlighting": "Resaltado de código",
"Enable Highlighting": "Activar resaltado",
"Code Language": "Lenguaje de código"
"Code Language": "Lenguaje de código",
"Top Margin (px)": "Margen superior (px)",
"Bottom Margin (px)": "Margen inferior (px)",
"Right Margin (px)": "Margen derecho (px)",
"Left Margin (px)": "Margen izquierdo (px)",
"Column Gap (%)": "Espacio entre columnas (%)",
"Always Show Status Bar": "Mostrar siempre la barra de estado"
}
@@ -114,8 +114,6 @@
"Your Library": "Votre bibliothèque",
"TTS not supported for PDF": "La synthèse vocale n'est pas prise en charge pour les fichiers PDF",
"Override Book Font": "Remplacer la police du livre",
"Vertical Margins (px)": "Marges verticales (px)",
"Horizontal Margins (%)": "Marges horizontales (%)",
"Apply to All Books": "Appliquer à tous les livres",
"Apply to This Book": "Appliquer à ce livre",
"Unable to fetch the translation. Try again later.": "Impossible de récupérer la traduction. Réessayez plus tard.",
@@ -266,7 +264,7 @@
"Clear Search": "Effacer la recherche",
"Header & Footer": "En-tête et pied de page",
"Apply also in Scrolled Mode": "Appliquer également en mode défilement",
"A new version of Readest is Available!": "Avis de mise à jour",
"A new version of Readest is available!": "Avis de mise à jour",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} est disponible (version installée {{currentVersion}}).",
"Download and install now?": "Télécharger et installer maintenant ?",
"Downloading {{downloaded}} of {{contentLength}}": "Téléchargement {{downloaded}} sur {{contentLength}}",
@@ -362,5 +360,11 @@
"Contact Support": "Contacter le support",
"Code Highlighting": "Coloration syntaxique",
"Enable Highlighting": "Activer la coloration syntaxique",
"Code Language": "Langage de programmation"
"Code Language": "Langage de programmation",
"Top Margin (px)": "Marge supérieure (px)",
"Bottom Margin (px)": "Marge inférieure (px)",
"Right Margin (px)": "Marge droite (px)",
"Left Margin (px)": "Marge gauche (px)",
"Column Gap (%)": "Espacement des colonnes (%)",
"Always Show Status Bar": "Afficher toujours la barre d'état"
}
@@ -114,8 +114,6 @@
"Your Library": "आपकी लाइब्रेरी",
"TTS not supported for PDF": "PDF के लिए TTS समर्थित नहीं है",
"Override Book Font": "पुस्तक फ़ॉन्ट ओवरराइड करें",
"Vertical Margins (px)": "ऊर्ध्वाधर मार्जिन (px)",
"Horizontal Margins (%)": "क्षैतिज मार्जिन (%)",
"Apply to All Books": "सभी पुस्तकों पर लागू करें",
"Apply to This Book": "इस पुस्तक पर लागू करें",
"Unable to fetch the translation. Try again later.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
@@ -266,7 +264,7 @@
"Clear Search": "खोज साफ करें",
"Header & Footer": "हेडर और फ़ुटर",
"Apply also in Scrolled Mode": "स्क्रॉल मोड में भी लागू करें",
"A new version of Readest is Available!": "Readest का एक नया संस्करण उपलब्ध है!",
"A new version of Readest is available!": "Readest का एक नया संस्करण उपलब्ध है!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} उपलब्ध है (स्थापित संस्करण {{currentVersion}})।",
"Download and install now?": "अब डाउनलोड और इंस्टॉल करें?",
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
@@ -359,5 +357,11 @@
"Contact Support": "सहायता से संपर्क करें",
"Code Highlighting": "कोड हाइलाइटिंग",
"Enable Highlighting": "हाइलाइटिंग सक्षम करें",
"Code Language": "कोड भाषा"
"Code Language": "कोड भाषा",
"Top Margin (px)": "ऊपरी हाशिया (px)",
"Bottom Margin (px)": "निचला हाशिया (px)",
"Right Margin (px)": "दायां हाशिया (px)",
"Left Margin (px)": "बायां हाशिया (px)",
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं"
}
@@ -114,8 +114,6 @@
"Your Library": "Perpustakaan Anda",
"TTS not supported for PDF": "TTS tidak didukung untuk PDF",
"Override Book Font": "Ganti Font Buku",
"Vertical Margins (px)": "Margin Vertikal (px)",
"Horizontal Margins (%)": "Margin Horizontal (%)",
"Apply to All Books": "Terapkan ke semua buku",
"Apply to This Book": "Terapkan ke buku ini",
"Unable to fetch the translation. Try again later.": "Tidak dapat mengambil terjemahan. Coba lagi nanti.",
@@ -266,7 +264,7 @@
"Clear Search": "Hapus Pencarian",
"Header & Footer": "Header & Footer",
"Apply also in Scrolled Mode": "Terapkan juga di Mode Gulir",
"A new version of Readest is Available!": "Versi baru Readest Tersedia!",
"A new version of Readest is available!": "Versi baru Readest Tersedia!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} tersedia (versi terinstal {{currentVersion}}).",
"Download and install now?": "Unduh dan instal sekarang?",
"Downloading {{downloaded}} of {{contentLength}}": "Mengunduh {{downloaded}} dari {{contentLength}}",
@@ -356,5 +354,11 @@
"Contact Support": "Hubungi Dukungan",
"Code Highlighting": "Penyorotan Kode",
"Enable Highlighting": "Aktifkan Penyorotan",
"Code Language": "Bahasa Kode"
"Code Language": "Bahasa Kode",
"Top Margin (px)": "Margin Atas (px)",
"Bottom Margin (px)": "Margin Bawah (px)",
"Right Margin (px)": "Margin Kanan (px)",
"Left Margin (px)": "Margin Kiri (px)",
"Column Gap (%)": "Jarak Antar Kolom (%)",
"Always Show Status Bar": "Selalu Tampilkan Bilah Status"
}
@@ -114,8 +114,6 @@
"Your Library": "La tua biblioteca",
"TTS not supported for PDF": "TTS non supportato per PDF",
"Override Book Font": "Sovrascrivi font libro",
"Vertical Margins (px)": "Margini verticali (px)",
"Horizontal Margins (%)": "Margini orizzontali (%)",
"Apply to All Books": "Applica a tutti i libri",
"Apply to This Book": "Applica a questo libro",
"Unable to fetch the translation. Try again later.": "Impossibile recuperare la traduzione. Riprova più tardi.",
@@ -266,7 +264,7 @@
"Clear Search": "Cancella ricerca",
"Header & Footer": "Intestazione e piè di pagina",
"Apply also in Scrolled Mode": "Applica anche in modalità scorrimento",
"A new version of Readest is Available!": "È disponibile una nuova versione di Readest!",
"A new version of Readest is available!": "È disponibile una nuova versione di Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} è disponibile (versione installata {{currentVersion}}).",
"Download and install now?": "Scarica e installa ora?",
"Downloading {{downloaded}} of {{contentLength}}": "Scaricando {{downloaded}} di {{contentLength}}",
@@ -362,5 +360,11 @@
"Contact Support": "Contatta il supporto",
"Code Highlighting": "Evidenziazione codice",
"Enable Highlighting": "Abilita evidenziazione",
"Code Language": "Tipo di codice"
"Code Language": "Tipo di codice",
"Top Margin (px)": "Margine Superiore (px)",
"Bottom Margin (px)": "Margine Inferiore (px)",
"Right Margin (px)": "Margine Destro (px)",
"Left Margin (px)": "Margine Sinistro (px)",
"Column Gap (%)": "Spazio tra Colonne (%)",
"Always Show Status Bar": "Mostra sempre la barra di stato"
}
@@ -114,8 +114,6 @@
"Your Library": "ライブラリー",
"TTS not supported for PDF": "PDFではTTSがサポートされていません",
"Override Book Font": "書籍フォントを上書き",
"Vertical Margins (px)": "縦マージン(px",
"Horizontal Margins (%)": "横マージン(%",
"Apply to All Books": "すべての書籍に適用",
"Apply to This Book": "この書籍に適用",
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。",
@@ -266,7 +264,7 @@
"Clear Search": "検索をクリア",
"Header & Footer": "ヘッダーとフッター",
"Apply also in Scrolled Mode": "スクロールモードでも適用",
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
"A new version of Readest is available!": "新しいバージョンのReadestが利用可能です!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}が利用可能です(インストールされたバージョン{{currentVersion}})。",
"Download and install now?": "今すぐダウンロードしてインストールしますか?",
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
@@ -356,5 +354,11 @@
"Contact Support": "サポートに連絡",
"Code Highlighting": "コードハイライト",
"Enable Highlighting": "ハイライトを有効にする",
"Code Language": "コード言語"
"Code Language": "コード言語",
"Top Margin (px)": "上マージン (px)",
"Bottom Margin (px)": "下マージン (px)",
"Right Margin (px)": "右マージン (px)",
"Left Margin (px)": "左マージン (px)",
"Column Gap (%)": "列間の間隔 (%)",
"Always Show Status Bar": "ステータスバーを常に表示"
}
@@ -114,8 +114,6 @@
"Your Library": "내 서재",
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음",
"Override Book Font": "책 글꼴 덮어쓰기",
"Vertical Margins (px)": "수직 여백 (px)",
"Horizontal Margins (%)": "수평 여백 (%)",
"Apply to All Books": "모든 책에 적용",
"Apply to This Book": "이 책에 적용",
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
@@ -266,7 +264,7 @@
"Clear Search": "검색 지우기",
"Header & Footer": "헤더 및 푸터",
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
"A new version of Readest is available!": "Readest의 새 버전이 있습니다!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}가 사용 가능 (설치된 버전 {{currentVersion}}).",
"Download and install now?": "지금 다운로드하고 설치하시겠습니까?",
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
@@ -356,5 +354,11 @@
"Contact Support": "지원팀에 문의",
"Code Highlighting": "코드 하이라이팅",
"Enable Highlighting": "하이라이팅 활성화",
"Code Language": "코드 언어"
"Code Language": "코드 언어",
"Top Margin (px)": "위쪽 여백 (px)",
"Bottom Margin (px)": "아래쪽 여백 (px)",
"Right Margin (px)": "오른쪽 여백 (px)",
"Left Margin (px)": "왼쪽 여백 (px)",
"Column Gap (%)": "열 간격 (%)",
"Always Show Status Bar": "상태 표시줄 항상 표시"
}
@@ -169,8 +169,6 @@
"Hyphenation": "Woordafbreking",
"Override Book Layout": "Boekopmaak overschrijven",
"Page": "Pagina",
"Vertical Margins (px)": "Verticale marges (px)",
"Horizontal Margins (%)": "Horizontale marges (%)",
"Maximum Number of Columns": "Maximum aantal kolommen",
"Maximum Column Height": "Maximale kolomhoogte",
"Maximum Column Width": "Maximale kolombreedte",
@@ -277,7 +275,7 @@
"Description:": "Beschrijving:",
"No description available": "Geen beschrijving beschikbaar",
"Drop to Import Books": "Sleep om boeken te importeren",
"A new version of Readest is Available!": "Een nieuwe versie van Readest is beschikbaar!",
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} is beschikbaar (geïnstalleerde versie {{currentVersion}}).",
"Download and install now?": "Nu downloaden en installeren?",
"Downloading {{downloaded}} of {{contentLength}}": "Downloaden {{downloaded}} van {{contentLength}}",
@@ -359,5 +357,11 @@
"Contact Support": "Neem contact op met ondersteuning",
"Code Highlighting": "Code markering",
"Enable Highlighting": "Markering inschakelen",
"Code Language": "Code taal"
"Code Language": "Code taal",
"Top Margin (px)": "Bovenmarge (px)",
"Bottom Margin (px)": "Ondermarge (px)",
"Right Margin (px)": "Rechter marge (px)",
"Left Margin (px)": "Linker marge (px)",
"Column Gap (%)": "Kolomafstand (%)",
"Always Show Status Bar": "Statusbalk altijd weergeven"
}
@@ -114,8 +114,6 @@
"Your Library": "Twoja biblioteka",
"TTS not supported for PDF": "TTS nie jest obsługiwane dla plików PDF",
"Override Book Font": "Nadpisz czcionkę książki",
"Vertical Margins (px)": "Marginesy pionowe (px)",
"Horizontal Margins (%)": "Marginesy poziome (%)",
"Apply to All Books": "Zastosuj do wszystkich książek",
"Apply to This Book": "Zastosuj do tej książki",
"Unable to fetch the translation. Try again later.": "Nie udało się pobrać tłumaczenia. Spróbuj ponownie później.",
@@ -266,7 +264,7 @@
"Clear Search": "Wyczyść wyszukiwanie",
"Header & Footer": "Nagłówek i stopka",
"Apply also in Scrolled Mode": "Zastosuj również w trybie przewijania",
"A new version of Readest is Available!": "Nowa wersja Readest jest dostępna!",
"A new version of Readest is available!": "Nowa wersja Readest jest dostępna!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} jest dostępny (zainstalowana wersja {{currentVersion}}).",
"Download and install now?": "Pobierz i zainstaluj teraz?",
"Downloading {{downloaded}} of {{contentLength}}": "Pobieranie {{downloaded}} z {{contentLength}}",
@@ -365,5 +363,11 @@
"Contact Support": "Skontaktuj się z pomocą techniczną",
"Code Highlighting": "Podświetlanie kodu",
"Enable Highlighting": "Zezwól na podświetlanie",
"Code Language": "Język kodu"
"Code Language": "Język kodu",
"Top Margin (px)": "Górny margines (px)",
"Bottom Margin (px)": "Dolny margines (px)",
"Right Margin (px)": "Prawy margines (px)",
"Left Margin (px)": "Lewy margines (px)",
"Column Gap (%)": "Odstęp między kolumnami (%)",
"Always Show Status Bar": "Zawsze pokazuj pasek stanu"
}
@@ -114,8 +114,6 @@
"Your Library": "Sua Biblioteca",
"TTS not supported for PDF": "TTS não suportado para PDF",
"Override Book Font": "Sobrescrever Fonte do Livro",
"Vertical Margins (px)": "Margens Verticais (px)",
"Horizontal Margins (%)": "Margens Horizontais (%)",
"Apply to All Books": "Aplicar a todos os livros",
"Apply to This Book": "Aplicar a este livro",
"Unable to fetch the translation. Try again later.": "Não foi possível buscar a tradução. Tente novamente mais tarde.",
@@ -266,7 +264,7 @@
"Clear Search": "Limpar Pesquisa",
"Header & Footer": "Cabeçalho e Rodapé",
"Apply also in Scrolled Mode": "Aplicar também no Modo Rolado",
"A new version of Readest is Available!": "Uma nova versão do Readest está disponível!",
"A new version of Readest is available!": "Uma nova versão do Readest está disponível!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponível (versão instalada {{currentVersion}}).",
"Download and install now?": "Baixar e instalar agora?",
"Downloading {{downloaded}} of {{contentLength}}": "Baixando {{downloaded}} de {{contentLength}}",
@@ -362,5 +360,11 @@
"Contact Support": "Contato com Suporte",
"Code Highlighting": "Realce de Código",
"Enable Highlighting": "Ativar Realce",
"Code Language": "Idioma do Código"
"Code Language": "Idioma do Código",
"Top Margin (px)": "Margem superior (px)",
"Bottom Margin (px)": "Margem inferior (px)",
"Right Margin (px)": "Margem direita (px)",
"Left Margin (px)": "Margem esquerda (px)",
"Column Gap (%)": "Espaço entre colunas (%)",
"Always Show Status Bar": "Exibir sempre a barra de status"
}
@@ -114,8 +114,6 @@
"Your Library": "Ваша библиотека",
"TTS not supported for PDF": "TTS не поддерживается для PDF",
"Override Book Font": "Переопределить шрифт книги",
"Vertical Margins (px)": "Вертикальные поля (px)",
"Horizontal Margins (%)": "Горизонтальные поля (%)",
"Apply to All Books": "Применить ко всем книгам",
"Apply to This Book": "Применить к этой книге",
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже.",
@@ -266,7 +264,7 @@
"Clear Search": "Очистить поиск",
"Header & Footer": "Заголовок и нижний колонтитул",
"Apply also in Scrolled Mode": "Применить также в прокручиваемом режиме",
"A new version of Readest is Available!": "Доступна новая версия Readest!",
"A new version of Readest is available!": "Доступна новая версия Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступен (установленная версия {{currentVersion}}).",
"Download and install now?": "Скачать и установить сейчас?",
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
@@ -365,5 +363,11 @@
"Contact Support": "Связаться с поддержкой",
"Code Highlighting": "Подсветка кода",
"Enable Highlighting": "Включить подсветку",
"Code Language": "Язык кода"
"Code Language": "Язык кода",
"Top Margin (px)": "Верхний отступ (пикс)",
"Bottom Margin (px)": "Нижний отступ (пикс)",
"Right Margin (px)": "Правый отступ (пикс)",
"Left Margin (px)": "Левый отступ (пикс)",
"Column Gap (%)": "Промежуток между колонками (%)",
"Always Show Status Bar": "Всегда показывать панель состояния"
}
@@ -114,8 +114,6 @@
"Your Library": "Kütüphaneniz",
"TTS not supported for PDF": "PDF için TTS desteklenmiyor",
"Override Book Font": "Kitap Yazı Tipini Geçersiz Kıl",
"Vertical Margins (px)": "Dikey Kenar Boşlukları (px)",
"Horizontal Margins (%)": "Yatay Kenar Boşlukları (%)",
"Apply to All Books": "Tüm kitaplara uygula",
"Apply to This Book": "Bu kitaba uygula",
"Unable to fetch the translation. Try again later.": "Çeviri alınamıyor. Daha sonra tekrar deneyin.",
@@ -266,7 +264,7 @@
"Clear Search": "Aramayı Temizle",
"Header & Footer": "Başlık ve Altbilgi",
"Apply also in Scrolled Mode": "Kaydırılmış Modda da Uygula",
"A new version of Readest is Available!": "Yeni bir Readest sürümü mevcut!",
"A new version of Readest is available!": "Yeni bir Readest sürümü mevcut!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} mevcut (yüklü sürüm {{currentVersion}}).",
"Download and install now?": "Şimdi indirip yükleyelim mi?",
"Downloading {{downloaded}} of {{contentLength}}": "İndiriliyor {{downloaded}}/{{contentLength}}",
@@ -359,5 +357,11 @@
"Contact Support": "Destekle İletişime Geç",
"Code Highlighting": "Code Vurgulama",
"Enable Highlighting": "Vurgulamayı Etkinleştir",
"Code Language": "Code Dili"
"Code Language": "Code Dili",
"Top Margin (px)": "Üst Kenar Boşluğu (px)",
"Bottom Margin (px)": "Alt Kenar Boşluğu (px)",
"Right Margin (px)": "Sağ Kenar Boşluğu (px)",
"Left Margin (px)": "Sol Kenar Boşluğu (px)",
"Column Gap (%)": "Sütun Aralığı (%)",
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster"
}
@@ -114,8 +114,6 @@
"Your Library": "Ваша бібліотека",
"TTS not supported for PDF": "TTS не підтримується для PDF",
"Override Book Font": "Перевизначити шрифт книги",
"Vertical Margins (px)": "Вертикальні відступи (px)",
"Horizontal Margins (%)": "Горизонтальні відступи (%)",
"Apply to All Books": "Застосувати до всіх книг",
"Apply to This Book": "Застосувати до цієї книги",
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
@@ -266,7 +264,7 @@
"Clear Search": "Очистити пошук",
"Header & Footer": "Заголовок і нижній колонтитул",
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
"A new version of Readest is Available!": "Доступна нова версія Readest!",
"A new version of Readest is available!": "Доступна нова версія Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
"Download and install now?": "Завантажити та встановити зараз?",
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
@@ -365,5 +363,11 @@
"Contact Support": "Зв'язатися з підтримкою",
"Code Highlighting": "Підсвічування коду",
"Enable Highlighting": "Увімкнути підсвічування",
"Code Language": "Мова коду"
"Code Language": "Мова коду",
"Top Margin (px)": "Верхній відступ (px)",
"Bottom Margin (px)": "Нижній відступ (px)",
"Right Margin (px)": "Правий відступ (px)",
"Left Margin (px)": "Лівий відступ (px)",
"Column Gap (%)": "Проміжок між стовпцями (%)",
"Always Show Status Bar": "Завжди показувати панель стану"
}
@@ -114,8 +114,6 @@
"Your Library": "Thư viện của bạn",
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF",
"Override Book Font": "Ghi đè phông chữ sách",
"Vertical Margins (px)": "Lề dọc (px)",
"Horizontal Margins (%)": "Lề ngang (%)",
"Apply to All Books": "Áp dụng cho tất cả sách",
"Apply to This Book": "Áp dụng cho sách này",
"Unable to fetch the translation. Try again later.": "Không thể tải dịch vụ. Vui lòng thử lại sau.",
@@ -266,7 +264,7 @@
"Clear Search": "Xóa tìm kiếm",
"Header & Footer": "Đầu trang & Chân trang",
"Apply also in Scrolled Mode": "Áp dụng cũng trong chế độ cuộn",
"A new version of Readest is Available!": "Đã có phiên bản mới của Readest!",
"A new version of Readest is available!": "Đã có phiên bản mới của Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} đã có sẵn (phiên bản đã cài đặt {{currentVersion}}).",
"Download and install now?": "Tải xuống và cài đặt ngay bây giờ?",
"Downloading {{downloaded}} of {{contentLength}}": "Đang tải {{downloaded}} trong {{contentLength}}",
@@ -356,5 +354,11 @@
"Contact Support": "Liên hệ hỗ trợ",
"Code Highlighting": "Đánh dấu mã",
"Enable Highlighting": "Cho phép đánh dấu",
"Code Language": "Ngôn ngữ mã"
"Code Language": "Ngôn ngữ mã",
"Top Margin (px)": "Lề trên (px)",
"Bottom Margin (px)": "Lề dưới (px)",
"Right Margin (px)": "Lề phải (px)",
"Left Margin (px)": "Lề trái (px)",
"Column Gap (%)": "Khoảng cách cột (%)",
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái"
}
@@ -114,8 +114,6 @@
"Your Library": "书库",
"TTS not supported for PDF": "PDF 文档暂不支持 TTS",
"Override Book Font": "覆盖书籍字体",
"Vertical Margins (px)": "垂直边距(px",
"Horizontal Margins (%)": "水平边距(%",
"Apply to All Books": "应用于所有书籍",
"Apply to This Book": "应用于此书籍",
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。",
@@ -266,7 +264,7 @@
"Clear Search": "清除搜索",
"Header & Footer": "页眉页脚",
"Apply also in Scrolled Mode": "应用到滚动模式",
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安装版本 {{currentVersion}})。",
"Download and install now?": "立即下载并安装?",
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
@@ -356,5 +354,11 @@
"Contact Support": "联系客服",
"Code Highlighting": "代码高亮",
"Enable Highlighting": "启用高亮",
"Code Language": "代码语言"
"Code Language": "代码语言",
"Top Margin (px)": "上边距",
"Bottom Margin (px)": "下边距",
"Right Margin (px)": "右边距",
"Left Margin (px)": "左边距",
"Column Gap (%)": "列间距",
"Always Show Status Bar": "始终显示状态栏"
}
@@ -114,8 +114,6 @@
"Your Library": "書庫",
"TTS not supported for PDF": "PDF 文檔暫不支持 TTS",
"Override Book Font": "覆蓋書籍字體",
"Vertical Margins (px)": "垂直邊距(px",
"Horizontal Margins (%)": "水平邊距(%",
"Apply to All Books": "應用於所有書籍",
"Apply to This Book": "應用於此書籍",
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。",
@@ -266,7 +264,7 @@
"Clear Search": "清除搜索",
"Header & Footer": "頁眉頁腳",
"Apply also in Scrolled Mode": "滾動模式同樣適用",
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安裝版本 {{currentVersion}})。",
"Download and install now?": "立即下載並安裝?",
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
@@ -356,5 +354,11 @@
"Contact Support": "聯繫支援",
"Code Highlighting": "程式碼高亮",
"Enable Highlighting": "啟用高亮",
"Code Language": "程式語言"
"Code Language": "程式語言",
"Top Margin (px)": "上邊距",
"Bottom Margin (px)": "下邊距",
"Right Margin (px)": "右邊距",
"Left Margin (px)": "左邊距",
"Column Gap (%)": "欄間距",
"Always Show Status Bar": "始終顯示狀態欄"
}
+24
View File
@@ -1,5 +1,29 @@
{
"releases": {
"0.9.59": {
"date": "2025-06-19",
"notes": [
"Layout: Image size now adjusts more accurately by converting screen units to pixels properly",
"Layout: Improved header layout on iOS landscape mode — extra spacing has been removed",
"Settings: Top and bottom margin settings are now more intuitive and better constrained"
]
},
"0.9.58": {
"date": "2025-06-18",
"notes": [
"TTS: Translation now works even when TTS is playing in the background",
"TTS: Press T on your keyboard to quickly toggle TTS on or off",
"TTS: Each open book now has its own independent TTS controller",
"Annotations: Annotation tools now function properly while TTS is active",
"Settings: Margins for top, bottom, left, and right can now be adjusted individually",
"Settings: New option to always show the status bar while reading",
"Layout: Reader page now adapts better to device safe areas for a more comfortable layout",
"UI: Added creation timestamps to annotations and made various interface improvements",
"iPad: Multiple columns are now supported in portrait mode",
"iPad: Fixed issue where sidebars couldn't be resized by dragging",
"iPad: Added support for split-screen mode"
]
},
"0.9.57": {
"date": "2025-06-14",
"notes": ["TTS: Fixed an issue to detect custom TTS engines on some Android devices"]
+2 -2
View File
@@ -11,7 +11,7 @@
<key>UIHomeIndicatorAutoHidden</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
<false/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
@@ -198,4 +198,4 @@
</dict>
</array>
</dict>
</plist>
</plist>
+2 -3
View File
@@ -18,14 +18,13 @@ import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { isTauriAppPlatform } from '@/services/environment';
import { getBaseUrl, isTauriAppPlatform } from '@/services/environment';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
import { openUrl } from '@tauri-apps/plugin-opener';
import { handleAuthCallback } from '@/helpers/auth';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
import { READEST_WEB_BASE_URL } from '@/services/constants';
import WindowButtons from '@/components/WindowButtons';
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
@@ -42,7 +41,7 @@ interface ProviderLoginProp {
label: string;
}
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
const WEB_AUTH_CALLBACK = `${getBaseUrl()}/auth/callback`;
const DEEPLINK_CALLBACK = 'readest://auth-callback';
const USE_APPLE_SIGN_IN = process.env['NEXT_PUBLIC_USE_APPLE_SIGN_IN'] === 'true';
+1 -1
View File
@@ -19,7 +19,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
useEffect(() => {
setBrowserInfo(parseWebViewVersion(appService));
posthog.captureException(error);
}, [error]);
}, [appService, error]);
const handleGoHome = () => {
window.location.href = '/library';
@@ -13,6 +13,7 @@ import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { navigateToLibrary } from '@/utils/nav';
import { debounce } from '@/utils/debounce';
@@ -44,7 +45,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const router = useRouter();
const searchParams = useSearchParams();
const { appService } = useEnv();
const { statusBarHeight } = useThemeStore();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const { currentBookshelf } = useLibraryStore();
const {
isTrafficLightVisible,
@@ -58,6 +59,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const headerRef = useRef<HTMLDivElement>(null);
const iconSize18 = useResponsiveSize(18);
const iconSize20 = useResponsiveSize(20);
const insets = useSafeAreaInsets();
useShortcuts({
onToggleSelectMode,
@@ -102,6 +104,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
0,
);
if (!insets) return null;
return (
<div
ref={headerRef}
@@ -111,8 +115,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
)}
style={{
marginTop: appService?.hasSafeAreaInset
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: '',
? `max(${insets.top}px, ${systemUIVisible ? statusBarHeight : 0}px)`
: '0px',
}}
>
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
@@ -38,6 +38,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
const [isAlwaysShowStatusBar, setIsAlwaysShowStatusBar] = useState(settings.alwaysShowStatusBar);
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
const [isOpenLastBooks, setIsOpenLastBooks] = useState(settings.openLastBooks);
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
@@ -92,6 +93,14 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const toggleAlwaysShowStatusBar = () => {
settings.alwaysShowStatusBar = !settings.alwaysShowStatusBar;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAlwaysShowStatusBar(settings.alwaysShowStatusBar);
setIsDropdownOpen?.(false);
};
const toggleAutoUploadBooks = () => {
settings.autoUpload = !settings.autoUpload;
setSettings(settings);
@@ -214,6 +223,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
onClick={toggleAlwaysOnTop}
/>
)}
{appService?.isMobileApp && (
<MenuItem
label={_('Always Show Status Bar')}
Icon={isAlwaysShowStatusBar ? MdCheck : undefined}
onClick={toggleAlwaysShowStatusBar}
/>
)}
<MenuItem
label={_('Keep Screen Awake')}
Icon={isScreenWakeLock ? MdCheck : undefined}
+8 -9
View File
@@ -28,7 +28,7 @@ import { usePullToRefresh } from '@/hooks/usePullToRefresh';
import { useTheme } from '@/hooks/useTheme';
import { useDemoBooks } from './hooks/useDemoBooks';
import { useBooksSync } from './hooks/useBooksSync';
import { useThemeStore } from '@/store/themeStore';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { lockScreenOrientation } from '@/utils/bridge';
@@ -69,8 +69,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
} = useLibraryStore();
const _ = useTranslation();
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
const insets = useSafeAreaInsets();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { statusBarHeight } = useThemeStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
const [libraryLoaded, setLibraryLoaded] = useState(false);
@@ -559,7 +559,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
setShowDetailsBook(book);
};
if (!appService) {
if (!appService || !insets) {
return null;
}
@@ -582,7 +582,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<div className='fixed top-0 z-40 w-full'>
<div className='top-0 z-40 w-full'>
<LibraryHeader
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
@@ -603,14 +603,13 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
ref={containerRef}
className={clsx(
'scroll-container drop-zone flex-grow overflow-y-auto',
appService?.hasSafeAreaInset && 'pt-[52px]',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
isDragging && 'drag-over',
)}
style={{
marginTop: appService?.hasSafeAreaInset
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: '48px',
paddingTop: '0px',
paddingRight: `${insets.right}px`,
paddingBottom: `${insets.bottom}px`,
paddingLeft: `${insets.left}px`,
}}
>
<DropIndicator />
@@ -4,10 +4,12 @@ import React, { useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { getGridTemplate, getInsetEdges } from '@/utils/grid';
import { getViewInsets } from '@/utils/insets';
import FoliateViewer from './FoliateViewer';
import getGridTemplate from '@/utils/grid';
import SectionInfo from './SectionInfo';
import HeaderBar from './HeaderBar';
import FooterBar from './FooterBar';
@@ -18,7 +20,6 @@ import Annotator from './annotator/Annotator';
import FootnotePopup from './FootnotePopup';
import HintInfo from './HintInfo';
import DoubleBorder from './DoubleBorder';
import TTSControl from './tts/TTSControl';
interface BooksGridProps {
bookKeys: string[];
@@ -28,10 +29,14 @@ interface BooksGridProps {
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings, hoveredBookKey } = useReaderStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { getGridInsets, setGridInsets, hoveredBookKey } = useReaderStore();
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
const screenInsets = useSafeAreaInsets();
const aspectRatio = window.innerWidth / window.innerHeight;
const gridTemplate = getGridTemplate(bookKeys.length, aspectRatio);
useEffect(() => {
if (!sideBarBookKey) return;
@@ -41,6 +46,23 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarBookKey]);
useEffect(() => {
if (!screenInsets) return;
bookKeys.forEach((bookKey, index) => {
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
const gridInsets = {
top: top ? screenInsets.top : 0,
right: right ? screenInsets.right : 0,
bottom: bottom ? screenInsets.bottom : 0,
left: left ? screenInsets.left : 0,
};
setGridInsets(bookKey, gridInsets);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bookKeys, screenInsets]);
if (!screenInsets) return null;
return (
<div
className={clsx('relative grid h-full flex-grow')}
@@ -54,13 +76,20 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const gridInsets = getGridInsets(bookKey);
const { book, bookDoc } = bookData || {};
if (!book || !config || !bookDoc || !viewSettings) return null;
if (!book || !config || !bookDoc || !viewSettings || !gridInsets) return null;
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
const horizontalGapPercent = viewSettings.gapPercent;
const verticalMarginPixels = viewSettings.marginPx;
const viewInsets = getViewInsets(viewSettings);
const contentInsets = {
top: gridInsets.top + viewInsets.top,
right: gridInsets.right + viewInsets.right,
bottom: gridInsets.bottom + viewInsets.bottom,
left: gridInsets.left + viewInsets.left,
};
const scrolled = viewSettings.scrolled;
const showBarsOnScroll = viewSettings.showBarsOnScroll;
const showHeader = viewSettings.showHeader && (scrolled ? showBarsOnScroll : true);
@@ -72,10 +101,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
key={bookKey}
className={clsx(
'relative h-full w-full overflow-hidden',
index === 0 &&
!viewSettings?.scrolled &&
appService?.hasSafeAreaInset &&
'pt-[env(safe-area-inset-top)]',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
)}
>
@@ -87,16 +112,22 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isHoveredAnim={bookKeys.length > 2}
onCloseBook={onCloseBook}
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
gridInsets={gridInsets}
/>
<FoliateViewer
bookKey={bookKey}
bookDoc={bookDoc}
config={config}
contentInsets={contentInsets}
/>
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
{viewSettings.vertical && viewSettings.scrolled && (
<>
{(showFooter || viewSettings.doubleBorder) && (
<div
className='bg-base-100 absolute left-0 top-0 h-full'
style={{
width: `calc(${horizontalGapPercent}%)`,
height: `calc(100% - ${verticalMarginPixels}px)`,
width: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px)`,
height: `calc(100%)`,
}}
/>
)}
@@ -104,8 +135,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<div
className='bg-base-100 absolute right-0 top-0 h-full'
style={{
width: `calc(${horizontalGapPercent}%)`,
height: `calc(100% - ${verticalMarginPixels}px)`,
width: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px)`,
height: `calc(100%)`,
}}
/>
)}
@@ -117,7 +148,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
showFooter={showFooter}
borderColor={viewSettings.borderColor}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
/>
)}
{showHeader && (
@@ -127,7 +158,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isScrolled={viewSettings.scrolled}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
gridInsets={gridInsets}
/>
)}
<HintInfo
@@ -136,7 +168,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isScrolled={viewSettings.scrolled}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
gridInsets={gridInsets}
/>
{showFooter && (
<ProgressInfoView
@@ -146,7 +179,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
pageinfo={pageinfo}
timeinfo={timeinfo}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
gridInsets={gridInsets}
/>
)}
<Annotator bookKey={bookKey} />
@@ -157,12 +191,12 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
section={section}
pageinfo={pageinfo}
isHoveredAnim={false}
gridInsets={gridInsets}
/>
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
</div>
);
})}
<TTSControl />
</div>
);
};
@@ -1,31 +1,32 @@
import { Insets } from '@/types/misc';
interface DoubleBorderProps {
borderColor: string;
horizontalGap: number;
verticalMargin: number;
showHeader: boolean;
showFooter: boolean;
contentInsets: Insets;
}
const paddingPx = 10;
const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderColor,
horizontalGap,
verticalMargin,
showHeader,
showFooter,
contentInsets,
}) => {
return (
<>
<div>
{/* outter frame */}
<div
className={'borderframe pointer-events-none absolute'}
style={{
border: `4px solid ${borderColor}`,
height: `calc(100% - ${verticalMargin * 2}px + ${paddingPx * 2}px)`,
top: `calc(${verticalMargin}px - ${paddingPx}px)`,
left: `calc(${horizontalGap}% - ${showFooter ? 32 : 0}px - ${paddingPx}px)`,
right: `calc(${horizontalGap}% - ${showHeader ? 32 : 0}px - ${paddingPx}px)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px + ${paddingPx * 2}px)`,
top: `calc(${contentInsets.top}px - ${paddingPx}px)`,
left: `calc(${contentInsets.left}px - ${paddingPx}px)`,
right: `calc(${contentInsets.right}px - ${paddingPx}px)`,
}}
></div>
{/* inner frame */}
@@ -33,10 +34,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
className={'borderframe pointer-events-none absolute'}
style={{
border: `1px solid ${borderColor}`,
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: showFooter ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
right: showHeader ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px`,
right: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px`,
}}
/>
{/* footer */}
@@ -48,9 +49,9 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderBottom: `1px solid ${borderColor}`,
borderLeft: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(${horizontalGap}% - 32px)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(${contentInsets.left}px)`,
}}
/>
)}
@@ -63,13 +64,13 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderBottom: `1px solid ${borderColor}`,
borderRight: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(100% - ${horizontalGap}%)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(100% - ${contentInsets.right}px - 32px)`,
}}
/>
)}
</>
</div>
);
};
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { BookDoc, getDirection } from '@/libs/document';
import { BookConfig } from '@/types/book';
import { FoliateView, wrappedFoliateView } from '@/types/view';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
@@ -38,18 +39,21 @@ const FoliateViewer: React.FC<{
bookKey: string;
bookDoc: BookDoc;
config: BookConfig;
}> = ({ bookKey, bookDoc, config }) => {
const { appService } = useEnv();
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<FoliateView | null>(null);
const isViewCreated = useRef(false);
contentInsets: Insets;
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
const { getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { getBookData } = useBookDataStore();
const { appService } = useEnv();
const { themeCode, isDarkMode } = useThemeStore();
const viewSettings = getViewSettings(bookKey);
const viewRef = useRef<FoliateView | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isViewCreated = useRef(false);
const [toastMessage, setToastMessage] = useState('');
useEffect(() => {
const timer = setTimeout(() => setToastMessage(''), 2000);
return () => clearTimeout(timer);
@@ -80,7 +84,7 @@ const FoliateViewer: React.FC<{
.then((data) => {
const viewSettings = getViewSettings(bookKey);
if (viewSettings && detail.type === 'text/css')
return transformStylesheet(viewSettings, width, height, data);
return transformStylesheet(width, height, data);
if (viewSettings && detail.type === 'application/xhtml+xml') {
const ctx = {
bookKey,
@@ -166,14 +170,6 @@ const FoliateViewer: React.FC<{
onRendererRelocate: docRelocateHandler,
});
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode]);
useEffect(() => {
if (isViewCreated.current) return;
isViewCreated.current = true;
@@ -186,10 +182,6 @@ const FoliateViewer: React.FC<{
document.body.append(view);
containerRef.current?.appendChild(view);
const containerRect = containerRef.current?.getBoundingClientRect();
const width = containerRect?.width || window.innerWidth;
const height = containerRect?.height || window.innerHeight;
const viewSettings = getViewSettings(bookKey)!;
const writingMode = viewSettings.writingMode;
if (writingMode) {
@@ -215,15 +207,13 @@ const FoliateViewer: React.FC<{
detail.allowScript = viewSettings.allowScript ?? false;
}
});
const viewWidth = appService?.isMobile ? screen.width : window.innerWidth;
const viewHeight = appService?.isMobile ? screen.height : window.innerHeight;
const width = viewWidth - insets.left - insets.right;
const height = viewHeight - insets.top - insets.bottom;
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
view.renderer.setStyles?.(getStyles(viewSettings));
const isScrolled = viewSettings.scrolled!;
const showHeader = viewSettings.showHeader!;
const showFooter = viewSettings.showFooter!;
const isCompact = !showHeader && !showFooter;
const marginPx = isCompact ? viewSettings.compactMarginPx : viewSettings.marginPx;
const gapPercent = isCompact ? viewSettings.compactGapPercent : viewSettings.gapPercent;
const animated = viewSettings.animated!;
const maxColumnCount = viewSettings.maxColumnCount!;
const maxInlineSize = getMaxInlineSize(viewSettings);
@@ -237,12 +227,10 @@ const FoliateViewer: React.FC<{
} else {
view.renderer.removeAttribute('animated');
}
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
view.renderer.setAttribute('margin', `${marginPx}px`);
view.renderer.setAttribute('gap', `${gapPercent}%`);
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;
if (lastLocation) {
@@ -256,15 +244,58 @@ const FoliateViewer: React.FC<{
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const applyMarginAndGap = () => {
const viewSettings = getViewSettings(bookKey)!;
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
viewRef.current?.renderer.setAttribute('margin-top', `${insets.top + moreTopInset}px`);
viewRef.current?.renderer.setAttribute('margin-right', `${insets.right + moreRightInset}px`);
viewRef.current?.renderer.setAttribute('margin-bottom', `${insets.bottom + moreBottomInset}px`);
viewRef.current?.renderer.setAttribute('margin-left', `${insets.left + moreLeftInset}px`);
viewRef.current?.renderer.setAttribute('gap', `${viewSettings.gapPercent}%`);
if (viewSettings.scrolled) {
viewRef.current?.renderer.setAttribute('flow', 'scrolled');
}
};
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode]);
useEffect(() => {
if (viewRef.current && viewRef.current.renderer && viewSettings) {
applyMarginAndGap();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
insets.top,
insets.right,
insets.bottom,
insets.left,
viewSettings?.doubleBorder,
viewSettings?.showHeader,
viewSettings?.showFooter,
]);
return (
<>
<div
ref={containerRef}
className='foliate-viewer h-[100%] w-[100%]'
{...mouseHandlers}
{...touchHandlers}
/>
</>
<div
ref={containerRef}
className='foliate-viewer h-[100%] w-[100%]'
{...mouseHandlers}
{...touchHandlers}
/>
);
};
@@ -21,8 +21,10 @@ import { eventDispatcher } from '@/utils/event';
import { viewPagination } from '../hooks/usePagination';
import { saveViewSettings } from '../utils/viewSettingsHelper';
import { PageInfo } from '@/types/book';
import { Insets } from '@/types/misc';
import Button from '@/components/Button';
import Slider from '@/components/Slider';
import TTSControl from './tts/TTSControl';
interface FooterBarProps {
bookKey: string;
@@ -30,6 +32,7 @@ interface FooterBarProps {
section?: PageInfo;
pageinfo?: PageInfo;
isHoveredAnim: boolean;
gridInsets: Insets;
}
const FooterBar: React.FC<FooterBarProps> = ({
@@ -38,6 +41,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
section,
pageinfo,
isHoveredAnim,
gridInsets,
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
@@ -64,9 +68,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
};
const handleMarginChange = (value: number) => {
const viewSettings = getViewSettings(bookKey)!;
const marginPx = Math.round((value / 100) * 88);
const gapPercent = Math.round((value / 100) * 10);
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx, false, false);
viewSettings.marginTopPx = marginPx;
viewSettings.marginBottomPx = marginPx / 2;
viewSettings.marginLeftPx = marginPx / 2;
viewSettings.marginRightPx = marginPx / 2;
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent, false, false);
view?.renderer.setAttribute('margin', `${marginPx}px`);
view?.renderer.setAttribute('gap', `${gapPercent}%`);
@@ -194,9 +202,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom) + 64px)'
: '64px',
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 64}px` : '64px',
}}
>
<div className='flex w-full items-center justify-between gap-x-6'>
@@ -250,9 +256,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom) + 64px)'
: '64px',
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 64}px` : '64px',
}}
>
<Slider
@@ -269,7 +273,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
<div className='flex w-full items-center justify-between gap-x-6'>
<Slider
initialValue={getMarginProgressValue(
viewSettings?.marginPx ?? 44,
viewSettings?.marginTopPx ?? 44,
viewSettings?.gapPercent ?? 5,
)}
bubbleElement={<TbBoxMargin size={marginIconSize} />}
@@ -292,8 +296,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
<div
className={clsx(
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom)+16px)]',
)}
style={{
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 16}px` : '0px',
}}
>
<Button
icon={<TOCIcon size={tocIconSize} className='' />}
@@ -318,8 +324,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
onClick={() => handleSetActionTab('tts')}
/>
</div>
{/* Desktop footer bar */}
<div className='hidden w-full items-center gap-x-4 px-4 sm:flex'>
{/* Desktop / Pad footer bar */}
<div
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
style={{
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom / 2}px` : '0px',
}}
>
<Button
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
@@ -372,6 +383,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
/>
</div>
</div>
<TTSControl bookKey={bookKey} />
</>
);
};
@@ -66,7 +66,10 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
footnoteRef.current?.replaceChildren(view);
const { renderer } = view;
renderer.setAttribute('flow', 'scrolled');
renderer.setAttribute('margin', '0px');
renderer.setAttribute('margin-top', '0px');
renderer.setAttribute('margin-right', '0px');
renderer.setAttribute('margin-bottom', '0px');
renderer.setAttribute('margin-left', '0px');
renderer.setAttribute('gap', '0%');
const viewSettings = getViewSettings(bookKey)!;
const themeCode = getThemeCode();
@@ -2,6 +2,7 @@ import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
@@ -22,6 +23,7 @@ interface HeaderBarProps {
bookTitle: string;
isTopLeft: boolean;
isHoveredAnim: boolean;
gridInsets: Insets;
onCloseBook: (bookKey: string) => void;
onSetSettingsDialogOpen: (open: boolean) => void;
}
@@ -31,6 +33,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
bookTitle,
isTopLeft,
isHoveredAnim,
gridInsets,
onCloseBook,
onSetSettingsDialogOpen,
}) => {
@@ -77,10 +80,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
return (
<div
className={clsx(
'bg-base-100 absolute top-0 w-full',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
)}
className={clsx('bg-base-100 absolute top-0 w-full')}
style={{
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
}}
>
<div
className={clsx('absolute top-0 z-10 hidden h-11 w-full sm:flex')}
@@ -89,11 +92,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
/>
<div
className={clsx(
'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'bg-base-100 absolute left-0 right-0 top-0 z-10',
isVisible ? 'visible' : 'hidden',
)}
style={{
height: systemUIVisible ? `max(env(safe-area-inset-top), ${statusBarHeight}px)` : '',
height: systemUIVisible ? `${Math.max(gridInsets.top, statusBarHeight)}px` : '0px',
}}
/>
<div
@@ -110,8 +113,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
)}
style={{
marginTop: systemUIVisible
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: 'env(safe-area-inset-top)',
? `${Math.max(gridInsets.top, statusBarHeight)}px`
: `${gridInsets.top}px`,
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
@@ -1,5 +1,8 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { eventDispatcher } from '@/utils/event';
interface SectionInfoProps {
@@ -8,7 +11,8 @@ interface SectionInfoProps {
isScrolled: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
contentInsets: Insets;
gridInsets: Insets;
}
const HintInfo: React.FC<SectionInfoProps> = ({
@@ -17,8 +21,16 @@ const HintInfo: React.FC<SectionInfoProps> = ({
isScrolled,
isVertical,
horizontalGap,
verticalMargin,
contentInsets,
gridInsets,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
const hintTimeout = useRef(2000);
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -50,14 +62,16 @@ const HintInfo: React.FC<SectionInfoProps> = ({
<>
<div
className={clsx(
'absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'absolute left-0 right-0 top-0 z-10',
hintMessage ? 'bg-base-100' : 'bg-transparent',
)}
style={{
height: `${topInset}px`,
}}
/>
<div
className={clsx(
'hintinfo absolute flex items-center justify-end overflow-hidden ps-2',
'mt-[env(safe-area-inset-top)]',
hintMessage ? 'bg-base-100' : 'bg-transparent',
isVertical ? 'writing-vertical-rl' : 'top-0 h-[44px]',
isScrolled
@@ -71,11 +85,16 @@ const HintInfo: React.FC<SectionInfoProps> = ({
style={
isVertical
? {
bottom: `${verticalMargin * 1.5}px`,
left: `calc(100% - ${horizontalGap}%)`,
bottom: `${contentInsets.bottom * 1.5}px`,
right: showDoubleBorder
? `calc(${contentInsets.right}px)`
: `calc(${Math.max(0, contentInsets.right - 32)}px)`,
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
}
: { insetInlineEnd: `${horizontalGap}%` }
: {
top: `${topInset}px`,
insetInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
}
}
>
<h2 className={clsx('text-neutral-content text-center font-sans text-xs font-light')}>
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import React from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -12,7 +13,8 @@ interface PageInfoProps {
pageinfo?: PageInfo;
timeinfo?: TimeInfo;
horizontalGap: number;
verticalMargin: number;
contentInsets: Insets;
gridInsets: Insets;
}
const ProgressInfoView: React.FC<PageInfoProps> = ({
@@ -22,7 +24,8 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
pageinfo,
timeinfo,
horizontalGap,
verticalMargin,
contentInsets,
gridInsets,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
@@ -51,25 +54,25 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
return (
<div
className={clsx(
'pageinfo absolute bottom-0 flex items-center justify-between',
'progressinfo absolute bottom-0 flex items-center justify-between',
'text-neutral-content font-sans text-xs font-extralight',
isVertical ? 'writing-vertical-rl' : 'h-12 w-full',
isVertical ? 'writing-vertical-rl' : 'h-[52px] w-full',
isScrolled && !isVertical && 'bg-base-100',
)}
style={
isVertical
? {
bottom: `${verticalMargin * 1.5}px`,
left: showDoubleBorder ? `calc(${horizontalGap}% - 32px)` : 0,
bottom: `${contentInsets.bottom * 1.5}px`,
left: showDoubleBorder
? `calc(${contentInsets.left}px)`
: `calc(${Math.max(0, contentInsets.left - 32)}px)`,
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 3}px)`,
height: `calc(100% - ${((contentInsets.top + contentInsets.bottom) / 2) * 3}px)`,
}
: {
paddingInlineStart: `${horizontalGap}%`,
paddingInlineEnd: `${horizontalGap}%`,
paddingBottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom)*0.67)'
: 0,
paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
paddingInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.67}px` : 0,
}
}
>
@@ -17,10 +17,13 @@ import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { eventDispatcher } from '@/utils/event';
import { interceptWindowOpen } from '@/utils/open';
import { mountAdditionalFonts } from '@/utils/font';
import { setSystemUIVisibility } from '@/utils/bridge';
import { isTauriAppPlatform } from '@/services/environment';
import { getSysFontsList, setSystemUIVisibility } from '@/utils/bridge';
import { AboutWindow } from '@/components/AboutWindow';
import { UpdaterWindow } from '@/components/UpdaterWindow';
import { Toast } from '@/components/Toast';
import { getLocale } from '@/utils/misc';
import { initDayjs } from '@/utils/time';
import ReaderContent from './ReaderContent';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
@@ -30,33 +33,23 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { settings, setSettings } = useSettingsStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [libraryLoaded, setLibraryLoaded] = useState(false);
const isInitiating = useRef(false);
useTheme({ systemUIVisible: false, appThemeColor: 'base-100' });
useTheme({ systemUIVisible: settings.alwaysShowStatusBar, appThemeColor: 'base-100' });
useScreenWakeLock(settings.screenWakeLock);
useEffect(() => {
mountAdditionalFonts(document);
interceptWindowOpen();
if (isTauriAppPlatform()) {
setTimeout(getSysFontsList, 3000);
}
initDayjs(getLocale());
}, []);
useEffect(() => {
const handleVisibilityChange = () => {
if (appService?.isMobileApp && !document.hidden) {
dismissSystemUI();
setSystemUIVisibility({ visible: false, darkMode: isDarkMode });
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService, isDarkMode]);
const handleKeyDown = (event: CustomEvent) => {
if (event.detail.keyName === 'Back') {
setSideBarVisible(false);
@@ -102,9 +95,10 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
useEffect(() => {
if (!appService?.isMobileApp) return;
const systemUIVisible = !!hoveredBookKey;
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
if (systemUIVisible) {
const systemUIVisible = !!hoveredBookKey || settings.alwaysShowStatusBar;
const visible = systemUIVisible && !systemUIAlwaysHidden;
setSystemUIVisibility({ visible, darkMode: isDarkMode });
if (visible) {
showSystemUI();
} else {
dismissSystemUI();
@@ -36,7 +36,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
const { saveSettings } = useSettingsStore();
const { getConfig, getBookData, saveConfig } = useBookDataStore();
const { getView, setBookKeys } = useReaderStore();
const { getView, setBookKeys, getViewSettings } = useReaderStore();
const { initViewState, getViewState, clearViewState } = useReaderStore();
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
const isInitiating = useRef(false);
@@ -150,7 +150,8 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
if (!bookKeys || bookKeys.length === 0) return null;
const bookData = getBookData(bookKeys[0]!);
if (!bookData || !bookData.book || !bookData.bookDoc) {
const viewSettings = getViewSettings(bookKeys[0]!);
if (!bookData || !bookData.book || !bookData.bookDoc || !viewSettings) {
setTimeout(() => setLoading(true), 300);
return (
loading && (
@@ -1,5 +1,8 @@
import clsx from 'clsx';
import React from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
interface SectionInfoProps {
section?: string;
@@ -7,7 +10,8 @@ interface SectionInfoProps {
isScrolled: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
contentInsets: Insets;
gridInsets: Insets;
}
const SectionInfo: React.FC<SectionInfoProps> = ({
@@ -16,32 +20,48 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
isScrolled,
isVertical,
horizontalGap,
verticalMargin,
contentInsets,
gridInsets,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
return (
<>
<div
className={clsx(
'absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'absolute left-0 right-0 top-0 z-10',
isScrolled && !isVertical && 'bg-base-100',
)}
style={{
height: `${topInset}px`,
}}
/>
<div
className={clsx(
'sectioninfo absolute flex items-center overflow-hidden',
'mt-[env(safe-area-inset-top)]',
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
isScrolled && !isVertical && 'bg-base-100',
)}
style={
isVertical
? {
top: `${verticalMargin * 1.5}px`,
left: `calc(100% - ${horizontalGap}%)`,
top: `${contentInsets.top * 1.5}px`,
right: showDoubleBorder
? `calc(${contentInsets.right}px)`
: `calc(${Math.max(0, contentInsets.right - 32)}px)`,
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 2}px)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
}
: {
top: `${topInset}px`,
insetInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
width: `calc(100% - ${contentInsets.left + contentInsets.right}px)`,
}
: { insetInlineStart: `${horizontalGap}%`, width: `calc(100% - ${horizontalGap * 2}%)` }
}
>
<h2
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { FiSearch } from 'react-icons/fi';
import { FiCopy } from 'react-icons/fi';
import { PiHighlighterFill } from 'react-icons/pi';
@@ -26,6 +26,7 @@ import { useTextSelector } from '../../hooks/useTextSelector';
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { findTocItemBS } from '@/utils/toc';
import { throttle } from '@/utils/throttle';
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
import AnnotationPopup from './AnnotationPopup';
import WiktionaryPopup from './WiktionaryPopup';
@@ -78,13 +79,17 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const annotPopupHeight = useResponsiveSize(44);
const androidSelectionHandlerHeight = 0;
const handleDismissPopup = () => {
setSelection(null);
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleDismissPopup = useCallback(
throttle(() => {
setSelection(null);
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
}, 500),
[],
);
const handleDismissPopupAndSelection = () => {
handleDismissPopup();
@@ -69,16 +69,18 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
className={clsx(
'w-4 p-0 text-center leading-none',
style === 'highlight' &&
(selectedStyle === 'highlight' ? `bg-${selectedColor}-400` : `bg-gray-300`),
(selectedStyle === 'highlight'
? `bg-${selectedColor}-300 pt-[2px]`
: `bg-gray-300 pt-[2px]`),
(style === 'underline' || style === 'squiggly') &&
'text-gray-300 underline decoration-2',
style === 'underline' &&
(selectedStyle === 'underline'
? `decoration-${selectedColor}-400`
? `decoration-${selectedColor}-300`
: `decoration-gray-300`),
style === 'squiggly' &&
(selectedStyle === 'squiggly'
? `decoration-wavy decoration-${selectedColor}-400`
? `decoration-wavy decoration-${selectedColor}-300`
: `decoration-gray-300 decoration-wavy`),
)}
>
@@ -100,10 +102,10 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
key={color}
onClick={() => handleSelectColor(color)}
style={{ width: size16, height: size16 }}
className={clsx(`rounded-full p-0`, selectedColor !== color && `bg-${color}-400`)}
className={clsx(`rounded-full p-0`, selectedColor !== color && `bg-${color}-300`)}
>
{selectedColor === color && (
<FaCheckCircle size={size16} className={clsx(`fill-${color}-400`)} />
<FaCheckCircle size={size16} className={clsx(`fill-${color}-300`)} />
)}
</button>
))}
@@ -7,20 +7,14 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useTranslator } from '@/hooks/useTranslator';
import { TRANSLATED_LANGS } from '@/services/constants';
import { UseTranslatorOptions, getTranslators } from '@/services/translators';
import { localeToLang } from '@/utils/lang';
import Select from '@/components/Select';
const notSupportedLangs = ['hi', 'vi'];
const notSupportedLangs = [''];
const generateTranslatorLangs = () => {
const langs = { ...TRANSLATED_LANGS };
const result: Record<string, string> = {};
for (const [code, name] of Object.entries(langs)) {
if (notSupportedLangs.includes(code)) continue;
const newCode = localeToLang(code).toUpperCase();
result[newCode] = name;
}
return result;
return Object.fromEntries(
Object.entries(TRANSLATED_LANGS).filter(([code]) => !notSupportedLangs.includes(code)),
);
};
const TRANSLATOR_LANGS = generateTranslatorLangs();
@@ -205,7 +199,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
</div>
<div className='absolute bottom-0 flex h-8 w-full items-center justify-between bg-gray-600 px-4'>
{provider && !loading && (
<div className='text-xs opacity-60'>
<div className='line-clamp-1 text-xs opacity-60'>
{error
? ''
: _('Translated by {{provider}}.', {
@@ -16,10 +16,11 @@ const NotebookHeader: React.FC<{
}> = ({ isPinned, isSearchBarVisible, handleClose, handleTogglePin, handleToggleSearchBar }) => {
const _ = useTranslation();
const iconSize14 = useResponsiveSize(14);
const iconSize18 = useResponsiveSize(18);
return (
<div className='notebook-header relative flex h-11 items-center px-3' dir='ltr'>
<div className='absolute inset-0 z-[-1] flex items-center justify-center space-x-2'>
<LuNotebookPen />
<LuNotebookPen size={iconSize18} />
<div className='notebook-title hidden text-sm font-medium sm:flex'>{_('Notebook')}</div>
</div>
<div className='flex w-full items-center gap-x-4'>
@@ -44,7 +45,7 @@ const NotebookHeader: React.FC<{
onClick={handleToggleSearchBar}
className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0', isSearchBarVisible && 'bg-base-300')}
>
<FiSearch />
<FiSearch size={iconSize18} />
</button>
</div>
</div>
@@ -216,8 +216,9 @@ const Notebook: React.FC = ({}) => {
}
`}</style>
<div
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
className='drag-bar absolute left-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3'
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
/>
<div className='flex-shrink-0'>
<NotebookHeader
@@ -8,6 +8,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { getTranslators } from '@/services/translators';
import { TRANSLATED_LANGS } from '@/services/constants';
import { initDayjs } from '@/utils/time';
import Select from '@/components/Select';
const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
@@ -43,7 +44,9 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const handleSelectUILang = (event: React.ChangeEvent<HTMLSelectElement>) => {
const option = event.target.value;
saveViewSettings(envConfig, bookKey, 'uiLanguage', option, false, false);
i18n.changeLanguage(option ? option : navigator.language);
const locale = option ? option : navigator.language;
i18n.changeLanguage(locale);
initDayjs(locale);
};
const getTranslationProviderOptions = () => {
@@ -18,11 +18,12 @@ import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
const { getBookData } = useBookDataStore();
const view = getView(bookKey);
const bookData = getBookData(bookKey)!;
const viewSettings = getViewSettings(bookKey)!;
const gridInsets = getGridInsets(bookKey)!;
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
@@ -31,10 +32,23 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [textIndent, setTextIndent] = useState(viewSettings.textIndent!);
const [fullJustification, setFullJustification] = useState(viewSettings.fullJustification!);
const [hyphenation, setHyphenation] = useState(viewSettings.hyphenation!);
const [marginPx, setMarginPx] = useState(viewSettings.marginPx!);
const [marginTopPx, setMarginTopPx] = useState(
viewSettings.marginPx || viewSettings.marginTopPx!,
);
const [marginBottomPx, setMarginBottomPx] = useState(viewSettings.marginBottomPx!);
const [marginLeftPx, setMarginLeftPx] = useState(viewSettings.marginLeftPx!);
const [marginRightPx, setMarginRightPx] = useState(viewSettings.marginRightPx!);
const [compactMarginTopPx, setCompactMarginTopPx] = useState(
viewSettings.compactMarginPx || viewSettings.compactMarginTopPx!,
);
const [compactMarginBottomPx, setCompactMarginBottomPx] = useState(
viewSettings.compactMarginBottomPx!,
);
const [gapPercent, setGapPercent] = useState(viewSettings.gapPercent!);
const [compactMarginPx, setCompactMarginPx] = useState(viewSettings.compactMarginPx!);
const [compactGapPercent, setCompactGapPercent] = useState(viewSettings.compactGapPercent!);
const [compactMarginLeftPx, setCompactMarginLeftPx] = useState(viewSettings.compactMarginLeftPx!);
const [compactMarginRightPx, setCompactMarginRightPx] = useState(
viewSettings.compactMarginRightPx!,
);
const [maxColumnCount, setMaxColumnCount] = useState(viewSettings.maxColumnCount!);
const [maxInlineSize, setMaxInlineSize] = useState(viewSettings.maxInlineSize!);
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!);
@@ -84,18 +98,90 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, [hyphenation]);
useEffect(() => {
if (marginPx === viewSettings.marginPx) return;
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx, false, false);
view?.renderer.setAttribute('margin', `${marginPx}px`);
if (marginTopPx === viewSettings.marginTopPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginTopPx', marginTopPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginPx]);
}, [marginTopPx]);
useEffect(() => {
if (compactMarginPx === viewSettings.compactMarginPx) return;
saveViewSettings(envConfig, bookKey, 'compactMarginPx', compactMarginPx, false, false);
view?.renderer.setAttribute('margin', `${compactMarginPx}px`);
if (marginBottomPx === viewSettings.marginBottomPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginBottomPx', marginBottomPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginPx]);
}, [marginBottomPx]);
useEffect(() => {
if (marginRightPx === viewSettings.marginRightPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginRightPx', marginRightPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginRightPx]);
useEffect(() => {
if (marginLeftPx === viewSettings.marginLeftPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginLeftPx', marginLeftPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginLeftPx]);
useEffect(() => {
if (compactMarginTopPx === viewSettings.compactMarginTopPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'compactMarginTopPx', compactMarginTopPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginTopPx]);
useEffect(() => {
if (compactMarginBottomPx === viewSettings.compactMarginBottomPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(
envConfig,
bookKey,
'compactMarginBottomPx',
compactMarginBottomPx,
false,
false,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginBottomPx]);
useEffect(() => {
if (compactMarginRightPx === viewSettings.compactMarginRightPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(
envConfig,
bookKey,
'compactMarginRightPx',
compactMarginRightPx,
false,
false,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginRightPx]);
useEffect(() => {
if (compactMarginLeftPx === viewSettings.compactMarginLeftPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'compactMarginLeftPx', compactMarginLeftPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginLeftPx]);
useEffect(() => {
if (gapPercent === viewSettings.gapPercent) return;
@@ -107,16 +193,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gapPercent]);
useEffect(() => {
if (compactGapPercent === viewSettings.compactGapPercent) return;
saveViewSettings(envConfig, bookKey, 'compactGapPercent', compactGapPercent, false, false);
view?.renderer.setAttribute('gap', `${compactGapPercent}%`);
if (viewSettings.scrolled) {
view?.renderer.setAttribute('flow', 'scrolled');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactGapPercent]);
useEffect(() => {
if (maxColumnCount === viewSettings.maxColumnCount) return;
saveViewSettings(envConfig, bookKey, 'maxColumnCount', maxColumnCount, false, false);
@@ -171,14 +247,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (doubleBorder === viewSettings.doubleBorder) return;
if (doubleBorder && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setGapPercent(viewSettings.gapPercent);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'doubleBorder', doubleBorder, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [doubleBorder]);
@@ -203,59 +271,35 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showPageNumber]);
const applyMarginAndGap = () => {
const isCompact = !showHeader && !showFooter;
const marginPx = isCompact ? viewSettings.compactMarginPx : viewSettings.marginPx;
const gapPercent = isCompact ? viewSettings.compactGapPercent : viewSettings.gapPercent;
view?.renderer.setAttribute('margin', `${marginPx}px`);
view?.renderer.setAttribute('gap', `${gapPercent}%`);
if (viewSettings.scrolled) {
view?.renderer.setAttribute('flow', 'scrolled');
}
};
useEffect(() => {
if (showHeader === viewSettings.showHeader) return;
if (showHeader && !viewSettings.vertical) {
viewSettings.marginPx = Math.max(viewSettings.marginPx, 44);
setMarginPx(viewSettings.marginPx);
setViewSettings(bookKey, viewSettings);
} else if (showHeader && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setGapPercent(viewSettings.gapPercent);
const minMarginTop = Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4);
viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, minMarginTop);
setMarginTopPx(viewSettings.marginTopPx);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'showHeader', showHeader, false, false);
applyMarginAndGap();
// Margin and gap settings will be applied in FoliateViewer
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showHeader]);
useEffect(() => {
if (showFooter === viewSettings.showFooter) return;
if (showFooter && !viewSettings.vertical) {
viewSettings.marginPx = Math.max(viewSettings.marginPx, 44);
setMarginPx(viewSettings.marginPx);
setViewSettings(bookKey, viewSettings);
} else if (showFooter && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setGapPercent(viewSettings.gapPercent);
const minMarginBottom = Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4);
viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, minMarginBottom);
setMarginBottomPx(viewSettings.marginBottomPx);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'showFooter', showFooter, false, false);
applyMarginAndGap();
// Margin and gap settings will be applied in FoliateViewer
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showFooter]);
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
const mightBeRTLBook = MIGHT_BE_RTL_LANGS.includes(langCode) || isCJKEnv();
const isVertical = viewSettings.vertical || writingMode.includes('vertical');
return (
<div className='my-4 w-full space-y-6'>
@@ -418,22 +462,50 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='card bg-base-100 border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<NumberInput
label={_('Vertical Margins (px)')}
value={showFooter || showHeader ? marginPx : compactMarginPx}
onChange={showFooter || showHeader ? setMarginPx : setCompactMarginPx}
min={!viewSettings.vertical && (showFooter || showHeader) ? 44 : 0}
label={_('Top Margin (px)')}
value={showHeader && !isVertical ? marginTopPx : compactMarginTopPx}
onChange={showHeader && !isVertical ? setMarginTopPx : setCompactMarginTopPx}
min={
showHeader && !isVertical
? Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4)
: 0
}
max={88}
step={4}
/>
<NumberInput
label={_('Horizontal Margins (%)')}
value={showFooter || showHeader ? gapPercent : compactGapPercent}
onChange={showFooter || showHeader ? setGapPercent : setCompactGapPercent}
label={_('Bottom Margin (px)')}
value={showFooter && !isVertical ? marginBottomPx : compactMarginBottomPx}
onChange={showFooter && !isVertical ? setMarginBottomPx : setCompactMarginBottomPx}
min={
viewSettings.vertical && (showFooter || showHeader)
? Math.ceil(4800 / window.innerWidth)
showFooter && !isVertical
? Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4)
: 0
}
max={88}
step={4}
/>
<NumberInput
label={_('Left Margin (px)')}
value={showFooter && isVertical ? marginLeftPx : compactMarginLeftPx}
onChange={showFooter && isVertical ? setMarginLeftPx : setCompactMarginLeftPx}
min={0}
max={88}
step={4}
/>
<NumberInput
label={_('Right Margin (px)')}
value={showHeader && isVertical ? marginRightPx : compactMarginRightPx}
onChange={showHeader && isVertical ? setMarginRightPx : setCompactMarginRightPx}
min={0}
max={88}
step={4}
/>
<NumberInput
label={_('Column Gap (%)')}
value={gapPercent}
onChange={setGapPercent}
min={0}
max={30}
/>
<NumberInput
@@ -1,4 +1,5 @@
import clsx from 'clsx';
import dayjs from 'dayjs';
import React from 'react';
import { marked } from 'marked';
@@ -103,7 +104,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<div className={clsx('content font-size-sm line-clamp-3', item.note && 'mt-2')}>
<span
className={clsx(
'inline',
'inline leading-normal',
item.note && 'content font-size-xs text-gray-500',
(item.style === 'underline' || item.style === 'squiggly') &&
'underline decoration-2',
@@ -130,47 +131,54 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
}
onClick={(e) => e.stopPropagation()}
>
<div className='flex justify-end space-x-3 p-2' dir='ltr'>
{item.note && (
<button
<div className='flex cursor-default items-center justify-between'>
<div className='flex items-center'>
<span className='text-sm text-gray-500 sm:text-xs'>
{dayjs(item.createdAt).fromNow()}
</span>
</div>
<div className='flex items-center justify-end space-x-3 p-2' dir='ltr'>
{item.note && (
<div
className={clsx(
'content settings-content cursor-pointer',
'flex items-end p-0 hover:bg-transparent',
)}
onClick={editNote.bind(null, item)}
>
<div
className={clsx(
'align-bottom text-blue-500',
'transition duration-300 ease-in-out',
'content font-size-sm',
'opacity-0 group-hover:opacity-100',
'hover:text-blue-600',
)}
>
{_('Edit')}
</div>
</div>
)}
<div
className={clsx(
'btn btn-ghost content settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
'content settings-content cursor-pointer',
'flex items-end p-0 hover:bg-transparent',
)}
onClick={editNote.bind(null, item)}
onClick={deleteNote.bind(null, item)}
>
<div
className={clsx(
'align-bottom text-blue-500',
'align-bottom text-red-500',
'transition duration-300 ease-in-out',
'content font-size-sm',
'opacity-0 group-hover:opacity-100',
'hover:text-blue-600',
'hover:text-red-600',
)}
>
{_('Edit')}
{_('Delete')}
</div>
</button>
)}
<button
className={clsx(
'btn btn-ghost content settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
)}
onClick={deleteNote.bind(null, item)}
>
<div
className={clsx(
'align-bottom text-red-500',
'transition duration-300 ease-in-out',
'content font-size-sm',
'opacity-0 group-hover:opacity-100',
'hover:text-red-600',
)}
>
{_('Delete')}
</div>
</button>
</div>
</div>
</div>
</li>
@@ -248,8 +248,9 @@ const SideBar: React.FC<{
<SidebarContent bookDoc={bookDoc} sideBarBookKey={sideBarBookKey!} />
)}
<div
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
className='drag-bar absolute right-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3'
onMouseDown={handleHorizontalDragStart}
onTouchStart={handleHorizontalDragStart}
></div>
</div>
{!isSideBarPinned && (
@@ -20,13 +20,16 @@ const POPUP_WIDTH = 282;
const POPUP_HEIGHT = 160;
const POPUP_PADDING = 10;
const TTSControl = () => {
interface TTSControlProps {
bookKey: string;
}
const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getProgress, getViewSettings } = useReaderStore();
const { setViewSettings, setTTSEnabled } = useReaderStore();
const [bookKey, setBookKey] = useState<string>('');
const [ttsLang, setTtsLang] = useState<string>('en');
const [isPlaying, setIsPlaying] = useState(false);
const [isPaused, setIsPaused] = useState(false);
@@ -152,7 +155,9 @@ const TTSControl = () => {
}, [ttsController, bookKey]);
const handleTTSSpeak = async (event: CustomEvent) => {
const { bookKey, range } = event.detail;
const { bookKey: ttsBookKey, range } = event.detail;
if (bookKey !== ttsBookKey) return;
const view = getView(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
@@ -182,7 +187,6 @@ const TTSControl = () => {
}
const primaryLang = bookData.book.primaryLanguage;
setBookKey(bookKey);
if (ttsControllerRef.current) {
ttsControllerRef.current.stop();
@@ -230,8 +234,8 @@ const TTSControl = () => {
};
const handleTTSStop = async (event: CustomEvent) => {
const { bookKey } = event.detail;
if (ttsControllerRef.current) {
const { bookKey: ttsBookKey } = event.detail;
if (ttsControllerRef.current && bookKey === ttsBookKey) {
handleStop(bookKey);
}
};
@@ -274,7 +278,7 @@ const TTSControl = () => {
const handleStop = async (bookKey: string) => {
const ttsController = ttsControllerRef.current;
if (ttsController) {
await ttsController.stop();
await ttsController.shutdown();
ttsControllerRef.current = null;
setTtsController(null);
getView(bookKey)?.deselect();
@@ -17,7 +17,7 @@ interface UseBookShortcutsProps {
}
const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) => {
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { getView, getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { toggleSideBar, setSideBarBookKey } = useSidebarStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { toggleNotebook } = useNotebookStore();
@@ -136,6 +136,13 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
const toggleTTS = () => {
if (!sideBarBookKey) return;
const bookKey = sideBarBookKey;
const viewState = getViewState(bookKey);
eventDispatcher.dispatch(viewState?.ttsEnabled ? 'tts-stop' : 'tts-speak', { bookKey });
};
useShortcuts(
{
onSwitchSideBar: switchSideBar,
@@ -144,8 +151,9 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
onToggleScrollMode: toggleScrollMode,
onOpenFontLayoutSettings: () => setFontLayoutSettingsDialogOpen(true),
onToggleSearchBar: showSearchBar,
onReloadPage: reloadPage,
onToggleFullscreen: toggleFullscreen,
onToggleTTS: toggleTTS,
onReloadPage: reloadPage,
onQuitApp: quitApp,
onGoLeft: goLeft,
onGoRight: goRight,
@@ -14,7 +14,7 @@ export const useTextSelector = (
) => {
const { appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getViewState, getViewSettings } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const bookData = getBookData(bookKey)!;
const primaryLang = bookData.book?.primaryLanguage || 'en';
@@ -94,9 +94,6 @@ export const useTextSelector = (
const handlePointerup = (doc: Document, index: number) => {
// Available on iOS and Desktop, fired at touchend or mouseup
// Note that on Android, pointerup event is fired after an additional touch event
const viewState = getViewState(bookKey);
if (viewState?.ttsEnabled) return;
const sel = doc.getSelection() as Selection;
if (isValidSelection(sel)) {
if (osPlatform === 'ios') {
@@ -116,6 +113,7 @@ export const useTextSelector = (
// Prevent the container from scrolling when text is selected in paginated mode
// FIXME: this is a workaround for issue #873
// TODO: support text selection across pages
handleDismissPopup();
const viewSettings = getViewSettings(bookKey);
if (
appService?.isAndroidApp &&
@@ -1,15 +1,17 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { FoliateView } from '@/types/view';
import { UseTranslatorOptions } from '@/services/translators';
import { useReaderStore } from '@/store/readerStore';
import { useTranslator } from '@/hooks/useTranslator';
import { walkTextNodes } from '@/utils/walk';
import { localeToLang } from '@/utils/lang';
import { debounce } from '@/utils/debounce';
import { getLocale } from '@/utils/misc';
export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElement | null) {
const { getViewSettings } = useReaderStore();
const { getViewSettings, getViewState, getProgress } = useReaderStore();
const viewSettings = getViewSettings(bookKey);
const viewState = getViewState(bookKey);
const progress = getProgress(bookKey);
const enabled = useRef(viewSettings?.translationEnabled);
const [provider, setProvider] = useState(viewSettings?.translationProvider);
@@ -17,7 +19,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
const { translate } = useTranslator({
provider,
targetLang: localeToLang(targetLang || getLocale()),
targetLang: targetLang || getLocale(),
} as UseTranslatorOptions);
const translateRef = useRef(translate);
@@ -118,7 +120,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
const translateElement = async (el: HTMLElement) => {
if (!enabled.current) return;
const text = el.textContent?.trim();
const text = el.textContent?.replaceAll('\n', '').trim();
if (!text) return;
if (el.querySelector('.translation-target')) {
@@ -156,6 +158,61 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
}
};
const findNodeIndicesInRange = (range: Range, nodes: HTMLElement[]) => {
const startContainer = range.startContainer;
const endContainer = range.endContainer;
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]!;
if (node === startContainer || node.contains(startContainer)) {
if (startIndex === -1) startIndex = i;
}
if (node === endContainer || node.contains(endContainer)) {
endIndex = i;
}
}
if (startIndex !== -1 && endIndex === -1) {
endIndex = startIndex;
}
return { startIndex, endIndex };
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const translateInRange = useCallback(
debounce((range: Range) => {
const nodes = allTextNodes.current;
if (nodes.length === 0) {
console.warn('No text nodes available for translation.');
return;
}
const { startIndex, endIndex } = findNodeIndicesInRange(range, nodes);
if (startIndex === -1) {
console.log('Range not found in text nodes');
return;
}
const beforeStart = Math.max(0, startIndex - 2);
const afterEnd = Math.min(nodes.length - 1, endIndex + 2);
for (let i = beforeStart; i <= afterEnd; i++) {
const node = nodes[i];
if (node) {
translateElement(node);
}
}
}, 500),
[],
);
useEffect(() => {
if (viewState?.ttsEnabled && progress && document.hidden) {
const { range } = progress;
translateInRange(range);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewState?.ttsEnabled, progress]);
useEffect(() => {
if (!viewSettings) return;
+1 -1
View File
@@ -192,7 +192,7 @@ const Dialog: React.FC<DialogProps> = ({
>
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
</div>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-2 sm:px-4'>
{header ? (
header
) : (
@@ -274,7 +274,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
<div className='text-base-content flex-grow text-sm'>
<h2 className='mb-4 text-center font-bold sm:text-start'>
{_('A new version of Readest is Available!')}
{_('A new version of Readest is available!')}
</h2>
<p className='mb-2'>
{_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
+6 -27
View File
@@ -1,36 +1,11 @@
export interface ShortcutConfig {
onSwitchSideBar: string[];
onToggleSideBar: string[];
onToggleNotebook: string[];
onToggleSearchBar: string[];
onToggleScrollMode: string[];
onToggleSelectMode: string[];
onOpenFontLayoutSettings: string[];
onReloadPage: string[];
onToggleFullscreen: string[];
onQuitApp: string[];
onGoLeft: string[];
onGoRight: string[];
onGoNext: string[];
onGoPrev: string[];
onGoHalfPageDown: string[];
onGoHalfPageUp: string[];
onGoBack: string[];
onGoForward: string[];
onZoomIn: string[];
onZoomOut: string[];
onResetZoom: string[];
onSaveNote: string[];
onCloseNote: string[];
}
const DEFAULT_SHORTCUTS: ShortcutConfig = {
const DEFAULT_SHORTCUTS = {
onSwitchSideBar: ['ctrl+Tab', 'opt+Tab', 'alt+Tab'],
onToggleSideBar: ['s'],
onToggleNotebook: ['n'],
onToggleSearchBar: ['ctrl+f', 'cmd+f'],
onToggleScrollMode: ['shift+j'],
onToggleSelectMode: ['shift+s'],
onToggleTTS: ['t'],
onOpenFontLayoutSettings: ['shift+f'],
onReloadPage: ['shift+r'],
onToggleFullscreen: ['F11'],
@@ -50,6 +25,10 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
onCloseNote: ['Escape'],
};
export type ShortcutConfig = {
[K in keyof typeof DEFAULT_SHORTCUTS]: string[];
};
// Load shortcuts from localStorage or fallback to defaults
export const loadShortcuts = (): ShortcutConfig => {
if (typeof localStorage === 'undefined') return DEFAULT_SHORTCUTS;
@@ -0,0 +1,35 @@
import { useState, useEffect, useCallback } from 'react';
export const useSafeAreaInsets = () => {
const [updated, setUpdated] = useState(false);
const [insets, setInsets] = useState({
top: 0,
right: 0,
bottom: 0,
left: 0,
});
const updateSafeAreaInsets = useCallback(() => {
const rootStyles = getComputedStyle(document.documentElement);
const hasCustomProperties = rootStyles.getPropertyValue('--safe-area-inset-top');
if (hasCustomProperties) {
setInsets({
top: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-top')) || 0,
right: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-right')) || 0,
bottom: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-bottom')) || 0,
left: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-left')) || 0,
});
}
setUpdated(true);
}, []);
useEffect(() => {
updateSafeAreaInsets();
window.addEventListener('resize', updateSafeAreaInsets);
return () => {
window.removeEventListener('resize', updateSafeAreaInsets);
};
}, [updateSafeAreaInsets]);
return updated ? insets : null;
};
+53 -10
View File
@@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { applyCustomTheme, Palette } from '@/styles/themes';
import { getStatusBarHeight, setSystemUIVisibility } from '@/utils/bridge';
import { getOSPlatform } from '@/utils/misc';
type UseThemeProps = {
systemUIVisible?: boolean;
@@ -23,18 +24,12 @@ export const useTheme = ({
dismissSystemUI,
updateAppTheme,
setStatusBarHeight,
systemUIAlwaysHidden,
setSystemUIAlwaysHidden,
} = useThemeStore();
useEffect(() => {
updateAppTheme(appThemeColor);
if (appService?.isMobileApp) {
if (systemUIVisible) {
showSystemUI();
} else {
dismissSystemUI();
}
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
}
if (appService?.isAndroidApp) {
getStatusBarHeight().then((res) => {
if (res.height && res.height > 0) {
@@ -43,7 +38,55 @@ export const useTheme = ({
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService, isDarkMode]);
}, []);
const handleSystemUIVisibility = useCallback(() => {
if (!appService?.isMobileApp) return;
const visible = systemUIVisible && !systemUIAlwaysHidden;
if (visible) {
showSystemUI();
} else {
dismissSystemUI();
}
setSystemUIVisibility({ visible, darkMode: isDarkMode });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService, isDarkMode, systemUIVisible]);
useEffect(() => {
if (appService?.isMobileApp) {
handleSystemUIVisibility();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSystemUIVisibility]);
useEffect(() => {
if (!appService?.isMobileApp) return;
handleSystemUIVisibility();
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
handleSystemUIVisibility();
}
};
const handleOrientationChange = () => {
if (appService?.isIOSApp && getOSPlatform() === 'ios') {
// FIXME: This is a workaround for iPhone apps where the system UI is not visible in landscape mode
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
// in the ViewController. Note that screen.orientation.type is not abailable in iOS before 16.4.
const systemUIAlwaysHidden = screen.orientation?.type.includes('landscape');
setSystemUIAlwaysHidden(systemUIAlwaysHidden);
handleSystemUIVisibility();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
screen.orientation.addEventListener('change', handleOrientationChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
screen.orientation.removeEventListener('change', handleOrientationChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSystemUIVisibility]);
useEffect(() => {
const customThemes = settings.globalReadSettings?.customThemes ?? [];
+10 -3
View File
@@ -37,6 +37,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
keepLogin: false,
autoUpload: true,
alwaysOnTop: false,
alwaysShowStatusBar: false,
autoCheckUpdates: true,
screenWakeLock: false,
openLastBooks: false,
@@ -87,10 +88,15 @@ export const DEFAULT_BOOK_FONT: BookFont = {
};
export const DEFAULT_BOOK_LAYOUT: BookLayout = {
marginPx: 44,
marginTopPx: 44,
marginBottomPx: 44,
marginLeftPx: 16,
marginRightPx: 16,
compactMarginTopPx: 16,
compactMarginBottomPx: 16,
compactMarginLeftPx: 16,
compactMarginRightPx: 16,
gapPercent: 5,
compactMarginPx: 0,
compactGapPercent: 5,
scrolled: false,
disableClick: false,
swapClickArea: false,
@@ -130,6 +136,7 @@ export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
fullJustification: false,
animated: true,
defaultFont: 'Sans-serif',
marginBottomPx: 16,
};
export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
+2 -3
View File
@@ -11,14 +11,13 @@ export const isTauriAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM']
export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web';
export const hasCli = () => window.__READEST_CLI_ACCESS === true;
export const isPWA = () => window.matchMedia('(display-mode: standalone)').matches;
export const getBaseUrl = () => process.env['NEXT_PUBLIC_API_BASE_URL'] ?? READEST_WEB_BASE_URL;
// Dev API only in development mode and web platform
// with command `pnpm dev-web`
// for production build or tauri app use the production Web API
export const getAPIBaseUrl = () =>
process.env['NODE_ENV'] === 'development' && isWebAppPlatform()
? '/api'
: `${process.env['NEXT_PUBLIC_API_BASE_URL'] ?? READEST_WEB_BASE_URL}/api`;
process.env['NODE_ENV'] === 'development' && isWebAppPlatform() ? '/api' : `${getBaseUrl()}/api`;
export interface EnvConfigType {
getAppService: () => Promise<AppService>;
@@ -2,7 +2,7 @@ import { stubTranslation as _ } from '@/utils/misc';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { isTauriAppPlatform } from '@/services/environment';
import { TranslationProvider } from '../types';
import { langToDefaultLocale } from '@/utils/lang';
import { normalizeToFullLang } from '@/utils/lang';
interface TokenCache {
token: string;
@@ -53,8 +53,8 @@ export const azureProvider: TranslationProvider = {
if (!text.length) return [];
const results: string[] = [];
const msSourceLang = sourceLang ? langToDefaultLocale(sourceLang.toLowerCase()) : '';
const msTargetLang = langToDefaultLocale(targetLang.toLowerCase());
const msSourceLang = sourceLang ? normalizeToFullLang(sourceLang) : '';
const msTargetLang = normalizeToFullLang(targetLang);
const translationPromises = text.map(async (line, index) => {
if (!line?.trim().length) {
@@ -67,7 +67,7 @@ export const azureProvider: TranslationProvider = {
to: msTargetLang,
'api-version': '3.0',
});
if (msSourceLang && msSourceLang !== 'auto') {
if (msSourceLang && msSourceLang.toLowerCase() !== 'auto') {
params.append('from', msSourceLang);
}
@@ -3,6 +3,7 @@ import { stubTranslation as _ } from '@/utils/misc';
import { ErrorCodes, TranslationProvider } from '../types';
import { UserPlan } from '@/types/user';
import { getUserPlan } from '@/utils/access';
import { normalizeToShortLang } from '@/utils/lang';
import { DEFAULT_DAILY_TRANSLATION_QUOTA } from '@/services/constants';
import { saveDailyUsage } from '../utils';
@@ -38,8 +39,8 @@ export const deeplProvider: TranslationProvider = {
const body = JSON.stringify({
text: text,
source_lang: sourceLang.toUpperCase(),
target_lang: targetLang.toUpperCase(),
source_lang: normalizeToShortLang(sourceLang).toUpperCase(),
target_lang: normalizeToShortLang(targetLang).toUpperCase(),
use_cache: useCache,
});
@@ -1,6 +1,7 @@
import { stubTranslation as _ } from '@/utils/misc';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { isTauriAppPlatform } from '@/services/environment';
import { normalizeToShortLang } from '@/utils/lang';
import { TranslationProvider } from '../types';
export const googleProvider: TranslationProvider = {
@@ -20,8 +21,8 @@ export const googleProvider: TranslationProvider = {
const url = new URL('https://translate.googleapis.com/translate_a/single');
url.searchParams.append('client', 'gtx');
url.searchParams.append('dt', 't');
url.searchParams.append('sl', sourceLang.toLowerCase() || 'auto');
url.searchParams.append('tl', targetLang.toLowerCase());
url.searchParams.append('sl', normalizeToShortLang(sourceLang).toLowerCase() || 'auto');
url.searchParams.append('tl', normalizeToShortLang(targetLang).toLowerCase());
url.searchParams.append('q', line);
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
@@ -1,7 +1,8 @@
import { FoliateView } from '@/types/view';
import { AppService } from '@/types/system';
import { parseSSMLMarks } from '@/utils/ssml';
import { TTSGranularity, TTSMark, TTSVoice } from './types';
import { Overlayer } from 'foliate-js/overlayer.js';
import { TTSGranularity, TTSHighlightOptions, TTSMark, TTSVoice } from './types';
import { WebSpeechClient } from './WebSpeechClient';
import { NativeTTSClient } from './NativeTTSClient';
import { EdgeTTSClient } from './EdgeTTSClient';
@@ -18,6 +19,8 @@ type TTSState =
| 'setrate-paused'
| 'setvoice-paused';
const HIGHLIGHT_KEY = 'tts-highlight';
export class TTSController extends EventTarget {
appService: AppService | null = null;
view: FoliateView;
@@ -51,13 +54,13 @@ export class TTSController extends EventTarget {
async init() {
const availableClients = [];
if (await this.ttsEdgeClient.init()) {
availableClients.push(this.ttsEdgeClient);
}
if (this.ttsNativeClient && (await this.ttsNativeClient.init())) {
availableClients.push(this.ttsNativeClient);
this.ttsNativeVoices = await this.ttsNativeClient.getAllVoices();
}
if (await this.ttsEdgeClient.init()) {
availableClients.push(this.ttsEdgeClient);
}
if (await this.ttsWebClient.init()) {
availableClients.push(this.ttsWebClient);
}
@@ -75,13 +78,35 @@ export class TTSController extends EventTarget {
this.ttsEdgeVoices = await this.ttsEdgeClient.getAllVoices();
}
#getHighlighter(options: TTSHighlightOptions) {
return (range: Range) => {
const { overlayer } = this.view.renderer.getContents()[0] as { overlayer: Overlayer };
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);
}
};
}
#clearHighlighter() {
const { overlayer } = (this.view.renderer.getContents()?.[0] || {}) as { overlayer: Overlayer };
overlayer?.remove(HIGHLIGHT_KEY);
}
async initViewTTS() {
let granularity: TTSGranularity = this.view.language.isCJK ? 'sentence' : 'word';
const supportedGranularities = this.ttsClient.getGranularities();
if (!supportedGranularities.includes(granularity)) {
granularity = supportedGranularities[0]!;
}
await this.view.initTTS(granularity);
const highlightOptions: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
await this.view.initTTS(granularity, this.#getHighlighter(highlightOptions));
}
async preloadSSML(ssml: string | undefined) {
@@ -113,6 +138,7 @@ export class TTSController extends EventTarget {
.replace('<break/>', ' ')
.replace(/\.{3,}/g, ' ')
.replace(/……/g, ' ')
.replace(/\*/g, ' ')
.replace(/·/g, ' ');
return ssml;
@@ -334,6 +360,7 @@ export class TTSController extends EventTarget {
async shutdown() {
await this.stop();
this.#clearHighlighter();
if (this.ttsWebClient.initialized) {
await this.ttsWebClient.shutdown();
}
@@ -1,5 +1,10 @@
export type TTSGranularity = 'sentence' | 'word';
export type TTSHighlightOptions = {
style: 'highlight' | 'underline' | 'squiggly' | 'outline';
color: string;
};
export type TTSVoice = {
id: string;
name: string;
+5 -1
View File
@@ -42,7 +42,11 @@ export const useBookDataStore = create<BookDataState>((set, get) => ({
setConfig: (key: string, partialConfig: Partial<BookConfig>) => {
set((state: BookDataState) => {
const id = key.split('-')[0]!;
const config = (state.booksData[id]?.config || {}) as BookConfig;
const config = (state.booksData[id]?.config || null) as BookConfig;
if (!config) {
console.warn('No config found for book', id);
return state;
}
Object.assign(config, partialConfig);
return {
booksData: {
+22 -2
View File
@@ -8,6 +8,7 @@ import {
ViewSettings,
TimeInfo,
} from '@/types/book';
import { Insets } from '@/types/misc';
import { EnvConfigType } from '@/services/environment';
import { FoliateView } from '@/types/view';
import { DocumentLoader, TOCItem } from '@/libs/document';
@@ -27,6 +28,7 @@ interface ViewState {
progress: BookProgress | null;
ribbonVisible: boolean;
ttsEnabled: boolean;
gridInsets: Insets | null;
/* View settings for the view:
generally view settings have a hierarchy of global settings < book settings < view settings
view settings for primary view are saved to book config which is persisted to config file
@@ -67,6 +69,8 @@ interface ReaderStore {
) => Promise<void>;
clearViewState: (key: string) => void;
getViewState: (key: string) => ViewState | null;
getGridInsets: (key: string) => Insets | null;
setGridInsets: (key: string, insets: Insets | null) => void;
}
export const useReaderStore = create<ReaderStore>((set, get) => ({
@@ -114,14 +118,15 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
progress: null,
ribbonVisible: false,
ttsEnabled: false,
gridInsets: null,
viewSettings: null,
},
},
}));
try {
const { settings } = useSettingsStore.getState();
if (!bookData) {
const appService = await envConfig.getAppService();
const { settings } = useSettingsStore.getState();
const { library } = useLibraryStore.getState();
const book = library.find((b) => b.hash === id);
if (!book) {
@@ -145,6 +150,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
const booksData = useBookDataStore.getState().booksData;
const config = booksData[id]?.config as BookConfig;
const configViewSettings = config.viewSettings!;
const globalViewSettings = settings.globalViewSettings;
set((state) => ({
viewStates: {
...state.viewStates,
@@ -158,7 +164,8 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
progress: null,
ribbonVisible: false,
ttsEnabled: false,
viewSettings: JSON.parse(JSON.stringify(configViewSettings)) as ViewSettings,
gridInsets: null,
viewSettings: { ...globalViewSettings, ...configViewSettings },
},
},
}));
@@ -177,6 +184,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
progress: null,
ribbonVisible: false,
ttsEnabled: false,
gridInsets: null,
viewSettings: null,
},
},
@@ -305,4 +313,16 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
},
},
})),
getGridInsets: (key: string) => get().viewStates[key]?.gridInsets || null,
setGridInsets: (key: string, insets: Insets | null) =>
set((state) => ({
viewStates: {
...state.viewStates,
[key]: {
...state.viewStates[key]!,
gridInsets: insets,
},
},
})),
}));
+4
View File
@@ -12,6 +12,8 @@ interface ThemeState {
isDarkMode: boolean;
systemUIVisible: boolean;
statusBarHeight: number;
systemUIAlwaysHidden: boolean;
setSystemUIAlwaysHidden: (hidden: boolean) => void;
setStatusBarHeight: (height: number) => void;
showSystemUI: () => void;
dismissSystemUI: () => void;
@@ -69,9 +71,11 @@ export const useThemeStore = create<ThemeState>((set, get) => {
themeCode,
systemUIVisible: false,
statusBarHeight: 24,
systemUIAlwaysHidden: false,
showSystemUI: () => set({ systemUIVisible: true }),
dismissSystemUI: () => set({ systemUIVisible: false }),
setStatusBarHeight: (height: number) => set({ statusBarHeight: height }),
setSystemUIAlwaysHidden: (hidden: boolean) => set({ systemUIAlwaysHidden: hidden }),
getIsDarkMode: () => get().isDarkMode,
setThemeMode: (mode) => {
if (typeof window !== 'undefined' && localStorage) {
+5
View File
@@ -6,6 +6,11 @@
border-radius: 10px;
scrollbar-gutter: auto !important;
overscroll-behavior: none;
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
/* Fixes the scrollbar gutter issue for DaisyUI */
+10 -3
View File
@@ -73,10 +73,17 @@ export interface BooknoteGroup {
export type WritingMode = 'auto' | 'horizontal-tb' | 'horizontal-rl' | 'vertical-rl';
export interface BookLayout {
marginPx: number;
marginTopPx: number;
marginBottomPx: number;
marginLeftPx: number;
marginRightPx: number;
marginPx?: number; // deprecated
compactMarginTopPx: number;
compactMarginBottomPx: number;
compactMarginLeftPx: number;
compactMarginRightPx: number;
compactMarginPx?: number; // deprecated
gapPercent: number;
compactMarginPx: number;
compactGapPercent: number;
scrolled: boolean;
disableClick: boolean;
swapClickArea: boolean;
+6
View File
@@ -0,0 +1,6 @@
export type Insets = {
top: number;
bottom: number;
left: number;
right: number;
};
+1
View File
@@ -28,6 +28,7 @@ export interface SystemSettings {
alwaysOnTop: boolean;
autoCheckUpdates: boolean;
screenWakeLock: boolean;
alwaysShowStatusBar: boolean;
openLastBooks: boolean;
lastOpenBooks: string[];
autoImportBooksOnOpen: boolean;
+4 -2
View File
@@ -20,7 +20,7 @@ export interface FoliateView extends HTMLElement {
clearSearch: () => void;
select: (target: string | number | { fraction: number }) => void;
deselect: () => void;
initTTS: (granularity?: TTSGranularity) => Promise<void>;
initTTS: (granularity?: TTSGranularity, highlight?: (range: Range) => void) => Promise<void>;
book: BookDoc;
tts: TTS | null;
language: {
@@ -41,6 +41,7 @@ export interface FoliateView extends HTMLElement {
start: number;
end: number;
containerPosition: number;
sideProp: 'width' | 'height';
setAttribute: (name: string, value: string | number) => void;
removeAttribute: (name: string) => void;
next: () => Promise<void>;
@@ -49,7 +50,8 @@ export interface FoliateView extends HTMLElement {
prevSection?: () => Promise<void>;
goTo?: (params: { index: number; anchor: number }) => void;
setStyles?: (css: string) => void;
getContents: () => { doc: Document; index?: number }[];
getContents: () => { doc: Document; index?: number; overlayer?: unknown }[];
scrollToAnchor: (anchor: number | Range) => void;
addEventListener: (
type: string,
listener: EventListener,
+16 -2
View File
@@ -1,4 +1,4 @@
const getGridTemplate = (count: number, aspectRatio: number) => {
export const getGridTemplate = (count: number, aspectRatio: number) => {
if (count <= 1) {
return { columns: '1fr', rows: '1fr' };
} else if (count === 2) {
@@ -12,4 +12,18 @@ const getGridTemplate = (count: number, aspectRatio: number) => {
}
};
export default getGridTemplate;
export const getInsetEdges = (index: number, count: number, aspectRatio: number) => {
const gridTemplate = getGridTemplate(count, aspectRatio);
const cols = gridTemplate.columns.split(' ').length;
const rows = gridTemplate.rows.split(' ').length;
const row = Math.floor(index / cols);
const col = index % cols;
return {
top: row === 0,
right: col === cols - 1,
bottom: row === rows - 1,
left: col === 0,
};
};
+23
View File
@@ -0,0 +1,23 @@
import { Insets } from '@/types/misc';
import { ViewSettings } from '@/types/book';
export const getViewInsets = (viewSettings: ViewSettings) => {
const showHeader = viewSettings.showHeader!;
const showFooter = viewSettings.showFooter!;
const isVertical = viewSettings.vertical || viewSettings.writingMode.includes('vertical');
const fullMarginTopPx = viewSettings.marginPx || viewSettings.marginTopPx;
const compactMarginTopPx = viewSettings.compactMarginPx || viewSettings.compactMarginTopPx;
const fullMarginBottomPx = viewSettings.marginBottomPx;
const compactMarginBottomPx = viewSettings.compactMarginBottomPx;
const fullMarginLeftPx = viewSettings.marginLeftPx;
const fullMarginRightPx = viewSettings.marginRightPx;
const compactMarginLeftPx = viewSettings.compactMarginLeftPx;
const compactMarginRightPx = viewSettings.compactMarginRightPx;
return {
top: showHeader && !isVertical ? fullMarginTopPx : compactMarginTopPx,
right: showHeader && isVertical ? fullMarginRightPx : compactMarginRightPx,
bottom: showFooter && !isVertical ? fullMarginBottomPx : compactMarginBottomPx,
left: showFooter && isVertical ? fullMarginLeftPx : compactMarginLeftPx,
} as Insets;
};
+16 -9
View File
@@ -8,7 +8,7 @@ export const isCJKLang = (lang: string | null | undefined): boolean => {
return ['zh', 'ja', 'ko'].includes(normalizedLang);
};
export const langToDefaultLocale = (langCode: string): string => {
export const normalizeToFullLang = (langCode: string): string => {
const mapping: Record<string, string> = {
en: 'en-US',
fr: 'fr-FR',
@@ -26,24 +26,31 @@ export const langToDefaultLocale = (langCode: string): string => {
ru: 'ru-RU',
uk: 'uk-UA',
zh: 'zh-Hans',
'zh-cn': 'zh-Hans',
'zh-tw': 'zh-Hant',
'zh-mo': 'zh-Hant',
'zh-hans': 'zh-Hans',
'zh-hant': 'zh-Hant',
};
return mapping[langCode] || langCode;
return mapping[langCode.toLowerCase()] || langCode;
};
export const localeToLang = (locale: string): string => {
export const normalizeToShortLang = (langCode: string): string => {
const lang = langCode.toLowerCase();
const mapping: Record<string, string> = {
'zh-CN': 'zh-Hans',
'zh-TW': 'zh-Hant',
'zh-HK': 'zh-Hant',
'zh-cn': 'zh-Hans',
'zh-tw': 'zh-Hant',
'zh-hk': 'zh-Hant',
'zh-mo': 'zh-Hant',
'zh-hans': 'zh-Hans',
'zh-hant': 'zh-Hant',
};
if (locale.startsWith('zh')) {
return mapping[locale] || 'zh-Hans';
if (lang.startsWith('zh')) {
return mapping[lang] || 'zh-Hans';
}
return mapping[locale] || locale.split('-')[0]!.toLowerCase();
return lang.split('-')[0]!;
};
export const normalizedLangCode = (lang: string | null | undefined): string => {
+8 -10
View File
@@ -241,6 +241,11 @@ const getLayoutStyles = (
${!vertical && overrideLayout ? `margin-bottom: ${paragraphMargin}em !important;` : ''}
}
:lang(zh), :lang(ja), :lang(ko) {
widows: 1;
orphans: 1;
}
pre {
white-space: pre-wrap !important;
}
@@ -415,16 +420,9 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
return `${layoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${userStylesheet}`;
};
export const transformStylesheet = (
viewSettings: ViewSettings,
width: number,
height: number,
css: string,
) => {
export const transformStylesheet = (vw: number, vh: number, css: string) => {
const isMobile = ['ios', 'android'].includes(getOSPlatform());
const fontScale = isMobile ? 1.25 : 1;
const w = width * (1 - viewSettings.gapPercent / 100);
const h = height - viewSettings.marginPx * 2;
const ruleRegex = /([^{]+)({[^}]+})/g;
css = css.replace(ruleRegex, (match, selector, block) => {
const hasTextAlignCenter = /text-align\s*:\s*center\s*[;$]/.test(block);
@@ -462,8 +460,8 @@ export const transformStylesheet = (
const rem = parseFloat(pt) / fontScale / 12;
return `font-size: ${rem}rem`;
})
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * w) / 100 + 'px')
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * h) / 100 + 'px')
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * vw) / 100 + 'px')
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * vh) / 100 + 'px')
.replace(/[\s;]color\s*:\s*black/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*#000/gi, 'color: var(--theme-fg-color)')
+33
View File
@@ -0,0 +1,33 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/en';
import 'dayjs/locale/zh';
import 'dayjs/locale/de';
import 'dayjs/locale/ja';
import 'dayjs/locale/ko';
import 'dayjs/locale/ru';
import 'dayjs/locale/fr';
import 'dayjs/locale/el';
import 'dayjs/locale/es';
import 'dayjs/locale/it';
import 'dayjs/locale/pt';
import 'dayjs/locale/pt-br';
import 'dayjs/locale/ar';
import 'dayjs/locale/id';
import 'dayjs/locale/hi';
import 'dayjs/locale/th';
import 'dayjs/locale/tr';
import 'dayjs/locale/vi';
import 'dayjs/locale/uk';
import 'dayjs/locale/pl';
import 'dayjs/locale/fi';
import 'dayjs/locale/nl';
import 'dayjs/locale/ro';
import 'dayjs/locale/zh-tw';
import 'dayjs/locale/zh-cn';
export const initDayjs = (locale: string) => {
dayjs.locale(locale);
dayjs.extend(relativeTime);
};
+54 -155
View File
@@ -13,7 +13,7 @@ importers:
version: 3.3.1
'@eslint/js':
specifier: ^9.9.1
version: 9.16.0
version: 9.28.0
'@sindresorhus/tsconfig':
specifier: ^6.0.0
version: 6.0.0
@@ -116,6 +116,9 @@ importers:
cssbeautify:
specifier: ^0.3.1
version: 0.3.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
foliate-js:
specifier: workspace:*
version: link:../../packages/foliate-js
@@ -224,10 +227,10 @@ importers:
version: 7.4.4
eslint:
specifier: ^9.16.0
version: 9.16.0(jiti@1.21.6)
version: 9.28.0(jiti@1.21.6)
eslint-config-next:
specifier: 15.0.3
version: 15.0.3(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
version: 15.0.3(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
i18next-scanner:
specifier: ^4.6.0
version: 4.6.0(typescript@5.7.2)
@@ -267,7 +270,7 @@ importers:
devDependencies:
'@eslint/js':
specifier: ^9.9.1
version: 9.16.0
version: 9.28.0
'@rollup/plugin-node-resolve':
specifier: ^15.2.3
version: 15.3.0(rollup@4.28.0)
@@ -1439,10 +1442,6 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.19.0':
resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/config-array@0.20.1':
resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1459,34 +1458,18 @@ packages:
resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.9.0':
resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.1':
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.16.0':
resolution: {integrity: sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.28.0':
resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.4':
resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.6':
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.2.3':
resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.3.2':
resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1518,10 +1501,6 @@ packages:
resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
engines: {node: '>=18.18'}
'@humanwhocodes/retry@0.4.1':
resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
engines: {node: '>=18.18'}
'@humanwhocodes/retry@0.4.3':
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
@@ -3421,6 +3400,9 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
debounce@2.2.0:
resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==}
engines: {node: '>=18'}
@@ -3734,10 +3716,6 @@ packages:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'}
eslint-scope@8.2.0:
resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3750,16 +3728,6 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.16.0:
resolution: {integrity: sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
jiti: '*'
peerDependenciesMeta:
jiti:
optional: true
eslint@9.28.0:
resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -8143,11 +8111,6 @@ snapshots:
'@esbuild/win32-x64@0.25.4':
optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@9.16.0(jiti@1.21.6))':
dependencies:
eslint: 9.16.0(jiti@1.21.6)
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.4.1(eslint@9.28.0(jiti@1.21.6))':
dependencies:
eslint: 9.28.0(jiti@1.21.6)
@@ -8155,14 +8118,6 @@ snapshots:
'@eslint-community/regexpp@4.12.1': {}
'@eslint/config-array@0.19.0':
dependencies:
'@eslint/object-schema': 2.1.4
debug: 4.3.7
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
'@eslint/config-array@0.20.1':
dependencies:
'@eslint/object-schema': 2.1.6
@@ -8181,8 +8136,6 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/core@0.9.0': {}
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
@@ -8197,18 +8150,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@eslint/js@9.16.0': {}
'@eslint/js@9.28.0': {}
'@eslint/object-schema@2.1.4': {}
'@eslint/object-schema@2.1.6': {}
'@eslint/plugin-kit@0.2.3':
dependencies:
levn: 0.4.1
'@eslint/plugin-kit@0.3.2':
dependencies:
'@eslint/core': 0.15.0
@@ -8235,8 +8180,6 @@ snapshots:
'@humanwhocodes/retry@0.3.1': {}
'@humanwhocodes/retry@0.4.1': {}
'@humanwhocodes/retry@0.4.3': {}
'@icons/material@0.2.4(react@19.0.0)':
@@ -9703,15 +9646,15 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)':
'@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/parser': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/scope-manager': 8.16.0
'@typescript-eslint/type-utils': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/utils': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/type-utils': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/utils': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/visitor-keys': 8.16.0
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@@ -9721,14 +9664,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)':
'@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@typescript-eslint/scope-manager': 8.16.0
'@typescript-eslint/types': 8.16.0
'@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2)
'@typescript-eslint/visitor-keys': 8.16.0
debug: 4.4.0
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
optionalDependencies:
typescript: 5.7.2
transitivePeerDependencies:
@@ -9739,12 +9682,12 @@ snapshots:
'@typescript-eslint/types': 8.16.0
'@typescript-eslint/visitor-keys': 8.16.0
'@typescript-eslint/type-utils@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)':
'@typescript-eslint/type-utils@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2)
'@typescript-eslint/utils': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/utils': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
debug: 4.4.0
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
ts-api-utils: 1.4.3(typescript@5.7.2)
optionalDependencies:
typescript: 5.7.2
@@ -9768,13 +9711,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)':
'@typescript-eslint/utils@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@1.21.6))
'@eslint-community/eslint-utils': 4.4.1(eslint@9.28.0(jiti@1.21.6))
'@typescript-eslint/scope-manager': 8.16.0
'@typescript-eslint/types': 8.16.0
'@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2)
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
optionalDependencies:
typescript: 5.7.2
transitivePeerDependencies:
@@ -10378,6 +10321,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
dayjs@1.11.13: {}
debounce@2.2.0: {}
debug@3.2.7:
@@ -10640,19 +10585,19 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-config-next@15.0.3(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2):
eslint-config-next@15.0.3(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2):
dependencies:
'@next/eslint-plugin-next': 15.0.3
'@rushstack/eslint-patch': 1.10.4
'@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/parser': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.16.0(jiti@1.21.6)
'@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/parser': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.28.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.16.0(jiti@1.21.6))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.16.0(jiti@1.21.6))
eslint-plugin-react: 7.37.2(eslint@9.16.0(jiti@1.21.6))
eslint-plugin-react-hooks: 5.0.0(eslint@9.16.0(jiti@1.21.6))
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@1.21.6))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@1.21.6))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.28.0(jiti@1.21.6))
eslint-plugin-react: 7.37.2(eslint@9.28.0(jiti@1.21.6))
eslint-plugin-react-hooks: 5.0.0(eslint@9.28.0(jiti@1.21.6))
optionalDependencies:
typescript: 5.7.2
transitivePeerDependencies:
@@ -10672,37 +10617,37 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.16.0(jiti@1.21.6)):
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@1.21.6)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
enhanced-resolve: 5.17.1
eslint: 9.16.0(jiti@1.21.6)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6))
eslint: 9.28.0(jiti@1.21.6)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@1.21.6))
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.3.0
is-glob: 4.0.3
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@1.21.6))
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6)):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@1.21.6)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.16.0(jiti@1.21.6)
'@typescript-eslint/parser': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.28.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.16.0(jiti@1.21.6))
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@1.21.6))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6)):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@1.21.6)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -10711,9 +10656,9 @@ snapshots:
array.prototype.flatmap: 1.3.2
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@1.21.6))
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -10725,13 +10670,13 @@ snapshots:
string.prototype.trimend: 1.0.8
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/parser': 8.16.0(eslint@9.28.0(jiti@1.21.6))(typescript@5.7.2)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.16.0(jiti@1.21.6)):
eslint-plugin-jsx-a11y@6.10.2(eslint@9.28.0(jiti@1.21.6)):
dependencies:
aria-query: 5.3.2
array-includes: 3.1.8
@@ -10741,7 +10686,7 @@ snapshots:
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
@@ -10750,11 +10695,11 @@ snapshots:
safe-regex-test: 1.0.3
string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.0.0(eslint@9.16.0(jiti@1.21.6)):
eslint-plugin-react-hooks@5.0.0(eslint@9.28.0(jiti@1.21.6)):
dependencies:
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
eslint-plugin-react@7.37.2(eslint@9.16.0(jiti@1.21.6)):
eslint-plugin-react@7.37.2(eslint@9.28.0(jiti@1.21.6)):
dependencies:
array-includes: 3.1.8
array.prototype.findlast: 1.2.5
@@ -10762,7 +10707,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.0
eslint: 9.16.0(jiti@1.21.6)
eslint: 9.28.0(jiti@1.21.6)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
@@ -10781,11 +10726,6 @@ snapshots:
esrecurse: 4.3.0
estraverse: 4.3.0
eslint-scope@8.2.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
@@ -10795,47 +10735,6 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint@9.16.0(jiti@1.21.6):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@1.21.6))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.0
'@eslint/core': 0.9.0
'@eslint/eslintrc': 3.3.1
'@eslint/js': 9.16.0
'@eslint/plugin-kit': 0.2.3
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.1
'@types/estree': 1.0.6
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.3.7
escape-string-regexp: 4.0.0
eslint-scope: 8.2.0
eslint-visitor-keys: 4.2.0
espree: 10.3.0
esquery: 1.6.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.6
transitivePeerDependencies:
- supports-color
eslint@9.28.0(jiti@1.21.6):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.28.0(jiti@1.21.6))
@@ -10849,12 +10748,12 @@ snapshots:
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
'@types/estree': 1.0.6
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0
debug: 4.3.7
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.0