Compare commits

..

18 Commits

Author SHA1 Message Date
Huang Xin 4fde9955b2 release: version 0.9.61 (#1479) 2025-06-26 11:27:02 +02:00
Huang Xin 3875c8f239 feat: add an option to show remaining pages in chapter (#1478) 2025-06-26 11:19:30 +02:00
Huang Xin 7bd1b03652 fix: get correct filename when importing txt files on iOS (#1477) 2025-06-26 10:21:55 +02:00
Huang Xin 73878c14e3 fix: EPUB covers occasionally fail to display in library view, closes #1029 (#1476) 2025-06-26 10:03:35 +02:00
Huang Xin bc2c3a2c9a feat: add options to reset settings in the config dialog, closes #1111 (#1475) 2025-06-26 09:59:34 +02:00
Huang Xin 460d37c656 chore: update assetlinks and deeplink for Android (#1474) 2025-06-25 14:47:50 +02:00
Huang Xin fff9e3221e chore: use default production supabase and posthog if not customized, closes #1454 (#1473) 2025-06-25 13:46:48 +02:00
Huang Xin f77fa17cbc bump: update tauri to latest dev branch, closes #1468 (#1471) 2025-06-25 05:36:45 +02:00
Huang Xin d4565f7d60 feat: add an option in the view menu to crop or fit book covers of the library page (#1469) 2025-06-25 03:48:08 +02:00
Huang Xin e8b1976f4f doc: adjust the priority of the readest koplugin for Koreader (#1467) 2025-06-24 08:34:31 +02:00
Huang Xin 6b4bb64885 feat: custom css for the reader UI (#1466) 2025-06-24 08:33:49 +02:00
Huang Xin fc0c42f7db android: fix transparent icon background on Android, closes #1462 (#1464) 2025-06-24 06:54:49 +02:00
Huang Xin 41e5f27d4d layout: view menu now has a maximum width, closes #1459 (#1463)
* refactor: auth callback page

* layout: view menu now has a maximum width, closes #1459
2025-06-24 05:44:03 +02:00
Huang Xin a042b4c739 fix: longer timeout to save changes before reloading page (#1461) 2025-06-24 05:01:46 +02:00
Huang Xin a1c1a9f983 chore: bump opennextjs and wrangler to latest versions (#1460) 2025-06-24 03:22:12 +02:00
Huang Xin 423ae77160 doc: add the Support section in README (#1458) 2025-06-23 12:46:08 +02:00
Huang Xin 05c61a4b1a doc: add donation badge and link with crypto payment (#1457) 2025-06-23 11:37:13 +02:00
Huang Xin ce8f06b7fb chore: add sponsor readest link 2025-06-23 05:03:21 +02:00
66 changed files with 1811 additions and 1299 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: ['readest']
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
Generated
+580 -670
View File
File diff suppressed because it is too large Load Diff
+20 -1
View File
@@ -15,6 +15,8 @@
[![][badge-discord]][link-discord]
[![AGPL Licence][badge-license]](LICENSE)
[![Latest release][badge-release]][link-gh-releases]
[![Donate][badge-donate]][link-donate]
<br>
[![Last commit][badge-last-commit]][link-gh-commits]
[![Commits][badge-commit-activity]][link-gh-pulse]
@@ -27,6 +29,7 @@
<a href="#downloads">Downloads</a> •
<a href="#getting-started">Getting Started</a> •
<a href="#troubleshooting">Troubleshooting</a> •
<a href="#support">Support</a> •
<a href="#license">License</a>
</p>
@@ -64,8 +67,8 @@
| **Feature** | **Description** | **Priority** |
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
@@ -243,6 +246,20 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
</p>
</a>
## Support
If Readest has been useful to you, consider supporting its development. Your contribution helps us squash bugs faster, improve performance, and keep building great features.
### How to Donate
1. **GitHub Sponsors**
Back the project directly on GitHub:
👉 [https://github.com/sponsors/readest](https://github.com/sponsors/readest)
2. **Crypto Donations**
Prefer crypto? You can donate here:
👉 [https://donate.readest.com/](https://donate.readest.com/)
## License
Readest is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file for details.
@@ -277,6 +294,8 @@ The following fonts are utilized in this software, either bundled within the app
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
[badge-donate]: https://donate.readest.com/badge.svg
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
[link-web-readest]: https://web.readest.com
+5 -2
View File
@@ -2,5 +2,8 @@ PDFJS_BUILD_PATH=../../packages/foliate-js/node_modules/pdfjs-dist/legacy/build
PDFJS_FONTS_PATH=../../packages/foliate-js/node_modules/pdfjs-dist
PDFJS_STYLE_PATH=../../packages/foliate-js/vendor/pdfjs
NEXT_PUBLIC_DEV_SUPABASE_URL=https://gxkhxxxeapexynpouyjz.supabase.co
NEXT_PUBLIC_DEV_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd4a2h4eHhlYXBleHlucG91eWp6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ0MzAwNTksImV4cCI6MjA1MDAwNjA1OX0.jhinkQsimQoidsg_U59YD5ROw4PmMJQNKuyXbr4TiQA
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64="aHR0cHM6Ly91cy5pLnBvc3Rob2cuY29t"
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThpTmRSNTNsR1A3VFM3VGh4S08="
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
+17 -17
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.60",
"version": "0.9.61",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -42,23 +42,23 @@
"@aws-sdk/s3-request-presigner": "^3.735.0",
"@ducanh2912/next-pwa": "^10.2.9",
"@fabianlars/tauri-plugin-oauth": "2",
"@opennextjs/cloudflare": "^1.1.0",
"@opennextjs/cloudflare": "^1.3.1",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.49.10",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-haptics": "^2.2.4",
"@tauri-apps/plugin-http": "^2.4.3",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.1",
"@tauri-apps/api": "2.6.0",
"@tauri-apps/plugin-cli": "^2.3.0",
"@tauri-apps/plugin-deep-link": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-haptics": "^2.2.5",
"@tauri-apps/plugin-http": "^2.4.4",
"@tauri-apps/plugin-log": "^2.5.1",
"@tauri-apps/plugin-opener": "^2.3.1",
"@tauri-apps/plugin-os": "^2.2.2",
"@tauri-apps/plugin-process": "^2.2.2",
"@tauri-apps/plugin-shell": "~2.2.2",
"@tauri-apps/plugin-updater": "^2.8.1",
"@zip.js/zip.js": "^2.7.53",
"aws4fetch": "^1.0.20",
"clsx": "^2.1.1",
@@ -88,7 +88,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "2.5.0",
"@tauri-apps/cli": "2.6.0",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -113,6 +113,6 @@
"raw-loader": "^4.0.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"wrangler": "^4.19.1"
"wrangler": "^4.21.0"
}
}
@@ -1,11 +1,15 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.bilingify.readest",
"sha256_cert_fingerprints": [
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82"
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82",
"E0:E7:60:55:80:8D:3A:DE:A0:D1:CF:7C:20:85:40:A3:DD:4B:E6:4D:17:5C:0F:DE:26:57:7D:9C:5B:29:5F:51"
]
}
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "تأكيد الحذف",
"Copied to notebook": "تم النسخ إلى المفكرة",
"Copy": "نسخ",
"Custom CSS": "CSS مخصص",
"Dark Mode": "الوضع الداكن",
"Default": "الافتراضي",
"Default Font": "الخط الافتراضي",
@@ -28,7 +27,6 @@
"Dictionary": "القاموس",
"Download Readest": "تحميل ريديست",
"Edit": "تحرير",
"Enter your custom CSS here...": "أدخل الـ CSS المخصص هنا...",
"Excerpts": "مقتطفات",
"Failed to import book(s): {{filenames}}": "فشل استيراد الكتاب/الكتب: {{filenames}}",
"Fast": "سريع",
@@ -376,5 +374,21 @@
"Left Margin (px)": "الهامش الأيسر (بكسل)",
"Column Gap (%)": "تباعد الأعمدة (%)",
"Always Show Status Bar": "دائمًا إظهار شريط الحالة",
"Translation Not Available": "الترجمة غير متاحة"
"Translation Not Available": "الترجمة غير متاحة",
"Custom Content CSS": "CSS مخصص للمحتوى",
"Enter CSS for book content styling...": "أدخل CSS لتنسيق محتوى الكتاب...",
"Custom Reader UI CSS": "CSS مخصص لواجهة القارئ",
"Enter CSS for reader interface styling...": "أدخل CSS لتنسيق واجهة القارئ...",
"Crop": "قص",
"Book Covers": "أغلفة الكتب",
"Fit": "ملاءمة",
"Reset {{settings}}": "إعادة تعيين {{settings}}",
"Reset Settings": "إعادة تعيين الإعدادات",
"{{count}} pages left in chapter_zero": "لم يتبق أي صفحات في هذا الفصل",
"{{count}} pages left in chapter_one": "تبقّت صفحة واحدة في هذا الفصل",
"{{count}} pages left in chapter_two": "تبقّت صفحتان في هذا الفصل",
"{{count}} pages left in chapter_few": "تبقّت {{count}} صفحات في هذا الفصل",
"{{count}} pages left in chapter_many": "تبقّت {{count}} صفحة في هذا الفصل",
"{{count}} pages left in chapter_other": "تبقّى {{count}} صفحة في هذا الفصل",
"Show Remaining Pages": "عرض الصفحات المتبقية"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Löschen bestätigen",
"Copied to notebook": "In Notizbuch kopiert",
"Copy": "Kopieren",
"Custom CSS": "Benutzerdefiniertes CSS",
"Dark Mode": "Dunkelmodus",
"Default": "Standard",
"Default Font": "Standardschriftart",
@@ -28,7 +27,6 @@
"Dictionary": "Wörterbuch",
"Download Readest": "Readest herunterladen",
"Edit": "Bearbeiten",
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
"Excerpts": "Auszüge",
"Failed to import book(s): {{filenames}}": "Fehler beim Importieren von Buch/Büchern: {{filenames}}",
"Fast": "Schnell",
@@ -364,5 +362,17 @@
"Left Margin (px)": "Linker Rand (px)",
"Column Gap (%)": "Spaltenabstand (%)",
"Always Show Status Bar": "Statusleiste immer anzeigen",
"Translation Not Available": "Übersetzung nicht verfügbar"
"Translation Not Available": "Übersetzung nicht verfügbar",
"Custom Content CSS": "Benutzerdefiniertes Inhalts-CSS",
"Enter CSS for book content styling...": "CSS für die Buchinhaltsgestaltung eingeben...",
"Custom Reader UI CSS": "Benutzerdefiniertes CSS für die Leseroberfläche",
"Enter CSS for reader interface styling...": "CSS für die Leseroberfläche eingeben...",
"Crop": "Zuschneiden",
"Book Covers": "Buchcover",
"Fit": "Anpassen",
"Reset {{settings}}": "{{settings}} zurücksetzen",
"Reset Settings": "Einstellungen zurücksetzen",
"{{count}} pages left in chapter_one": "{{count}} Seite verbleibend im Kapitel",
"{{count}} pages left in chapter_other": "{{count}} Seiten verbleibend im Kapitel",
"Show Remaining Pages": "Verbleibende Seiten anzeigen"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Επιβεβαίωση διαγραφής",
"Copied to notebook": "Αντιγράφηκε στο σημειωματάριο",
"Copy": "Αντιγραφή",
"Custom CSS": "Προσαρμοσμένο CSS",
"Dark Mode": "Σκοτεινή λειτουργία",
"Default": "Προεπιλογή",
"Default Font": "Προεπιλεγμένη γραμματοσειρά",
@@ -28,7 +27,6 @@
"Dictionary": "Λεξικό",
"Download Readest": "Λήψη Readest",
"Edit": "Επεξεργασία",
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
"Excerpts": "Αποσπάσματα",
"Failed to import book(s): {{filenames}}": "Αποτυχία εισαγωγής βιβλίου(ων): {{filenames}}",
"Fast": "Γρήγορα",
@@ -364,5 +362,17 @@
"Left Margin (px)": "Αριστερό περιθώριο (px)",
"Column Gap (%)": "Απόσταση στηλών (%)",
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης",
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη"
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη",
"Custom Content CSS": "Προσαρμοσμένο CSS περιεχομένου",
"Enter CSS for book content styling...": "Εισαγάγετε CSS για στυλ περιεχομένου βιβλίου...",
"Custom Reader UI CSS": "Προσαρμοσμένο CSS διεπαφής αναγνώστη",
"Enter CSS for reader interface styling...": "Εισαγάγετε CSS για στυλ διεπαφής αναγνώστη...",
"Crop": "Περικοπή",
"Book Covers": "Εξώφυλλα βιβλίων",
"Fit": "Προσαρμογή",
"Reset {{settings}}": "Επαναφορά {{settings}}",
"Reset Settings": "Επαναφορά ρυθμίσεων",
"{{count}} pages left in chapter_one": "Μένει {{count}} σελίδα",
"{{count}} pages left in chapter_other": "Μένουν {{count}} σελίδες",
"Show Remaining Pages": "Εμφάνιση υπολοίπων"
}
@@ -4,5 +4,7 @@
"Are you sure to delete {{count}} selected book(s)?_one": "Are you sure to delete {{count}} selected book?",
"Are you sure to delete {{count}} selected book(s)?_other": "Are you sure to delete {{count}} selected books?",
"Search in {{count}} Book(s)..._one": "Search in {{count}} book...",
"Search in {{count}} Book(s)..._other": "Search in {{count}} books..."
"Search in {{count}} Book(s)..._other": "Search in {{count}} books...",
"{{count}} pages left in chapter_one": "{{count}} page left in chapter",
"{{count}} pages left in chapter_other": "{{count}} pages left in chapter"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmar eliminación",
"Copied to notebook": "Copiado al cuaderno",
"Copy": "Copiar",
"Custom CSS": "CSS personalizado",
"Dark Mode": "Modo oscuro",
"Default": "Predeterminado",
"Default Font": "Fuente predeterminada",
@@ -28,7 +27,6 @@
"Dictionary": "Diccionario",
"Download Readest": "Descargar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
"Excerpts": "Extractos",
"Failed to import book(s): {{filenames}}": "Error al importar libro(s): {{filenames}}",
"Fast": "Rápido",
@@ -367,5 +365,18 @@
"Left Margin (px)": "Margen izquierdo (px)",
"Column Gap (%)": "Espacio entre columnas (%)",
"Always Show Status Bar": "Mostrar siempre la barra de estado",
"Translation Not Available": "Traducción no disponible"
"Translation Not Available": "Traducción no disponible",
"Custom Content CSS": "CSS del contenido",
"Enter CSS for book content styling...": "Introduce CSS para el contenido del libro...",
"Custom Reader UI CSS": "CSS de la interfaz",
"Enter CSS for reader interface styling...": "Introduce CSS para la interfaz de lectura...",
"Crop": "Recortar",
"Book Covers": "Portadas de libros",
"Fit": "Encajar",
"Reset {{settings}}": "Restablecer {{settings}}",
"Reset Settings": "Restablecer configuración",
"{{count}} pages left in chapter_one": "{{count}} página restante en el capítulo",
"{{count}} pages left in chapter_many": "{{count}} páginas restantes en el capítulo",
"{{count}} pages left in chapter_other": "{{count}} páginas restantes en el capítulo",
"Show Remaining Pages": "Mostrar resto"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmer la suppression",
"Copied to notebook": "Copié dans le carnet de notes",
"Copy": "Copier",
"Custom CSS": "CSS personnalisé",
"Dark Mode": "Mode sombre",
"Default": "Par défaut",
"Default Font": "Police par défaut",
@@ -28,7 +27,6 @@
"Dictionary": "Dictionnaire",
"Download Readest": "Télécharger Readest",
"Edit": "Modifier",
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
"Excerpts": "Extraits",
"Failed to import book(s): {{filenames}}": "Échec de l'importation du/des livre(s) : {{filenames}}",
"Fast": "Rapide",
@@ -367,5 +365,18 @@
"Left Margin (px)": "Marge gauche (px)",
"Column Gap (%)": "Espacement des colonnes (%)",
"Always Show Status Bar": "Afficher toujours la barre d'état",
"Translation Not Available": "Traduction non disponible"
"Translation Not Available": "Traduction non disponible",
"Custom Content CSS": "CSS du contenu",
"Enter CSS for book content styling...": "Saisissez le CSS pour le contenu du livre...",
"Custom Reader UI CSS": "CSS de linterface",
"Enter CSS for reader interface styling...": "Saisissez le CSS pour linterface de lecture...",
"Crop": "Rogner",
"Book Covers": "Couvertures de livres",
"Fit": "Adapter",
"Reset {{settings}}": "Réinitialiser {{settings}}",
"Reset Settings": "Réinitialiser les paramètres",
"{{count}} pages left in chapter_one": "{{count}} page restant dans le chapitre",
"{{count}} pages left in chapter_many": "{{count}} pages restantes dans le chapitre",
"{{count}} pages left in chapter_other": "{{count}} pages restantes dans le chapitre",
"Show Remaining Pages": "Voir restantes"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "हटाने की पुष्टि करें",
"Copied to notebook": "नोटबुक में कॉपी किया गया",
"Copy": "कॉपी",
"Custom CSS": "कस्टम CSS",
"Dark Mode": "डार्क मोड",
"Default": "डिफ़ॉल्ट",
"Default Font": "डिफ़ॉल्ट फ़ॉन्ट",
@@ -28,7 +27,6 @@
"Dictionary": "शब्दकोश",
"Download Readest": "Readest डाउनलोड करें",
"Edit": "संपादित करें",
"Enter your custom CSS here...": "अपना कस्टम CSS यहां दर्ज करें...",
"Excerpts": "अंश",
"Failed to import book(s): {{filenames}}": "पुस्तक(ें) आयात करने में विफल: {{filenames}}",
"Fast": "तेज़",
@@ -364,5 +362,17 @@
"Left Margin (px)": "बायां हाशिया (px)",
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं",
"Translation Not Available": "अनुवाद उपलब्ध नहीं है"
"Translation Not Available": "अनुवाद उपलब्ध नहीं है",
"Custom Content CSS": "सामग्री का CSS",
"Enter CSS for book content styling...": "पुस्तक सामग्री के लिए CSS दर्ज करें...",
"Custom Reader UI CSS": "इंटरफ़ेस का CSS",
"Enter CSS for reader interface styling...": "रीडर इंटरफ़ेस के लिए CSS दर्ज करें...",
"Crop": "काटें",
"Book Covers": "पुस्तक कवर",
"Fit": "फिट",
"Reset {{settings}}": "रीसेट {{settings}}",
"Reset Settings": "सेटिंग्स रीसेट करें",
"{{count}} pages left in chapter_one": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
"{{count}} pages left in chapter_other": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
"Show Remaining Pages": "शेष पृष्ठ दिखाएँ"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Konfirmasi Penghapusan",
"Copied to notebook": "Disalin ke buku catatan",
"Copy": "Salin",
"Custom CSS": "CSS Kustom",
"Dark Mode": "Mode Gelap",
"Default": "Default",
"Default Font": "Font Default",
@@ -28,7 +27,6 @@
"Dictionary": "Kamus",
"Download Readest": "Unduh Readest",
"Edit": "Edit",
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
"Excerpts": "Kutipan",
"Failed to import book(s): {{filenames}}": "Gagal mengimpor buku: {{filenames}}",
"Fast": "Cepat",
@@ -361,5 +359,16 @@
"Left Margin (px)": "Margin Kiri (px)",
"Column Gap (%)": "Jarak Antar Kolom (%)",
"Always Show Status Bar": "Selalu Tampilkan Bilah Status",
"Translation Not Available": "Terjemahan Tidak Tersedia"
"Translation Not Available": "Terjemahan Tidak Tersedia",
"Custom Content CSS": "CSS konten",
"Enter CSS for book content styling...": "Masukkan CSS untuk gaya konten buku...",
"Custom Reader UI CSS": "CSS antarmuka",
"Enter CSS for reader interface styling...": "Masukkan CSS untuk gaya antarmuka pembaca...",
"Crop": "Potong",
"Book Covers": "Sampul Buku",
"Fit": "Pas",
"Reset {{settings}}": "Atur Ulang {{settings}}",
"Reset Settings": "Atur Ulang Pengaturan",
"{{count}} pages left in chapter_other": "{{count}} halaman tersisa di bab ini",
"Show Remaining Pages": "Tampilkan halaman tersisa"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Conferma eliminazione",
"Copied to notebook": "Copiato nel quaderno",
"Copy": "Copia",
"Custom CSS": "CSS personalizzato",
"Dark Mode": "Modalità scura",
"Default": "Predefinito",
"Default Font": "Font predefinito",
@@ -28,7 +27,6 @@
"Dictionary": "Dizionario",
"Download Readest": "Scarica Readest",
"Edit": "Modifica",
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
"Excerpts": "Estratti",
"Failed to import book(s): {{filenames}}": "Impossibile importare il/i libro/i: {{filenames}}",
"Fast": "Veloce",
@@ -367,5 +365,18 @@
"Left Margin (px)": "Margine Sinistro (px)",
"Column Gap (%)": "Spazio tra Colonne (%)",
"Always Show Status Bar": "Mostra sempre la barra di stato",
"Translation Not Available": "Traduzione non disponibile"
"Translation Not Available": "Traduzione non disponibile",
"Custom Content CSS": "CSS contenuto",
"Enter CSS for book content styling...": "Inserisci il CSS per il contenuto del libro...",
"Custom Reader UI CSS": "CSS interfaccia",
"Enter CSS for reader interface styling...": "Inserisci il CSS per l'interfaccia di lettura...",
"Crop": "Ritaglia",
"Book Covers": "Copertine libri",
"Fit": "Adatta",
"Reset {{settings}}": "Reimposta {{settings}}",
"Reset Settings": "Reimposta impostazioni",
"{{count}} pages left in chapter_one": "{{count}} pagina rimasta nel capitolo",
"{{count}} pages left in chapter_many": "{{count}} pagine rimaste nel capitolo",
"{{count}} pages left in chapter_other": "{{count}} pagine rimaste nel capitolo",
"Show Remaining Pages": "Mostra pagine rimanenti"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "削除の確認",
"Copied to notebook": "ノートブックにコピー",
"Copy": "コピー",
"Custom CSS": "カスタムCSS",
"Dark Mode": "ダークモード",
"Default": "デフォルト",
"Default Font": "デフォルトフォント",
@@ -28,7 +27,6 @@
"Dictionary": "辞書",
"Download Readest": "Readestをダウンロード",
"Edit": "編集",
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
"Excerpts": "抜粋",
"Failed to import book(s): {{filenames}}": "書籍のインポートに失敗しました:{{filenames}}",
"Fast": "高速",
@@ -361,5 +359,16 @@
"Left Margin (px)": "左マージン (px)",
"Column Gap (%)": "列間の間隔 (%)",
"Always Show Status Bar": "ステータスバーを常に表示",
"Translation Not Available": "翻訳は利用できません"
"Translation Not Available": "翻訳は利用できません",
"Custom Content CSS": "コンテンツCSS",
"Enter CSS for book content styling...": "本のコンテンツ用CSSを入力...",
"Custom Reader UI CSS": "リーダーUIのCSS",
"Enter CSS for reader interface styling...": "リーダーインターフェース用CSSを入力...",
"Crop": "トリミング",
"Book Covers": "書籍の表紙",
"Fit": "フィット",
"Reset {{settings}}": "{{settings}}をリセット",
"Reset Settings": "設定をリセット",
"{{count}} pages left in chapter_other": "{{count}} ページ残り",
"Show Remaining Pages": "残りを表示"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "삭제 확인",
"Copied to notebook": "노트북에 복사됨",
"Copy": "복사",
"Custom CSS": "사용자 정의 CSS",
"Dark Mode": "다크 모드",
"Default": "기본값",
"Default Font": "기본 글꼴",
@@ -28,7 +27,6 @@
"Dictionary": "사전",
"Download Readest": "Readest 다운로드",
"Edit": "편집",
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
"Excerpts": "발췌",
"Failed to import book(s): {{filenames}}": "책 가져오기 실패: {{filenames}}",
"Fast": "빠름",
@@ -361,5 +359,16 @@
"Left Margin (px)": "왼쪽 여백 (px)",
"Column Gap (%)": "열 간격 (%)",
"Always Show Status Bar": "상태 표시줄 항상 표시",
"Translation Not Available": "번역을 사용할 수 없음"
"Translation Not Available": "번역을 사용할 수 없음",
"Custom Content CSS": "콘텐츠 CSS",
"Enter CSS for book content styling...": "도서 콘텐츠 스타일을 위한 CSS 입력...",
"Custom Reader UI CSS": "리더 UI CSS",
"Enter CSS for reader interface styling...": "리더 인터페이스 스타일용 CSS 입력...",
"Crop": "자르기",
"Book Covers": "책 표지",
"Fit": "맞춤",
"Reset {{settings}}": "{{settings}} 재설정",
"Reset Settings": "설정 재설정",
"{{count}} pages left in chapter_other": "남은 페이지 {{count}}장",
"Show Remaining Pages": "남은 페이지 보기"
}
@@ -189,8 +189,6 @@
"Volume Keys for Page Flip": "Volumetoetsen voor pagina omslaan",
"Clicks for Page Flip": "Klikken voor pagina omslaan",
"Swap Clicks Area": "Klikgebieden omwisselen",
"Custom CSS": "Aangepaste CSS",
"Enter your custom CSS here...": "Voer hier uw aangepaste CSS in...",
"Apply": "Toepassen",
"Font": "Lettertype",
"Layout": "Opmaak",
@@ -364,5 +362,17 @@
"Left Margin (px)": "Linker marge (px)",
"Column Gap (%)": "Kolomafstand (%)",
"Always Show Status Bar": "Statusbalk altijd weergeven",
"Translation Not Available": "Vertaling niet beschikbaar"
"Translation Not Available": "Vertaling niet beschikbaar",
"Custom Content CSS": "Aangepaste inhoud-CSS",
"Enter CSS for book content styling...": "Voer CSS in voor de opmaak van de boekinhoud...",
"Custom Reader UI CSS": "Aangepaste interface-CSS",
"Enter CSS for reader interface styling...": "Voer CSS in voor de opmaak van de leesinterface...",
"Crop": "Bijsnijden",
"Book Covers": "Boekomslagen",
"Fit": "Passend maken",
"Reset {{settings}}": "Reset {{settings}}",
"Reset Settings": "Instellingen resetten",
"{{count}} pages left in chapter_one": "Nog {{count}} pagina in hoofdstuk",
"{{count}} pages left in chapter_other": "Nog {{count}} paginas in hoofdstuk",
"Show Remaining Pages": "Toon resterende paginas"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Potwierdź usunięcie",
"Copied to notebook": "Skopiowano do notatnika",
"Copy": "Kopiuj",
"Custom CSS": "Własny CSS",
"Dark Mode": "Tryb ciemny",
"Default": "Domyślne",
"Default Font": "Domyślna czcionka",
@@ -28,7 +27,6 @@
"Dictionary": "Słownik",
"Download Readest": "Pobierz Readest",
"Edit": "Edytuj",
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
"Excerpts": "Fragmenty",
"Failed to import book(s): {{filenames}}": "Nie udało się zaimportować książek: {{filenames}}",
"Fast": "Szybko",
@@ -370,5 +368,19 @@
"Left Margin (px)": "Lewy margines (px)",
"Column Gap (%)": "Odstęp między kolumnami (%)",
"Always Show Status Bar": "Zawsze pokazuj pasek stanu",
"Translation Not Available": "Brak dostępnego tłumaczenia"
"Translation Not Available": "Brak dostępnego tłumaczenia",
"Custom Content CSS": "CSS treści",
"Enter CSS for book content styling...": "Wprowadź CSS dla stylu treści książki...",
"Custom Reader UI CSS": "CSS interfejsu",
"Enter CSS for reader interface styling...": "Wprowadź CSS dla stylu interfejsu czytnika...",
"Crop": "Przytnij",
"Book Covers": "Okładki książek",
"Fit": "Pasuje",
"Reset {{settings}}": "Zresetuj {{settings}}",
"Reset Settings": "Zresetuj ustawienia",
"{{count}} pages left in chapter_one": "Pozostała {{count}} strona w rozdziale",
"{{count}} pages left in chapter_few": "Pozostały {{count}} strony w rozdziale",
"{{count}} pages left in chapter_many": "Pozostało {{count}} stron w rozdziale",
"{{count}} pages left in chapter_other": "Pozostało {{count}} stron w rozdziale",
"Show Remaining Pages": "Pokaż pozostałe strony"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmar Exclusão",
"Copied to notebook": "Copiado para o caderno",
"Copy": "Copiar",
"Custom CSS": "CSS Personalizado",
"Dark Mode": "Modo Escuro",
"Default": "Padrão",
"Default Font": "Fonte Padrão",
@@ -28,7 +27,6 @@
"Dictionary": "Dicionário",
"Download Readest": "Baixar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
"Excerpts": "Trechos",
"Failed to import book(s): {{filenames}}": "Falha ao importar livro(s): {{filenames}}",
"Fast": "Rápido",
@@ -367,5 +365,18 @@
"Left Margin (px)": "Margem esquerda (px)",
"Column Gap (%)": "Espaço entre colunas (%)",
"Always Show Status Bar": "Exibir sempre a barra de status",
"Translation Not Available": "Tradução não disponível"
"Translation Not Available": "Tradução não disponível",
"Custom Content CSS": "CSS do conteúdo",
"Enter CSS for book content styling...": "Insira o CSS para o estilo do conteúdo do livro...",
"Custom Reader UI CSS": "CSS da interface",
"Enter CSS for reader interface styling...": "Insira o CSS para o estilo da interface de leitura...",
"Crop": "Cortar",
"Book Covers": "Capas de Livros",
"Fit": "Encaixar",
"Reset {{settings}}": "Redefinir {{settings}}",
"Reset Settings": "Redefinir Configurações",
"{{count}} pages left in chapter_one": "Falta {{count}} página neste capítulo",
"{{count}} pages left in chapter_many": "Faltam {{count}} páginas neste capítulo",
"{{count}} pages left in chapter_other": "Faltam {{count}} páginas neste capítulo",
"Show Remaining Pages": "Mostrar páginas restantes"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Подтвердить удаление",
"Copied to notebook": "Скопировано в блокнот",
"Copy": "Копировать",
"Custom CSS": "Пользовательский CSS",
"Dark Mode": "Тёмная тема",
"Default": "По умолчанию",
"Default Font": "Шрифт по умолчанию",
@@ -28,7 +27,6 @@
"Dictionary": "Словарь",
"Download Readest": "Скачать Readest",
"Edit": "Редактировать",
"Enter your custom CSS here...": "Введите ваш пользовательский CSS здесь...",
"Excerpts": "Отрывки",
"Failed to import book(s): {{filenames}}": "Не удалось импортировать книгу(и): {{filenames}}",
"Fast": "Быстро",
@@ -370,5 +368,19 @@
"Left Margin (px)": "Левый отступ (пикс)",
"Column Gap (%)": "Промежуток между колонками (%)",
"Always Show Status Bar": "Всегда показывать панель состояния",
"Translation Not Available": "Перевод недоступен"
"Translation Not Available": "Перевод недоступен",
"Custom Content CSS": "CSS содержимого",
"Enter CSS for book content styling...": "Введите CSS для оформления содержимого книги...",
"Custom Reader UI CSS": "CSS интерфейса",
"Enter CSS for reader interface styling...": "Введите CSS для оформления интерфейса ридера...",
"Crop": "Обрезать",
"Book Covers": "Обложки книг",
"Fit": "Подогнать",
"Reset {{settings}}": "Сбросить {{settings}}",
"Reset Settings": "Сбросить настройки",
"{{count}} pages left in chapter_one": "Осталась {{count}} страница в главе",
"{{count}} pages left in chapter_few": "Осталось {{count}} страницы в главе",
"{{count}} pages left in chapter_many": "Осталось {{count}} страниц в главе",
"{{count}} pages left in chapter_other": "Осталось {{count}} страниц в главе",
"Show Remaining Pages": "Показать оставшиеся страницы"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Silmeyi Onayla",
"Copied to notebook": "Not defterine kopyalandı",
"Copy": "Kopyala",
"Custom CSS": "Özel CSS",
"Dark Mode": "Karanlık Mod",
"Default": "Varsayılan",
"Default Font": "Varsayılan Yazı Tipi",
@@ -28,7 +27,6 @@
"Dictionary": "Sözlük",
"Download Readest": "Readest'i İndir",
"Edit": "Düzenle",
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
"Excerpts": "Alıntılar",
"Failed to import book(s): {{filenames}}": "Kitap(lar) içe aktarılamadı: {{filenames}}",
"Fast": "Hızlı",
@@ -364,5 +362,17 @@
"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",
"Translation Not Available": "Çeviri Mevcut Değil"
"Translation Not Available": "Çeviri Mevcut Değil",
"Custom Content CSS": "Özel İçerik CSS'si",
"Enter CSS for book content styling...": "Kitap içeriği stili için CSS girin...",
"Custom Reader UI CSS": "Özel Okuyucu Arayüzü CSS'si",
"Enter CSS for reader interface styling...": "Okuyucu arayüzü stili için CSS girin...",
"Crop": "Kes",
"Book Covers": "Kitap Kapağı",
"Fit": "Uygun",
"Reset {{settings}}": "{{settings}}'i Sıfırla",
"Reset Settings": "Ayarları Sıfırla",
"{{count}} pages left in chapter_one": "Bu bölümde {{count}} sayfa kaldı",
"{{count}} pages left in chapter_other": "Bu bölümde {{count}} sayfa kaldı",
"Show Remaining Pages": "Kalan sayfaları göster"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Підтвердити видалення",
"Copied to notebook": "Скопійовано до нотатника",
"Copy": "Копіювати",
"Custom CSS": "Користувацький CSS",
"Dark Mode": "Темний режим",
"Default": "За замовчуванням",
"Default Font": "Шрифт за замовчуванням",
@@ -28,7 +27,6 @@
"Dictionary": "Словник",
"Download Readest": "Завантажити Readest",
"Edit": "Редагувати",
"Enter your custom CSS here...": "Введіть ваш користувацький CSS тут...",
"Excerpts": "Уривки",
"Failed to import book(s): {{filenames}}": "Не вдалося імпортувати книгу(и): {{filenames}}",
"Fast": "Швидко",
@@ -370,5 +368,19 @@
"Left Margin (px)": "Лівий відступ (px)",
"Column Gap (%)": "Проміжок між стовпцями (%)",
"Always Show Status Bar": "Завжди показувати панель стану",
"Translation Not Available": "Переклад недоступний"
"Translation Not Available": "Переклад недоступний",
"Custom Content CSS": "CSS вмісту",
"Enter CSS for book content styling...": "Введіть CSS для стилізації вмісту книги...",
"Custom Reader UI CSS": "CSS інтерфейсу",
"Enter CSS for reader interface styling...": "Введіть CSS для стилізації інтерфейсу рідера...",
"Crop": "Обрізати",
"Book Covers": "Обкладинки книг",
"Fit": "Вписати",
"Reset {{settings}}": "Скинути {{settings}}",
"Reset Settings": "Скинути налаштування",
"{{count}} pages left in chapter_one": "Залишилася {{count}} сторінка у розділі",
"{{count}} pages left in chapter_few": "Залишилось {{count}} сторінки у розділі",
"{{count}} pages left in chapter_many": "Залишилось {{count}} сторінок у розділі",
"{{count}} pages left in chapter_other": "Залишилось {{count}} сторінок у розділі",
"Show Remaining Pages": "Показати залишок сторінок"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Xác nhận xóa",
"Copied to notebook": "Đã sao chép vào sổ ghi chú",
"Copy": "Sao chép",
"Custom CSS": "CSS tùy chỉnh",
"Dark Mode": "Chế độ tối",
"Default": "Mặc định",
"Default Font": "Phông chữ mặc định",
@@ -28,7 +27,6 @@
"Dictionary": "Từ điển",
"Download Readest": "Tải Readest",
"Edit": "Chỉnh sửa",
"Enter your custom CSS here...": "Nhập CSS tùy chỉnh của bạn vào đây...",
"Excerpts": "Trích dẫn",
"Failed to import book(s): {{filenames}}": "Không thể nhập sách: {{filenames}}",
"Fast": "Nhanh",
@@ -361,5 +359,16 @@
"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",
"Translation Not Available": "Không có bản dịch"
"Translation Not Available": "Không có bản dịch",
"Custom Content CSS": "CSS nội dung",
"Enter CSS for book content styling...": "Nhập CSS cho phần nội dung sách...",
"Custom Reader UI CSS": "CSS giao diện đọc",
"Enter CSS for reader interface styling...": "Nhập CSS cho giao diện trình đọc...",
"Crop": "Cắt",
"Book Covers": "Bìa sách",
"Fit": "Vừa vặn",
"Reset {{settings}}": "Đặt lại {{settings}}",
"Reset Settings": "Đặt lại cài đặt",
"{{count}} pages left in chapter_other": "Còn {{count}} trang trong chương",
"Show Remaining Pages": "Hiển thị trang còn lại"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "确认删除",
"Copied to notebook": "已复制到笔记本",
"Copy": "复制",
"Custom CSS": "自定义 CSS",
"Dark Mode": "深色主题",
"Default": "默认",
"Default Font": "默认字体",
@@ -28,7 +27,6 @@
"Dictionary": "词典",
"Download Readest": "下载 Readest",
"Edit": "编辑",
"Enter your custom CSS here...": "在此处输入您的自定义 CSS...",
"Excerpts": "摘录",
"Failed to import book(s): {{filenames}}": "导入书籍失败:{{filenames}}",
"Fast": "快",
@@ -361,5 +359,16 @@
"Left Margin (px)": "左边距",
"Column Gap (%)": "列间距",
"Always Show Status Bar": "始终显示状态栏",
"Translation Not Available": "翻译不可用"
"Translation Not Available": "翻译不可用",
"Custom Content CSS": "自定义内容 CSS",
"Enter CSS for book content styling...": "输入书籍内容样式的CSS…",
"Custom Reader UI CSS": "自定义界面 CSS",
"Enter CSS for reader interface styling...": "输入阅读器界面样式的CSS…",
"Crop": "裁剪",
"Book Covers": "书籍封面",
"Fit": "适应",
"Reset {{settings}}": "重置{{settings}}",
"Reset Settings": "重置设置",
"{{count}} pages left in chapter_other": "本章剩余 {{count}} 页",
"Show Remaining Pages": "显示剩余页数"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "確認刪除",
"Copied to notebook": "已複製到筆記本",
"Copy": "複製",
"Custom CSS": "自定義 CSS",
"Dark Mode": "深色主題",
"Default": "預設",
"Default Font": "預設字體",
@@ -28,7 +27,6 @@
"Dictionary": "詞典",
"Download Readest": "下載 Readest",
"Edit": "編輯",
"Enter your custom CSS here...": "在此處輸入您的自定義 CSS...",
"Excerpts": "摘錄",
"Failed to import book(s): {{filenames}}": "導入書籍失敗:{{filenames}}",
"Fast": "快",
@@ -361,5 +359,16 @@
"Left Margin (px)": "左邊距",
"Column Gap (%)": "欄間距",
"Always Show Status Bar": "始終顯示狀態欄",
"Translation Not Available": "翻譯不可用"
"Translation Not Available": "翻譯不可用",
"Custom Content CSS": "自訂內容 CSS",
"Enter CSS for book content styling...": "輸入書籍內容樣式的CSS…",
"Custom Reader UI CSS": "自訂介面 CSS",
"Enter CSS for reader interface styling...": "輸入閱讀器介面樣式的CSS…",
"Crop": "裁剪",
"Book Covers": "書籍封面",
"Fit": "適應",
"Reset {{settings}}": "重置{{settings}}",
"Reset Settings": "重置設置",
"{{count}} pages left in chapter_other": "本章剩餘 {{count}} 頁",
"Show Remaining Pages": "顯示剩餘頁數"
}
+13
View File
@@ -1,5 +1,18 @@
{
"releases": {
"0.9.61": {
"date": "2025-06-26",
"notes": [
"Layout: Fixed view menu width on smaller screens",
"Layout: Added option to crop or fit book covers in library view",
"Reader: You can now apply custom CSS to the reading interface (UI)",
"Reader: Added an option to show remaining pages in the current chapter",
"Settings: Added options to reset settings from the config dialog",
"Import: Fixed encoded filenames when importing TXT files on iOS",
"Linux: Fixed missing app icon on some Linux distributions",
"GitHub: Added donation link to support Readest development"
]
},
"0.9.60": {
"date": "2025-06-22",
"notes": [
+1 -1
View File
@@ -32,7 +32,7 @@ reqwest = { version = "0.12", default-features = false, features = [
] }
# FIXME: remove the devtools feature in production
tauri = { version = "2.5.1", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.1.1"
tauri-build = "2.3.0"
tauri-plugin-log = "2.4.0"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
@@ -9,10 +9,11 @@
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:largeHeap="true"
android:label="@string/app_name"
android:theme="@style/Theme.readest"
android:hardwareAccelerated="true"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
@@ -41,15 +42,15 @@
<data android:mimeType="application/zip" />
<data android:mimeType="text/plain" />
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.pdf" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.fb2.zip" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.azw" />
<data android:pathPattern=".*\\.cbz" />
<data android:pathPattern=".*\\.zip" />
<data android:pathPattern=".*\\.txt" />
<data android:pathPattern="/.*\\.epub" />
<data android:pathPattern="/.*\\.pdf" />
<data android:pathPattern="/.*\\.fb2" />
<data android:pathPattern="/.*\\.fb2.zip" />
<data android:pathPattern="/.*\\.mobi" />
<data android:pathPattern="/.*\\.azw" />
<data android:pathPattern="/.*\\.cbz" />
<data android:pathPattern="/.*\\.zip" />
<data android:pathPattern="/.*\\.txt" />
</intent-filter>
<intent-filter android:label="oauth">
@@ -69,10 +70,9 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="web.readest.com" />
</intent-filter>
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity>
@@ -81,39 +81,15 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
updateTaskDescription()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateTaskDescription()
}
override fun onResume() {
super.onResume()
updateTaskDescription()
}
private fun updateTaskDescription() {
val backgroundColor = if (isDarkTheme()) {
Color.parseColor("#2D2D2D")
} else {
Color.parseColor("#FFFFFF")
}
setTaskDescription(
ActivityManager.TaskDescription(
"Readest",
null,
backgroundColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setTaskDescription(
ActivityManager.TaskDescription(
getString(R.string.app_name),
null,
Color.TRANSPARENT
)
)
)
}
private fun isDarkTheme(): Boolean {
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> true
else -> false
}
}
}
+25 -25
View File
@@ -1,42 +1,42 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { handleAuthCallback } from '@/helpers/auth';
import { useAuth } from '@/context/AuthContext';
import { handleAuthCallback } from '@/helpers/auth';
export default function AuthCallback() {
const router = useRouter();
const { login } = useAuth();
if (typeof window === 'undefined') {
return null;
}
const hash = window.location.hash || '';
const params = new URLSearchParams(hash.slice(1));
useEffect(() => {
const hash = window.location.hash || '';
const params = new URLSearchParams(hash.slice(1));
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
const next = params.get('next') ?? '/';
const error = params.get('error');
const errorDescription = params.get('error_description');
const errorCode = params.get('error_code');
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
const next = params.get('next') ?? '/';
const error = params.get('error');
const errorDescription = params.get('error_description');
const errorCode = params.get('error_code');
handleAuthCallback({
accessToken,
refreshToken,
type,
next,
error,
errorCode,
errorDescription,
login,
navigate: router.push,
});
handleAuthCallback({
accessToken,
refreshToken,
type,
next,
error,
errorCode,
errorDescription,
login,
navigate: router.push,
});
}, [login, router]);
return (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<span className='loading loading-infinity loading-xl w-20'></span>
<span className='loading loading-infinity loading-xl w-20' />
</div>
);
}
@@ -11,15 +11,16 @@ import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { navigateToLogin } from '@/utils/nav';
import { formatAuthors } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
import BookCover from '@/components/BookCover';
interface BookItemProps {
mode: LibraryViewModeType;
book: Book;
mode: LibraryViewModeType;
coverFit: LibraryCoverFitType;
isSelectMode: boolean;
selectedBooks: string[];
transferProgress: number | null;
@@ -29,8 +30,9 @@ interface BookItemProps {
}
const BookItem: React.FC<BookItemProps> = ({
mode,
book,
mode,
coverFit,
isSelectMode,
selectedBooks,
transferProgress,
@@ -59,11 +61,11 @@ const BookItem: React.FC<BookItemProps> = ({
>
<div
className={clsx(
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
'relative flex aspect-[28/41] items-center justify-center',
mode === 'list' && 'min-w-20',
)}
>
<BookCover mode={mode} book={book} />
<BookCover mode={mode} book={book} coverFit={coverFit} className='drop-shadow-md' />
{selectedBooks.includes(book.hash) && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
@@ -6,7 +6,7 @@ import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-ico
import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
@@ -66,6 +66,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [sortOrder, setSortOrder] = useState(
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
);
const [coverFit, setCoverFit] = useState(searchParams?.get('cover') || settings.libraryCoverFit);
const isImportingBook = useRef(false);
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
@@ -106,6 +107,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const view = searchParams?.get('view') || settings.libraryViewMode;
const sort = searchParams?.get('sort') || settings.librarySortBy;
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
const cover = searchParams?.get('cover') || settings.libraryCoverFit;
const params = new URLSearchParams(searchParams?.toString());
if (query) {
params.set('q', query);
@@ -132,6 +134,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
} else {
params.delete('view');
}
setCoverFit(cover);
if (cover === 'crop') {
params.delete('cover');
}
if (sort === 'updated' && order === 'desc' && view === 'grid') {
params.delete('sort');
params.delete('order');
@@ -287,9 +293,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
>
{filteredBookshelfItems.map((item, index) => (
<BookshelfItem
key={`library-item-${index}`}
mode={viewMode as LibraryViewModeType}
key={`library-item-${'hash' in item ? item.hash : item.id}-${index}`}
item={item}
mode={viewMode as LibraryViewModeType}
coverFit={coverFit as LibraryCoverFitType}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
setLoading={setLoading}
@@ -10,7 +10,7 @@ import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { getOSPlatform } from '@/utils/misc';
import { getLocalBookFilename } from '@/utils/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { Book, BookGroupType, BooksGroup } from '@/types/book';
@@ -73,6 +73,7 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
interface BookshelfItemProps {
mode: LibraryViewModeType;
item: BookshelfItem;
coverFit: LibraryCoverFitType;
isSelectMode: boolean;
selectedBooks: string[];
transferProgress: number | null;
@@ -88,6 +89,7 @@ interface BookshelfItemProps {
const BookshelfItem: React.FC<BookshelfItemProps> = ({
mode,
item,
coverFit,
isSelectMode,
selectedBooks,
transferProgress,
@@ -276,6 +278,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
<BookItem
mode={mode}
book={item}
coverFit={coverFit}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
transferProgress={transferProgress}
@@ -22,7 +22,7 @@ import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
import ImportMenu from './ImportMenu';
import SortMenu from './SortMenu';
import ViewMenu from './ViewMenu';
interface LibraryHeaderProps {
isSelectMode: boolean;
@@ -228,7 +228,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<SortMenu />
<ViewMenu />
</Dropdown>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
@@ -4,15 +4,15 @@ import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
interface SortMenuProps {
interface ViewMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
@@ -22,12 +22,18 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const viewMode = settings.libraryViewMode;
const sortBy = settings.librarySortBy;
const isAscending = settings.librarySortAscending;
const coverFit = settings.libraryCoverFit;
const viewOptions = [
{ label: _('List'), value: 'list' },
{ label: _('Grid'), value: 'grid' },
];
const coverFitOptions = [
{ label: _('Crop'), value: 'crop' },
{ label: _('Fit'), value: 'fit' },
];
const sortByOptions = [
{ label: _('Title'), value: 'title' },
{ label: _('Author'), value: 'author' },
@@ -52,6 +58,17 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
navigateToLibrary(router, `${params.toString()}`);
};
const handleToggleCropCovers = (value: LibraryCoverFitType) => {
settings.libraryCoverFit = value;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
params.set('cover', value);
navigateToLibrary(router, `${params.toString()}`);
};
const handleSetSortBy = (value: LibrarySortByType) => {
settings.librarySortBy = value;
setSettings(settings);
@@ -89,6 +106,22 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Book Covers')}
buttonClass='h-8'
labelClass='text-sm sm:text-xs'
disabled
/>
{coverFitOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={coverFit === option.value ? MdCheck : undefined}
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Sort by...')}
buttonClass='h-8'
@@ -118,4 +151,4 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
);
};
export default SortMenu;
export default ViewMenu;
@@ -118,7 +118,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4'>
<div className='bg-base-100 sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4 pe-2'>
<div className='hidden sm:flex'>
<SidebarToggler bookKey={bookKey} />
</div>
@@ -132,7 +132,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
</h2>
</div>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4 ps-2'>
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
@@ -29,7 +29,8 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getViewSettings } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
@@ -47,9 +48,12 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
totalPage: pageinfo.total,
})
: '';
const timeInfo = timeinfo
const timeLeft = timeinfo
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
: '';
const { page = 0, pages = 0 } = view?.renderer || {};
const pageLeft =
pages - 1 > page ? _('{{count}} pages left in chapter', { count: pages - 1 - page }) : '';
return (
<div
@@ -76,7 +80,11 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
}
}
>
{viewSettings.showRemainingTime && <span className='text-start'>{timeInfo}</span>}
{viewSettings.showRemainingTime ? (
<span className='text-start'>{timeLeft}</span>
) : viewSettings.showRemainingPages ? (
<span className='text-start'>{pageLeft}</span>
) : null}
{viewSettings.showPageNumber && <span className='ms-auto text-end'>{pageInfo}</span>}
</div>
);
@@ -107,7 +107,14 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
return (
<div
tabIndex={0}
className='view-menu dropdown-content bgcolor-base-200 dropdown-right no-triangle border-base-200 z-20 mt-1 border shadow-2xl'
className={clsx(
'view-menu dropdown-content dropdown-right no-triangle z-20 mt-1 border',
'bgcolor-base-200 border-base-200 shadow-2xl',
)}
style={{
maxWidth: `${window.innerWidth - 40}px`,
marginRight: window.innerWidth < 640 ? '-36px' : '0px',
}}
>
<div className={clsx('flex items-center justify-between rounded-md')}>
<button
@@ -18,12 +18,14 @@ import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { CODE_LANGUAGES, CodeLanguage, manageSyntaxHighlighting } from '@/utils/highlightjs';
import { SettingsPanelPanelProp } from './SettingsDialog';
import Select from '@/components/Select';
import ThemeEditor from './ThemeEditor';
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor, saveCustomTheme } =
useThemeStore();
@@ -44,6 +46,24 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [codeHighlighting, setcodeHighlighting] = useState(viewSettings.codeHighlighting!);
const [codeLanguage, setCodeLanguage] = useState(viewSettings.codeLanguage!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
overrideColor: setOverrideColor,
invertImgColorInDark: setInvertImgColorInDark,
codeHighlighting: setcodeHighlighting,
codeLanguage: setCodeLanguage,
});
setThemeColor('default');
setThemeMode('auto');
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (invertImgColorInDark === viewSettings.invertImgColorInDark) return;
saveViewSettings(envConfig, bookKey, 'invertImgColorInDark', invertImgColorInDark);
@@ -7,9 +7,12 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import NumberInput from './NumberInput';
const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings } = useReaderStore();
@@ -27,6 +30,26 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [animated, setAnimated] = useState(viewSettings.animated!);
const [allowScript, setAllowScript] = useState(viewSettings.allowScript!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
scrolled: setScrolledMode,
continuousScroll: setIsContinuousScroll,
scrollingOverlap: setScrollingOverlap,
volumeKeysToFlip: setVolumeKeysToFlip,
disableClick: setIsDisableClick,
swapClickArea: setSwapClickArea,
animated: setAnimated,
allowScript: setAllowScript,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isScrolledMode === viewSettings.scrolled) return;
saveViewSettings(envConfig, bookKey, 'scrolled', isScrolledMode);
@@ -85,7 +108,7 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (viewSettings.allowScript === allowScript) return;
saveViewSettings(envConfig, bookKey, 'allowScript', allowScript, true, false);
setTimeout(() => window.location.reload(), 100);
setTimeout(() => window.location.reload(), RELOAD_BEFREE_SAVED_TIMEOUT_MS);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allowScript]);
@@ -3,20 +3,30 @@ import React from 'react';
import { MdCheck } from 'react-icons/md';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { SettingsPanelType } from './SettingsDialog';
import MenuItem from '@/components/MenuItem';
interface DialogMenuProps {
toggleDropdown?: () => void;
activePanel: SettingsPanelType;
setIsDropdownOpen?: (open: boolean) => void;
onReset: () => void;
resetLabel?: string;
}
const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
const DialogMenu: React.FC<DialogMenuProps> = ({ setIsDropdownOpen, onReset, resetLabel }) => {
const _ = useTranslation();
const iconSize = useDefaultIconSize();
const iconSize = useResponsiveSize(16);
const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useSettingsStore();
const handleToggleGlobal = () => {
setFontLayoutSettingsGlobal(!isFontLayoutSettingsGlobal);
toggleDropdown?.();
setIsDropdownOpen?.(false);
};
const handleResetToDefaults = () => {
onReset();
setIsDropdownOpen?.(false);
};
return (
@@ -27,24 +37,18 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
'text-base sm:text-sm',
)}
>
<button
className='hover:bg-base-200 text-base-content flex w-full items-center justify-between rounded-md p-1'
<MenuItem
label={_('Global Settings')}
tooltip={isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')}
buttonClass='lg:tooltip'
Icon={
isFontLayoutSettingsGlobal ? (
<MdCheck size={iconSize} className='text-base-content' />
) : null
}
onClick={handleToggleGlobal}
>
<div className='flex items-center'>
<span style={{ minWidth: `${iconSize}px` }}>
{isFontLayoutSettingsGlobal && <MdCheck className='text-base-content' />}
</span>
<div
className='lg:tooltip'
data-tip={
isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')
}
>
<span className='ml-2 whitespace-nowrap'>{_('Global Settings')}</span>
</div>
</div>
</button>
/>
<MenuItem label={resetLabel || _('Reset Settings')} onClick={handleResetToDefaults} />
</div>
);
};
@@ -24,6 +24,8 @@ import { getOSPlatform, isCJKEnv } from '@/utils/misc';
import { getSysFontsList } from '@/utils/bridge';
import { isTauriAppPlatform } from '@/services/environment';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
import FontDropdown from './FontDropDown';
@@ -79,7 +81,7 @@ const FontFace = ({
);
};
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getView, getViewSettings } = useReaderStore();
@@ -119,10 +121,10 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
break;
}
const [sysFonts, setSysFonts] = useState<string[]>(defaultSysFonts);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultCJKFont, setDefaultCJKFont] = useState(viewSettings.defaultCJKFont!);
const [serifFont, setSerifFont] = useState(viewSettings.serifFont!);
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
@@ -132,6 +134,27 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
return genCJKFontsList(sysFonts);
});
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
defaultFont: setDefaultFont,
defaultFontSize: setDefaultFontSize,
minimumFontSize: setMinFontSize,
overrideFont: setOverrideFont,
defaultCJKFont: setDefaultCJKFont,
serifFont: setSerifFont,
sansSerifFont: setSansSerifFont,
monospaceFont: setMonospaceFont,
fontWeight: setFontWeight,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setCJKFonts((prev) => {
const newFonts = genCJKFontsList(sysFonts);
@@ -8,20 +8,39 @@ import { useTranslation } from '@/hooks/useTranslation';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { getTranslators } from '@/services/translators';
import { TRANSLATED_LANGS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { initDayjs } from '@/utils/time';
import Select from '@/components/Select';
const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { token } = useAuth();
const { envConfig } = useEnv();
const { getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [uiLanguage, setUILanguage] = useState(viewSettings.uiLanguage!);
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
uiLanguage: setUILanguage,
translationEnabled: setTranslationEnabled,
translationProvider: setTranslationProvider,
translateTargetLang: setTranslateTargetLang,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getCurrentUILangOption = () => {
const uiLanguage = viewSettings.uiLanguage;
return {
@@ -43,10 +62,7 @@ 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);
const locale = option ? option : navigator.language;
i18n.changeLanguage(locale);
initDayjs(locale);
setUILanguage(option);
};
const getTranslationProviderOptions = () => {
@@ -97,6 +113,15 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setViewSettings(bookKey, { ...viewSettings });
};
useEffect(() => {
if (uiLanguage === viewSettings.uiLanguage) return;
saveViewSettings(envConfig, bookKey, 'uiLanguage', uiLanguage, false, false);
const locale = uiLanguage ? uiLanguage : navigator.language;
i18n.changeLanguage(locale);
initDayjs(locale);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uiLanguage]);
useEffect(() => {
if (translationEnabled === viewSettings.translationEnabled) return;
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
@@ -1,23 +1,27 @@
import React, { useEffect, useState } from 'react';
import { MdOutlineAutoMode } from 'react-icons/md';
import { MdOutlineAutoMode, MdOutlineScreenRotation } from 'react-icons/md';
import { MdOutlineTextRotationNone, MdTextRotateVertical } from 'react-icons/md';
import { IoPhoneLandscapeOutline, IoPhonePortraitOutline } from 'react-icons/io5';
import { TbTextDirectionRtl } from 'react-icons/tb';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { isCJKEnv } from '@/utils/misc';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { lockScreenOrientation } from '@/utils/bridge';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import { MIGHT_BE_RTL_LANGS } from '@/services/constants';
import { MIGHT_BE_RTL_LANGS, RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
const { getBookData } = useBookDataStore();
const view = getView(bookKey);
@@ -60,7 +64,49 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [showFooter, setShowFooter] = useState(viewSettings.showFooter!);
const [showBarsOnScroll, setShowBarsOnScroll] = useState(viewSettings.showBarsOnScroll!);
const [showRemainingTime, setShowRemainingTime] = useState(viewSettings.showRemainingTime!);
const [showRemainingPages, setShowRemainingPages] = useState(viewSettings.showRemainingPages!);
const [showPageNumber, setShowPageNumber] = useState(viewSettings.showPageNumber!);
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
paragraphMargin: setParagraphMargin,
lineHeight: setLineHeight,
wordSpacing: setWordSpacing,
letterSpacing: setLetterSpacing,
textIndent: setTextIndent,
fullJustification: setFullJustification,
hyphenation: setHyphenation,
marginTopPx: setMarginTopPx,
marginBottomPx: setMarginBottomPx,
marginLeftPx: setMarginLeftPx,
marginRightPx: setMarginRightPx,
compactMarginTopPx: setCompactMarginTopPx,
compactMarginBottomPx: setCompactMarginBottomPx,
compactMarginLeftPx: setCompactMarginLeftPx,
compactMarginRightPx: setCompactMarginRightPx,
gapPercent: setGapPercent,
maxColumnCount: setMaxColumnCount,
maxInlineSize: setMaxInlineSize,
maxBlockSize: setMaxBlockSize,
overrideLayout: setOverrideLayout,
doubleBorder: setDoubleBorder,
borderColor: setBorderColor,
showHeader: setShowHeader,
showFooter: setShowFooter,
showBarsOnScroll: setShowBarsOnScroll,
showRemainingTime: setShowRemainingTime,
showRemainingPages: setShowRemainingPages,
showPageNumber: setShowPageNumber,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'paragraphMargin', paragraphMargin);
@@ -234,7 +280,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
(['horizontal-rl', 'vertical-rl'].includes(writingMode) ||
['horizontal-rl', 'vertical-rl'].includes(prevWritingMode))
) {
setTimeout(() => window.location.reload(), 100);
setTimeout(() => window.location.reload(), RELOAD_BEFREE_SAVED_TIMEOUT_MS);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [writingMode]);
@@ -266,6 +312,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showRemainingTime]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showRemainingPages', showRemainingPages, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showRemainingPages]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showPageNumber', showPageNumber, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -297,6 +348,14 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showFooter]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'screenOrientation', screenOrientation, false, false);
if (appService?.isMobileApp) {
lockScreenOrientation({ orientation: screenOrientation });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenOrientation]);
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
const mightBeRTLBook = MIGHT_BE_RTL_LANGS.includes(langCode) || isCJKEnv();
const isVertical = viewSettings.vertical || writingMode.includes('vertical');
@@ -566,7 +625,31 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
className='toggle'
checked={showRemainingTime}
disabled={!showFooter}
onChange={() => setShowRemainingTime(!showRemainingTime)}
onChange={() => {
if (!showRemainingTime) {
setShowRemainingTime(true);
setShowRemainingPages(false);
} else {
setShowRemainingTime(false);
}
}}
/>
</div>
<div className='config-item'>
<span className=''>{_('Show Remaining Pages')}</span>
<input
type='checkbox'
className='toggle'
checked={showRemainingPages}
disabled={!showFooter}
onChange={() => {
if (!showRemainingPages) {
setShowRemainingPages(true);
setShowRemainingTime(false);
} else {
setShowRemainingPages(false);
}
}}
/>
</div>
<div className='config-item'>
@@ -591,6 +674,47 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</div>
</div>
</div>
{appService?.isMobileApp && (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Orientation')}</span>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('auto')}
>
<MdOutlineScreenRotation />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Portrait')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'portrait' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('portrait')}
>
<IoPhonePortraitOutline />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Landscape')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'landscape' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('landscape')}
>
<IoPhoneLandscapeOutline />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
@@ -1,70 +1,116 @@
import clsx from 'clsx';
import cssbeautify from 'cssbeautify';
import React, { useEffect, useRef, useState } from 'react';
import { IoPhoneLandscapeOutline, IoPhonePortraitOutline } from 'react-icons/io5';
import { MdOutlineScreenRotation } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { getStyles } from '@/utils/style';
import { lockScreenOrientation } from '@/utils/bridge';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import cssbeautify from 'cssbeautify';
import cssValidate from '@/utils/css';
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
type CSSType = 'book' | 'reader';
const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!);
const [draftContentStylesheet, setDraftContentStylesheet] = useState(viewSettings.userStylesheet);
const [draftContentStylesheetSaved, setDraftContentStylesheetSaved] = useState(true);
const [contentError, setContentError] = useState<string | null>(null);
const [draftUIStylesheet, setDraftUIStylesheet] = useState(viewSettings.userUIStylesheet);
const [draftUIStylesheetSaved, setDraftUIStylesheetSaved] = useState(true);
const [uiError, setUIError] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [inputFocusInAndroid, setInputFocusInAndroid] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const contentTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const uiTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const handleUserStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const cssInput = e.target.value;
setDraftStylesheet(cssInput);
setDraftStylesheetSaved(false);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
userStylesheet: setDraftContentStylesheet,
userUIStylesheet: setDraftUIStylesheet,
});
applyStyles('book', true);
applyStyles('reader', true);
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const validateCSS = (cssInput: string): { isValid: boolean; error?: string } => {
if (!cssInput.trim()) return { isValid: true };
try {
const { isValid, error } = cssValidate(cssInput);
if (cssInput && !isValid) {
throw new Error(error || 'Invalid CSS');
if (!isValid) {
return { isValid: false, error: error || 'Invalid CSS' };
}
setError(null);
return { isValid: true };
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Invalid CSS: Please check your input.');
return { isValid: false, error: err.message };
}
console.log('CSS Error:', err);
return { isValid: false, error: 'Invalid CSS: Please check your input.' };
}
};
const applyStyles = () => {
const formattedCSS = cssbeautify(draftStylesheet, {
const handleStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>, type: CSSType) => {
const cssInput = e.target.value;
if (type === 'book') {
setDraftContentStylesheet(cssInput);
setDraftContentStylesheetSaved(false);
const { isValid, error } = validateCSS(cssInput);
setContentError(isValid ? null : error || 'Invalid CSS');
} else {
setDraftUIStylesheet(cssInput);
setDraftUIStylesheetSaved(false);
const { isValid, error } = validateCSS(cssInput);
setUIError(isValid ? null : error || 'Invalid CSS');
}
};
const applyStyles = (type: CSSType, clear = false) => {
const cssInput = type === 'book' ? draftContentStylesheet : draftUIStylesheet;
const formattedCSS = cssbeautify(clear ? '' : cssInput, {
indent: ' ',
openbrace: 'end-of-line',
autosemicolon: true,
});
setDraftStylesheet(formattedCSS);
setDraftStylesheetSaved(true);
viewSettings.userStylesheet = formattedCSS;
setViewSettings(bookKey, { ...viewSettings });
if (type === 'book') {
setDraftContentStylesheet(formattedCSS);
setDraftContentStylesheetSaved(true);
viewSettings.userStylesheet = formattedCSS;
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userStylesheet = formattedCSS;
setSettings(settings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userStylesheet = formattedCSS;
setSettings(settings);
}
} else {
setDraftUIStylesheet(formattedCSS);
setDraftUIStylesheetSaved(true);
viewSettings.userUIStylesheet = formattedCSS;
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userUIStylesheet = formattedCSS;
setSettings(settings);
}
}
setViewSettings(bookKey, { ...viewSettings });
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings));
};
@@ -73,7 +119,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
e.nativeEvent.stopImmediatePropagation();
};
const handleInputFocus = () => {
const handleInputFocus = (textareaRef: React.RefObject<HTMLTextAreaElement>) => {
if (appService?.isAndroidApp) {
setInputFocusInAndroid(true);
}
@@ -93,13 +139,53 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
};
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'screenOrientation', screenOrientation, false, false);
if (appService?.isMobileApp) {
lockScreenOrientation({ orientation: screenOrientation });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenOrientation]);
const renderCSSEditor = (
type: CSSType,
title: string,
placeholder: string,
value: string,
error: string | null,
saved: boolean,
textareaRef: React.RefObject<HTMLTextAreaElement>,
) => (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_(title)}</h2>
<div
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
>
<div className='relative p-1'>
<textarea
ref={textareaRef}
className={clsx(
'textarea textarea-ghost h-48 w-full border-0 p-3 text-base !outline-none sm:text-sm',
'placeholder:text-base-content/70',
)}
placeholder={_(placeholder)}
spellCheck='false'
value={value}
onFocus={() => handleInputFocus(textareaRef)}
onBlur={handleInputBlur}
onInput={handleInput}
onKeyDown={handleInput}
onKeyUp={handleInput}
onChange={(e) => handleStylesheetChange(e, type)}
/>
<button
className={clsx(
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
saved ? 'hidden' : '',
error ? 'btn-disabled' : '',
)}
onClick={() => applyStyles(type)}
disabled={!!error}
>
{_('Apply')}
</button>
</div>
</div>
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
</div>
);
return (
<div
@@ -108,84 +194,25 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
inputFocusInAndroid && 'h-[50%] overflow-y-auto pb-[200px]',
)}
>
{appService?.isMobileApp && (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Orientation')}</span>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('auto')}
>
<MdOutlineScreenRotation />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Portrait')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'portrait' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('portrait')}
>
<IoPhonePortraitOutline />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Landscape')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'landscape' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('landscape')}
>
<IoPhoneLandscapeOutline />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{renderCSSEditor(
'book',
_('Custom Content CSS'),
_('Enter CSS for book content styling...'),
draftContentStylesheet,
contentError,
draftContentStylesheetSaved,
contentTextareaRef,
)}
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Custom CSS')}</h2>
<div
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
>
<div className='relative p-1'>
<textarea
ref={textareaRef}
className={clsx(
'textarea textarea-ghost h-48 w-full border-0 p-3 text-base !outline-none sm:text-sm',
'placeholder:text-base-content/70',
)}
placeholder={_('Enter your custom CSS here...')}
spellCheck='false'
value={draftStylesheet}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onInput={handleInput}
onKeyDown={handleInput}
onKeyUp={handleInput}
onChange={handleUserStylesheetChange}
/>
<button
className={clsx(
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
draftStylesheetSaved ? 'hidden' : '',
error ? 'btn-disabled' : '',
)}
onClick={applyStyles}
disabled={!!error}
>
{_('Apply')}
</button>
</div>
</div>
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
</div>
{renderCSSEditor(
'reader',
_('Custom Reader UI CSS'),
_('Enter CSS for reader interface styling...'),
draftUIStylesheet,
uiError,
draftUIStylesheetSaved,
uiTextareaRef,
)}
</div>
);
};
@@ -67,7 +67,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
return (
<div className={clsx('config-item', className)}>
<span className='text-base-content'>{label}</span>
<span className='text-base-content line-clamp-2'>{label}</span>
<div className='text-base-content flex items-center gap-2'>
<input
type='text'
@@ -75,7 +75,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
value={localValue}
onChange={handleChange}
onBlur={handleOnBlur}
className='input input-ghost settings-content text-base-content w-20 max-w-xs rounded border-0 bg-transparent px-3 py-1 text-right !outline-none'
className='input input-ghost settings-content text-base-content w-16 max-w-xs rounded border-0 bg-transparent py-1 pe-3 ps-1 text-right !outline-none'
onFocus={(e) => e.target.select()}
/>
<button
@@ -22,7 +22,11 @@ import ControlPanel from './ControlPanel';
import LangPanel from './LangPanel';
import MiscPanel from './MiscPanel';
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
export type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
export type SettingsPanelPanelProp = {
bookKey: string;
onRegisterReset: (resetFn: () => void) => void;
};
type TabConfig = {
tab: SettingsPanelType;
@@ -84,6 +88,28 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
localStorage.setItem('lastConfigPanel', tab);
};
const [resetFunctions, setResetFunctions] = useState<
Record<SettingsPanelType, (() => void) | null>
>({
Font: null,
Layout: null,
Color: null,
Control: null,
Language: null,
Custom: null,
});
const registerResetFunction = (panel: SettingsPanelType, resetFn: () => void) => {
setResetFunctions((prev) => ({ ...prev, [panel]: resetFn }));
};
const handleResetCurrentPanel = () => {
const resetFn = resetFunctions[activePanel];
if (resetFn) {
resetFn();
}
};
const handleClose = () => {
setFontLayoutSettingsDialogOpen(false);
};
@@ -130,6 +156,8 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
};
}, []);
const currentPanel = tabConfig.find((tab) => tab.tab === activePanel);
return (
<Dialog
isOpen={true}
@@ -140,7 +168,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
header={
<div className='flex w-full flex-col items-center'>
<div className='tab-title flex pb-2 text-base font-semibold sm:hidden'>
{tabConfig.find((tab) => tab.tab === activePanel)?.label || ''}
{currentPanel?.label || ''}
</div>
<div className='flex w-full flex-row items-center justify-between'>
<button
@@ -184,7 +212,15 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 flex items-center justify-center'
toggleButton={<PiDotsThreeVerticalBold />}
>
<DialogMenu />
<DialogMenu
activePanel={activePanel}
onReset={handleResetCurrentPanel}
resetLabel={
currentPanel
? _('Reset {{settings}}', { settings: currentPanel.label })
: undefined
}
/>
</Dropdown>
<button
onClick={handleClose}
@@ -209,12 +245,39 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
</div>
}
>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Control' && <ControlPanel bookKey={bookKey} />}
{activePanel === 'Language' && <LangPanel bookKey={bookKey} />}
{activePanel === 'Custom' && <MiscPanel bookKey={bookKey} />}
{activePanel === 'Font' && (
<FontPanel bookKey={bookKey} onRegisterReset={(fn) => registerResetFunction('Font', fn)} />
)}
{activePanel === 'Layout' && (
<LayoutPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Layout', fn)}
/>
)}
{activePanel === 'Color' && (
<ColorPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Color', fn)}
/>
)}
{activePanel === 'Control' && (
<ControlPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Control', fn)}
/>
)}
{activePanel === 'Language' && (
<LangPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Language', fn)}
/>
)}
{activePanel === 'Custom' && (
<MiscPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Custom', fn)}
/>
)}
</Dialog>
);
};
@@ -1,8 +1,6 @@
import { useReaderStore } from '@/store/readerStore';
import { useNotebookStore } from '@/store/notebookStore';
import { isTauriAppPlatform } from '@/services/environment';
import useShortcuts from '@/hooks/useShortcuts';
import useBooksManager from './useBooksManager';
import { useSidebarStore } from '@/store/sidebarStore';
import { useSettingsStore } from '@/store/settingsStore';
import { getStyles } from '@/utils/style';
@@ -10,6 +8,8 @@ import { tauriHandleToggleFullScreen, tauriQuitApp } from '@/utils/window';
import { eventDispatcher } from '@/utils/event';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
import { viewPagination } from './usePagination';
import useShortcuts from '@/hooks/useShortcuts';
import useBooksManager from './useBooksManager';
interface UseBookShortcutsProps {
sideBarBookKey: string | null;
@@ -0,0 +1,28 @@
import { Dispatch, SetStateAction } from 'react';
import { useEnv } from '@/context/EnvContext';
import { ViewSettings } from '@/types/book';
type SetterKey = keyof ViewSettings;
type SetterValue = SetStateAction<string> & SetStateAction<number> & SetStateAction<boolean>;
type StateSetters = Partial<{
[Key in SetterKey]: Dispatch<SetterValue>;
}>;
export const useResetViewSettings = () => {
const { appService } = useEnv();
const resetToDefaults = (setters: StateSetters) => {
if (!appService) return;
const defaultSettings = appService.getDefaultViewSettings();
Object.entries(setters).forEach(([settingKey, setter]) => {
const freshValue = defaultSettings[settingKey as SetterKey];
if (freshValue !== undefined) {
setter(freshValue as SetterValue);
}
});
};
return resetToDefaults;
};
+21 -6
View File
@@ -1,23 +1,38 @@
import clsx from 'clsx';
import Image from 'next/image';
import { Book } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { formatAuthors, formatTitle } from '@/utils/book';
interface BookCoverProps {
book: Book;
isPreview?: boolean;
mode?: LibraryViewModeType;
coverFit?: LibraryCoverFitType;
className?: string;
imageClassName?: string;
isPreview?: boolean;
}
const BookCover: React.FC<BookCoverProps> = ({ mode = 'grid', book, isPreview }) => {
const BookCover: React.FC<BookCoverProps> = ({
book,
mode = 'grid',
coverFit = 'crop',
className,
imageClassName,
isPreview,
}) => {
return (
<div className={clsx('relative flex h-full w-full')}>
<div className={clsx('relative flex h-full w-full', className)}>
<Image
src={book.coverImageUrl!}
alt={book.title}
fill={true}
className='object-cover'
className={clsx(
coverFit === 'crop'
? 'object-cover'
: `object-contain ${mode === 'grid' ? 'object-bottom' : ''}`,
imageClassName,
)}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
@@ -27,7 +42,7 @@ const BookCover: React.FC<BookCoverProps> = ({ mode = 'grid', book, isPreview })
className={clsx(
'invisible absolute inset-0 rounded-none p-2',
'text-neutral-content text-center font-serif font-medium',
isPreview && 'bg-base-200/50',
isPreview ? 'bg-base-200/50' : 'bg-base-100',
)}
>
<div className='flex h-1/2 items-center justify-center'>
+1 -1
View File
@@ -36,7 +36,7 @@ const Dropdown: React.FC<DropdownProps> = ({
: children;
return (
<div className='dropdown-container'>
<div className='dropdown-container flex'>
{isOpen && (
<div className='fixed inset-0 bg-transparent' onClick={() => setIsDropdownOpen(false)} />
)}
@@ -6,6 +6,7 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface MenuItemProps {
label: string;
description?: string;
tooltip?: string;
buttonClass?: string;
labelClass?: string;
shortcut?: string;
@@ -19,6 +20,7 @@ interface MenuItemProps {
const MenuItem: React.FC<MenuItemProps> = ({
label,
description,
tooltip,
buttonClass,
labelClass,
shortcut,
@@ -36,6 +38,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
disabled && 'btn-disabled text-gray-400',
buttonClass,
)}
data-tip={tooltip ? tooltip : ''}
onClick={onClick}
disabled={disabled}
>
+11 -7
View File
@@ -1,4 +1,5 @@
'use client';
import posthog from 'posthog-js';
import { ReactNode } from 'react';
import { PostHogProvider } from 'posthog-js/react';
@@ -9,13 +10,16 @@ const shouldDisablePostHog = () => {
return localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === 'true';
};
if (
typeof window !== 'undefined' &&
process.env['NODE_ENV'] === 'production' &&
process.env['NEXT_PUBLIC_POSTHOG_KEY']
) {
posthog.init(process.env['NEXT_PUBLIC_POSTHOG_KEY'], {
api_host: process.env['NEXT_PUBLIC_POSTHOG_HOST'],
const posthogUrl =
process.env['NEXT_PUBLIC_POSTHOG_HOST'] ||
atob(process.env['NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64']!);
const posthogKey =
process.env['NEXT_PUBLIC_POSTHOG_KEY'] ||
atob(process.env['NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64']!);
if (typeof window !== 'undefined' && process.env['NODE_ENV'] === 'production' && posthogKey) {
posthog.init(posthogKey, {
api_host: posthogUrl,
person_profiles: 'always',
});
if (shouldDisablePostHog()) {
+1 -1
View File
@@ -14,7 +14,7 @@ export const useUICSS = (bookKey: string) => {
styleElement.remove();
}
const rawCSS = viewSettings.userStylesheet || '';
const rawCSS = viewSettings.userUIStylesheet || '';
const newStyleEl = document.createElement('style');
newStyleEl.textContent = rawCSS.replace('foliate-view', `#foliate-view-${bookKey}`);
document.head.appendChild(newStyleEl);
+17 -21
View File
@@ -2,7 +2,7 @@ import { AppPlatform, AppService, OsPlatform } from '@/types/system';
import { SystemSettings } from '@/types/settings';
import { FileSystem, BaseDir } from '@/types/system';
import { Book, BookConfig, BookContent, BookFormat } from '@/types/book';
import { Book, BookConfig, BookContent, BookFormat, ViewSettings } from '@/types/book';
import {
getDir,
getLocalBookFilename,
@@ -74,6 +74,20 @@ export abstract class BaseAppService implements AppService {
abstract selectDirectory(): Promise<string>;
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
getDefaultViewSettings(): ViewSettings {
return {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
};
}
async loadSettings(): Promise<SystemSettings> {
let settings: SystemSettings;
const { fp, base } = this.resolvePath('settings.json', 'Settings');
@@ -90,15 +104,7 @@ export abstract class BaseAppService implements AppService {
settings = { ...DEFAULT_SYSTEM_SETTINGS, ...settings };
settings.globalReadSettings = { ...DEFAULT_READSETTINGS, ...settings.globalReadSettings };
settings.globalViewSettings = {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
...this.getDefaultViewSettings(),
...settings.globalViewSettings,
};
} catch {
@@ -110,17 +116,7 @@ export abstract class BaseAppService implements AppService {
...DEFAULT_READSETTINGS,
...(this.isMobile ? DEFAULT_MOBILE_READSETTINGS : {}),
},
globalViewSettings: {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
},
globalViewSettings: this.getDefaultViewSettings(),
} as SystemSettings;
await this.fs.createDir('', 'Books', true);
@@ -47,6 +47,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
libraryViewMode: 'grid',
librarySortBy: 'updated',
librarySortAscending: false,
libraryCoverFit: 'crop',
lastSyncedAtBooks: 0,
lastSyncedAtConfigs: 0,
@@ -130,6 +131,7 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
codeHighlighting: false,
codeLanguage: 'auto-detect',
userStylesheet: '',
userUIStylesheet: '',
};
export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
@@ -156,6 +158,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = {
showFooter: true,
showBarsOnScroll: false,
showRemainingTime: false,
showRemainingPages: false,
showPageNumber: true,
};
@@ -534,6 +537,8 @@ export const SYNC_NOTES_INTERVAL_SEC = 5;
export const SYNC_BOOKS_INTERVAL_SEC = 5;
export const CHECK_UPDATE_INTERVAL_SEC = 24 * 60 * 60;
export const RELOAD_BEFREE_SAVED_TIMEOUT_MS = 300;
export const MAX_ZOOM_LEVEL = 500;
export const MIN_ZOOM_LEVEL = 50;
export const ZOOM_STEP = 10;
+3 -2
View File
@@ -2,7 +2,6 @@ export type BookFormat = 'EPUB' | 'PDF' | 'MOBI' | 'CBZ' | 'FB2' | 'FBZ';
export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt';
export type HighlightStyle = 'highlight' | 'underline' | 'squiggly';
export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet';
import { CodeLanguage } from '@/utils/highlightjs';
export interface Book {
// if Book is a remote book we just lazy load the book content via url
@@ -115,8 +114,9 @@ export interface BookStyle {
overrideLayout: boolean;
overrideColor: boolean;
codeHighlighting: boolean;
codeLanguage: CodeLanguage;
codeLanguage: string;
userStylesheet: string;
userUIStylesheet: string;
}
export interface BookFont {
@@ -141,6 +141,7 @@ export interface ViewConfig {
showHeader: boolean;
showFooter: boolean;
showRemainingTime: boolean;
showRemainingPages: boolean;
showPageNumber: boolean;
showBarsOnScroll: boolean;
}
+2
View File
@@ -4,6 +4,7 @@ import { HighlightColor, HighlightStyle, ViewSettings } from './book';
export type ThemeType = 'light' | 'dark' | 'auto';
export type LibraryViewModeType = 'grid' | 'list';
export type LibrarySortByType = 'title' | 'author' | 'updated' | 'created' | 'size' | 'format';
export type LibraryCoverFitType = 'crop' | 'fit';
export interface ReadSettings {
sideBarWidth: string;
@@ -36,6 +37,7 @@ export interface SystemSettings {
libraryViewMode: LibraryViewModeType;
librarySortBy: LibrarySortByType;
librarySortAscending: boolean;
libraryCoverFit: LibraryCoverFitType;
lastSyncedAtBooks: number;
lastSyncedAtConfigs: number;
+2 -1
View File
@@ -1,5 +1,5 @@
import { SystemSettings } from './settings';
import { Book, BookConfig, BookContent } from './book';
import { Book, BookConfig, BookContent, ViewSettings } from './book';
import { BookDoc } from '@/libs/document';
import { ProgressHandler } from '@/utils/transfer';
@@ -44,6 +44,7 @@ export interface AppService {
selectDirectory(): Promise<string>;
selectFiles(name: string, extensions: string[]): Promise<string[]>;
getDefaultViewSettings(): ViewSettings;
loadSettings(): Promise<SystemSettings>;
saveSettings(settings: SystemSettings): Promise<void>;
importBook(
+2
View File
@@ -40,6 +40,8 @@ export interface FoliateView extends HTMLElement {
viewSize: number; // whole document view height
start: number;
end: number;
page: number;
pages: number;
containerPosition: number;
sideProp: 'width' | 'height';
setAttribute: (name: string, value: string | number) => void;
+2 -2
View File
@@ -1,6 +1,6 @@
import { EXTS } from '@/libs/document';
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
import { getUserLang, isContentURI, isValidURL, makeSafeFilename } from './misc';
import { getUserLang, isContentURI, isFileURI, isValidURL, makeSafeFilename } from './misc';
import { getStorageType } from './object';
import { getDirFromLanguage } from './rtl';
import { SUPPORTED_LANGS } from '@/services/constants';
@@ -34,7 +34,7 @@ export const isBookFile = (filename: string) => {
return Object.values(EXTS).includes(filename.split('.').pop()!);
};
export const getFilename = (fileOrUri: string) => {
if (isValidURL(fileOrUri) || isContentURI(fileOrUri)) {
if (isValidURL(fileOrUri) || isContentURI(fileOrUri) || isFileURI(fileOrUri)) {
fileOrUri = decodeURI(fileOrUri);
}
const normalizedPath = fileOrUri.replace(/\\/g, '/');
+4
View File
@@ -85,6 +85,10 @@ export const isContentURI = (uri: string) => {
return uri.startsWith('content://');
};
export const isFileURI = (uri: string) => {
return uri.startsWith('file://');
};
export const isValidURL = (url: string, allowedSchemes: string[] = ['http', 'https']) => {
try {
const { protocol } = new URL(url);
+4 -2
View File
@@ -1,9 +1,11 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl =
process.env['NEXT_PUBLIC_SUPABASE_URL'] || process.env['NEXT_PUBLIC_DEV_SUPABASE_URL']!;
process.env['NEXT_PUBLIC_SUPABASE_URL'] ||
atob(process.env['NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64']!);
const supabaseAnonKey =
process.env['NEXT_PUBLIC_SUPABASE_ANON_KEY'] || process.env['NEXT_PUBLIC_DEV_SUPABASE_ANON_KEY']!;
process.env['NEXT_PUBLIC_SUPABASE_ANON_KEY'] ||
atob(process.env['NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64']!);
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
+188 -248
View File
File diff suppressed because it is too large Load Diff