Compare commits

...

19 Commits

Author SHA1 Message Date
Huang Xin f5b686abd9 release: version 0.9.63 (#1550) 2025-07-07 15:07:35 +02:00
Huang Xin 74e1b733a9 layout: dismiss system context menu popup when selecting text on iPad, closes #1485 (#1549) 2025-07-07 14:57:04 +02:00
Huang Xin a9e0090275 i18n: add Thai (th-TH) translations (#1548) 2025-07-07 11:21:41 +02:00
Huang Xin 68d57da9ca perf: speed up opening for large MOBI books, closes #1544 (#1547) 2025-07-07 10:15:54 +02:00
Bhscer 3cef529ebd feat: support deleting only the cloud backup of a book (#1546)
* feat: support delete the cloud storage of a book

* feat: merge to delete function

* fix: change a logic
2025-07-07 09:37:11 +02:00
Huang Xin 85ac399e2c tts: fix some direct text being skipped in tts, closes #1515 (#1543) 2025-07-06 12:13:40 +02:00
Huang Xin e8d09773a2 fix: handling toc links for some mobi books, closes #1525 (#1542) 2025-07-06 08:49:47 +02:00
Huang Xin 23905407ca doc: layout for the download badges (#1541) 2025-07-06 05:42:29 +02:00
Huang Xin 2ab1304b6d doc: update Readme for mobile apps downloads (#1540) 2025-07-06 05:20:30 +02:00
Huang Xin d94236e127 layout: fix grid cover image height in crop mode (#1537)
Support setting arbitrary columns with custom UI css:
```
.bookshelf-items {
  grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}
```
2025-07-05 17:29:33 +02:00
Huang Xin 630025c123 chore: update Android target sdk to 35 (#1534) 2025-07-05 08:49:41 +02:00
Huang Xin 2b73c5a85a perf: lazy loading fonts in fonts list (#1533) 2025-07-05 07:32:22 +02:00
Huang Xin a03f2c585f layout: fix overlay scrollbar with virtualized list, closes #1524 (#1530) 2025-07-04 19:36:13 +02:00
Huang Xin 8a0216dcf8 fix: links in mobi are now properly followed, closes #1503 (#1528) 2025-07-04 07:47:27 +02:00
Huang Xin 59013be586 settings: remove unsupported screen orientation lock on iPad, closes #1520 (#1522) 2025-07-03 13:41:42 +02:00
Huang Xin c6a638dbfd compat: footnote img size, closes #1518 (#1519) 2025-07-02 18:55:09 +02:00
Huang Xin e54440ad80 chore: retry workflow for r2 uploading (#1517) 2025-07-02 17:37:05 +02:00
Huang Xin 446b014f7d chore: upload to r2 after release with retry (#1516) 2025-07-02 17:23:12 +02:00
Huang Xin 9190433ea1 subscription: fix action button in plan card (#1514) 2025-07-02 16:48:32 +02:00
62 changed files with 1045 additions and 315 deletions
+7
View File
@@ -320,3 +320,10 @@ jobs:
draft: false,
prerelease: false
})
upload-to-r2:
needs: [get-release, update-release]
uses: ./.github/workflows/upload-to-r2.yml
with:
tag: ${{ needs.get-release.outputs.release_tag }}
secrets: inherit
+18
View File
@@ -0,0 +1,18 @@
name: Retry workflow
on:
workflow_dispatch:
inputs:
run_id:
required: true
jobs:
rerun:
runs-on: ubuntu-latest
steps:
- name: rerun ${{ inputs.run_id }}
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
gh run watch ${{ inputs.run_id }} > /dev/null 2>&1
gh run rerun ${{ inputs.run_id }} --failed
+17
View File
@@ -17,6 +17,9 @@ on:
jobs:
upload-to-r2:
runs-on: ubuntu-latest
timeout-minutes: 3
strategy:
fail-fast: false
env:
RELEASE_R2_BUCKET: readest-releases
RELEASE_R2_ACCOUNT_ID: ${{ secrets.RELEASE_R2_ACCOUNT_ID }}
@@ -58,3 +61,17 @@ jobs:
mv ./release-assets/release-notes.json releases
rclone copy ./release-assets r2:${RELEASE_R2_BUCKET}/releases/${{ inputs.tag }}/
rclone copy ./releases r2:${RELEASE_R2_BUCKET}/releases/
- name: Upload successful
if: success()
run: echo "Upload completed successfully"
retry-on-failure:
if: failure() && fromJSON(github.run_attempt) < 3
needs: [upload-to-r2]
runs-on: ubuntu-latest
steps:
- env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: gh workflow run retry-workflow.yml -F run_id=${{ github.run_id }}
+10 -1
View File
@@ -96,7 +96,16 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
## Downloads
The Readest app is available for download! 🥳 🚀
### Mobile Apps
<div align="center">
<a href="https://apps.apple.com/app/id6738622779">
<img alt="Download on the App Store" src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" style="height: 50px;" /></a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://play.google.com/store/apps/details?id=com.bilingify.readest">
<img alt="Get it on Google Play" src="https://upload.wikimedia.org/wikipedia/commons/7/78/Google_Play_Store_badge_EN.svg" style="height: 50px;" /></a>
</div>
### Platform-Specific Downloads
- macOS / iOS / iPadOS : Search for "Readest" on the [App Store][link-appstore], also available on TestFlight for beta test (send your Apple ID to <readestapp@gmail.com> to request access).
- Windows / Linux / Android: Visit [https://readest.com][link-website] or the [Releases on GitHub][link-gh-releases].
@@ -26,6 +26,7 @@ module.exports = {
'id',
'vi',
'ar',
'th',
'zh-CN',
'zh-TW',
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.62",
"version": "0.9.63",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -156,7 +156,7 @@
"Create a Password": "إنشاء كلمة مرور",
"Sign up": "إنشاء حساب",
"Signing up...": "يتم الآن إنشاء الحساب...",
"Dont have an account? Sign up": "ليس لديك حساب؟ قم بالتسجيل",
"Don't have an account? Sign up": "ليس لديك حساب؟ قم بالتسجيل",
"Check your email for the confirmation link": "تحقق من بريدك الإلكتروني للحصول على رابط التأكيد",
"Signing in ...": "يتم الآن تسجيل الدخول ...",
"Send a magic link email": "إرسال رابط سحري إلى بريدك الإلكتروني",
@@ -374,8 +374,6 @@
"Upgrade to Plus or Pro": "الترقية إلى Plus أو Pro",
"Current Plan": "الخطة الحالية",
"Plan Limits": "حدود الخطة",
"Available Plans": "الخطط المتاحة",
"{{current}} of {{all}}": "{{current}} من {{all}}",
"Failed to manage subscription. Please try again later.": "فشل في إدارة الاشتراك. يرجى المحاولة مرة أخرى لاحقًا.",
"Processing your payment...": "جارٍ معالجة الدفع...",
"Please wait while we confirm your subscription.": "يرجى الانتظار بينما نقوم بتأكيد اشتراكك.",
@@ -432,5 +430,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "ترجم حتى 500 ألف حرف يوميًا بأدق محرك ترجمة متاح.",
"10 GB Cloud Sync Storage": "10 جيجابايت تخزين سحابي",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "احفظ واوصل لمجموعة قراءتك كاملة بـ 2 جيجابايت تخزين سحابي آمن.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "احفظ واوصل لمجموعة قراءتك كاملة بـ 10 جيجابايت تخزين سحابي آمن."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "احفظ واوصل لمجموعة قراءتك كاملة بـ 10 جيجابايت تخزين سحابي آمن.",
"Deleted cloud backup of the book: {{title}}": "تم حذف النسخة الاحتياطية السحابية للكتاب: {{title}}",
"Failed to delete cloud backup of the book": "فشل في حذف النسخة الاحتياطية السحابية للكتاب",
"Are you sure to delete the cloud backup of the selected book?": "هل أنت متأكد من حذف النسخة الاحتياطية السحابية للكتاب المحدد؟"
}
@@ -156,7 +156,7 @@
"Create a Password": "Passwort erstellen",
"Sign up": "Registrieren",
"Signing up...": "Registrierung...",
"Dont have an account? Sign up": "Kein Konto? Registrieren",
"Don't have an account? Sign up": "Kein Konto? Registrieren",
"Check your email for the confirmation link": "Überprüfen Sie Ihre E-Mails auf den Bestätigungslink",
"Signing in ...": "Anmeldung ...",
"Send a magic link email": "Magic-Link-E-Mail senden",
@@ -194,7 +194,7 @@
"Fullscreen": "Vollbild",
"No supported files found. Supported formats: {{formats}}": "Keine unterstützten Dateien gefunden. Unterstützte Formate: {{formats}}",
"Drop to Import Books": "Zum Importieren von Büchern ablegen",
"Custom": "Benutzerdefiniert",
"Custom": "Eigen",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "Die ganze Welt ist eine Bühne,\nUnd alle Männer und Frauen sind bloß Spieler;\nSie haben ihre Abgänge und ihre Auftritte,\nUnd ein Mann spielt in seinem Leben viele Rollen,\nSeine Taten sind sieben Zeitalter.\n\n— William Shakespeare",
"Custom Theme": "Benutzerdefiniertes Thema",
"Theme Name": "Themenname",
@@ -202,7 +202,7 @@
"Background Color": "Hintergrundfarbe",
"Preview": "Vorschau",
"Contrast": "Kontrast",
"Sunset": "Sonnenuntergang",
"Sunset": "Abend",
"Double Border": "Doppelter Rand",
"Border Color": "Randfarbe",
"Border Frame": "Rahmenrand",
@@ -291,10 +291,10 @@
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln",
"Help improve Readest": "Readest verbessern helfen",
"Sharing anonymized statistics": "Anonymisierte Statistiken teilen",
"Interface Language": "Benutzeroberflächensprache",
"Interface Language": "UI-Sprache",
"Translation": "Übersetzung",
"Enable Translation": "Übersetzung aktivieren",
"Translation Service": "Übersetzungsdienst",
"Translation Service": "Übersetzer",
"Translate To": "Übersetzen nach",
"Disable Translation": "Übersetzung deaktivieren",
"Scroll": "Scrollen",
@@ -358,8 +358,6 @@
"Upgrade to Plus or Pro": "Upgrade auf Plus oder Pro",
"Current Plan": "Aktueller Tarif",
"Plan Limits": "Tariflimits",
"Available Plans": "Verfügbare Tarife",
"{{current}} of {{all}}": "{{current}} von {{all}}",
"Failed to manage subscription. Please try again later.": "Verwaltung des Abos fehlgeschlagen. Bitte versuchen Sie es später erneut.",
"Processing your payment...": "Zahlung wird verarbeitet...",
"Please wait while we confirm your subscription.": "Bitte warten Sie, während wir Ihr Abo bestätigen.",
@@ -416,5 +414,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Übersetzen Sie täglich bis zu 500.000 Zeichen mit der genauesten verfügbaren Übersetzungsmaschine.",
"10 GB Cloud Sync Storage": "10 GB Cloud-Sync-Speicher",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Speichern und greifen Sie sicher auf Ihre gesamte Lesekollektion mit bis zu 2 GB Cloud-Speicher zu.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Speichern und greifen Sie sicher auf Ihre gesamte Lesekollektion mit bis zu 10 GB Cloud-Speicher zu."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Speichern und greifen Sie sicher auf Ihre gesamte Lesekollektion mit bis zu 10 GB Cloud-Speicher zu.",
"Deleted cloud backup of the book: {{title}}": "Gelöschte Cloud-Sicherung des Buches: {{title}}",
"Failed to delete cloud backup of the book": "Fehler beim Löschen der Cloud-Sicherung des Buches",
"Are you sure to delete the cloud backup of the selected book?": "Sind Sie sicher, dass Sie die Cloud-Sicherung des ausgewählten Buches löschen möchten?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Δημιουργία κωδικού πρόσβασης",
"Sign up": "Εγγραφή",
"Signing up...": "Εγγραφή...",
"Dont have an account? Sign up": "Δεν έχετε λογαριασμό; Εγγραφείτε",
"Don't have an account? Sign up": "Δεν έχετε λογαριασμό; Εγγραφείτε",
"Check your email for the confirmation link": "Ελέγξτε το email σας για τον σύνδεσμο επιβεβαίωσης",
"Signing in ...": "Σύνδεση ...",
"Send a magic link email": "Αποστολή email με μαγικό σύνδεσμο",
@@ -359,8 +359,6 @@
"Upgrade to Plus or Pro": "Αναβάθμιση σε Plus ή Pro",
"Current Plan": "Τρέχον πρόγραμμα",
"Plan Limits": "Όρια προγράμματος",
"Available Plans": "Διαθέσιμα προγράμματα",
"{{current}} of {{all}}": "{{current}} από {{all}}",
"Failed to manage subscription. Please try again later.": "Αποτυχία διαχείρισης της συνδρομής. Παρακαλώ δοκιμάστε ξανά αργότερα.",
"Processing your payment...": "Επεξεργασία πληρωμής...",
"Please wait while we confirm your subscription.": "Παρακαλώ περιμένετε όσο επιβεβαιώνουμε τη συνδρομή σας.",
@@ -416,5 +414,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Μεταφράστε έως 500.000 χαρακτήρες ημερησίως με τη πιο ακριβή μηχανή μετάφρασης.",
"10 GB Cloud Sync Storage": "10 GB Αποθήκευση Cloud Sync",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Αποθηκεύστε και αποκτήστε πρόσβαση στη συλλογή ανάγνωσης με έως 2 GB ασφαλή cloud αποθήκευση.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Αποθηκεύστε και αποκτήστε πρόσβαση στη συλλογή ανάγνωσης με έως 10 GB ασφαλή cloud αποθήκευση."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Αποθηκεύστε και αποκτήστε πρόσβαση στη συλλογή ανάγνωσης με έως 10 GB ασφαλή cloud αποθήκευση.",
"Deleted cloud backup of the book: {{title}}": "Διαγράφηκε το αντίγραφο ασφαλείας του βιβλίου: {{title}}",
"Failed to delete cloud backup of the book": "Αποτυχία διαγραφής του αντιγράφου ασφαλείας του βιβλίου",
"Are you sure to delete the cloud backup of the selected book?": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αντίγραφο ασφαλείας του επιλεγμένου βιβλίου;"
}
@@ -157,7 +157,7 @@
"Create a Password": "Crear una contraseña",
"Sign up": "Registrarse",
"Signing up...": "Registrándose...",
"Dont have an account? Sign up": "¿No tienes una cuenta? Regístrate",
"Don't have an account? Sign up": "¿No tienes una cuenta? Regístrate",
"Check your email for the confirmation link": "Revisa tu correo electrónico para el enlace de confirmación",
"Signing in ...": "Iniciando sesión ...",
"Send a magic link email": "Enviar un enlace mágico por correo",
@@ -363,8 +363,6 @@
"Upgrade to Plus or Pro": "Actualizar a Plus o Pro",
"Current Plan": "Plan actual",
"Plan Limits": "Límites del plan",
"Available Plans": "Planes disponibles",
"{{current}} of {{all}}": "{{current}} de {{all}}",
"Failed to manage subscription. Please try again later.": "No se pudo gestionar la suscripción. Por favor, inténtalo de nuevo más tarde.",
"Processing your payment...": "Procesando tu pago...",
"Please wait while we confirm your subscription.": "Espera mientras confirmamos tu suscripción.",
@@ -420,5 +418,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduce hasta 500,000 caracteres diarios con el motor de traducción más preciso disponible.",
"10 GB Cloud Sync Storage": "10 GB de Almacenamiento en la Nube",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Almacena y accede de forma segura a toda tu colección de lectura con hasta 2 GB de almacenamiento en la nube.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Almacena y accede de forma segura a toda tu colección de lectura con hasta 10 GB de almacenamiento en la nube."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Almacena y accede de forma segura a toda tu colección de lectura con hasta 10 GB de almacenamiento en la nube.",
"Deleted cloud backup of the book: {{title}}": "Respaldo en la nube del libro eliminado: {{title}}",
"Failed to delete cloud backup of the book": "Fallo al eliminar el respaldo en la nube del libro",
"Are you sure to delete the cloud backup of the selected book?": "¿Estás seguro de que deseas eliminar el respaldo en la nube del libro seleccionado?"
}
@@ -157,7 +157,6 @@
"Create a Password": "Créer un mot de passe",
"Sign up": "S'inscrire",
"Signing up...": "Inscription en cours...",
"Dont have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous",
"Check your email for the confirmation link": "Vérifiez votre e-mail pour le lien de confirmation",
"Signing in ...": "Connexion en cours ...",
"Send a magic link email": "Envoyer un e-mail avec un lien magique",
@@ -363,8 +362,6 @@
"Upgrade to Plus or Pro": "Passer à Plus ou Pro",
"Current Plan": "Abonnement actuel",
"Plan Limits": "Limites de labonnement",
"Available Plans": "Abonnements disponibles",
"{{current}} of {{all}}": "{{current}} sur {{all}}",
"Failed to manage subscription. Please try again later.": "Échec de la gestion de labonnement. Veuillez réessayer plus tard.",
"Processing your payment...": "Traitement de votre paiement...",
"Please wait while we confirm your subscription.": "Veuillez patienter pendant la confirmation de votre abonnement.",
@@ -420,5 +417,9 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduisez jusqu'à 500 000 caractères par jour avec le moteur de traduction le plus précis.",
"10 GB Cloud Sync Storage": "10 GB de Stockage Cloud Sync",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Stockez et accédez en sécurité à votre collection de lecture avec jusqu'à 2 GB de stockage cloud.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Stockez et accédez en sécurité à votre collection de lecture avec jusqu'à 10 GB de stockage cloud."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Stockez et accédez en sécurité à votre collection de lecture avec jusqu'à 10 GB de stockage cloud.",
"Deleted cloud backup of the book: {{title}}": "Suppression de la sauvegarde cloud du livre : {{title}}",
"Failed to delete cloud backup of the book": "Échec de la suppression de la sauvegarde cloud du livre",
"Are you sure to delete the cloud backup of the selected book?": "Êtes-vous sûr de vouloir supprimer la sauvegarde cloud du livre sélectionné ?",
"Don't have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous"
}
@@ -157,7 +157,7 @@
"Create a Password": "पासवर्ड बनाएं",
"Sign up": "साइन अप करें",
"Signing up...": "साइन अप हो रहा है...",
"Dont have an account? Sign up": "कोई खाता नहीं है? साइन अप करें",
"Don't have an account? Sign up": "कोई खाता नहीं है? साइन अप करें",
"Check your email for the confirmation link": "पुष्टि लिंक के लिए अपना ईमेल जांचें",
"Signing in ...": "साइन इन हो रहा है ...",
"Send a magic link email": "जादू लिंक ईमेल भेजें",
@@ -359,8 +359,6 @@
"Upgrade to Plus or Pro": "Plus या Pro में अपग्रेड करें",
"Current Plan": "वर्तमान योजना",
"Plan Limits": "योजना की सीमाएँ",
"Available Plans": "उपलब्ध योजनाएँ",
"{{current}} of {{all}}": "{{all}} में से {{current}}",
"Failed to manage subscription. Please try again later.": "सदस्यता प्रबंधित करने में विफल। कृपया बाद में पुनः प्रयास करें।",
"Processing your payment...": "आपका भुगतान संसाधित हो रहा है...",
"Please wait while we confirm your subscription.": "कृपया प्रतीक्षा करें, हम आपकी सदस्यता की पुष्टि कर रहे हैं।",
@@ -416,5 +414,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "सबसे सटीक अनुवाद इंजन के साथ दैनिक 5 लाख तक अक्षर अनुवाद करें।",
"10 GB Cloud Sync Storage": "10 GB क्लाउड सिंक स्टोरेज",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "2 GB सुरक्षित क्लाउड स्टोरेज के साथ अपना संपूर्ण पठन संग्रह संग्रहीत करें।",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "10 GB सुरक्षित क्लाउड स्टोरेज के साथ अपना संपूर्ण पठन संग्रह संग्रहीत करें।"
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "10 GB सुरक्षित क्लाउड स्टोरेज के साथ अपना संपूर्ण पठन संग्रह संग्रहीत करें।",
"Deleted cloud backup of the book: {{title}}": "पुस्तक का क्लाउड बैकअप हटाया गया: {{title}}",
"Failed to delete cloud backup of the book": "पुस्तक का क्लाउड बैकअप हटाने में विफल",
"Are you sure to delete the cloud backup of the selected book?": "क्या आप सुनिश्चित हैं कि आप चयनित पुस्तक का क्लाउड बैकअप हटाना चाहते हैं?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Buat kata sandi",
"Sign up": "Daftar",
"Signing up...": "Sedang mendaftar...",
"Dont have an account? Sign up": "Belum punya akun? Daftar",
"Don't have an account? Sign up": "Belum punya akun? Daftar",
"Check your email for the confirmation link": "Periksa email Anda untuk tautan konfirmasi",
"Signing in ...": "Sedang masuk ...",
"Send a magic link email": "Kirim email tautan ajaib",
@@ -355,8 +355,6 @@
"Upgrade to Plus or Pro": "Tingkatkan ke Plus atau Pro",
"Current Plan": "Paket Saat Ini",
"Plan Limits": "Batas Paket",
"Available Plans": "Paket yang Tersedia",
"{{current}} of {{all}}": "{{current}} dari {{all}}",
"Failed to manage subscription. Please try again later.": "Gagal mengelola langganan. Silakan coba lagi nanti.",
"Processing your payment...": "Memproses pembayaran Anda...",
"Please wait while we confirm your subscription.": "Mohon tunggu saat kami mengonfirmasi langganan Anda.",
@@ -412,5 +410,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Terjemahkan hingga 500.000 karakter harian dengan mesin terjemahan paling akurat.",
"10 GB Cloud Sync Storage": "Penyimpanan Cloud Sync 10 GB",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Simpan dan akses koleksi bacaan lengkap dengan aman menggunakan penyimpanan cloud 2 GB.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Simpan dan akses koleksi bacaan lengkap dengan aman menggunakan penyimpanan cloud 10 GB."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Simpan dan akses koleksi bacaan lengkap dengan aman menggunakan penyimpanan cloud 10 GB.",
"Deleted cloud backup of the book: {{title}}": "Cadangan cloud buku dihapus: {{title}}",
"Failed to delete cloud backup of the book": "Cadangan cloud buku gagal dihapus",
"Are you sure to delete the cloud backup of the selected book?": "Apakah Anda yakin ingin menghapus cadangan cloud dari buku yang dipilih?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Crea una password",
"Sign up": "Registrati",
"Signing up...": "Registrazione in corso...",
"Dont have an account? Sign up": "Non hai un account? Registrati",
"Don't have an account? Sign up": "Non hai un account? Registrati",
"Check your email for the confirmation link": "Controlla la tua email per il link di conferma",
"Signing in ...": "Accesso in corso ...",
"Send a magic link email": "Invia un'email con link magico",
@@ -363,8 +363,6 @@
"Upgrade to Plus or Pro": "Aggiorna a Plus o Pro",
"Current Plan": "Piano Attuale",
"Plan Limits": "Limiti del Piano",
"Available Plans": "Piani Disponibili",
"{{current}} of {{all}}": "{{current}} di {{all}}",
"Failed to manage subscription. Please try again later.": "Impossibile gestire l'abbonamento. Riprova più tardi.",
"Processing your payment...": "Elaborazione del pagamento...",
"Please wait while we confirm your subscription.": "Attendi mentre confermiamo il tuo abbonamento.",
@@ -420,5 +418,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduci fino a 500.000 caratteri al giorno con il motore di traduzione più accurato.",
"10 GB Cloud Sync Storage": "Archiviazione Cloud Sync da 10 GB",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Archivia e accedi in sicurezza alla tua intera collezione di lettura con fino a 2 GB di storage cloud.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Archivia e accedi in sicurezza alla tua intera collezione di lettura con fino a 10 GB di storage cloud."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Archivia e accedi in sicurezza alla tua intera collezione di lettura con fino a 10 GB di storage cloud.",
"Deleted cloud backup of the book: {{title}}": "Backup cloud del libro eliminato: {{title}}",
"Failed to delete cloud backup of the book": "Impossibile eliminare il backup cloud del libro",
"Are you sure to delete the cloud backup of the selected book?": "Sei sicuro di voler eliminare il backup cloud del libro selezionato?"
}
@@ -157,7 +157,7 @@
"Create a Password": "パスワードを作成",
"Sign up": "サインアップ",
"Signing up...": "サインアップ中...",
"Dont have an account? Sign up": "アカウントをお持ちでないですか? サインアップ",
"Don't have an account? Sign up": "アカウントをお持ちでないですか? サインアップ",
"Check your email for the confirmation link": "確認リンクのためにメールを確認してください",
"Signing in ...": "サインイン中 ...",
"Send a magic link email": "マジックリンクメールを送信",
@@ -355,8 +355,6 @@
"Upgrade to Plus or Pro": "Plus または Pro にアップグレード",
"Current Plan": "現在のプラン",
"Plan Limits": "プランの上限",
"Available Plans": "利用可能なプラン",
"{{current}} of {{all}}": "{{all}} 中 {{current}}",
"Failed to manage subscription. Please try again later.": "サブスクリプションの管理に失敗しました。後でもう一度お試しください。",
"Processing your payment...": "お支払いを処理中...",
"Please wait while we confirm your subscription.": "サブスクリプションを確認しています。しばらくお待ちください。",
@@ -412,5 +410,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "最も正確な翻訳エンジンで1日最大50万文字を翻訳。",
"10 GB Cloud Sync Storage": "10GBクラウド同期ストレージ",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "最大2GBの安全なクラウドストレージで読書コレクション全体を保存・アクセス。",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "最大10GBの安全なクラウドストレージで読書コレクション全体を保存・アクセス。"
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "最大10GBの安全なクラウドストレージで読書コレクション全体を保存・アクセス。",
"Deleted cloud backup of the book: {{title}}": "書籍のクラウドバックアップを削除しました:{{title}}",
"Failed to delete cloud backup of the book": "書籍のクラウドバックアップを削除できませんでした",
"Are you sure to delete the cloud backup of the selected book?": "選択した書籍のクラウドバックアップを削除してもよろしいですか?"
}
@@ -157,7 +157,7 @@
"Create a Password": "비밀번호 만들기",
"Sign up": "회원가입",
"Signing up...": "회원가입 중...",
"Dont have an account? Sign up": "계정이 없습니까? 회원가입",
"Don't have an account? Sign up": "계정이 없습니까? 회원가입",
"Check your email for the confirmation link": "확인 링크를 위해 이메일을 확인하세요",
"Signing in ...": "로그인 중 ...",
"Send a magic link email": "마법의 링크 이메일 보내기",
@@ -355,8 +355,6 @@
"Upgrade to Plus or Pro": "Plus 또는 Pro로 업그레이드",
"Current Plan": "현재 요금제",
"Plan Limits": "요금제 한도",
"Available Plans": "이용 가능한 요금제",
"{{current}} of {{all}}": "{{all}} 중 {{current}}",
"Failed to manage subscription. Please try again later.": "구독 관리에 실패했습니다. 나중에 다시 시도해주세요.",
"Processing your payment...": "결제를 처리 중입니다...",
"Please wait while we confirm your subscription.": "구독을 확인하는 중입니다. 잠시만 기다려주세요.",
@@ -412,5 +410,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "가장 정확한 번역 엔진으로 매일 최대 50만 자를 번역하세요.",
"10 GB Cloud Sync Storage": "10GB 클라우드 동기화 저장소",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "최대 2GB의 안전한 클라우드 저장소로 전체 독서 컬렉션을 저장하고 액세스하세요.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "최대 10GB의 안전한 클라우드 저장소로 전체 독서 컬렉션을 저장하고 액세스하세요."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "최대 10GB의 안전한 클라우드 저장소로 전체 독서 컬렉션을 저장하고 액세스하세요.",
"Deleted cloud backup of the book: {{title}}": "책의 클라우드 백업이 삭제됨: {{title}}",
"Failed to delete cloud backup of the book": "책의 클라우드 백업을 삭제하지 못했습니다.",
"Are you sure to delete the cloud backup of the selected book?": "선택한 책의 클라우드 백업을 삭제하시겠습니까?"
}
@@ -273,7 +273,7 @@
"Reveal in Finder": "Tonen in Finder",
"Reveal in File Explorer": "Tonen in Verkenner",
"Reveal in Folder": "Tonen in map",
"Dont have an account? Sign up": "Nog geen account? Registreer u",
"Don't have an account? Sign up": "Nog geen account? Registreer u",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "De hele wereld is een toneel,\nEn alle mannen en vrouwen slechts spelers;\nZij hebben hun uitgangen en hun entrees,\nEn één man speelt in zijn tijd vele rollen,\nZijn daden zijnde zeven leeftijden.\n\n— William Shakespeare",
"Next Section": "Volgende sectie",
"Previous Section": "Vorige sectie",
@@ -359,8 +359,6 @@
"Upgrade to Plus or Pro": "Upgrade naar Plus of Pro",
"Current Plan": "Huidig abonnement",
"Plan Limits": "Abonnementslimieten",
"Available Plans": "Beschikbare abonnementen",
"{{current}} of {{all}}": "{{current}} van {{all}}",
"Failed to manage subscription. Please try again later.": "Abonnement beheren is mislukt. Probeer het later opnieuw.",
"Processing your payment...": "Je betaling wordt verwerkt...",
"Please wait while we confirm your subscription.": "Even geduld terwijl we je abonnement bevestigen.",
@@ -416,5 +414,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Vertaal dagelijks tot 500.000 tekens met de meest nauwkeurige vertaalengine.",
"10 GB Cloud Sync Storage": "10 GB Cloud Sync Opslag",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Sla je hele leesverzameling veilig op en benader deze met tot 2 GB cloudopslag.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Sla je hele leesverzameling veilig op en benader deze met tot 10 GB cloudopslag."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Sla je hele leesverzameling veilig op en benader deze met tot 10 GB cloudopslag.",
"Deleted cloud backup of the book: {{title}}": "Verwijderde cloudback-up van het boek: {{title}}",
"Failed to delete cloud backup of the book": "Verwijderen van cloudback-up van het boek is mislukt.",
"Are you sure to delete the cloud backup of the selected book?": "Weet je zeker dat je de cloudback-up van het geselecteerde boek wilt verwijderen?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Utwórz hasło",
"Sign up": "Zarejestruj się",
"Signing up...": "Rejestracja...",
"Dont have an account? Sign up": "Nie masz konta? Zarejestruj się",
"Don't have an account? Sign up": "Nie masz konta? Zarejestruj się",
"Check your email for the confirmation link": "Sprawdź swój email, aby uzyskać link potwierdzający",
"Signing in ...": "Logowanie ...",
"Send a magic link email": "Wyślij link magiczny na email",
@@ -367,8 +367,6 @@
"Upgrade to Plus or Pro": "Uaktualnij do Plus lub Pro",
"Current Plan": "Obecny plan",
"Plan Limits": "Limity planu",
"Available Plans": "Dostępne plany",
"{{current}} of {{all}}": "{{current}} z {{all}}",
"Failed to manage subscription. Please try again later.": "Nie udało się zarządzać subskrypcją. Spróbuj ponownie później.",
"Processing your payment...": "Przetwarzanie płatności...",
"Please wait while we confirm your subscription.": "Proszę czekać, potwierdzamy subskrypcję.",
@@ -424,5 +422,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Tłumacz do 500 000 znaków dziennie z najdokładniejszą dostępną maszyną tłumaczeniową.",
"10 GB Cloud Sync Storage": "10 GB Pamięci Cloud Sync",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Bezpiecznie przechowuj i uzyskuj dostęp do całej kolekcji czytelniczej z do 2 GB pamięci w chmurze.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Bezpiecznie przechowuj i uzyskuj dostęp do całej kolekcji czytelniczej z do 10 GB pamięci w chmurze."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Bezpiecznie przechowuj i uzyskuj dostęp do całej kolekcji czytelniczej z do 10 GB pamięci w chmurze.",
"Deleted cloud backup of the book: {{title}}": "Usunięto kopię zapasową książki w chmurze: {{title}}",
"Failed to delete cloud backup of the book": "Nie udało się usunąć kopii zapasowej książki w chmurze.",
"Are you sure to delete the cloud backup of the selected book?": "Czy na pewno chcesz usunąć kopię zapasową chmury wybranej książki?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Criar uma senha",
"Sign up": "Cadastrar-se",
"Signing up...": "Cadastrando...",
"Dont have an account? Sign up": "Não tem uma conta? Cadastre-se",
"Don't have an account? Sign up": "Não tem uma conta? Cadastre-se",
"Check your email for the confirmation link": "Verifique seu e-mail para o link de confirmação",
"Signing in ...": "Entrando ...",
"Send a magic link email": "Enviar um link mágico por e-mail",
@@ -363,8 +363,6 @@
"Upgrade to Plus or Pro": "Atualizar para Plus ou Pro",
"Current Plan": "Plano atual",
"Plan Limits": "Limites do plano",
"Available Plans": "Planos disponíveis",
"{{current}} of {{all}}": "{{current}} de {{all}}",
"Failed to manage subscription. Please try again later.": "Não foi possível gerenciar a assinatura. Tente novamente mais tarde.",
"Processing your payment...": "Processando seu pagamento...",
"Please wait while we confirm your subscription.": "Por favor, aguarde enquanto confirmamos sua assinatura.",
@@ -420,5 +418,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduza até 500.000 caracteres diariamente com o motor de tradução mais preciso disponível.",
"10 GB Cloud Sync Storage": "10 GB de Armazenamento na Nuvem",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Armazene e acesse com segurança sua coleção completa de leitura com até 2 GB de armazenamento na nuvem.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Armazene e acesse com segurança sua coleção completa de leitura com até 10 GB de armazenamento na nuvem."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Armazene e acesse com segurança sua coleção completa de leitura com até 10 GB de armazenamento na nuvem.",
"Deleted cloud backup of the book: {{title}}": "Backup na nuvem do livro excluído: {{title}}",
"Failed to delete cloud backup of the book": "Falha ao excluir o backup na nuvem do livro.",
"Are you sure to delete the cloud backup of the selected book?": "Tem certeza de que deseja excluir o backup na nuvem do livro selecionado?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Создать пароль",
"Sign up": "Зарегистрироваться",
"Signing up...": "Регистрация...",
"Dont have an account? Sign up": "Нет аккаунта? Зарегистрируйтесь",
"Don't have an account? Sign up": "Нет аккаунта? Зарегистрируйтесь",
"Check your email for the confirmation link": "Проверьте свою почту для подтверждающей ссылки",
"Signing in ...": "Вход ...",
"Send a magic link email": "Отправить магическую ссылку по электронной почте",
@@ -367,8 +367,6 @@
"Upgrade to Plus or Pro": "Обновить до Plus или Pro",
"Current Plan": "Текущий план",
"Plan Limits": "Лимиты плана",
"Available Plans": "Доступные планы",
"{{current}} of {{all}}": "{{current}} из {{all}}",
"Failed to manage subscription. Please try again later.": "Не удалось управлять подпиской. Пожалуйста, попробуйте позже.",
"Processing your payment...": "Обработка платежа...",
"Please wait while we confirm your subscription.": "Пожалуйста, подождите, пока мы подтверждаем вашу подписку.",
@@ -424,5 +422,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Переводите до 500 000 символов ежедневно с самым точным доступным движком перевода.",
"10 GB Cloud Sync Storage": "10 ГБ Облачного Хранилища",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Безопасно храните и получайте доступ ко всей коллекции чтения с до 2 ГБ облачного хранилища.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Безопасно храните и получайте доступ ко всей коллекции чтения с до 10 ГБ облачного хранилища."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Безопасно храните и получайте доступ ко всей коллекции чтения с до 10 ГБ облачного хранилища.",
"Deleted cloud backup of the book: {{title}}": "Удалено облачное резервное копирование книги: {{title}}",
"Failed to delete cloud backup of the book": "Не удалось удалить облачное резервное копирование книги.",
"Are you sure to delete the cloud backup of the selected book?": "Вы уверены, что хотите удалить облачное резервное копирование выбранной книги?"
}
@@ -0,0 +1,417 @@
{
"Email address": "อีเมล",
"Your Password": "รหัสผ่าน",
"Your email address": "อีเมลของคุณ",
"Your password": "รหัสผ่านของคุณ",
"Sign in": "เข้าสู่ระบบ",
"Signing in...": "กำลังเข้าสู่ระบบ...",
"Sign in with {{provider}}": "เข้าสู่ระบบด้วย {{provider}}",
"Already have an account? Sign in": "มีบัญชีแล้ว? เข้าสู่ระบบ",
"Create a Password": "สร้างรหัสผ่าน",
"Sign up": "สมัครสมาชิก",
"Signing up...": "กำลังสมัครสมาชิก...",
"Check your email for the confirmation link": "ตรวจสอบอีเมลเพื่อยืนยัน",
"Signing in ...": "กำลังเข้าสู่ระบบ...",
"Send a magic link email": "ส่งลิงก์เข้าสู่ระบบ",
"Check your email for the magic link": "ตรวจสอบอีเมลเพื่อรับลิงก์",
"Send reset password instructions": "ส่งคำแนะนำรีเซ็ตรหัส",
"Sending reset instructions ...": "กำลังส่งคำแนะนำ...",
"Forgot your password?": "ลืมรหัสผ่าน?",
"Check your email for the password reset link": "ตรวจสอบอีเมลเพื่อรีเซ็ตรหัส",
"Phone number": "เบอร์โทรศัพท์",
"Your phone number": "เบอร์โทรศัพท์ของคุณ",
"Token": "รหัสยืนยัน",
"Your OTP token": "รหัส OTP ของคุณ",
"Verify token": "ยืนยันรหัส",
"Sign in with Google": "เข้าสู่ระบบด้วย Google",
"Sign in with Apple": "เข้าสู่ระบบด้วย Apple",
"Sign in with GitHub": "เข้าสู่ระบบด้วย GitHub",
"New Password": "รหัสผ่านใหม่",
"Your new password": "รหัสผ่านใหม่ของคุณ",
"Update password": "อัปเดตรหัสผ่าน",
"Updating password ...": "กำลังอัปเดตรหัสผ่าน...",
"Your password has been updated": "รหัสผ่านได้รับการอัปเดตแล้ว",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "เกิดข้อผิดพลาด ทีมงานได้รับแจ้งแล้วและกำลังแก้ไข",
"Error Details:": "รายละเอียดข้อผิดพลาด:",
"Try Again": "ลองใหม่",
"Go Back": "กลับ",
"Your Library": "คลังหนังสือ",
"Need help?": "ต้องการความช่วยเหลือ?",
"Contact Support": "ติดต่อฝ่ายสนับสนุน",
"Open": "เปิด",
"Group": "กลุ่ม",
"Details": "รายละเอียด",
"Delete": "ลบ",
"Cancel": "ยกเลิก",
"Confirm Deletion": "ยืนยันการลบ",
"Are you sure to delete {{count}} selected book(s)?_other": "ยืนยันลบหนังสือ {{count}} เล่ม?",
"Deselect Book": "ยกเลิกเลือกหนังสือ",
"Select Book": "เลือกหนังสือ",
"Show Book Details": "แสดงรายละเอียดหนังสือ",
"Download Book": "ดาวน์โหลดหนังสือ",
"Upload Book": "อัปโหลดหนังสือ",
"Deselect Group": "ยกเลิกเลือกกลุ่ม",
"Select Group": "เลือกกลุ่ม",
"Untitled Group": "กลุ่มไม่มีชื่อ",
"Group Books": "จัดกลุ่มหนังสือ",
"Remove From Group": "ลบออกจากกลุ่ม",
"Create New Group": "สร้างกลุ่มใหม่",
"Save": "บันทึก",
"Confirm": "ยืนยัน",
"From Local File": "จากไฟล์ในเครื่อง",
"Search in {{count}} Book(s)..._other": "ค้นหาใน {{count}} เล่ม...",
"Search Books...": "ค้นหาหนังสือ...",
"Clear Search": "ล้างการค้นหา",
"Import Books": "นำเข้าหนังสือ",
"Select Multiple Books": "เลือกหลายเล่ม",
"Select Books": "เลือกหนังสือ",
"Deselect": "ยกเลิกเลือก",
"Select All": "เลือกทั้งหมด",
"Logged in as {{userDisplayName}}": "เข้าสู่ระบบในชื่อ {{userDisplayName}}",
"Logged in": "เข้าสู่ระบบแล้ว",
"Account": "บัญชี",
"Sign In": "เข้าสู่ระบบ",
"Auto Upload Books to Cloud": "อัปโหลดหนังสืออัตโนมัติ",
"Auto Import on File Open": "นำเข้าอัตโนมัติเมื่อเปิดไฟล์",
"Open Last Book on Start": "เปิดหนังสือล่าสุดเมื่อเริ่มใช้",
"Check Updates on Start": "ตรวจสอบอัปเดตเมื่อเริ่มใช้",
"Fullscreen": "เต็มจอ",
"Always on Top": "แสดงด้านบนเสมอ",
"Always Show Status Bar": "แสดงแถบสถานะเสมอ",
"Keep Screen Awake": "เปิดหน้าจอค้าง",
"Reload Page": "โหลดหน้าใหม่",
"Dark Mode": "โหมดมืด",
"Light Mode": "โหมดสว่าง",
"Auto Mode": "โหมดอัตโนมัติ",
"Upgrade to Readest Premium": "อัปเกรดเป็น Readest Premium",
"Download Readest": "ดาวน์โหลด Readest",
"About Readest": "เกี่ยวกับ Readest",
"Help improve Readest": "ช่วยพัฒนา Readest",
"Sharing anonymized statistics": "แชร์สถิติแบบไม่ระบุตัวตน",
"List": "รายการ",
"Grid": "ตาราง",
"Crop": "ครอบตัด",
"Fit": "ปรับพอดี",
"Title": "ชื่อเรื่อง",
"Author": "ผู้แต่ง",
"Format": "รูปแบบ",
"Date Read": "วันที่อ่าน",
"Date Added": "วันที่เพิ่ม",
"Ascending": "น้อยไปมาก",
"Descending": "มากไปน้อย",
"Book Covers": "ปกหนังสือ",
"Sort by...": "เรียงตาม...",
"No supported files found. Supported formats: {{formats}}": "ไม่พบไฟล์ที่รองรับ รูปแบบที่รองรับ: {{formats}}",
"No chapters detected.": "ไม่พบบท",
"Failed to parse the EPUB file.": "ไม่สามารถอ่านไฟล์ EPUB",
"This book format is not supported.": "ไม่รองรับรูปแบบหนังสือนี้",
"Failed to import book(s): {{filenames}}": "ไม่สามารถนำเข้าหนังสือ: {{filenames}}",
"Book uploaded: {{title}}": "อัปโหลดหนังสือแล้ว: {{title}}",
"Insufficient storage quota": "พื้นที่จัดเก็บไม่เพียงพอ",
"Failed to upload book: {{title}}": "ไม่สามารถอัปโหลดหนังสือ: {{title}}",
"Book downloaded: {{title}}": "ดาวน์โหลดหนังสือแล้ว: {{title}}",
"Failed to download book: {{title}}": "ไม่สามารถดาวน์โหลดหนังสือ: {{title}}",
"Book deleted: {{title}}": "ลบหนังสือแล้ว: {{title}}",
"Failed to delete book: {{title}}": "ไม่สามารถลบหนังสือ: {{title}}",
"Deleted cloud backup of the book: {{title}}": "ลบสำรองบนคลาวด์แล้ว: {{title}}",
"Failed to delete cloud backup of the book": "ไม่สามารถลบสำรองบนคลาวด์",
"Welcome to your library. You can import your books here and read them anytime.": "ยินดีต้อนรับสู่คลังหนังสือ คุณสามารถนำเข้าหนังสือและอ่านได้ตลอดเวลา",
"Copied to notebook": "คัดลอกไปยังสมุดบันทึก",
"No annotations to export": "ไม่มีบันทึกให้ส่งออก",
"Exported from Readest": "ส่งออกจาก Readest",
"Highlights & Annotations": "ไฮไลต์และบันทึก",
"Untitled": "ไม่มีชื่อ",
"Note": "บันทึก",
"Copied to clipboard": "คัดลอกแล้ว",
"Copy": "คัดลอก",
"Delete Highlight": "ลบไฮไลต์",
"Highlight": "ไฮไลต์",
"Annotate": "บันทึก",
"Search": "ค้นหา",
"Dictionary": "พจนานุกรม",
"Wikipedia": "วิกิพีเดีย",
"Translate": "แปล",
"Speak": "อ่านออกเสียง",
"Login Required": "ต้องเข้าสู่ระบบ",
"Quota Exceeded": "เกินโควตา",
"Unable to fetch the translation. Please log in first and try again.": "ไม่สามารถแปลได้ กรุณาเข้าสู่ระบบก่อน",
"Unable to fetch the translation. Try again later.": "ไม่สามารถแปลได้ ลองใหม่ภายหลัง",
"Original Text": "ข้อความต้นฉบับ",
"Auto Detect": "ตรวจสอบอัตโนมัติ",
"(detected)": "(ตรวจพบ)",
"Translated Text": "ข้อความแปล",
"Loading...": "กำลังโหลด...",
"No translation available.": "ไม่มีการแปล",
"Translated by {{provider}}.": "แปลโดย {{provider}}",
"Bookmark": "บุ๊กมาร์ก",
"Next Section": "ส่วนถัดไป",
"Previous Section": "ส่วนก่อนหน้า",
"Next Page": "หน้าถัดไป",
"Previous Page": "หน้าก่อนหน้า",
"Go Forward": "ไปข้างหน้า",
"Small": "เล็ก",
"Large": "ใหญ่",
"Notebook": "สมุดบันทึก",
"No notes match your search": "ไม่พบบันทึกที่ตรงกับการค้นหา",
"Excerpts": "ข้อความที่เลือก",
"Notes": "บันทึก",
"Add your notes here...": "เพิ่มบันทึกที่นี่...",
"Search notes and excerpts...": "ค้นหาบันทึกและข้อความ...",
"{{time}} min left in chapter": "เหลือ {{time}} นาทีในบท",
"{{count}} pages left in chapter_other": "เหลือ {{count}} หน้าในบท",
"Theme Mode": "โหมดธีม",
"Invert Image In Dark Mode": "กลับสีรูปในโหมดมืด",
"Override Book Color": "เขียนทับสีหนังสือ",
"Theme Color": "สีธีม",
"Custom": "กำหนดเอง",
"Code Highlighting": "ไฮไลต์โค้ด",
"Enable Highlighting": "เปิดไฮไลต์",
"Code Language": "ภาษาโค้ด",
"Scroll": "เลื่อน",
"Scrolled Mode": "โหมดเลื่อน",
"Continuous Scroll": "เลื่อนต่อเนื่อง",
"Overlap Pixels": "พิกเซลซ้อน",
"Click": "คลิก",
"Clicks for Page Flip": "คลิกเพื่อพลิกหน้า",
"Swap Clicks Area": "สลับพื้นที่คลิก",
"Volume Keys for Page Flip": "ปุ่มเสียงเพื่อพลิกหน้า",
"Animation": "ภาพเคลื่อนไหว",
"Paging Animation": "ภาพเคลื่อนไหวพลิกหน้า",
"Security": "ความปลอดภัย",
"Allow JavaScript": "อนุญาต JavaScript",
"Enable only if you trust the file.": "เปิดเฉพาะไฟล์ที่เชื่อถือได้",
"Global Settings": "การตั้งค่าทั่วไป",
"Apply to All Books": "ใช้กับหนังสือทั้งหมด",
"Apply to This Book": "ใช้กับหนังสือนี้",
"Reset Settings": "รีเซ็ตการตั้งค่า",
"System Fonts": "ฟอนต์ระบบ",
"Serif Font": "ฟอนต์ Serif",
"Sans-Serif Font": "ฟอนต์ Sans-Serif",
"Override Book Font": "เขียนทับฟอนต์หนังสือ",
"Font Size": "ขนาดฟอนต์",
"Default Font Size": "ขนาดฟอนต์เริ่มต้น",
"Minimum Font Size": "ขนาดฟอนต์ต่ำสุด",
"Font Weight": "น้ำหนักฟอนต์",
"Font Family": "ตระกูลฟอนต์",
"Default Font": "ฟอนต์เริ่มต้น",
"CJK Font": "ฟอนต์ CJK",
"Font Face": "หน้าฟอนต์",
"Monospace Font": "ฟอนต์ Monospace",
"Auto": "อัตโนมัติ",
"System Language": "ภาษาระบบ",
"Language": "ภาษา",
"Interface Language": "ภาษาส่วนติดต่อ",
"Translation": "การแปล",
"Enable Translation": "เปิดการแปล",
"Show Source Text": "แสดงข้อความต้นฉบับ",
"Translation Service": "บริการแปล",
"Translate To": "แปลเป็น",
"Override Book Layout": "เขียนทับเลย์เอาต์หนังสือ",
"Writing Mode": "โหมดการเขียน",
"Default": "เริ่มต้น",
"Horizontal Direction": "ทิศทางแนวนอน",
"Vertical Direction": "ทิศทางแนวตั้ง",
"RTL Direction": "ทิศทาง RTL",
"Border Frame": "กรอบขอบ",
"Double Border": "ขอบคู่",
"Border Color": "สีขอบ",
"Paragraph": "ย่อหน้า",
"Paragraph Margin": "ระยะขอบย่อหน้า",
"Line Spacing": "ระยะห่างบรรทัด",
"Word Spacing": "ระยะห่างคำ",
"Letter Spacing": "ระยะห่างตัวอักษร",
"Text Indent": "การเยื้องข้อความ",
"Full Justification": "จัดแนวเต็มบรรทัด",
"Hyphenation": "การใส่เครื่องหมายยัติภังค์",
"Page": "หน้า",
"Top Margin (px)": "ขอบบน (px)",
"Bottom Margin (px)": "ขอบล่าง (px)",
"Left Margin (px)": "ขอบซ้าย (px)",
"Right Margin (px)": "ขอบขวา (px)",
"Column Gap (%)": "ช่องว่างคอลัมน์ (%)",
"Maximum Number of Columns": "จำนวนคอลัมน์สูงสุด",
"Maximum Column Height": "ความสูงคอลัมน์สูงสุด",
"Maximum Column Width": "ความกว้างคอลัมน์สูงสุด",
"Header & Footer": "หัวกระดาษและท้ายกระดาษ",
"Show Header": "แสดงหัวกระดาษ",
"Show Footer": "แสดงท้ายกระดาษ",
"Show Remaining Time": "แสดงเวลาที่เหลือ",
"Show Remaining Pages": "แสดงหน้าที่เหลือ",
"Show Page Number": "แสดงเลขหน้า",
"Apply also in Scrolled Mode": "ใช้ในโหมดเลื่อนด้วย",
"Screen": "หน้าจอ",
"Orientation": "การวางแนว",
"Portrait": "แนวตั้ง",
"Landscape": "แนวนอน",
"Apply": "ใช้",
"Custom Content CSS": "CSS เนื้อหากำหนดเอง",
"Enter CSS for book content styling...": "ป้อน CSS สำหรับจัดรูปแบบเนื้อหา...",
"Custom Reader UI CSS": "CSS ส่วนติดต่อผู้ใช้",
"Enter CSS for reader interface styling...": "ป้อน CSS สำหรับจัดรูปแบบส่วนติดต่อ...",
"Font": "ฟอนต์",
"Layout": "เลย์เอาต์",
"Color": "สี",
"Behavior": "พฤติกรรม",
"Reset {{settings}}": "รีเซ็ต {{settings}}",
"(from 'As You Like It', Act II)": "(จาก 'As You Like It', องก์ที่ 2)",
"Custom Theme": "ธีมกำหนดเอง",
"Theme Name": "ชื่อธีม",
"Text Color": "สีข้อความ",
"Background Color": "สีพื้นหลัง",
"Link Color": "สีลิงก์",
"Preview": "ดูตัวอย่าง",
"Font & Layout": "ฟอนต์และเลย์เอาต์",
"Book Cover": "ปกหนังสือ",
"More Info": "ข้อมูลเพิ่มเติม",
"Parallel Read": "อ่านคู่ขนาน",
"Disable": "ปิด",
"Enable": "เปิด",
"Export Annotations": "ส่งออกบันทึก",
"Sort TOC by Page": "เรียงสารบัญตามหน้า",
"Edit": "แก้ไข",
"Search...": "ค้นหา...",
"Book": "หนังสือ",
"Chapter": "บท",
"Match Case": "ตรงตามตัวอักษร",
"Match Whole Words": "ตรงทั้งคำ",
"Match Diacritics": "ตรงตามเครื่องหมายวรรณยุกต์",
"TOC": "สารบัญ",
"Sidebar": "แถบข้าง",
"Disable Translation": "ปิดการแปล",
"Translation Not Available": "ไม่มีการแปล",
"TTS not supported for PDF": "ไม่รองรับ TTS สำหรับ PDF",
"TTS not supported in this device": "อุปกรณ์นี้ไม่รองรับ TTS",
"No Timeout": "ไม่จำกัดเวลา",
"{{value}} minute": "{{value}} นาที",
"{{value}} minutes": "{{value}} นาที",
"{{value}} hour": "{{value}} ชั่วโมง",
"{{value}} hours": "{{value}} ชั่วโมง",
"Slow": "ช้า",
"Fast": "เร็ว",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} เสียง",
"Sign in to Sync": "เข้าสู่ระบบเพื่อซิงค์",
"Synced at {{time}}": "ซิงค์เมื่อ {{time}}",
"Never synced": "ไม่เคยซิงค์",
"Reading Progress Synced": "ซิงค์ความคืบหน้าแล้ว",
"Delete Your Account?": "ลบบัญชีของคุณ?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "การกระทำนี้ไม่สามารถยกเลิกได้ ข้อมูลทั้งหมดบนคลาวด์จะถูกลบถาวร",
"Delete Permanently": "ลบถาวร",
"Manage Subscription": "จัดการการสมัครสมาชิก",
"Sign Out": "ออกจากระบบ",
"Delete Account": "ลบบัญชี",
"Upgrade to {{plan}}": "อัปเกรดเป็น {{plan}}",
"Complete Your Subscription": "ดำเนินการสมัครสมาชิกให้เสร็จ",
"Coming Soon": "เร็วๆ นี้",
"Upgrade to Plus or Pro": "อัปเกรดเป็น Plus หรือ Pro",
"Current Plan": "แผนปัจจุบัน",
"Plan Limits": "ข้อจำกัดแผน",
"Failed to delete user. Please try again later.": "ไม่สามารถลบผู้ใช้ ลองใหม่ภายหลัง",
"Failed to manage subscription. Please try again later.": "ไม่สามารถจัดการสมาชิก ลองใหม่ภายหลัง",
"Loading profile...": "กำลังโหลดโปรไฟล์...",
"Processing your payment...": "กำลังประมวลผลการชำระเงิน...",
"Please wait while we confirm your subscription.": "กรุณารอสักครู่เพื่อยืนยันการสมัครสมาชิก",
"Payment Processing": "กำลังประมวลผลการชำระเงิน",
"Your payment is being processed. This usually takes a few moments.": "กำลังประมวลผลการชำระเงิน โดยปกติใช้เวลาสักครู่",
"Payment Failed": "การชำระเงินล้มเหลว",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "ไม่สามารถประมวลผลการสมัครสมาชิก ลองใหม่หรือติดต่อฝ่ายสนับสนุน",
"Back to Profile": "กลับไปที่โปรไฟล์",
"Subscription Successful!": "สมัครสมาชิกสำเร็จ!",
"Thank you for your subscription! Your payment has been processed successfully.": "ขอบคุณสำหรับการสมัครสมาชิก! ชำระเงินเรียบร้อยแล้ว",
"Email:": "อีเมล:",
"Plan:": "แผน:",
"Amount:": "จำนวนเงิน:",
"Subscription ID:": "รหัสสมาชิก:",
"Go to Library": "ไปที่คลังหนังสือ",
"Need help? Contact our support team at support@readest.com": "ต้องการความช่วยเหลือ? ติดต่อทีมสนับสนุนที่ support@readest.com",
"Free Plan": "แผนฟรี",
"month": "เดือน",
"Cross-Platform Sync": "ซิงค์ข้ามแพลตฟอร์ม",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "ซิงค์คลังหนังสือ ความคืบหน้า ไฮไลต์ และบันทึกข้ามอุปกรณ์—ไม่สูญหายอีกต่อไป",
"Customizable Reading": "การอ่านแบบปรับแต่งได้",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "ปรับแต่งทุกรายละเอียดด้วยฟอนต์ เลย์เอาต์ ธีม และการตั้งค่าขั้นสูงเพื่อประสบการณ์การอ่านที่สมบูรณ์แบบ",
"AI Read Aloud": "AI อ่านออกเสียง",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "เพลิดเพลินกับการอ่านแบบเสรีมือด้วยเสียง AI ที่เป็นธรรมชาติ",
"AI Translations": "การแปลด้วย AI",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "แปลข้อความได้ทันทีด้วยพลัง Google, Azure หรือ DeepL—เข้าใจเนื้อหาทุกภาษา",
"Community Support": "การสนับสนุนชุมชน",
"Connect with fellow readers and get help fast in our friendly community channels.": "เชื่อมต่อกับนักอ่านอื่นๆ และรับความช่วยเหลือในชุมชนที่เป็นมิตร",
"Cloud Sync Storage": "พื้นที่จัดเก็บซิงค์คลาวด์",
"AI Translations (per day)": "การแปล AI (ต่อวัน)",
"Plus Plan": "แผน Plus",
"Includes All Free Plan Benefits": "รวมสิทธิประโยชน์แผนฟรีทั้งหมด",
"Unlimited AI Read Aloud Hours": "AI อ่านออกเสียงไม่จำกัด",
"Listen without limits—convert as much text as you like into immersive audio.": "ฟังไม่จำกัด—แปลงข้อความเป็นเสียงได้ไม่อั้น",
"More AI Translations": "การแปล AI เพิ่มขึ้น",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "ปลดล็อกความสามารถการแปลที่เพิ่มขึ้นพร้อมโควตารายวันและตัวเลือกขั้นสูง",
"DeepL Pro Access": "การเข้าถึง DeepL Pro",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "แปลได้สูงสุด 100,000 อักขระต่อวันด้วยเครื่องมือแปลที่แม่นยำที่สุด",
"2 GB Cloud Sync Storage": "พื้นที่ซิงค์คลาวด์ 2 GB",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "จัดเก็บและเข้าถึงคอลเลคชันการอ่านอย่างปลอดภัยด้วยพื้นที่คลาวด์ 2 GB",
"Priority Support": "การสนับสนุนแบบพิเศษ",
"Enjoy faster responses and dedicated assistance whenever you need help.": "รับการตอบสนองที่รวดเร็วและความช่วยเหลือเฉพาะเมื่อต้องการ",
"Pro Plan": "แผน Pro",
"Includes All Plus Plan Benefits": "รวมสิทธิประโยชน์แผน Plus ทั้งหมด",
"Early Feature Access": "เข้าถึงฟีเจอร์ใหม่ก่อนใคร",
"Be the first to explore new features, updates, and innovations before anyone else.": "เป็นคนแรกที่ได้สำรวจฟีเจอร์ใหม่ อัปเดต และนวัตกรรมก่อนใคร",
"Advanced AI Tools": "เครื่องมือ AI ขั้นสูง",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "ใช้เครื่องมือ AI อันทรงพลังเพื่อการอ่าน การแปล และการค้นหาเนื้อหาที่ฉลาดขึ้น",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "แปลได้สูงสุด 500,000 อักขระต่อวันด้วยเครื่องมือแปลที่แม่นยำที่สุด",
"10 GB Cloud Sync Storage": "พื้นที่ซิงค์คลาวด์ 10 GB",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "จัดเก็บและเข้าถึงคอลเลคชันการอ่านอย่างปลอดภัยด้วยพื้นที่คลาวด์ 10 GB",
"Version {{version}}": "เวอร์ชัน {{version}}",
"Check Update": "ตรวจสอบอัปเดต",
"Already the latest version": "เป็นเวอร์ชันล่าสุดแล้ว",
"Checking for updates...": "กำลังตรวจสอบอัปเดต...",
"Error checking for updates": "เกิดข้อผิดพลาดในการตรวจสอบอัปเดต",
"Book Details": "รายละเอียดหนังสือ",
"Unknown": "ไม่ทราบ",
"Publisher:": "สำนักพิมพ์:",
"Published:": "วันที่พิมพ์:",
"Updated:": "อัปเดต:",
"Added:": "เพิ่มเมื่อ:",
"Language:": "ภาษา:",
"Subjects:": "หัวข้อ:",
"Format:": "รูปแบบ:",
"File Size:": "ขนาดไฟล์:",
"Description:": "คำอธิบาย:",
"No description available": "ไม่มีคำอธิบาย",
"Are you sure to delete the selected book?": "ยืนยันลบหนังสือที่เลือก?",
"Are you sure to delete the cloud backup of the selected book?": "ยืนยันลบสำรองคลาวด์ของหนังสือที่เลือก?",
"Drop to Import Books": "วางเพื่อนำเข้าหนังสือ",
"A new version of Readest is available!": "Readest เวอร์ชันใหม่พร้อมใช้งาน!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} พร้อมใช้งาน (เวอร์ชันที่ติดตั้ง {{currentVersion}})",
"Download and install now?": "ดาวน์โหลดและติดตั้งเลย?",
"Downloading {{downloaded}} of {{contentLength}}": "กำลังดาวน์โหลด {{downloaded}} จาก {{contentLength}}",
"Download finished": "ดาวน์โหลดเสร็จสิ้น",
"DOWNLOAD & INSTALL": "ดาวน์โหลดและติดตั้ง",
"Changelog:": "รายการเปลี่ยนแปลง:",
"Software Update": "อัปเดตซอฟต์แวร์",
"{{percentage}}% of Cloud Sync Space Used.": "ใช้พื้นที่ซิงค์คลาวด์ {{percentage}}%",
"Translation Characters": "อักขระการแปล",
"{{percentage}}% of Daily Translation Characters Used.": "ใช้อักขระการแปลรายวัน {{percentage}}%",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "ใช้โควตาการแปลรายวันครบแล้ว อัปเกรดแผนเพื่อใช้การแปล AI ต่อ",
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Azure Translator": "Azure Translator",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Gray": "สีเทา",
"Sepia": "สีซีเปีย",
"Grass": "สีเขียว",
"Cherry": "สีชมพู",
"Sky": "สีฟ้า",
"Solarized": "Solarized",
"Gruvbox": "Gruvbox",
"Nord": "Nord",
"Contrast": "ความคมชัด",
"Sunset": "สีพระอาทิตย์ตก",
"Reveal in Finder": "แสดงใน Finder",
"Reveal in File Explorer": "แสดงใน File Explorer",
"Reveal in Folder": "แสดงในโฟลเดอร์",
"Don't have an account? Sign up": "ยังไม่มีบัญชี? สมัครสมาชิก",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "โลกทั้งใบเป็นเวที\nและชายหญิงทั้งปวงเป็นนักแสดง\nมีทางเข้าและทางออก\nแต่ละคนในชีวิตเล่นหลายบท\nการกระทำของเขาแบ่งเป็นเจ็ดวัย\n\n— วิลเลียม เชกสเปียร์"
}
@@ -157,7 +157,7 @@
"Create a Password": "Şifre oluştur",
"Sign up": "Kaydol",
"Signing up...": "Kaydolunuyor...",
"Dont have an account? Sign up": "Hesabınız yok mu? Kaydolun",
"Don't have an account? Sign up": "Hesabınız yok mu? Kaydolun",
"Check your email for the confirmation link": "Onay bağlantısı için e-posta adresinizi kontrol edin",
"Signing in ...": "Giriş yapılıyor...",
"Send a magic link email": "Sihirli bağlantı e-postası gönder",
@@ -359,8 +359,6 @@
"Upgrade to Plus or Pro": "Plus veya Pro'ya yükselt",
"Current Plan": "Mevcut Plan",
"Plan Limits": "Plan Limitleri",
"Available Plans": "Mevcut Planlar",
"{{current}} of {{all}}": "{{all}} üzerinden {{current}}",
"Failed to manage subscription. Please try again later.": "Abonelik yönetimi başarısız oldu. Lütfen daha sonra tekrar deneyin.",
"Processing your payment...": "Ödemeniz işleniyor...",
"Please wait while we confirm your subscription.": "Aboneliğinizi onaylıyoruz, lütfen bekleyin.",
@@ -416,5 +414,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Mevcut en doğru çeviri motoruyla günlük 500.000 karaktere kadar çeviri yapın.",
"10 GB Cloud Sync Storage": "10 GB Bulut Senkronizasyon Depolama",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Tüm okuma koleksiyonunuzu 2 GB'a kadar güvenli bulut depolamayla saklayın ve erişin.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Tüm okuma koleksiyonunuzu 10 GB'a kadar güvenli bulut depolamayla saklayın ve erişin."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Tüm okuma koleksiyonunuzu 10 GB'a kadar güvenli bulut depolamayla saklayın ve erişin.",
"Deleted cloud backup of the book: {{title}}": "Kitabın bulut yedeği silindi: {{title}}",
"Failed to delete cloud backup of the book": "Kitabın bulut yedeğini silme işlemi başarısız oldu.",
"Are you sure to delete the cloud backup of the selected book?": "Seçilen kitabın bulut yedeğini silmek istediğinize emin misiniz?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Створити пароль",
"Sign up": "Зареєструватися",
"Signing up...": "Реєстрація...",
"Dont have an account? Sign up": "Немає акаунту? Зареєструйтеся",
"Don't have an account? Sign up": "Немає акаунту? Зареєструйтеся",
"Check your email for the confirmation link": "Перевірте свою електронну пошту для підтвердження посилання",
"Signing in ...": "Увійти...",
"Send a magic link email": "Відправити магічне посилання на електронну пошту",
@@ -367,8 +367,6 @@
"Upgrade to Plus or Pro": "Перейти на Plus або Pro",
"Current Plan": "Поточний план",
"Plan Limits": "Обмеження плану",
"Available Plans": "Доступні плани",
"{{current}} of {{all}}": "{{current}} із {{all}}",
"Failed to manage subscription. Please try again later.": "Не вдалося керувати підпискою. Спробуйте ще раз пізніше.",
"Processing your payment...": "Обробка вашого платежу...",
"Please wait while we confirm your subscription.": "Зачекайте, ми підтверджуємо вашу підписку.",
@@ -424,5 +422,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Перекладайте до 500 000 символів щодня з найточнішим доступним движком перекладу.",
"10 GB Cloud Sync Storage": "10 ГБ Хмарного Сховища",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Безпечно зберігайте та отримуйте доступ до всієї колекції читання з до 2 ГБ хмарного сховища.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Безпечно зберігайте та отримуйте доступ до всієї колекції читання з до 10 ГБ хмарного сховища."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Безпечно зберігайте та отримуйте доступ до всієї колекції читання з до 10 ГБ хмарного сховища.",
"Deleted cloud backup of the book: {{title}}": "Видалено хмарну резервну копію книги: {{title}}",
"Failed to delete cloud backup of the book": "Не вдалося видалити хмарну резервну копію книги.",
"Are you sure to delete the cloud backup of the selected book?": "Ви впевнені, що хочете видалити хмарну резервну копію вибраної книги?"
}
@@ -157,7 +157,7 @@
"Create a Password": "Tạo mật khẩu",
"Sign up": "Đăng ký",
"Signing up...": "Đang đăng ký...",
"Dont have an account? Sign up": "Chưa có tài khoản? Đăng ký",
"Don't have an account? Sign up": "Chưa có tài khoản? Đăng ký",
"Check your email for the confirmation link": "Kiểm tra email của bạn để nhận liên kết xác nhận",
"Signing in ...": "Đang đăng nhập ...",
"Send a magic link email": "Gửi email liên kết ma thuật",
@@ -355,8 +355,6 @@
"Upgrade to Plus or Pro": "Nâng cấp lên Plus hoặc Pro",
"Current Plan": "Gói hiện tại",
"Plan Limits": "Giới hạn gói",
"Available Plans": "Gói khả dụng",
"{{current}} of {{all}}": "{{current}} trên {{all}}",
"Failed to manage subscription. Please try again later.": "Không thể quản lý đăng ký. Vui lòng thử lại sau.",
"Processing your payment...": "Đang xử lý thanh toán...",
"Please wait while we confirm your subscription.": "Vui lòng chờ trong khi chúng tôi xác nhận đăng ký của bạn.",
@@ -412,5 +410,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Dịch tới 500.000 ký tự hàng ngày với công cụ dịch chính xác nhất hiện có.",
"10 GB Cloud Sync Storage": "Lưu Trữ Đồng Bộ Đám Mây 10 GB",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "Lưu trữ và truy cập an toàn toàn bộ bộ sưu tập đọc với tới 2 GB lưu trữ đám mây.",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Lưu trữ và truy cập an toàn toàn bộ bộ sưu tập đọc với tới 10 GB lưu trữ đám mây."
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Lưu trữ và truy cập an toàn toàn bộ bộ sưu tập đọc với tới 10 GB lưu trữ đám mây.",
"Deleted cloud backup of the book: {{title}}": "Đã xóa bản sao lưu đám mây của sách: {{title}}",
"Failed to delete cloud backup of the book": "Không thể xóa bản sao lưu đám mây của sách.",
"Are you sure to delete the cloud backup of the selected book?": "Bạn có chắc chắn muốn xóa bản sao lưu đám mây của sách đã chọn không?"
}
@@ -157,7 +157,7 @@
"Create a Password": "创建密码",
"Sign up": "注册",
"Signing up...": "正在注册...",
"Dont have an account? Sign up": "没有账号?注册",
"Don't have an account? Sign up": "没有账号?注册",
"Check your email for the confirmation link": "检查您的电子邮件以获取确认链接",
"Signing in ...": "正在登录...",
"Send a magic link email": "发送魔法链接邮件",
@@ -355,8 +355,6 @@
"Upgrade to Plus or Pro": "升级到 Plus 或 Pro",
"Current Plan": "当前套餐",
"Plan Limits": "套餐限制",
"Available Plans": "可用套餐",
"{{current}} of {{all}}": "{{current}} / {{all}}",
"Failed to manage subscription. Please try again later.": "无法管理订阅,请稍后再试。",
"Processing your payment...": "正在处理付款...",
"Please wait while we confirm your subscription.": "请稍候,我们正在确认您的订阅。",
@@ -412,5 +410,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "使用最精准的翻译引擎每天可翻译50万字。",
"10 GB Cloud Sync Storage": "10GB 云同步存储",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "使用多达 2GB 的安全云存储保存和访问您的整个阅读收藏。",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "使用多达 10GB 的安全云存储保存和访问您的整个阅读收藏。"
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "使用多达 10GB 的安全云存储保存和访问您的整个阅读收藏。",
"Deleted cloud backup of the book: {{title}}": "已删除书籍云备份:{{title}}",
"Failed to delete cloud backup of the book": "无法删除书籍云备份。",
"Are you sure to delete the cloud backup of the selected book?": "您确定要删除所选书籍的云备份吗?"
}
@@ -157,7 +157,7 @@
"Create a Password": "創建密碼",
"Sign up": "註冊",
"Signing up...": "正在註冊...",
"Dont have an account? Sign up": "沒有帳號?註冊",
"Don't have an account? Sign up": "沒有帳號?註冊",
"Check your email for the confirmation link": "檢查您的電子郵件以獲取確認連結",
"Signing in ...": "正在登入...",
"Send a magic link email": "發送魔法連結郵件",
@@ -355,8 +355,6 @@
"Upgrade to Plus or Pro": "升級到 Plus 或 Pro",
"Current Plan": "當前方案",
"Plan Limits": "方案限制",
"Available Plans": "可用方案",
"{{current}} of {{all}}": "{{current}} / {{all}}",
"Failed to manage subscription. Please try again later.": "無法管理訂閱,請稍後再試。",
"Processing your payment...": "正在處理付款...",
"Please wait while we confirm your subscription.": "請稍候,我們正在確認您的訂閱。",
@@ -412,5 +410,8 @@
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "使用最精準的翻譯引擎每天翻譯多達50萬字元。",
"10 GB Cloud Sync Storage": "10GB 雲端同步儲存",
"Securely store and access your entire reading collection with up to 2 GB of cloud storage.": "使用多達 2GB 的安全雲端儲存保存和存取您的整個閱讀收藏。",
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "使用多達 10GB 的安全雲端儲存保存和存取您的整個閱讀收藏。"
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "使用多達 10GB 的安全雲端儲存保存和存取您的整個閱讀收藏。",
"Deleted cloud backup of the book: {{title}}": "已刪除書籍的雲端備份:{{title}}",
"Failed to delete cloud backup of the book": "無法刪除書籍的雲端備份。",
"Are you sure to delete the cloud backup of the selected book?": "您確定要刪除所選書籍的雲端備份嗎?"
}
+15
View File
@@ -1,5 +1,20 @@
{
"releases": {
"0.9.63": {
"date": "2025-07-07",
"notes": [
"Reader: Fixed links not working in some MOBI books",
"TTS: Fixed an issue where some text was skipped during reading",
"Library: You can now delete only the cloud backup of a book without removing the local copy",
"Performance: Fonts in the font list now load only when displayed for faster startup",
"Performance: Opening large MOBI books is now significantly faster",
"Layout: Fixed overlay scrollbar display in virtualized book lists",
"Layout: Improved grid cover image height in crop mode",
"Layout: Removed unsupported screen orientation lock option on iPad",
"Layout: System context menu no longer appears when selecting text on iPad",
"Translation: Added Thai (th-TH) language support"
]
},
"0.9.62": {
"date": "2025-07-02",
"notes": [
@@ -15,28 +15,29 @@ val tauriProperties = Properties().apply {
}
android {
compileSdk = 34
compileSdk = 35
namespace = "com.bilingify.readest"
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.bilingify.readest"
minSdk = 26
targetSdk = 34
targetSdk = 35
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
if (keystorePropertiesFile.exists()) {
create("signing") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["password"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["password"] as String
}
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["password"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["password"] as String
}
}
buildTypes {
@@ -45,14 +46,20 @@ android {
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("signing")
}
packaging {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("signing")
}
}
}
kotlinOptions {
@@ -73,6 +73,10 @@
<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>
@@ -5,7 +5,7 @@ plugins {
android {
namespace = "com.readest.native_bridge"
compileSdk = 34
compileSdk = 35
defaultConfig {
minSdk = 21
@@ -5,7 +5,7 @@ plugins {
android {
namespace = "com.readest.native_tts"
compileSdk = 34
compileSdk = 35
defaultConfig {
minSdk = 21
+1 -1
View File
@@ -15,7 +15,7 @@
"security": {
"csp": {
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://*.microsofttranslator.com https://edge.microsoft.com",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://ik.imagekit.io",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://ik.imagekit.io",
+1 -1
View File
@@ -241,7 +241,7 @@ export default function AuthPage() {
button_label: _('Sign up'),
loading_button_label: _('Signing up...'),
social_provider_text: _('Sign in with {{provider}}'),
link_text: _('Dont have an account? Sign up'),
link_text: _("Don't have an account? Sign up"),
confirmation_text: _('Check your email for the confirmation link'),
},
magic_link: {
@@ -62,6 +62,7 @@ const BookItem: React.FC<BookItemProps> = ({
<div
className={clsx(
'relative flex aspect-[28/41] items-center justify-center',
coverFit === 'crop' && 'overflow-hidden shadow-md',
mode === 'list' && 'min-w-20',
)}
>
@@ -121,7 +122,7 @@ const BookItem: React.FC<BookItemProps> = ({
onPointerLeave={(e) => stopEvent(e)}
onClick={() => showBookDetailsModal(book)}
>
<div className='pt-[1px]'>
<div className='pt-[2px] sm:pt-[1px]'>
<LiaInfoCircleSolid size={iconSize15} />
</div>
</button>
@@ -285,15 +285,15 @@ const Bookshelf: React.FC<BookshelfProps> = ({
<div className='bookshelf'>
<div
className={clsx(
'transform-wrapper',
'bookshelf-items transform-wrapper',
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
viewMode === 'list' && 'flex flex-col',
)}
>
{filteredBookshelfItems.map((item, index) => (
{filteredBookshelfItems.map((item) => (
<BookshelfItem
key={`library-item-${'hash' in item ? item.hash : item.id}-${index}`}
key={`library-item-${'hash' in item ? item.hash : item.id}`}
item={item}
mode={viewMode as LibraryViewModeType}
coverFit={coverFit as LibraryCoverFitType}
+27 -6
View File
@@ -35,7 +35,6 @@ import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { lockScreenOrientation } from '@/utils/bridge';
import { mountAdditionalFonts } from '@/utils/font';
import {
tauriHandleSetAlwaysOnTop,
tauriHandleToggleFullScreen,
@@ -115,10 +114,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
},
});
useEffect(() => {
mountAdditionalFonts(document);
}, []);
useEffect(() => {
const doCheckAppUpdates = async () => {
if (appService?.hasUpdater && settings.autoCheckUpdates) {
@@ -508,7 +503,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const handleBookDelete = async (book: Book) => {
try {
await appService?.deleteBook(book, !!book.uploadedAt);
await appService?.deleteBook(book, !!book.uploadedAt, true);
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
@@ -530,6 +525,31 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
};
const handleBookDeleteCloudBackup = async (book: Book) => {
try {
await appService?.deleteBook(book, !!book.uploadedAt, false);
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Deleted cloud backup of the book: {{title}}', {
title: book.title,
}),
});
return true;
} catch (e) {
console.error(e);
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Failed to delete cloud backup of the book', {
title: book.title,
}),
});
return false;
}
};
const handleImportBooks = async () => {
setIsSelectMode(false);
console.log('Importing books...');
@@ -659,6 +679,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleBookDeleteCloudBackup={handleBookDeleteCloudBackup}
/>
)}
<AboutWindow />
@@ -15,6 +15,7 @@ import { RxLineHeight } from 'react-icons/rx';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { eventDispatcher } from '@/utils/event';
@@ -45,6 +46,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getConfig, setConfig } = useBookDataStore();
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
@@ -55,6 +57,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
const marginIconSize = useResponsiveSize(20);
const view = getView(bookKey);
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const viewState = getViewState(bookKey);
@@ -131,15 +134,17 @@ const FooterBar: React.FC<FooterBarProps> = ({
handleSpeakText();
} else if (tab === 'toc') {
setHoveredBookKey('');
if (viewSettings) {
viewSettings.sideBarTab = 'toc';
if (config && config.viewSettings) {
config.viewSettings.sideBarTab = 'toc';
setConfig(bookKey, config);
}
setSideBarVisible(true);
} else if (tab === 'note') {
setHoveredBookKey('');
setSideBarVisible(true);
if (viewSettings) {
viewSettings.sideBarTab = 'annotations';
if (config && config.viewSettings) {
config.viewSettings.sideBarTab = 'annotations';
setConfig(bookKey, config);
}
}
};
@@ -42,7 +42,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
? `${section.current + 1} · ${section.total}`
: `${section.current + 1} / ${section.total}`
: ''
: pageinfo
: pageinfo && pageinfo.current >= 0 && pageinfo.total > 0
? _(isVertical ? '{{currentPage}} · {{totalPage}}' : 'Loc. {{currentPage}} / {{totalPage}}', {
currentPage: pageinfo.current + 1,
totalPage: pageinfo.total,
@@ -120,7 +120,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
detail.doc?.addEventListener('touchstart', handleTouchStart);
detail.doc?.addEventListener('touchmove', handleTouchmove);
detail.doc?.addEventListener('touchend', handleTouchEnd);
detail.doc?.addEventListener('pointerup', () => handlePointerup(doc, index));
detail.doc?.addEventListener('pointerup', (ev: PointerEvent) =>
handlePointerup(doc, index, ev),
);
detail.doc?.addEventListener('selectionchange', () => handleSelectionchange(doc, index));
// Disable the default context menu on mobile devices,
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import React from 'react';
import React, { useMemo } from 'react';
import { FixedSizeList as List } from 'react-window';
import { FiChevronUp, FiChevronLeft } from 'react-icons/fi';
import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
@@ -15,6 +16,44 @@ interface DropdownProps {
onGetFontFamily: (option: string, family: string) => string;
}
interface FontItemProps {
index: number;
style: React.CSSProperties;
data: {
options: { option: string; label?: string }[];
selected: string;
onSelect: (option: string) => void;
onGetFontFamily: (option: string, family: string) => string;
family: string;
iconSize: number;
};
}
const FontItem: React.FC<FontItemProps> = ({ index, style, data }) => {
const { options, selected, onSelect, onGetFontFamily, family, iconSize: iconSize16 } = data;
const option = options[index]!;
return (
<li
className='px-1 sm:px-2'
key={option.option}
style={style}
onClick={() => onSelect(option.option)}
>
<div className='flex w-full items-center overflow-hidden !px-0 text-sm'>
<span style={{ minWidth: `${iconSize16}px` }}>
{selected === option.option && (
<MdCheck className='text-base-content' size={iconSize16} />
)}
</span>
<span style={{ fontFamily: onGetFontFamily(option.option, family) }}>
{option.label || option.option}
</span>
</div>
</li>
);
};
const FontDropdown: React.FC<DropdownProps> = ({
family,
selected,
@@ -25,9 +64,38 @@ const FontDropdown: React.FC<DropdownProps> = ({
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const iconSize16 = useResponsiveSize(16);
const iconSize = useResponsiveSize(16);
const allOptions = [...options, ...(moreOptions ?? [])];
const selectedOption = allOptions.find((option) => option.option === selected) ?? allOptions[0]!;
const ITEM_HEIGHT = 40;
const MAX_HEIGHT = 320;
const mainListData = useMemo(
() => ({
options,
selected,
onSelect,
onGetFontFamily,
family: family ?? '',
iconSize,
appService,
}),
[options, selected, onSelect, onGetFontFamily, family, iconSize, appService],
);
const moreListData = useMemo(
() => ({
options: moreOptions ?? [],
selected,
onSelect,
onGetFontFamily,
family: family ?? '',
iconSize,
}),
[moreOptions, selected, onSelect, onGetFontFamily, family, iconSize],
);
return (
<div className='dropdown dropdown-top'>
<button
@@ -44,34 +112,36 @@ const FontDropdown: React.FC<DropdownProps> = ({
>
{selectedOption.label}
</span>
<FiChevronUp size={iconSize16} />
<FiChevronUp size={iconSize} />
</div>
</button>
<ul
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute z-[1] mt-4 shadow',
'!sm:px-2 right-[-32px] w-[46vw] !px-1 sm:right-0 sm:w-44',
moreOptions?.length ? '' : 'inline max-h-80 overflow-y-scroll',
'right-[-32px] w-[46vw] !px-0 sm:right-0 sm:w-44',
moreOptions?.length ? '' : 'inline overflow-hidden',
)}
>
{options.map(({ option, label }) => (
<li key={option} onClick={() => onSelect(option)}>
<div className='flex w-full items-center overflow-hidden px-0 text-sm'>
<span style={{ minWidth: `${iconSize16}px` }}>
{selected === option && <MdCheck className='text-base-content' size={iconSize16} />}
</span>
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
{label || option}
</span>
</div>
</li>
))}
{/* Virtualized main options */}
<div style={{ height: Math.min(options.length * ITEM_HEIGHT, MAX_HEIGHT) }}>
<List
width='100%'
height={Math.min(options.length * ITEM_HEIGHT, MAX_HEIGHT)}
itemCount={options.length}
itemSize={ITEM_HEIGHT}
itemData={mainListData}
>
{FontItem}
</List>
</div>
{/* More options with nested dropdown */}
{moreOptions && moreOptions.length > 0 && (
<li className='dropdown dropdown-left dropdown-top'>
<li className='dropdown dropdown-left dropdown-top px-1 sm:px-2'>
<div className='flex items-center px-0 text-sm'>
<span style={{ minWidth: `${iconSize16}px` }}>
<FiChevronLeft size={iconSize16} />
<span style={{ minWidth: `${iconSize}px` }}>
<FiChevronLeft size={iconSize} />
</span>
<span>{_('System Fonts')}</span>
</div>
@@ -79,29 +149,21 @@ const FontDropdown: React.FC<DropdownProps> = ({
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] shadow',
'!sm:px-2 !mr-4 mb-[-46px] inline max-h-80 w-[46vw] overflow-y-scroll !px-1 sm:w-[200px]',
'!mr-4 mb-[-46px] inline w-[46vw] overflow-hidden !px-0 sm:w-[200px]',
)}
>
{moreOptions.map((option, index) => (
<li key={`${index}-${option.option}`} onClick={() => onSelect(option.option)}>
<div className='flex w-full items-center overflow-hidden px-0 text-sm'>
<span style={{ minWidth: `${iconSize16}px` }}>
{selected === option.option && (
<MdCheck className='text-base-content' size={iconSize16} />
)}
</span>
<span
style={
!appService?.isLinuxApp
? { fontFamily: onGetFontFamily(option.option, family ?? '') }
: {}
}
>
{option.label || option.option}
</span>
</div>
</li>
))}
{/* Virtualized more options */}
<div style={{ height: Math.min(moreOptions.length * ITEM_HEIGHT, MAX_HEIGHT) }}>
<List
width='100%'
height={Math.min(moreOptions.length * ITEM_HEIGHT, MAX_HEIGHT)}
itemCount={moreOptions.length}
itemSize={ITEM_HEIGHT}
itemData={moreListData}
>
{FontItem}
</List>
</div>
</ul>
</li>
)}
@@ -676,7 +676,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
</div>
</div>
{appService?.isMobileApp && (
{appService?.hasOrientationLock && (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
@@ -51,12 +51,16 @@ const SidebarContent: React.FC<{
'font-sans text-base font-normal sm:text-sm',
)}
>
<OverlayScrollbarsComponent options={{ scrollbars: { autoHide: 'scroll' } }} defer>
<OverlayScrollbarsComponent
className='min-h-0 flex-1'
options={{ scrollbars: { autoHide: 'scroll' }, showNativeOverlaidScrollbars: false }}
defer
>
<div
className={clsx(
'scroll-container min-h-0 flex-1 transition-opacity duration-300 ease-in-out',
{ 'opacity-0': fade, 'opacity-100': !fade },
)}
className={clsx('scroll-container h-full transition-opacity duration-300 ease-in-out', {
'opacity-0': fade,
'opacity-100': !fade,
})}
>
{targetTab === 'toc' && bookDoc.toc && (
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
@@ -1,7 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FixedSizeList as VirtualList } from 'react-window';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import 'overlayscrollbars/overlayscrollbars.css';
import { TOCItem } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { findParentPath } from '@/utils/toc';
@@ -32,6 +35,7 @@ const TOCView: React.FC<{
bookKey: string;
toc: TOCItem[];
}> = ({ bookKey, toc }) => {
const { appService } = useEnv();
const { getView, getProgress, getViewState, getViewSettings } = useReaderStore();
const { sideBarBookKey, isSideBarVisible } = useSidebarStore();
const viewSettings = getViewSettings(bookKey)!;
@@ -42,9 +46,41 @@ const TOCView: React.FC<{
const [containerHeight, setContainerHeight] = useState(400);
const containerRef = useRef<HTMLDivElement | null>(null);
const listOuterRef = useRef<HTMLDivElement | null>(null);
const vitualListRef = useRef<VirtualList | null>(null);
const staticListRef = useRef<HTMLDivElement | null>(null);
const [initialize] = useOverlayScrollbars({
defer: true,
options: {
scrollbars: {
autoHide: 'scroll',
},
showNativeOverlaidScrollbars: false,
},
events: {
initialized(osInstance) {
const { viewport } = osInstance.elements();
viewport.style.overflowX = `var(--os-viewport-overflow-x)`;
viewport.style.overflowY = `var(--os-viewport-overflow-y)`;
},
},
});
useEffect(() => {
const { current: root } = containerRef;
const { current: virtualOuter } = listOuterRef;
if (root && virtualOuter) {
initialize({
target: root,
elements: {
viewport: virtualOuter,
},
});
}
}, [initialize]);
useTextTranslation(bookKey, containerRef.current, false, 'translation-target-toc');
useEffect(() => {
@@ -78,8 +114,11 @@ const TOCView: React.FC<{
};
}, []);
const activeHref = useMemo(() => progress?.sectionHref || null, [progress]);
const activeHref = useMemo(() => progress?.sectionHref || null, [progress?.sectionHref]);
const flatItems = useFlattenedTOC(toc, expandedItems);
const activeItemIndex = useMemo(() => {
return flatItems.findIndex((item) => item.item.href === activeHref);
}, [flatItems, activeHref]);
const handleToggleExpand = useCallback((item: TOCItem) => {
const href = item.href || '';
@@ -136,7 +175,7 @@ const TOCView: React.FC<{
const virtualItemSize = useMemo(() => {
return window.innerWidth >= 640 && !viewSettings?.translationEnabled ? 37 : 57;
}, [viewSettings]);
}, [viewSettings?.translationEnabled]);
const virtualListData = useMemo(
() => ({
@@ -163,40 +202,44 @@ const TOCView: React.FC<{
useEffect(() => {
if (flatItems.length > 0) {
setTimeout(scrollToActiveItem, 0);
setTimeout(scrollToActiveItem, appService?.isAndroidApp ? 300 : 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [flatItems, scrollToActiveItem]);
return (
<div className='rounded' ref={containerRef}>
{flatItems.length > 256 ? (
<VirtualList
ref={vitualListRef}
width='100%'
height={containerHeight}
itemCount={flatItems.length}
itemSize={virtualItemSize}
itemData={virtualListData}
overscanCount={20}
>
{VirtualListRow}
</VirtualList>
) : (
<div className='pt-2' ref={staticListRef}>
{flatItems.map((flatItem, index) => (
<StaticListRow
key={`static-row-${index}`}
bookKey={bookKey}
flatItem={flatItem}
activeHref={activeHref}
onToggleExpand={handleToggleExpand}
onItemClick={handleItemClick}
/>
))}
</div>
)}
return flatItems.length > 256 ? (
<div className='rounded' data-overlayscrollbars-initialize='' ref={containerRef}>
<VirtualList
ref={vitualListRef}
outerRef={listOuterRef}
width='100%'
height={containerHeight}
itemCount={flatItems.length}
itemSize={virtualItemSize}
itemData={virtualListData}
overscanCount={20}
initialScrollOffset={
appService?.isAndroidApp && activeItemIndex >= 0
? Math.max(0, activeItemIndex * virtualItemSize - containerHeight / 2)
: undefined
}
>
{VirtualListRow}
</VirtualList>
</div>
) : (
<div className='rounded pt-2' ref={staticListRef}>
{flatItems.map((flatItem, index) => (
<StaticListRow
key={`static-row-${index}`}
bookKey={bookKey}
flatItem={flatItem}
activeHref={activeHref}
onToggleExpand={handleToggleExpand}
onItemClick={handleItemClick}
/>
))}
</div>
);
};
export default TOCView;
@@ -171,20 +171,23 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
return;
}
let ttsFromRange = range || progress.range;
if (viewSettings.ttsLocation) {
let ttsFromRange = range;
if (!ttsFromRange && viewSettings.ttsLocation) {
const { location } = progress;
const ttsCfi = viewSettings.ttsLocation;
const start = CFI.collapse(location);
const end = CFI.collapse(location, true);
if (CFI.compare(start, ttsCfi) * CFI.compare(end, ttsCfi) <= 0) {
const { index, anchor } = view.resolveCFI(ttsCfi);
const { doc } = view.renderer.getContents().find((x) => (x.index = index)) || {};
const { doc } = view.renderer.getContents().find((x) => x.index === index) || {};
if (doc) {
ttsFromRange = anchor(doc) || ttsFromRange;
ttsFromRange = anchor(doc);
}
}
}
if (!ttsFromRange) {
ttsFromRange = progress.range;
}
const primaryLang = bookData.book.primaryLanguage;
@@ -63,14 +63,14 @@ export const useTextSelector = (
if (!isTextSelected.current) return;
sel.addRange(range);
setSelection({ key: bookKey, text: await getAnnotationText(range), range, index });
}, 40);
}, 0);
}, 30);
}, 30);
};
const handleSelectionchange = (doc: Document, index: number) => {
// Available on iOS, Android and Desktop, fired when the selection is changed
// Ideally the popup only shows when the selection is done,
const sel = doc.getSelection() as Selection;
if (osPlatform === 'ios') return;
if (osPlatform === 'ios' || appService?.isIOSApp) return;
if (!isValidSelection(sel)) {
if (!isUpToPopup.current) {
handleDismissPopup();
@@ -91,12 +91,30 @@ export const useTextSelector = (
}
isUpToPopup.current = true;
};
const handlePointerup = (doc: Document, index: number) => {
const isPointerInsideSelection = (selection: Selection, ev: PointerEvent) => {
if (selection.rangeCount === 0) return false;
const range = selection.getRangeAt(0);
const rects = range.getClientRects();
const padding = 80;
for (let i = 0; i < rects.length; i++) {
const rect = rects[i]!;
if (
ev.clientX >= rect.left - padding &&
ev.clientX <= rect.right + padding &&
ev.clientY >= rect.top - padding &&
ev.clientY <= rect.bottom + padding
) {
return true;
}
}
return false;
};
const handlePointerup = (doc: Document, index: number, ev: PointerEvent) => {
// Available on iOS and Desktop, fired at touchend or mouseup
// Note that on Android, pointerup event is fired after an additional touch event
const sel = doc.getSelection() as Selection;
if (isValidSelection(sel)) {
if (osPlatform === 'ios') {
if (isValidSelection(sel) && isPointerInsideSelection(sel, ev)) {
if (osPlatform === 'ios' || appService?.isIOSApp) {
makeSelectionOnIOS(sel, index);
} else {
makeSelection(sel, index, true);
@@ -134,7 +152,7 @@ export const useTextSelector = (
}
isPopuped.current = showPopup;
},
['android', 'ios'].includes(osPlatform) ? 0 : 500,
['android', 'ios'].includes(osPlatform) || appService?.isIOSApp ? 0 : 500,
);
};
@@ -7,7 +7,7 @@ interface PlanActionButtonProps {
comingSoon?: boolean;
upgradable?: boolean;
onSubscribe: (priceId?: string) => void;
onPlanSwipe: (direction: 'left' | 'right') => void;
onSelectPlan: (index: number) => void;
}
const PlanActionButton: React.FC<PlanActionButtonProps> = ({
@@ -16,7 +16,7 @@ const PlanActionButton: React.FC<PlanActionButtonProps> = ({
comingSoon,
upgradable,
onSubscribe,
onPlanSwipe,
onSelectPlan,
}) => {
const _ = useTranslation();
@@ -44,7 +44,7 @@ const PlanActionButton: React.FC<PlanActionButtonProps> = ({
if (plan.plan === 'free' && isUserPlan) {
return (
<button
onClick={() => onPlanSwipe('left')}
onClick={() => onSelectPlan(1)}
className='w-full rounded-lg bg-blue-600 px-6 py-3 font-semibold text-white transition-colors hover:bg-blue-700'
>
{_('Upgrade to Plus or Pro')}
@@ -11,7 +11,6 @@ interface PlanCardProps {
index: number;
currentPlanIndex: number;
onSubscribe: (priceId?: string) => void;
onPlanSwipe: (direction: 'left' | 'right') => void;
onSelectPlan: (index: number) => void;
}
@@ -23,7 +22,6 @@ const PlanCard: React.FC<PlanCardProps> = ({
index,
currentPlanIndex,
onSubscribe,
onPlanSwipe,
onSelectPlan,
}) => {
const _ = useTranslation();
@@ -33,7 +31,6 @@ const PlanCard: React.FC<PlanCardProps> = ({
key={plan.plan}
className='w-full flex-shrink-0 p-6 sm:min-w-96 sm:max-w-96'
style={{ scrollSnapAlign: 'start' }}
onClick={() => onSelectPlan(index)}
>
<div
className={`rounded-xl border-2 p-4 ${plan.color} ${index === currentPlanIndex ? 'ring-2 ring-blue-500' : ''}`}
@@ -46,7 +43,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
</div>
</div>
<div className='mb-6 space-y-3'>
<div className='mb-6 space-y-3' onClick={() => onSelectPlan(index)}>
{plan.features.map((feature, featureIndex) => (
<div key={featureIndex} className='flex flex-col'>
<div className='flex items-center gap-2'>
@@ -62,7 +59,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
))}
</div>
<div className='mb-6 rounded-lg bg-white/50 p-4'>
<div className='mb-6 rounded-lg bg-white/50 p-4' onClick={() => onSelectPlan(index)}>
<h5 className='mb-3 font-semibold'>{_('Plan Limits')}</h5>
<div className='space-y-2'>
{Object.entries(plan.limits).map(([key, value]) => (
@@ -80,7 +77,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
upgradable={upgradable}
isUserPlan={isUserPlan}
onSubscribe={onSubscribe}
onPlanSwipe={onPlanSwipe}
onSelectPlan={onSelectPlan}
/>
</div>
</div>
@@ -3,19 +3,19 @@ import { PlanDetails } from '../utils/plan';
interface PlanIndicatorsProps {
allPlans: PlanDetails[];
currentPlanIndex: number;
onPlanSelect: (index: number) => void;
onSelectPlan: (index: number) => void;
}
const PlanIndicators: React.FC<PlanIndicatorsProps> = ({
allPlans,
currentPlanIndex,
onPlanSelect,
onSelectPlan,
}) => (
<div className='flex justify-center space-x-2 pb-6 pt-2'>
{allPlans.map((_, index) => (
<button
key={index}
onClick={() => onPlanSelect(index)}
onClick={() => onSelectPlan(index)}
className={`h-2 w-2 rounded-full transition-colors ${
index === currentPlanIndex ? 'bg-blue-500' : 'bg-gray-300'
}`}
@@ -1,44 +1,36 @@
import { IoChevronBack, IoChevronForward } from 'react-icons/io5';
import { useTranslation } from '@/hooks/useTranslation';
import { PlanDetails } from '../utils/plan';
interface PlanNavigationProps {
allPlans: PlanDetails[];
currentPlanIndex: number;
totalPlans: number;
onPlanSwipe: (direction: 'left' | 'right') => void;
onSelectPlan: (index: number) => void;
}
const PlanNavigation: React.FC<PlanNavigationProps> = ({
allPlans,
currentPlanIndex,
totalPlans,
onPlanSwipe,
onSelectPlan,
}) => {
const _ = useTranslation();
return (
<div className='border-b bg-gray-50 px-6 py-4'>
<div className='flex items-center justify-between'>
<h3 className='text-lg font-semibold'>{_('Available Plans')}</h3>
<div className='flex items-center space-x-2'>
<button
onClick={() => onPlanSwipe('right')}
disabled={currentPlanIndex === 0}
className='rounded-lg p-1 hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50'
>
<IoChevronBack className='h-5 w-5' />
</button>
<span className='text-sm text-gray-600'>
{_('{{current}} of {{all}}', {
current: currentPlanIndex + 1,
all: totalPlans,
})}
</span>
<button
onClick={() => onPlanSwipe('left')}
disabled={currentPlanIndex === totalPlans - 1}
className='rounded-lg p-1 hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50'
>
<IoChevronForward className='h-5 w-5' />
</button>
<div className='bg-base-200/50 border-base-200 border-b px-6 py-4'>
<div className='flex items-center justify-center'>
<div className='bg-base-300 flex items-center space-x-1 rounded-lg p-1'>
{allPlans.map((plan, index) => (
<button
key={plan.name}
onClick={() => onSelectPlan(index)}
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors ${
currentPlanIndex === index
? `${plan.color} shadow-sm`
: 'text-base-content hover:bg-base-200'
}`}
>
{_(plan.name)}
</button>
))}
</div>
</div>
</div>
@@ -104,9 +104,11 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
}, [currentPlanIndex, handleScroll]);
useEffect(() => {
console.log('Current plan index:', currentPlanIndex);
if (plansScrollRef.current) {
const planWidth = plansScrollRef.current.scrollWidth / allPlans.length;
const scrollPosition = currentPlanIndex * planWidth;
console.log('Scroll position:', scrollPosition);
plansScrollRef.current.scrollTo({
left: scrollPosition,
behavior: 'smooth',
@@ -120,19 +122,19 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
}));
return (
<div className='overflow-hidden rounded-xl border bg-white shadow-sm'>
<div className='bg-base-100 border-base-200 overflow-hidden rounded-xl border shadow-sm'>
<PlanNavigation
allPlans={allPlans}
currentPlanIndex={currentPlanIndex}
totalPlans={allPlans.length}
onPlanSwipe={handlePlanSwipe}
onSelectPlan={setCurrentPlanIndex}
/>
<div
ref={plansScrollRef}
className='scrollbar-hide flex items-center overflow-x-auto scroll-smooth'
className='scrollbar-hide flex items-start overflow-x-auto scroll-smooth'
onTouchStart={handleTouchStart}
style={{
scrollSnapType: 'x mandatory',
scrollSnapType: appService?.isAndroidApp ? 'none' : 'x mandatory',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
@@ -147,10 +149,7 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
index={index}
currentPlanIndex={currentPlanIndex}
onSubscribe={onSubscribe}
onPlanSwipe={handlePlanSwipe}
onSelectPlan={(selectedIndex) => {
setCurrentPlanIndex(selectedIndex);
}}
onSelectPlan={setCurrentPlanIndex}
/>
))}
</div>
@@ -158,7 +157,7 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
<PlanIndicators
allPlans={allPlans}
currentPlanIndex={currentPlanIndex}
onPlanSelect={setCurrentPlanIndex}
onSelectPlan={setCurrentPlanIndex}
/>
</div>
);
@@ -1,4 +1,5 @@
import { PiUserCircle } from 'react-icons/pi';
import { useTranslation } from '@/hooks/useTranslation';
import { PlanDetails } from '../utils/plan';
import UserAvatar from '@/components/UserAvatar';
@@ -9,34 +10,37 @@ interface UserInfoProps {
planDetails: PlanDetails;
}
const UserInfo: React.FC<UserInfoProps> = ({ avatarUrl, userFullName, userEmail, planDetails }) => (
<div className='flex flex-col items-center gap-x-6 gap-y-4 md:flex-row md:items-start'>
<div className='flex-shrink-0'>
{avatarUrl ? (
<UserAvatar
url={avatarUrl}
size={128}
DefaultIcon={PiUserCircle}
className='h-16 w-16 md:h-24 md:w-24'
borderClassName='border-base-100 border-4'
/>
) : (
<PiUserCircle className='h-16 w-16 md:h-24 md:w-24' />
)}
</div>
const UserInfo: React.FC<UserInfoProps> = ({ avatarUrl, userFullName, userEmail, planDetails }) => {
const _ = useTranslation();
return (
<div className='flex flex-col items-center gap-x-6 gap-y-4 md:flex-row md:items-start'>
<div className='flex-shrink-0'>
{avatarUrl ? (
<UserAvatar
url={avatarUrl}
size={window.innerWidth < 640 ? 64 : 128}
DefaultIcon={PiUserCircle}
className='h-16 w-16 md:h-24 md:w-24'
borderClassName='border-base-100 border-4'
/>
) : (
<PiUserCircle className='h-16 w-16 md:h-24 md:w-24' />
)}
</div>
<div className='flex-grow text-center md:text-left'>
<h2 className='text-base-content text-xl font-bold md:text-2xl'>{userFullName}</h2>
<p className='text-base-content/60'>{userEmail}</p>
<div className='mt-3'>
<span
className={`inline-block rounded-full px-3 py-1 text-sm font-medium ${planDetails.color}`}
>
{planDetails.name}
</span>
<div className='flex-grow text-center md:text-left'>
<h2 className='text-base-content text-xl font-bold md:text-2xl'>{userFullName}</h2>
<p className='text-base-content/60'>{userEmail}</p>
<div className='mt-3'>
<span
className={`inline-block rounded-full px-3 py-1 text-sm font-medium ${planDetails.color}`}
>
{_(planDetails.name)}
</span>
</div>
</div>
</div>
</div>
);
);
};
export default UserInfo;
@@ -40,7 +40,7 @@ const BookCover: React.FC<BookCoverProps> = ({
src={book.coverImageUrl!}
alt={book.title}
fill={true}
className={clsx('crop-cover-img object-cover shadow-md', imageClassName)}
className={clsx('crop-cover-img object-cover', imageClassName)}
onError={handleImageError}
/>
) : (
@@ -1,6 +1,11 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { MdDelete, MdCloudDownload, MdCloudUpload } from 'react-icons/md';
import {
MdOutlineDelete,
MdOutlineCloudDownload,
MdOutlineCloudUpload,
MdOutlineCloudOff,
} from 'react-icons/md';
import { Book } from '@/types/book';
import { BookDoc } from '@/libs/document';
@@ -28,6 +33,7 @@ interface BookDetailModalProps {
handleBookDownload?: (book: Book) => void;
handleBookUpload?: (book: Book) => void;
handleBookDelete?: (book: Book) => void;
handleBookDeleteCloudBackup?: (book: Book) => void;
}
const BookDetailModal = ({
@@ -37,10 +43,12 @@ const BookDetailModal = ({
handleBookDownload,
handleBookUpload,
handleBookDelete,
handleBookDeleteCloudBackup,
}: BookDetailModalProps) => {
const _ = useTranslation();
const [loading, setLoading] = useState(false);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [showDeleteCloudBackupAlert, setShowDeleteCloudBackupAlert] = useState(false);
const [bookMeta, setBookMeta] = useState<BookDoc['metadata'] | null>(null);
const [fileSize, setFileSize] = useState<number | null>(null);
const { envConfig } = useEnv();
@@ -73,6 +81,10 @@ const BookDetailModal = ({
setShowDeleteAlert(true);
};
const handleDeleteCloudBackup = () => {
setShowDeleteCloudBackupAlert(true);
};
const confirmDelete = async () => {
handleClose();
setShowDeleteAlert(false);
@@ -81,6 +93,14 @@ const BookDetailModal = ({
}
};
const confirmDeleteCloudBackup = async () => {
handleClose();
setShowDeleteCloudBackupAlert(false);
if (handleBookDeleteCloudBackup) {
handleBookDeleteCloudBackup(book);
}
};
const handleRedownload = async () => {
handleClose();
if (handleBookDownload) {
@@ -132,17 +152,22 @@ const BookDetailModal = ({
<div className='flex flex-wrap items-center gap-x-4'>
{handleBookDelete && (
<button onClick={handleDelete}>
<MdDelete className='fill-red-500' />
<MdOutlineDelete className='fill-red-500' />
</button>
)}
{book.uploadedAt && handleBookDeleteCloudBackup && (
<button onClick={handleDeleteCloudBackup}>
<MdOutlineCloudOff className='fill-red-500' />
</button>
)}
{book.uploadedAt && handleBookDownload && (
<button onClick={handleRedownload}>
<MdCloudDownload className='fill-base-content' />
<MdOutlineCloudDownload className='fill-base-content' />
</button>
)}
{book.downloadedAt && handleBookUpload && (
<button onClick={handleReupload}>
<MdCloudUpload className='fill-base-content' />
<MdOutlineCloudUpload className='fill-base-content' />
</button>
)}
</div>
@@ -196,9 +221,12 @@ const BookDetailModal = ({
</div>
<div>
<span className='font-bold'>{_('Description:')}</span>
<p className='text-neutral-content text-sm prose prose-sm'
dangerouslySetInnerHTML={{ __html: bookMeta.description || _('No description available') }}>
</p>
<p
className='text-neutral-content prose prose-sm text-sm'
dangerouslySetInnerHTML={{
__html: bookMeta.description || _('No description available'),
}}
></p>
</div>
</div>
</div>
@@ -221,6 +249,23 @@ const BookDetailModal = ({
/>
</div>
)}
{showDeleteCloudBackupAlert && (
<div
className={clsx(
'fixed bottom-0 left-0 right-0 z-50 flex justify-center',
'pb-[calc(env(safe-area-inset-bottom)+16px)]',
)}
>
<Alert
title={_('Confirm Deletion')}
message={_('Are you sure to delete the cloud backup of the selected book?')}
onCancel={() => {
setShowDeleteAlert(false);
}}
onConfirm={confirmDeleteCloudBackup}
/>
</div>
)}
</div>
);
};
+8 -8
View File
@@ -115,26 +115,26 @@ const Popup = ({
trianglePosition?.dir === 'right'
? 'none'
: trianglePosition?.dir === 'left'
? `6px solid`
: '6px solid transparent',
? `7px solid`
: '7px solid transparent',
borderRight:
trianglePosition?.dir === 'left'
? 'none'
: trianglePosition?.dir === 'right'
? `6px solid`
: '6px solid transparent',
? `7px solid`
: '7px solid transparent',
borderTop:
trianglePosition?.dir === 'down'
? 'none'
: trianglePosition?.dir === 'up'
? `6px solid`
: '6px solid transparent',
? `7px solid`
: '7px solid transparent',
borderBottom:
trianglePosition?.dir === 'up'
? 'none'
: trianglePosition?.dir === 'down'
? `6px solid`
: '6px solid transparent',
? `7px solid`
: '7px solid transparent',
transform:
trianglePosition?.dir === 'left' || trianglePosition?.dir === 'right'
? 'translateY(-50%)'
@@ -103,7 +103,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
setUpdate({
currentVersion,
version: data.version,
date: data.date,
date: data.pub_date,
body: data.notes,
downloadAndInstall: async (onEvent) => {
await new Promise<void>(async (resolve, reject) => {
@@ -175,7 +175,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
.sort(([a], [b]) => semver.rcompare(a, b))
.map(([version, info]) => ({
version,
date: new Date(info.date).toDateString(),
date: new Date(info.date).toLocaleDateString(),
notes: info.notes,
}));
@@ -198,7 +198,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
changelogs = [
{
version: update.version,
date: new Date(update.date!).toDateString(),
date: new Date(update.date!).toLocaleDateString(),
notes: parseNumberedList(update.body ?? ''),
},
];
+16 -10
View File
@@ -63,6 +63,7 @@ export abstract class BaseAppService implements AppService {
hasSafeAreaInset = false;
hasHaptics = false;
hasUpdater = false;
hasOrientationLock = false;
distChannel = 'readest';
abstract fs: FileSystem;
@@ -271,16 +272,18 @@ export abstract class BaseAppService implements AppService {
}
}
async deleteBook(book: Book, includingUploaded = false): Promise<void> {
const fps = [getRemoteBookFilename(book), getCoverFilename(book)];
const localDeleteFps = [getLocalBookFilename(book), getCoverFilename(book)];
for (const fp of localDeleteFps) {
if (await this.fs.exists(fp, 'Books')) {
await this.fs.removeFile(fp, 'Books');
async deleteBook(book: Book, includingUploaded = false, includingLocal = true): Promise<void> {
if (includingLocal) {
const localDeleteFps = [getLocalBookFilename(book), getCoverFilename(book)];
for (const fp of localDeleteFps) {
if (await this.fs.exists(fp, 'Books')) {
await this.fs.removeFile(fp, 'Books');
}
}
}
for (const fp of fps) {
if (includingUploaded) {
if (includingUploaded) {
const fps = [getRemoteBookFilename(book), getCoverFilename(book)];
for (const fp of fps) {
console.log('Deleting uploaded file:', fp);
const cfp = `${CLOUD_BOOKS_SUBDIR}/${fp}`;
try {
@@ -290,8 +293,11 @@ export abstract class BaseAppService implements AppService {
}
}
}
book.deletedAt = Date.now();
book.downloadedAt = null;
if (includingLocal) {
book.deletedAt = Date.now();
book.downloadedAt = null;
}
if (includingUploaded) {
book.uploadedAt = null;
}
@@ -646,6 +646,7 @@ export const TRANSLATED_LANGS = {
hi: 'हिन्दी',
id: 'Indonesia',
vi: 'Tiếng Việt',
th: 'ภาษาไทย',
'zh-CN': '简体中文',
'zh-TW': '正體中文',
};
@@ -19,7 +19,7 @@ import { type as osType } from '@tauri-apps/plugin-os';
import { Book } from '@/types/book';
import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { isContentURI, isValidURL } from '@/utils/misc';
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
import { getCoverFilename, getFilename } from '@/utils/book';
import { copyURIToPath } from '@/utils/bridge';
import { NativeFile, RemoteFile } from '@/utils/file';
@@ -227,6 +227,9 @@ export class NativeAppService extends BaseAppService {
OS_TYPE !== 'ios' &&
!process.env['NEXT_PUBLIC_DISABLE_UPDATER'] &&
!window.__READEST_UPDATER_DISABLED;
// orientation lock is not supported on iPad
override hasOrientationLock =
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
override distChannel = process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest';
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
+2 -1
View File
@@ -34,6 +34,7 @@ export interface AppService {
hasSafeAreaInset: boolean;
hasHaptics: boolean;
hasUpdater: boolean;
hasOrientationLock: boolean;
isMobile: boolean;
isAppDataSandbox: boolean;
isMobileApp: boolean;
@@ -56,7 +57,7 @@ export interface AppService {
overwrite?: boolean,
transient?: boolean,
): Promise<Book | null>;
deleteBook(book: Book, includingUploaded?: boolean): Promise<void>;
deleteBook(book: Book, includingUploaded?: boolean, includingLocal?: boolean): Promise<void>;
uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void>;
downloadBook(book: Book, onlyCover?: boolean, onProgress?: ProgressHandler): Promise<void>;
isBookAvailable(book: Book): Promise<boolean>;
+1
View File
@@ -25,6 +25,7 @@ export const normalizeToFullLang = (langCode: string): string => {
id: 'id-ID',
ru: 'ru-RU',
uk: 'uk-UA',
th: 'th-TH',
zh: 'zh-Hans',
'zh-cn': 'zh-Hans',
'zh-tw': 'zh-Hant',
+8
View File
@@ -230,6 +230,10 @@ const getLayoutStyles = (
p:has(> a:first-child + img:last-child) {
text-indent: unset !important;
}
blockquote[align="center"], div[align="center"],
p[align="center"], dd[align="center"] {
text-indent: unset !important;
}
p {
${vertical ? `margin-left: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${vertical ? `margin-right: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
@@ -294,6 +298,10 @@ const getLayoutStyles = (
width: auto;
height: auto;
}
.duokan-footnote img {
width: 0.8em;
height: 0.8em;
}
/* workaround for some badly designed epubs */
div.left *, p.left * { text-align: left; }