forked from akai/readest
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc3cc58de0 | |||
| 80d60266c2 | |||
| f06cdb5899 | |||
| dfe68988aa | |||
| d0264de3eb | |||
| 77c4303938 | |||
| 5724d2eeec | |||
| 64007b1188 | |||
| 50ee7bd8be | |||
| 2bcbde4bf1 | |||
| 99f75dd3b6 | |||
| 2b9132b4a8 | |||
| 82782be16e | |||
| 7067129887 | |||
| 6e706ac8f7 | |||
| 39b902c927 | |||
| 94a9cd7c9d | |||
| 0dae7db140 | |||
| 32954e025c | |||
| 61dda9a517 | |||
| 99a4cb50ac | |||
| f4b00d6dec | |||
| f69422a61c | |||
| 7f4c157a90 | |||
| 7d9724ff8b | |||
| 06db7f36f8 |
@@ -210,11 +210,6 @@ jobs:
|
||||
pnpm tauri icon ../../data/icons/readest-book.png
|
||||
git checkout .
|
||||
|
||||
MANIFEST="src-tauri/gen/android/app/src/main/AndroidManifest.xml"
|
||||
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST" || \
|
||||
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
|
||||
|
||||
pushd src-tauri/gen/android
|
||||
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
|
||||
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
[![Web App][badge-web-app]][link-web-readest]
|
||||
[![OS][badge-platforms]][link-website]
|
||||
<br>
|
||||
[![][badge-hellogithub]][link-hellogithub]
|
||||
[![][badge-discord]][link-discord]
|
||||
[![Discord][badge-discord]][link-discord]
|
||||
[![Reddit][badge-reddit]][link-reddit]
|
||||
[![AGPL Licence][badge-license]](LICENSE)
|
||||
[![Latest release][badge-release]][link-gh-releases]
|
||||
[![Language Coverage][badge-language-coverage]]()
|
||||
[![Donate][badge-donate]][link-donate]
|
||||
<br>
|
||||
[![Latest release][badge-release]][link-gh-releases]
|
||||
[![Last commit][badge-last-commit]][link-gh-commits]
|
||||
[![Commits][badge-commit-activity]][link-gh-pulse]
|
||||
[![][badge-hellogithub]][link-hellogithub]
|
||||
[![Ask DeepWiki][badge-deepwiki]][link-deepwiki]
|
||||
|
||||
</div>
|
||||
@@ -67,16 +68,16 @@
|
||||
<div align="left">🛠 Building</div>
|
||||
<div align="left">🔄 Planned</div>
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
|
||||
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
|
||||
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
|
||||
Stay tuned for continuous improvements and updates! Contributions and suggestions are always welcome—let's build the ultimate reading experience together. 😊
|
||||
|
||||
@@ -109,9 +110,9 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||
### 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].
|
||||
- Web: Visit [https://web.readest.com][link-web-readest].
|
||||
- macOS / iOS / iPadOS : Search and install **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 and download **Readest** at [https://readest.com][link-website] or the [Releases on GitHub][link-gh-releases].
|
||||
- Web: Visit and use **Readest for Web** at [https://web.readest.com][link-web-readest].
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -303,12 +304,14 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[badge-license]: https://img.shields.io/github/license/readest/readest?color=teal
|
||||
[badge-release]: https://img.shields.io/github/release/readest/readest?color=green
|
||||
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%2C%20Windows%2C%20Linux%2C%20Android%2C%20iOS%2C%20Web%2C%20PWA-green
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=green
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=blue
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest?color=blue
|
||||
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
|
||||
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
|
||||
[badge-donate]: https://donate.readest.com/badge.svg
|
||||
[badge-deepwiki]: https://deepwiki.com/badge.svg
|
||||
[badge-reddit]: https://img.shields.io/reddit/subreddit-subscribers/readest?style=flat&logo=reddit&color=F37E41
|
||||
[badge-language-coverage]: https://img.shields.io/badge/coverage-52%25%20population%20🌍-green
|
||||
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
|
||||
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
|
||||
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
|
||||
@@ -322,3 +325,5 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[link-koreader]: https://github.com/koreader/koreader
|
||||
[link-hellogithub]: https://hellogithub.com/repository/8a5b6ade2aee461a8bd94e59200682a7
|
||||
[link-deepwiki]: https://deepwiki.com/readest/readest
|
||||
[link-kosync-wiki]: https://github.com/readest/readest/wiki/Sync-with-Koreader-devices
|
||||
[link-reddit]: https://reddit.com/r/readest/
|
||||
|
||||
@@ -28,6 +28,9 @@ module.exports = {
|
||||
'ar',
|
||||
'th',
|
||||
'bo',
|
||||
'bn',
|
||||
'ta',
|
||||
'si',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.76",
|
||||
"version": "0.9.78",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
|
||||
@@ -494,7 +494,6 @@
|
||||
"Always use latest": "استخدام الأحدث دائمًا",
|
||||
"Send changes only": "إرسال التغييرات فقط",
|
||||
"Receive changes only": "استلام التغييرات فقط",
|
||||
"Disabled": "معطل",
|
||||
"Checksum Method": "طريقة التحقق من الصحة",
|
||||
"File Content (recommended)": "محتوى الملف (موصى به)",
|
||||
"File Name": "اسم الملف",
|
||||
@@ -532,5 +531,12 @@
|
||||
"Select Fonts": "حدد الخطوط",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "تنسيقات الخط المدعومة: .ttf، .otf، .woff، .woff2",
|
||||
"Push Progress": "تقدم الدفع",
|
||||
"Pull Progress": "تقدم السحب"
|
||||
"Pull Progress": "تقدم السحب",
|
||||
"Previous Paragraph": "الفقرة السابقة",
|
||||
"Previous Sentence": "الجملة السابقة",
|
||||
"Pause": "إيقاف مؤقت",
|
||||
"Play": "تشغيل",
|
||||
"Next Sentence": "الجملة التالية",
|
||||
"Next Paragraph": "الفقرة التالية",
|
||||
"Separate Cover Page": "صفحة غلاف منفصلة"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
{
|
||||
"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...": "সাইন আপ করা হচ্ছে...",
|
||||
"Don't have an account? Sign 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": "আপনার ওটিপি টোকেন",
|
||||
"Verify token": "টোকেন যাচাই করুন",
|
||||
"Sign in with Google": "গুগল দিয়ে সাইন ইন",
|
||||
"Sign in with Apple": "অ্যাপল দিয়ে সাইন ইন",
|
||||
"Sign in with 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)?_one": "আপনি কি নিশ্চিত যে নির্বাচিত বইটি মুছবেন?",
|
||||
"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)..._one": "বইটিতে খুঁজুন...",
|
||||
"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": "শুরুতে আপডেট চেক করুন",
|
||||
"Open Book in New Window": "নতুন উইন্ডোতে বই খুলুন",
|
||||
"Fullscreen": "পূর্ণ স্ক্রিন",
|
||||
"Always on Top": "সবসময় উপরে",
|
||||
"Always Show Status Bar": "সবসময় স্ট্যাটাস বার দেখান",
|
||||
"Keep Screen Awake": "স্ক্রিন জাগিয়ে রাখুন",
|
||||
"Reload Page": "পৃষ্ঠা রিলোড করুন",
|
||||
"Dark Mode": "ডার্ক মোড",
|
||||
"Light Mode": "লাইট মোড",
|
||||
"Auto Mode": "অটো মোড",
|
||||
"Upgrade to Readest Premium": "রিডেস্ট প্রিমিয়ামে আপগ্রেড",
|
||||
"Download Readest": "রিডেস্ট ডাউনলোড",
|
||||
"About Readest": "রিডেস্ট সম্পর্কে",
|
||||
"Help improve 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}}",
|
||||
"Deleted cloud backup of the book: {{title}}": "বইয়ের ক্লাউড ব্যাকআপ মুছে ফেলা হয়েছে: {{title}}",
|
||||
"Deleted local copy of the book: {{title}}": "বইয়ের স্থানীয় কপি মুছে ফেলা হয়েছে: {{title}}",
|
||||
"Failed to delete book: {{title}}": "বই মুছতে ব্যর্থ: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "বইয়ের ক্লাউড ব্যাকআপ মুছতে ব্যর্থ: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "বইয়ের স্থানীয় কপি মুছতে ব্যর্থ: {{title}}",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "আপনার লাইব্রেরিতে স্বাগতম। আপনি এখানে বই আমদানি করে যেকোনো সময় পড়তে পারেন।",
|
||||
"Copied to notebook": "নোটবুকে কপি করা হয়েছে",
|
||||
"No annotations to export": "রপ্তানি করার জন্য কোন টীকা নেই",
|
||||
"Exported from 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": "অনূদিত টেক্সট",
|
||||
"System Language": "সিস্টেম ভাষা",
|
||||
"Loading...": "লোড হচ্ছে...",
|
||||
"No translation available.": "কোন অনুবাদ উপলব্ধ নয়।",
|
||||
"Translated by {{provider}}.": "{{provider}} দ্বারা অনূদিত।",
|
||||
"Bookmark": "বুকমার্ক",
|
||||
"Next Section": "পরবর্তী বিভাগ",
|
||||
"Previous Section": "পূর্ববর্তী বিভাগ",
|
||||
"Next Page": "পরবর্তী পৃষ্ঠা",
|
||||
"Previous Page": "পূর্ববর্তী পৃষ্ঠা",
|
||||
"Go Forward": "এগিয়ে যান",
|
||||
"Small": "ছোট",
|
||||
"Large": "বড়",
|
||||
"Sync Conflict": "সিঙ্ক দ্বন্দ্ব",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "\"{{deviceName}}\" থেকে পড়ার অগ্রগতি সিঙ্ক করবেন?",
|
||||
"another device": "অন্য ডিভাইস",
|
||||
"Local Progress": "স্থানীয় অগ্রগতি",
|
||||
"Remote Progress": "দূরবর্তী অগ্রগতি",
|
||||
"Failed to connect": "সংযোগ ব্যর্থ",
|
||||
"Disconnected": "সংযোগ বিচ্ছিন্ন",
|
||||
"KOReader Sync Settings": "KOReader সিঙ্ক সেটিংস",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}} হিসেবে সিঙ্ক",
|
||||
"Sync Server Connected": "সিঙ্ক সার্ভার সংযুক্ত",
|
||||
"Sync Strategy": "সিঙ্ক কৌশল",
|
||||
"Ask on conflict": "দ্বন্দ্বে জিজ্ঞাসা করুন",
|
||||
"Always use latest": "সবসময় সর্বশেষ ব্যবহার করুন",
|
||||
"Send changes only": "শুধু পরিবর্তন পাঠান",
|
||||
"Receive changes only": "শুধু পরিবর্তন গ্রহণ করুন",
|
||||
"Checksum Method": "চেকসাম পদ্ধতি",
|
||||
"File Content (recommended)": "ফাইল কন্টেন্ট (সুপারিশকৃত)",
|
||||
"File Name": "ফাইলের নাম",
|
||||
"Device Name": "ডিভাইসের নাম",
|
||||
"Connect to your KOReader Sync server.": "আপনার KOReader সিঙ্ক সার্ভারে সংযোগ করুন।",
|
||||
"Server URL": "সার্ভার URL",
|
||||
"Username": "ইউজারনেম",
|
||||
"Your Username": "আপনার ইউজারনেম",
|
||||
"Password": "পাসওয়ার্ড",
|
||||
"Connect": "সংযোগ",
|
||||
"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_one": "অধ্যায়ে ১ পৃষ্ঠা বাকি",
|
||||
"{{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": "কোড ভাষা",
|
||||
"Auto": "অটো",
|
||||
"Scroll": "স্ক্রল",
|
||||
"Scrolled Mode": "স্ক্রল মোড",
|
||||
"Continuous Scroll": "ক্রমাগত স্ক্রল",
|
||||
"Overlap Pixels": "ওভারল্যাপ পিক্সেল",
|
||||
"Click": "ক্লিক",
|
||||
"Clicks for Page Flip": "পৃষ্ঠা উল্টানোর জন্য ক্লিক",
|
||||
"Swap Clicks Area": "ক্লিক এরিয়া অদলবদল",
|
||||
"Disable Double Click": "ডাবল ক্লিক নিষ্ক্রিয়",
|
||||
"Volume Keys for Page Flip": "পৃষ্ঠা উল্টানোর জন্য ভলিউম কী",
|
||||
"Animation": "অ্যানিমেশন",
|
||||
"Paging Animation": "পৃষ্ঠা অ্যানিমেশন",
|
||||
"Security": "নিরাপত্তা",
|
||||
"Allow JavaScript": "জাভাস্ক্রিপ্ট অনুমতি",
|
||||
"Enable only if you trust the file.": "শুধুমাত্র ফাইলে বিশ্বাস থাকলে সক্রিয় করুন।",
|
||||
"Font": "ফন্ট",
|
||||
"Custom Fonts": "কাস্টম ফন্ট",
|
||||
"Cancel Delete": "মুছা বাতিল",
|
||||
"Delete Font": "ফন্ট মুছুন",
|
||||
"Import Font": "ফন্ট আমদানি",
|
||||
"Tips": "টিপস",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "সমর্থিত ফন্ট ফরম্যাট: .ttf, .otf, .woff, .woff2",
|
||||
"Custom fonts can be selected from the Font Face menu": "কাস্টম ফন্ট Font Face মেনু থেকে নির্বাচন করা যায়",
|
||||
"Global Settings": "গ্লোবাল সেটিংস",
|
||||
"Apply to All Books": "সব বইয়ে প্রয়োগ",
|
||||
"Apply to This Book": "এই বইয়ে প্রয়োগ",
|
||||
"Reset Settings": "সেটিংস রিসেট",
|
||||
"Manage Custom Fonts": "কাস্টম ফন্ট ব্যবস্থাপনা",
|
||||
"System Fonts": "সিস্টেম ফন্ট",
|
||||
"Serif Font": "সেরিফ ফন্ট",
|
||||
"Sans-Serif Font": "স্যান্স-সেরিফ ফন্ট",
|
||||
"Override Book Font": "বইয়ের ফন্ট পরিবর্তন",
|
||||
"Font Size": "ফন্ট সাইজ",
|
||||
"Default Font Size": "ডিফল্ট ফন্ট সাইজ",
|
||||
"Minimum Font Size": "সর্বনিম্ন ফন্ট সাইজ",
|
||||
"Font Weight": "ফন্ট ওয়েট",
|
||||
"Font Family": "ফন্ট ফ্যামিলি",
|
||||
"Default Font": "ডিফল্ট ফন্ট",
|
||||
"CJK Font": "CJK ফন্ট",
|
||||
"Font Face": "ফন্ট ফেস",
|
||||
"Monospace Font": "মনোস্পেস ফন্ট",
|
||||
"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 Reading Progress": "পড়ার অগ্রগতি দেখান",
|
||||
"Reading Progress Style": "পড়ার অগ্রগতির স্টাইল",
|
||||
"Page Number": "পৃষ্ঠা নম্বর",
|
||||
"Percentage": "শতাংশ",
|
||||
"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": "কাস্টম রিডার UI CSS",
|
||||
"Enter CSS for reader interface styling...": "রিডার ইন্টারফেস স্টাইলিংয়ের জন্য CSS লিখুন...",
|
||||
"Layout": "লেআউট",
|
||||
"Color": "রঙ",
|
||||
"Behavior": "আচরণ",
|
||||
"Reset {{settings}}": "{{settings}} রিসেট",
|
||||
"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— উইলিয়াম শেক্সপিয়র",
|
||||
"(from 'As You Like It', Act II)": "('অ্যাজ ইউ লাইক ইট', দ্বিতীয় অঙ্ক থেকে)",
|
||||
"Custom Theme": "কাস্টম থিম",
|
||||
"Theme Name": "থিমের নাম",
|
||||
"Text Color": "টেক্সট রঙ",
|
||||
"Background Color": "ব্যাকগ্রাউন্ড রঙ",
|
||||
"Link Color": "লিঙ্ক রঙ",
|
||||
"Preview": "প্রিভিউ",
|
||||
"Font & Layout": "ফন্ট ও লেআউট",
|
||||
"Book Cover": "বইয়ের মলাট",
|
||||
"More Info": "আরো তথ্য",
|
||||
"Parallel Read": "সমান্তরাল পড়া",
|
||||
"Disable": "নিষ্ক্রিয়",
|
||||
"Enable": "সক্রিয়",
|
||||
"KOReader Sync": "KOReader সিঙ্ক",
|
||||
"Push Progress": "অগ্রগতি পুশ",
|
||||
"Pull Progress": "অগ্রগতি পুল",
|
||||
"Export Annotations": "টীকা রপ্তানি",
|
||||
"Sort TOC by Page": "পৃষ্ঠা অনুযায়ী TOC সাজান",
|
||||
"Edit": "সম্পাদনা",
|
||||
"Search...": "খুঁজুন...",
|
||||
"Book": "বই",
|
||||
"Chapter": "অধ্যায়",
|
||||
"Match Case": "কেস মিলান",
|
||||
"Match Whole Words": "পূর্ণ শব্দ মিলান",
|
||||
"Match Diacritics": "ডায়াক্রিটিক্স মিলান",
|
||||
"TOC": "সূচিপত্র",
|
||||
"Sidebar": "সাইডবার",
|
||||
"Disable Translation": "অনুবাদ নিষ্ক্রিয়",
|
||||
"Translation Not Available": "অনুবাদ উপলব্ধ নয়",
|
||||
"TTS not supported for PDF": "PDF এর জন্য TTS সমর্থিত নয়",
|
||||
"TTS not supported for this document": "এই ডকুমেন্টের জন্য TTS সমর্থিত নয়",
|
||||
"No Timeout": "কোন টাইমআউট নেই",
|
||||
"{{value}} minute": "{{value}} মিনিট",
|
||||
"{{value}} minutes": "{{value}} মিনিট",
|
||||
"{{value}} hour": "{{value}} ঘন্টা",
|
||||
"{{value}} hours": "{{value}} ঘন্টা",
|
||||
"Voices for {{lang}}": "{{lang}} এর জন্য কণ্ঠস্বর",
|
||||
"Slow": "ধীর",
|
||||
"Fast": "দ্রুত",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: ১টি কণ্ঠস্বর",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}}টি কণ্ঠস্বর",
|
||||
"Sign in to Sync": "সিঙ্কের জন্য সাইন ইন",
|
||||
"Synced at {{time}}": "{{time}} এ সিঙ্ক হয়েছে",
|
||||
"Never synced": "কখনো সিঙ্ক হয়নি",
|
||||
"Reading Progress Synced": "পড়ার অগ্রগতি সিঙ্ক হয়েছে",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "{{total}} এর {{page}} পৃষ্ঠা ({{percentage}}%)",
|
||||
"Current position": "বর্তমান অবস্থান",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "প্রায় {{total}} এর {{page}} পৃষ্ঠা ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "প্রায় {{percentage}}%",
|
||||
"Delete Your Account?": "আপনার অ্যাকাউন্ট মুছবেন?",
|
||||
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "এই কাজ পূর্বাবস্থায় ফেরানো যাবে না। ক্লাউডে আপনার সব ডেটা স্থায়ীভাবে মুছে যাবে।",
|
||||
"Delete Permanently": "স্থায়ীভাবে মুছুন",
|
||||
"Restore Purchase": "ক্রয় পুনরুদ্ধার",
|
||||
"Manage Subscription": "সাবস্ক্রিপশন পরিচালনা",
|
||||
"Reset Password": "পাসওয়ার্ড রিসেট",
|
||||
"Sign Out": "সাইন আউট",
|
||||
"Delete Account": "অ্যাকাউন্ট মুছুন",
|
||||
"Upgrade to {{plan}}": "{{plan}} এ আপগ্রেড",
|
||||
"Complete Your Subscription": "আপনার সাবস্ক্রিপশন সম্পূর্ণ করুন",
|
||||
"Coming Soon": "শীঘ্রই আসছে",
|
||||
"Upgrade to Plus or Pro": "প্লাস বা প্রো তে আপগ্রেড",
|
||||
"Current Plan": "বর্তমান প্ল্যান",
|
||||
"Plan Limits": "প্ল্যান সীমা",
|
||||
"Failed to delete user. Please try again later.": "ইউজার মুছতে ব্যর্থ। পরে আবার চেষ্টা করুন।",
|
||||
"Failed to create checkout session": "চেকআউট সেশন তৈরি করতে ব্যর্থ",
|
||||
"No purchases found to restore.": "পুনরুদ্ধার করার জন্য কোন ক্রয় পাওয়া যায়নি।",
|
||||
"Failed to restore purchases.": "ক্রয় পুনরুদ্ধার করতে ব্যর্থ।",
|
||||
"Failed to manage subscription.": "সাবস্ক্রিপশন পরিচালনা করতে ব্যর্থ।",
|
||||
"Failed to load subscription plans.": "সাবস্ক্রিপশন প্ল্যান লোড করতে ব্যর্থ।",
|
||||
"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:": "সাবস্ক্রিপশন ID:",
|
||||
"Go to Library": "লাইব্রেরিতে যান",
|
||||
"Need help? Contact our support team at support@readest.com": "সাহায্য দরকার? support@readest.com এ আমাদের সাপোর্ট টিমের সাথে যোগাযোগ করুন",
|
||||
"Free Plan": "ফ্রি প্ল্যান",
|
||||
"month": "মাস",
|
||||
"year": "বছর",
|
||||
"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": "প্লাস প্ল্যান",
|
||||
"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.": "উপলব্ধ সবচেয়ে নির্ভুল অনুবাদ ইঞ্জিন দিয়ে দৈনিক ১,০০,০০০ অক্ষর পর্যন্ত অনুবাদ করুন।",
|
||||
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "৫ GB ক্লাউড স্টোরেজ দিয়ে আপনার সম্পূর্ণ পড়ার সংগ্রহ নিরাপদে সংরক্ষণ এবং অ্যাক্সেস করুন।",
|
||||
"Priority Support": "অগ্রাধিকার সাপোর্ট",
|
||||
"Enjoy faster responses and dedicated assistance whenever you need help.": "যখনই সাহায্যের প্রয়োজন হয় দ্রুত প্রতিক্রিয়া এবং নিবেদিত সহায়তা উপভোগ করুন।",
|
||||
"Pro Plan": "প্রো প্ল্যান",
|
||||
"Includes All Plus Plan Benefits": "সব প্লাস প্ল্যান সুবিধা অন্তর্ভুক্ত",
|
||||
"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.": "উপলব্ধ সবচেয়ে নির্ভুল অনুবাদ ইঞ্জিন দিয়ে দৈনিক ৫,০০,০০০ অক্ষর পর্যন্ত অনুবাদ করুন।",
|
||||
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "২০ GB ক্লাউড স্টোরেজ দিয়ে আপনার সম্পূর্ণ পড়ার সংগ্রহ নিরাপদে সংরক্ষণ এবং অ্যাক্সেস করুন।",
|
||||
"Version {{version}}": "সংস্করণ {{version}}",
|
||||
"Check Update": "আপডেট চেক করুন",
|
||||
"Already the latest version": "ইতিমধ্যে সর্বশেষ সংস্করণ",
|
||||
"Checking for updates...": "আপডেট চেক করা হচ্ছে...",
|
||||
"Error checking for updates": "আপডেট চেক করতে ত্রুটি",
|
||||
"Drop to Import Books": "বই আমদানি করতে ড্রপ করুন",
|
||||
"Terms of Service": "সেবার শর্তাবলী",
|
||||
"Privacy Policy": "গোপনীয়তা নীতি",
|
||||
"Enter book title": "বইয়ের শিরোনাম লিখুন",
|
||||
"Subtitle": "উপশিরোনাম",
|
||||
"Enter book subtitle": "বইয়ের উপশিরোনাম লিখুন",
|
||||
"Enter author name": "লেখকের নাম লিখুন",
|
||||
"Series": "সিরিজ",
|
||||
"Enter series name": "সিরিজের নাম লিখুন",
|
||||
"Series Index": "সিরিজ ইনডেক্স",
|
||||
"Enter series index": "সিরিজ ইনডেক্স লিখুন",
|
||||
"Total in Series": "সিরিজের মোট",
|
||||
"Enter total books in series": "সিরিজের মোট বই সংখ্যা লিখুন",
|
||||
"Publisher": "প্রকাশক",
|
||||
"Enter publisher": "প্রকাশক লিখুন",
|
||||
"Publication Date": "প্রকাশনার তারিখ",
|
||||
"YYYY or YYYY-MM-DD": "YYYY বা YYYY-MM-DD",
|
||||
"Identifier": "শনাক্তকারী",
|
||||
"Subjects": "বিষয়",
|
||||
"Fiction, Science, History": "কল্পকাহিনী, বিজ্ঞান, ইতিহাস",
|
||||
"Description": "বিবরণ",
|
||||
"Enter book description": "বইয়ের বিবরণ লিখুন",
|
||||
"Change cover image": "মলাট ছবি পরিবর্তন",
|
||||
"Replace": "প্রতিস্থাপন",
|
||||
"Unlock cover": "মলাট আনলক",
|
||||
"Lock cover": "মলাট লক",
|
||||
"Auto-Retrieve Metadata": "মেটাডেটা স্বয়ংক্রিয় পুনরুদ্ধার",
|
||||
"Auto-Retrieve": "স্বয়ংক্রিয় পুনরুদ্ধার",
|
||||
"Unlock all fields": "সব ফিল্ড আনলক",
|
||||
"Unlock All": "সব আনলক",
|
||||
"Lock all fields": "সব ফিল্ড লক",
|
||||
"Lock All": "সব লক",
|
||||
"Reset": "রিসেট",
|
||||
"Are you sure to delete the selected book?": "আপনি কি নিশ্চিত যে নির্বাচিত বইটি মুছবেন?",
|
||||
"Are you sure to delete the cloud backup of the selected book?": "আপনি কি নিশ্চিত যে নির্বাচিত বইয়ের ক্লাউড ব্যাকআপ মুছবেন?",
|
||||
"Are you sure to delete the local copy of the selected book?": "আপনি কি নিশ্চিত যে নির্বাচিত বইয়ের স্থানীয় কপি মুছবেন?",
|
||||
"Edit Metadata": "মেটাডেটা সম্পাদনা",
|
||||
"Book Details": "বইয়ের বিস্তারিত",
|
||||
"Unknown": "অজানা",
|
||||
"Remove from Cloud & Device": "ক্লাউড ও ডিভাইস থেকে সরান",
|
||||
"Remove from Cloud Only": "শুধু ক্লাউড থেকে সরান",
|
||||
"Remove from Device Only": "শুধু ডিভাইস থেকে সরান",
|
||||
"Published": "প্রকাশিত",
|
||||
"Updated": "আপডেট হয়েছে",
|
||||
"Added": "যোগ করা হয়েছে",
|
||||
"File Size": "ফাইল সাইজ",
|
||||
"No description available": "কোন বিবরণ উপলব্ধ নয়",
|
||||
"Locked": "লক করা",
|
||||
"Select Metadata Source": "মেটাডেটা সোর্স নির্বাচন",
|
||||
"Keep manual input": "ম্যানুয়াল ইনপুট রাখুন",
|
||||
"A new version of Readest is available!": "রিডেস্টের একটি নতুন সংস্করণ উপলব্ধ!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "রিডেস্ট {{newVersion}} উপলব্ধ (ইনস্টল করা সংস্করণ {{currentVersion}})।",
|
||||
"Download and install now?": "এখনই ডাউনলোড এবং ইনস্টল করবেন?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "{{contentLength}} এর {{downloaded}} ডাউনলোড হচ্ছে",
|
||||
"Download finished": "ডাউনলোড সম্পূর্ণ",
|
||||
"DOWNLOAD & INSTALL": "ডাউনলোড ও ইনস্টল",
|
||||
"Changelog": "পরিবর্তন লগ",
|
||||
"Software Update": "সফটওয়্যার আপডেট",
|
||||
"What's New in Readest": "রিডেস্টে নতুন কী",
|
||||
"Select Files": "ফাইল নির্বাচন",
|
||||
"Select Image": "ছবি নির্বাচন",
|
||||
"Select Video": "ভিডিও নির্বাচন",
|
||||
"Select Audio": "অডিও নির্বাচন",
|
||||
"Select Fonts": "ফন্ট নির্বাচন",
|
||||
"Storage": "স্টোরেজ",
|
||||
"{{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",
|
||||
"Google Books": "গুগল বুকস",
|
||||
"Open Library": "ওপেন লাইব্রেরি",
|
||||
"Azure Translator": "Azure অনুবাদক",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "গুগল ট্রান্সলেট",
|
||||
"Yandex Translate": "ইয়ান্ডেক্স ট্রান্সলেট",
|
||||
"Gray": "ধূসর",
|
||||
"Sepia": "সেপিয়া",
|
||||
"Grass": "ঘাস",
|
||||
"Cherry": "চেরি",
|
||||
"Sky": "আকাশ",
|
||||
"Solarized": "সোলারাইজড",
|
||||
"Gruvbox": "গ্রুভবক্স",
|
||||
"Nord": "নর্ড",
|
||||
"Contrast": "কন্ট্রাস্ট",
|
||||
"Sunset": "সূর্যাস্ত",
|
||||
"Reveal in Finder": "ফাইন্ডারে দেখান",
|
||||
"Reveal in File Explorer": "ফাইল এক্সপ্লোরারে দেখান",
|
||||
"Reveal in Folder": "ফোল্ডারে দেখান",
|
||||
"Previous Paragraph": "পূর্ববর্তী অনুচ্ছেদ",
|
||||
"Previous Sentence": "পূর্ববর্তী বাক্য",
|
||||
"Pause": "বিরতি",
|
||||
"Play": "চালান",
|
||||
"Next Sentence": "পরবর্তী বাক্য",
|
||||
"Next Paragraph": "পরবর্তী অনুচ্ছেদ",
|
||||
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা"
|
||||
}
|
||||
@@ -476,7 +476,6 @@
|
||||
"Always use latest": "དེ་ལས་འབྱུང་ཁུངས་འདེམས་པ་བྱས་ནས་བརྗེ་བ།",
|
||||
"Send changes only": "འབྱུང་ཁུངས་འདེམས་པ་བྱས་ནས་བརྗེ་བ།",
|
||||
"Receive changes only": "འབྱུང་ཁུངས་འདེམས་པ་བྱས་ནས་བརྗེ་བ།",
|
||||
"Disabled": "བཀག་པ།",
|
||||
"Checksum Method": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"File Content (recommended)": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"File Name": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "ཡིག་གཟུགས་འདེམས་པ།",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "ཡིག་གཟུགས་འདེམས་པ་བྱས་ནས་བསྐྱར་བརྗེ་བ། ཡིག་གཟུགས་ཀྱི་སྒྲ་སྐད། .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "འདེགས་པའི་འཕེལ་རིམ།",
|
||||
"Pull Progress": "འདྲུད་པའི་འཕེལ་རིམ།"
|
||||
"Pull Progress": "འདྲུད་པའི་འཕེལ་རིམ།",
|
||||
"Previous Paragraph": "སྔོན་གྱི་དོན་ཚན།",
|
||||
"Previous Sentence": "སྔོན་གྱི་ཚིག་གྲུབ།",
|
||||
"Pause": "སྐར་མ་འཇོག",
|
||||
"Play": "གཏོང་བ།",
|
||||
"Next Sentence": "རྗེས་ཀྱི་ཚིག་གྲུབ།",
|
||||
"Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།",
|
||||
"Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།"
|
||||
}
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"Always use latest": "Immer die neueste Version verwenden",
|
||||
"Send changes only": "Nur Änderungen senden",
|
||||
"Receive changes only": "Nur Änderungen empfangen",
|
||||
"Disabled": "Deaktiviert",
|
||||
"Checksum Method": "Prüfziffernverfahren",
|
||||
"File Content (recommended)": "Dateiinhalte (empfohlen)",
|
||||
"File Name": "Dateiname",
|
||||
@@ -516,5 +515,12 @@
|
||||
"Select Fonts": "Schriftarten auswählen",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Unterstützte Schriftartenformate: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Fortschritt senden",
|
||||
"Pull Progress": "Fortschritt empfangen"
|
||||
"Pull Progress": "Fortschritt empfangen",
|
||||
"Previous Paragraph": "Vorheriger Absatz",
|
||||
"Previous Sentence": "Vorheriger Satz",
|
||||
"Pause": "Pause",
|
||||
"Play": "Wiedergabe",
|
||||
"Next Sentence": "Nächster Satz",
|
||||
"Next Paragraph": "Nächster Absatz",
|
||||
"Separate Cover Page": "Titelseite separat anzeigen"
|
||||
}
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"Always use latest": "Χρησιμοποίησε πάντα την τελευταία έκδοση",
|
||||
"Send changes only": "Αποστολή μόνο αλλαγών",
|
||||
"Receive changes only": "Λήψη μόνο αλλαγών",
|
||||
"Disabled": "Απενεργοποιημένο",
|
||||
"Checksum Method": "Μέθοδος ελέγχου",
|
||||
"File Content (recommended)": "Περιεχόμενο αρχείου (συνιστάται)",
|
||||
"File Name": "Όνομα αρχείου",
|
||||
@@ -516,5 +515,12 @@
|
||||
"Select Fonts": "Επιλογή Γραμματοσειρών",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Υποστηριζόμενες μορφές γραμματοσειρών: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Αποστολή προόδου",
|
||||
"Pull Progress": "Λήψη προόδου"
|
||||
"Pull Progress": "Λήψη προόδου",
|
||||
"Previous Paragraph": "Προηγούμενη παράγραφος",
|
||||
"Previous Sentence": "Προηγούμενη πρόταση",
|
||||
"Pause": "Παύση",
|
||||
"Play": "Αναπαραγωγή",
|
||||
"Next Sentence": "Επόμενη πρόταση",
|
||||
"Next Paragraph": "Επόμενη παράγραφος",
|
||||
"Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου"
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@
|
||||
"Always use latest": "Usar siempre el más reciente",
|
||||
"Send changes only": "Solo enviar cambios",
|
||||
"Receive changes only": "Solo recibir cambios",
|
||||
"Disabled": "Desactivado",
|
||||
"Checksum Method": "Método de identificación",
|
||||
"File Content (recommended)": "Contenido del archivo (recomendado)",
|
||||
"File Name": "Nombre del archivo",
|
||||
@@ -520,5 +519,12 @@
|
||||
"Select Fonts": "Seleccionar Fuentes",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fuente compatibles: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Empujar progreso",
|
||||
"Pull Progress": "Tirar progreso"
|
||||
"Pull Progress": "Tirar progreso",
|
||||
"Previous Paragraph": "Párrafo anterior",
|
||||
"Previous Sentence": "Oración anterior",
|
||||
"Pause": "Pausar",
|
||||
"Play": "Reproducir",
|
||||
"Next Sentence": "Siguiente oración",
|
||||
"Next Paragraph": "Siguiente párrafo",
|
||||
"Separate Cover Page": "Portada separada"
|
||||
}
|
||||
|
||||
@@ -482,7 +482,6 @@
|
||||
"Always use latest": "Toujours utiliser la dernière version",
|
||||
"Send changes only": "Envoyer uniquement les modifications",
|
||||
"Receive changes only": "Recevoir uniquement les modifications",
|
||||
"Disabled": "Désactivé",
|
||||
"Checksum Method": "Méthode de somme de contrôle",
|
||||
"File Content (recommended)": "Contenu du fichier (recommandé)",
|
||||
"File Name": "Nom du fichier",
|
||||
@@ -520,5 +519,12 @@
|
||||
"Select Fonts": "Sélectionner des Polices",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formats de police pris en charge : .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Pousser la Progression",
|
||||
"Pull Progress": "Tirer la Progression"
|
||||
"Pull Progress": "Tirer la Progression",
|
||||
"Previous Paragraph": "Paragraphe précédent",
|
||||
"Previous Sentence": "Phrase précédente",
|
||||
"Pause": "Pause",
|
||||
"Play": "Lecture",
|
||||
"Next Sentence": "Phrase suivante",
|
||||
"Next Paragraph": "Paragraphe suivant",
|
||||
"Separate Cover Page": "Page de couverture séparée"
|
||||
}
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"Always use latest": "हमेशा नवीनतम का उपयोग करें",
|
||||
"Send changes only": "केवल परिवर्तन भेजें",
|
||||
"Receive changes only": "केवल परिवर्तन प्राप्त करें",
|
||||
"Disabled": "अक्षम",
|
||||
"Checksum Method": "चेकसम विधि",
|
||||
"File Content (recommended)": "फाइल सामग्री (अनुशंसित)",
|
||||
"File Name": "फाइल नाम",
|
||||
@@ -515,6 +514,13 @@
|
||||
"Select Audio": "ऑडियो चुनें",
|
||||
"Select Fonts": "फ़ॉन्ट चुनें",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "समर्थित फ़ॉन्ट प्रारूप: .ttf, .otf, .woff, .woff2",
|
||||
"Push progress": "पुश प्रगति",
|
||||
"Pull progress": "पुल प्रगति"
|
||||
"Push Progress": "पुश प्रगति",
|
||||
"Pull Progress": "पुल प्रगति",
|
||||
"Previous Paragraph": "पिछला पैराग्राफ",
|
||||
"Previous Sentence": "पिछला वाक्य",
|
||||
"Pause": "रोकें",
|
||||
"Play": "चलाएं",
|
||||
"Next Sentence": "अगला वाक्य",
|
||||
"Next Paragraph": "अगला पैराग्राफ",
|
||||
"Separate Cover Page": "अलग कवर पृष्ठ"
|
||||
}
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "Selalu gunakan yang terbaru",
|
||||
"Send changes only": "Kirim perubahan saja",
|
||||
"Receive changes only": "Terima perubahan saja",
|
||||
"Disabled": "Dinonaktifkan",
|
||||
"Checksum Method": "Metode Checksum",
|
||||
"File Content (recommended)": "Konten File (disarankan)",
|
||||
"File Name": "Nama File",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "Pilih Font",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format font yang didukung: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Dorong Progres",
|
||||
"Pull Progress": "Tarik Progres"
|
||||
"Pull Progress": "Tarik Progres",
|
||||
"Previous Paragraph": "Paragraf Sebelumnya",
|
||||
"Previous Sentence": "Kalimat Sebelumnya",
|
||||
"Pause": "Jeda",
|
||||
"Play": "Putar",
|
||||
"Next Sentence": "Kalimat Berikutnya",
|
||||
"Next Paragraph": "Paragraf Berikutnya",
|
||||
"Separate Cover Page": "Halaman Sampul Terpisah"
|
||||
}
|
||||
|
||||
@@ -482,7 +482,6 @@
|
||||
"Always use latest": "Usa sempre l'ultima versione",
|
||||
"Send changes only": "Invia solo le modifiche",
|
||||
"Receive changes only": "Ricevi solo le modifiche",
|
||||
"Disabled": "Disabilitato",
|
||||
"Checksum Method": "Metodo di Controllo",
|
||||
"File Content (recommended)": "Contenuto del File (consigliato)",
|
||||
"File Name": "Nome del File",
|
||||
@@ -519,6 +518,13 @@
|
||||
"Select Audio": "Seleziona Audio",
|
||||
"Select Fonts": "Seleziona Font",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formati di font supportati: .ttf, .otf, .woff, .woff2",
|
||||
"Push progress": "Progresso push",
|
||||
"Pull progress": "Progresso pull"
|
||||
"Push Progress": "Progresso push",
|
||||
"Pull Progress": "Progresso pull",
|
||||
"Previous Paragraph": "Paragrafo precedente",
|
||||
"Previous Sentence": "Frase precedente",
|
||||
"Pause": "Pausa",
|
||||
"Play": "Riproduci",
|
||||
"Next Sentence": "Frase successiva",
|
||||
"Next Paragraph": "Paragrafo successivo",
|
||||
"Separate Cover Page": "Pagina di copertina separata"
|
||||
}
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "最新を常に使用",
|
||||
"Send changes only": "変更のみを送信",
|
||||
"Receive changes only": "変更のみを受信",
|
||||
"Disabled": "無効",
|
||||
"Checksum Method": "チェックサム方式",
|
||||
"File Content (recommended)": "ファイル内容(推奨)",
|
||||
"File Name": "ファイル名",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "フォントを選択",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "サポートされているフォント形式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "進捗をプッシュ",
|
||||
"Pull Progress": "進捗をプル"
|
||||
"Pull Progress": "進捗をプル",
|
||||
"Previous Paragraph": "前の段落",
|
||||
"Previous Sentence": "前の文",
|
||||
"Pause": "一時停止",
|
||||
"Play": "再生",
|
||||
"Next Sentence": "次の文",
|
||||
"Next Paragraph": "次の段落",
|
||||
"Separate Cover Page": "別の表紙ページ"
|
||||
}
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "항상 최신 버전 사용",
|
||||
"Send changes only": "변경 사항만 전송",
|
||||
"Receive changes only": "변경 사항만 수신",
|
||||
"Disabled": "비활성화",
|
||||
"Checksum Method": "체크섬 방법",
|
||||
"File Content (recommended)": "파일 내용 (권장)",
|
||||
"File Name": "파일 이름",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "글꼴 선택",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "지원되는 글꼴 형식: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "진행 상황 푸시",
|
||||
"Pull Progress": "진행 상황 끌어오기"
|
||||
"Pull Progress": "진행 상황 끌어오기",
|
||||
"Previous Paragraph": "이전 단락",
|
||||
"Previous Sentence": "이전 문장",
|
||||
"Pause": "일시정지",
|
||||
"Play": "재생",
|
||||
"Next Sentence": "다음 문장",
|
||||
"Next Paragraph": "다음 단락",
|
||||
"Separate Cover Page": "별도의 표지 페이지"
|
||||
}
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"Always use latest": "Altijd de nieuwste gebruiken",
|
||||
"Send changes only": "Verzend alleen wijzigingen",
|
||||
"Receive changes only": "Ontvang alleen wijzigingen",
|
||||
"Disabled": "Uitgeschakeld",
|
||||
"Checksum Method": "Checksum-methode",
|
||||
"File Content (recommended)": "Bestandsinhoud (aanbevolen)",
|
||||
"File Name": "Bestandsnaam",
|
||||
@@ -516,5 +515,12 @@
|
||||
"Select Fonts": "Lettertypen Selecteren",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Ondersteunde lettertypeformaten: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Voortgang pushen",
|
||||
"Pull Progress": "Voortgang pullen"
|
||||
"Pull Progress": "Voortgang pullen",
|
||||
"Previous Paragraph": "Vorige alinea",
|
||||
"Previous Sentence": "Vorige zin",
|
||||
"Pause": "Pauzeren",
|
||||
"Play": "Afspelen",
|
||||
"Next Sentence": "Volgende zin",
|
||||
"Next Paragraph": "Volgende alinea",
|
||||
"Separate Cover Page": "Afzonderlijke omslagpagina"
|
||||
}
|
||||
|
||||
@@ -486,7 +486,6 @@
|
||||
"Always use latest": "Zawsze używaj najnowszej",
|
||||
"Send changes only": "Wyślij tylko zmiany",
|
||||
"Receive changes only": "Odbierz tylko zmiany",
|
||||
"Disabled": "Wyłączone",
|
||||
"Checksum Method": "Metoda sumy kontrolnej",
|
||||
"File Content (recommended)": "Zawartość pliku (zalecane)",
|
||||
"File Name": "Nazwa pliku",
|
||||
@@ -524,5 +523,12 @@
|
||||
"Select Fonts": "Wybierz Czcionki",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Obsługiwane formaty czcionek: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Wyślij postęp",
|
||||
"Pull Progress": "Pobierz postęp"
|
||||
"Pull Progress": "Pobierz postęp",
|
||||
"Previous Paragraph": "Poprzedni akapit",
|
||||
"Previous Sentence": "Poprzednie zdanie",
|
||||
"Pause": "Pauza",
|
||||
"Play": "Odtwórz",
|
||||
"Next Sentence": "Następne zdanie",
|
||||
"Next Paragraph": "Następny akapit",
|
||||
"Separate Cover Page": "Osobna strona okładki"
|
||||
}
|
||||
|
||||
@@ -482,7 +482,6 @@
|
||||
"Always use latest": "Sempre usar o mais recente",
|
||||
"Send changes only": "Enviar apenas alterações",
|
||||
"Receive changes only": "Receber apenas alterações",
|
||||
"Disabled": "Desativado",
|
||||
"Checksum Method": "Método de Verificação",
|
||||
"File Content (recommended)": "Conteúdo do Arquivo (recomendado)",
|
||||
"File Name": "Nome do Arquivo",
|
||||
@@ -520,5 +519,12 @@
|
||||
"Select Fonts": "Selecionar Fontes",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fonte suportados: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Enviar Progresso",
|
||||
"Pull Progress": "Obter Progresso"
|
||||
"Pull Progress": "Obter Progresso",
|
||||
"Previous Paragraph": "Parágrafo anterior",
|
||||
"Previous Sentence": "Frase anterior",
|
||||
"Pause": "Pausar",
|
||||
"Play": "Reproduzir",
|
||||
"Next Sentence": "Próxima frase",
|
||||
"Next Paragraph": "Próximo parágrafo",
|
||||
"Separate Cover Page": "Páginas de Capa Separadas"
|
||||
}
|
||||
|
||||
@@ -486,7 +486,6 @@
|
||||
"Always use latest": "Всегда использовать последнюю",
|
||||
"Send changes only": "Отправлять только изменения",
|
||||
"Receive changes only": "Получать только изменения",
|
||||
"Disabled": "Отключено",
|
||||
"Checksum Method": "Метод контрольной суммы",
|
||||
"File Content (recommended)": "Содержимое файла (рекомендуется)",
|
||||
"File Name": "Имя файла",
|
||||
@@ -524,5 +523,12 @@
|
||||
"Select Fonts": "Выбрать Шрифты",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Поддерживаемые форматы шрифтов: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Отправить прогресс",
|
||||
"Pull Progress": "Получить прогресс"
|
||||
"Pull Progress": "Получить прогресс",
|
||||
"Previous Paragraph": "Предыдущий абзац",
|
||||
"Previous Sentence": "Предыдущее предложение",
|
||||
"Pause": "Пауза",
|
||||
"Play": "Воспроизвести",
|
||||
"Next Sentence": "Следующее предложение",
|
||||
"Next Paragraph": "Следующий абзац",
|
||||
"Separate Cover Page": "Отдельная страница обложки"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
{
|
||||
"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...": "ලියාපදිංචි වෙමින්...",
|
||||
"Don't have an account? Sign 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)?_one": "ඔබට විශ්වාසද තෝරාගත් පොත මකා දැමීමට?",
|
||||
"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)..._one": "පොතක සොයන්න...",
|
||||
"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": "ස්වයංක්රීයව cloud එකට පොත් උඩුගත කරන්න",
|
||||
"Auto Import on File Open": "ගොනුව විවෘත කිරීමේදී ස්වයංක්රීයව ආයාත කරන්න",
|
||||
"Open Last Book on Start": "ආරම්භයේදී අන්තිම පොත විවෘත කරන්න",
|
||||
"Check Updates on Start": "ආරම්භයේදී යාවත්කාලීන පරීක්ෂා කරන්න",
|
||||
"Open Book in New Window": "නව කවුළුවක පොත විවෘත කරන්න",
|
||||
"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}}",
|
||||
"Deleted cloud backup of the book: {{title}}": "පොතේ cloud backup මකා දමන ලදී: {{title}}",
|
||||
"Deleted local copy of the book: {{title}}": "පොතේ දේශීය පිටපත මකා දමන ලදී: {{title}}",
|
||||
"Failed to delete book: {{title}}": "පොත මකා දැමීමට අසමත්: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "පොතේ cloud backup මකා දැමීමට අසමත්: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "පොතේ දේශීය පිටපත මකා දැමීමට අසමත්: {{title}}",
|
||||
"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": "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": "පරිවර්තනය කළ පෙළ",
|
||||
"System Language": "පද්ධති භාෂාව",
|
||||
"Loading...": "පූරණය වෙමින්...",
|
||||
"No translation available.": "පරිවර්තනය ලබා ගත නොහැක.",
|
||||
"Translated by {{provider}}.": "{{provider}} විසින් පරිවර්තනය කරන ලදී.",
|
||||
"Bookmark": "පොත් සලකුණ",
|
||||
"Next Section": "ඊළඟ කොටස",
|
||||
"Previous Section": "පෙර කොටස",
|
||||
"Next Page": "ඊළඟ පිටුව",
|
||||
"Previous Page": "පෙර පිටුව",
|
||||
"Go Forward": "ඉදිරියට යන්න",
|
||||
"Small": "කුඩා",
|
||||
"Large": "විශාල",
|
||||
"Sync Conflict": "සමමුහුර්ත ගැටුම",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "\"{{deviceName}}\" වෙතින් කියවීමේ ප්රගතිය සමමුහුර්ත කරන්නද?",
|
||||
"another device": "වෙනත් උපකරණයක්",
|
||||
"Local Progress": "දේශීය ප්රගතිය",
|
||||
"Remote Progress": "දුරස්ථ ප්රගතිය",
|
||||
"Failed to connect": "සම්බන්ධ වීමට අසමත්",
|
||||
"Disconnected": "විසන්ධි වුණි",
|
||||
"KOReader Sync Settings": "KOReader සමමුහුර්ත සැකසුම්",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}} ලෙස සමමුහුර්ත කරන්න",
|
||||
"Sync Server Connected": "සමමුහුර්ත සේවාදායකය සම්බන්ධ විය",
|
||||
"Sync Strategy": "සමමුහුර්ත උපාය",
|
||||
"Ask on conflict": "ගැටුමේදී විමසන්න",
|
||||
"Always use latest": "සෑම විටම නවතම භාවිතා කරන්න",
|
||||
"Send changes only": "වෙනස්කම් පමණක් යවන්න",
|
||||
"Receive changes only": "වෙනස්කම් පමණක් ලබන්න",
|
||||
"Checksum Method": "පරීක්ෂණ ක්රමය",
|
||||
"File Content (recommended)": "ගොනු අන්තර්ගතය (නිර්දේශිත)",
|
||||
"File Name": "ගොනුවේ නම",
|
||||
"Device Name": "උපකරණයේ නම",
|
||||
"Connect to your KOReader Sync server.": "ඔබේ KOReader සමමුහුර්ත සේවාදායකයට සම්බන්ධ වන්න.",
|
||||
"Server URL": "සේවාදායක URL",
|
||||
"Username": "පරිශීලක නම",
|
||||
"Your Username": "ඔබේ පරිශීලක නම",
|
||||
"Password": "මුරපදය",
|
||||
"Connect": "සම්බන්ධ කරන්න",
|
||||
"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_one": "පරිච්ඡේදයේ පිටුව 1 ක් ඉතිරි",
|
||||
"{{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": "කේත භාෂාව",
|
||||
"Auto": "ස්වයංක්රීය",
|
||||
"Scroll": "අනුචලනය",
|
||||
"Scrolled Mode": "අනුචලන ආකාරය",
|
||||
"Continuous Scroll": "අඛණ්ඩ අනුචලනය",
|
||||
"Overlap Pixels": "අතිච්ඡාදන පික්සල්",
|
||||
"Click": "ක්ලික්",
|
||||
"Clicks for Page Flip": "පිටු පෙරැළීම සඳහා ක්ලික්",
|
||||
"Swap Clicks Area": "ක්ලික් ප්රදේශය මාරු කරන්න",
|
||||
"Disable Double Click": "ද්විත්ව ක්ලික් අබල කරන්න",
|
||||
"Volume Keys for Page Flip": "පිටු පෙරැළීම සඳහා ශබ්ද යතුර",
|
||||
"Animation": "සජීවිකරණය",
|
||||
"Paging Animation": "පිටු සජීවිකරණය",
|
||||
"Security": "ආරක්ෂාව",
|
||||
"Allow JavaScript": "JavaScript ට ඉඩ දෙන්න",
|
||||
"Enable only if you trust the file.": "ගොනුව විශ්වාසදායක නම් පමණක් සක්රීය කරන්න.",
|
||||
"Font": "අකුරු වර්ගය",
|
||||
"Custom Fonts": "අභිරුචි අකුරු",
|
||||
"Cancel Delete": "මකා දැමීම අවලංගු කරන්න",
|
||||
"Delete Font": "අකුරු මකන්න",
|
||||
"Import Font": "අකුරු ආයාත කරන්න",
|
||||
"Tips": "ඉඟි",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "සහාය දක්වන අකුරු ආකෘති: .ttf, .otf, .woff, .woff2",
|
||||
"Custom fonts can be selected from the Font Face menu": "Font Face මෙනුවෙන් අභිරුචි අකුරු තෝරා ගත හැක",
|
||||
"Global Settings": "ගෝලීය සැකසුම්",
|
||||
"Apply to All Books": "සියලු පොත් වලට අනුගත කරන්න",
|
||||
"Apply to This Book": "මෙම පොතට අනුගත කරන්න",
|
||||
"Reset Settings": "සැකසුම් නැවත සකස් කරන්න",
|
||||
"Manage Custom Fonts": "අභිරුචි අකුරු කළමනාකරණය කරන්න",
|
||||
"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 අකුරු",
|
||||
"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 Reading Progress": "කියවීමේ ප්රගතිය පෙන්වන්න",
|
||||
"Reading Progress Style": "කියවීමේ ප්රගති ශෛලිය",
|
||||
"Page Number": "පිටු අංකය",
|
||||
"Percentage": "ප්රතිශතය",
|
||||
"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": "අභිරුචි කියවන්නා UI CSS",
|
||||
"Enter CSS for reader interface styling...": "කියවන්නා අතුරුමුහුණත ශෛලිකරණය සඳහා CSS ඇතුල් කරන්න...",
|
||||
"Layout": "පිරිසැලසුම",
|
||||
"Color": "වර්ණය",
|
||||
"Behavior": "හැසිරීම",
|
||||
"Reset {{settings}}": "{{settings}} නැවත සකස් කරන්න",
|
||||
"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— විලියම් ශේක්ස්පියර්",
|
||||
"(from 'As You Like It', Act II)": "('ඔබට අවශ්ය ලෙස', දෙවන අංකයෙන්)",
|
||||
"Custom Theme": "අභිරුචි තේමාව",
|
||||
"Theme Name": "තේමා නාමය",
|
||||
"Text Color": "පෙළ වර්ණය",
|
||||
"Background Color": "පසුබිම් වර්ණය",
|
||||
"Link Color": "සබැඳි වර්ණය",
|
||||
"Preview": "පෙර දසුන",
|
||||
"Font & Layout": "අකුරු සහ පිරිසැලසුම",
|
||||
"Book Cover": "පොත් ආවරණය",
|
||||
"More Info": "තවත් තොරතුරු",
|
||||
"Parallel Read": "සමාන්තර කියවීම",
|
||||
"Disable": "අබල කරන්න",
|
||||
"Enable": "සක්රීය කරන්න",
|
||||
"KOReader Sync": "KOReader සමමුහුර්තය",
|
||||
"Push Progress": "ප්රගතිය ඉදිරියට",
|
||||
"Pull Progress": "ප්රගතිය අදින්න",
|
||||
"Export Annotations": "සටහන් නිර්යාත කරන්න",
|
||||
"Sort TOC by Page": "පිටුව අනුව TOC සකස් කරන්න",
|
||||
"Edit": "සංස්කරණය",
|
||||
"Search...": "සොයන්න...",
|
||||
"Book": "පොත",
|
||||
"Chapter": "පරිච්ඡේදය",
|
||||
"Match Case": "අකුරු ගැළපීම",
|
||||
"Match Whole Words": "සම්පූර්ණ වචන ගැළපීම",
|
||||
"Match Diacritics": "ධ්වනි ලකුණු ගැළපීම",
|
||||
"TOC": "සූචිය",
|
||||
"Sidebar": "පැති තීරුව",
|
||||
"Disable Translation": "පරිවර්තනය අබල කරන්න",
|
||||
"Translation Not Available": "පරිවර්තනය ලබා ගත නොහැක",
|
||||
"TTS not supported for PDF": "PDF සඳහා TTS සහාය නොදක්වයි",
|
||||
"TTS not supported for this document": "මෙම ලේඛනය සඳහා TTS සහාය නොදක්වයි",
|
||||
"No Timeout": "කාල සීමාවක් නැත",
|
||||
"{{value}} minute": "මිනිත්තු {{value}}",
|
||||
"{{value}} minutes": "මිනිත්තු {{value}}",
|
||||
"{{value}} hour": "පැය {{value}}",
|
||||
"{{value}} hours": "පැය {{value}}",
|
||||
"Voices for {{lang}}": "{{lang}} සඳහා හඬ",
|
||||
"Slow": "මන්දගාමී",
|
||||
"Fast": "වේගවත්",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: හඬ 1",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: හඬ {{count}}",
|
||||
"Sign in to Sync": "සමමුහුර්ත කිරීම සඳහා ඇතුල් වන්න",
|
||||
"Synced at {{time}}": "{{time}} වේලාවේ සමමුහුර්ත විය",
|
||||
"Never synced": "කිසි විටෙක සමමුහුර්ත නොවිණි",
|
||||
"Reading Progress Synced": "කියවීමේ ප්රගතිය සමමුහුර්ත විය",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "{{total}} න් {{page}} පිටුව ({{percentage}}%)",
|
||||
"Current position": "වර්තමාන ස්ථානය",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "දළ වශයෙන් {{total}} න් {{page}} පිටුව ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "දළ වශයෙන් {{percentage}}%",
|
||||
"Delete Your Account?": "ඔබේ ගිණුම මකන්නද?",
|
||||
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "මෙම ක්රියාව අවලංගු කළ නොහැක. cloud හි ඔබේ සියලු දත්ත ස්ථිරවම මකා දැමෙනු ඇත.",
|
||||
"Delete Permanently": "ස්ථිරවම මකන්න",
|
||||
"Restore Purchase": "මිලදී ගැනීම ප්රතිෂ්ඨාපනය කරන්න",
|
||||
"Manage Subscription": "දායකත්වය කළමනාකරණය කරන්න",
|
||||
"Reset Password": "මුරපදය නැවත සකස් කරන්න",
|
||||
"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 create checkout session": "checkout session නිර්මාණය කිරීමට අසමත්",
|
||||
"No purchases found to restore.": "ප්රතිෂ්ඨාපනය කිරීමට මිලදී ගැනීම් නොමැත.",
|
||||
"Failed to restore purchases.": "මිලදී ගැනීම් ප්රතිෂ්ඨාපනය කිරීමට අසමත්.",
|
||||
"Failed to manage subscription.": "දායකත්වය කළමනාකරණය කිරීමට අසමත්.",
|
||||
"Failed to load subscription plans.": "දායකත්ව සැලැස්ම් පූරණය කිරීමට අසමත්.",
|
||||
"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:": "දායකත්ව ID:",
|
||||
"Go to Library": "පුස්තකාලයට යන්න",
|
||||
"Need help? Contact our support team at support@readest.com": "උදව් අවශ්යද? support@readest.com හි අපගේ සහාය කණ්ඩායමට සම්බන්ධ වන්න",
|
||||
"Free Plan": "නොමිලේ සැලැස්ම",
|
||||
"month": "මාසය",
|
||||
"year": "වර්ෂය",
|
||||
"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": "Cloud සමමුහුර්ත ගබඩාව",
|
||||
"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 ක් දක්වා පරිවර්තනය කරන්න.",
|
||||
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "GB 5 ක් දක්වා cloud ගබඩාව සමඟ ඔබේ සම්පූර්ණ කියවීමේ එකතුව ආරක්ෂිතව ගබඩා කරන්න සහ ප්රවේශ වන්න.",
|
||||
"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 ක් දක්වා පරිවර්තනය කරන්න.",
|
||||
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "GB 20 ක් දක්වා cloud ගබඩාව සමඟ ඔබේ සම්පූර්ණ කියවීමේ එකතුව ආරක්ෂිතව ගබඩා කරන්න සහ ප්රවේශ වන්න.",
|
||||
"Version {{version}}": "සංස්කරණය {{version}}",
|
||||
"Check Update": "යාවත්කාලීන පරීක්ෂා කරන්න",
|
||||
"Already the latest version": "දැනටමත් නවතම සංස්කරණය",
|
||||
"Checking for updates...": "යාවත්කාලීන සඳහා පරීක්ෂා කරමින්...",
|
||||
"Error checking for updates": "යාවත්කාලීන පරීක්ෂා කිරීමේ දෝෂය",
|
||||
"Drop to Import Books": "පොත් ආයාත කිරීමට අතහරින්න",
|
||||
"Terms of Service": "සේවා නියමයන්",
|
||||
"Privacy Policy": "පෞද්ගලිකත්ව ප්රතිපත්තිය",
|
||||
"Enter book title": "පොතේ නම ඇතුල් කරන්න",
|
||||
"Subtitle": "උප නාමය",
|
||||
"Enter book subtitle": "පොතේ උප නාමය ඇතුල් කරන්න",
|
||||
"Enter author name": "කතුවරයාගේ නම ඇතුල් කරන්න",
|
||||
"Series": "කතා මාලාව",
|
||||
"Enter series name": "කතා මාලාවේ නම ඇතුල් කරන්න",
|
||||
"Series Index": "කතා මාලා සූචිය",
|
||||
"Enter series index": "කතා මාලා සූචිය ඇතුල් කරන්න",
|
||||
"Total in Series": "කතා මාලාවේ මුළු ගණන",
|
||||
"Enter total books in series": "කතා මාලාවේ මුළු පොත් ගණන ඇතුල් කරන්න",
|
||||
"Publisher": "ප්රකාශක",
|
||||
"Enter publisher": "ප්රකාශක ඇතුල් කරන්න",
|
||||
"Publication Date": "ප්රකාශන දිනය",
|
||||
"YYYY or YYYY-MM-DD": "YYYY හෝ YYYY-MM-DD",
|
||||
"Identifier": "හඳුනාගැනීම",
|
||||
"Subjects": "විෂයන්",
|
||||
"Fiction, Science, History": "ප්රබන්ධ, විද්යාව, ඉතිහාසය",
|
||||
"Description": "විස්තරය",
|
||||
"Enter book description": "පොතේ විස්තරය ඇතුල් කරන්න",
|
||||
"Change cover image": "ආවරණ රූපය වෙනස් කරන්න",
|
||||
"Replace": "ආදේශ කරන්න",
|
||||
"Unlock cover": "ආවරණය අගුළු ඇරින්න",
|
||||
"Lock cover": "ආවරණය අගුළු දමන්න",
|
||||
"Auto-Retrieve Metadata": "Metadata ස්වයංක්රීයව ලබා ගන්න",
|
||||
"Auto-Retrieve": "ස්වයංක්රීයව ලබා ගන්න",
|
||||
"Unlock all fields": "සියලු ක්ෂේත්ර අගුළු ඇරින්න",
|
||||
"Unlock All": "සියල්ල අගුළු ඇරින්න",
|
||||
"Lock all fields": "සියලු ක්ෂේත්ර අගුළු දමන්න",
|
||||
"Lock All": "සියල්ල අගුළු දමන්න",
|
||||
"Reset": "නැවත සකස් කරන්න",
|
||||
"Are you sure to delete the selected book?": "ඔබට විශ්වාසද තෝරාගත් පොත මකා දැමීමට?",
|
||||
"Are you sure to delete the cloud backup of the selected book?": "ඔබට විශ්වාසද තෝරාගත් පොතේ cloud backup මකා දැමීමට?",
|
||||
"Are you sure to delete the local copy of the selected book?": "ඔබට විශ්වාසද තෝරාගත් පොතේ දේශීය පිටපත මකා දැමීමට?",
|
||||
"Edit Metadata": "Metadata සංස්කරණය කරන්න",
|
||||
"Book Details": "පොතේ විස්තර",
|
||||
"Unknown": "නොදන්නා",
|
||||
"Remove from Cloud & Device": "Cloud සහ උපකරණයෙන් ඉවත් කරන්න",
|
||||
"Remove from Cloud Only": "Cloud එකෙන් පමණක් ඉවත් කරන්න",
|
||||
"Remove from Device Only": "උපකරණයෙන් පමණක් ඉවත් කරන්න",
|
||||
"Published": "ප්රකාශිතය",
|
||||
"Updated": "යාවත්කාලීන කරන ලදී",
|
||||
"Added": "එක් කරන ලදී",
|
||||
"File Size": "ගොනු ප්රමාණය",
|
||||
"No description available": "විස්තරයක් ලබා ගත නොහැක",
|
||||
"Locked": "අගුළු දමන ලදී",
|
||||
"Select Metadata Source": "Metadata මූලාශ්රය තෝරන්න",
|
||||
"Keep manual input": "අතින් ඇතුල් කළ දේ තබාගන්න",
|
||||
"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}}": "{{contentLength}} න් {{downloaded}} බාගනිමින්",
|
||||
"Download finished": "බාගැනීම සම්පූර්ණයි",
|
||||
"DOWNLOAD & INSTALL": "බාගන්න සහ ස්ථාපනය කරන්න",
|
||||
"Changelog": "වෙනස්කම් ලිපිය",
|
||||
"Software Update": "මෘදුකාංග යාවත්කාලීන",
|
||||
"What's New in Readest": "Readest හි නවතම දේ",
|
||||
"Select Files": "ගොනු තෝරන්න",
|
||||
"Select Image": "රූපය තෝරන්න",
|
||||
"Select Video": "වීඩියෝව තෝරන්න",
|
||||
"Select Audio": "ශ්රව්ය තෝරන්න",
|
||||
"Select Fonts": "අකුරු තෝරන්න",
|
||||
"Storage": "ගබඩාව",
|
||||
"{{percentage}}% of Cloud Sync Space Used.": "Cloud සමමුහුර්ත ඉඩෙන් {{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",
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"Gray": "අළු",
|
||||
"Sepia": "සීපියා",
|
||||
"Grass": "තණකොළ",
|
||||
"Cherry": "චෙරි",
|
||||
"Sky": "අහස",
|
||||
"Solarized": "සූර්යකිරණ",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Nord": "Nord",
|
||||
"Contrast": "පරස්පර",
|
||||
"Sunset": "හිරු බැස්ම",
|
||||
"Reveal in Finder": "Finder හි පෙන්වන්න",
|
||||
"Reveal in File Explorer": "File Explorer හි පෙන්වන්න",
|
||||
"Reveal in Folder": "ෆෝල්ඩරයේ පෙන්වන්න",
|
||||
"Previous Paragraph": "කලින් ඡේදය",
|
||||
"Previous Sentence": "කලින් වාක්යය",
|
||||
"Pause": "විරාමය",
|
||||
"Play": "වාදනය කරන්න",
|
||||
"Next Sentence": "මීළඟ වාක්යය",
|
||||
"Next Paragraph": "මීළඟ ඡේදය",
|
||||
"Separate Cover Page": "වෙන් වූ ආවරණ පිටුව"
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
{
|
||||
"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...": "பதிவு செய்கிறது...",
|
||||
"Don't have an account? Sign 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)?_one": "தேர்ந்தெடுக்கப்பட்ட புத்தகத்தை நீக்க உறுதியா?",
|
||||
"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)..._one": "புத்தகத்தில் தேடவும்...",
|
||||
"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": "புத்தகங்களை cloud இல் தானாக பதிவேற்றவும்",
|
||||
"Auto Import on File Open": "கோப்பு திறக்கும்போது தானாக இறக்குமதி",
|
||||
"Open Last Book on Start": "தொடங்கும்போது கடைசி புத்தகத்தைத் திறக்கவும்",
|
||||
"Check Updates on Start": "தொடங்கும்போது புதுப்பிப்புகளைச் சரிபார்க்கவும்",
|
||||
"Open Book in New Window": "புதிய சாளரத்தில் புத்தகத்தைத் திறக்கவும்",
|
||||
"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}}",
|
||||
"Deleted cloud backup of the book: {{title}}": "புத்தகத்தின் cloud backup நீக்கப்பட்டது: {{title}}",
|
||||
"Deleted local copy of the book: {{title}}": "புத்தகத்தின் உள்ளூர் நகல் நீக்கப்பட்டது: {{title}}",
|
||||
"Failed to delete book: {{title}}": "புத்தகத்தை நீக்க முடியவில்லை: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "புத்தகத்தின் cloud backup நீக்க முடியவில்லை: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "புத்தகத்தின் உள்ளூர் நகல் நீக்க முடியவில்லை: {{title}}",
|
||||
"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": "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": "மொழிபெயர்க்கப்பட்ட உரை",
|
||||
"System Language": "கணினி மொழி",
|
||||
"Loading...": "ஏற்றுகிறது...",
|
||||
"No translation available.": "மொழிபெயர்ப்பு கிடைக்கவில்லை.",
|
||||
"Translated by {{provider}}.": "{{provider}} மூலம் மொழிபெயர்க்கப்பட்டது.",
|
||||
"Bookmark": "புக்மார்க்",
|
||||
"Next Section": "அடுத்த பிரிவு",
|
||||
"Previous Section": "முந்தைய பிரிவு",
|
||||
"Next Page": "அடுத்த பக்கம்",
|
||||
"Previous Page": "முந்தைய பக்கம்",
|
||||
"Go Forward": "முன்னோக்கி செல்லவும்",
|
||||
"Small": "சிறிய",
|
||||
"Large": "பெரிய",
|
||||
"Sync Conflict": "ஒத்திசைவு முரண்பாடு",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "\"{{deviceName}}\" இல் இருந்து வாசிப்பு முன்னேற்றத்தை ஒத்திசைக்கவா?",
|
||||
"another device": "மற்றொரு சாதனம்",
|
||||
"Local Progress": "உள்ளூர் முன்னேற்றம்",
|
||||
"Remote Progress": "தொலை முன்னேற்றம்",
|
||||
"Failed to connect": "இணைக்க முடியவில்லை",
|
||||
"Disconnected": "துண்டிக்கப்பட்டது",
|
||||
"KOReader Sync Settings": "KOReader ஒத்திசைவு அமைப்புகள்",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}} ஆக ஒத்திசைக்கவும்",
|
||||
"Sync Server Connected": "ஒத்திசைவு சர்வர் இணைக்கப்பட்டது",
|
||||
"Sync Strategy": "ஒத்திசைவு உத்தி",
|
||||
"Ask on conflict": "முரண்பாட்டில் கேட்கவும்",
|
||||
"Always use latest": "எப்போதும் சமீபத்தியதைப் பயன்படுத்தவும்",
|
||||
"Send changes only": "மாற்றங்களை மட்டும் அனுப்பவும்",
|
||||
"Receive changes only": "மாற்றங்களை மட்டும் பெறவும்",
|
||||
"Checksum Method": "சரிபார்ப்பு முறை",
|
||||
"File Content (recommended)": "கோப்பு உள்ளடக்கம் (பரிந்துரைக்கப்பட்டது)",
|
||||
"File Name": "கோப்பு பெயர்",
|
||||
"Device Name": "சாதன பெயர்",
|
||||
"Connect to your KOReader Sync server.": "உங்கள் KOReader ஒத்திசைவு சர்வருடன் இணையவும்.",
|
||||
"Server URL": "சர்வர் URL",
|
||||
"Username": "பயனர்பெயர்",
|
||||
"Your Username": "உங்கள் பயனர்பெயர்",
|
||||
"Password": "கடவுச்சொல்",
|
||||
"Connect": "இணைக்கவும்",
|
||||
"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_one": "அத்தியாயத்தில் 1 பக்கம் மீதமுள்ளது",
|
||||
"{{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": "குறியீடு மொழி",
|
||||
"Auto": "தானியங்கி",
|
||||
"Scroll": "உருட்டவும்",
|
||||
"Scrolled Mode": "உருட்டல் பயன்முறை",
|
||||
"Continuous Scroll": "தொடர்ச்சியான உருட்டல்",
|
||||
"Overlap Pixels": "ஒன்றுடன் ஒன்று பிக்சல்கள்",
|
||||
"Click": "கிளிக்",
|
||||
"Clicks for Page Flip": "பக்கம் புரட்டுவதற்கு கிளிக்கள்",
|
||||
"Swap Clicks Area": "கிளிக் பகுதியை மாற்றவும்",
|
||||
"Disable Double Click": "இரட்டை கிளிக்கை முடக்கவும்",
|
||||
"Volume Keys for Page Flip": "பக்கம் புரட்டுவதற்கு ஒலி விசைகள்",
|
||||
"Animation": "அனிமேஷன்",
|
||||
"Paging Animation": "பக்க அனிமேஷன்",
|
||||
"Security": "பாதுகாப்பு",
|
||||
"Allow JavaScript": "JavaScript அனுமதிக்கவும்",
|
||||
"Enable only if you trust the file.": "கோப்பை நம்பினால் மட்டும் இயக்கவும்.",
|
||||
"Font": "எழுத்துரு",
|
||||
"Custom Fonts": "தனிப்பயன் எழுத்துருக்கள்",
|
||||
"Cancel Delete": "நீக்குதலை ரத்துசெய்யவும்",
|
||||
"Delete Font": "எழுத்துருவை நீக்கவும்",
|
||||
"Import Font": "எழுத்துருவை இறக்குமதி செய்யவும்",
|
||||
"Tips": "குறிப்புகள்",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "ஆதரிக்கப்படும் எழுத்துரு வடிவங்கள்: .ttf, .otf, .woff, .woff2",
|
||||
"Custom fonts can be selected from the Font Face menu": "Font Face மெனுவிலிருந்து தனிப்பயன் எழுத்துருக்களை தேர்வு செய்யலாம்",
|
||||
"Global Settings": "உலகளாவிய அமைப்புகள்",
|
||||
"Apply to All Books": "அனைத்து புத்தகங்களுக்கும் பொருந்தும்",
|
||||
"Apply to This Book": "இந்த புத்தகத்திற்கு பொருந்தும்",
|
||||
"Reset Settings": "அமைப்புகளை மீட்டமைக்கவும்",
|
||||
"Manage Custom Fonts": "தனிப்பயன் எழுத்துருக்களை நிர்வகிக்கவும்",
|
||||
"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 எழுத்துரு",
|
||||
"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 Reading Progress": "வாசிப்பு முன்னேற்றத்தைக் காட்டவும்",
|
||||
"Reading Progress Style": "வாசிப்பு முன்னேற்ற பாணி",
|
||||
"Page Number": "பக்க எண்",
|
||||
"Percentage": "சதவீதம்",
|
||||
"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": "தனிப்பயன் வாசிப்பான் UI CSS",
|
||||
"Enter CSS for reader interface styling...": "வாசிப்பான் இடைமுக வடிவமைப்பிற்கு CSS ஐ உள்ளிடவும்...",
|
||||
"Layout": "அமைப்பு",
|
||||
"Color": "நிறம்",
|
||||
"Behavior": "நடத்தை",
|
||||
"Reset {{settings}}": "{{settings}} மீட்டமைக்கவும்",
|
||||
"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— வில்லியம் ஷேக்ஸ்பியர்",
|
||||
"(from 'As You Like It', Act II)": "('ஆஸ் யூ லைக் இட்', இரண்டாவது அங்கத்திலிருந்து)",
|
||||
"Custom Theme": "தனிப்பயன் தீம்",
|
||||
"Theme Name": "தீம் பெயர்",
|
||||
"Text Color": "உரை நிறம்",
|
||||
"Background Color": "பின்னணி நிறம்",
|
||||
"Link Color": "இணைப்பு நிறம்",
|
||||
"Preview": "முன்னோட்டம்",
|
||||
"Font & Layout": "எழுத்துரு மற்றும் அமைப்பு",
|
||||
"Book Cover": "புத்தக அட்டை",
|
||||
"More Info": "மேலும் தகவல்",
|
||||
"Parallel Read": "இணை வாசிப்பு",
|
||||
"Disable": "முடக்கவும்",
|
||||
"Enable": "இயக்கவும்",
|
||||
"KOReader Sync": "KOReader ஒத்திசைவு",
|
||||
"Push Progress": "முன்னேற்றத்தை அனுப்பவும்",
|
||||
"Pull Progress": "முன்னேற்றத்தை இழுக்கவும்",
|
||||
"Export Annotations": "குறிப்புகளை ஏற்றுமதி செய்யவும்",
|
||||
"Sort TOC by Page": "பக்கத்தின்படி TOC வரிசைப்படுத்தவும்",
|
||||
"Edit": "திருத்தவும்",
|
||||
"Search...": "தேடவும்...",
|
||||
"Book": "புத்தகம்",
|
||||
"Chapter": "அத்தியாயம்",
|
||||
"Match Case": "எழுத்து பொருத்தம்",
|
||||
"Match Whole Words": "முழு வார்த்தைகள் பொருத்தம்",
|
||||
"Match Diacritics": "உச்சரிப்பு குறிகள் பொருத்தம்",
|
||||
"TOC": "உள்ளடக்கம்",
|
||||
"Sidebar": "பக்கப்பட்டி",
|
||||
"Disable Translation": "மொழிபெயர்ப்பை முடக்கவும்",
|
||||
"Translation Not Available": "மொழிபெயர்ப்பு கிடைக்கவில்லை",
|
||||
"TTS not supported for PDF": "PDF க்கு TTS ஆதரிக்கப்படவில்லை",
|
||||
"TTS not supported for this document": "இந்த ஆவணத்திற்கு TTS ஆதரிக்கப்படவில்லை",
|
||||
"No Timeout": "நேர வரம்பு இல்லை",
|
||||
"{{value}} minute": "{{value}} நிமிடம்",
|
||||
"{{value}} minutes": "{{value}} நிமிடங்கள்",
|
||||
"{{value}} hour": "{{value}} மணிநேரம்",
|
||||
"{{value}} hours": "{{value}} மணிநேரங்கள்",
|
||||
"Voices for {{lang}}": "{{lang}} க்கான குரல்கள்",
|
||||
"Slow": "மெதுவாக",
|
||||
"Fast": "வேகமாக",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: 1 குரல்",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} குரல்கள்",
|
||||
"Sign in to Sync": "ஒத்திசைக்க உள்நுழையவும்",
|
||||
"Synced at {{time}}": "{{time}} இல் ஒத்திசைக்கப்பட்டது",
|
||||
"Never synced": "ஒத்திசைக்கப்படவில்லை",
|
||||
"Reading Progress Synced": "வாசிப்பு முன்னேற்றம் ஒத்திசைக்கப்பட்டது",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "{{total}} இல் {{page}} பக்கம் ({{percentage}}%)",
|
||||
"Current position": "தற்போதைய நிலை",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "தோராயமாக {{total}} இல் {{page}} பக்கம் ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "தோராயமாக {{percentage}}%",
|
||||
"Delete Your Account?": "உங்கள் கணக்கை நீக்கவா?",
|
||||
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "இந்த செயலை மாற்ற முடியாது. cloud இல் உள்ள உங்கள் எல்லா தரவும் நிரந்தரமாக நீக்கப்படும்.",
|
||||
"Delete Permanently": "நிரந்தரமாக நீக்கவும்",
|
||||
"Restore Purchase": "வாங்குதலை மீட்டெடுக்கவும்",
|
||||
"Manage Subscription": "சந்தாவை நிர்வகிக்கவும்",
|
||||
"Reset Password": "கடவுச்சொல் மீட்டமைக்கவும்",
|
||||
"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 create checkout session": "checkout session உருவாக்க முடியவில்லை",
|
||||
"No purchases found to restore.": "மீட்டெடுக்க வாங்குதல்கள் இல்லை.",
|
||||
"Failed to restore purchases.": "வாங்குதல்களை மீட்டெடுக்க முடியவில்லை.",
|
||||
"Failed to manage subscription.": "சந்தாவை நிர்வகிக்க முடியவில்லை.",
|
||||
"Failed to load subscription plans.": "சந்தா திட்டங்களை ஏற்ற முடியவில்லை.",
|
||||
"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:": "சந்தா ID:",
|
||||
"Go to Library": "நூலகத்திற்கு செல்லவும்",
|
||||
"Need help? Contact our support team at support@readest.com": "உதவி தேவையா? support@readest.com இல் எங்கள் ஆதரவு குழுவைத் தொடர்பு கொள்ளவும்",
|
||||
"Free Plan": "இலவச திட்டம்",
|
||||
"month": "மாதம்",
|
||||
"year": "வருடம்",
|
||||
"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": "Cloud ஒத்திசைவு சேமிப்பு",
|
||||
"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 எழுத்துகள் வரை மொழிபெயர்க்கவும்.",
|
||||
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "5 GB cloud சேமிப்புடன் உங்கள் முழு வாசிப்பு சேகரிப்பையும் பாதுகாப்பாக சேமித்து அணுகவும்.",
|
||||
"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 எழுத்துகள் வரை மொழிபெயர்க்கவும்.",
|
||||
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "20 GB cloud சேமிப்புடன் உங்கள் முழு வாசிப்பு சேகரிப்பையும் பாதுகாப்பாக சேமித்து அணுகவும்.",
|
||||
"Version {{version}}": "பதிப்பு {{version}}",
|
||||
"Check Update": "புதுப்பிப்பு சரிபார்க்கவும்",
|
||||
"Already the latest version": "ஏற்கனவே சமீபத்திய பதிப்பு",
|
||||
"Checking for updates...": "புதுப்பிப்புகளுக்கு சரிபார்க்கிறது...",
|
||||
"Error checking for updates": "புதுப்பிப்புகள் சரிபார்க்கும்போது பிழை",
|
||||
"Drop to Import Books": "புத்தகங்களை இறக்குமதி செய்ய இழுத்து விடவும்",
|
||||
"Terms of Service": "சேவை விதிமுறைகள்",
|
||||
"Privacy Policy": "தனியுரிமைக் கொள்கை",
|
||||
"Enter book title": "புத்தக தலைப்பை உள்ளிடவும்",
|
||||
"Subtitle": "துணைத்தலைப்பு",
|
||||
"Enter book subtitle": "புத்தக துணைத்தலைப்பை உள்ளிடவும்",
|
||||
"Enter author name": "ஆசிரியர் பெயரை உள்ளிடவும்",
|
||||
"Series": "தொடர்",
|
||||
"Enter series name": "தொடர் பெயரை உள்ளிடவும்",
|
||||
"Series Index": "தொடர் குறியீடு",
|
||||
"Enter series index": "தொடர் குறியீட்டை உள்ளிடவும்",
|
||||
"Total in Series": "தொடரில் மொத்தம்",
|
||||
"Enter total books in series": "தொடரில் மொத்த புத்தகங்களை உள்ளிடவும்",
|
||||
"Publisher": "வெளியீட்டாளர்",
|
||||
"Enter publisher": "வெளியீட்டாளரை உள்ளிடவும்",
|
||||
"Publication Date": "வெளியீட்டு தேதி",
|
||||
"YYYY or YYYY-MM-DD": "YYYY அல்லது YYYY-MM-DD",
|
||||
"Identifier": "அடையாளங்காட்டி",
|
||||
"Subjects": "பாடங்கள்",
|
||||
"Fiction, Science, History": "கற்பனை, அறிவியல், வரலாறு",
|
||||
"Description": "விளக்கம்",
|
||||
"Enter book description": "புத்தக விளக்கத்தை உள்ளிடவும்",
|
||||
"Change cover image": "அட்டை படத்தை மாற்றவும்",
|
||||
"Replace": "மாற்றவும்",
|
||||
"Unlock cover": "அட்டையை திறக்கவும்",
|
||||
"Lock cover": "அட்டையை பூட்டவும்",
|
||||
"Auto-Retrieve Metadata": "Metadata ஐ தானாக பெறவும்",
|
||||
"Auto-Retrieve": "தானாக பெறவும்",
|
||||
"Unlock all fields": "அனைத்து புலங்களையும் திறக்கவும்",
|
||||
"Unlock All": "அனைத்தையும் திறக்கவும்",
|
||||
"Lock all fields": "அனைத்து புலங்களையும் பூட்டவும்",
|
||||
"Lock All": "அனைத்தையும் பூட்டவும்",
|
||||
"Reset": "மீட்டமைக்கவும்",
|
||||
"Are you sure to delete the selected book?": "தேர்ந்தெடுக்கப்பட்ட புத்தகத்தை நீக்க உறுதியா?",
|
||||
"Are you sure to delete the cloud backup of the selected book?": "தேர்ந்தெடுக்கப்பட்ட புத்தகத்தின் cloud backup ஐ நீக்க உறுதியா?",
|
||||
"Are you sure to delete the local copy of the selected book?": "தேர்ந்தெடுக்கப்பட்ட புத்தகத்தின் உள்ளூர் நகலை நீக்க உறுதியா?",
|
||||
"Edit Metadata": "Metadata ஐ திருத்தவும்",
|
||||
"Book Details": "புத்தக விவரங்கள்",
|
||||
"Unknown": "தெரியாதது",
|
||||
"Remove from Cloud & Device": "Cloud மற்றும் சாதனத்திலிருந்து நீக்கவும்",
|
||||
"Remove from Cloud Only": "Cloud இலிருந்து மட்டும் நீக்கவும்",
|
||||
"Remove from Device Only": "சாதனத்திலிருந்து மட்டும் நீக்கவும்",
|
||||
"Published": "வெளியிடப்பட்டது",
|
||||
"Updated": "புதுப்பிக்கப்பட்டது",
|
||||
"Added": "சேர்க்கப்பட்டது",
|
||||
"File Size": "கோப்பு அளவு",
|
||||
"No description available": "விளக்கம் கிடைக்கவில்லை",
|
||||
"Locked": "பூட்டப்பட்டது",
|
||||
"Select Metadata Source": "Metadata மூலத்தைத் தேர்வுசெய்யவும்",
|
||||
"Keep manual input": "கைமுறை உள்ளீட்டைத் தக்கவைக்கவும்",
|
||||
"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}}": "{{contentLength}} இல் {{downloaded}} பதிவிறக்கம்",
|
||||
"Download finished": "பதிவிறக்கம் முடிந்தது",
|
||||
"DOWNLOAD & INSTALL": "பதிவிறக்கம் மற்றும் நிறுவல்",
|
||||
"Changelog": "மாற்ற பதிவு",
|
||||
"Software Update": "மென்பொருள் புதுப்பிப்பு",
|
||||
"What's New in Readest": "Readest இல் புதியது",
|
||||
"Select Files": "கோப்புகளைத் தேர்வுசெய்யவும்",
|
||||
"Select Image": "படத்தைத் தேர்வுசெய்யவும்",
|
||||
"Select Video": "வீடியோவைத் தேர்வுசெய்யவும்",
|
||||
"Select Audio": "ஆடியோவைத் தேர்வுசெய்யவும்",
|
||||
"Select Fonts": "எழுத்துருக்களைத் தேர்வுசெய்யவும்",
|
||||
"Storage": "சேமிப்பு",
|
||||
"{{percentage}}% of Cloud Sync Space Used.": "Cloud Sync இடத்தில் {{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",
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"Gray": "சாம்பல்",
|
||||
"Sepia": "செபியா",
|
||||
"Grass": "புல்",
|
||||
"Cherry": "செர்ரி",
|
||||
"Sky": "வானம்",
|
||||
"Solarized": "சூரியமயம்",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Nord": "Nord",
|
||||
"Contrast": "மாறுபாடு",
|
||||
"Sunset": "சூரிய அஸ்தமனம்",
|
||||
"Reveal in Finder": "Finder இல் காட்டவும்",
|
||||
"Reveal in File Explorer": "File Explorer இல் காட்டவும்",
|
||||
"Reveal in Folder": "கோப்புறையில் காட்டவும்",
|
||||
"Previous Paragraph": "முந்தைய பத்தி",
|
||||
"Previous Sentence": "முந்தைய வாக்கியம்",
|
||||
"Pause": "இடைநிறுத்தம்",
|
||||
"Play": "இயக்கு",
|
||||
"Next Sentence": "அடுத்த வாக்கியம்",
|
||||
"Next Paragraph": "அடுத்த பத்தி",
|
||||
"Separate Cover Page": "தனித்த அட்டை பக்கம்"
|
||||
}
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "ใช้เวอร์ชันล่าสุดเสมอ",
|
||||
"Send changes only": "ส่งการเปลี่ยนแปลงเท่านั้น",
|
||||
"Receive changes only": "รับการเปลี่ยนแปลงเท่านั้น",
|
||||
"Disabled": "ปิดใช้งาน",
|
||||
"Checksum Method": "วิธีการตรวจสอบความถูกต้อง",
|
||||
"File Content (recommended)": "เนื้อหาไฟล์ (แนะนำ)",
|
||||
"File Name": "ชื่อไฟล์",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "เลือกฟอนต์",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "รูปแบบฟอนต์ที่รองรับ: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "ส่งความคืบหน้า",
|
||||
"Pull Progress": "ดึงความคืบหน้า"
|
||||
"Pull Progress": "ดึงความคืบหน้า",
|
||||
"Previous Paragraph": "ย่อหน้าก่อนหน้า",
|
||||
"Previous Sentence": "ประโยคก่อนหน้า",
|
||||
"Pause": "หยุดชั่วคราว",
|
||||
"Play": "เล่น",
|
||||
"Next Sentence": "ประโยคถัดไป",
|
||||
"Next Paragraph": "ย่อหน้าถัดไป",
|
||||
"Separate Cover Page": "หน้าปกแยกต่างหาก"
|
||||
}
|
||||
|
||||
@@ -478,7 +478,6 @@
|
||||
"Always use latest": "Her Zaman En Sonunu Kullan",
|
||||
"Send changes only": "Sadece Değişiklikleri Gönder",
|
||||
"Receive changes only": "Sadece Değişiklikleri Al",
|
||||
"Disabled": "Devre Dışı",
|
||||
"Checksum Method": "Kontrol Toplamı Yöntemi",
|
||||
"File Content (recommended)": "Dosya İçeriği (önerilen)",
|
||||
"File Name": "Dosya Adı",
|
||||
@@ -516,5 +515,12 @@
|
||||
"Select Fonts": "Yazı Tiplerini Seç",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Desteklenen yazı tipi formatları: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "İlerlemeyi Gönder",
|
||||
"Pull Progress": "İlerlemeyi Al"
|
||||
"Pull Progress": "İlerlemeyi Al",
|
||||
"Previous Paragraph": "Önceki Paragraf",
|
||||
"Previous Sentence": "Önceki Cümle",
|
||||
"Pause": "Duraklat",
|
||||
"Play": "Oynat",
|
||||
"Next Sentence": "Sonraki Cümle",
|
||||
"Next Paragraph": "Sonraki Paragraf",
|
||||
"Separate Cover Page": "Ayrı Kapak Sayfası"
|
||||
}
|
||||
|
||||
@@ -486,7 +486,6 @@
|
||||
"Always use latest": "Завжди використовувати найновішу",
|
||||
"Send changes only": "Відправити тільки зміни",
|
||||
"Receive changes only": "Отримати тільки зміни",
|
||||
"Disabled": "Вимкнено",
|
||||
"Checksum Method": "Метод контролю суми",
|
||||
"File Content (recommended)": "Вміст файлу (рекомендується)",
|
||||
"File Name": "Ім'я файлу",
|
||||
@@ -524,5 +523,12 @@
|
||||
"Select Fonts": "Вибрати Шрифти",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Підтримувані формати шрифтів: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Надіслати прогрес",
|
||||
"Pull Progress": "Отримати прогрес"
|
||||
"Pull Progress": "Отримати прогрес",
|
||||
"Previous Paragraph": "Попередній абзац",
|
||||
"Previous Sentence": "Попереднє речення",
|
||||
"Pause": "Пауза",
|
||||
"Play": "Відтворити",
|
||||
"Next Sentence": "Наступне речення",
|
||||
"Next Paragraph": "Наступний абзац",
|
||||
"Separate Cover Page": "Окрема сторінка обкладинки"
|
||||
}
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "Luôn sử dụng phiên bản mới nhất",
|
||||
"Send changes only": "Chỉ gửi thay đổi",
|
||||
"Receive changes only": "Chỉ nhận thay đổi",
|
||||
"Disabled": "Đã tắt",
|
||||
"Checksum Method": "Phương pháp kiểm tra",
|
||||
"File Content (recommended)": "Nội dung tệp (được khuyến nghị)",
|
||||
"File Name": "Tên tệp",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "Chọn Phông Chữ",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Định dạng phông chữ được hỗ trợ: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Đẩy tiến độ",
|
||||
"Pull Progress": "Kéo tiến độ"
|
||||
"Pull Progress": "Kéo tiến độ",
|
||||
"Previous Paragraph": "Đoạn trước",
|
||||
"Previous Sentence": "Câu trước",
|
||||
"Pause": "Tạm dừng",
|
||||
"Play": "Phát",
|
||||
"Next Sentence": "Câu tiếp theo",
|
||||
"Next Paragraph": "Đoạn tiếp theo",
|
||||
"Separate Cover Page": "Trang Bìa Riêng"
|
||||
}
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "始终使用最新",
|
||||
"Send changes only": "仅发送更改",
|
||||
"Receive changes only": "仅接收更改",
|
||||
"Disabled": "已禁用",
|
||||
"Checksum Method": "校验和方法",
|
||||
"File Content (recommended)": "文件内容(推荐)",
|
||||
"File Name": "文件名",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "选择字体",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "支持的字体格式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "推送阅读进度",
|
||||
"Pull Progress": "拉取阅读进度"
|
||||
"Pull Progress": "拉取阅读进度",
|
||||
"Previous Paragraph": "上一段",
|
||||
"Previous Sentence": "上一句",
|
||||
"Pause": "暂停",
|
||||
"Play": "播放",
|
||||
"Next Sentence": "下一句",
|
||||
"Next Paragraph": "下一段",
|
||||
"Separate Cover Page": "单独封面页"
|
||||
}
|
||||
|
||||
@@ -474,7 +474,6 @@
|
||||
"Always use latest": "始終使用最新",
|
||||
"Send changes only": "僅發送更改",
|
||||
"Receive changes only": "僅接收更改",
|
||||
"Disabled": "已禁用",
|
||||
"Checksum Method": "校驗和方法",
|
||||
"File Content (recommended)": "文件內容(推薦)",
|
||||
"File Name": "文件名",
|
||||
@@ -512,5 +511,12 @@
|
||||
"Select Fonts": "選擇字型",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "支援的字型格式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "推送閱讀進度",
|
||||
"Pull Progress": "拉取閱讀進度"
|
||||
"Pull Progress": "拉取閱讀進度",
|
||||
"Previous Paragraph": "上一段",
|
||||
"Previous Sentence": "上一句",
|
||||
"Pause": "暫停",
|
||||
"Play": "播放",
|
||||
"Next Sentence": "下一句",
|
||||
"Next Paragraph": "下一段",
|
||||
"Separate Cover Page": "單獨封面頁"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.78": {
|
||||
"date": "2025-09-06",
|
||||
"notes": [
|
||||
"TTS: Added option to keep a persistent TTS bar at the bottom of the screen",
|
||||
"TTS: Added previous/next sentence navigation controls in the TTS bar",
|
||||
"TTS: Added media control support on iOS lock screen and AirPods",
|
||||
"TTS: Fixed an issue where headings could not be highlighted for TTS",
|
||||
"Sync: Fixed occasional sync failures between KOReader and Readest",
|
||||
"Fonts: Grouped font styles into custom font families for easier selection",
|
||||
"Reader: Improved text selection sensitivity when using a stylus",
|
||||
"Reader: Books can now be properly closed using the system back button on Android",
|
||||
"EPUB: Fixed parsing to correctly detect the front cover in some EPUB files",
|
||||
"PDF: Added zoom level, zoom mode, and spread mode settings for PDFs",
|
||||
"Reader: Bookmarks can now be edited directly in the sidebar",
|
||||
"Layout: Prevented layout shift when the virtual keyboard appears on Android",
|
||||
"TXT: Improved chapter detection and added support for nested table of contents in TXT files"
|
||||
]
|
||||
},
|
||||
"0.9.76": {
|
||||
"date": "2025-08-30",
|
||||
"notes": [
|
||||
|
||||
@@ -23,6 +23,8 @@ VERSION_CODE=$((10#$MAJOR * 10000 + 10#$MINOR * 1000 + 10#$PATCH))
|
||||
echo "🔢 Computed versionCode: $VERSION_CODE"
|
||||
|
||||
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
|
||||
MANIFEST="./src-tauri/gen/android/app/src/main/AndroidManifest.xml"
|
||||
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
|
||||
if [[ ! -f "$PROPERTIES_FILE" ]]; then
|
||||
echo "❌ File not found: $PROPERTIES_FILE"
|
||||
@@ -36,9 +38,31 @@ mv "$tmpfile" "$PROPERTIES_FILE"
|
||||
|
||||
echo "✅ Updated $PROPERTIES_FILE"
|
||||
|
||||
# --- REMOVE PERMISSION BEFORE BUILD ---
|
||||
if grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST"; then
|
||||
echo "🧹 Removing REQUEST_INSTALL_PACKAGES from AndroidManifest.xml"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"
|
||||
else
|
||||
sed -i "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🚀 Running: pnpm tauri android build"
|
||||
pnpm tauri android build
|
||||
|
||||
# --- ADD PERMISSION BACK AFTER BUILD ---
|
||||
if ! grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST"; then
|
||||
echo "♻️ Restoring REQUEST_INSTALL_PACKAGES in AndroidManifest.xml"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "/android.permission.INTERNET/a\\
|
||||
$PERMISSION_LINE
|
||||
" "$MANIFEST"
|
||||
else
|
||||
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
|
||||
fi
|
||||
fi
|
||||
|
||||
source .env.google-play.local
|
||||
if [[ -z "$GOOGLE_PLAY_JSON_KEY_FILE" ]]; then
|
||||
echo "❌ GOOGLE_PLAY_JSON_KEY_FILE is not set"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer).toEqual({
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[0]',
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[1]',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer).toEqual({
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[1]',
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[2]',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer).toEqual({
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[2]',
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[3]',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -126,9 +126,9 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.pos0!, xpointer.pos1!);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toEqual('/body/DocFragment[3]/body/div/p[0]/text().6');
|
||||
expect(xpointer.pos0).toEqual('/body/DocFragment[3]/body/div/p[0]/text().6');
|
||||
expect(xpointer.pos1).toEqual('/body/DocFragment[3]/body/div/p[1]/text().16');
|
||||
expect(xpointer.xpointer).toEqual('/body/DocFragment[3]/body/div/p[1]/text().6');
|
||||
expect(xpointer.pos0).toEqual('/body/DocFragment[3]/body/div/p[1]/text().6');
|
||||
expect(xpointer.pos1).toEqual('/body/DocFragment[3]/body/div/p[2]/text().16');
|
||||
});
|
||||
|
||||
it('should convert range CFI within same element', () => {
|
||||
@@ -147,8 +147,8 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.pos0!, xpointer.pos1!);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.pos0).toMatch(/p\[0\]/);
|
||||
expect(xpointer.pos1).toMatch(/p\[2\]/);
|
||||
expect(xpointer.pos0).toMatch(/p\[1\]/);
|
||||
expect(xpointer.pos1).toMatch(/p\[3\]/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.xpointer);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[0\]/);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[1\]/);
|
||||
});
|
||||
|
||||
it('should handle elements with IDs', () => {
|
||||
@@ -172,7 +172,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.xpointer);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[1\]\/p\[0\]/);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[2\]\/p\[1\]/);
|
||||
});
|
||||
|
||||
it('should handle inline elements', () => {
|
||||
@@ -181,7 +181,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.xpointer);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[1\]\/p\[1\]/);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[2\]\/p\[2\]/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
});
|
||||
|
||||
it('should convert XPointer to CFI for first element', () => {
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[0]';
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[1]';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
|
||||
// Verify by converting back to XPointer
|
||||
@@ -221,7 +221,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
});
|
||||
|
||||
it('should convert XPointer to CFI for second element', () => {
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[1]';
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[2]';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
|
||||
const backToXPointer = converter.cfiToXPointer(cfi);
|
||||
@@ -229,14 +229,14 @@ describe('CFIToXPointerConverter', () => {
|
||||
});
|
||||
|
||||
it('should convert XPointer with text offset to CFI', () => {
|
||||
const xpointer = '/body/DocFragment[2]/body/div[0]/p[0]/text().6';
|
||||
const xpointer = '/body/DocFragment[2]/body/div[0]/p[1]/text().6';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
expect(cfi).toBe('epubcfi(/6/4!/4/2/2/1:6)');
|
||||
});
|
||||
|
||||
it('should convert range XPointer to CFI', () => {
|
||||
const pos0 = '/body/DocFragment[2]/body/div/p[0]/text().6';
|
||||
const pos1 = '/body/DocFragment[2]/body/div/p[1]/text().16';
|
||||
const pos0 = '/body/DocFragment[2]/body/div/p[1]/text().6';
|
||||
const pos1 = '/body/DocFragment[2]/body/div/p[2]/text().16';
|
||||
const cfi = converter.xPointerToCFI(pos0, pos1);
|
||||
const xpointer = converter.cfiToXPointer(cfi);
|
||||
|
||||
@@ -269,7 +269,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
it('should handle CFI without spine step prefix', () => {
|
||||
// Test the adjustSpineIndex method handles CFIs that don't start with /6/n!
|
||||
const converter = new XCFI(simpleDoc, 3); // Use different spine index
|
||||
const xpointer = '/body/DocFragment[4]/body/div/p[0]';
|
||||
const xpointer = '/body/DocFragment[4]/body/div/p[1]';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
|
||||
// Verify the spine step is correctly added/adjusted
|
||||
@@ -331,7 +331,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const cfi = 'epubcfi(/6/6!/4/2/2)'; // Empty p element
|
||||
const result = converter.cfiToXPointer(cfi);
|
||||
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[0]');
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[1]');
|
||||
});
|
||||
|
||||
it('should handle whitespace-only text nodes', () => {
|
||||
@@ -353,7 +353,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const cfi = 'epubcfi(/6/6!/4/2/4)'; // Second p element
|
||||
const result = converter.cfiToXPointer(cfi);
|
||||
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[1]');
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[2]');
|
||||
});
|
||||
|
||||
it('should handle deeply nested elements', () => {
|
||||
@@ -379,7 +379,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const cfi = 'epubcfi(/6/6!/4/2/2/2/2)'; // Deeply nested p
|
||||
const result = converter.cfiToXPointer(cfi);
|
||||
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/section/article/p[0]');
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/section/article/p[1]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,6 +123,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
bookKey={bookKey}
|
||||
bookDoc={bookDoc}
|
||||
config={config}
|
||||
gridInsets={gridInsets}
|
||||
contentInsets={contentInsets}
|
||||
/>
|
||||
{viewSettings.vertical && viewSettings.scrolled && (
|
||||
|
||||
@@ -46,7 +46,7 @@ import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import ConfirmSyncDialog from './ConfirmSyncDialog';
|
||||
import KOSyncConflictResolver from './KOSyncResolver';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -58,16 +58,18 @@ const FoliateViewer: React.FC<{
|
||||
bookKey: string;
|
||||
bookDoc: BookDoc;
|
||||
config: BookConfig;
|
||||
gridInsets: Insets;
|
||||
contentInsets: Insets;
|
||||
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
|
||||
}> = ({ bookKey, bookDoc, config, gridInsets, contentInsets: insets }) => {
|
||||
const { appService, envConfig } = useEnv();
|
||||
const { themeCode, isDarkMode } = useThemeStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { loadCustomFonts, getLoadedFonts } = useCustomFontStore();
|
||||
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getViewState, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const viewState = getViewState(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
const viewRef = useRef<FoliateView | null>(null);
|
||||
@@ -86,8 +88,7 @@ const FoliateViewer: React.FC<{
|
||||
useUICSS(bookKey);
|
||||
useProgressSync(bookKey);
|
||||
useProgressAutoSave(bookKey);
|
||||
const { syncState, conflictDetails, resolveConflictWithLocal, resolveConflictWithRemote } =
|
||||
useKOSync(bookKey);
|
||||
const { syncState, conflictDetails, resolveWithLocal, resolveWithRemote } = useKOSync(bookKey);
|
||||
useTextTranslation(bookKey, viewRef.current);
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
@@ -255,6 +256,12 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
}
|
||||
|
||||
if (bookDoc.rendition?.layout === 'pre-paginated' && bookDoc.sections) {
|
||||
bookDoc.rendition.spread = viewSettings.spreadMode;
|
||||
const coverSide = bookDoc.dir === 'rtl' ? 'right' : 'left';
|
||||
bookDoc.sections[0]!.pageSpread = viewSettings.keepCoverSpread ? '' : coverSide;
|
||||
}
|
||||
|
||||
await view.open(bookDoc);
|
||||
// make sure we can listen renderer events after opening book
|
||||
viewRef.current = view;
|
||||
@@ -290,9 +297,14 @@ const FoliateViewer: React.FC<{
|
||||
} else {
|
||||
view.renderer.removeAttribute('animated');
|
||||
}
|
||||
view.renderer.setAttribute('max-column-count', maxColumnCount);
|
||||
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
|
||||
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
|
||||
if (bookDoc?.rendition?.layout === 'pre-paginated') {
|
||||
view.renderer.setAttribute('zoom', viewSettings.zoomMode);
|
||||
view.renderer.setAttribute('spread', viewSettings.spreadMode);
|
||||
} else {
|
||||
view.renderer.setAttribute('max-column-count', maxColumnCount);
|
||||
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
|
||||
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
|
||||
}
|
||||
applyMarginAndGap();
|
||||
|
||||
const lastLocation = config.location;
|
||||
@@ -309,6 +321,7 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const applyMarginAndGap = () => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const viewState = getViewState(bookKey);
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
|
||||
@@ -316,7 +329,11 @@ const FoliateViewer: React.FC<{
|
||||
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
|
||||
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
|
||||
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
|
||||
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
|
||||
const ttsBarHeight =
|
||||
viewState?.ttsEnabled && viewSettings.showTTSBar ? 52 + gridInsets.bottom * 0.33 : 0;
|
||||
const moreBottomInset = showBottomFooter
|
||||
? Math.max(0, Math.max(ttsBarHeight, 44) - insets.bottom)
|
||||
: Math.max(0, ttsBarHeight);
|
||||
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
|
||||
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
|
||||
const topMargin = (showTopHeader ? insets.top : viewInsets.top) + moreTopInset;
|
||||
@@ -380,6 +397,8 @@ const FoliateViewer: React.FC<{
|
||||
viewSettings?.doubleBorder,
|
||||
viewSettings?.showHeader,
|
||||
viewSettings?.showFooter,
|
||||
viewSettings?.showTTSBar,
|
||||
viewState?.ttsEnabled,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -392,11 +411,11 @@ const FoliateViewer: React.FC<{
|
||||
/>
|
||||
{!docLoaded.current && loading && <Spinner loading={true} />}
|
||||
{syncState === 'conflict' && conflictDetails && (
|
||||
<ConfirmSyncDialog
|
||||
<KOSyncConflictResolver
|
||||
details={conflictDetails}
|
||||
onConfirmLocal={resolveConflictWithLocal}
|
||||
onConfirmRemote={resolveConflictWithRemote}
|
||||
onClose={resolveConflictWithLocal}
|
||||
onResolveWithLocal={resolveWithLocal}
|
||||
onResolveWithRemote={resolveWithRemote}
|
||||
onClose={resolveWithLocal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -183,7 +183,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'footer-bar shadow-xs bottom-0 z-50 flex w-full flex-col',
|
||||
'footer-bar shadow-xs bottom-0 z-30 flex w-full flex-col',
|
||||
'sm:h-[52px] sm:justify-center',
|
||||
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
|
||||
'transition-[opacity,transform] duration-300',
|
||||
@@ -211,7 +211,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
@@ -265,7 +265,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
@@ -304,10 +304,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
'bg-base-200 z-30 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom + 16}px` : '0px',
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom * 0.33 + 16}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -337,7 +337,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom / 2}px` : '0px',
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -392,7 +392,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TTSControl bookKey={bookKey} />
|
||||
<TTSControl bookKey={bookKey} gridInsets={gridInsets} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -148,7 +148,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
!windowButtonVisible && 'max-w-[50%]',
|
||||
)}
|
||||
>
|
||||
{bookTitle} {bookTitle} {bookTitle} {bookTitle}
|
||||
{bookTitle}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
+12
-9
@@ -3,17 +3,17 @@ import Dialog from '@/components/Dialog';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { SyncDetails } from '../hooks/useKOSync';
|
||||
|
||||
interface ConfirmSyncDialogProps {
|
||||
interface KOSyncConflictResolverProps {
|
||||
details: SyncDetails | null;
|
||||
onConfirmLocal: () => void;
|
||||
onConfirmRemote: () => void;
|
||||
onResolveWithLocal: () => void;
|
||||
onResolveWithRemote: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ConfirmSyncDialog: React.FC<ConfirmSyncDialogProps> = ({
|
||||
const KOSyncConflictResolver: React.FC<KOSyncConflictResolverProps> = ({
|
||||
details,
|
||||
onConfirmLocal,
|
||||
onConfirmRemote,
|
||||
onResolveWithLocal,
|
||||
onResolveWithRemote,
|
||||
onClose,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
@@ -28,7 +28,10 @@ const ConfirmSyncDialog: React.FC<ConfirmSyncDialogProps> = ({
|
||||
})}
|
||||
</p>
|
||||
<div className='mt-4 space-y-4'>
|
||||
<button className='btn h-auto w-full flex-col items-start py-2' onClick={onConfirmLocal}>
|
||||
<button
|
||||
className='btn h-auto w-full flex-col items-start py-2'
|
||||
onClick={onResolveWithLocal}
|
||||
>
|
||||
<span>{_('Local Progress')}</span>
|
||||
<span className='text-xs font-normal normal-case text-gray-500'>
|
||||
{details.local.preview}
|
||||
@@ -36,7 +39,7 @@ const ConfirmSyncDialog: React.FC<ConfirmSyncDialogProps> = ({
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-primary h-auto w-full flex-col items-start py-2'
|
||||
onClick={onConfirmRemote}
|
||||
onClick={onResolveWithRemote}
|
||||
>
|
||||
<span>{_('Remote Progress')}</span>
|
||||
<span className='text-xs font-normal normal-case'>{details.remote.preview}</span>
|
||||
@@ -46,4 +49,4 @@ const ConfirmSyncDialog: React.FC<ConfirmSyncDialogProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSyncDialog;
|
||||
export default KOSyncConflictResolver;
|
||||
@@ -62,7 +62,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
className={clsx(
|
||||
'progressinfo absolute bottom-0 flex items-center justify-between',
|
||||
'text-neutral-content font-sans text-xs font-extralight',
|
||||
isVertical ? 'writing-vertical-rl' : 'h-[52px] w-full',
|
||||
isVertical ? 'writing-vertical-rl' : 'w-full',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={
|
||||
@@ -78,16 +78,23 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
: {
|
||||
paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
|
||||
paddingInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.67}px` : 0,
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
{viewSettings.showRemainingTime ? (
|
||||
<span className='text-start'>{timeLeft}</span>
|
||||
) : viewSettings.showRemainingPages ? (
|
||||
<span className='text-start'>{pageLeft}</span>
|
||||
) : null}
|
||||
{viewSettings.showProgressInfo && <span className='ms-auto text-end'>{progressInfo}</span>}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center',
|
||||
isVertical ? 'h-full' : 'h-[52px] w-full',
|
||||
)}
|
||||
>
|
||||
{viewSettings.showRemainingTime ? (
|
||||
<span className='text-start'>{timeLeft}</span>
|
||||
) : viewSettings.showRemainingPages ? (
|
||||
<span className='text-start'>{pageLeft}</span>
|
||||
) : null}
|
||||
{viewSettings.showProgressInfo && <span className='ms-auto text-end'>{progressInfo}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
@@ -27,13 +28,35 @@ import { getLocale } from '@/utils/misc';
|
||||
import { initDayjs } from '@/utils/time';
|
||||
import ReaderContent from './ReaderContent';
|
||||
|
||||
/*
|
||||
Z-Index Layering Guide:
|
||||
---------------------------------
|
||||
99 – Window Border (Linux only)
|
||||
• Ensures the border stays on top of all UI elements.
|
||||
50 – Loading Progress / Toast Notifications / Dialogs
|
||||
• Includes Settings, About, Updater, and KOSync dialogs.
|
||||
45 – Sidebar / Notebook (Unpinned)
|
||||
• Floats above the content but below global dialogs.
|
||||
40 – TTS Bar
|
||||
• Mini controls for TTS playback on top of the TTS Control.
|
||||
30 – Footbar (TTS Control)
|
||||
• Persistent bottom controls and the TTS icon/panel.
|
||||
20 – Menu / Sidebar / Notebook (Pinned)
|
||||
• Docked navigation or note views.
|
||||
10 – Headerbar / Ribbon
|
||||
• Top toolbar and ribbon elements.
|
||||
0 – Base Content
|
||||
• Main reading area or background content.
|
||||
*/
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isSideBarVisible, getIsSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, getIsNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
@@ -53,8 +76,14 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
|
||||
const handleKeyDown = (event: CustomEvent) => {
|
||||
if (event.detail.keyName === 'Back') {
|
||||
setSideBarVisible(false);
|
||||
setNotebookVisible(false);
|
||||
if (getIsSideBarVisible()) {
|
||||
setSideBarVisible(false);
|
||||
} else if (getIsNotebookVisible()) {
|
||||
setNotebookVisible(false);
|
||||
} else {
|
||||
eventDispatcher.dispatch('close-reader');
|
||||
router.back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -62,22 +91,23 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isAndroidApp) return;
|
||||
if (isSideBarVisible || isNotebookVisible) {
|
||||
acquireBackKeyInterception();
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
if (!isSideBarVisible && !isNotebookVisible) {
|
||||
acquireBackKeyInterception();
|
||||
return () => {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService?.isAndroidApp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isAndroidApp) return;
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
return () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSideBarVisible, isNotebookVisible]);
|
||||
}, [appService?.isAndroidApp, isSideBarVisible, isNotebookVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
|
||||
@@ -67,10 +67,12 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
if (index === 0) setSideBarBookKey(key);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowBookDetails = (event: CustomEvent) => {
|
||||
const book = event.detail as Book;
|
||||
setShowDetailsBook(book);
|
||||
setShowDetailsBook(event.detail as Book);
|
||||
return true;
|
||||
};
|
||||
eventDispatcher.onSync('show-book-details', handleShowBookDetails);
|
||||
@@ -78,7 +80,6 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
return () => {
|
||||
eventDispatcher.offSync('show-book-details', handleShowBookDetails);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -94,10 +95,12 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
}
|
||||
window.addEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.on('beforereload', handleCloseBooks);
|
||||
eventDispatcher.on('close-reader', handleCloseBooks);
|
||||
eventDispatcher.on('quit-app', handleCloseBooks);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.off('beforereload', handleCloseBooks);
|
||||
eventDispatcher.off('close-reader', handleCloseBooks);
|
||||
eventDispatcher.off('quit-app', handleCloseBooks);
|
||||
unlistenOnCloseWindow?.then((fn) => fn());
|
||||
};
|
||||
|
||||
@@ -6,6 +6,9 @@ import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
|
||||
import { MdSync, MdSyncProblem } from 'react-icons/md';
|
||||
import { IoMdExpand } from 'react-icons/io';
|
||||
import { TbArrowAutofitWidth } from 'react-icons/tb';
|
||||
import { TbColumns1, TbColumns2 } from 'react-icons/tb';
|
||||
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -37,14 +40,18 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { getConfig } = useBookDataStore();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const { themeMode, isDarkMode, setThemeMode } = useThemeStore();
|
||||
const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled);
|
||||
const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!);
|
||||
const [zoomMode, setZoomMode] = useState(viewSettings!.zoomMode!);
|
||||
const [spreadMode, setSpreadMode] = useState(viewSettings!.spreadMode!);
|
||||
const [keepCoverSpread, setKeepCoverSpread] = useState(viewSettings!.keepCoverSpread!);
|
||||
const [invertImgColorInDark, setInvertImgColorInDark] = useState(
|
||||
viewSettings!.invertImgColorInDark,
|
||||
);
|
||||
@@ -93,6 +100,9 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'zoomLevel', zoomLevel, true, true);
|
||||
if (bookData.bookDoc?.rendition?.layout === 'pre-paginated') {
|
||||
getView(bookKey)?.renderer.setAttribute('scale-factor', zoomLevel);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [zoomLevel]);
|
||||
|
||||
@@ -102,6 +112,36 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [invertImgColorInDark]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zoomMode === viewSettings.zoomMode) return;
|
||||
viewSettings.zoomMode = zoomMode;
|
||||
getView(bookKey)?.renderer.setAttribute('zoom', zoomMode);
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
saveViewSettings(envConfig, bookKey, 'zoomMode', zoomMode, true, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [zoomMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spreadMode === viewSettings.spreadMode) return;
|
||||
viewSettings.spreadMode = spreadMode;
|
||||
getView(bookKey)?.renderer.setAttribute('spread', spreadMode);
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
saveViewSettings(envConfig, bookKey, 'spreadMode', spreadMode, true, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [spreadMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keepCoverSpread === viewSettings.keepCoverSpread) return;
|
||||
if (!bookData?.bookDoc?.sections?.length) return;
|
||||
viewSettings.keepCoverSpread = keepCoverSpread;
|
||||
const coverSide = bookData.bookDoc.dir === 'rtl' ? 'right' : 'left';
|
||||
bookData.bookDoc.sections[0]!.pageSpread = keepCoverSpread ? '' : coverSide;
|
||||
getView(bookKey)?.renderer.setAttribute('spread', spreadMode);
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
saveViewSettings(envConfig, bookKey, 'keepCoverSpread', keepCoverSpread, true, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [keepCoverSpread]);
|
||||
|
||||
const lastSyncTime = Math.max(config?.lastSyncedAtConfig || 0, config?.lastSyncedAtNotes || 0);
|
||||
|
||||
return (
|
||||
@@ -145,6 +185,56 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{bookData.bookDoc?.rendition?.layout === 'pre-paginated' && (
|
||||
<>
|
||||
<div className={clsx('my-2 flex items-center justify-between rounded-md')}>
|
||||
<button
|
||||
onClick={setSpreadMode.bind(null, 'none')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
spreadMode === 'none' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<TbColumns1 />
|
||||
</button>
|
||||
<button
|
||||
onClick={setSpreadMode.bind(null, 'auto')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
spreadMode === 'auto' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<TbColumns2 />
|
||||
</button>
|
||||
<div className='bg-base-300 mx-2 h-6 w-[1px]' />
|
||||
<button
|
||||
onClick={setZoomMode.bind(null, 'fit-page')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomMode === 'fit-page' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<IoMdExpand />
|
||||
</button>
|
||||
<button
|
||||
onClick={setZoomMode.bind(null, 'fit-width')}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomMode === 'fit-width' && 'bg-base-300/75',
|
||||
)}
|
||||
>
|
||||
<TbArrowAutofitWidth />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MenuItem
|
||||
label={_('Separate Cover Page')}
|
||||
Icon={keepCoverSpread ? MdCheck : undefined}
|
||||
onClick={() => setKeepCoverSpread(!keepCoverSpread)}
|
||||
disabled={spreadMode === 'none'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<hr className='border-base-300 my-1' />
|
||||
|
||||
<MenuItem label={_('Font & Layout')} shortcut='Shift+F' onClick={openFontLayoutMenu} />
|
||||
@@ -154,6 +244,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
shortcut='Shift+J'
|
||||
Icon={isScrolledMode ? MdCheck : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
disabled={bookData.bookDoc?.rendition?.layout === 'pre-paginated'}
|
||||
/>
|
||||
|
||||
<hr className='border-base-300 my-1' />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
@@ -7,6 +6,8 @@ import { TextSelection } from '@/utils/sel';
|
||||
import { md5Fingerprint } from '@/utils/md5';
|
||||
import { BookNote } from '@/types/book';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import TextEditor, { TextEditorRef } from '@/components/TextEditor';
|
||||
import TextButton from '@/components/TextButton';
|
||||
|
||||
interface NoteEditorProps {
|
||||
onSave: (selection: TextSelection, note: string) => void;
|
||||
@@ -23,110 +24,96 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
saveNotebookAnnotationDraft,
|
||||
getNotebookAnnotationDraft,
|
||||
} = useNotebookStore();
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [note, setNote] = React.useState('');
|
||||
|
||||
const editorRef = useRef<TextEditorRef>(null);
|
||||
const [note, setNote] = useState('');
|
||||
const separatorWidth = useResponsiveSize(3);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, [editorRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (notebookEditAnnotation) {
|
||||
setNote(notebookEditAnnotation.note);
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = notebookEditAnnotation.note;
|
||||
editorRef.current.focus();
|
||||
adjustHeight();
|
||||
}
|
||||
const noteText = notebookEditAnnotation.note;
|
||||
setNote(noteText);
|
||||
editorRef.current?.setValue(noteText);
|
||||
editorRef.current?.focus();
|
||||
} else if (notebookNewAnnotation) {
|
||||
const noteText = getAnnotationText();
|
||||
if (noteText) {
|
||||
const draftNote = getNotebookAnnotationDraft(md5Fingerprint(noteText)) || '';
|
||||
setNote(draftNote);
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = draftNote;
|
||||
editorRef.current.focus();
|
||||
adjustHeight();
|
||||
}
|
||||
editorRef.current?.setValue(draftNote);
|
||||
editorRef.current?.focus();
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [notebookNewAnnotation, notebookEditAnnotation]);
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.style.height = 'auto';
|
||||
editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const getAnnotationText = () => {
|
||||
return notebookEditAnnotation?.text || notebookNewAnnotation?.text || '';
|
||||
};
|
||||
|
||||
const handleOnChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
adjustHeight();
|
||||
setNote(e.currentTarget.value);
|
||||
const handleNoteChange = (value: string) => {
|
||||
setNote(value);
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
if (editorRef.current && editorRef.current.value) {
|
||||
const handleBlur = () => {
|
||||
const currentValue = editorRef.current?.getValue();
|
||||
if (currentValue) {
|
||||
const noteText = getAnnotationText();
|
||||
if (noteText) {
|
||||
saveNotebookAnnotationDraft(md5Fingerprint(noteText), editorRef.current.value);
|
||||
saveNotebookAnnotationDraft(md5Fingerprint(noteText), currentValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveNote = () => {
|
||||
if (editorRef.current && notebookNewAnnotation) {
|
||||
onSave(notebookNewAnnotation, editorRef.current.value);
|
||||
} else if (editorRef.current && notebookEditAnnotation) {
|
||||
notebookEditAnnotation.note = editorRef.current.value;
|
||||
onEdit(notebookEditAnnotation);
|
||||
const currentValue = editorRef.current?.getValue();
|
||||
if (currentValue) {
|
||||
if (notebookNewAnnotation) {
|
||||
onSave(notebookNewAnnotation, currentValue);
|
||||
} else if (notebookEditAnnotation) {
|
||||
notebookEditAnnotation.note = currentValue;
|
||||
onEdit(notebookEditAnnotation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
if (notebookNewAnnotation) {
|
||||
setNotebookNewAnnotation(null);
|
||||
}
|
||||
if (notebookEditAnnotation) {
|
||||
setNotebookEditAnnotation(null);
|
||||
}
|
||||
};
|
||||
|
||||
useShortcuts({
|
||||
onSaveNote: () => {
|
||||
if (editorRef.current && editorRef.current.value) {
|
||||
const currentValue = editorRef.current?.getValue();
|
||||
if (currentValue) {
|
||||
handleSaveNote();
|
||||
}
|
||||
},
|
||||
onEscape: () => {
|
||||
if (notebookNewAnnotation) {
|
||||
setNotebookNewAnnotation(null);
|
||||
}
|
||||
if (notebookEditAnnotation) {
|
||||
setNotebookEditAnnotation(null);
|
||||
}
|
||||
},
|
||||
onEscape: handleEscape,
|
||||
});
|
||||
|
||||
const canSave = Boolean(note.trim());
|
||||
|
||||
return (
|
||||
<div className='content note-editor-container bg-base-100 mt-2 rounded-md p-2'>
|
||||
<div className='flex w-full justify-between space-x-2'>
|
||||
<div className='relative w-full'>
|
||||
<textarea
|
||||
className={clsx(
|
||||
'note-editor textarea textarea-ghost min-h-[1em] resize-none !outline-none',
|
||||
'inset-0 w-full rounded-none border-0 bg-transparent p-0',
|
||||
'content font-size-sm',
|
||||
)}
|
||||
dir='auto'
|
||||
ref={editorRef}
|
||||
value={note}
|
||||
rows={1}
|
||||
spellCheck={false}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleOnBlur}
|
||||
placeholder={_('Add your notes here...')}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className='flex w-full'>
|
||||
<TextEditor
|
||||
ref={editorRef}
|
||||
value={note}
|
||||
onChange={handleNoteChange}
|
||||
onBlur={handleBlur}
|
||||
onSave={handleSaveNote}
|
||||
onEscape={handleEscape}
|
||||
placeholder={_('Add your notes here...')}
|
||||
autoFocus={true}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center pt-2'>
|
||||
<div
|
||||
className='me-2 mt-0.5 min-h-full self-stretch rounded-xl bg-gray-300'
|
||||
@@ -138,17 +125,12 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
<span className='content font-size-xs text-gray-500'>{getAnnotationText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-end p-2' dir='ltr'>
|
||||
<button
|
||||
className={clsx(
|
||||
'content btn btn-ghost font-size-sm hover:bg-transparent',
|
||||
'flex h-[1.3em] min-h-[1.3em] items-end p-0',
|
||||
editorRef.current && editorRef.current.value ? '' : 'btn-disabled !bg-opacity-0',
|
||||
)}
|
||||
onClick={handleSaveNote}
|
||||
>
|
||||
<div className='font-size-sm pr-1 align-bottom text-blue-500'>{_('Save')}</div>
|
||||
</button>
|
||||
|
||||
<div className='flex justify-end space-x-3 p-2' dir='ltr'>
|
||||
<TextButton onClick={handleEscape}>{_('Cancel')}</TextButton>
|
||||
<TextButton onClick={handleSaveNote} disabled={!canSave}>
|
||||
{_('Save')}
|
||||
</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -189,15 +189,15 @@ const Notebook: React.FC = ({}) => {
|
||||
return isNotebookVisible ? (
|
||||
<>
|
||||
{!isNotebookPinned && (
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/20' onClick={handleClickOverlay} />
|
||||
<div className='overlay fixed inset-0 z-[45] bg-black/20' onClick={handleClickOverlay} />
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 flex min-w-60 select-none flex-col',
|
||||
'notebook-container bg-base-200 right-0 flex min-w-60 select-none flex-col',
|
||||
'font-sans text-base font-normal sm:text-sm',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
|
||||
!isNotebookPinned && 'shadow-2xl',
|
||||
isNotebookPinned ? 'z-20' : 'z-[45] shadow-2xl',
|
||||
)}
|
||||
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
|
||||
style={{
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCustomFontStore } from '@/store/customFontStore';
|
||||
import { useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { mountCustomFont } from '@/styles/fonts';
|
||||
import { parseFontName } from '@/utils/font';
|
||||
import { CustomFont, mountCustomFont } from '@/styles/fonts';
|
||||
import { parseFontInfo } from '@/utils/font';
|
||||
import { getFilename } from '@/utils/path';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
|
||||
@@ -17,6 +17,11 @@ interface CustomFontsProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
type FontFamily = {
|
||||
name: string;
|
||||
fonts: CustomFont[];
|
||||
};
|
||||
|
||||
const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService, envConfig } = useEnv();
|
||||
@@ -62,12 +67,14 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const fontName = parseFontName(await fontFile.arrayBuffer(), fontPath);
|
||||
const fontInfo = parseFontInfo(await fontFile.arrayBuffer(), fontPath);
|
||||
const customFont = addFont(fontPath, {
|
||||
name: fontName.name,
|
||||
family: fontName.family,
|
||||
style: fontName.style,
|
||||
name: fontInfo.name,
|
||||
family: fontInfo.family,
|
||||
style: fontInfo.style,
|
||||
weight: fontInfo.weight,
|
||||
});
|
||||
console.log('Added custom font:', customFont);
|
||||
if (customFont && !customFont.error) {
|
||||
const loadedFont = await loadFont(envConfig, customFont.id);
|
||||
mountCustomFont(document, loadedFont);
|
||||
@@ -77,27 +84,25 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteFont = (fontId: string) => {
|
||||
const font = customFonts.find((f) => f.id === fontId);
|
||||
if (font) {
|
||||
if (removeFont(fontId)) {
|
||||
appService!.fs.removeFile(font.path, 'Fonts');
|
||||
saveCustomFonts(envConfig);
|
||||
if (getAvailableFonts().length === 0) {
|
||||
setIsDeleteMode(false);
|
||||
const handleDeleteFamily = (family: FontFamily) => {
|
||||
for (const font of family.fonts) {
|
||||
if (font) {
|
||||
if (removeFont(font.id)) {
|
||||
appService!.fs.removeFile(font.path, 'Fonts');
|
||||
saveCustomFonts(envConfig);
|
||||
if (getAvailableFonts().length === 0) {
|
||||
setIsDeleteMode(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFont = (fontId: string) => {
|
||||
const font = customFonts.find((f) => f.id === fontId);
|
||||
if (font) {
|
||||
if (currentDefaultFont === 'serif') {
|
||||
saveViewSettings(envConfig, bookKey, 'serifFont', font.name);
|
||||
} else {
|
||||
saveViewSettings(envConfig, bookKey, 'sansSerifFont', font.name);
|
||||
}
|
||||
const handleSelectFamily = (family: FontFamily) => {
|
||||
if (currentDefaultFont === 'serif') {
|
||||
saveViewSettings(envConfig, bookKey, 'serifFont', family.name);
|
||||
} else {
|
||||
saveViewSettings(envConfig, bookKey, 'sansSerifFont', family.name);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,10 +110,29 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
setIsDeleteMode(!isDeleteMode);
|
||||
};
|
||||
|
||||
const getAvailableFamilies = (fonts: CustomFont[]): FontFamily[] => {
|
||||
const familyMap = new Map<string, string[]>();
|
||||
|
||||
for (const font of fonts) {
|
||||
const family = font.family || font.name;
|
||||
if (!familyMap.has(family)) {
|
||||
familyMap.set(family, []);
|
||||
}
|
||||
familyMap.get(family)!.push(font.id);
|
||||
}
|
||||
|
||||
return Array.from(familyMap.entries()).map(([family, ids]) => ({
|
||||
name: family,
|
||||
fonts: ids.map((id) => fonts.find((f) => f.id === id)!).filter((f): f is CustomFont => !!f),
|
||||
}));
|
||||
};
|
||||
|
||||
const availableFonts = customFonts
|
||||
.filter((font) => !font.deletedAt)
|
||||
.sort((a, b) => (b.downloadedAt || 0) - (a.downloadedAt || 0));
|
||||
|
||||
const availableFamilies = getAvailableFamilies(availableFonts);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className='mb-6 flex h-8 items-center justify-between'>
|
||||
@@ -157,31 +181,31 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{availableFonts.map((font) => (
|
||||
{availableFamilies.map((family) => (
|
||||
<div
|
||||
key={font.id}
|
||||
key={family.name}
|
||||
className={clsx(
|
||||
'card h-12 border shadow-sm',
|
||||
currentFontFamily === font.name
|
||||
currentFontFamily === family.name
|
||||
? 'border-primary/50 bg-primary/50'
|
||||
: `border-base-200 bg-base-200 ${isDeleteMode ? '' : 'cursor-pointer'}`,
|
||||
)}
|
||||
onClick={!isDeleteMode ? () => handleSelectFont(font.id) : undefined}
|
||||
title={font.name}
|
||||
onClick={!isDeleteMode ? () => handleSelectFamily(family) : undefined}
|
||||
title={family.fonts.map((f) => f.name).join('\n')}
|
||||
>
|
||||
<div className='card-body flex items-center justify-center p-2'>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.loaded ? `"${font.name}", sans-serif` : 'sans-serif',
|
||||
fontFamily: `"${family.name}", sans-serif`,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
className='text-base-content line-clamp-1 break-all'
|
||||
>
|
||||
{font.family || font.name}
|
||||
{family.name}
|
||||
</div>
|
||||
{isDeleteMode && (
|
||||
<button
|
||||
onClick={() => handleDeleteFont(font.id)}
|
||||
onClick={() => handleDeleteFamily(family)}
|
||||
className='btn btn-ghost btn-xs absolute right-[-10px] top-[-10px] h-6 min-h-0 w-6 p-0 hover:bg-transparent'
|
||||
title={_('Delete Font')}
|
||||
>
|
||||
|
||||
@@ -104,7 +104,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span
|
||||
className='line-clamp-1 overflow-visible break-all leading-loose'
|
||||
className='line-clamp-1 break-all leading-loose'
|
||||
style={{
|
||||
fontFamily: onGetFontFamily(selectedOption.option, family ?? ''),
|
||||
}}
|
||||
|
||||
@@ -74,7 +74,7 @@ const FontFace = ({
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className='min-w-10'>{label}</span>
|
||||
<span className='line-clamp-2 min-w-10'>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options.map((option) => ({ option, label: _(option) }))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -13,6 +13,8 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import useScrollToItem from '../../hooks/useScrollToItem';
|
||||
import TextButton from '@/components/TextButton';
|
||||
import TextEditor, { TextEditorRef } from '@/components/TextEditor';
|
||||
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
@@ -26,9 +28,13 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
const { getProgress, getView, getViewsById } = useReaderStore();
|
||||
const { setNotebookEditAnnotation, setNotebookVisible } = useNotebookStore();
|
||||
const separatorWidth = useResponsiveSize(3);
|
||||
|
||||
const { text, cfi, note } = item;
|
||||
const editorRef = useRef<TextEditorRef>(null);
|
||||
const editorDraftRef = useRef<string>(text || '');
|
||||
const [inlineEditMode, setInlineEditMode] = useState(false);
|
||||
const separatorWidth = useResponsiveSize(3);
|
||||
|
||||
const progress = getProgress(bookKey);
|
||||
const { isCurrent, viewRef } = useScrollToItem(cfi, progress);
|
||||
|
||||
@@ -65,6 +71,57 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
setNotebookEditAnnotation(note);
|
||||
};
|
||||
|
||||
const editBookmark = () => {
|
||||
setInlineEditMode(true);
|
||||
};
|
||||
|
||||
const handleSaveBookmark = () => {
|
||||
setInlineEditMode(false);
|
||||
const config = getConfig(bookKey);
|
||||
if (!config || !editorDraftRef.current) return;
|
||||
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex((annotation) => item.id === annotation.id);
|
||||
if (existingIndex === -1) return;
|
||||
annotations[existingIndex]!.updatedAt = Date.now();
|
||||
annotations[existingIndex]!.text = editorDraftRef.current;
|
||||
const updatedConfig = updateBooknotes(bookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
};
|
||||
|
||||
if (inlineEditMode) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-base-300 content group relative my-2 cursor-pointer rounded-lg p-2',
|
||||
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/55 bg-base-100',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
)}
|
||||
>
|
||||
<div className='flex w-full'>
|
||||
<TextEditor
|
||||
className='!leading-normal'
|
||||
ref={editorRef}
|
||||
value={editorDraftRef.current}
|
||||
onChange={(value) => (editorDraftRef.current = value)}
|
||||
onSave={handleSaveBookmark}
|
||||
onEscape={() => setInlineEditMode(false)}
|
||||
autoFocus={true}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end space-x-3 p-2' dir='ltr'>
|
||||
<TextButton onClick={() => setInlineEditMode(false)}>{_('Cancel')}</TextButton>
|
||||
<TextButton onClick={handleSaveBookmark} disabled={!editorDraftRef.current}>
|
||||
{_('Save')}
|
||||
</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={viewRef}
|
||||
@@ -120,7 +177,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'max-h-0 overflow-hidden p-0 text-xs',
|
||||
'max-h-0 overflow-hidden p-0',
|
||||
'transition-[max-height] duration-300 ease-in-out',
|
||||
'group-hover:max-h-8 group-hover:overflow-visible',
|
||||
)}
|
||||
@@ -138,46 +195,23 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center justify-end space-x-3 p-2' dir='ltr'>
|
||||
{item.note && (
|
||||
<div
|
||||
className={clsx(
|
||||
'content settings-content cursor-pointer',
|
||||
'flex items-end p-0 hover:bg-transparent',
|
||||
)}
|
||||
onClick={editNote.bind(null, item)}
|
||||
{(item.note || item.type === 'bookmark') && (
|
||||
<TextButton
|
||||
onClick={item.type === 'bookmark' ? editBookmark : editNote.bind(null, item)}
|
||||
variant='primary'
|
||||
className='opacity-0 transition duration-300 ease-in-out group-hover:opacity-100'
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'align-bottom text-blue-500',
|
||||
'transition duration-300 ease-in-out',
|
||||
'content font-size-sm',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'hover:text-blue-600',
|
||||
)}
|
||||
>
|
||||
{_('Edit')}
|
||||
</div>
|
||||
</div>
|
||||
{_('Edit')}
|
||||
</TextButton>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'content settings-content cursor-pointer',
|
||||
'flex items-end p-0 hover:bg-transparent',
|
||||
)}
|
||||
|
||||
<TextButton
|
||||
onClick={deleteNote.bind(null, item)}
|
||||
variant='danger'
|
||||
className='opacity-0 transition duration-300 ease-in-out group-hover:opacity-100'
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'align-bottom text-red-500',
|
||||
'transition duration-300 ease-in-out',
|
||||
'content font-size-sm',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'hover:text-red-600',
|
||||
)}
|
||||
>
|
||||
{_('Delete')}
|
||||
</div>
|
||||
</div>
|
||||
{_('Delete')}
|
||||
</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
@@ -16,11 +18,14 @@ const SidebarContent: React.FC<{
|
||||
sideBarBookKey: string;
|
||||
}> = ({ bookDoc, sideBarBookKey }) => {
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const { setSideBarVisible } = useSidebarStore();
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const [activeTab, setActiveTab] = useState(config?.viewSettings?.sideBarTab || 'toc');
|
||||
const [fade, setFade] = useState(false);
|
||||
const [targetTab, setTargetTab] = useState(activeTab);
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
@@ -32,8 +37,13 @@ const SidebarContent: React.FC<{
|
||||
const handleTabChange = (tab: string) => {
|
||||
setFade(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setFade(false);
|
||||
if (activeTab === tab && isMobile) {
|
||||
setHoveredBookKey(sideBarBookKey);
|
||||
setSideBarVisible(false);
|
||||
return;
|
||||
}
|
||||
setTargetTab(tab);
|
||||
setFade(false);
|
||||
setConfig(sideBarBookKey!, config);
|
||||
clearTimeout(timeout);
|
||||
}, 300);
|
||||
|
||||
@@ -181,13 +181,19 @@ const SideBar: React.FC<{
|
||||
|
||||
return isSideBarVisible ? (
|
||||
<>
|
||||
{!isSideBarPinned && (
|
||||
<div
|
||||
className='overlay fixed inset-0 z-[45] bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 flex min-w-60 select-none flex-col',
|
||||
'sidebar-container bg-base-200 flex min-w-60 select-none flex-col',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
'transition-[padding-top] duration-300',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isSideBarPinned && 'shadow-2xl',
|
||||
isSideBarPinned ? 'z-20' : 'z-[45] shadow-2xl',
|
||||
)}
|
||||
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
|
||||
style={{
|
||||
@@ -266,12 +272,6 @@ const SideBar: React.FC<{
|
||||
onTouchStart={handleHorizontalDragStart}
|
||||
></div>
|
||||
</div>
|
||||
{!isSideBarPinned && (
|
||||
<div
|
||||
className='overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const TabNavigation: React.FC<{
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-300 absolute bottom-1.5 start-1 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
|
||||
'bg-base-300 absolute bottom-1.5 start-1 z-10 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
|
||||
'transform transition-transform duration-300',
|
||||
activeTab === 'toc' && 'translate-x-0',
|
||||
activeTab === 'annotations' && 'translate-x-[calc(100%+8px)]',
|
||||
@@ -36,7 +36,7 @@ const TabNavigation: React.FC<{
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab}
|
||||
className='lg:tooltip lg:tooltip-top z-50 m-1.5 flex-1 cursor-pointer rounded-md p-2'
|
||||
className='lg:tooltip lg:tooltip-top z-20 m-1.5 flex-1 cursor-pointer rounded-md p-2'
|
||||
data-tip={
|
||||
tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import {
|
||||
MdPlayArrow,
|
||||
MdOutlinePause,
|
||||
MdFastRewind,
|
||||
MdFastForward,
|
||||
MdSkipPrevious,
|
||||
MdSkipNext,
|
||||
} from 'react-icons/md';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
type TTSBarProps = {
|
||||
bookKey: string;
|
||||
isPlaying: boolean;
|
||||
onTogglePlay: () => void;
|
||||
onBackward: (byMark: boolean) => void;
|
||||
onForward: (byMark: boolean) => void;
|
||||
gridInsets: Insets;
|
||||
};
|
||||
|
||||
const TTSBar = ({
|
||||
bookKey,
|
||||
isPlaying,
|
||||
onTogglePlay,
|
||||
onBackward,
|
||||
onForward,
|
||||
gridInsets,
|
||||
}: TTSBarProps) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const iconSize32 = useResponsiveSize(30);
|
||||
const iconSize48 = useResponsiveSize(36);
|
||||
|
||||
const isVisible = hoveredBookKey !== bookKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 absolute bottom-0 z-40',
|
||||
'inset-x-0 mx-auto flex w-full justify-center sm:w-fit',
|
||||
'transition-opacity duration-300',
|
||||
isVisible ? `pointer-events-auto opacity-100` : `pointer-events-none opacity-0`,
|
||||
)}
|
||||
style={{ paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0 }}
|
||||
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
>
|
||||
<div className='text-base-content flex h-[52px] items-center space-x-2 px-2'>
|
||||
<button
|
||||
onClick={onBackward.bind(null, false)}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={_('Previous Paragraph')}
|
||||
aria-label={_('Previous Paragraph')}
|
||||
>
|
||||
<MdFastRewind size={iconSize32} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onBackward.bind(null, true)}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={_('Previous Sentence')}
|
||||
aria-label={_('Previous Sentence')}
|
||||
>
|
||||
<MdSkipPrevious size={iconSize32} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={isPlaying ? _('Pause') : _('Play')}
|
||||
aria-label={isPlaying ? _('Pause') : _('Play')}
|
||||
>
|
||||
{isPlaying ? <MdOutlinePause size={iconSize48} /> : <MdPlayArrow size={iconSize48} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onForward.bind(null, true)}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={_('Next Sentence')}
|
||||
aria-label={_('Next Sentence')}
|
||||
>
|
||||
<MdSkipNext size={iconSize32} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onForward.bind(null, false)}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={_('Next Paragraph')}
|
||||
aria-label={_('Next Paragraph')}
|
||||
>
|
||||
<MdFastForward size={iconSize32} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TTSBar;
|
||||
@@ -13,9 +13,11 @@ import { parseSSMLLang } from '@/utils/ssml';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { invokeUseBackgroundAudio } from '@/utils/bridge';
|
||||
import { CFI } from '@/libs/document';
|
||||
import { Insets } from '@/types/misc';
|
||||
import Popup from '@/components/Popup';
|
||||
import TTSPanel from './TTSPanel';
|
||||
import TTSIcon from './TTSIcon';
|
||||
import TTSBar from './TTSBar';
|
||||
|
||||
const POPUP_WIDTH = 282;
|
||||
const POPUP_HEIGHT = 160;
|
||||
@@ -23,20 +25,23 @@ const POPUP_PADDING = 10;
|
||||
|
||||
interface TTSControlProps {
|
||||
bookKey: string;
|
||||
gridInsets: Insets;
|
||||
}
|
||||
|
||||
const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getProgress, getViewSettings } = useReaderStore();
|
||||
const { hoveredBookKey, getView, getProgress, getViewSettings } = useReaderStore();
|
||||
const { setViewSettings, setTTSEnabled } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const [ttsLang, setTtsLang] = useState<string>('en');
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [showIndicator, setShowIndicator] = useState(false);
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [showTTSBar, setShowTTSBar] = useState(() => !!viewSettings?.showTTSBar);
|
||||
const [panelPosition, setPanelPosition] = useState<Position>();
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
|
||||
@@ -44,7 +49,6 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
const [timeoutTimestamp, setTimeoutTimestamp] = useState(0);
|
||||
const [timeoutFunc, setTimeoutFunc] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const popupPadding = useResponsiveSize(POPUP_PADDING);
|
||||
const maxWidth = window.innerWidth - 2 * popupPadding;
|
||||
const popupWidth = Math.min(maxWidth, useResponsiveSize(POPUP_WIDTH));
|
||||
@@ -64,11 +68,6 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
unblockerAudioRef.current.addEventListener('play', () => {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = null;
|
||||
navigator.mediaSession.setActionHandler('play', null);
|
||||
navigator.mediaSession.setActionHandler('pause', null);
|
||||
navigator.mediaSession.setActionHandler('stop', null);
|
||||
navigator.mediaSession.setActionHandler('seekbackward', null);
|
||||
navigator.mediaSession.setActionHandler('seekforward', null);
|
||||
}
|
||||
});
|
||||
unblockerAudioRef.current.preload = 'auto';
|
||||
@@ -123,7 +122,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
const mark = (e as CustomEvent<TTSMark>).detail;
|
||||
if (appService?.isMobileApp && 'mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: mark.text,
|
||||
title: mark?.text || '',
|
||||
artist: sectionLabel || title,
|
||||
album: author,
|
||||
artwork: [{ src: coverImageUrl || '/icon.png', sizes: '512x512', type: 'image/png' }],
|
||||
@@ -240,7 +239,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
const handleTogglePlay = useCallback(async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (!ttsController) return;
|
||||
|
||||
@@ -259,41 +258,54 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
await ttsController.start();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [isPlaying, isPaused]);
|
||||
|
||||
const handleBackward = async () => {
|
||||
const handleBackward = useCallback(async (byMark = false) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.backward();
|
||||
await ttsController.backward(byMark);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleForward = async () => {
|
||||
const handleForward = useCallback(async (byMark = false) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.forward();
|
||||
await ttsController.forward(byMark);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStop = async (bookKey: string) => {
|
||||
const handlePause = useCallback(async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.shutdown();
|
||||
ttsControllerRef.current = null;
|
||||
setTtsController(null);
|
||||
getView(bookKey)?.deselect();
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
setShowIndicator(false);
|
||||
setIsPaused(true);
|
||||
await ttsController.pause();
|
||||
}
|
||||
if (appService?.isIOSApp) {
|
||||
await invokeUseBackgroundAudio({ enabled: false });
|
||||
}
|
||||
if (appService?.isMobile) {
|
||||
releaseUnblockAudio();
|
||||
}
|
||||
setTTSEnabled(bookKey, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStop = useCallback(
|
||||
async (bookKey: string) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.shutdown();
|
||||
ttsControllerRef.current = null;
|
||||
setTtsController(null);
|
||||
getView(bookKey)?.deselect();
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
setShowIndicator(false);
|
||||
}
|
||||
if (appService?.isIOSApp) {
|
||||
await invokeUseBackgroundAudio({ enabled: false });
|
||||
}
|
||||
if (appService?.isMobile) {
|
||||
releaseUnblockAudio();
|
||||
}
|
||||
setTTSEnabled(bookKey, false);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[appService],
|
||||
);
|
||||
|
||||
// rate range: 0.5 - 3, 1.0 is normal speed
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -363,6 +375,16 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTTSBar = () => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewSettings.showTTSBar = !viewSettings.showTTSBar;
|
||||
setShowTTSBar(viewSettings.showTTSBar);
|
||||
if (viewSettings.showTTSBar) {
|
||||
setShowPanel(false);
|
||||
}
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
};
|
||||
|
||||
const updatePanelPosition = () => {
|
||||
if (iconRef.current) {
|
||||
const rect = iconRef.current.getBoundingClientRect();
|
||||
@@ -401,6 +423,30 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
setShowPanel(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
handleTogglePlay();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
handleTogglePlay();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('stop', () => {
|
||||
handlePause();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('seekforward', () => {
|
||||
handleForward();
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('seekbackward', () => {
|
||||
handleBackward();
|
||||
});
|
||||
}
|
||||
}, [handleTogglePlay, handlePause, handleForward, handleBackward]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!iconRef.current || !showPanel) return;
|
||||
const parentElement = iconRef.current.parentElement;
|
||||
@@ -430,12 +476,13 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
ref={iconRef}
|
||||
className={clsx(
|
||||
'absolute h-12 w-12',
|
||||
viewSettings?.rtl ? 'left-6' : 'right-6',
|
||||
'transition-transform duration-300',
|
||||
viewSettings?.rtl ? 'left-8' : 'right-6',
|
||||
!appService?.hasSafeAreaInset && 'bottom-[70px] sm:bottom-14',
|
||||
)}
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset
|
||||
? `${(safeAreaInsets?.bottom || 0) + 70}px`
|
||||
? `${(safeAreaInsets?.bottom || 0) * 0.33 + (hoveredBookKey ? 70 : 52)}px`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
@@ -464,9 +511,20 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
onSetVoice={handleSetVoice}
|
||||
onGetVoiceId={handleGetVoiceId}
|
||||
onSelectTimeout={handleSelectTimeout}
|
||||
onToogleTTSBar={handleToggleTTSBar}
|
||||
/>
|
||||
</Popup>
|
||||
)}
|
||||
{showIndicator && showTTSBar && ttsClientsInited && (
|
||||
<TTSBar
|
||||
bookKey={bookKey}
|
||||
isPlaying={isPlaying}
|
||||
onBackward={handleBackward}
|
||||
onTogglePlay={handleTogglePlay}
|
||||
onForward={handleForward}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdAlarm } from 'react-icons/md';
|
||||
import { TbChevronCompactDown, TbChevronCompactUp } from 'react-icons/tb';
|
||||
import { RiVoiceAiFill } from 'react-icons/ri';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { TTSVoicesGroup } from '@/services/tts';
|
||||
@@ -25,6 +26,7 @@ type TTSPanelProps = {
|
||||
onSetVoice: (voice: string, lang: string) => void;
|
||||
onGetVoiceId: () => string;
|
||||
onSelectTimeout: (bookKey: string, value: number) => void;
|
||||
onToogleTTSBar: () => void;
|
||||
};
|
||||
|
||||
const getTTSTimeoutOptions = (_: TranslationFunc) => {
|
||||
@@ -113,6 +115,7 @@ const TTSPanel = ({
|
||||
onSetVoice,
|
||||
onGetVoiceId,
|
||||
onSelectTimeout,
|
||||
onToogleTTSBar,
|
||||
}: TTSPanelProps) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
@@ -200,7 +203,7 @@ const TTSPanel = ({
|
||||
const timeoutOptions = getTTSTimeoutOptions(_);
|
||||
|
||||
return (
|
||||
<div className='flex w-full flex-col items-center justify-center gap-2 rounded-2xl p-4'>
|
||||
<div className='flex w-full flex-col items-center justify-center gap-2 rounded-2xl px-4 pt-4 sm:gap-1'>
|
||||
<div className='flex w-full flex-col items-center gap-0.5'>
|
||||
<input
|
||||
className='range'
|
||||
@@ -231,23 +234,38 @@ const TTSPanel = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<button onClick={onBackward} className='rounded-full p-1'>
|
||||
<button
|
||||
onClick={onBackward}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={_('Previous Paragraph')}
|
||||
aria-label={_('Previous Paragraph')}
|
||||
>
|
||||
<MdFastRewind size={iconSize32} />
|
||||
</button>
|
||||
<button onClick={onTogglePlay} className='rounded-full p-1'>
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={isPlaying ? _('Pause') : _('Play')}
|
||||
aria-label={isPlaying ? _('Pause') : _('Play')}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<MdPauseCircle size={iconSize48} className='fill-primary' />
|
||||
) : (
|
||||
<MdPlayCircle size={iconSize48} className='fill-primary' />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onForward} className='rounded-full p-1'>
|
||||
<button
|
||||
onClick={onForward}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
title={_('Next Paragraph')}
|
||||
aria-label={_('Next Paragraph')}
|
||||
>
|
||||
<MdFastForward size={iconSize32} />
|
||||
</button>
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='flex flex-col items-center justify-center rounded-full p-1'
|
||||
className='flex flex-col items-center justify-center rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
>
|
||||
<MdAlarm size={iconSize32} />
|
||||
@@ -293,7 +311,7 @@ const TTSPanel = ({
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='rounded-full p-1'
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
>
|
||||
<RiVoiceAiFill size={iconSize32} />
|
||||
@@ -350,6 +368,15 @@ const TTSPanel = ({
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-4 items-center justify-center opacity-60 transition-transform duration-200 hover:scale-105 hover:opacity-100'>
|
||||
<button onClick={onToogleTTSBar} className='p-0'>
|
||||
{viewSettings?.showTTSBar ? (
|
||||
<TbChevronCompactUp size={iconSize48} style={{ transform: 'scaleY(0.85)' }} />
|
||||
) : (
|
||||
<TbChevronCompactDown size={iconSize48} style={{ transform: 'scaleY(0.85)' }} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -96,12 +96,8 @@ export const useKOSync = (bookKey: string) => {
|
||||
if (!remote.progress?.startsWith('/body')) return;
|
||||
try {
|
||||
const content = view?.renderer.getContents()[0];
|
||||
const cfi = await getCFIFromXPointer(
|
||||
remote.progress!,
|
||||
content?.doc,
|
||||
content?.index,
|
||||
bookDoc,
|
||||
);
|
||||
const koProgress = normalizeProgressXPointer(remote.progress);
|
||||
const cfi = await getCFIFromXPointer(koProgress, content?.doc, content?.index, bookDoc);
|
||||
view?.goTo(cfi);
|
||||
} catch (error) {
|
||||
console.error('Failed to convert XPointer to CFI', error);
|
||||
@@ -242,6 +238,7 @@ export const useKOSync = (bookKey: string) => {
|
||||
const handlePushProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
};
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
@@ -285,14 +282,14 @@ export const useKOSync = (bookKey: string) => {
|
||||
}
|
||||
}, [progress, syncState, settings.kosync, pushProgress]);
|
||||
|
||||
const resolveConflictWithLocal = () => {
|
||||
const resolveWithLocal = () => {
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
setSyncState('synced');
|
||||
setConflictDetails(null);
|
||||
};
|
||||
|
||||
const resolveConflictWithRemote = async () => {
|
||||
const resolveWithRemote = async () => {
|
||||
const view = getView(bookKey);
|
||||
const remote = conflictDetails?.remote;
|
||||
const book = conflictDetails?.book;
|
||||
@@ -311,7 +308,7 @@ export const useKOSync = (bookKey: string) => {
|
||||
errorMessage,
|
||||
pushProgress,
|
||||
pullProgress,
|
||||
resolveConflictWithLocal,
|
||||
resolveConflictWithRemote,
|
||||
resolveWithLocal,
|
||||
resolveWithRemote,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -116,8 +116,9 @@ export const useProgressSync = (bookKey: string) => {
|
||||
const view = getView(bookKey);
|
||||
if (xPointer && view && bookData && bookData.bookDoc) {
|
||||
const content = view.renderer.getContents()[0];
|
||||
const koProgress = normalizeProgressXPointer(xPointer);
|
||||
const candidateCFI = await getCFIFromXPointer(
|
||||
xPointer,
|
||||
koProgress,
|
||||
content?.doc,
|
||||
content?.index,
|
||||
bookData.bookDoc,
|
||||
|
||||
@@ -57,7 +57,10 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
{!noIcon && (
|
||||
<span style={{ minWidth: `${iconSize}px` }}>
|
||||
{typeof Icon === 'function' ? (
|
||||
<Icon className='text-base-content' size={iconSize} />
|
||||
<Icon
|
||||
className={disabled ? 'text-gray-400' : 'text-base-content'}
|
||||
size={iconSize}
|
||||
/>
|
||||
) : (
|
||||
Icon
|
||||
)}
|
||||
@@ -73,8 +76,9 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
{shortcut && (
|
||||
<kbd
|
||||
className={clsx(
|
||||
'border-base-300/40 bg-base-300/75 text-neutral-content hidden rounded-md border shadow-sm sm:flex',
|
||||
'border-base-300/40 bg-base-300/75 hidden rounded-md border shadow-sm sm:flex',
|
||||
'shrink-0 px-1.5 py-0.5 text-xs font-medium',
|
||||
disabled ? 'text-gray-400' : 'text-neutral-content',
|
||||
)}
|
||||
>
|
||||
{shortcut}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface TextButtonProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'success';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
}
|
||||
|
||||
const TextButton: React.FC<TextButtonProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
variant = 'primary',
|
||||
size = 'sm',
|
||||
type = 'button',
|
||||
}) => {
|
||||
const variantClasses = {
|
||||
primary: 'text-blue-500 hover:text-blue-600',
|
||||
secondary: 'text-gray-500 hover:text-gray-600',
|
||||
danger: 'text-red-500 hover:text-red-600',
|
||||
success: 'text-green-500 hover:text-green-600',
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'font-size-sm h-[1.3em] min-h-[1.3em]',
|
||||
md: 'font-size-md h-[1.5em] min-h-[1.5em]',
|
||||
lg: 'font-size-lg h-[1.8em] min-h-[1.8em]',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={clsx(
|
||||
'content settings-content btn btn-ghost hover:bg-transparent',
|
||||
'flex items-end p-0',
|
||||
sizeClasses[size],
|
||||
disabled ? 'btn-disabled !bg-opacity-0' : '',
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div
|
||||
className={clsx('align-bottom', sizeClasses[size].split(' ')[0], variantClasses[variant])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextButton;
|
||||
@@ -0,0 +1,143 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
|
||||
|
||||
interface TextEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onBlur?: () => void;
|
||||
onSave?: () => void;
|
||||
onEscape?: () => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
autoFocus?: boolean;
|
||||
spellCheck?: boolean;
|
||||
disabled?: boolean;
|
||||
maxRows?: number;
|
||||
minRows?: number;
|
||||
}
|
||||
|
||||
export interface TextEditorRef {
|
||||
focus: () => void;
|
||||
blur: () => void;
|
||||
getValue: () => string;
|
||||
setValue: (value: string) => void;
|
||||
getElement: () => HTMLTextAreaElement | null;
|
||||
}
|
||||
|
||||
const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
onSave,
|
||||
onEscape,
|
||||
placeholder,
|
||||
className,
|
||||
autoFocus = false,
|
||||
spellCheck = false,
|
||||
disabled = false,
|
||||
maxRows,
|
||||
minRows = 1,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
editorRef.current?.focus();
|
||||
},
|
||||
blur: () => {
|
||||
editorRef.current?.blur();
|
||||
},
|
||||
getValue: () => {
|
||||
return editorRef.current?.value || '';
|
||||
},
|
||||
setValue: (newValue: string) => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = newValue;
|
||||
adjustHeight();
|
||||
}
|
||||
},
|
||||
getElement: () => editorRef.current,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus && editorRef.current) {
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = value;
|
||||
adjustHeight();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.style.height = 'auto';
|
||||
const scrollHeight = editorRef.current.scrollHeight;
|
||||
|
||||
// Calculate max height based on maxRows
|
||||
let maxHeight = Infinity;
|
||||
if (maxRows) {
|
||||
const lineHeight = parseInt(getComputedStyle(editorRef.current).lineHeight);
|
||||
maxHeight = lineHeight * maxRows;
|
||||
}
|
||||
|
||||
// Calculate min height based on minRows
|
||||
const lineHeight = parseInt(getComputedStyle(editorRef.current).lineHeight) || 20;
|
||||
const minHeight = lineHeight * minRows;
|
||||
|
||||
const finalHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
|
||||
editorRef.current.style.height = `${finalHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
adjustHeight();
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Escape' && onEscape) {
|
||||
onEscape();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && onSave) {
|
||||
e.preventDefault();
|
||||
onSave();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={editorRef}
|
||||
className={clsx(
|
||||
'textarea textarea-ghost min-h-[1em] resize-none !outline-none',
|
||||
'inset-0 w-full rounded-none border-0 bg-transparent p-0',
|
||||
'content font-size-sm',
|
||||
className,
|
||||
)}
|
||||
dir='auto'
|
||||
rows={minRows}
|
||||
spellCheck={spellCheck}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder || ''}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TextEditor.displayName = 'TextEditor';
|
||||
|
||||
export default TextEditor;
|
||||
@@ -1,20 +1,31 @@
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { getSafeAreaInsets } from '@/utils/bridge';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
export const useSafeAreaInsets = () => {
|
||||
const { appService } = useEnv();
|
||||
const [updated, setUpdated] = useState(false);
|
||||
const [insets, setInsets] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
});
|
||||
const [insets, setInsets] = useState({ top: 0, right: 0, bottom: 0, left: 0 });
|
||||
const currentInsets = useRef(insets);
|
||||
|
||||
const { updateSafeAreaInsets } = useThemeStore();
|
||||
|
||||
const updateInsets = (insets: Insets) => {
|
||||
const { top, right, bottom, left } = currentInsets.current;
|
||||
if (
|
||||
insets.top !== top ||
|
||||
insets.right !== right ||
|
||||
insets.bottom !== bottom ||
|
||||
insets.left !== left
|
||||
) {
|
||||
currentInsets.current = insets;
|
||||
setInsets(insets);
|
||||
updateSafeAreaInsets(insets);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateInsets = useCallback(() => {
|
||||
if (!appService) return;
|
||||
|
||||
@@ -40,10 +51,9 @@ export const useSafeAreaInsets = () => {
|
||||
bottom: response.bottom,
|
||||
left: response.left,
|
||||
};
|
||||
setInsets(insets);
|
||||
updateSafeAreaInsets(insets);
|
||||
setUpdated(true);
|
||||
updateInsets(insets);
|
||||
}
|
||||
setUpdated(true);
|
||||
});
|
||||
} else if (hasCustomProperties) {
|
||||
const insets = {
|
||||
@@ -52,8 +62,7 @@ export const useSafeAreaInsets = () => {
|
||||
bottom: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-bottom')) || 0,
|
||||
left: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-left')) || 0,
|
||||
};
|
||||
setInsets(insets);
|
||||
updateSafeAreaInsets(insets);
|
||||
updateInsets(insets);
|
||||
setUpdated(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { ErrorCodes, getTranslator, getTranslators, TranslatorName } from '@/services/translators';
|
||||
import { getFromCache, storeInCache, polish, UseTranslatorOptions } from '@/services/translators';
|
||||
import { getFromCache, storeInCache, UseTranslatorOptions } from '@/services/translators';
|
||||
import { polish, preprocess } from '@/services/translators';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useTranslation } from './useTranslation';
|
||||
|
||||
@@ -10,6 +11,7 @@ export function useTranslator({
|
||||
sourceLang = 'AUTO',
|
||||
targetLang = 'EN',
|
||||
enablePolishing = true,
|
||||
enablePreprocessing = true,
|
||||
}: UseTranslatorOptions = {}) {
|
||||
const _ = useTranslation();
|
||||
const { token } = useAuth();
|
||||
@@ -42,7 +44,7 @@ export function useTranslator({
|
||||
const sourceLanguage = options?.source || sourceLang;
|
||||
const targetLanguage = options?.target || targetLang;
|
||||
const useCache = options?.useCache ?? false;
|
||||
const textsToTranslate = input;
|
||||
const textsToTranslate = enablePreprocessing ? preprocess(input) : input;
|
||||
|
||||
if (textsToTranslate.length === 0 || textsToTranslate.every((t) => !t?.trim())) {
|
||||
return textsToTranslate;
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface SectionItem {
|
||||
size: number;
|
||||
linear: string;
|
||||
location?: Location;
|
||||
pageSpread?: 'left' | 'right' | 'center' | '';
|
||||
|
||||
createDocument: () => Promise<Document>;
|
||||
}
|
||||
@@ -87,6 +88,7 @@ export interface BookDoc {
|
||||
metadata: BookMetadata;
|
||||
rendition?: {
|
||||
layout?: 'pre-paginated' | 'reflowable';
|
||||
spread?: 'auto' | 'none';
|
||||
viewport?: { width: number; height: number };
|
||||
};
|
||||
dir: string;
|
||||
|
||||
@@ -2,7 +2,14 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { AppPlatform, AppService, OsPlatform } from '@/types/system';
|
||||
import { FileSystem, BaseDir, DeleteAction } from '@/types/system';
|
||||
import { Book, BookConfig, BookContent, BookFormat, ViewSettings } from '@/types/book';
|
||||
import {
|
||||
Book,
|
||||
BookConfig,
|
||||
BookContent,
|
||||
BookFormat,
|
||||
FIXED_LAYOUT_FORMATS,
|
||||
ViewSettings,
|
||||
} from '@/types/book';
|
||||
import {
|
||||
getDir,
|
||||
getLocalBookFilename,
|
||||
@@ -35,6 +42,7 @@ import {
|
||||
DEFAULT_MOBILE_READSETTINGS,
|
||||
DEFAULT_SCREEN_CONFIG,
|
||||
DEFAULT_TRANSLATOR_CONFIG,
|
||||
DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS,
|
||||
} from './constants';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { getOSPlatform, getTargetLang, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
@@ -503,7 +511,10 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
async loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig> {
|
||||
const { globalViewSettings } = settings;
|
||||
const globalViewSettings = {
|
||||
...settings.globalViewSettings,
|
||||
...(FIXED_LAYOUT_FORMATS.has(book.format) ? DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS : {}),
|
||||
};
|
||||
try {
|
||||
let str = '{}';
|
||||
if (await this.fs.exists(getConfigFilename(book), 'Books')) {
|
||||
@@ -532,7 +543,10 @@ export abstract class BaseAppService implements AppService {
|
||||
async saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings) {
|
||||
let serializedConfig: string;
|
||||
if (settings) {
|
||||
const { globalViewSettings } = settings;
|
||||
const globalViewSettings = {
|
||||
...settings.globalViewSettings,
|
||||
...(FIXED_LAYOUT_FORMATS.has(book.format) ? DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS : {}),
|
||||
};
|
||||
serializedConfig = serializeConfig(config, globalViewSettings, DEFAULT_BOOK_SEARCH_CONFIG);
|
||||
} else {
|
||||
serializedConfig = JSON.stringify(config);
|
||||
|
||||
@@ -151,6 +151,10 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
codeLanguage: 'auto-detect',
|
||||
userStylesheet: '',
|
||||
userUIStylesheet: '',
|
||||
|
||||
zoomMode: 'fit-page',
|
||||
spreadMode: 'auto',
|
||||
keepCoverSpread: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
@@ -166,6 +170,10 @@ export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
textIndent: 2,
|
||||
};
|
||||
|
||||
export const DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
overrideColor: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_CONFIG: ViewConfig = {
|
||||
sideBarTab: 'toc',
|
||||
uiLanguage: '',
|
||||
@@ -187,6 +195,7 @@ export const DEFAULT_TTS_CONFIG: TTSConfig = {
|
||||
ttsRate: 1.3,
|
||||
ttsVoice: '',
|
||||
ttsLocation: '',
|
||||
showTTSBar: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
|
||||
@@ -669,6 +678,9 @@ export const TRANSLATED_LANGS = {
|
||||
vi: 'Tiếng Việt',
|
||||
th: 'ภาษาไทย',
|
||||
bo: 'བོད་སྐད་',
|
||||
bn: 'বাংলা',
|
||||
ta: 'தமிழ்',
|
||||
si: 'සිංහල',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '正體中文',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './types';
|
||||
export * from './cache';
|
||||
export * from './polish';
|
||||
export * from './preprocess';
|
||||
export * from './providers';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export type Preprocessor = (text: string) => string;
|
||||
export type TextSubstitutions = Record<string, string>;
|
||||
|
||||
const defaultSubstitutions: TextSubstitutions = {
|
||||
Cover: 'The Cover',
|
||||
Dedication: 'Dedication Page',
|
||||
Acknowledgements: 'The Acknowledgements',
|
||||
};
|
||||
|
||||
export const basicSubstitute = (text: string, substitutions: TextSubstitutions): string => {
|
||||
const allSubstitutions = { ...defaultSubstitutions, ...substitutions };
|
||||
|
||||
if (Object.keys(allSubstitutions).length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (allSubstitutions[text]) {
|
||||
return allSubstitutions[text];
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export function createPreprocessor(substitutions: TextSubstitutions = {}): Preprocessor {
|
||||
return (text: string) => basicSubstitute(text, substitutions);
|
||||
}
|
||||
|
||||
export function preprocess(texts: string[], substitutions: TextSubstitutions = {}): string[] {
|
||||
const preprocessor = createPreprocessor(substitutions);
|
||||
return texts.map(preprocessor);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export interface UseTranslatorOptions {
|
||||
sourceLang?: string;
|
||||
targetLang?: string;
|
||||
enablePolishing?: boolean;
|
||||
enablePreprocessing?: boolean;
|
||||
}
|
||||
|
||||
export const ErrorCodes = {
|
||||
|
||||
@@ -219,6 +219,7 @@ export class TTSController extends EventTarget {
|
||||
await this.initViewTTS();
|
||||
this.#speak(ssml).catch((e) => this.error(e));
|
||||
this.preloadNextSSML();
|
||||
this.dispatchSpeakMark();
|
||||
}
|
||||
|
||||
play() {
|
||||
@@ -270,30 +271,36 @@ export class TTSController extends EventTarget {
|
||||
this.state = 'stopped';
|
||||
}
|
||||
|
||||
// goto previous paragraph
|
||||
async backward() {
|
||||
// goto previous mark/paragraph
|
||||
async backward(byMark = false) {
|
||||
await this.initViewTTS();
|
||||
if (this.state === 'playing') {
|
||||
await this.stop();
|
||||
this.#speak(this.view.tts?.prev());
|
||||
if (byMark) this.#speak(this.view.tts?.prevMark());
|
||||
else this.#speak(this.view.tts?.prev());
|
||||
} else {
|
||||
await this.stop();
|
||||
this.state = 'backward-paused';
|
||||
this.view.tts?.prev(true);
|
||||
if (byMark) this.view.tts?.prevMark(true);
|
||||
else this.view.tts?.prev(true);
|
||||
}
|
||||
}
|
||||
|
||||
// goto next paragraph
|
||||
async forward() {
|
||||
// goto next mark/paragraph
|
||||
async forward(byMark = false) {
|
||||
await this.initViewTTS();
|
||||
if (this.state === 'playing') {
|
||||
await this.stop();
|
||||
this.#speak(this.view.tts?.next());
|
||||
this.preloadNextSSML();
|
||||
if (byMark) this.#speak(this.view.tts?.nextMark());
|
||||
else {
|
||||
this.#speak(this.view.tts?.next());
|
||||
this.preloadNextSSML();
|
||||
}
|
||||
} else {
|
||||
await this.stop();
|
||||
this.state = 'forward-paused';
|
||||
this.view.tts?.next(true);
|
||||
if (byMark) this.view.tts?.nextMark(true);
|
||||
else this.view.tts?.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,8 +364,8 @@ export class TTSController extends EventTarget {
|
||||
return this.ttsClient.getSpeakingLang();
|
||||
}
|
||||
|
||||
dispatchSpeakMark(mark: TTSMark) {
|
||||
this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark }));
|
||||
dispatchSpeakMark(mark?: TTSMark) {
|
||||
this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark || { text: '' } }));
|
||||
}
|
||||
|
||||
error(e: unknown) {
|
||||
|
||||
@@ -10,7 +10,7 @@ interface FontStoreState {
|
||||
setFonts: (fonts: CustomFont[]) => void;
|
||||
addFont: (
|
||||
path: string,
|
||||
options?: { name?: string; family?: string; style?: string },
|
||||
options?: { name?: string; family?: string; style?: string; weight?: number },
|
||||
) => CustomFont;
|
||||
removeFont: (id: string) => boolean;
|
||||
updateFont: (id: string, updates: Partial<CustomFont>) => boolean;
|
||||
@@ -240,7 +240,9 @@ export const useCustomFontStore = create<FontStoreState>((set, get) => ({
|
||||
return get()
|
||||
.getAvailableFonts()
|
||||
.filter((font) => font.loaded && !font.error)
|
||||
.map((font) => font.name);
|
||||
.map((font) => font.family || font.name)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
},
|
||||
|
||||
getLoadedFonts: () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ interface NotebookState {
|
||||
notebookNewAnnotation: TextSelection | null;
|
||||
notebookEditAnnotation: BookNote | null;
|
||||
notebookAnnotationDrafts: { [key: string]: string };
|
||||
getIsNotebookVisible: () => boolean;
|
||||
toggleNotebook: () => void;
|
||||
toggleNotebookPin: () => void;
|
||||
setNotebookWidth: (width: string) => void;
|
||||
@@ -27,6 +28,7 @@ export const useNotebookStore = create<NotebookState>((set, get) => ({
|
||||
notebookNewAnnotation: null,
|
||||
notebookEditAnnotation: null,
|
||||
notebookAnnotationDrafts: {},
|
||||
getIsNotebookVisible: () => get().isNotebookVisible,
|
||||
setNotebookWidth: (width: string) => set({ notebookWidth: width }),
|
||||
toggleNotebook: () => set((state) => ({ isNotebookVisible: !state.isNotebookVisible })),
|
||||
toggleNotebookPin: () => set((state) => ({ isNotebookPinned: !state.isNotebookPinned })),
|
||||
|
||||
@@ -327,7 +327,8 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
},
|
||||
})),
|
||||
|
||||
getGridInsets: (key: string) => get().viewStates[key]?.gridInsets || null,
|
||||
getGridInsets: (key: string) =>
|
||||
get().viewStates[key]?.gridInsets || { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
setGridInsets: (key: string, insets: Insets | null) =>
|
||||
set((state) => ({
|
||||
viewStates: {
|
||||
|
||||
@@ -5,6 +5,7 @@ interface SidebarState {
|
||||
sideBarWidth: string;
|
||||
isSideBarVisible: boolean;
|
||||
isSideBarPinned: boolean;
|
||||
getIsSideBarVisible: () => boolean;
|
||||
setSideBarBookKey: (key: string) => void;
|
||||
setSideBarWidth: (width: string) => void;
|
||||
toggleSideBar: () => void;
|
||||
@@ -13,11 +14,12 @@ interface SidebarState {
|
||||
setSideBarPin: (pinned: boolean) => void;
|
||||
}
|
||||
|
||||
export const useSidebarStore = create<SidebarState>((set) => ({
|
||||
export const useSidebarStore = create<SidebarState>((set, get) => ({
|
||||
sideBarBookKey: null,
|
||||
sideBarWidth: '',
|
||||
isSideBarVisible: false,
|
||||
isSideBarPinned: false,
|
||||
getIsSideBarVisible: () => get().isSideBarVisible,
|
||||
setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }),
|
||||
setSideBarWidth: (width: string) => set({ sideBarWidth: width }),
|
||||
toggleSideBar: () => set((state) => ({ isSideBarVisible: !state.isSideBarVisible })),
|
||||
|
||||
@@ -67,7 +67,7 @@ export const useThemeStore = create<ThemeState>((set, get) => {
|
||||
systemUIVisible: false,
|
||||
statusBarHeight: 24,
|
||||
systemUIAlwaysHidden: false,
|
||||
safeAreaInsets: null,
|
||||
safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
showSystemUI: () => set({ systemUIVisible: true }),
|
||||
dismissSystemUI: () => set({ systemUIVisible: false }),
|
||||
setStatusBarHeight: (height: number) => set({ statusBarHeight: height }),
|
||||
|
||||
@@ -115,11 +115,14 @@ export const mountAdditionalFonts = (document: Document, isCJK = false) => {
|
||||
});
|
||||
};
|
||||
|
||||
export type FontStyle = 'normal' | 'italic' | 'oblique';
|
||||
|
||||
export interface CustomFont {
|
||||
id: string;
|
||||
name: string;
|
||||
family?: string;
|
||||
style?: string;
|
||||
weight?: number;
|
||||
path: string;
|
||||
downloadedAt?: number;
|
||||
deletedAt?: number;
|
||||
@@ -171,7 +174,9 @@ export function createFontFamily(name: string): string {
|
||||
export function createFontCSS(font: CustomFont): string {
|
||||
const format = getFontFormat(font.path);
|
||||
const cssFormat = getCSSFormatString(format);
|
||||
const fontFamily = createFontFamily(font.name);
|
||||
const fontFamily = createFontFamily(font.family || font.name);
|
||||
const fontStyle = font.style || 'normal';
|
||||
const fontWeight = font.weight || 400;
|
||||
if (!font.blobUrl) {
|
||||
throw new Error(`Blob URL not available for font: ${font.name}`);
|
||||
}
|
||||
@@ -179,6 +184,8 @@ export function createFontCSS(font: CustomFont): string {
|
||||
const css = `
|
||||
@font-face {
|
||||
font-family: "${fontFamily}";
|
||||
font-style: ${fontStyle};
|
||||
font-weight: ${fontWeight};
|
||||
src: url("${font.blobUrl}") format("${cssFormat}");
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -193,10 +200,18 @@ export function createCustomFont(
|
||||
name?: string;
|
||||
family?: string;
|
||||
style?: string;
|
||||
weight?: number;
|
||||
},
|
||||
): CustomFont {
|
||||
const name = options?.name || getFontName(path);
|
||||
return { id: getFontId(name), name, family: options?.family, style: options?.style, path };
|
||||
return {
|
||||
id: getFontId(name),
|
||||
name,
|
||||
family: options?.family,
|
||||
style: options?.style,
|
||||
weight: options?.weight,
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
export const mountCustomFont = (document: Document, font: CustomFont) => {
|
||||
|
||||
@@ -260,17 +260,17 @@ foliate-view {
|
||||
|
||||
.content.font-size-base {
|
||||
font-size: 1em !important;
|
||||
line-height: 1.5em !important;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.content.font-size-sm {
|
||||
font-size: 0.875em !important;
|
||||
line-height: 1.25em !important;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.content.font-size-xs {
|
||||
font-size: 0.75em !important;
|
||||
line-height: 1em !important;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.writing-vertical-rl {
|
||||
|
||||
@@ -126,6 +126,11 @@ export interface BookStyle {
|
||||
codeLanguage: string;
|
||||
userStylesheet: string;
|
||||
userUIStylesheet: string;
|
||||
|
||||
// fixed-layout specific
|
||||
zoomMode: 'fit-page' | 'fit-width' | 'original-size' | 'custom';
|
||||
spreadMode: 'auto' | 'none';
|
||||
keepCoverSpread: boolean;
|
||||
}
|
||||
|
||||
export interface BookFont {
|
||||
@@ -160,6 +165,7 @@ export interface TTSConfig {
|
||||
ttsRate: number;
|
||||
ttsVoice: string;
|
||||
ttsLocation: string;
|
||||
showTTSBar: boolean;
|
||||
}
|
||||
|
||||
export interface TranslatorConfig {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FontStyle } from '@/styles/fonts';
|
||||
import { getUserLang } from './misc';
|
||||
|
||||
function parseUnicodeString(dataView: DataView, offset: number, length: number): string {
|
||||
@@ -77,6 +78,74 @@ function getLanguagePriority(platformID: number, languageID: number, userLanguag
|
||||
return priority;
|
||||
}
|
||||
|
||||
function parseOS2Weight(dataView: DataView, os2TableOffset: number): number {
|
||||
// OS/2 table usWeightClass is at offset 4
|
||||
return dataView.getUint16(os2TableOffset + 4, false);
|
||||
}
|
||||
|
||||
function parseOS2Selection(dataView: DataView, os2TableOffset: number): number {
|
||||
// OS/2 table fsSelection is at offset 62
|
||||
return dataView.getUint16(os2TableOffset + 62, false);
|
||||
}
|
||||
|
||||
function weightClassToCSSWeight(weightClass: number): number {
|
||||
// Map OpenType weight class to CSS weight
|
||||
if (weightClass >= 1 && weightClass <= 100) return 100;
|
||||
if (weightClass >= 101 && weightClass <= 200) return 200;
|
||||
if (weightClass >= 201 && weightClass <= 300) return 300;
|
||||
if (weightClass >= 301 && weightClass <= 400) return 400;
|
||||
if (weightClass >= 401 && weightClass <= 500) return 500;
|
||||
if (weightClass >= 501 && weightClass <= 600) return 600;
|
||||
if (weightClass >= 601 && weightClass <= 700) return 700;
|
||||
if (weightClass >= 701 && weightClass <= 800) return 800;
|
||||
if (weightClass >= 801 && weightClass <= 900) return 900;
|
||||
return 400; // Default to normal weight
|
||||
}
|
||||
|
||||
function inferWeightFromStyleName(styleName: string): number {
|
||||
const lowerStyle = styleName.toLowerCase();
|
||||
|
||||
// Check for specific weight keywords
|
||||
if (lowerStyle.includes('thin') || lowerStyle.includes('hairline')) return 100;
|
||||
if (lowerStyle.includes('extralight') || lowerStyle.includes('ultralight')) return 200;
|
||||
if (
|
||||
lowerStyle.includes('light') &&
|
||||
!lowerStyle.includes('extralight') &&
|
||||
!lowerStyle.includes('ultralight')
|
||||
)
|
||||
return 300;
|
||||
if (lowerStyle.includes('medium')) return 500;
|
||||
if (lowerStyle.includes('semibold') || lowerStyle.includes('demibold')) return 600;
|
||||
if (lowerStyle.includes('extrabold') || lowerStyle.includes('ultrabold')) return 800;
|
||||
if (lowerStyle.includes('black') || lowerStyle.includes('heavy')) return 900;
|
||||
if (
|
||||
lowerStyle.includes('bold') &&
|
||||
!lowerStyle.includes('semibold') &&
|
||||
!lowerStyle.includes('extrabold') &&
|
||||
!lowerStyle.includes('ultrabold')
|
||||
)
|
||||
return 700;
|
||||
|
||||
return 400; // Default to normal weight
|
||||
}
|
||||
|
||||
function inferStyleFromName(
|
||||
styleName: string,
|
||||
fsSelection: number,
|
||||
): 'normal' | 'italic' | 'oblique' {
|
||||
const lowerStyle = styleName.toLowerCase();
|
||||
|
||||
// Check fsSelection flags first (bit 0 = italic, bit 9 = oblique)
|
||||
if (fsSelection & 0x200) return 'oblique'; // Bit 9
|
||||
if (fsSelection & 0x1) return 'italic'; // Bit 0
|
||||
|
||||
// Fallback to style name analysis
|
||||
if (lowerStyle.includes('oblique')) return 'oblique';
|
||||
if (lowerStyle.includes('italic') || lowerStyle.includes('slant')) return 'italic';
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
type FontNameType = {
|
||||
name: string;
|
||||
platformID: number;
|
||||
@@ -84,7 +153,7 @@ type FontNameType = {
|
||||
priority: number;
|
||||
};
|
||||
|
||||
export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
export const parseFontInfo = (fontData: ArrayBuffer, filename: string) => {
|
||||
const fallbackName = filename.replace(/\.[^/.]+$/, '');
|
||||
try {
|
||||
const dataView = new DataView(fontData);
|
||||
@@ -94,6 +163,7 @@ export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
}
|
||||
const numTables = dataView.getUint16(4, false);
|
||||
let nameTableOffset = 0;
|
||||
let os2TableOffset = 0;
|
||||
for (let i = 0; i < numTables; i++) {
|
||||
const tableOffset = 12 + i * 16;
|
||||
const tag = String.fromCharCode(
|
||||
@@ -105,7 +175,8 @@ export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
|
||||
if (tag === 'name') {
|
||||
nameTableOffset = dataView.getUint32(tableOffset + 8, false);
|
||||
break;
|
||||
} else if (tag === 'OS/2') {
|
||||
os2TableOffset = dataView.getUint32(tableOffset + 8, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +190,8 @@ export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
const userLanguage = getUserLang();
|
||||
const fontFamilyNames: Array<FontNameType> = [];
|
||||
const fontStyleNames: Array<FontNameType> = [];
|
||||
const preferredFamilyNames: Array<FontNameType> = [];
|
||||
const preferredStyleNames: Array<FontNameType> = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const recordOffset = nameTableOffset + 6 + i * 12;
|
||||
const platformID = dataView.getUint16(recordOffset, false);
|
||||
@@ -128,7 +201,8 @@ export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
const nameOffsetInTable = dataView.getUint16(recordOffset + 10, false);
|
||||
|
||||
// nameID 1 = Font Family name, nameID 2 = Font Subfamily name (style)
|
||||
if (nameID === 1 || nameID === 2) {
|
||||
// nameID 16 = Typographic Family name, nameID 17 = Typographic Subfamily name
|
||||
if (nameID === 1 || nameID === 2 || nameID === 16 || nameID === 17) {
|
||||
const stringStart = nameTableOffset + stringOffset + nameOffsetInTable;
|
||||
let fontName = '';
|
||||
|
||||
@@ -153,6 +227,10 @@ export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
fontFamilyNames.push(nameEntry);
|
||||
} else if (nameID === 2) {
|
||||
fontStyleNames.push(nameEntry);
|
||||
} else if (nameID === 16) {
|
||||
preferredFamilyNames.push(nameEntry);
|
||||
} else if (nameID === 17) {
|
||||
preferredStyleNames.push(nameEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,23 +240,55 @@ export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
|
||||
}
|
||||
fontFamilyNames.sort((a, b) => b.priority - a.priority);
|
||||
fontStyleNames.sort((a, b) => b.priority - a.priority);
|
||||
const fontStyleName = fontStyleNames[0];
|
||||
const familyName = fontFamilyNames[0]!.name;
|
||||
preferredFamilyNames.sort((a, b) => b.priority - a.priority);
|
||||
preferredStyleNames.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
// Prefer typographic names if available
|
||||
const familyName = (preferredFamilyNames[0] || fontFamilyNames[0])!.name;
|
||||
const fontStyleName = preferredStyleNames[0] || fontStyleNames[0];
|
||||
const styleName = fontStyleName?.name || '';
|
||||
|
||||
// Parse weight and style information
|
||||
let fontWeight = 400;
|
||||
let fontStyle: FontStyle = 'normal';
|
||||
let fsSelection = 0;
|
||||
|
||||
if (os2TableOffset > 0) {
|
||||
try {
|
||||
const weightClass = parseOS2Weight(dataView, os2TableOffset);
|
||||
fontWeight = weightClassToCSSWeight(weightClass);
|
||||
fsSelection = parseOS2Selection(dataView, os2TableOffset);
|
||||
} catch {
|
||||
console.warn('Failed to parse OS/2 table, falling back to style name analysis');
|
||||
}
|
||||
}
|
||||
|
||||
// If OS/2 table weight is default (400) or unavailable, try to infer from style name
|
||||
if (fontWeight === 400 && styleName) {
|
||||
const inferredWeight = inferWeightFromStyleName(styleName);
|
||||
if (inferredWeight !== 400) {
|
||||
fontWeight = inferredWeight;
|
||||
}
|
||||
}
|
||||
|
||||
fontStyle = inferStyleFromName(styleName, fsSelection);
|
||||
|
||||
return {
|
||||
name:
|
||||
fontStyleName && !NO_STYLE_LANGUAGE_IDS.has(fontStyleName.languageID)
|
||||
? `${familyName} ${styleName}`
|
||||
: familyName,
|
||||
family: familyName,
|
||||
style: styleName,
|
||||
weight: fontWeight,
|
||||
style: fontStyle,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse font: ${error}`);
|
||||
return {
|
||||
name: fallbackName,
|
||||
family: fallbackName,
|
||||
style: '',
|
||||
weight: 400,
|
||||
style: 'normal' as FontStyle,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,6 +43,9 @@ export const normalizeToFullLang = (langCode: string): string => {
|
||||
sl: 'sl-SI',
|
||||
sk: 'sk-SK',
|
||||
bo: 'bo-CN',
|
||||
bn: 'bn-BD',
|
||||
ta: 'ta-IN',
|
||||
si: 'si-LK',
|
||||
zh: 'zh-Hans',
|
||||
'zh-cn': 'zh-Hans',
|
||||
'zh-tw': 'zh-Hant',
|
||||
|
||||
@@ -81,7 +81,7 @@ export const navigateToLibrary = (
|
||||
queryParams?: string,
|
||||
navOptions?: { scroll?: boolean },
|
||||
) => {
|
||||
router.push(`/library${queryParams ? `?${queryParams}` : ''}`, navOptions);
|
||||
router.replace(`/library${queryParams ? `?${queryParams}` : ''}`, navOptions);
|
||||
};
|
||||
|
||||
export const redirectToLibrary = () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Chapter {
|
||||
title: string;
|
||||
content: string;
|
||||
text: string;
|
||||
isVolume: boolean;
|
||||
}
|
||||
|
||||
interface Txt2EpubOptions {
|
||||
@@ -112,8 +113,8 @@ export class TxtToEpubConverter {
|
||||
String.raw`(?:^|\n)\s*` +
|
||||
'(' +
|
||||
[
|
||||
String.raw`第[零〇一二三四五六七八九十0-9][零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封])(?:[::、 \(\)0-9]*[^\n-]{0,24})(?!\S)`,
|
||||
String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论)(?:[:: ][^\n-]{0,24})?(?!\S)`,
|
||||
String.raw`第[零〇一二三四五六七八九十0-9][零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封本册部话])(?:[::、 \(\)0-9]*[^\n-]{0,24})(?!\S)`,
|
||||
String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论|后记)(?:[:: ][^\n-]{0,24})?(?!\S)`,
|
||||
].join('|') +
|
||||
')',
|
||||
'gu',
|
||||
@@ -197,7 +198,7 @@ export class TxtToEpubConverter {
|
||||
const formattedSegment = formatSegment(chunks.join('\n'));
|
||||
const title = `${chapters.length + 1}`;
|
||||
const content = `<h2>${title}</h2><p>${formattedSegment}</p>`;
|
||||
chapters.push({ title, content, text: chunks.join('\n') });
|
||||
chapters.push({ title, content, text: chunks.join('\n'), isVolume: false });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -208,7 +209,7 @@ export class TxtToEpubConverter {
|
||||
|
||||
let isVolume = false;
|
||||
if (language === 'zh') {
|
||||
isVolume = /第[零〇一二三四五六七八九十百千万0-9]+卷/.test(title);
|
||||
isVolume = /第[零〇一二三四五六七八九十百千万0-9]+(卷|本|册|部)/.test(title);
|
||||
} else {
|
||||
isVolume = /\b(Part|Volume|Book)\b/i.test(title);
|
||||
}
|
||||
@@ -219,6 +220,7 @@ export class TxtToEpubConverter {
|
||||
title: escapeXml(title),
|
||||
content: `${headTitle}<p>${formattedSegment}</p>`,
|
||||
text: content,
|
||||
isVolume: isVolume,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +235,7 @@ export class TxtToEpubConverter {
|
||||
title: escapeXml(segmentTitle),
|
||||
content: `<h3></h3><p>${formattedSegment}</p>`,
|
||||
text: initialContent,
|
||||
isVolume: false,
|
||||
});
|
||||
}
|
||||
chapters.push(...segmentChapters);
|
||||
@@ -261,20 +264,28 @@ export class TxtToEpubConverter {
|
||||
await zipWriter.add('META-INF/container.xml', new TextReader(containerXml), zipWriteOptions);
|
||||
|
||||
// Create navigation points for TOC
|
||||
const navPoints = chapters
|
||||
.map((chapter, index) => {
|
||||
const id = `chapter${index + 1}`;
|
||||
const playOrder = index + 1;
|
||||
return `
|
||||
<navPoint id="navPoint-${id}" playOrder="${playOrder}">
|
||||
<navLabel>
|
||||
<text>${chapter.title}</text>
|
||||
</navLabel>
|
||||
<content src="./OEBPS/${id}.xhtml" />
|
||||
</navPoint>
|
||||
`.trim();
|
||||
})
|
||||
.join('\n');
|
||||
let isNested = false;
|
||||
let navPoints = ``;
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const id = `chapter${i + 1}`;
|
||||
const playOrder = i + 1;
|
||||
if (chapters[i]!.isVolume && isNested) {
|
||||
navPoints += `</navPoint>\n`;
|
||||
isNested = !isNested;
|
||||
}
|
||||
navPoints +=
|
||||
`<navPoint id="navPoint-${id}" playOrder="${playOrder}">\n` +
|
||||
`<navLabel><text>${chapters[i]!.title}</text></navLabel>\n` +
|
||||
`<content src="./OEBPS/${id}.xhtml" />\n`;
|
||||
if (chapters[i]!.isVolume && !isNested) {
|
||||
isNested = !isNested;
|
||||
} else {
|
||||
navPoints += `</navPoint>\n`;
|
||||
}
|
||||
}
|
||||
if (isNested) {
|
||||
navPoints += `</navPoint>`;
|
||||
}
|
||||
|
||||
// Add NCX file (table of contents)
|
||||
const tocNcx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
@@ -30,7 +30,11 @@ export const walkTextNodes = (root: HTMLElement, rejectTags: string[] = []): HTM
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
|
||||
return true;
|
||||
}
|
||||
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === 'SPAN') {
|
||||
if (
|
||||
node.nodeType === Node.ELEMENT_NODE &&
|
||||
(node as HTMLElement).tagName === 'SPAN' &&
|
||||
node.textContent?.trim()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -35,6 +35,8 @@ export class XCFI {
|
||||
// Convert to 0-based: (step - 2) / 2 = 0, 1, 2, 3, ...
|
||||
return Math.floor((spineStep - 2) / 2);
|
||||
} else if (cfiOrXPath.startsWith('/body/DocFragment[')) {
|
||||
// Note that all indices in XPointer/XPath are 1-based
|
||||
// but the text() offsets are 0-based
|
||||
const match = cfiOrXPath.match(/DocFragment\[(\d+)\]/);
|
||||
if (match) {
|
||||
return parseInt(match[1]!, 10) - 1;
|
||||
@@ -224,10 +226,10 @@ export class XCFI {
|
||||
let index: number;
|
||||
|
||||
if (segmentWithIndexMatch) {
|
||||
// Format: tag[index]
|
||||
// Format: tag[index] (1-based index)
|
||||
const [, tag, indexStr] = segmentWithIndexMatch;
|
||||
tagName = tag!;
|
||||
index = parseInt(indexStr!, 10);
|
||||
index = Math.max(0, parseInt(indexStr!, 10) - 1);
|
||||
} else if (segmentWithoutIndexMatch) {
|
||||
// Format: tag (implicit index 0)
|
||||
const [, tag] = segmentWithoutIndexMatch;
|
||||
@@ -379,7 +381,7 @@ export class XCFI {
|
||||
if (totalSameTagSiblings === 1) {
|
||||
pathParts.unshift(tagName);
|
||||
} else {
|
||||
pathParts.unshift(`${tagName}[${siblingIndex}]`);
|
||||
pathParts.unshift(`${tagName}[${siblingIndex + 1}]`); // Convert to 1-based index for XPointer
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
@@ -545,7 +547,7 @@ export const getXPointerFromCFI = async (
|
||||
// This has neglectable effect on position accuracy as the XPointer still point to the correct element
|
||||
// while offset within the text node is usually ignored by pagination.
|
||||
export const normalizeProgressXPointer = (xpointer: string): string => {
|
||||
const tailingTextRange = /\/text\(\)\.\d+$/;
|
||||
const tailingTextRange = /\/text\(\).*$/;
|
||||
if (xpointer.match(tailingTextRange)) {
|
||||
xpointer = xpointer.replace(tailingTextRange, '');
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
head_sampling_rate = 1
|
||||
head_sampling_rate = 0.01
|
||||
|
||||
[assets]
|
||||
directory = ".open-next/assets"
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 2138ab9838...21e68481d5
Reference in New Issue
Block a user