Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05e9549410 | |||
| 35fbce3506 | |||
| c1d6825793 | |||
| ae6c6970b4 | |||
| f8fef57cf3 | |||
| 4dc191735d | |||
| 4f11f437e1 | |||
| 5773e31990 | |||
| 9dda5d4c88 | |||
| 7f5b6959e1 | |||
| 126981d085 | |||
| 854a578929 | |||
| 6dd0daa597 | |||
| fd24a5e9ac | |||
| 948b35f244 | |||
| 96cc182a8c | |||
| e2291ed5b1 | |||
| 60ddb17547 | |||
| 9a991a31ba | |||
| 6de10c6c3b | |||
| c7d49c1504 | |||
| c721bd4d97 | |||
| 5b0a1968d8 | |||
| ad9064fa46 | |||
| f1c92e5702 | |||
| 92c1441922 | |||
| 3820950c7c | |||
| 08e558bd41 | |||
| 00af4a3d60 | |||
| ada427b134 | |||
| f696b9a573 | |||
| 195beeacac | |||
| 5b23ed35b6 | |||
| 42c8702704 | |||
| ad81376da8 | |||
| 562fea3a36 | |||
| b69d9ed69f | |||
| ad6a21a68f | |||
| 173aa0fcd1 | |||
| 788e08a75a | |||
| ec397a2daf | |||
| 2a5758fbc4 | |||
| 562ccd4b9e | |||
| 4d577774f3 | |||
| 4c10c16491 | |||
| 7695f31a5a | |||
| 06b27f8d39 | |||
| e1691661d9 | |||
| bc8e419d51 | |||
| ccad1ca0d9 | |||
| 75c315fca8 | |||
| bdef593cdd | |||
| d8943fc0c5 | |||
| cdd274e5ca | |||
| 2230741779 | |||
| 0e6950a60f | |||
| 1d4541e353 | |||
| 0a1e0212e2 | |||
| 25b44176b9 | |||
| e1deff341d | |||
| 3fc4c05e50 | |||
| 1843a74dbb | |||
| 9aa6a8e6b9 | |||
| 1de89069ce | |||
| 1e6525d085 | |||
| d9d71b7d38 | |||
| e85dec2b49 | |||
| c0f77b3368 | |||
| 8c9855d167 | |||
| 7c12bc8595 | |||
| 5102f6b0ff | |||
| dd40b9bae0 | |||
| 98b4026990 | |||
| b0640ba55b | |||
| c9a1557674 | |||
| 855f98722c | |||
| 21a82b70c7 | |||
| 519e222883 | |||
| 488a242787 | |||
| 523a03d212 | |||
| d49f3442e8 | |||
| 48dc570809 | |||
| 509320813f | |||
| 54d7e73acb |
@@ -19,6 +19,7 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -81,6 +81,28 @@ jobs:
|
||||
prerelease: false
|
||||
})
|
||||
|
||||
build-koreader-plugin:
|
||||
needs: get-release
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: create KOReader plugin zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
plugin_zip="Readest-${version}-1.koplugin.zip"
|
||||
|
||||
cd apps/readest.koplugin
|
||||
zip -r ../../${plugin_zip} .
|
||||
cd ../..
|
||||
|
||||
echo "Uploading ${plugin_zip} to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} ${plugin_zip} --clobber
|
||||
|
||||
build-tauri:
|
||||
needs: get-release
|
||||
permissions:
|
||||
@@ -153,7 +175,7 @@ jobs:
|
||||
|
||||
- name: install NDK (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
run: sdkmanager "ndk;27.0.11902837"
|
||||
run: sdkmanager "ndk;28.2.13676358"
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm install
|
||||
@@ -200,7 +222,7 @@ jobs:
|
||||
if: matrix.config.release == 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.0.11902837
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
@@ -288,12 +310,20 @@ jobs:
|
||||
echo "Uploading release notes to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} apps/readest-app/release-notes.json --clobber
|
||||
|
||||
- name: upload portable binaries (Windows only)
|
||||
- name: build and upload portable binaries (Windows only)
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Building Portable Binaries"
|
||||
pushd apps/readest-app/
|
||||
echo "NEXT_PUBLIC_PORTABLE_APP=true" >> .env.local
|
||||
pnpm tauri build
|
||||
|
||||
popd
|
||||
echo "Uploading Portable Binaries"
|
||||
arch=${{ matrix.config.arch }}
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
|
||||
@@ -4,9 +4,6 @@ members = [
|
||||
"packages/tauri/crates/tauri",
|
||||
"packages/tauri/crates/tauri-utils",
|
||||
"packages/tauri/crates/tauri-build",
|
||||
"packages/tauri-plugins/plugins/fs",
|
||||
"packages/tauri-plugins/plugins/dialog",
|
||||
"packages/tauri-plugins/plugins/deep-link",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -38,7 +35,3 @@ rust-version = "1.77.2"
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
|
||||
tauri-build = { path = "packages/tauri/crates/tauri-build" }
|
||||
tauri-plugin-os = { path = "packages/tauri-plugins/plugins/os" }
|
||||
tauri-plugin-fs = { path = "packages/tauri-plugins/plugins/fs" }
|
||||
tauri-plugin-dialog = { path = "packages/tauri-plugins/plugins/dialog" }
|
||||
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
|
||||
|
||||
@@ -18,6 +18,7 @@ module.exports = {
|
||||
'ko',
|
||||
'uk',
|
||||
'nl',
|
||||
'sv',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.80",
|
||||
"version": "0.9.88",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
|
||||
"build": "dotenv -e .env.tauri -- next build",
|
||||
"start": "dotenv -e .env.tauri -- next start",
|
||||
"dev-web": "dotenv -e .env.web -- next dev",
|
||||
"dev-web": "dotenv -e .env.web -- next dev --turbopack",
|
||||
"build-web": "dotenv -e .env.web -- next build",
|
||||
"start-web": "dotenv -e .env.web -- next start",
|
||||
"i18n:extract": "i18next-scanner",
|
||||
@@ -55,16 +55,16 @@
|
||||
"@supabase/supabase-js": "^2.55.0",
|
||||
"@tauri-apps/api": "2.8.0",
|
||||
"@tauri-apps/plugin-cli": "^2.4.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.1",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-haptics": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "^2.5.1",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "^2.5.2",
|
||||
"@tauri-apps/plugin-log": "^2.7.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "~2.3.0",
|
||||
"@tauri-apps/plugin-shell": "~2.3.1",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"app-store-server-api": "^0.17.1",
|
||||
@@ -105,7 +105,7 @@
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^15.4.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.8.1",
|
||||
"@tauri-apps/cli": "2.8.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
@@ -120,6 +120,7 @@
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"cpx2": "^8.0.0",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 318 KiB |
@@ -579,5 +579,51 @@
|
||||
"Toggle Translation": "تبديل الترجمة",
|
||||
"Translation Disabled": "تم تعطيل الترجمة",
|
||||
"Minimize": "تصغير",
|
||||
"Maximize or Restore": "تكبير أو استعادة"
|
||||
"Maximize or Restore": "تكبير أو استعادة",
|
||||
"Exit Parallel Read": "الخروج من القراءة المتزامنة",
|
||||
"Enter Parallel Read": "الدخول في القراءة المتزامنة",
|
||||
"Zoom Level": "مستوى التكبير",
|
||||
"Zoom Out": "تصغير",
|
||||
"Reset Zoom": "إعادة تعيين التكبير",
|
||||
"Zoom In": "تكبير",
|
||||
"Zoom Mode": "وضع التكبير",
|
||||
"Single Page": "صفحة واحدة",
|
||||
"Auto Spread": "انتشار تلقائي",
|
||||
"Fit Page": "تناسب الصفحة",
|
||||
"Fit Width": "تناسب العرض",
|
||||
"Failed to select directory": "فشل في اختيار الدليل",
|
||||
"The new data directory must be different from the current one.": "يجب أن يكون دليل البيانات الجديد مختلفًا عن الدليل الحالي.",
|
||||
"Migration failed: {{error}}": "فشل في الترحيل: {{error}}",
|
||||
"Change Data Location": "تغيير موقع البيانات",
|
||||
"Current Data Location": "موقع البيانات الحالي",
|
||||
"Total size: {{size}}": "الحجم الكلي: {{size}}",
|
||||
"Calculating file info...": "جارٍ حساب معلومات الملف...",
|
||||
"New Data Location": "موقع البيانات الجديد",
|
||||
"Choose Different Folder": "اختر مجلدًا مختلفًا",
|
||||
"Choose New Folder": "اختر مجلدًا جديدًا",
|
||||
"Migrating data...": "جارٍ ترحيل البيانات...",
|
||||
"Copying: {{file}}": "جارٍ نسخ: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} من {{total}} ملف",
|
||||
"Migration completed successfully!": "اكتمل الترحيل بنجاح!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "تم نقل بياناتك إلى الموقع الجديد. يرجى إعادة تشغيل التطبيق لإكمال العملية.",
|
||||
"Migration failed": "فشل في الترحيل",
|
||||
"Important Notice": "إشعار مهم",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "سيؤدي ذلك إلى نقل جميع بيانات التطبيق الخاصة بك إلى الموقع الجديد. تأكد من أن الوجهة تحتوي على مساحة خالية كافية.",
|
||||
"Restart App": "إعادة تشغيل التطبيق",
|
||||
"Start Migration": "بدء الترحيل",
|
||||
"Advanced Settings": "إعدادات متقدمة",
|
||||
"File count: {{size}}": "عدد الملفات: {{size}}",
|
||||
"Background Read Aloud": "قراءة بصوت عالٍ في الخلفية",
|
||||
"Ready to read aloud": "جاهز للقراءة بصوت عالٍ",
|
||||
"Read Aloud": "القراءة بصوت عالٍ",
|
||||
"Screen Brightness": "سطوع الشاشة",
|
||||
"Background Image": "صورة الخلفية",
|
||||
"Import Image": "استيراد صورة",
|
||||
"Opacity": "شفافية",
|
||||
"Size": "حجم",
|
||||
"Cover": "غطاء",
|
||||
"Contain": "احتواء",
|
||||
"{{number}} pages left in chapter": "تبقّت {{number}} صفحات في هذا الفصل",
|
||||
"Device": "الجهاز",
|
||||
"E-Ink Mode": "وضع الحبر الإلكتروني"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "অনুবাদ টগল করুন",
|
||||
"Translation Disabled": "অনুবাদ অক্ষম",
|
||||
"Minimize": "সঙ্কুচিত করুন",
|
||||
"Maximize or Restore": "বৃহৎ করুন বা পুনরুদ্ধার করুন"
|
||||
"Maximize or Restore": "বৃহৎ করুন বা পুনরুদ্ধার করুন",
|
||||
"Exit Parallel Read": "সমান্তরাল পড়া থেকে বেরিয়ে আসুন",
|
||||
"Enter Parallel Read": "সমান্তরাল পড়ায় প্রবেশ করুন",
|
||||
"Zoom Level": "জুম লেভেল",
|
||||
"Zoom Out": "জুম আউট",
|
||||
"Reset Zoom": "জুম রিসেট",
|
||||
"Zoom In": "জুম ইন",
|
||||
"Zoom Mode": "জুম মোড",
|
||||
"Single Page": "একক পৃষ্ঠা",
|
||||
"Auto Spread": "অটো স্প্রেড",
|
||||
"Fit Page": "ফিট পৃষ্ঠা",
|
||||
"Fit Width": "ফিট প্রস্থ",
|
||||
"Failed to select directory": "ডিরেক্টরি নির্বাচন করতে ব্যর্থ",
|
||||
"The new data directory must be different from the current one.": "নতুন ডেটা ডিরেক্টরি বর্তমানটির থেকে আলাদা হতে হবে।",
|
||||
"Migration failed: {{error}}": "মাইগ্রেশন ব্যর্থ হয়েছে: {{error}}",
|
||||
"Change Data Location": "ডেটা অবস্থান পরিবর্তন করুন",
|
||||
"Current Data Location": "বর্তমান ডেটা অবস্থান",
|
||||
"Total size: {{size}}": "মোট আকার: {{size}}",
|
||||
"Calculating file info...": "ফাইলের তথ্য গণনা করা হচ্ছে...",
|
||||
"New Data Location": "নতুন ডেটা অবস্থান",
|
||||
"Choose Different Folder": "ভিন্ন ফোল্ডার নির্বাচন করুন",
|
||||
"Choose New Folder": "নতুন ফোল্ডার নির্বাচন করুন",
|
||||
"Migrating data...": "ডেটা স্থানান্তরিত হচ্ছে...",
|
||||
"Copying: {{file}}": "কপি করা হচ্ছে: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} এর {{total}} ফাইল",
|
||||
"Migration completed successfully!": "মাইগ্রেশন সফলভাবে সম্পন্ন হয়েছে!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "আপনার ডেটা নতুন অবস্থানে স্থানান্তরিত হয়েছে। প্রক্রিয়া সম্পন্ন করতে দয়া করে অ্যাপ্লিকেশনটি পুনরায় চালু করুন।",
|
||||
"Migration failed": "মাইগ্রেশন ব্যর্থ হয়েছে",
|
||||
"Important Notice": "গুরুতর বিজ্ঞপ্তি",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "এটি আপনার সমস্ত অ্যাপ ডেটা নতুন অবস্থানে স্থানান্তরিত করবে। নিশ্চিত করুন যে গন্তব্যে পর্যাপ্ত ফ্রি স্পেস রয়েছে।",
|
||||
"Restart App": "অ্যাপ পুনরায় চালু করুন",
|
||||
"Start Migration": "মাইগ্রেশন শুরু করুন",
|
||||
"Advanced Settings": "উন্নত সেটিংস",
|
||||
"File count: {{size}}": "ফাইলের সংখ্যা: {{size}}",
|
||||
"Background Read Aloud": "পটভূমিতে উচ্চস্বরে পড়ুন",
|
||||
"Ready to read aloud": "উচ্চস্বরে পড়ার জন্য প্রস্তুত",
|
||||
"Read Aloud": "উচ্চস্বরে পড়ুন",
|
||||
"Screen Brightness": "স্ক্রিন উজ্জ্বলতা",
|
||||
"Background Image": "ব্যাকগ্রাউন্ড ছবি",
|
||||
"Import Image": "ছবি আমদানি করুন",
|
||||
"Opacity": "স্বচ্ছতা",
|
||||
"Size": "আকার",
|
||||
"Cover": "ঢাকা",
|
||||
"Contain": "ধারণ",
|
||||
"{{number}} pages left in chapter": "অধ্যায়ে {{number}} পৃষ্ঠা বাকি",
|
||||
"Device": "যন্ত্র",
|
||||
"E-Ink Mode": "ই-ইঙ্ক মোড"
|
||||
}
|
||||
|
||||
@@ -539,25 +539,71 @@
|
||||
"OFF": "བརྗེ་བ",
|
||||
"Reading Progress": "ཀློག་པའི་ཡར་འཕེལ།",
|
||||
"Page Margin": "ཤོག་གདོང་།",
|
||||
"Remove Bookmark": "བརྗེ་བའི་སྒྲིག་འདེམས་པ།",
|
||||
"Add Bookmark": "བརྗེ་བའི་སྒྲིག་འདེམས་པ།",
|
||||
"Books Content": "དེབ་ཀྱི་དོན་ཚན",
|
||||
"Jump to Location": "གནས་སྟངས་འདེམས་པ།",
|
||||
"Unpin Notebook": "དེབ་སྣོད་འདེམས་པ།",
|
||||
"Pin Notebook": "དེབ་སྣོད་འདེམས་པ།",
|
||||
"Hide Search Bar": "འཚོལ་བའི་སྒྲིག་འདེམས་པ།",
|
||||
"Show Search Bar": "འཚོལ་བའི་སྒྲིག་འདེམས་པ།",
|
||||
"On {{current}} of {{total}} page": "ཤོག་ {{current}} དང་ {{total}}",
|
||||
"Section Title": "དོན་ཚན་གྱི་མགོ་སྒམ།",
|
||||
"Decrease": "བཏང་བ།",
|
||||
"Increase": "བཀྲ་ཤིས་བཏང་བ།",
|
||||
"Settings Panels": "སྒྲིག་སྟངས་མ་ལག",
|
||||
"Settings": "སྒྲིག་སྟངས",
|
||||
"Unpin Sidebar": "གཡས་སྒོ་འདེམས་པ།",
|
||||
"Pin Sidebar": "གཡས་སྒོ་འདེམས་པ།",
|
||||
"Toggle Sidebar": "གཡས་སྒོ་འདེམས་པ།",
|
||||
"Toggle Translation": "བརྗེ་བའི་སྒྲིག་འདེམས་པ།",
|
||||
"Translation Disabled": "བརྗེ་བའི་སྒྲིག་འདེམས་པ།",
|
||||
"Minimize": "སྐར་མ་འཇོག",
|
||||
"Maximize or Restore": "བཀྲ་ཤིས་བཏང་བ།"
|
||||
"Remove Bookmark": "དེབ་གྲངས་འདོར་བ།",
|
||||
"Add Bookmark": "དེབ་གྲངས་སྣོན་པ།",
|
||||
"Books Content": "དེབ་ཀྱི་ནང་དོན།",
|
||||
"Jump to Location": "གནས་ས་ལ་འགྲོ།",
|
||||
"Unpin Notebook": "དེབ་སྣོད་བཤོལ་བ།",
|
||||
"Pin Notebook": "དེབ་སྣོད་བཀར་བ།",
|
||||
"Hide Search Bar": "འཚོལ་བཤེར་སྒོ་སྦེད།",
|
||||
"Show Search Bar": "འཚོལ་བཤེར་སྒོ་སྟོན།",
|
||||
"On {{current}} of {{total}} page": "ཤོག་ངོས་ {{total}} ནས་ {{current}} ལ།",
|
||||
"Section Title": "དོན་ཚན་མགོ་མིང་།",
|
||||
"Decrease": "མར་འབེབས།",
|
||||
"Increase": "ཡར་འཕར།",
|
||||
"Settings Panels": "སྒྲིག་སྟངས་གནས་སྟོན།",
|
||||
"Settings": "སྒྲིག་སྟངས།",
|
||||
"Unpin Sidebar": "ཟུར་སྒོ་བཤོལ་བ།",
|
||||
"Pin Sidebar": "ཟུར་སྒོ་བཀར་བ།",
|
||||
"Toggle Sidebar": "ཟུར་སྒོ་སྤོ་བ།",
|
||||
"Toggle Translation": "ཡིག་སྒྱུར་སྤོ་བ།",
|
||||
"Translation Disabled": "ཡིག་སྒྱུར་མེད།",
|
||||
"Minimize": "ཆུང་དུ་བསྡུ།",
|
||||
"Maximize or Restore": "ཆེན་པོ་བཟོ/གསོག།",
|
||||
"Exit Parallel Read": "མཉམ་དུ་ཀློག་ཕྱིར་འཐེན།",
|
||||
"Enter Parallel Read": "མཉམ་དུ་ཀློག་འཛུལ།",
|
||||
"Zoom Level": "རྒྱབ་སྐོར་གྱི་གདམ་ཁ",
|
||||
"Zoom Out": "རྒྱབ་སྐོར་བཏང་བ།",
|
||||
"Reset Zoom": "རྒྱབ་སྐོར་རྩོལ་བ།",
|
||||
"Zoom In": "རྒྱབ་སྐོར་འཕེལ་བ།",
|
||||
"Zoom Mode": "རྒྱབ་སྐོར་སྤོ་བ།",
|
||||
"Single Page": "དེབ་གཅིག",
|
||||
"Auto Spread": "རང་འགུལ་སྤྲོད།",
|
||||
"Fit Page": "དེབ་འདེམས་པ།",
|
||||
"Fit Width": "ཁྱབ་སྒྲིག་འདེམས་པ།",
|
||||
"Failed to select directory": "གནས་ས་འདེམས་པ་བྱས་མ་ཐུབ།",
|
||||
"The new data directory must be different from the current one.": "གསར་བཅས་སྐོར་འདེམས་པ་གནས་ས་དང་མཉམ་འབྱུང་བར་བྱས་མ་ཐུབ།",
|
||||
"Migration failed: {{error}}": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
|
||||
"Change Data Location": "གསར་བཅས་སྐོར་འདེམས་པ།",
|
||||
"Current Data Location": "ད་ལྟ་བཅས་སྐོར་འདེམས་པ།",
|
||||
"Total size: {{size}}": "དངོས་གནས་བཅས་: {{size}}",
|
||||
"Calculating file info...": "དེབ་གནས་བཅས་པའི་གདམ་ཁ་བཟོ་བྱས་མ་ཐུབ།",
|
||||
"New Data Location": "གསར་བཅས་སྐོར་འདེམས་པ།",
|
||||
"Choose Different Folder": "གཞན་དེབ་སྣོད་བཀར་བ།",
|
||||
"Choose New Folder": "གསར་བཅས་སྣོད་བཀར་བ།",
|
||||
"Migrating data...": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Copying: {{file}}": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"{{current}} of {{total}} files": "{{current}} དང་ {{total}} དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Migration completed successfully!": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "ཁྱོད་ཀྱི་དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Migration failed": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
|
||||
"Important Notice": "དོན་གཞི་བརྗེ་བ།",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "འདི་ནས་ཁྱོད་ཀྱི་ཨང་གཏོད་དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Restart App": "དེབ་ཀྱི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Start Migration": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
|
||||
"Advanced Settings": "དབང་བསྐྱོད་སྒྲིག་སྟངས།",
|
||||
"File count: {{size}}": "དེབ་གནས་བཅས་པའི་ཨང་། {{size}}",
|
||||
"Background Read Aloud": "གནས་སྟངས་ཀྱིས་ཀློག་བྱེད།",
|
||||
"Ready to read aloud": "དེབ་ཀྱི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Read Aloud": "ཀློག་བྱེད།",
|
||||
"Screen Brightness": "རྒྱབ་སྐོར་གྱི་འོད་ཟེར།",
|
||||
"Background Image": "གནས་སྟངས་ཀྱི་རི་མོ།",
|
||||
"Import Image": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Opacity": "སྤོ་སྐོར་གྱི་འོད་ཟེར།",
|
||||
"Size": "ཨང་",
|
||||
"Cover": "དེབ་གྱི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Contain": "འབྱོར་བ།",
|
||||
"{{number}} pages left in chapter": "དོན་ཚན་ནང་ཤོག་ཨང་ {{number}} ལོག་གི་འདུག།",
|
||||
"Device": "རྐྱབ་སྐོར",
|
||||
"E-Ink Mode": "ཡིག་སྒྱུར་སྤོ་བ"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "Übersetzung umschalten",
|
||||
"Translation Disabled": "Übersetzung deaktiviert",
|
||||
"Minimize": "Minimieren",
|
||||
"Maximize or Restore": "Maximieren oder Wiederherstellen"
|
||||
"Maximize or Restore": "Maximieren oder Wiederherstellen",
|
||||
"Exit Parallel Read": "Parallel Read beenden",
|
||||
"Enter Parallel Read": "Parallel Read starten",
|
||||
"Zoom Level": "Zoomstufe",
|
||||
"Zoom Out": "Hineinzoomen",
|
||||
"Reset Zoom": "Zoom zurücksetzen",
|
||||
"Zoom In": "Herauszoomen",
|
||||
"Zoom Mode": "Zoom-Modus",
|
||||
"Single Page": "Einzelne Seite",
|
||||
"Auto Spread": "Automatische Verbreitung",
|
||||
"Fit Page": "Seite anpassen",
|
||||
"Fit Width": "Breite anpassen",
|
||||
"Failed to select directory": "Fehler beim Auswählen des Verzeichnisses",
|
||||
"The new data directory must be different from the current one.": "Das neue Datenverzeichnis muss sich vom aktuellen unterscheiden.",
|
||||
"Migration failed: {{error}}": "Migration fehlgeschlagen: {{error}}",
|
||||
"Change Data Location": "Datenstandort ändern",
|
||||
"Current Data Location": "Aktueller Datenstandort",
|
||||
"Total size: {{size}}": "Gesamtgröße: {{size}}",
|
||||
"Calculating file info...": "Dateiinformationen werden berechnet...",
|
||||
"New Data Location": "Neuer Datenstandort",
|
||||
"Choose Different Folder": "Anderen Ordner wählen",
|
||||
"Choose New Folder": "Neuen Ordner wählen",
|
||||
"Migrating data...": "Daten werden migriert...",
|
||||
"Copying: {{file}}": "Kopiere: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} von {{total}} Dateien",
|
||||
"Migration completed successfully!": "Migration erfolgreich abgeschlossen!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ihre Daten wurden an den neuen Speicherort verschoben. Bitte starten Sie die Anwendung neu, um den Vorgang abzuschließen.",
|
||||
"Migration failed": "Migration fehlgeschlagen",
|
||||
"Important Notice": "Wichtiger Hinweis",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Dies wird alle Ihre App-Daten an den neuen Speicherort verschieben. Stellen Sie sicher, dass das Ziel über ausreichend freien Speicherplatz verfügt.",
|
||||
"Restart App": "App neu starten",
|
||||
"Start Migration": "Migration starten",
|
||||
"Advanced Settings": "Erweiterte Einstellungen",
|
||||
"File count: {{size}}": "Dateianzahl: {{size}}",
|
||||
"Background Read Aloud": "Hintergrund-Vorlesen",
|
||||
"Ready to read aloud": "Bereit zum Vorlesen",
|
||||
"Read Aloud": "Vorlesen",
|
||||
"Screen Brightness": "Bildschirmhelligkeit",
|
||||
"Background Image": "Hintergrundbild",
|
||||
"Import Image": "Bild importieren",
|
||||
"Opacity": "Opazität",
|
||||
"Size": "Größe",
|
||||
"Cover": "Cover",
|
||||
"Contain": "Inhalt",
|
||||
"{{number}} pages left in chapter": "{{number}} Seiten verbleibend im Kapitel",
|
||||
"Device": "Gerät",
|
||||
"E-Ink Mode": "E-Ink-Modus"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "Εναλλαγή μετάφρασης",
|
||||
"Translation Disabled": "Η μετάφραση είναι απενεργοποιημένη",
|
||||
"Minimize": "Ελαχιστοποίηση",
|
||||
"Maximize or Restore": "Μεγιστοποίηση ή Επαναφορά"
|
||||
"Maximize or Restore": "Μεγιστοποίηση ή Επαναφορά",
|
||||
"Exit Parallel Read": "Έξοδος από την παράλληλη ανάγνωση",
|
||||
"Enter Parallel Read": "Είσοδος στην παράλληλη ανάγνωση",
|
||||
"Zoom Level": "Επίπεδο ζουμ",
|
||||
"Zoom Out": "Μείωση ζουμ",
|
||||
"Reset Zoom": "Επαναφορά ζουμ",
|
||||
"Zoom In": "Αύξηση ζουμ",
|
||||
"Zoom Mode": "Λειτουργία ζουμ",
|
||||
"Single Page": "Μοναδική σελίδα",
|
||||
"Auto Spread": "Αυτόματη εξάπλωση",
|
||||
"Fit Page": "Προσαρμογή σελίδας",
|
||||
"Fit Width": "Προσαρμογή πλάτους",
|
||||
"Failed to select directory": "Αποτυχία επιλογής καταλόγου",
|
||||
"The new data directory must be different from the current one.": "Ο νέος κατάλογος δεδομένων πρέπει να είναι διαφορετικός από τον τρέχοντα.",
|
||||
"Migration failed: {{error}}": "Η μετανάστευση απέτυχε: {{error}}",
|
||||
"Change Data Location": "Αλλαγή τοποθεσίας δεδομένων",
|
||||
"Current Data Location": "Τρέχουσα τοποθεσία δεδομένων",
|
||||
"Total size: {{size}}": "Συνολικό μέγεθος: {{size}}",
|
||||
"Calculating file info...": "Υπολογισμός πληροφοριών αρχείου...",
|
||||
"New Data Location": "Νέα τοποθεσία δεδομένων",
|
||||
"Choose Different Folder": "Επιλέξτε διαφορετικό φάκελο",
|
||||
"Choose New Folder": "Επιλέξτε νέο φάκελο",
|
||||
"Migrating data...": "Μεταφορά δεδομένων...",
|
||||
"Copying: {{file}}": "Αντιγραφή: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} από {{total}} αρχεία",
|
||||
"Migration completed successfully!": "Η μετανάστευση ολοκληρώθηκε με επιτυχία!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Τα δεδομένα σας έχουν μεταφερθεί στη νέα τοποθεσία. Παρακαλώ επανεκκινήστε την εφαρμογή για να ολοκληρώσετε τη διαδικασία.",
|
||||
"Migration failed": "Η μετανάστευση απέτυχε",
|
||||
"Important Notice": "Σημαντική ειδοποίηση",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Αυτό θα μεταφέρει όλα τα δεδομένα της εφαρμογής σας στη νέα τοποθεσία. Βεβαιωθείτε ότι ο προορισμός έχει αρκετό ελεύθερο χώρο.",
|
||||
"Restart App": "Επανεκκίνηση εφαρμογής",
|
||||
"Start Migration": "Έναρξη μετανάστευσης",
|
||||
"Advanced Settings": "Προηγμένες ρυθμίσεις",
|
||||
"File count: {{size}}": "Αριθμός αρχείων: {{size}}",
|
||||
"Background Read Aloud": "Ανάγνωση στο παρασκήνιο",
|
||||
"Ready to read aloud": "Έτοιμο για ανάγνωση",
|
||||
"Read Aloud": "Ανάγνωση",
|
||||
"Screen Brightness": "Φωτεινότητα οθόνης",
|
||||
"Background Image": "Εικόνα φόντου",
|
||||
"Import Image": "Εισαγωγή εικόνας",
|
||||
"Opacity": "Αδιαφάνεια",
|
||||
"Size": "Μέγεθος",
|
||||
"Cover": "Εξώφυλλο",
|
||||
"Contain": "Περιέχει",
|
||||
"{{number}} pages left in chapter": "Μένουν {{number}} σελίδες στο κεφάλαιο",
|
||||
"Device": "Συσκευή",
|
||||
"E-Ink Mode": "Λειτουργία E-Ink"
|
||||
}
|
||||
|
||||
@@ -567,5 +567,51 @@
|
||||
"Toggle Translation": "Alternar Traducción",
|
||||
"Translation Disabled": "Traducción Deshabilitada",
|
||||
"Minimize": "Minimizar",
|
||||
"Maximize or Restore": "Maximizar o Restaurar"
|
||||
"Maximize or Restore": "Maximizar o Restaurar",
|
||||
"Exit Parallel Read": "Salir de Lectura Paralela",
|
||||
"Enter Parallel Read": "Entrar en Lectura Paralela",
|
||||
"Zoom Level": "Nivel de Zoom",
|
||||
"Zoom Out": "Alejar",
|
||||
"Reset Zoom": "Restablecer Zoom",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Mode": "Modo de Zoom",
|
||||
"Single Page": "Página Única",
|
||||
"Auto Spread": "Distribución Automática",
|
||||
"Fit Page": "Ajustar Página",
|
||||
"Fit Width": "Ajustar Ancho",
|
||||
"Failed to select directory": "Error al seleccionar el directorio",
|
||||
"The new data directory must be different from the current one.": "El nuevo directorio de datos debe ser diferente del actual.",
|
||||
"Migration failed: {{error}}": "La migración falló: {{error}}",
|
||||
"Change Data Location": "Cambiar Ubicación de Datos",
|
||||
"Current Data Location": "Ubicación Actual de Datos",
|
||||
"Total size: {{size}}": "Tamaño total: {{size}}",
|
||||
"Calculating file info...": "Calculando información del archivo...",
|
||||
"New Data Location": "Nueva Ubicación de Datos",
|
||||
"Choose Different Folder": "Elegir Carpeta Diferente",
|
||||
"Choose New Folder": "Elegir Nueva Carpeta",
|
||||
"Migrating data...": "Migrando datos...",
|
||||
"Copying: {{file}}": "Copiando: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} de {{total}} archivos",
|
||||
"Migration completed successfully!": "¡Migración completada con éxito!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Tus datos han sido movidos a la nueva ubicación. Por favor, reinicia la aplicación para completar el proceso.",
|
||||
"Migration failed": "La migración falló",
|
||||
"Important Notice": "Aviso Importante",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Esto moverá todos los datos de tu aplicación a la nueva ubicación. Asegúrate de que el destino tenga suficiente espacio libre.",
|
||||
"Restart App": "Reiniciar Aplicación",
|
||||
"Start Migration": "Iniciar Migración",
|
||||
"Advanced Settings": "Configuraciones Avanzadas",
|
||||
"File count: {{size}}": "Número de archivos: {{size}}",
|
||||
"Background Read Aloud": "Lectura en segundo plano",
|
||||
"Ready to read aloud": "Listo para leer en voz alta",
|
||||
"Read Aloud": "Leer en voz alta",
|
||||
"Screen Brightness": "Brillo de pantalla",
|
||||
"Background Image": "Imagen de fondo",
|
||||
"Import Image": "Importar imagen",
|
||||
"Opacity": "Opacidad",
|
||||
"Size": "Tamaño",
|
||||
"Cover": "Cubierta",
|
||||
"Contain": "Contener",
|
||||
"{{number}} pages left in chapter": "{{number}} páginas restantes en el capítulo",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modo E-Ink"
|
||||
}
|
||||
|
||||
@@ -567,5 +567,51 @@
|
||||
"Toggle Translation": "Alternar Traduction",
|
||||
"Translation Disabled": "Traduction Désactivée",
|
||||
"Minimize": "Minimiser",
|
||||
"Maximize or Restore": "Maximiser ou Restaurer"
|
||||
"Maximize or Restore": "Maximiser ou Restaurer",
|
||||
"Exit Parallel Read": "Quitter la Lecture Parallèle",
|
||||
"Enter Parallel Read": "Entrer dans la Lecture Parallèle",
|
||||
"Zoom Level": "Zoom",
|
||||
"Zoom Out": "Réduire le Zoom",
|
||||
"Reset Zoom": "Réinitialiser le Zoom",
|
||||
"Zoom In": "Agrandir le Zoom",
|
||||
"Zoom Mode": "Mode Zoom",
|
||||
"Single Page": "Page Unique",
|
||||
"Auto Spread": "Répartition Automatique",
|
||||
"Fit Page": "Ajuster la Page",
|
||||
"Fit Width": "Ajuster la Largeur",
|
||||
"Failed to select directory": "Échec de la sélection du répertoire",
|
||||
"The new data directory must be different from the current one.": "Le nouveau répertoire de données doit être différent de l'actuel.",
|
||||
"Migration failed: {{error}}": "La migration a échoué : {{error}}",
|
||||
"Change Data Location": "Changer l'Emplacement des Données",
|
||||
"Current Data Location": "Emplacement Actuel des Données",
|
||||
"Total size: {{size}}": "Taille totale : {{size}}",
|
||||
"Calculating file info...": "Calcul des informations sur le fichier...",
|
||||
"New Data Location": "Nouvel Emplacement des Données",
|
||||
"Choose Different Folder": "Choisir un Dossier Différent",
|
||||
"Choose New Folder": "Choisir un Nouveau Dossier",
|
||||
"Migrating data...": "Migration des données...",
|
||||
"Copying: {{file}}": "Copie : {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} sur {{total}} fichiers",
|
||||
"Migration completed successfully!": "Migration terminée avec succès !",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Vos données ont été déplacées vers le nouvel emplacement. Veuillez redémarrer l'application pour terminer le processus.",
|
||||
"Migration failed": "La migration a échoué",
|
||||
"Important Notice": "Avis Important",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Cela déplacera toutes vos données d'application vers le nouvel emplacement. Assurez-vous que la destination dispose de suffisamment d'espace libre.",
|
||||
"Restart App": "Redémarrer l'Application",
|
||||
"Start Migration": "Démarrer la Migration",
|
||||
"Advanced Settings": "Paramètres Avancés",
|
||||
"File count: {{size}}": "Nombre de fichiers : {{size}}",
|
||||
"Background Read Aloud": "Lecture Vocale en Arrière-Plan",
|
||||
"Ready to read aloud": "Prêt à lire à haute voix",
|
||||
"Read Aloud": "Lire à haute voix",
|
||||
"Screen Brightness": "Luminosité de l'Écran",
|
||||
"Background Image": "Image de Fond",
|
||||
"Import Image": "Importer une Image",
|
||||
"Opacity": "Opacité",
|
||||
"Size": "Taille",
|
||||
"Cover": "Couverture",
|
||||
"Contain": "Contenir",
|
||||
"{{number}} pages left in chapter": "{{number}} pages restantes dans le chapitre",
|
||||
"Device": "Appareil",
|
||||
"E-Ink Mode": "Mode E-Ink"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "अनुवाद टॉगल करें",
|
||||
"Translation Disabled": "अनुवाद अक्षम",
|
||||
"Minimize": "न्यूनतम करें",
|
||||
"Maximize or Restore": "अधिकतम या पुनर्स्थापित करें"
|
||||
"Maximize or Restore": "अधिकतम या पुनर्स्थापित करें",
|
||||
"Exit Parallel Read": "पैरालल रीड से बाहर निकलें",
|
||||
"Enter Parallel Read": "पैरालल रीड में प्रवेश करें",
|
||||
"Zoom Level": "ज़ूम स्तर",
|
||||
"Zoom Out": "ज़ूम आउट",
|
||||
"Reset Zoom": "ज़ूम रीसेट करें",
|
||||
"Zoom In": "ज़ूम इन",
|
||||
"Zoom Mode": "ज़ूम मोड",
|
||||
"Single Page": "एकल पृष्ठ",
|
||||
"Auto Spread": "स्वचालित फैलाव",
|
||||
"Fit Page": "पृष्ठ में फिट करें",
|
||||
"Fit Width": "चौड़ाई में फिट करें",
|
||||
"Failed to select directory": "निर्देशिका का चयन करने में विफल",
|
||||
"The new data directory must be different from the current one.": "नया डेटा निर्देशिका वर्तमान से भिन्न होना चाहिए।",
|
||||
"Migration failed: {{error}}": "माइग्रेशन विफल: {{error}}",
|
||||
"Change Data Location": "डेटा स्थान बदलें",
|
||||
"Current Data Location": "वर्तमान डेटा स्थान",
|
||||
"Total size: {{size}}": "कुल आकार: {{size}}",
|
||||
"Calculating file info...": "फ़ाइल जानकारी की गणना कर रहे हैं...",
|
||||
"New Data Location": "नया डेटा स्थान",
|
||||
"Choose Different Folder": "विभिन्न फ़ोल्डर चुनें",
|
||||
"Choose New Folder": "नया फ़ोल्डर चुनें",
|
||||
"Migrating data...": "डेटा माइग्रेट कर रहे हैं...",
|
||||
"Copying: {{file}}": "कॉपी कर रहे हैं: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} में से {{total}} फ़ाइलें",
|
||||
"Migration completed successfully!": "माइग्रेशन सफलतापूर्वक पूरा हुआ!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "आपका डेटा नए स्थान पर स्थानांतरित कर दिया गया है। कृपया प्रक्रिया को पूरा करने के लिए एप्लिकेशन को पुनरारंभ करें।",
|
||||
"Migration failed": "माइग्रेशन विफल",
|
||||
"Important Notice": "महत्वपूर्ण सूचना",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "यह आपके सभी ऐप डेटा को नए स्थान पर स्थानांतरित कर देगा। सुनिश्चित करें कि गंतव्य में पर्याप्त खाली स्थान है।",
|
||||
"Restart App": "एप्लिकेशन पुनरारंभ करें",
|
||||
"Start Migration": "माइग्रेशन प्रारंभ करें",
|
||||
"Advanced Settings": "उन्नत सेटिंग्स",
|
||||
"File count: {{size}}": "फ़ाइलों की संख्या: {{size}}",
|
||||
"Background Read Aloud": "पृष्ठभूमि में वाचन",
|
||||
"Ready to read aloud": "उच्च स्वर में पढ़ने के लिए तैयार",
|
||||
"Read Aloud": "उच्च स्वर में पढ़ें",
|
||||
"Screen Brightness": "स्क्रीन ब्राइटनेस",
|
||||
"Background Image": "पृष्ठभूमि छवि",
|
||||
"Import Image": "छवि आयात करें",
|
||||
"Opacity": "अस्पष्टता",
|
||||
"Size": "आकार",
|
||||
"Cover": "कवर",
|
||||
"Contain": "समाहित करें",
|
||||
"{{number}} pages left in chapter": "इस अध्याय में {{number}} पृष्ठ शेष हैं",
|
||||
"Device": "डिवाइस",
|
||||
"E-Ink Mode": "ई-इंक मोड"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "Toggel Terjemahan",
|
||||
"Translation Disabled": "Terjemahan Dinonaktifkan",
|
||||
"Minimize": "Minimalkan",
|
||||
"Maximize or Restore": "Maksimalkan atau Pulihkan"
|
||||
"Maximize or Restore": "Maksimalkan atau Pulihkan",
|
||||
"Exit Parallel Read": "Keluar dari Baca Paralel",
|
||||
"Enter Parallel Read": "Masuk ke Baca Paralel",
|
||||
"Zoom Level": "Level Zoom",
|
||||
"Zoom Out": "Perbesar Zoom",
|
||||
"Reset Zoom": "Reset Zoom",
|
||||
"Zoom In": "Perkecil Zoom",
|
||||
"Zoom Mode": "Mode Zoom",
|
||||
"Single Page": "Halaman Tunggal",
|
||||
"Auto Spread": "Penyebaran Otomatis",
|
||||
"Fit Page": "Sesuaikan Halaman",
|
||||
"Fit Width": "Sesuaikan Lebar",
|
||||
"Failed to select directory": "Gagal memilih direktori",
|
||||
"The new data directory must be different from the current one.": "Direktori data baru harus berbeda dari yang saat ini.",
|
||||
"Migration failed: {{error}}": "Migrasi gagal: {{error}}",
|
||||
"Change Data Location": "Ubah Lokasi Data",
|
||||
"Current Data Location": "Lokasi Data Saat Ini",
|
||||
"Total size: {{size}}": "Ukuran total: {{size}}",
|
||||
"Calculating file info...": "Menghitung informasi file...",
|
||||
"New Data Location": "Lokasi Data Baru",
|
||||
"Choose Different Folder": "Pilih Folder Berbeda",
|
||||
"Choose New Folder": "Pilih Folder Baru",
|
||||
"Migrating data...": "Migrasi data...",
|
||||
"Copying: {{file}}": "Menyalin: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} dari {{total}} file",
|
||||
"Migration completed successfully!": "Migrasi selesai dengan sukses!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Data Anda telah dipindahkan ke lokasi baru. Silakan restart aplikasi untuk menyelesaikan proses.",
|
||||
"Migration failed": "Migrasi gagal",
|
||||
"Important Notice": "Pemberitahuan Penting",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Ini akan memindahkan semua data aplikasi Anda ke lokasi baru. Pastikan tujuan memiliki cukup ruang kosong.",
|
||||
"Restart App": "Restart Aplikasi",
|
||||
"Start Migration": "Mulai Migrasi",
|
||||
"Advanced Settings": "Pengaturan Lanjutan",
|
||||
"File count: {{size}}": "Jumlah file: {{size}}",
|
||||
"Background Read Aloud": "Bacakan Latar Belakang",
|
||||
"Ready to read aloud": "Siap untuk dibacakan",
|
||||
"Read Aloud": "Bacakan",
|
||||
"Screen Brightness": "Kecerahan Layar",
|
||||
"Background Image": "Gambar Latar Belakang",
|
||||
"Import Image": "Impor Gambar",
|
||||
"Opacity": "Keburaman",
|
||||
"Size": "Ukuran",
|
||||
"Cover": "Sampul",
|
||||
"Contain": "Mengandung",
|
||||
"{{number}} pages left in chapter": "{{number}} halaman tersisa di bab",
|
||||
"Device": "Perangkat",
|
||||
"E-Ink Mode": "Mode E-Ink"
|
||||
}
|
||||
|
||||
@@ -567,5 +567,51 @@
|
||||
"Toggle Translation": "Toggel Terjemahan",
|
||||
"Translation Disabled": "Terjemahan Dinonaktifkan",
|
||||
"Minimize": "Minimalkan",
|
||||
"Maximize or Restore": "Maksimalkan atau Pulihkan"
|
||||
"Maximize or Restore": "Maksimalkan atau Pulihkan",
|
||||
"Exit Parallel Read": "Esci dalla lettura parallela",
|
||||
"Enter Parallel Read": "Entra nella lettura parallela",
|
||||
"Zoom Level": "Livello di zoom",
|
||||
"Zoom Out": "Riduci zoom",
|
||||
"Reset Zoom": "Ripristina zoom",
|
||||
"Zoom In": "Inganna zoom",
|
||||
"Zoom Mode": "Modalità zoom",
|
||||
"Single Page": "Pagina singola",
|
||||
"Auto Spread": "Distribuzione automatica",
|
||||
"Fit Page": "Adatta pagina",
|
||||
"Fit Width": "Adatta larghezza",
|
||||
"Failed to select directory": "Impossibile selezionare la directory",
|
||||
"The new data directory must be different from the current one.": "La nuova directory dei dati deve essere diversa da quella attuale.",
|
||||
"Migration failed: {{error}}": "Migrzione fallita: {{error}}",
|
||||
"Change Data Location": "Cambia posizione dei dati",
|
||||
"Current Data Location": "Posizione attuale dei dati",
|
||||
"Total size: {{size}}": "Dimensione totale: {{size}}",
|
||||
"Calculating file info...": "Calcolo informazioni file...",
|
||||
"New Data Location": "Nuova posizione dei dati",
|
||||
"Choose Different Folder": "Scegli una cartella diversa",
|
||||
"Choose New Folder": "Scegli una nuova cartella",
|
||||
"Migrating data...": "Migrando dati...",
|
||||
"Copying: {{file}}": "Copia in corso: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} di {{total}} file",
|
||||
"Migration completed successfully!": "Migrzione completata con successo!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "I tuoi dati sono stati spostati nella nuova posizione. Riavvia l'applicazione per completare il processo.",
|
||||
"Migration failed": "Migrzione fallita",
|
||||
"Important Notice": "Avviso Importante",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Questo sposterà tutti i dati dell'app nella nuova posizione. Assicurati che la destinazione abbia spazio libero sufficiente.",
|
||||
"Restart App": "Riavvia App",
|
||||
"Start Migration": "Inizia Migrazione",
|
||||
"Advanced Settings": "Impostazioni Avanzate",
|
||||
"File count: {{size}}": "Conteggio file: {{size}}",
|
||||
"Background Read Aloud": "Riproduzione in background",
|
||||
"Ready to read aloud": "Pronto per la lettura ad alta voce",
|
||||
"Read Aloud": "Leggi ad alta voce",
|
||||
"Screen Brightness": "Luminosità dello schermo",
|
||||
"Background Image": "Immagine di sfondo",
|
||||
"Import Image": "Importa immagine",
|
||||
"Opacity": "Opacità",
|
||||
"Size": "Dimensione",
|
||||
"Cover": "Copertura",
|
||||
"Contain": "Contenere",
|
||||
"{{number}} pages left in chapter": "{{number}} pagine rimaste nel capitolo",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modalità E-Ink"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "翻訳の切り替え",
|
||||
"Translation Disabled": "翻訳が無効化されました",
|
||||
"Minimize": "最小化",
|
||||
"Maximize or Restore": "最大化または復元"
|
||||
"Maximize or Restore": "最大化または復元",
|
||||
"Exit Parallel Read": "並行読書を終了",
|
||||
"Enter Parallel Read": "並行読書に入る",
|
||||
"Zoom Level": "ズームレベル",
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Reset Zoom": "ズームリセット",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Mode": "ズームモード",
|
||||
"Single Page": "シングルページ",
|
||||
"Auto Spread": "自動スプレッド",
|
||||
"Fit Page": "ページに合わせる",
|
||||
"Fit Width": "幅に合わせる",
|
||||
"Failed to select directory": "ディレクトリの選択に失敗しました",
|
||||
"The new data directory must be different from the current one.": "新しいデータディレクトリは現在のものと異なる必要があります。",
|
||||
"Migration failed: {{error}}": "移行に失敗しました: {{error}}",
|
||||
"Change Data Location": "データの場所を変更",
|
||||
"Current Data Location": "現在のデータの場所",
|
||||
"Total size: {{size}}": "合計サイズ: {{size}}",
|
||||
"Calculating file info...": "ファイル情報を計算中...",
|
||||
"New Data Location": "新しいデータの場所",
|
||||
"Choose Different Folder": "別のフォルダーを選択",
|
||||
"Choose New Folder": "新しいフォルダーを選択",
|
||||
"Migrating data...": "データを移行中...",
|
||||
"Copying: {{file}}": "コピー中: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} ファイル",
|
||||
"Migration completed successfully!": "移行が成功しました!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "データは新しい場所に移動されました。プロセスを完了するにはアプリケーションを再起動してください。",
|
||||
"Migration failed": "移行に失敗しました",
|
||||
"Important Notice": "重要なお知らせ",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "これにより、すべてのアプリデータが新しい場所に移動されます。宛先に十分な空き容量があることを確認してください。",
|
||||
"Restart App": "アプリを再起動",
|
||||
"Start Migration": "移行を開始",
|
||||
"Advanced Settings": "詳細設定",
|
||||
"File count: {{size}}": "ファイル数: {{size}}",
|
||||
"Background Read Aloud": "バックグラウンド読み上げ",
|
||||
"Ready to read aloud": "読み上げの準備ができました",
|
||||
"Read Aloud": "読み上げ",
|
||||
"Screen Brightness": "画面の明るさ",
|
||||
"Background Image": "背景画像",
|
||||
"Import Image": "画像をインポート",
|
||||
"Opacity": "不透明度",
|
||||
"Size": "サイズ",
|
||||
"Cover": "カバー",
|
||||
"Contain": "含む",
|
||||
"{{number}} pages left in chapter": "章に{{number}}ページ残り",
|
||||
"Device": "デバイス",
|
||||
"E-Ink Mode": "E-Inkモード"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "번역 전환",
|
||||
"Translation Disabled": "번역 비활성화",
|
||||
"Minimize": "최소화",
|
||||
"Maximize or Restore": "최대화 또는 복원"
|
||||
"Maximize or Restore": "최대화 또는 복원",
|
||||
"Exit Parallel Read": "병렬 읽기 종료",
|
||||
"Enter Parallel Read": "병렬 읽기 시작",
|
||||
"Zoom Level": "확대/축소 수준",
|
||||
"Zoom Out": "축소",
|
||||
"Reset Zoom": "줌 재설정",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Mode": "줌 모드",
|
||||
"Single Page": "단일 페이지",
|
||||
"Auto Spread": "자동 스프레드",
|
||||
"Fit Page": "페이지에 맞춤",
|
||||
"Fit Width": "너비에 맞춤",
|
||||
"Failed to select directory": "디렉토리 선택 실패",
|
||||
"The new data directory must be different from the current one.": "새 데이터 디렉토리는 현재 디렉토리와 달라야 합니다.",
|
||||
"Migration failed: {{error}}": "마이그레이션 실패: {{error}}",
|
||||
"Change Data Location": "데이터 위치 변경",
|
||||
"Current Data Location": "현재 데이터 위치",
|
||||
"Total size: {{size}}": "총 크기: {{size}}",
|
||||
"Calculating file info...": "파일 정보 계산 중...",
|
||||
"New Data Location": "새 데이터 위치",
|
||||
"Choose Different Folder": "다른 폴더 선택",
|
||||
"Choose New Folder": "새 폴더 선택",
|
||||
"Migrating data...": "데이터 마이그레이션 중...",
|
||||
"Copying: {{file}}": "복사 중: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} 파일",
|
||||
"Migration completed successfully!": "마이그레이션이 성공적으로 완료되었습니다!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "데이터가 새 위치로 이동되었습니다. 프로세스를 완료하려면 애플리케이션을 재시작하십시오.",
|
||||
"Migration failed": "마이그레이션 실패",
|
||||
"Important Notice": "중요한 공지",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "이 작업은 모든 앱 데이터를 새 위치로 이동합니다. 대상에 충분한 여유 공간이 있는지 확인하십시오.",
|
||||
"Restart App": "앱 재시작",
|
||||
"Start Migration": "마이그레이션 시작",
|
||||
"Advanced Settings": "고급 설정",
|
||||
"File count: {{size}}": "파일 수: {{size}}",
|
||||
"Background Read Aloud": "백그라운드 음성 읽기",
|
||||
"Ready to read aloud": "읽기 준비 완료",
|
||||
"Read Aloud": "음성 읽기",
|
||||
"Screen Brightness": "화면 밝기",
|
||||
"Background Image": "배경 이미지",
|
||||
"Import Image": "이미지 가져오기",
|
||||
"Opacity": "불투명도",
|
||||
"Size": "크기",
|
||||
"Cover": "표지",
|
||||
"Contain": "포함",
|
||||
"{{number}} pages left in chapter": "남은 페이지 {{number}}장",
|
||||
"Device": "장치",
|
||||
"E-Ink Mode": "E-Ink 모드"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "Vertaling Wisselen",
|
||||
"Translation Disabled": "Vertaling Uitgeschakeld",
|
||||
"Minimize": "Minimaliseren",
|
||||
"Maximize or Restore": "Maximaliseren of Herstellen"
|
||||
"Maximize or Restore": "Maximaliseren of Herstellen",
|
||||
"Exit Parallel Read": "Parallel Lezen Afsluiten",
|
||||
"Enter Parallel Read": "Parallel Lezen Starten",
|
||||
"Zoom Level": "Zoomniveau",
|
||||
"Zoom Out": "Zoom Verkleinen",
|
||||
"Reset Zoom": "Zoom Reset",
|
||||
"Zoom In": "Zoom Vergroten",
|
||||
"Zoom Mode": "Zoom Modus",
|
||||
"Single Page": "Enkele Pagina",
|
||||
"Auto Spread": "Automatische Verspreiding",
|
||||
"Fit Page": "Pagina Aanpassen",
|
||||
"Fit Width": "Breedte Aanpassen",
|
||||
"Failed to select directory": "Selecteren van map is mislukt",
|
||||
"The new data directory must be different from the current one.": "De nieuwe gegevensmap moet anders zijn dan de huidige.",
|
||||
"Migration failed: {{error}}": "Migratie mislukt: {{error}}",
|
||||
"Change Data Location": "Gegevenslocatie Wijzigen",
|
||||
"Current Data Location": "Huidige Gegevenslocatie",
|
||||
"Total size: {{size}}": "Totale grootte: {{size}}",
|
||||
"Calculating file info...": "Bestandinformatie wordt berekend...",
|
||||
"New Data Location": "Nieuwe Gegevenslocatie",
|
||||
"Choose Different Folder": "Kies Een Andere Map",
|
||||
"Choose New Folder": "Kies Nieuwe Map",
|
||||
"Migrating data...": "Gegevens worden gemigreerd...",
|
||||
"Copying: {{file}}": "Kopiëren: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} van {{total}} bestanden",
|
||||
"Migration completed successfully!": "Migratie succesvol voltooid!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Uw gegevens zijn naar de nieuwe locatie verplaatst. Start de applicatie opnieuw op om het proces te voltooien.",
|
||||
"Migration failed": "Migratie mislukt",
|
||||
"Important Notice": "Belangrijke Kennisgeving",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Dit zal al uw app-gegevens naar de nieuwe locatie verplaatsen. Zorg ervoor dat de bestemming voldoende vrije ruimte heeft.",
|
||||
"Restart App": "Herstart App",
|
||||
"Start Migration": "Start Migratie",
|
||||
"Advanced Settings": "Geavanceerde Instellingen",
|
||||
"File count: {{size}}": "Bestandsaantal: {{size}}",
|
||||
"Background Read Aloud": "Achtergrond Voorlezen",
|
||||
"Ready to read aloud": "Klaar om voor te lezen",
|
||||
"Read Aloud": "Voorlezen",
|
||||
"Screen Brightness": "Schermhelderheid",
|
||||
"Background Image": "Achtergrondafbeelding",
|
||||
"Import Image": "Afbeelding importeren",
|
||||
"Opacity": "Doorzichtigheid",
|
||||
"Size": "Grootte",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Bevatten",
|
||||
"{{number}} pages left in chapter": "Nog {{number}} pagina’s in hoofdstuk",
|
||||
"Device": "Apparaat",
|
||||
"E-Ink Mode": "E-Ink Modus"
|
||||
}
|
||||
|
||||
@@ -571,5 +571,51 @@
|
||||
"Toggle Translation": "Włącz/Wyłącz tłumaczenie",
|
||||
"Translation Disabled": "Tłumaczenie wyłączone",
|
||||
"Minimize": "Minimalizuj",
|
||||
"Maximize or Restore": "Maksymalizuj lub Przywróć"
|
||||
"Maximize or Restore": "Maksymalizuj lub Przywróć",
|
||||
"Exit Parallel Read": "Wyjdź z czytania równoległego",
|
||||
"Enter Parallel Read": "Wejdź w czytanie równoległe",
|
||||
"Zoom Level": "Poziom powiększenia",
|
||||
"Zoom Out": "Zoom Wydłuż",
|
||||
"Reset Zoom": "Resetuj Zoom",
|
||||
"Zoom In": "Zoom Skróć",
|
||||
"Zoom Mode": "Tryb Zoom",
|
||||
"Single Page": "Pojedyncza Strona",
|
||||
"Auto Spread": "Automatyczne Rozprzestrzenianie",
|
||||
"Fit Page": "Dopasuj Stronę",
|
||||
"Fit Width": "Dopasuj Szerokość",
|
||||
"Failed to select directory": "Nie udało się wybrać katalogu",
|
||||
"The new data directory must be different from the current one.": "Nowy katalog danych musi różnić się od obecnego.",
|
||||
"Migration failed: {{error}}": "Migracja nie powiodła się: {{error}}",
|
||||
"Change Data Location": "Zmień lokalizację danych",
|
||||
"Current Data Location": "Bieżąca lokalizacja danych",
|
||||
"Total size: {{size}}": "Całkowity rozmiar: {{size}}",
|
||||
"Calculating file info...": "Obliczanie informacji o pliku...",
|
||||
"New Data Location": "Nowa lokalizacja danych",
|
||||
"Choose Different Folder": "Wybierz inny folder",
|
||||
"Choose New Folder": "Wybierz nowy folder",
|
||||
"Migrating data...": "Migracja danych...",
|
||||
"Copying: {{file}}": "Kopiowanie: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} z {{total}} plików",
|
||||
"Migration completed successfully!": "Migracja zakończona pomyślnie!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Twoje dane zostały przeniesione do nowej lokalizacji. Proszę zrestartować aplikację, aby zakończyć proces.",
|
||||
"Migration failed": "Migracja nie powiodła się",
|
||||
"Important Notice": "Ważne powiadomienie",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "To przeniesie wszystkie dane aplikacji do nowej lokalizacji. Upewnij się, że docelowy folder ma wystarczająco dużo wolnego miejsca.",
|
||||
"Restart App": "Zrestartuj aplikację",
|
||||
"Start Migration": "Rozpocznij migrację",
|
||||
"Advanced Settings": "Ustawienia zaawansowane",
|
||||
"File count: {{size}}": "Liczba plików: {{size}}",
|
||||
"Background Read Aloud": "Czytanie na głos w tle",
|
||||
"Ready to read aloud": "Gotowy do przeczytania na głos",
|
||||
"Read Aloud": "Czytaj na głos",
|
||||
"Screen Brightness": "Jasność ekranu",
|
||||
"Background Image": "Obraz tła",
|
||||
"Import Image": "Importuj obraz",
|
||||
"Opacity": "Przezroczystość",
|
||||
"Size": "Rozmiar",
|
||||
"Cover": "Okładka",
|
||||
"Contain": "Zawierać",
|
||||
"{{number}} pages left in chapter": "Pozostało {{number}} stron w rozdziale",
|
||||
"Device": "Urządzenie",
|
||||
"E-Ink Mode": "Tryb E-Ink"
|
||||
}
|
||||
|
||||
@@ -567,5 +567,51 @@
|
||||
"Toggle Translation": "Alternar Tradução",
|
||||
"Translation Disabled": "Tradução Desativada",
|
||||
"Minimize": "Minimizar",
|
||||
"Maximize or Restore": "Maximizar ou Restaurar"
|
||||
"Maximize or Restore": "Maximizar ou Restaurar",
|
||||
"Exit Parallel Read": "Sair da Leitura Paralela",
|
||||
"Enter Parallel Read": "Entrar na Leitura Paralela",
|
||||
"Zoom Level": "Nivel de Zoom",
|
||||
"Zoom Out": "Reduzir Zoom",
|
||||
"Reset Zoom": "Redefinir Zoom",
|
||||
"Zoom In": "Aumentar Zoom",
|
||||
"Zoom Mode": "Modo de Zoom",
|
||||
"Single Page": "Página Única",
|
||||
"Auto Spread": "Espalhamento Automático",
|
||||
"Fit Page": "Ajustar Página",
|
||||
"Fit Width": "Ajustar Largura",
|
||||
"Failed to select directory": "Falha ao selecionar diretório",
|
||||
"The new data directory must be different from the current one.": "O novo diretório de dados deve ser diferente do atual.",
|
||||
"Migration failed: {{error}}": "A migração falhou: {{error}}",
|
||||
"Change Data Location": "Alterar Localização dos Dados",
|
||||
"Current Data Location": "Localização Atual dos Dados",
|
||||
"Total size: {{size}}": "Tamanho total: {{size}}",
|
||||
"Calculating file info...": "Calculando informações do arquivo...",
|
||||
"New Data Location": "Nova Localização dos Dados",
|
||||
"Choose Different Folder": "Escolher Pasta Diferente",
|
||||
"Choose New Folder": "Escolher Nova Pasta",
|
||||
"Migrating data...": "Migrando dados...",
|
||||
"Copying: {{file}}": "Copiando: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} de {{total}} arquivos",
|
||||
"Migration completed successfully!": "Migração concluída com sucesso!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Seus dados foram movidos para a nova localização. Reinicie o aplicativo para concluir o processo.",
|
||||
"Migration failed": "Migração falhou",
|
||||
"Important Notice": "Aviso Importante",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Isso moverá todos os dados do seu aplicativo para a nova localização. Certifique-se de que o destino tenha espaço livre suficiente.",
|
||||
"Restart App": "Reiniciar Aplicativo",
|
||||
"Start Migration": "Iniciar Migração",
|
||||
"Advanced Settings": "Configurações Avançadas",
|
||||
"File count: {{size}}": "Contagem de arquivos: {{size}}",
|
||||
"Background Read Aloud": "Leitura em Segundo Plano",
|
||||
"Ready to read aloud": "Pronto para leitura em voz alta",
|
||||
"Read Aloud": "Ler em Voz Alta",
|
||||
"Screen Brightness": "Brilho da Tela",
|
||||
"Background Image": "Imagem de Fundo",
|
||||
"Import Image": "Importar Imagem",
|
||||
"Opacity": "Opacidade",
|
||||
"Size": "Tamanho",
|
||||
"Cover": "Capa",
|
||||
"Contain": "Contém",
|
||||
"{{number}} pages left in chapter": "Faltam {{number}} páginas neste capítulo",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modo E-Ink"
|
||||
}
|
||||
|
||||
@@ -571,5 +571,51 @@
|
||||
"Toggle Translation": "Переключить перевод",
|
||||
"Translation Disabled": "Перевод отключен",
|
||||
"Minimize": "Свернуть",
|
||||
"Maximize or Restore": "Максимизировать или восстановить"
|
||||
"Maximize or Restore": "Максимизировать или восстановить",
|
||||
"Exit Parallel Read": "Выйти из параллельного чтения",
|
||||
"Enter Parallel Read": "Войти в параллельное чтение",
|
||||
"Zoom Level": "Уровень масштабирования",
|
||||
"Zoom Out": "Уменьшить масштаб",
|
||||
"Reset Zoom": "Сбросить масштаб",
|
||||
"Zoom In": "Увеличить масштаб",
|
||||
"Zoom Mode": "Режим масштабирования",
|
||||
"Single Page": "Одна страница",
|
||||
"Auto Spread": "Автоматическое растяжение",
|
||||
"Fit Page": "Подогнать страницу",
|
||||
"Fit Width": "Подогнать ширину",
|
||||
"Failed to select directory": "Не удалось выбрать каталог",
|
||||
"The new data directory must be different from the current one.": "Новая директория данных должна отличаться от текущей.",
|
||||
"Migration failed: {{error}}": "Ошибка миграции: {{error}}",
|
||||
"Change Data Location": "Изменить расположение данных",
|
||||
"Current Data Location": "Текущее расположение данных",
|
||||
"Total size: {{size}}": "Общий размер: {{size}}",
|
||||
"Calculating file info...": "Вычисление информации о файле...",
|
||||
"New Data Location": "Новое расположение данных",
|
||||
"Choose Different Folder": "Выбрать другую папку",
|
||||
"Choose New Folder": "Выбрать новую папку",
|
||||
"Migrating data...": "Миграция данных...",
|
||||
"Copying: {{file}}": "Копирование: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} из {{total}} файлов",
|
||||
"Migration completed successfully!": "Миграция завершена успешно!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ваши данные были перемещены в новое расположение. Пожалуйста, перезапустите приложение, чтобы завершить процесс.",
|
||||
"Migration failed": "Ошибка миграции",
|
||||
"Important Notice": "Важное уведомление",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Это переместит все ваши данные приложения в новое расположение. Убедитесь, что в целевом месте достаточно свободного места.",
|
||||
"Restart App": "Перезапустить приложение",
|
||||
"Start Migration": "Начать миграцию",
|
||||
"Advanced Settings": "Расширенные настройки",
|
||||
"File count: {{size}}": "Количество файлов: {{size}}",
|
||||
"Background Read Aloud": "Озвучивание в фоне",
|
||||
"Ready to read aloud": "Готов к чтению вслух",
|
||||
"Read Aloud": "Читать вслух",
|
||||
"Screen Brightness": "Яркость экрана",
|
||||
"Background Image": "Фоновое изображение",
|
||||
"Import Image": "Импортировать изображение",
|
||||
"Opacity": "Непрозрачность",
|
||||
"Size": "Размер",
|
||||
"Cover": "Обложка",
|
||||
"Contain": "Содержать",
|
||||
"{{number}} pages left in chapter": "Осталось {{number}} страниц в главе",
|
||||
"Device": "Устройство",
|
||||
"E-Ink Mode": "E-Ink режим"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "පරිවර්තනය ස්ථානය වෙනස් කරන්න",
|
||||
"Translation Disabled": "පරිවර්තනය අක්රීය කර ඇත",
|
||||
"Minimize": "අඩු කරන්න",
|
||||
"Maximize or Restore": "ඉහළ නැංවීම හෝ ප්රතිසංස්කරණය"
|
||||
"Maximize or Restore": "ඉහළ නැංවීම හෝ ප්රතිසංස්කරණය",
|
||||
"Exit Parallel Read": "සමාන්තර කියවීමෙන් පිටවන්න",
|
||||
"Enter Parallel Read": "සමාන්තර කියවීමේදී පිවිසෙන්න",
|
||||
"Zoom Level": "සංයුතිය මට්ටම",
|
||||
"Zoom Out": "සංයුතිය අඩු කරන්න",
|
||||
"Reset Zoom": "සංයුතිය නැවත සකසන්න",
|
||||
"Zoom In": "සංයුතිය වැඩි කරන්න",
|
||||
"Zoom Mode": "සංයුතිය ආකාරය",
|
||||
"Single Page": "එක් පිටුව",
|
||||
"Auto Spread": "ස්වයංක්රීය ව්යාප්තිය",
|
||||
"Fit Page": "පිටුවට ගැලපෙන ලෙස",
|
||||
"Fit Width": "පැතිකඩට ගැලපෙන ලෙස",
|
||||
"Failed to select directory": "ෆෝල්ඩරය තෝරා ගැනීමට අසමත්",
|
||||
"The new data directory must be different from the current one.": "නව දත්ත ෆෝල්ඩරය වර්තමාන ෆෝල්ඩරයෙන් වෙනස් විය යුතුය.",
|
||||
"Migration failed: {{error}}": "මැතිවරණය අසමත් විය: {{error}}",
|
||||
"Change Data Location": "දත්ත ස්ථානය වෙනස් කරන්න",
|
||||
"Current Data Location": "වර්තමාන දත්ත ස්ථානය",
|
||||
"Total size: {{size}}": "මුළු ප්රමාණය: {{size}}",
|
||||
"Calculating file info...": "ගොනු තොරතුරු ගණනය කරමින්...",
|
||||
"New Data Location": "නව දත්ත ස්ථානය",
|
||||
"Choose Different Folder": "වෙනත් ෆෝල්ඩරයක් තෝරන්න",
|
||||
"Choose New Folder": "නව ෆෝල්ඩරයක් තෝරන්න",
|
||||
"Migrating data...": "දත්ත මැතිවරණය කරමින්...",
|
||||
"Copying: {{file}}": "පිටපත් කරමින්: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}}/{{total}} ගොනු",
|
||||
"Migration completed successfully!": "මැතිවරණය සාර්ථකව නිමවිය!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "ඔබගේ දත්ත නව ස්ථානයට මාරු කර ඇත. ක්රියාවලිය සම්පූර්ණ කිරීමට කරුණාකර යෙදුම නැවත ආරම්භ කරන්න.",
|
||||
"Migration failed": "මැතිවරණය අසමත් විය",
|
||||
"Important Notice": "මහත් අවධානයක්",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "මෙය ඔබගේ යෙදුම් දත්ත සියල්ල නව ස්ථානයට මාරු කරනු ඇත. ගමන් මාර්ගය ප්රමාණවත් නිදහස් ස්ථානයක් ඇති බවට සහතික වන්න.",
|
||||
"Restart App": "යෙදුම නැවත ආරම්භ කරන්න",
|
||||
"Start Migration": "මැතිවරණය ආරම්භ කරන්න",
|
||||
"Advanced Settings": "උසස් සැකසුම්",
|
||||
"File count: {{size}}": "ගොනු ගණන: {{size}}",
|
||||
"Background Read Aloud": "පසුබිම් පවසා කියවීම",
|
||||
"Ready to read aloud": "වාදනය කිරීමට සූදානම්",
|
||||
"Read Aloud": "පවසා කියවීම",
|
||||
"Screen Brightness": "තිරයේ දිදුලනුම",
|
||||
"Background Image": "පසුබිම් රූපය",
|
||||
"Import Image": "රූපය ආයාත කරන්න",
|
||||
"Opacity": "පැහැදිලිත්වය",
|
||||
"Size": "ප්රමාණය",
|
||||
"Cover": "ආවරණය",
|
||||
"Contain": "අඩංගු වන්න",
|
||||
"{{number}} pages left in chapter": "පරිච්ඡේදයේ පිටු {{number}} ඉතිරිව ඇත",
|
||||
"Device": "උපකරණය",
|
||||
"E-Ink Mode": "E-Ink ආකාරය"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
{
|
||||
"Email address": "E-postadress",
|
||||
"Your Password": "Ditt lösenord",
|
||||
"Your email address": "Din e-postadress",
|
||||
"Your password": "Ditt lösenord",
|
||||
"Sign in": "Logga in",
|
||||
"Signing in...": "Loggar in...",
|
||||
"Sign in with {{provider}}": "Logga in med {{provider}}",
|
||||
"Already have an account? Sign in": "Har du konto? Logga in",
|
||||
"Create a Password": "Skapa lösenord",
|
||||
"Sign up": "Registrera",
|
||||
"Signing up...": "Registrerar...",
|
||||
"Don't have an account? Sign up": "Inget konto? Registrera",
|
||||
"Check your email for the confirmation link": "Kolla din e-post för bekräftelselänk",
|
||||
"Signing in ...": "Loggar in...",
|
||||
"Send a magic link email": "Skicka magisk länk",
|
||||
"Check your email for the magic link": "Kolla din e-post för magisk länk",
|
||||
"Send reset password instructions": "Skicka återställningsinstruktioner",
|
||||
"Sending reset instructions ...": "Skickar instruktioner...",
|
||||
"Forgot your password?": "Glömt lösenord?",
|
||||
"Check your email for the password reset link": "Kolla din e-post för återställningslänk",
|
||||
"Phone number": "Telefonnummer",
|
||||
"Your phone number": "Ditt telefonnummer",
|
||||
"Token": "Token",
|
||||
"Your OTP token": "Din OTP-token",
|
||||
"Verify token": "Verifiera token",
|
||||
"Go Back": "Tillbaka",
|
||||
"Sign in with Google": "Logga in med Google",
|
||||
"Sign in with Apple": "Logga in med Apple",
|
||||
"Sign in with GitHub": "Logga in med GitHub",
|
||||
"New Password": "Nytt lösenord",
|
||||
"Your new password": "Ditt nya lösenord",
|
||||
"Update password": "Uppdatera lösenord",
|
||||
"Updating password ...": "Uppdaterar lösenord...",
|
||||
"Your password has been updated": "Ditt lösenord har uppdaterats",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Något gick fel. Oroa dig inte, vårt team har meddelats och vi arbetar på en lösning.",
|
||||
"Error Details:": "Feldetaljer:",
|
||||
"Try Again": "Försök igen",
|
||||
"Your Library": "Ditt bibliotek",
|
||||
"Need help?": "Behöver du hjälp?",
|
||||
"Contact Support": "Kontakta support",
|
||||
"Show Book Details": "Visa bokdetaljer",
|
||||
"Bookshelf": "Bokhylla",
|
||||
"Import Books": "Importera böcker",
|
||||
"Open": "Öppna",
|
||||
"Group": "Grupp",
|
||||
"Details": "Detaljer",
|
||||
"Delete": "Ta bort",
|
||||
"Cancel": "Avbryt",
|
||||
"Confirm Deletion": "Bekräfta borttagning",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Vill du ta bort {{count}} vald bok?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Vill du ta bort {{count}} valda böcker?",
|
||||
"Deselect Book": "Avmarkera bok",
|
||||
"Select Book": "Välj bok",
|
||||
"Download Book": "Ladda ner bok",
|
||||
"Upload Book": "Ladda upp bok",
|
||||
"Deselect Group": "Avmarkera grupp",
|
||||
"Select Group": "Välj grupp",
|
||||
"Untitled Group": "Namnlös grupp",
|
||||
"Group Books": "Gruppera böcker",
|
||||
"Remove From Group": "Ta bort från grupp",
|
||||
"Create New Group": "Skapa ny grupp",
|
||||
"Save": "Spara",
|
||||
"Confirm": "Bekräfta",
|
||||
"From Local File": "Från lokal fil",
|
||||
"Search in {{count}} Book(s)..._one": "Sök i {{count}} bok...",
|
||||
"Search in {{count}} Book(s)..._other": "Sök i {{count}} böcker...",
|
||||
"Search Books...": "Sök böcker...",
|
||||
"Clear Search": "Rensa sökning",
|
||||
"Select Books": "Välj böcker",
|
||||
"Deselect": "Avmarkera",
|
||||
"Select All": "Välj alla",
|
||||
"View Menu": "Visa meny",
|
||||
"Settings Menu": "Inställningsmeny",
|
||||
"Failed to select directory": "Kunde inte välja katalog",
|
||||
"The new data directory must be different from the current one.": "Den nya datakatalogen måste skilja sig från den nuvarande.",
|
||||
"Migration failed: {{error}}": "Migrering misslyckades: {{error}}",
|
||||
"Change Data Location": "Ändra dataplats",
|
||||
"Current Data Location": "Nuvarande dataplats",
|
||||
"Loading...": "Laddar...",
|
||||
"File count: {{size}}": "Antal filer: {{size}}",
|
||||
"Total size: {{size}}": "Total storlek: {{size}}",
|
||||
"Calculating file info...": "Beräknar filinfo...",
|
||||
"New Data Location": "Ny dataplats",
|
||||
"Choose New Folder": "Välj ny mapp",
|
||||
"Choose Different Folder": "Välj annan mapp",
|
||||
"Migrating data...": "Migrerar data...",
|
||||
"Copying: {{file}}": "Kopierar: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} av {{total}} filer",
|
||||
"Migration completed successfully!": "Migrering slutförd!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Din data har flyttats till den nya platsen. Starta om appen för att slutföra processen.",
|
||||
"Migration failed": "Migrering misslyckades",
|
||||
"Important Notice": "Viktigt meddelande",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Detta flyttar all din appdata till den nya platsen. Se till att målet har tillräckligt med utrymme.",
|
||||
"Close": "Stäng",
|
||||
"Restart App": "Starta om app",
|
||||
"Start Migration": "Starta migrering",
|
||||
"Dark Mode": "Mörkt läge",
|
||||
"Light Mode": "Ljust läge",
|
||||
"Auto Mode": "Auto-läge",
|
||||
"Logged in as {{userDisplayName}}": "Inloggad som {{userDisplayName}}",
|
||||
"Logged in": "Inloggad",
|
||||
"View account details and quota": "Visa kontodetaljer och kvot",
|
||||
"Account": "Konto",
|
||||
"Sign In": "Logga in",
|
||||
"Auto Upload Books to Cloud": "Auto-ladda upp böcker till moln",
|
||||
"Auto Import on File Open": "Auto-importera vid filöppning",
|
||||
"Open Last Book on Start": "Öppna senaste bok vid start",
|
||||
"Check Updates on Start": "Sök uppdateringar vid start",
|
||||
"Open Book in New Window": "Öppna bok i nytt fönster",
|
||||
"Fullscreen": "Helskärm",
|
||||
"Always on Top": "Alltid överst",
|
||||
"Always Show Status Bar": "Visa alltid statusfält",
|
||||
"Keep Screen Awake": "Håll skärm vaken",
|
||||
"Background Read Aloud": "Läs upp i bakgrunden",
|
||||
"Reload Page": "Ladda om sida",
|
||||
"Settings": "Inställningar",
|
||||
"Advanced Settings": "Avancerade inställningar",
|
||||
"Upgrade to Readest Premium": "Uppgradera till Readest Premium",
|
||||
"Download Readest": "Ladda ner Readest",
|
||||
"About Readest": "Om Readest",
|
||||
"Help improve Readest": "Hjälp förbättra Readest",
|
||||
"Sharing anonymized statistics": "Dela anonymiserad statistik",
|
||||
"List": "Lista",
|
||||
"Grid": "Rutnät",
|
||||
"Crop": "Beskär",
|
||||
"Fit": "Anpassa",
|
||||
"Title": "Titel",
|
||||
"Author": "Författare",
|
||||
"Format": "Format",
|
||||
"Date Read": "Läsdatum",
|
||||
"Date Added": "Datum tillagd",
|
||||
"Ascending": "Stigande",
|
||||
"Descending": "Fallande",
|
||||
"Book Covers": "Bokomslag",
|
||||
"Sort by...": "Sortera efter...",
|
||||
"No supported files found. Supported formats: {{formats}}": "Inga filer hittades. Format som stöds: {{formats}}",
|
||||
"No chapters detected.": "Inga kapitel hittades.",
|
||||
"Failed to parse the EPUB file.": "Kunde inte läsa EPUB-filen.",
|
||||
"This book format is not supported.": "Detta bokformat stöds inte.",
|
||||
"Failed to import book(s): {{filenames}}": "Kunde inte importera bok(er): {{filenames}}",
|
||||
"Book uploaded: {{title}}": "Bok uppladdad: {{title}}",
|
||||
"Insufficient storage quota": "Otillräcklig lagringskvot",
|
||||
"Failed to upload book: {{title}}": "Kunde inte ladda upp bok: {{title}}",
|
||||
"Book downloaded: {{title}}": "Bok nedladdad: {{title}}",
|
||||
"Failed to download book: {{title}}": "Kunde inte ladda ner bok: {{title}}",
|
||||
"Book deleted: {{title}}": "Bok borttagen: {{title}}",
|
||||
"Deleted cloud backup of the book: {{title}}": "Raderade molnkopia av bok: {{title}}",
|
||||
"Deleted local copy of the book: {{title}}": "Raderade lokal kopia av bok: {{title}}",
|
||||
"Failed to delete book: {{title}}": "Kunde inte ta bort bok: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "Kunde inte radera molnkopia av bok: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "Kunde inte radera lokal kopia av bok: {{title}}",
|
||||
"Library Header": "Bibliotekshuvud",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Välkommen till ditt bibliotek. Du kan importera dina böcker här och läsa dem när som helst.",
|
||||
"Copied to notebook": "Kopierat till anteckningsbok",
|
||||
"No annotations to export": "Inga anteckningar att exportera",
|
||||
"Exported from Readest": "Exporterad från Readest",
|
||||
"Highlights & Annotations": "Markeringar & anteckningar",
|
||||
"Untitled": "Namnlös",
|
||||
"Note": "Anteckning",
|
||||
"Copied to clipboard": "Kopierat",
|
||||
"Copy": "Kopiera",
|
||||
"Delete Highlight": "Ta bort markering",
|
||||
"Highlight": "Markera",
|
||||
"Annotate": "Anteckna",
|
||||
"Search": "Sök",
|
||||
"Dictionary": "Ordbok",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Translate": "Översätt",
|
||||
"Speak": "Läs upp",
|
||||
"Login Required": "Inloggning krävs",
|
||||
"Quota Exceeded": "Kvot överskriden",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Kunde inte hämta översättning. Logga in och försök igen.",
|
||||
"Unable to fetch the translation. Try again later.": "Kunde inte hämta översättning. Försök senare.",
|
||||
"Original Text": "Originaltext",
|
||||
"Auto Detect": "Auto-identifiera",
|
||||
"(detected)": "(identifierad)",
|
||||
"Translated Text": "Översatt text",
|
||||
"System Language": "Systemspråk",
|
||||
"No translation available.": "Ingen översättning tillgänglig.",
|
||||
"Translated by {{provider}}.": "Översatt av {{provider}}.",
|
||||
"Remove Bookmark": "Ta bort bokmärke",
|
||||
"Add Bookmark": "Lägg till bokmärke",
|
||||
"Books Content": "Bokinnehåll",
|
||||
"Book Content": "Bokinnehåll",
|
||||
"Footer Bar": "Sidfot",
|
||||
"Reading Progress": "Läsframsteg",
|
||||
"Next Section": "Nästa sektion",
|
||||
"Previous Section": "Föregående sektion",
|
||||
"Next Page": "Nästa sida",
|
||||
"Previous Page": "Föregående sida",
|
||||
"Go Forward": "Framåt",
|
||||
"Font Size": "Teckenstorlek",
|
||||
"Page Margin": "Sidmarginal",
|
||||
"Small": "Liten",
|
||||
"Large": "Stor",
|
||||
"Line Spacing": "Radavstånd",
|
||||
"Table of Contents": "Innehåll",
|
||||
"Notes": "Anteckningar",
|
||||
"Font & Layout": "Typsnitt & layout",
|
||||
"Jump to Location": "Hoppa till plats",
|
||||
"Header Bar": "Sidhuvud",
|
||||
"View Options": "Visningsalternativ",
|
||||
"Sync Conflict": "Synkkonflikt",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "Synka läsframsteg från \"{{deviceName}}\"?",
|
||||
"another device": "annan enhet",
|
||||
"Local Progress": "Lokalt framsteg",
|
||||
"Remote Progress": "Fjärrframsteg",
|
||||
"Failed to connect": "Kunde inte ansluta",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"KOReader Sync Settings": "KOReader-synkinställningar",
|
||||
"Sync as {{userDisplayName}}": "Synka som {{userDisplayName}}",
|
||||
"Sync Server Connected": "Synkserver ansluten",
|
||||
"Sync Strategy": "Synkstrategi",
|
||||
"Ask on conflict": "Fråga vid konflikt",
|
||||
"Always use latest": "Använd alltid senaste",
|
||||
"Send changes only": "Skicka endast ändringar",
|
||||
"Receive changes only": "Ta endast emot ändringar",
|
||||
"Checksum Method": "Kontrollsummemetod",
|
||||
"File Content (recommended)": "Filinnehåll (rekommenderas)",
|
||||
"File Name": "Filnamn",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Connect to your KOReader Sync server.": "Anslut till din KOReader-synkserver.",
|
||||
"Server URL": "Server-URL",
|
||||
"Username": "Användarnamn",
|
||||
"Your Username": "Ditt användarnamn",
|
||||
"Password": "Lösenord",
|
||||
"Connect": "Anslut",
|
||||
"Notebook": "Anteckningsbok",
|
||||
"Unpin Notebook": "Lossa anteckningsbok",
|
||||
"Pin Notebook": "Fäst anteckningsbok",
|
||||
"Hide Search Bar": "Dölj sökfält",
|
||||
"Show Search Bar": "Visa sökfält",
|
||||
"Resize Notebook": "Ändra storlek på anteckningsbok",
|
||||
"No notes match your search": "Inga anteckningar matchar din sökning",
|
||||
"Excerpts": "Utdrag",
|
||||
"Add your notes here...": "Lägg till dina anteckningar här...",
|
||||
"Search notes and excerpts...": "Sök anteckningar och utdrag...",
|
||||
"{{time}} min left in chapter": "{{time}} min kvar i kapitlet",
|
||||
"{{count}} pages left in chapter_one": "{{count}} sida kvar i kapitlet",
|
||||
"{{count}} pages left in chapter_other": "{{count}} sidor kvar i kapitlet",
|
||||
"On {{current}} of {{total}} page": "På {{current}} av {{total}} sidor",
|
||||
"Section Title": "Sektionstitel",
|
||||
"Book Cover": "Bokomslag",
|
||||
"More Info": "Mer info",
|
||||
"Parallel Read": "Parallell läsning",
|
||||
"Disable": "Inaktivera",
|
||||
"Enable": "Aktivera",
|
||||
"Exit Parallel Read": "Avsluta parallell läsning",
|
||||
"Enter Parallel Read": "Starta parallell läsning",
|
||||
"KOReader Sync": "KOReader-synk",
|
||||
"Push Progress": "Skicka framsteg",
|
||||
"Pull Progress": "Hämta framsteg",
|
||||
"Export Annotations": "Exportera anteckningar",
|
||||
"Sort TOC by Page": "Sortera innehåll efter sida",
|
||||
"Edit": "Redigera",
|
||||
"Go to Library": "Gå till bibliotek",
|
||||
"Book Menu": "Bokmeny",
|
||||
"Unpin Sidebar": "Lossa sidofält",
|
||||
"Pin Sidebar": "Fäst sidofält",
|
||||
"Search...": "Sök...",
|
||||
"Search Options": "Sökalternativ",
|
||||
"Book": "Bok",
|
||||
"Chapter": "Kapitel",
|
||||
"Match Case": "Matchning av versaler",
|
||||
"Match Whole Words": "Matchning av hela ord",
|
||||
"Match Diacritics": "Matchning av diakritiska tecken",
|
||||
"Sidebar": "Sidofält",
|
||||
"Resize Sidebar": "Ändra storlek på sidofält",
|
||||
"TOC": "Innehåll",
|
||||
"Bookmark": "Bokmärke",
|
||||
"Toggle Sidebar": "Visa/dölj sidofält",
|
||||
"Toggle Translation": "Visa/dölj översättning",
|
||||
"Disable Translation": "Inaktivera översättning",
|
||||
"Enable Translation": "Aktivera översättning",
|
||||
"Translation Disabled": "Översättning inaktiverad",
|
||||
"Previous Paragraph": "Föregående stycke",
|
||||
"Previous Sentence": "Föregående mening",
|
||||
"Pause": "Pausa",
|
||||
"Play": "Spela",
|
||||
"Next Sentence": "Nästa mening",
|
||||
"Next Paragraph": "Nästa stycke",
|
||||
"Read Aloud": "Läs upp",
|
||||
"Ready to read aloud": "Redo att läsa upp",
|
||||
"TTS not supported for PDF": "Uppläsning stöds inte för PDF",
|
||||
"TTS not supported for this document": "Uppläsning stöds inte för detta dokument",
|
||||
"No Timeout": "Ingen timeout",
|
||||
"{{value}} minute": "{{value}} minut",
|
||||
"{{value}} minutes": "{{value}} minuter",
|
||||
"{{value}} hour": "{{value}} timme",
|
||||
"{{value}} hours": "{{value}} timmar",
|
||||
"Voices for {{lang}}": "Röster för {{lang}}",
|
||||
"Slow": "Långsam",
|
||||
"Fast": "Snabb",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} röst",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} röster",
|
||||
"Zoom Level": "Zoomnivå",
|
||||
"Zoom Out": "Zooma ut",
|
||||
"Reset Zoom": "Återställ zoom",
|
||||
"Zoom In": "Zooma in",
|
||||
"Zoom Mode": "Zoomläge",
|
||||
"Single Page": "Enkel sida",
|
||||
"Auto Spread": "Auto-uppslag",
|
||||
"Fit Page": "Anpassa sida",
|
||||
"Fit Width": "Anpassa bredd",
|
||||
"Separate Cover Page": "Separat omslagssida",
|
||||
"Scrolled Mode": "Rullningsläge",
|
||||
"Sign in to Sync": "Logga in för synk",
|
||||
"Synced at {{time}}": "Synkad {{time}}",
|
||||
"Never synced": "Aldrig synkad",
|
||||
"Invert Image In Dark Mode": "Invertera bild i mörkt läge",
|
||||
"Reading Progress Synced": "Läsframsteg synkat",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "Sida {{page}} av {{total}} ({{percentage}}%)",
|
||||
"Current position": "Nuvarande position",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Cirka sida {{page}} av {{total}} ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "Cirka {{percentage}}%",
|
||||
"Delete Your Account?": "Ta bort ditt konto?",
|
||||
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Detta kan inte ångras. All din data i molnet raderas permanent.",
|
||||
"Delete Permanently": "Ta bort permanent",
|
||||
"Restore Purchase": "Återställ köp",
|
||||
"Manage Subscription": "Hantera prenumeration",
|
||||
"Reset Password": "Återställ lösenord",
|
||||
"Sign Out": "Logga ut",
|
||||
"Delete Account": "Ta bort konto",
|
||||
"Upgrade to {{plan}}": "Uppgradera till {{plan}}",
|
||||
"Complete Your Subscription": "Slutför din prenumeration",
|
||||
"Coming Soon": "Kommer snart",
|
||||
"Upgrade to Plus or Pro": "Uppgradera till Plus eller Pro",
|
||||
"Current Plan": "Nuvarande plan",
|
||||
"Plan Limits": "Plangränser",
|
||||
"Failed to delete user. Please try again later.": "Kunde inte ta bort användare. Försök senare.",
|
||||
"Failed to create checkout session": "Kunde inte skapa betalningssession",
|
||||
"No purchases found to restore.": "Inga köp att återställa.",
|
||||
"Failed to restore purchases.": "Kunde inte återställa köp.",
|
||||
"Failed to manage subscription.": "Kunde inte hantera prenumeration.",
|
||||
"Failed to load subscription plans.": "Kunde inte ladda prenumerationsplaner.",
|
||||
"Loading profile...": "Laddar profil...",
|
||||
"Processing your payment...": "Behandlar din betalning...",
|
||||
"Please wait while we confirm your subscription.": "Vänta medan vi bekräftar din prenumeration.",
|
||||
"Payment Processing": "Betalning behandlas",
|
||||
"Your payment is being processed. This usually takes a few moments.": "Din betalning behandlas. Detta tar vanligtvis några ögonblick.",
|
||||
"Payment Failed": "Betalning misslyckades",
|
||||
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Vi kunde inte behandla din prenumeration. Försök igen eller kontakta support om problemet kvarstår.",
|
||||
"Back to Profile": "Tillbaka till profil",
|
||||
"Subscription Successful!": "Prenumeration lyckades!",
|
||||
"Thank you for your subscription! Your payment has been processed successfully.": "Tack för din prenumeration! Din betalning har behandlats.",
|
||||
"Email:": "E-post:",
|
||||
"Plan:": "Plan:",
|
||||
"Amount:": "Belopp:",
|
||||
"Subscription ID:": "Prenumerations-ID:",
|
||||
"Need help? Contact our support team at support@readest.com": "Behöver du hjälp? Kontakta vår support på support@readest.com",
|
||||
"Free Plan": "Gratisplan",
|
||||
"month": "månad",
|
||||
"year": "år",
|
||||
"Cross-Platform Sync": "Plattformsoberoende synk",
|
||||
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Synka sömlöst ditt bibliotek, framsteg, markeringar och anteckningar mellan alla dina enheter.",
|
||||
"Customizable Reading": "Anpassningsbar läsning",
|
||||
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Anpassa varje detalj med justerbara typsnitt, layouter, teman och avancerade visningsinställningar.",
|
||||
"AI Read Aloud": "AI-uppläsning",
|
||||
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Njut av handsfree-läsning med naturligt ljudande AI-röster som ger dina böcker liv.",
|
||||
"AI Translations": "AI-översättningar",
|
||||
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Översätt vilken text som helst direkt med Google, Azure eller DeepL.",
|
||||
"Community Support": "Community-support",
|
||||
"Connect with fellow readers and get help fast in our friendly community channels.": "Anslut med andra läsare och få snabb hjälp i våra vänliga community-kanaler.",
|
||||
"Cloud Sync Storage": "Molnsynklagring",
|
||||
"AI Translations (per day)": "AI-översättningar (per dag)",
|
||||
"Plus Plan": "Plus-plan",
|
||||
"Includes All Free Plan Benefits": "Inkluderar alla gratisplanfördelar",
|
||||
"Unlimited AI Read Aloud Hours": "Obegränsad AI-uppläsning",
|
||||
"Listen without limits—convert as much text as you like into immersive audio.": "Lyssna obegränsat—konvertera så mycket text du vill till ljud.",
|
||||
"More AI Translations": "Fler AI-översättningar",
|
||||
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Lås upp förbättrad översättning med mer daglig användning och avancerade alternativ.",
|
||||
"DeepL Pro Access": "DeepL Pro-åtkomst",
|
||||
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Översätt upp till 100 000 tecken dagligen med den mest exakta översättningsmotorn.",
|
||||
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Lagra säkert hela din lässamling med upp till 5 GB molnlagring.",
|
||||
"Priority Support": "Prioriterad support",
|
||||
"Enjoy faster responses and dedicated assistance whenever you need help.": "Få snabbare svar och dedikerad hjälp när du behöver det.",
|
||||
"Pro Plan": "Pro-plan",
|
||||
"Includes All Plus Plan Benefits": "Inkluderar alla Plus-planfördelar",
|
||||
"Early Feature Access": "Tidig funktionsåtkomst",
|
||||
"Be the first to explore new features, updates, and innovations before anyone else.": "Var först att utforska nya funktioner och uppdateringar innan andra.",
|
||||
"Advanced AI Tools": "Avancerade AI-verktyg",
|
||||
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Använd kraftfulla AI-verktyg för smartare läsning, översättning och innehållsupptäckt.",
|
||||
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Översätt upp till 500 000 tecken dagligen med den mest exakta översättningsmotorn.",
|
||||
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Lagra säkert hela din lässamling med upp till 20 GB molnlagring.",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Check Update": "Sök uppdatering",
|
||||
"Already the latest version": "Redan senaste versionen",
|
||||
"Checking for updates...": "Söker uppdateringar...",
|
||||
"Error checking for updates": "Fel vid uppdateringskontroll",
|
||||
"Drop to Import Books": "Släpp för att importera böcker",
|
||||
"Terms of Service": "Användarvillkor",
|
||||
"Privacy Policy": "Integritetspolicy",
|
||||
"ON": "PÅ",
|
||||
"OFF": "AV",
|
||||
"Enter book title": "Ange boktitel",
|
||||
"Subtitle": "Undertitel",
|
||||
"Enter book subtitle": "Ange underrubrik",
|
||||
"Enter author name": "Ange författarnamn",
|
||||
"Series": "Serie",
|
||||
"Enter series name": "Ange serienamn",
|
||||
"Series Index": "Serieindex",
|
||||
"Enter series index": "Ange serieindex",
|
||||
"Total in Series": "Totalt i serien",
|
||||
"Enter total books in series": "Ange totalt antal böcker i serien",
|
||||
"Publisher": "Förlag",
|
||||
"Enter publisher": "Ange förlag",
|
||||
"Publication Date": "Publiceringsdatum",
|
||||
"YYYY or YYYY-MM-DD": "ÅÅÅÅ eller ÅÅÅÅ-MM-DD",
|
||||
"Language": "Språk",
|
||||
"Identifier": "Identifierare",
|
||||
"Subjects": "Ämnen",
|
||||
"Fiction, Science, History": "Skönlitteratur, Vetenskap, Historia",
|
||||
"Description": "Beskrivning",
|
||||
"Enter book description": "Ange bokbeskrivning",
|
||||
"Change cover image": "Ändra omslagsbild",
|
||||
"Replace": "Ersätt",
|
||||
"Remove cover image": "Ta bort omslagsbild",
|
||||
"Unlock cover": "Lås upp omslag",
|
||||
"Lock cover": "Lås omslag",
|
||||
"Auto-Retrieve Metadata": "Auto-hämta metadata",
|
||||
"Auto": "Auto",
|
||||
"Auto-Retrieve": "Auto-hämta",
|
||||
"Unlock all fields": "Lås upp alla fält",
|
||||
"Unlock All": "Lås upp alla",
|
||||
"Lock all fields": "Lås alla fält",
|
||||
"Lock All": "Lås alla",
|
||||
"Reset": "Återställ",
|
||||
"Are you sure to delete the selected book?": "Vill du ta bort den valda boken?",
|
||||
"Are you sure to delete the cloud backup of the selected book?": "Vill du ta bort molnkopian av den valda boken?",
|
||||
"Are you sure to delete the local copy of the selected book?": "Vill du ta bort den lokala kopian av den valda boken?",
|
||||
"Edit Metadata": "Redigera metadata",
|
||||
"Book Details": "Bokdetaljer",
|
||||
"Unknown": "Okänd",
|
||||
"Delete Book Options": "Ta bort bokalternativ",
|
||||
"Remove from Cloud & Device": "Ta bort från moln & enhet",
|
||||
"Remove from Cloud Only": "Ta bort endast från moln",
|
||||
"Remove from Device Only": "Ta bort endast från enhet",
|
||||
"Published": "Publicerad",
|
||||
"Updated": "Uppdaterad",
|
||||
"Added": "Tillagd",
|
||||
"File Size": "Filstorlek",
|
||||
"No description available": "Ingen beskrivning tillgänglig",
|
||||
"Locked": "Låst",
|
||||
"Select Metadata Source": "Välj metadatakälla",
|
||||
"Keep manual input": "Behåll manuell inmatning",
|
||||
"Theme Mode": "Temaläge",
|
||||
"Override Book Color": "Åsidosätt bokfärg",
|
||||
"Theme Color": "Temafärg",
|
||||
"Custom": "Anpassad",
|
||||
"Code Highlighting": "Kodmarkering",
|
||||
"Enable Highlighting": "Aktivera markering",
|
||||
"Code Language": "Kodspråk",
|
||||
"Scroll": "Rullning",
|
||||
"Continuous Scroll": "Kontinuerlig rullning",
|
||||
"Overlap Pixels": "Överlappande pixlar",
|
||||
"Click": "Klick",
|
||||
"Clicks for Page Flip": "Klick för sidbyte",
|
||||
"Swap Clicks Area": "Byt klickområde",
|
||||
"Disable Double Click": "Inaktivera dubbelklick",
|
||||
"Volume Keys for Page Flip": "Volymknappar för sidbyte",
|
||||
"Animation": "Animation",
|
||||
"Paging Animation": "Sidanimation",
|
||||
"Security": "Säkerhet",
|
||||
"Allow JavaScript": "Tillåt JavaScript",
|
||||
"Enable only if you trust the file.": "Aktivera endast om du litar på filen.",
|
||||
"Font": "Typsnitt",
|
||||
"Custom Fonts": "Anpassade typsnitt",
|
||||
"Cancel Delete": "Avbryt borttagning",
|
||||
"Delete Font": "Ta bort typsnitt",
|
||||
"Import Font": "Importera typsnitt",
|
||||
"Tips": "Tips",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format som stöds: .ttf, .otf, .woff, .woff2",
|
||||
"Custom fonts can be selected from the Font Face menu": "Anpassade typsnitt kan väljas från typsnittsmeny",
|
||||
"Global Settings": "Globala inställningar",
|
||||
"Apply to All Books": "Tillämpa på alla böcker",
|
||||
"Apply to This Book": "Tillämpa på denna bok",
|
||||
"Reset Settings": "Återställ inställningar",
|
||||
"Manage Custom Fonts": "Hantera anpassade typsnitt",
|
||||
"System Fonts": "Systemtypsnitt",
|
||||
"Serif Font": "Serif-typsnitt",
|
||||
"Sans-Serif Font": "Sans-serif-typsnitt",
|
||||
"Override Book Font": "Åsidosätt boktypsnitt",
|
||||
"Default Font Size": "Standard teckenstorlek",
|
||||
"Minimum Font Size": "Minsta teckenstorlek",
|
||||
"Font Weight": "Typsnittsvikt",
|
||||
"Font Family": "Typsnittsfamilj",
|
||||
"Default Font": "Standardtypsnitt",
|
||||
"CJK Font": "CJK-typsnitt",
|
||||
"Font Face": "Typsnittssnitt",
|
||||
"Monospace Font": "Monospace-typsnitt",
|
||||
"Interface Language": "Gränssnittsspråk",
|
||||
"Translation": "Översättning",
|
||||
"Show Source Text": "Visa källtext",
|
||||
"Translation Service": "Översättningstjänst",
|
||||
"Translate To": "Översätt till",
|
||||
"Override Book Layout": "Åsidosätt boklayout",
|
||||
"Writing Mode": "Skrivläge",
|
||||
"Default": "Standard",
|
||||
"Horizontal Direction": "Horisontell riktning",
|
||||
"Vertical Direction": "Vertikal riktning",
|
||||
"RTL Direction": "RTL-riktning",
|
||||
"Border Frame": "Kantram",
|
||||
"Double Border": "Dubbel kant",
|
||||
"Border Color": "Kantfärg",
|
||||
"Paragraph": "Stycke",
|
||||
"Paragraph Margin": "Styckemarginal",
|
||||
"Word Spacing": "Ordavstånd",
|
||||
"Letter Spacing": "Bokstavsavstånd",
|
||||
"Text Indent": "Textindrag",
|
||||
"Full Justification": "Full justering",
|
||||
"Hyphenation": "Avstavning",
|
||||
"Page": "Sida",
|
||||
"Top Margin (px)": "Övre marginal (px)",
|
||||
"Bottom Margin (px)": "Nedre marginal (px)",
|
||||
"Left Margin (px)": "Vänster marginal (px)",
|
||||
"Right Margin (px)": "Höger marginal (px)",
|
||||
"Column Gap (%)": "Kolumnavstånd (%)",
|
||||
"Maximum Number of Columns": "Max antal kolumner",
|
||||
"Maximum Column Height": "Max kolumnhöjd",
|
||||
"Maximum Column Width": "Max kolumnbredd",
|
||||
"Header & Footer": "Sidhuvud & sidfot",
|
||||
"Show Header": "Visa sidhuvud",
|
||||
"Show Footer": "Visa sidfot",
|
||||
"Show Remaining Time": "Visa återstående tid",
|
||||
"Show Remaining Pages": "Visa återstående sidor",
|
||||
"Show Reading Progress": "Visa läsframsteg",
|
||||
"Reading Progress Style": "Läsframstegsstil",
|
||||
"Page Number": "Sidnummer",
|
||||
"Percentage": "Procent",
|
||||
"Apply also in Scrolled Mode": "Tillämpa även i rullningsläge",
|
||||
"Screen": "Skärm",
|
||||
"Orientation": "Orientering",
|
||||
"Portrait": "Porträtt",
|
||||
"Landscape": "Landskap",
|
||||
"Apply": "Tillämpa",
|
||||
"Custom Content CSS": "Anpassad innehålls-CSS",
|
||||
"Enter CSS for book content styling...": "Ange CSS för bokinnehållsstil...",
|
||||
"Custom Reader UI CSS": "Anpassad läsar-UI CSS",
|
||||
"Enter CSS for reader interface styling...": "Ange CSS för läsargränssnittsstil...",
|
||||
"Decrease": "Minska",
|
||||
"Increase": "Öka",
|
||||
"Layout": "Layout",
|
||||
"Color": "Färg",
|
||||
"Behavior": "Beteende",
|
||||
"Settings Panels": "Inställningspaneler",
|
||||
"Reset {{settings}}": "Återställ {{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": "Hela världen är en scen,\nOch alla män och kvinnor bara spelare;\nDe har sina utgångar och entréer,\nOch en man spelar i sin tid många roller,\nHans akter är sju åldrar.\n\n— William Shakespeare",
|
||||
"(from 'As You Like It', Act II)": "(från 'Som ni behagar', Akt II)",
|
||||
"Custom Theme": "Anpassat tema",
|
||||
"Theme Name": "Temanamn",
|
||||
"Text Color": "Textfärg",
|
||||
"Background Color": "Bakgrundsfärg",
|
||||
"Link Color": "Länkfärg",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Get Help from the Readest Community": "Få hjälp från Readest-communityn",
|
||||
"A new version of Readest is available!": "En ny version av Readest finns!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} finns tillgänglig (installerad version {{currentVersion}}).",
|
||||
"Download and install now?": "Ladda ner och installera nu?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Laddar ner {{downloaded}} av {{contentLength}}",
|
||||
"Download finished": "Nedladdning klar",
|
||||
"DOWNLOAD & INSTALL": "LADDA NER & INSTALLERA",
|
||||
"Changelog": "Ändringslogg",
|
||||
"Software Update": "Programuppdatering",
|
||||
"What's New in Readest": "Nyheter i Readest",
|
||||
"Minimize": "Minimera",
|
||||
"Maximize or Restore": "Maximera eller återställ",
|
||||
"Select Files": "Välj filer",
|
||||
"Select Image": "Välj bild",
|
||||
"Select Video": "Välj video",
|
||||
"Select Audio": "Välj ljud",
|
||||
"Select Fonts": "Välj typsnitt",
|
||||
"Storage": "Lagring",
|
||||
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% av molnsynkutrymme använt.",
|
||||
"Translation Characters": "Översättningstecken",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% av dagliga översättningstecken använt.",
|
||||
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Daglig översättningskvot nådd. Uppgradera din plan för att fortsätta använda AI-översättningar.",
|
||||
"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 Böcker",
|
||||
"Open Library": "Open Library",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"Gray": "Grå",
|
||||
"Sepia": "Sepia",
|
||||
"Grass": "Gräs",
|
||||
"Cherry": "Körsbär",
|
||||
"Sky": "Himmel",
|
||||
"Solarized": "Solarized",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Nord": "Nord",
|
||||
"Contrast": "Kontrast",
|
||||
"Sunset": "Solnedgång",
|
||||
"Reveal in Finder": "Visa i Finder",
|
||||
"Reveal in File Explorer": "Visa i Utforskaren",
|
||||
"Reveal in Folder": "Visa i mapp",
|
||||
"Screen Brightness": "Skärmens ljusstyrka",
|
||||
"Background Image": "Bakgrundsbild",
|
||||
"Import Image": "Importera bild",
|
||||
"Opacity": "Opacitet",
|
||||
"Size": "Storlek",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Innehålla",
|
||||
"{{number}} pages left in chapter": "{{number}} sidor kvar i kapitlet",
|
||||
"Device": "Enhet",
|
||||
"E-Ink Mode": "E-Ink-läge"
|
||||
}
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "மொழிபெயர்ப்பை மாற்றவும்",
|
||||
"Translation Disabled": "மொழிபெயர்ப்பு முடக்கப்பட்டுள்ளது",
|
||||
"Minimize": "சிறிது செய்யவும்",
|
||||
"Maximize or Restore": "அதிகரிக்கவும் அல்லது மீட்டமைக்கவும்"
|
||||
"Maximize or Restore": "அதிகரிக்கவும் அல்லது மீட்டமைக்கவும்",
|
||||
"Exit Parallel Read": "இணை வாசிப்பை வெளியேறு",
|
||||
"Enter Parallel Read": "இணை வாசிப்பில் நுழையவும்",
|
||||
"Zoom Level": "ஜூம் நிலை",
|
||||
"Zoom Out": "ஜூம் குறைக்கவும்",
|
||||
"Reset Zoom": "ஜூம் மீட்டமைக்கவும்",
|
||||
"Zoom In": "ஜூம் அதிகரிக்கவும்",
|
||||
"Zoom Mode": "ஜூம் முறை",
|
||||
"Single Page": "ஒரு பக்கம்",
|
||||
"Auto Spread": "தானாக பரவல்",
|
||||
"Fit Page": "பக்கத்திற்கு பொருந்தும்",
|
||||
"Fit Width": "அகலத்திற்கு பொருந்தும்",
|
||||
"Failed to select directory": "கோப்புறையைத் தேர்வுசெய்ய முடியவில்லை",
|
||||
"The new data directory must be different from the current one.": "புதிய தரவுக் கோப்புறை தற்போதையதைப் போலவே இருக்கக்கூடாது.",
|
||||
"Migration failed: {{error}}": "மொத்தமாக்கல் தோல்வியுற்றது: {{error}}",
|
||||
"Change Data Location": "தரவுப் இடத்தை மாற்றவும்",
|
||||
"Current Data Location": "தற்போதைய தரவுப் இடம்",
|
||||
"Total size: {{size}}": "மொத்த அளவு: {{size}}",
|
||||
"Calculating file info...": "கோப்பு தகவலை கணக்கிடுகிறது...",
|
||||
"New Data Location": "புதிய தரவுப் இடம்",
|
||||
"Choose Different Folder": "வேறுபட்ட கோப்புறையைத் தேர்ந்தெடுக்கவும்",
|
||||
"Choose New Folder": "புதிய கோப்புறையைத் தேர்ந்தெடுக்கவும்",
|
||||
"Migrating data...": "தரவை மாறுகிறது...",
|
||||
"Copying: {{file}}": "பகுப்பாய்வு: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}}/{{total}} கோப்புகள்",
|
||||
"Migration completed successfully!": "மொத்தமாக்கல் வெற்றிகரமாக முடிந்தது!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "உங்கள் தரவுகள் புதிய இடத்திற்கு மாறியுள்ளன. செயலியை மீண்டும் தொடங்கவும்.",
|
||||
"Migration failed": "மொத்தமாக்கல் தோல்வியுற்றது",
|
||||
"Important Notice": "முக்கிய அறிவிப்பு",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "இது உங்கள் செயலியின் அனைத்து தரவுகளையும் புதிய இடத்திற்கு நகர்த்தும். இலக்கு இடத்தில் போதுமான இலவச இடம் உள்ளதா என்பதை உறுதிப்படுத்தவும்.",
|
||||
"Restart App": "செயலியை மீண்டும் தொடங்கவும்",
|
||||
"Start Migration": "மொத்தமாக்கலை தொடங்கவும்",
|
||||
"Advanced Settings": "மேம்பட்ட அமைப்புகள்",
|
||||
"File count: {{size}}": "கோப்பு எண்ணிக்கை: {{size}}",
|
||||
"Background Read Aloud": "பின்னணி உரை வாசிப்பு",
|
||||
"Ready to read aloud": "வாசிக்க தயாராக உள்ளது",
|
||||
"Read Aloud": "உரை வாசிப்பு",
|
||||
"Screen Brightness": "திரை பிரகாசம்",
|
||||
"Background Image": "பின்னணி படம்",
|
||||
"Import Image": "புகைப்படம் இறக்குமதி",
|
||||
"Opacity": "பார்வை",
|
||||
"Size": "அளவு",
|
||||
"Cover": "மூடு",
|
||||
"Contain": "அடங்கும்",
|
||||
"{{number}} pages left in chapter": "அத்தியாயத்தில் {{number}} பக்கங்கள் மீதமுள்ளன",
|
||||
"Device": "சாதனம்",
|
||||
"E-Ink Mode": "E-Ink முறை"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "เปลี่ยนการแปล",
|
||||
"Translation Disabled": "การแปลถูกปิดใช้งาน",
|
||||
"Minimize": "ย่อ",
|
||||
"Maximize or Restore": "ขยายหรือคืนค่า"
|
||||
"Maximize or Restore": "ขยายหรือคืนค่า",
|
||||
"Exit Parallel Read": "ออกจากการอ่านคู่ขนาน",
|
||||
"Enter Parallel Read": "เริ่มการอ่านคู่ขนาน",
|
||||
"Zoom Level": "ระดับการซูม",
|
||||
"Zoom Out": "ลดระดับการซูม",
|
||||
"Reset Zoom": "รีเซ็ตระดับการซูม",
|
||||
"Zoom In": "เพิ่มระดับการซูม",
|
||||
"Zoom Mode": "โหมดการซูม",
|
||||
"Single Page": "หน้าเดียว",
|
||||
"Auto Spread": "การกระจายอัตโนมัติ",
|
||||
"Fit Page": "พอดีกับหน้า",
|
||||
"Fit Width": "พอดีกับความกว้าง",
|
||||
"Failed to select directory": "ไม่สามารถเลือกไดเรกทอรีได้",
|
||||
"The new data directory must be different from the current one.": "ไดเรกทอรีข้อมูลใหม่จะต้องแตกต่างจากไดเรกทอรีปัจจุบัน",
|
||||
"Migration failed: {{error}}": "การย้ายข้อมูลล้มเหลว: {{error}}",
|
||||
"Change Data Location": "เปลี่ยนตำแหน่งข้อมูล",
|
||||
"Current Data Location": "ตำแหน่งข้อมูลปัจจุบัน",
|
||||
"Total size: {{size}}": "ขนาดรวม: {{size}}",
|
||||
"Calculating file info...": "กำลังคำนวณข้อมูลไฟล์...",
|
||||
"New Data Location": "ตำแหน่งข้อมูลใหม่",
|
||||
"Choose Different Folder": "เลือกโฟลเดอร์ที่แตกต่าง",
|
||||
"Choose New Folder": "เลือกโฟลเดอร์ใหม่",
|
||||
"Migrating data...": "กำลังย้ายข้อมูล...",
|
||||
"Copying: {{file}}": "กำลังคัดลอก: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} จาก {{total}} ไฟล์",
|
||||
"Migration completed successfully!": "การย้ายข้อมูลเสร็จสมบูรณ์!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "ข้อมูลของคุณได้ถูกย้ายไปยังตำแหน่งใหม่แล้ว กรุณารีสตาร์ทแอปพลิเคชันเพื่อทำให้กระบวนการเสร็จสมบูรณ์",
|
||||
"Migration failed": "การย้ายข้อมูลล้มเหลว",
|
||||
"Important Notice": "ประกาศสำคัญ",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "สิ่งนี้จะย้ายข้อมูลแอปทั้งหมดของคุณไปยังตำแหน่งใหม่ ตรวจสอบให้แน่ใจว่าตำแหน่งปลายทางมีพื้นที่ว่างเพียงพอ",
|
||||
"Restart App": "รีสตาร์ทแอป",
|
||||
"Start Migration": "เริ่มการย้ายข้อมูล",
|
||||
"Advanced Settings": "การตั้งค่าขั้นสูง",
|
||||
"File count: {{size}}": "จำนวนไฟล์: {{size}}",
|
||||
"Background Read Aloud": "อ่านออกเสียงเบื้องหลัง",
|
||||
"Ready to read aloud": "พร้อมที่จะอ่านออกเสียง",
|
||||
"Read Aloud": "อ่านออกเสียง",
|
||||
"Screen Brightness": "ความสว่างหน้าจอ",
|
||||
"Background Image": "ภาพพื้นหลัง",
|
||||
"Import Image": "นำเข้าภาพ",
|
||||
"Opacity": "ความโปร่งใส",
|
||||
"Size": "ขนาด",
|
||||
"Cover": "ปก",
|
||||
"Contain": "บรรจุ",
|
||||
"{{number}} pages left in chapter": "เหลือ {{number}} หน้าในบท",
|
||||
"Device": "อุปกรณ์",
|
||||
"E-Ink Mode": "โหมด E-Ink"
|
||||
}
|
||||
|
||||
@@ -563,5 +563,51 @@
|
||||
"Toggle Translation": "Çeviriyi Değiştir",
|
||||
"Translation Disabled": "Çeviri Devre Dışı",
|
||||
"Minimize": "Küçült",
|
||||
"Maximize or Restore": "Büyüt veya Geri Yükle"
|
||||
"Maximize or Restore": "Büyüt veya Geri Yükle",
|
||||
"Exit Parallel Read": "Paralel Okumadan Çık",
|
||||
"Enter Parallel Read": "Paralel Okumaya Gir",
|
||||
"Zoom Level": "Yakınlaştırma Düzeyi",
|
||||
"Zoom Out": "Yakınlaştırmayı Kaldır",
|
||||
"Reset Zoom": "Yakınlaştırmayı Sıfırla",
|
||||
"Zoom In": "Yakınlaştırmayı Artır",
|
||||
"Zoom Mode": "Yakınlaştırma Modu",
|
||||
"Single Page": "Tek Sayfa",
|
||||
"Auto Spread": "Otomatik Yayılma",
|
||||
"Fit Page": "Sayfaya Sığdır",
|
||||
"Fit Width": "Genişliğe Sığdır",
|
||||
"Failed to select directory": "Dizini seçme başarısız oldu",
|
||||
"The new data directory must be different from the current one.": "Yeni veri dizini mevcut olanla farklı olmalıdır.",
|
||||
"Migration failed: {{error}}": "Taşıma işlemi başarısız oldu: {{error}}",
|
||||
"Change Data Location": "Veri Konumunu Değiştir",
|
||||
"Current Data Location": "Mevcut Veri Konumu",
|
||||
"Total size: {{size}}": "Toplam boyut: {{size}}",
|
||||
"Calculating file info...": "Dosya bilgileri hesaplanıyor...",
|
||||
"New Data Location": "Yeni Veri Konumu",
|
||||
"Choose Different Folder": "Farklı Klasör Seç",
|
||||
"Choose New Folder": "Yeni Klasör Seç",
|
||||
"Migrating data...": "Veri taşınıyor...",
|
||||
"Copying: {{file}}": "Kopyalanıyor: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} dosya",
|
||||
"Migration completed successfully!": "Taşıma işlemi başarıyla tamamlandı!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Verileriniz yeni konuma taşındı. Lütfen işlemi tamamlamak için uygulamayı yeniden başlatın.",
|
||||
"Migration failed": "Taşıma işlemi başarısız oldu",
|
||||
"Important Notice": "Önemli Duyuru",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Bu, tüm uygulama verilerinizi yeni konuma taşıyacaktır. Hedefin yeterli boş alana sahip olduğundan emin olun.",
|
||||
"Restart App": "Uygulamayı Yeniden Başlat",
|
||||
"Start Migration": "Taşımayı Başlat",
|
||||
"Advanced Settings": "Gelişmiş Ayarlar",
|
||||
"File count: {{size}}": "Dosya sayısı: {{size}}",
|
||||
"Background Read Aloud": "Arka Planda Sesli Okuma",
|
||||
"Ready to read aloud": "Sesli okumaya hazır",
|
||||
"Read Aloud": "Sesli Oku",
|
||||
"Screen Brightness": "Ekran Parlaklığı",
|
||||
"Background Image": "Arka Plan Görüntüsü",
|
||||
"Import Image": "Görüntü İçe Aktar",
|
||||
"Opacity": "Opaklık",
|
||||
"Size": "Boyut",
|
||||
"Cover": "Kapak",
|
||||
"Contain": "İçerir",
|
||||
"{{number}} pages left in chapter": "Bu bölümde {{number}} sayfa kaldı",
|
||||
"Device": "Cihaz",
|
||||
"E-Ink Mode": "E-Ink Modu"
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"Apply to This Book": "Застосувати лише до цієї книги",
|
||||
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
|
||||
"Check Update": "Перевірити оновлення",
|
||||
"Already the latest version": "У вас вже встановлена остання версія",
|
||||
"Already the latest version": "У вас вже встановлена найновіша версія",
|
||||
"Book Details": "Деталі книги",
|
||||
"From Local File": "З локального файлу",
|
||||
"TOC": "Зміст",
|
||||
@@ -499,7 +499,7 @@
|
||||
"Sync Conflict": "Конфлікт синхронізації",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "Синхронізувати прогрес читання із \"{{deviceName}}\"?",
|
||||
"another device": "інший пристрій",
|
||||
"Local Progress": "Локальний прогрес",
|
||||
"Local Progress": "Локальний проґрес",
|
||||
"Remote Progress": "Проґрес у хмарі",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "Сторінка {{page}} з {{total}} ({{percentage}}%)",
|
||||
"Current position": "Поточна позиція",
|
||||
@@ -547,8 +547,8 @@
|
||||
"Search Options": "Параметри пошуку",
|
||||
"Close": "Закрити",
|
||||
"Delete Book Options": "Параметри видалення книги",
|
||||
"ON": "УВІМКНЕННЯ",
|
||||
"OFF": "ВИМКНЕННЯ",
|
||||
"ON": "УВІМКНЕННО",
|
||||
"OFF": "ВИМКНЕНО",
|
||||
"Reading Progress": "Проґрес читання",
|
||||
"Page Margin": "Поля сторінки",
|
||||
"Remove Bookmark": "Видалити закладку",
|
||||
@@ -571,5 +571,51 @@
|
||||
"Toggle Translation": "Перемкнути переклад",
|
||||
"Translation Disabled": "Переклад вимкнено",
|
||||
"Minimize": "Згорнути",
|
||||
"Maximize or Restore": "Збільшити або відновити"
|
||||
"Maximize or Restore": "Збільшити або відновити",
|
||||
"Exit Parallel Read": "Вийти з паралельного читання",
|
||||
"Enter Parallel Read": "Войти в параллельное чтение",
|
||||
"Zoom Level": "Рівень масштабування",
|
||||
"Zoom Out": "Зменшити масштаб",
|
||||
"Reset Zoom": "Скинути масштаб",
|
||||
"Zoom In": "Збільшити масштаб",
|
||||
"Zoom Mode": "Режим масштабування",
|
||||
"Single Page": "Одна сторінка",
|
||||
"Auto Spread": "Автоматичне розповсюдження",
|
||||
"Fit Page": "Підганяти під сторінку",
|
||||
"Fit Width": "Підганяти під ширину",
|
||||
"Failed to select directory": "Не вдалося вибрати каталог",
|
||||
"The new data directory must be different from the current one.": "Новий каталог даних повинен відрізнятися від поточного.",
|
||||
"Migration failed: {{error}}": "Переміщення не вдалося: {{error}}",
|
||||
"Change Data Location": "Змінити розташування даних",
|
||||
"Current Data Location": "Поточне розташування даних",
|
||||
"Total size: {{size}}": "Загальний розмір: {{size}}",
|
||||
"Calculating file info...": "Обчислення інформації про файл...",
|
||||
"New Data Location": "Нове розташування даних",
|
||||
"Choose Different Folder": "Вибрати іншу папку",
|
||||
"Choose New Folder": "Вибрати нову папку",
|
||||
"Migrating data...": "Переміщення даних...",
|
||||
"Copying: {{file}}": "Копіювання: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} з {{total}} файлів",
|
||||
"Migration completed successfully!": "Переміщення завершено успішно!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ваші дані були переміщені до нового розташування. Будь ласка, перезапустіть програму, щоб завершити процес.",
|
||||
"Migration failed": "Переміщення не вдалося",
|
||||
"Important Notice": "Важливе повідомлення",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Це перемістить всі ваші дані програми до нового розташування. Переконайтеся, що в призначенні достатньо вільного місця.",
|
||||
"Restart App": "Перезапустити програму",
|
||||
"Start Migration": "Почати переміщення",
|
||||
"Advanced Settings": "Розширені налаштування",
|
||||
"File count: {{size}}": "Кількість файлів: {{size}}",
|
||||
"Background Read Aloud": "Озвучування у фоновому режимі",
|
||||
"Ready to read aloud": "Готовий до читання вголос",
|
||||
"Read Aloud": "Читати вголос",
|
||||
"Screen Brightness": "Яскравість екрану",
|
||||
"Background Image": "Фонове зображення",
|
||||
"Import Image": "Імпортувати зображення",
|
||||
"Opacity": "Непрозорість",
|
||||
"Size": "Розмір",
|
||||
"Cover": "Обкладинка",
|
||||
"Contain": "Вміщати",
|
||||
"{{number}} pages left in chapter": "Залишилось {{number}} сторінок у розділі",
|
||||
"Device": "Пристрій",
|
||||
"E-Ink Mode": "E-Ink режим"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "Chuyển đổi dịch thuật",
|
||||
"Translation Disabled": "Dịch thuật đã tắt",
|
||||
"Minimize": "Thu nhỏ",
|
||||
"Maximize or Restore": "Tối đa hóa hoặc Khôi phục"
|
||||
"Maximize or Restore": "Tối đa hóa hoặc Khôi phục",
|
||||
"Exit Parallel Read": "Thoát Đọc Song Song",
|
||||
"Enter Parallel Read": "Vào Đọc Song Song",
|
||||
"Zoom Level": "Độ phóng to",
|
||||
"Zoom Out": "Thu nhỏ",
|
||||
"Reset Zoom": "Đặt lại độ phóng to",
|
||||
"Zoom In": "Phóng to",
|
||||
"Zoom Mode": "Chế độ phóng to",
|
||||
"Single Page": "Một trang",
|
||||
"Auto Spread": "Tự động trải rộng",
|
||||
"Fit Page": "Vừa trang",
|
||||
"Fit Width": "Vừa chiều rộng",
|
||||
"Failed to select directory": "Không thể chọn thư mục",
|
||||
"The new data directory must be different from the current one.": "Thư mục dữ liệu mới phải khác với thư mục hiện tại.",
|
||||
"Migration failed: {{error}}": "Di chuyển không thành công: {{error}}",
|
||||
"Change Data Location": "Thay đổi vị trí dữ liệu",
|
||||
"Current Data Location": "Vị trí dữ liệu hiện tại",
|
||||
"Total size: {{size}}": "Tổng kích thước: {{size}}",
|
||||
"Calculating file info...": "Đang tính toán thông tin tệp...",
|
||||
"New Data Location": "Vị trí dữ liệu mới",
|
||||
"Choose Different Folder": "Chọn thư mục khác",
|
||||
"Choose New Folder": "Chọn thư mục mới",
|
||||
"Migrating data...": "Đang di chuyển dữ liệu...",
|
||||
"Copying: {{file}}": "Đang sao chép: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} của {{total}} tệp",
|
||||
"Migration completed successfully!": "Di chuyển hoàn tất thành công!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Dữ liệu của bạn đã được chuyển đến vị trí mới. Vui lòng khởi động lại ứng dụng để hoàn tất quá trình.",
|
||||
"Migration failed": "Di chuyển không thành công",
|
||||
"Important Notice": "Thông báo quan trọng",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Điều này sẽ di chuyển tất cả dữ liệu ứng dụng của bạn đến vị trí mới. Hãy chắc chắn rằng đích đến có đủ không gian trống.",
|
||||
"Restart App": "Khởi động lại ứng dụng",
|
||||
"Start Migration": "Bắt đầu di chuyển",
|
||||
"Advanced Settings": "Cài đặt nâng cao",
|
||||
"File count: {{size}}": "Số lượng tệp: {{size}}",
|
||||
"Background Read Aloud": "Đọc To Nền",
|
||||
"Ready to read aloud": "Sẵn sàng để đọc to",
|
||||
"Read Aloud": "Đọc To",
|
||||
"Screen Brightness": "Độ sáng màn hình",
|
||||
"Background Image": "Ảnh nền",
|
||||
"Import Image": "Nhập ảnh",
|
||||
"Opacity": "Độ mờ",
|
||||
"Size": "Kích thước",
|
||||
"Cover": "Bìa",
|
||||
"Contain": "Chứa",
|
||||
"{{number}} pages left in chapter": "Còn {{number}} trang trong chương",
|
||||
"Device": "Thiết bị",
|
||||
"E-Ink Mode": "Chế độ E-Ink"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "切换翻译",
|
||||
"Translation Disabled": "翻译已禁用",
|
||||
"Minimize": "最小化",
|
||||
"Maximize or Restore": "最大化或还原"
|
||||
"Maximize or Restore": "最大化或还原",
|
||||
"Exit Parallel Read": "退出并排阅读",
|
||||
"Enter Parallel Read": "进入并排阅读",
|
||||
"Zoom Level": "缩放级别",
|
||||
"Zoom Out": "缩小",
|
||||
"Reset Zoom": "重置缩放",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Mode": "缩放模式",
|
||||
"Single Page": "单页",
|
||||
"Auto Spread": "自动展开",
|
||||
"Fit Page": "适应页面",
|
||||
"Fit Width": "适应宽度",
|
||||
"Failed to select directory": "选择目录失败",
|
||||
"The new data directory must be different from the current one.": "新的数据目录必须与当前目录不同。",
|
||||
"Migration failed: {{error}}": "迁移失败: {{error}}",
|
||||
"Change Data Location": "更改数据位置",
|
||||
"Current Data Location": "当前数据位置",
|
||||
"Total size: {{size}}": "总大小: {{size}}",
|
||||
"Calculating file info...": "正在计算文件信息...",
|
||||
"New Data Location": "新数据位置",
|
||||
"Choose Different Folder": "选择不同的文件夹",
|
||||
"Choose New Folder": "选择新文件夹",
|
||||
"Migrating data...": "正在迁移数据...",
|
||||
"Copying: {{file}}": "正在复制: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} 个文件",
|
||||
"Migration completed successfully!": "迁移成功完成!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "您的数据已移动到新位置。请重新启动应用程序以完成此过程。",
|
||||
"Migration failed": "迁移失败",
|
||||
"Important Notice": "重要通知",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "这将把您所有的应用数据移动到新位置。请确保目标位置有足够的可用空间。",
|
||||
"Restart App": "重启应用",
|
||||
"Start Migration": "开始迁移",
|
||||
"Advanced Settings": "高级设置",
|
||||
"File count: {{size}}": "文件数量: {{size}}",
|
||||
"Background Read Aloud": "后台语音朗读",
|
||||
"Ready to read aloud": "语音朗读已就绪",
|
||||
"Read Aloud": "语音朗读",
|
||||
"Screen Brightness": "屏幕亮度",
|
||||
"Background Image": "背景图片",
|
||||
"Import Image": "导入图片",
|
||||
"Opacity": "不透明度",
|
||||
"Size": "大小",
|
||||
"Cover": "覆盖",
|
||||
"Contain": "适应",
|
||||
"{{number}} pages left in chapter": "本章剩余{{number}}页",
|
||||
"Device": "设备",
|
||||
"E-Ink Mode": "E-Ink 模式"
|
||||
}
|
||||
|
||||
@@ -559,5 +559,51 @@
|
||||
"Toggle Translation": "切換翻譯",
|
||||
"Translation Disabled": "翻譯已禁用",
|
||||
"Minimize": "最小化",
|
||||
"Maximize or Restore": "最大化或還原"
|
||||
"Maximize or Restore": "最大化或還原",
|
||||
"Exit Parallel Read": "退出並排閱讀",
|
||||
"Enter Parallel Read": "進入並排閱讀",
|
||||
"Zoom Level": "縮放級別",
|
||||
"Zoom Out": "縮小",
|
||||
"Reset Zoom": "重置縮放",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Mode": "縮放模式",
|
||||
"Single Page": "單頁",
|
||||
"Auto Spread": "自動展開",
|
||||
"Fit Page": "適應頁面",
|
||||
"Fit Width": "適應寬度",
|
||||
"Failed to select directory": "選擇目錄失敗",
|
||||
"The new data directory must be different from the current one.": "新的數據目錄必須與當前目錄不同。",
|
||||
"Migration failed: {{error}}": "遷移失敗: {{error}}",
|
||||
"Change Data Location": "更改數據位置",
|
||||
"Current Data Location": "當前數據位置",
|
||||
"Total size: {{size}}": "總大小: {{size}}",
|
||||
"Calculating file info...": "正在計算文件信息...",
|
||||
"New Data Location": "新的數據位置",
|
||||
"Choose Different Folder": "選擇不同的文件夾",
|
||||
"Choose New Folder": "選擇新文件夾",
|
||||
"Migrating data...": "正在遷移數據...",
|
||||
"Copying: {{file}}": "正在複製: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} 個文件",
|
||||
"Migration completed successfully!": "遷移成功完成!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "您的數據已移動到新位置。請重新啟動應用程序以完成此過程。",
|
||||
"Migration failed": "遷移失敗",
|
||||
"Important Notice": "重要通知",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "這將把您所有的應用數據移動到新位置。請確保目標位置有足夠的可用空間。",
|
||||
"Restart App": "重新啟動應用",
|
||||
"Start Migration": "開始遷移",
|
||||
"Advanced Settings": "高級設置",
|
||||
"File count: {{size}}": "文件數量: {{size}}",
|
||||
"Background Read Aloud": "背景朗讀",
|
||||
"Ready to read aloud": "朗讀準備就緒",
|
||||
"Read Aloud": "朗讀",
|
||||
"Screen Brightness": "螢幕亮度",
|
||||
"Background Image": "背景圖片",
|
||||
"Import Image": "匯入圖片",
|
||||
"Opacity": "不透明度",
|
||||
"Size": "大小",
|
||||
"Cover": "覆蓋",
|
||||
"Contain": "適應",
|
||||
"{{number}} pages left in chapter": "本章剩餘{{number}}頁",
|
||||
"Device": "設備",
|
||||
"E-Ink Mode": "E-Ink 模式"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,72 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.88": {
|
||||
"date": "2025-10-18",
|
||||
"notes": [
|
||||
"TTS: Fixed issue where the read-aloud indicator disappeared too quickly",
|
||||
"Layout: Overriding book layout no longer affects explicitly set text alignment",
|
||||
"Reader: Added localized number display for reading progress in vertical layout",
|
||||
"Reader: Enhanced swipe responsiveness for smoother page transitions",
|
||||
"Settings: Added an option to disable animations when using E-Ink mode"
|
||||
]
|
||||
},
|
||||
"0.9.87": {
|
||||
"date": "2025-10-16",
|
||||
"notes": [
|
||||
"Reader: Improved chapter detection with better chapter title recognition",
|
||||
"Reader: Enhanced compatibility for parsing more footnote formats",
|
||||
"Theme: Fixed auto theme switching in new reader windows",
|
||||
"TTS: Improved scrolling accuracy to highlighted text during read-aloud",
|
||||
"TTS: Fixed crash issues on certain Android systems"
|
||||
]
|
||||
},
|
||||
"0.9.86": {
|
||||
"date": "2025-10-14",
|
||||
"notes": [
|
||||
"Reader: Added support for per-book background image customization",
|
||||
"Reader: Added option to override book language metadata for improved hyphenation accuracy",
|
||||
"Sync: Readest sync in KOReader can now be triggered using gesture controls",
|
||||
"Sync: Fixed a crash in KOReader that occurred when syncing progress without an open book",
|
||||
"Sync: Improved upload performance with concurrent upload support"
|
||||
]
|
||||
},
|
||||
"0.9.85": {
|
||||
"date": "2025-10-14",
|
||||
"notes": [
|
||||
"TTS: Improved media session control compatibility across more Android systems",
|
||||
"TTS: Made the TTS icon less distracting during read-aloud mode",
|
||||
"Settings: Added an option to adjust screen brightness while reading",
|
||||
"Settings: Added support for custom background images",
|
||||
"PDF: Fixed an issue where scrolling and panning didn’t work correctly when zoomed in",
|
||||
"Android: Added adaptive launcher icon for better system integration",
|
||||
"Reader: Added keyboard shortcuts for toggling bookmarks",
|
||||
"Reader: Annotations are now rendered instantly when opening files"
|
||||
]
|
||||
},
|
||||
"0.9.82": {
|
||||
"date": "2025-10-01",
|
||||
"notes": [
|
||||
"Sync: More reliable syncing between KOReader and Readest",
|
||||
"MOBI: Fixed an issue where some footnotes in MOBI/AZW files were not parsed correctly",
|
||||
"Storage: Added support for changing data location on Windows, macOS, Linux, and Android",
|
||||
"Windows: Portable EXE version now stores all reading data in the directory of the executable",
|
||||
"Android: Added background TTS support with media session controls",
|
||||
"WebView: Extended compatibility down to version 92",
|
||||
"Settings: Added global settings menu to the library page",
|
||||
"iOS: Fixed an issue where custom theme colors were not handled correctly on older Safari versions",
|
||||
"TTS: Added more languages for Edge TTS"
|
||||
]
|
||||
},
|
||||
"0.9.81": {
|
||||
"date": "2025-09-19",
|
||||
"notes": [
|
||||
"TTS: Added desktop common media control support when TTS is playing",
|
||||
"PDF: Disabled swipe-up gesture for toggling the action bar when zoomed in",
|
||||
"Fonts: Fixed an issue where embedded fonts in ebooks were occasionally not applied",
|
||||
"Reader: Fixed an issue where newly created highlights sometimes would not appear",
|
||||
"Reader: Enabled scrolling in the menu when the menu is too long"
|
||||
]
|
||||
},
|
||||
"0.9.80": {
|
||||
"date": "2025-09-19",
|
||||
"notes": [
|
||||
|
||||
@@ -24,7 +24,8 @@ 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"/>'
|
||||
INSTALL_PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
STORAGE_PERMISSION_LINE='<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>'
|
||||
|
||||
if [[ ! -f "$PROPERTIES_FILE" ]]; then
|
||||
echo "❌ File not found: $PROPERTIES_FILE"
|
||||
@@ -38,13 +39,34 @@ mv "$tmpfile" "$PROPERTIES_FILE"
|
||||
|
||||
echo "✅ Updated $PROPERTIES_FILE"
|
||||
|
||||
ised() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "$@"
|
||||
else
|
||||
sed -i "$@"
|
||||
fi
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
# --- 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"
|
||||
if ised "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"; then
|
||||
echo "✅ Successfully removed REQUEST_INSTALL_PACKAGES"
|
||||
else
|
||||
sed -i "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"
|
||||
echo "❌ Failed to remove REQUEST_INSTALL_PACKAGES" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
|
||||
echo "🧹 Removing MANAGE_EXTERNAL_STORAGE from AndroidManifest.xml"
|
||||
if ised "/MANAGE_EXTERNAL_STORAGE/d" "$MANIFEST"; then
|
||||
echo "✅ Successfully removed MANAGE_EXTERNAL_STORAGE"
|
||||
else
|
||||
echo "❌ Failed to remove MANAGE_EXTERNAL_STORAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -54,13 +76,16 @@ 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
|
||||
ised "/android.permission.INTERNET/a\\
|
||||
$INSTALL_PERMISSION_LINE
|
||||
" "$MANIFEST"
|
||||
fi
|
||||
|
||||
if ! grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
|
||||
echo "♻️ Restoring MANAGE_EXTERNAL_STORAGE in AndroidManifest.xml"
|
||||
ised "/android.permission.WRITE_EXTERNAL_STORAGE/a\\
|
||||
$STORAGE_PERMISSION_LINE
|
||||
" "$MANIFEST"
|
||||
fi
|
||||
|
||||
source .env.google-play.local
|
||||
|
||||
@@ -49,6 +49,7 @@ tauri-plugin-opener = "2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2"
|
||||
tauri-plugin-persisted-scope = "2"
|
||||
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
|
||||
|
||||
|
||||
@@ -61,6 +61,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{
|
||||
"path": "**/Readest"
|
||||
},
|
||||
{
|
||||
"path": "**/Readest/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<?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.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
@@ -111,6 +115,23 @@
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name="androidx.media.session.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name="com.readest.native_tts.MediaPlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -33,8 +33,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.readest.native_bridge
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.KeyEvent
|
||||
import android.view.WindowInsets
|
||||
@@ -13,15 +16,19 @@ import android.view.WindowInsetsController
|
||||
import android.graphics.Color
|
||||
import android.webkit.WebView
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.fonts.SystemFonts
|
||||
import android.graphics.fonts.Font
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Permission
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
@@ -62,12 +69,21 @@ class LockScreenOrientationRequestArgs {
|
||||
var orientation: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class SetScreenBrightnessRequestArgs {
|
||||
var brightness: Double? = null // 0.0 to 1.0
|
||||
}
|
||||
|
||||
interface KeyDownInterceptor {
|
||||
fun interceptVolumeKeys(enabled: Boolean)
|
||||
fun interceptBackKey(enabled: Boolean)
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
@TauriPlugin(
|
||||
permissions = [
|
||||
Permission(strings = [Manifest.permission.MANAGE_EXTERNAL_STORAGE], alias = "manageStorage"),
|
||||
]
|
||||
)
|
||||
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
private val implementation = NativeBridge()
|
||||
private var redirectScheme = "readest"
|
||||
@@ -75,6 +91,7 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
|
||||
companion object {
|
||||
var pendingInvoke: Invoke? = null
|
||||
private const val REQUEST_MANAGE_STORAGE = 1001
|
||||
}
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
@@ -402,4 +419,91 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_screen_brightness(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val brightness = Settings.System.getInt(
|
||||
activity.contentResolver,
|
||||
Settings.System.SCREEN_BRIGHTNESS
|
||||
)
|
||||
val normalizedBrightness = brightness / 255.0
|
||||
ret.put("brightness", normalizedBrightness)
|
||||
} catch (e: Exception) {
|
||||
ret.put("error", e.message)
|
||||
ret.put("brightness", -1.0)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_screen_brightness(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetScreenBrightnessRequestArgs::class.java)
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val brightness = (args.brightness ?: 0.5).toFloat()
|
||||
if (brightness < 0.0 || brightness > 1.0) {
|
||||
invoke.reject("Brightness must be between 0.0 and 1.0")
|
||||
return
|
||||
}
|
||||
val layoutParams = activity.window.attributes
|
||||
layoutParams.screenBrightness = brightness
|
||||
activity.window.attributes = layoutParams
|
||||
ret.put("success", true)
|
||||
} catch (e: Exception) {
|
||||
ret.put("success", false)
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun request_manage_storage_permission(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.data = Uri.parse("package:${activity.packageName}")
|
||||
activity.startActivityForResult(intent, REQUEST_MANAGE_STORAGE)
|
||||
ret.put("manageStorage", "denied")
|
||||
invoke.resolve(ret)
|
||||
} catch (e: Exception) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
activity.startActivity(intent)
|
||||
ret.put("manageStorage", "denied")
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
} else {
|
||||
ret.put("manageStorage", "granted")
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
} else {
|
||||
val readPermission = ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val writePermission = ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
if (readPermission == PackageManager.PERMISSION_GRANTED &&
|
||||
writePermission == PackageManager.PERMISSION_GRANTED) {
|
||||
ret.put("manageStorage", "granted")
|
||||
invoke.resolve(ret)
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
REQUEST_MANAGE_STORAGE
|
||||
)
|
||||
ret.put("manageStorage", "prompt")
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ const COMMANDS: &[&str] = &[
|
||||
"iap_restore_purchases",
|
||||
"get_system_color_scheme",
|
||||
"get_safe_area_insets",
|
||||
"get_screen_brightness",
|
||||
"set_screen_brightness",
|
||||
"request_manage_storage_permission",
|
||||
"checkPermissions",
|
||||
"requestPermissions",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -49,6 +49,10 @@ class LockScreenOrientationRequestArgs: Decodable {
|
||||
let orientation: String?
|
||||
}
|
||||
|
||||
class SetScreenBrightnessRequestArgs: Decodable {
|
||||
let brightness: Float?
|
||||
}
|
||||
|
||||
struct InitializeRequest: Decodable {
|
||||
let publicKey: String?
|
||||
}
|
||||
@@ -539,6 +543,28 @@ class NativeBridgePlugin: Plugin {
|
||||
let colorScheme = (userInterfaceStyle == .dark) ? "dark" : "light"
|
||||
invoke.resolve(["colorScheme": colorScheme])
|
||||
}
|
||||
|
||||
@objc public func get_screen_brightness(_ invoke: Invoke) {
|
||||
let brightness = UIScreen.main.brightness
|
||||
invoke.resolve(["brightness": brightness])
|
||||
}
|
||||
|
||||
@objc public func set_screen_brightness(_ invoke: Invoke) {
|
||||
guard let args = try? invoke.parseArgs(SetScreenBrightnessRequestArgs.self) else {
|
||||
return invoke.reject("Failed to parse arguments")
|
||||
}
|
||||
|
||||
let brightness = args.brightness ?? 0.5
|
||||
|
||||
if brightness < 0.0 || brightness > 1.0 {
|
||||
return invoke.reject("Brightness must be between 0.0 and 1.0")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIScreen.main.brightness = CGFloat(brightness)
|
||||
}
|
||||
invoke.resolve(["success": true])
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_native_bridge")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-checkPermissions"
|
||||
description = "Enables the checkPermissions command without any pre-configured scope."
|
||||
commands.allow = ["checkPermissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-checkPermissions"
|
||||
description = "Denies the checkPermissions command without any pre-configured scope."
|
||||
commands.deny = ["checkPermissions"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-screen-brightness"
|
||||
description = "Enables the get_screen_brightness command without any pre-configured scope."
|
||||
commands.allow = ["get_screen_brightness"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-screen-brightness"
|
||||
description = "Denies the get_screen_brightness command without any pre-configured scope."
|
||||
commands.deny = ["get_screen_brightness"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-requestPermissions"
|
||||
description = "Enables the requestPermissions command without any pre-configured scope."
|
||||
commands.allow = ["requestPermissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-requestPermissions"
|
||||
description = "Denies the requestPermissions command without any pre-configured scope."
|
||||
commands.deny = ["requestPermissions"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-request-manage-storage-permission"
|
||||
description = "Enables the request_manage_storage_permission command without any pre-configured scope."
|
||||
commands.allow = ["request_manage_storage_permission"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-request-manage-storage-permission"
|
||||
description = "Denies the request_manage_storage_permission command without any pre-configured scope."
|
||||
commands.deny = ["request_manage_storage_permission"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-screen-brightness"
|
||||
description = "Enables the set_screen_brightness command without any pre-configured scope."
|
||||
commands.allow = ["set_screen_brightness"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-screen-brightness"
|
||||
description = "Denies the set_screen_brightness command without any pre-configured scope."
|
||||
commands.deny = ["set_screen_brightness"]
|
||||
@@ -20,6 +20,11 @@ Default permissions for the plugin
|
||||
- `allow-iap-restore-purchases`
|
||||
- `allow-get-system-color-scheme`
|
||||
- `allow-get-safe-area-insets`
|
||||
- `allow-get-screen-brightness`
|
||||
- `allow-set-screen-brightness`
|
||||
- `allow-request-manage-storage-permission`
|
||||
- `allow-checkPermissions`
|
||||
- `allow-requestPermissions`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -85,6 +90,32 @@ Denies the auth_with_safari command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-checkPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the checkPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-checkPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the checkPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-copy-uri-to-path`
|
||||
|
||||
</td>
|
||||
@@ -137,6 +168,32 @@ Denies the get_safe_area_insets command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-screen-brightness`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_screen_brightness command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-screen-brightness`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_screen_brightness command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-status-bar-height`
|
||||
|
||||
</td>
|
||||
@@ -397,6 +454,84 @@ Denies the lock_screen_orientation command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-requestPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the requestPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-requestPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the requestPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-request-manage-storage-permission`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the request_manage_storage_permission command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-request-manage-storage-permission`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the request_manage_storage_permission command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-set-screen-brightness`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_screen_brightness command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-set-screen-brightness`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_screen_brightness command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-set-system-ui-visibility`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -17,4 +17,9 @@ permissions = [
|
||||
"allow-iap-restore-purchases",
|
||||
"allow-get-system-color-scheme",
|
||||
"allow-get-safe-area-insets",
|
||||
"allow-get-screen-brightness",
|
||||
"allow-set-screen-brightness",
|
||||
"allow-request-manage-storage-permission",
|
||||
"allow-checkPermissions",
|
||||
"allow-requestPermissions",
|
||||
]
|
||||
|
||||
@@ -318,6 +318,18 @@
|
||||
"const": "deny-auth-with-safari",
|
||||
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-checkPermissions",
|
||||
"markdownDescription": "Enables the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-checkPermissions",
|
||||
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -342,6 +354,18 @@
|
||||
"const": "deny-get-safe-area-insets",
|
||||
"markdownDescription": "Denies the get_safe_area_insets command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_screen_brightness command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-screen-brightness",
|
||||
"markdownDescription": "Enables the get_screen_brightness command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_screen_brightness command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-screen-brightness",
|
||||
"markdownDescription": "Denies the get_screen_brightness command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_status_bar_height command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -462,6 +486,42 @@
|
||||
"const": "deny-lock-screen-orientation",
|
||||
"markdownDescription": "Denies the lock_screen_orientation command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-requestPermissions",
|
||||
"markdownDescription": "Enables the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-requestPermissions",
|
||||
"markdownDescription": "Denies the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_manage_storage_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-manage-storage-permission",
|
||||
"markdownDescription": "Enables the request_manage_storage_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_manage_storage_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-manage-storage-permission",
|
||||
"markdownDescription": "Denies the request_manage_storage_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_screen_brightness command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-screen-brightness",
|
||||
"markdownDescription": "Enables the set_screen_brightness command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_screen_brightness command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-screen-brightness",
|
||||
"markdownDescription": "Denies the set_screen_brightness command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -487,10 +547,10 @@
|
||||
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`",
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -126,3 +126,25 @@ pub(crate) async fn get_safe_area_insets<R: Runtime>(
|
||||
) -> Result<GetSafeAreaInsetsResponse> {
|
||||
app.native_bridge().get_safe_area_insets()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_screen_brightness<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetScreenBrightnessResponse> {
|
||||
app.native_bridge().get_screen_brightness()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_screen_brightness<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: SetScreenBrightnessRequest,
|
||||
) -> Result<SetScreenBrightnessResponse> {
|
||||
app.native_bridge().set_screen_brightness(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<RequestManageStoragePermissionResponse> {
|
||||
app.native_bridge().request_manage_storage_permission()
|
||||
}
|
||||
|
||||
@@ -106,4 +106,21 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_safe_area_insets(&self) -> crate::Result<GetSafeAreaInsetsResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn get_screen_brightness(&self) -> crate::Result<GetScreenBrightnessResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn set_screen_brightness(
|
||||
&self,
|
||||
_payload: SetScreenBrightnessRequest,
|
||||
) -> crate::Result<SetScreenBrightnessResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::iap_restore_purchases,
|
||||
commands::get_system_color_scheme,
|
||||
commands::get_safe_area_insets,
|
||||
commands::get_screen_brightness,
|
||||
commands::set_screen_brightness,
|
||||
commands::request_manage_storage_permission,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -169,3 +169,32 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_screen_brightness(&self) -> crate::Result<GetScreenBrightnessResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_screen_brightness", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn set_screen_brightness(
|
||||
&self,
|
||||
payload: SetScreenBrightnessRequest,
|
||||
) -> crate::Result<SetScreenBrightnessResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_screen_brightness", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("request_manage_storage_permission", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,3 +167,28 @@ pub struct GetSafeAreaInsetsResponse {
|
||||
pub left: f64,
|
||||
pub right: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetScreenBrightnessResponse {
|
||||
pub brightness: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetScreenBrightnessRequest {
|
||||
pub brightness: f64, // 0.0 to 1.0
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetScreenBrightnessResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestManageStoragePermissionResponse {
|
||||
pub manage_storage: String, // "granted", "denied", or "prompt"
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("androidx.media:media:1.7.1")
|
||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.readest.native_tts
|
||||
|
||||
import com.readest.native_tts.R
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.graphics.Bitmap
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import app.tauri.plugin.JSObject
|
||||
|
||||
class MediaPlaybackService : MediaBrowserServiceCompat() {
|
||||
private var mediaSession: MediaSessionCompat? = null
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var stateBuilder: PlaybackStateCompat.Builder
|
||||
private lateinit var audioManager: AudioManager
|
||||
|
||||
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||
Log.i("MediaPlaybackService", "Audio focus changed: $focusChange, $player.isPlaying")
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
player.volume = 1.0f
|
||||
if (!player.isPlaying) player.play()
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
player.volume = 0.3f
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
if (player.isPlaying) player.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "media2_playback_channel"
|
||||
private const val NOTIFICATION_ID = 1002
|
||||
private const val MEDIA_ROOT_ID = "media_root_id"
|
||||
|
||||
var pluginEventTrigger: ((String, JSObject) -> Unit)? = null
|
||||
|
||||
var currentTitle: String = "Read Aloud"
|
||||
var currentArtist: String = "Reading your content"
|
||||
var currentArtwork: Bitmap? = null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val result = audioManager.requestAudioFocus(
|
||||
afChangeListener,
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN
|
||||
)
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.d("MediaPlaybackService", "Audio focus granted")
|
||||
} else {
|
||||
Log.w("MediaPlaybackService", "Failed to gain audio focus")
|
||||
}
|
||||
|
||||
player = ExoPlayer.Builder(this).build()
|
||||
|
||||
mediaSession = MediaSessionCompat(baseContext, "ReadestMediaSession").apply {
|
||||
stateBuilder = PlaybackStateCompat.Builder().setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_PAUSE or
|
||||
PlaybackStateCompat.ACTION_STOP or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
)
|
||||
setPlaybackState(stateBuilder.build())
|
||||
setCallback(SessionCallback())
|
||||
setSessionToken(sessionToken)
|
||||
isActive = true
|
||||
}
|
||||
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
updatePlaybackState()
|
||||
}
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
updatePlaybackState()
|
||||
}
|
||||
})
|
||||
|
||||
val mediaItem = MediaItem.fromUri("asset:///silence.mp3")
|
||||
player.setMediaItem(mediaItem)
|
||||
player.repeatMode = Player.REPEAT_MODE_ONE
|
||||
player.prepare()
|
||||
player.playWhenReady = true
|
||||
|
||||
showNotification(PlaybackStateCompat.STATE_PLAYING)
|
||||
}
|
||||
|
||||
private inner class SessionCallback : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
player.play()
|
||||
pluginEventTrigger?.invoke("media-session-play", JSObject())
|
||||
updatePlaybackState()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
player.pause()
|
||||
pluginEventTrigger?.invoke("media-session-pause", JSObject())
|
||||
updatePlaybackState()
|
||||
}
|
||||
|
||||
override fun onSkipToNext() {
|
||||
player.seekTo(0)
|
||||
pluginEventTrigger?.invoke("media-session-next", JSObject())
|
||||
}
|
||||
|
||||
override fun onSkipToPrevious() {
|
||||
player.seekTo(0)
|
||||
pluginEventTrigger?.invoke("media-session-previous", JSObject())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlaybackState() {
|
||||
val state = if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
|
||||
mediaSession?.setPlaybackState(
|
||||
stateBuilder.setState(state, player.currentPosition, 1f).build()
|
||||
)
|
||||
showNotification(state)
|
||||
}
|
||||
|
||||
private fun showNotification(playbackState: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(CHANNEL_ID, "Media Controls", NotificationManager.IMPORTANCE_LOW)
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
}
|
||||
startForeground(NOTIFICATION_ID, buildNotification(playbackState))
|
||||
}
|
||||
|
||||
private fun buildNotification(playbackState: Int): Notification {
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply {
|
||||
setContentTitle(currentTitle)
|
||||
setContentText(currentArtist)
|
||||
setLargeIcon(currentArtwork)
|
||||
setContentIntent(mediaSession!!.controller.sessionActivity)
|
||||
setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this@MediaPlaybackService, PlaybackStateCompat.ACTION_STOP))
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
|
||||
addAction(
|
||||
android.R.drawable.ic_media_previous,
|
||||
"Previous",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
)
|
||||
)
|
||||
if (playbackState == PlaybackStateCompat.STATE_PLAYING) {
|
||||
addAction(
|
||||
android.R.drawable.ic_media_pause,
|
||||
"Pause",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_PAUSE
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addAction(
|
||||
android.R.drawable.ic_media_play,
|
||||
"Play",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_PLAY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addAction(
|
||||
android.R.drawable.ic_media_next,
|
||||
"Next",
|
||||
MediaButtonReceiver.buildMediaButtonPendingIntent(
|
||||
this@MediaPlaybackService,
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||
)
|
||||
)
|
||||
|
||||
setStyle(
|
||||
androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSession?.sessionToken)
|
||||
.setShowActionsInCompactView(0, 1, 2)
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
||||
return BrowserRoot(MEDIA_ROOT_ID, null)
|
||||
}
|
||||
|
||||
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
result.sendResult(null)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
MediaButtonReceiver.handleIntent(mediaSession, intent)
|
||||
|
||||
if (intent?.action == "UPDATE_METADATA") {
|
||||
currentTitle = intent.getStringExtra("title") ?: currentTitle
|
||||
currentArtist = intent.getStringExtra("artist") ?: currentArtist
|
||||
val newArtwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra("artwork", Bitmap::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("artwork")
|
||||
}
|
||||
if (newArtwork != null) {
|
||||
currentArtwork = newArtwork
|
||||
}
|
||||
|
||||
val metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentTitle)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentArtist)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, currentArtwork)
|
||||
|
||||
mediaSession?.setMetadata(metadataBuilder.build())
|
||||
|
||||
showNotification(if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED)
|
||||
} else if (intent?.action == "UPDATE_PLAYBACK_STATE") {
|
||||
val isPlaying = intent.getBooleanExtra("playing", false)
|
||||
val position = intent.getLongExtra("position", 0L) // in milliseconds
|
||||
val duration = intent.getLongExtra("duration", 0L) // in milliseconds
|
||||
|
||||
if (isPlaying && !player.isPlaying) {
|
||||
player.play()
|
||||
} else if (!isPlaying && player.isPlaying) {
|
||||
player.pause()
|
||||
}
|
||||
player.seekTo(position)
|
||||
|
||||
val state = if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
|
||||
mediaSession?.setPlaybackState(
|
||||
stateBuilder.setState(state, position, 1f).build()
|
||||
)
|
||||
showNotification(state)
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
player.release()
|
||||
mediaSession?.release()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.readest.native_tts
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Bundle
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
@@ -8,8 +9,12 @@ import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.speech.tts.Voice
|
||||
import android.util.Log
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Permission
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
@@ -25,6 +30,16 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.*
|
||||
import java.net.URL
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
||||
data class TTSVoiceData(
|
||||
val id: String,
|
||||
@@ -60,12 +75,45 @@ class SetVoiceArgs(
|
||||
val voice: String? = null
|
||||
)
|
||||
|
||||
@TauriPlugin
|
||||
@InvokeArg
|
||||
class UpdateMediaSessionMetadataArgs {
|
||||
var title: String? = null
|
||||
var artist: String? = null
|
||||
var album: String? = null
|
||||
var artwork: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class UpdateMediaSessionStateArgs {
|
||||
var playing: Boolean? = null
|
||||
var position: Int? = null // in milliseconds
|
||||
var duration: Int? = null // in milliseconds
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class SetMediaSessionActiveArgs {
|
||||
var active: Boolean? = null
|
||||
var keepAppInForeground: Boolean? = null
|
||||
var notificationTitle: String? = null
|
||||
var notificationText: String? = null
|
||||
var foregroundServiceTitle: String? = null
|
||||
var foregroundServiceText: String? = null
|
||||
}
|
||||
|
||||
@TauriPlugin(
|
||||
permissions = [
|
||||
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification")
|
||||
]
|
||||
)
|
||||
class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NativeTTSPlugin"
|
||||
private const val CHANNEL_NAME = "tts_events"
|
||||
var NOTIFICATION_TITLE = "Read Aloud"
|
||||
var NOTIFICATION_TEXT = "Ready to read aloud"
|
||||
var FOREGROUND_SERVICE_TITLE = "Read Aloud"
|
||||
var FOREGROUND_SERVICE_TEXT = "Ready to read aloud"
|
||||
}
|
||||
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
@@ -77,9 +125,8 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
|
||||
private val speakingJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
|
||||
@Command
|
||||
fun init(invoke: Invoke) {
|
||||
coroutineScope.launch {
|
||||
@@ -373,13 +420,121 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
invoke.reject("Exception getting voices: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadArtworkFromUrl(urlString: String): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
when {
|
||||
urlString.startsWith("data:image/") -> {
|
||||
val base64Data = urlString.substringAfter("base64,")
|
||||
val decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT)
|
||||
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
||||
}
|
||||
urlString.startsWith("http") -> {
|
||||
val url = URL(urlString)
|
||||
val input: java.io.InputStream = url.openStream()
|
||||
BitmapFactory.decodeStream(input)
|
||||
}
|
||||
else -> {
|
||||
val assetPath = urlString.removePrefix("/")
|
||||
val inputStream = activity.assets.open(assetPath)
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun update_media_session_metadata(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(UpdateMediaSessionMetadataArgs::class.java)
|
||||
val title = args.title ?: ""
|
||||
val artist = args.artist ?: ""
|
||||
val album = args.album ?: ""
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val artworkBitmap = args.artwork?.let { loadArtworkFromUrl(it) }
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java).apply {
|
||||
action = "UPDATE_METADATA"
|
||||
putExtra("title", title)
|
||||
putExtra("artist", artist)
|
||||
putExtra("album", album)
|
||||
putExtra("artwork", artworkBitmap)
|
||||
}
|
||||
activity.startService(intent)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Failed to update metadata: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun update_media_session_state(invoke: Invoke) {
|
||||
var args = invoke.parseArgs(UpdateMediaSessionStateArgs::class.java)
|
||||
val isPlaying = args.playing ?: false
|
||||
val position = args.position ?: 0
|
||||
val duration = args.duration ?: 0
|
||||
|
||||
try {
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java).apply {
|
||||
action = "UPDATE_PLAYBACK_STATE"
|
||||
putExtra("playing", isPlaying)
|
||||
putExtra("position", position)
|
||||
putExtra("duration", duration)
|
||||
}
|
||||
activity.startService(intent)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Failed to update playback state: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_media_session_active(invoke: Invoke) {
|
||||
var args = invoke.parseArgs(SetMediaSessionActiveArgs::class.java)
|
||||
val active = args.active ?: true
|
||||
|
||||
args.notificationTitle?.let { NOTIFICATION_TITLE = it }
|
||||
args.notificationText?.let { NOTIFICATION_TEXT = it }
|
||||
args.foregroundServiceTitle?.let { FOREGROUND_SERVICE_TITLE = it }
|
||||
args.foregroundServiceText?.let { FOREGROUND_SERVICE_TEXT = it }
|
||||
|
||||
try {
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
if (active) {
|
||||
MediaPlaybackService.pluginEventTrigger = { event, data -> trigger(event, data) }
|
||||
MediaPlaybackService.currentTitle = FOREGROUND_SERVICE_TITLE
|
||||
MediaPlaybackService.currentArtist = FOREGROUND_SERVICE_TEXT
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
} else {
|
||||
activity.stopService(intent)
|
||||
MediaPlaybackService.pluginEventTrigger = null
|
||||
}
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Failed to set media session active state: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
eventChannels.values.forEach { it.close() }
|
||||
eventChannels.clear()
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
speakingJobs.clear()
|
||||
try {
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
activity.stopService(intent)
|
||||
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
eventChannels.values.forEach { it.close() }
|
||||
eventChannels.clear()
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
speakingJobs.clear()
|
||||
|
||||
Log.d(TAG, "Plugin destroyed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during plugin destruction", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 42 KiB |
@@ -8,6 +8,11 @@ const COMMANDS: &[&str] = &[
|
||||
"set_pitch",
|
||||
"set_voice",
|
||||
"get_all_voices",
|
||||
"set_media_session_active",
|
||||
"update_media_session_state",
|
||||
"update_media_session_metadata",
|
||||
"checkPermissions",
|
||||
"requestPermissions",
|
||||
"registerListener",
|
||||
"remove_listener",
|
||||
];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-checkPermissions"
|
||||
description = "Enables the checkPermissions command without any pre-configured scope."
|
||||
commands.allow = ["checkPermissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-checkPermissions"
|
||||
description = "Denies the checkPermissions command without any pre-configured scope."
|
||||
commands.deny = ["checkPermissions"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-requestPermissions"
|
||||
description = "Enables the requestPermissions command without any pre-configured scope."
|
||||
commands.allow = ["requestPermissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-requestPermissions"
|
||||
description = "Denies the requestPermissions command without any pre-configured scope."
|
||||
commands.deny = ["requestPermissions"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-media-session-active"
|
||||
description = "Enables the set_media_session_active command without any pre-configured scope."
|
||||
commands.allow = ["set_media_session_active"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-media-session-active"
|
||||
description = "Denies the set_media_session_active command without any pre-configured scope."
|
||||
commands.deny = ["set_media_session_active"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-update-media-session-metadata"
|
||||
description = "Enables the update_media_session_metadata command without any pre-configured scope."
|
||||
commands.allow = ["update_media_session_metadata"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-update-media-session-metadata"
|
||||
description = "Denies the update_media_session_metadata command without any pre-configured scope."
|
||||
commands.deny = ["update_media_session_metadata"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-update-media-session-state"
|
||||
description = "Enables the update_media_session_state command without any pre-configured scope."
|
||||
commands.allow = ["update_media_session_state"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-update-media-session-state"
|
||||
description = "Denies the update_media_session_state command without any pre-configured scope."
|
||||
commands.deny = ["update_media_session_state"]
|
||||
@@ -13,6 +13,11 @@ Default permissions for the plugin
|
||||
- `allow-set-pitch`
|
||||
- `allow-set-voice`
|
||||
- `allow-get-all-voices`
|
||||
- `allow-set-media-session-active`
|
||||
- `allow-update-media-session-state`
|
||||
- `allow-update-media-session-metadata`
|
||||
- `allow-checkPermissions`
|
||||
- `allow-requestPermissions`
|
||||
- `allow-registerListener`
|
||||
- `allow-remove-listener`
|
||||
|
||||
@@ -25,6 +30,32 @@ Default permissions for the plugin
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-checkPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the checkPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-checkPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the checkPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -158,6 +189,32 @@ Denies the remove_listener command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-requestPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the requestPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-requestPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the requestPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-resume`
|
||||
|
||||
</td>
|
||||
@@ -184,6 +241,32 @@ Denies the resume command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-media-session-active`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_media_session_active command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-media-session-active`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_media_session_active command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-pitch`
|
||||
|
||||
</td>
|
||||
@@ -308,6 +391,58 @@ Enables the stop command without any pre-configured scope.
|
||||
|
||||
Denies the stop command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-update-media-session-metadata`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the update_media_session_metadata command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-update-media-session-metadata`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the update_media_session_metadata command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-update-media-session-state`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the update_media_session_state command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-update-media-session-state`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the update_media_session_state command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -10,6 +10,11 @@ permissions = [
|
||||
"allow-set-pitch",
|
||||
"allow-set-voice",
|
||||
"allow-get-all-voices",
|
||||
"allow-set-media-session-active",
|
||||
"allow-update-media-session-state",
|
||||
"allow-update-media-session-metadata",
|
||||
"allow-checkPermissions",
|
||||
"allow-requestPermissions",
|
||||
"allow-registerListener",
|
||||
"allow-remove-listener",
|
||||
]
|
||||
|
||||
@@ -294,6 +294,18 @@
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-checkPermissions",
|
||||
"markdownDescription": "Enables the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-checkPermissions",
|
||||
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_all_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -354,6 +366,18 @@
|
||||
"const": "deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-requestPermissions",
|
||||
"markdownDescription": "Enables the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-requestPermissions",
|
||||
"markdownDescription": "Denies the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the resume command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -366,6 +390,18 @@
|
||||
"const": "deny-resume",
|
||||
"markdownDescription": "Denies the resume command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_media_session_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-media-session-active",
|
||||
"markdownDescription": "Enables the set_media_session_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_media_session_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-media-session-active",
|
||||
"markdownDescription": "Denies the set_media_session_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_pitch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -427,10 +463,34 @@
|
||||
"markdownDescription": "Denies the stop command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-registerListener`\n- `allow-remove-listener`",
|
||||
"description": "Enables the update_media_session_metadata command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-update-media-session-metadata",
|
||||
"markdownDescription": "Enables the update_media_session_metadata command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the update_media_session_metadata command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-update-media-session-metadata",
|
||||
"markdownDescription": "Denies the update_media_session_metadata command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the update_media_session_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-update-media-session-state",
|
||||
"markdownDescription": "Enables the update_media_session_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the update_media_session_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-update-media-session-state",
|
||||
"markdownDescription": "Denies the update_media_session_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-checkPermissions`\n- `allow-requestPermissions`\n- `allow-registerListener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-registerListener`\n- `allow-remove-listener`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-checkPermissions`\n- `allow-requestPermissions`\n- `allow-registerListener`\n- `allow-remove-listener`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -51,3 +51,27 @@ pub(crate) async fn set_voice<R: Runtime>(app: AppHandle<R>, payload: SetVoiceAr
|
||||
pub(crate) async fn get_all_voices<R: Runtime>(app: AppHandle<R>) -> Result<GetVoicesResponse> {
|
||||
app.native_tts().get_all_voices()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_media_session_active<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: SetMediaSessionActiveRequest,
|
||||
) -> Result<()> {
|
||||
app.native_tts().set_media_session_active(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn update_media_session_state<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: UpdateMediaSessionStateRequest,
|
||||
) -> Result<()> {
|
||||
app.native_tts().update_media_session_state(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn update_media_session_metadata<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: UpdateMediaSessionMetadataRequest,
|
||||
) -> Result<()> {
|
||||
app.native_tts().update_media_session_metadata(payload)
|
||||
}
|
||||
|
||||
@@ -41,4 +41,22 @@ impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_all_voices(&self) -> crate::Result<GetVoicesResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_media_session_active(
|
||||
&self,
|
||||
_payload: SetMediaSessionActiveRequest,
|
||||
) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn update_media_session_state(
|
||||
&self,
|
||||
_payload: UpdateMediaSessionStateRequest,
|
||||
) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn update_media_session_metadata(
|
||||
&self,
|
||||
_payload: UpdateMediaSessionMetadataRequest,
|
||||
) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::set_pitch,
|
||||
commands::set_voice,
|
||||
commands::get_all_voices,
|
||||
commands::set_media_session_active,
|
||||
commands::update_media_session_state,
|
||||
commands::update_media_session_metadata,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -87,3 +87,36 @@ impl<R: Runtime> NativeTts<R> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_media_session_active(
|
||||
&self,
|
||||
payload: SetMediaSessionActiveRequest,
|
||||
) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_media_session_active", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn update_media_session_state(
|
||||
&self,
|
||||
payload: UpdateMediaSessionStateRequest,
|
||||
) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("update_media_session_state", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn update_media_session_metadata(
|
||||
&self,
|
||||
payload: UpdateMediaSessionMetadataRequest,
|
||||
) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("update_media_session_metadata", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,3 +61,31 @@ pub struct SetVoiceArgs {
|
||||
pub struct GetVoicesResponse {
|
||||
pub voices: Vec<TTSVoice>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetMediaSessionActiveRequest {
|
||||
pub active: bool,
|
||||
pub keep_app_in_foreground: bool,
|
||||
pub notification_title: Option<String>,
|
||||
pub notification_text: Option<String>,
|
||||
pub foreground_service_title: Option<String>,
|
||||
pub foreground_service_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateMediaSessionStateRequest {
|
||||
pub playing: bool,
|
||||
pub position: Option<f64>,
|
||||
pub duration: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateMediaSessionMetadataRequest {
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
}
|
||||
|
||||
@@ -42,6 +42,22 @@ fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn allow_dir_in_scopes(app: &AppHandle, dir: &PathBuf) {
|
||||
let fs_scope = app.fs_scope();
|
||||
let asset_protocol_scope = app.asset_protocol_scope();
|
||||
if let Err(e) = fs_scope.allow_directory(dir, true) {
|
||||
eprintln!("Failed to allow directory in fs_scope: {e}");
|
||||
} else {
|
||||
println!("Allowed directory in fs_scope: {dir:?}");
|
||||
}
|
||||
if let Err(e) = asset_protocol_scope.allow_directory(dir, true) {
|
||||
eprintln!("Failed to allow directory in asset_protocol_scope: {e}");
|
||||
} else {
|
||||
println!("Allowed directory in asset_protocol_scope: {dir:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn get_files_from_argv(argv: Vec<String>) -> Vec<PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
@@ -87,15 +103,6 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn set_rounded_window(app: &AppHandle, rounded: bool) {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let script = format!("window.IS_ROUNDED = {rounded};");
|
||||
if let Err(e) = window.eval(&script) {
|
||||
eprintln!("Failed to set IS_ROUNDED variable: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn start_server(window: Window) -> Result<u16, String> {
|
||||
start(move |url| {
|
||||
@@ -111,6 +118,15 @@ fn get_environment_variable(name: &str) -> String {
|
||||
std::env::var(String::from(name)).unwrap_or(String::from(""))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_executable_dir() -> String {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|p| p.to_path_buf()))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Payload {
|
||||
@@ -128,6 +144,7 @@ pub fn run() {
|
||||
download_file,
|
||||
upload_file,
|
||||
get_environment_variable,
|
||||
get_executable_dir,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::safari_auth::auth_with_safari,
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -135,14 +152,15 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::traffic_light::set_traffic_lights,
|
||||
])
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_persisted_scope::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_native_bridge::init())
|
||||
.plugin(tauri_plugin_native_tts::init())
|
||||
.plugin(tauri_plugin_fs::init());
|
||||
.plugin(tauri_plugin_native_tts::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
@@ -193,6 +211,11 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
allow_dir_in_scopes(app.handle(), &PathBuf::from(get_executable_dir()));
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
@@ -204,13 +227,6 @@ pub fn run() {
|
||||
.eval("window.__READEST_CLI_ACCESS = true;")
|
||||
.expect("Failed to set cli access config");
|
||||
|
||||
set_rounded_window(&app_handle, true);
|
||||
#[cfg(target_os = "windows")]
|
||||
if tauri_plugin_os::version()
|
||||
<= tauri_plugin_os::Version::from_string("10.0.19045")
|
||||
{
|
||||
set_rounded_window(&app_handle, false);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let is_appimage = std::env::var("APPIMAGE").is_ok()
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
|
||||
"frame-src": "'self' blob: asset: http://asset.localhost https://*.stripe.com",
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
|
||||
},
|
||||
@@ -30,6 +30,7 @@
|
||||
"$APPDATA/**/*",
|
||||
"$APPCACHE/**/*",
|
||||
"$TEMP/**/*",
|
||||
"**/Readest/**/*",
|
||||
"/private/var/mobile/Containers/Data/Application/**/*"
|
||||
],
|
||||
"deny": []
|
||||
|
||||
@@ -344,8 +344,7 @@ export default function AuthPage() {
|
||||
return isTauriAppPlatform() ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 inset-0 flex select-none flex-col items-center overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
'bg-base-100 inset-0 flex h-[100vh] select-none flex-col items-center overflow-hidden',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import posthog from 'posthog-js';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { parseWebViewVersion } from '@/utils/ua';
|
||||
import { parseWebViewInfo } from '@/utils/ua';
|
||||
import { handleGlobalError } from '@/utils/error';
|
||||
|
||||
interface ErrorPageProps {
|
||||
@@ -18,7 +18,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
|
||||
const [browserInfo, setBrowserInfo] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
setBrowserInfo(parseWebViewInfo(appService));
|
||||
posthog.captureException(error);
|
||||
handleGlobalError(error);
|
||||
}, [appService, error]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
@@ -14,7 +14,8 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
|
||||
import { formatAuthors, formatTitle } from '@/utils/book';
|
||||
import { createBookFilter, createBookSorter } from '../utils/libraryUtils';
|
||||
import { formatTitle } from '@/utils/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isMd5 } from '@/utils/md5';
|
||||
|
||||
@@ -30,11 +31,12 @@ interface BookshelfProps {
|
||||
isSelectAll: boolean;
|
||||
isSelectNone: boolean;
|
||||
handleImportBooks: () => void;
|
||||
handleBookUpload: (book: Book) => Promise<boolean>;
|
||||
handleBookDownload: (book: Book) => Promise<boolean>;
|
||||
handleBookDelete: (book: Book) => Promise<boolean>;
|
||||
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleSetSelectMode: (selectMode: boolean) => void;
|
||||
handleShowDetailsBook: (book: Book) => void;
|
||||
handlePushLibrary: () => Promise<void>;
|
||||
booksTransferProgress: { [key: string]: number | null };
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleBookDelete,
|
||||
handleSetSelectMode,
|
||||
handleShowDetailsBook,
|
||||
handlePushLibrary,
|
||||
booksTransferProgress,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
@@ -75,7 +78,18 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
|
||||
const allBookshelfItems = generateBookshelfItems(libraryBooks);
|
||||
|
||||
const bookFilter = useMemo(() => createBookFilter(queryTerm), [queryTerm]);
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
const bookSorter = useMemo(() => createBookSorter(sortBy, uiLanguage), [sortBy, uiLanguage]);
|
||||
|
||||
const filteredBooks = useMemo(() => {
|
||||
return queryTerm ? libraryBooks.filter((book) => bookFilter(book)) : libraryBooks;
|
||||
}, [libraryBooks, queryTerm, bookFilter]);
|
||||
|
||||
const allBookshelfItems = useMemo(() => {
|
||||
return generateBookshelfItems(filteredBooks);
|
||||
}, [filteredBooks]);
|
||||
|
||||
const autofocusRef = useAutoFocus<HTMLDivElement>();
|
||||
|
||||
@@ -104,8 +118,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
} else {
|
||||
setCurrentBookshelf(allBookshelfItems);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [libraryBooks, navBooksGroup]);
|
||||
}, [allBookshelfItems, navBooksGroup, setCurrentBookshelf]);
|
||||
|
||||
useEffect(() => {
|
||||
const group = searchParams?.get('group') || '';
|
||||
@@ -165,13 +178,14 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
params.delete('group');
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, libraryBooks, showGroupingModal]);
|
||||
}, [router, settings, searchParams, allBookshelfItems, showGroupingModal]);
|
||||
|
||||
const toggleSelection = useCallback((id: string) => {
|
||||
toggleSelectedBook(id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const toggleSelection = useCallback(
|
||||
(id: string) => {
|
||||
toggleSelectedBook(id);
|
||||
},
|
||||
[toggleSelectedBook],
|
||||
);
|
||||
|
||||
const openSelectedBooks = () => {
|
||||
handleSetSelectMode(false);
|
||||
@@ -195,7 +209,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const getBooksToDelete = () => {
|
||||
const booksToDelete: Book[] = [];
|
||||
bookIdsToDelete.forEach((id) => {
|
||||
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
|
||||
for (const book of filteredBooks.filter((book) => book.hash === id || book.groupId === id)) {
|
||||
if (book && !book.deletedAt) {
|
||||
booksToDelete.push(book);
|
||||
}
|
||||
@@ -205,9 +219,14 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
for (const book of getBooksToDelete()) {
|
||||
handleBookDelete(book);
|
||||
const books = getBooksToDelete();
|
||||
const concurrency = 4;
|
||||
|
||||
for (let i = 0; i < books.length; i += concurrency) {
|
||||
const batch = books.slice(i, i + concurrency);
|
||||
await Promise.all(batch.map((book) => handleBookDelete(book, false)));
|
||||
}
|
||||
handlePushLibrary();
|
||||
setSelectedBooks([]);
|
||||
setShowDeleteAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
@@ -231,50 +250,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
|
||||
const bookFilter = (item: Book, queryTerm: string) => {
|
||||
if (item.deletedAt) return false;
|
||||
const searchTerm = new RegExp(queryTerm, 'i');
|
||||
const title = formatTitle(item.title);
|
||||
const authors = formatAuthors(item.author);
|
||||
return (
|
||||
searchTerm.test(title) ||
|
||||
searchTerm.test(authors) ||
|
||||
searchTerm.test(item.format) ||
|
||||
(item.groupName && searchTerm.test(item.groupName)) ||
|
||||
(item.metadata?.description && searchTerm.test(item.metadata?.description))
|
||||
);
|
||||
};
|
||||
const bookSorter = (a: Book, b: Book) => {
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
switch (sortBy) {
|
||||
case 'title':
|
||||
const aTitle = formatTitle(a.title);
|
||||
const bTitle = formatTitle(b.title);
|
||||
return aTitle.localeCompare(bTitle, uiLanguage || navigator.language);
|
||||
case 'author':
|
||||
const aAuthors = formatAuthors(a.author, a?.primaryLanguage || 'en', true);
|
||||
const bAuthors = formatAuthors(b.author, b?.primaryLanguage || 'en', true);
|
||||
return aAuthors.localeCompare(bAuthors, uiLanguage || navigator.language);
|
||||
case 'updated':
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||
case 'created':
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
case 'format':
|
||||
return a.format.localeCompare(b.format, uiLanguage || navigator.language);
|
||||
default:
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||
}
|
||||
};
|
||||
const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1;
|
||||
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
|
||||
const filteredBookshelfItems = currentBookshelfItems
|
||||
.filter((item) => {
|
||||
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
|
||||
else if (queryTerm) return bookFilter(item, queryTerm);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
const sortedBookshelfItems = useMemo(() => {
|
||||
const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1;
|
||||
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
|
||||
return currentBookshelfItems.sort((a, b) => {
|
||||
if (sortBy === 'updated') {
|
||||
return (
|
||||
(new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()) * sortOrderMultiplier
|
||||
@@ -289,14 +268,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}, [sortOrder, sortBy, uiLanguage, navBooksGroup, allBookshelfItems, bookSorter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectMode) {
|
||||
setShowSelectModeActions(true);
|
||||
if (isSelectAll) {
|
||||
setSelectedBooks(
|
||||
filteredBookshelfItems.map((item) => ('hash' in item ? item.hash : item.id)),
|
||||
);
|
||||
setSelectedBooks(filteredBooks.map((item) => item.hash));
|
||||
} else if (isSelectNone) {
|
||||
setSelectedBooks([]);
|
||||
}
|
||||
@@ -305,7 +283,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
setShowSelectModeActions(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSelectMode, isSelectAll, isSelectNone]);
|
||||
}, [isSelectMode, isSelectAll, isSelectNone, filteredBooks]);
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('delete-books', handleDeleteBooksIntent);
|
||||
@@ -330,7 +308,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
role='main'
|
||||
aria-label={_('Bookshelf')}
|
||||
>
|
||||
{filteredBookshelfItems.map((item) => (
|
||||
{sortedBookshelfItems.map((item) => (
|
||||
<BookshelfItem
|
||||
key={`library-item-${'hash' in item ? item.hash : item.id}`}
|
||||
item={item}
|
||||
@@ -353,16 +331,21 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
/>
|
||||
))}
|
||||
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
<button
|
||||
aria-label={_('Import Books')}
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
|
||||
'mx-0 my-4 aspect-[28/41] sm:mx-4',
|
||||
)}
|
||||
onClick={handleImportBooks}
|
||||
>
|
||||
<PiPlus className='size-10' color='gray' />
|
||||
</button>
|
||||
<div className={clsx('mx-0 my-4 sm:mx-4')}>
|
||||
<button
|
||||
aria-label={_('Import Books')}
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50',
|
||||
'flex items-center justify-center',
|
||||
'aspect-[28/41] w-full',
|
||||
)}
|
||||
onClick={handleImportBooks}
|
||||
>
|
||||
<div className='flex items-center justify-center'>
|
||||
<PiPlus className='size-10' color='gray' />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{loading && (
|
||||
|
||||
@@ -77,9 +77,9 @@ interface BookshelfItemProps {
|
||||
transferProgress: number | null;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
toggleSelection: (hash: string) => void;
|
||||
handleBookUpload: (book: Book) => Promise<boolean>;
|
||||
handleBookDownload: (book: Book) => Promise<boolean>;
|
||||
handleBookDelete: (book: Book) => Promise<boolean>;
|
||||
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleSetSelectMode: (selectMode: boolean) => void;
|
||||
handleShowDetailsBook: (book: Book) => void;
|
||||
}
|
||||
@@ -290,7 +290,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
groupContextMenuHandler(item as BooksGroup);
|
||||
}
|
||||
}, 100),
|
||||
[itemSelected],
|
||||
[itemSelected, settings.localBooksDir],
|
||||
);
|
||||
|
||||
const { pressing, handlers } = useLongPress(
|
||||
@@ -342,7 +342,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
onKeyDown={handleKeyDown}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
<div className='flex h-full flex-col justify-end'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
mode={mode}
|
||||
|
||||
@@ -96,12 +96,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'group-item flex h-full flex-col justify-end',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
>
|
||||
<div className={clsx('group-item', appService?.hasContextMenu ? 'cursor-pointer' : '')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex overflow-hidden',
|
||||
|
||||
@@ -20,7 +20,6 @@ const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBook
|
||||
|
||||
return (
|
||||
<Menu
|
||||
label={_('Import Books')}
|
||||
className={clsx(
|
||||
'dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow',
|
||||
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
|
||||
|
||||
@@ -109,7 +109,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar bg-base-200 z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px]',
|
||||
'titlebar z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px]',
|
||||
windowButtonVisible ? 'sm:pr-4' : 'sm:pr-6',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
|
||||
)}
|
||||
@@ -150,7 +150,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
onChange={handleSearchChange}
|
||||
spellCheck='false'
|
||||
className={clsx(
|
||||
'input rounded-badge bg-base-300/50 h-9 w-full pl-10 pr-10 sm:h-7',
|
||||
'input rounded-badge bg-base-300/45 h-9 w-full pl-10 pr-10 sm:h-7',
|
||||
'font-sans text-sm font-light',
|
||||
'border-none focus:outline-none focus:ring-0',
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
RiFolderOpenLine,
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiLoader2Line,
|
||||
} from 'react-icons/ri';
|
||||
import { documentDir, join } from '@tauri-apps/api/path';
|
||||
import { invoke, PermissionState } from '@tauri-apps/api/core';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { DATA_SUBDIR } from '@/services/constants';
|
||||
import { FileItem } from '@/types/system';
|
||||
import { getDirPath } from '@/utils/path';
|
||||
import { formatBytes } from '@/utils/book';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
export const setMigrateDataDirDialogVisible = (visible: boolean) => {
|
||||
const dialog = document.getElementById('migrate_data_dir_window');
|
||||
if (dialog) {
|
||||
const event = new CustomEvent('setDialogVisibility', {
|
||||
detail: { visible },
|
||||
});
|
||||
dialog.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
type MigrationStatus = 'idle' | 'selecting' | 'migrating' | 'completed' | 'error';
|
||||
|
||||
interface MigrationProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
currentFile?: string;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
manageStorage: PermissionState;
|
||||
}
|
||||
|
||||
export const MigrateDataWindow = () => {
|
||||
const _ = useTranslation();
|
||||
const { appService, envConfig } = useEnv();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentDataDir, setCurrentDataDir] = useState('');
|
||||
const [newDataDir, setNewDataDir] = useState('');
|
||||
const [migrationStatus, setMigrationStatus] = useState<MigrationStatus>('idle');
|
||||
const [migrationProgress, setMigrationProgress] = useState<MigrationProgress>({
|
||||
current: 0,
|
||||
total: 0,
|
||||
});
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [filesToMigrate, setFilesToMigrate] = useState<FileItem[]>([]);
|
||||
const [currentDirFileCount, setCurrentDirFileCount] = useState('');
|
||||
const [currentDirFileSize, setCurrentDirFileSize] = useState(0);
|
||||
const [androidNewDirs, setAndroidNewDirs] = useState<{ path: string; label: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleCustomEvent = (event: CustomEvent) => {
|
||||
setIsOpen(event.detail.visible);
|
||||
if (event.detail.visible) {
|
||||
loadCurrentDataDir();
|
||||
loadAndroidDirs();
|
||||
}
|
||||
};
|
||||
|
||||
const el = document.getElementById('migrate_data_dir_window');
|
||||
if (el) {
|
||||
el.addEventListener('setDialogVisibility', handleCustomEvent as EventListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (el) {
|
||||
el.removeEventListener('setDialogVisibility', handleCustomEvent as EventListener);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const loadCurrentDataDir = async () => {
|
||||
try {
|
||||
if (!appService) return;
|
||||
|
||||
const dataDir = await appService.resolveFilePath('', 'Data');
|
||||
setCurrentDataDir(dataDir);
|
||||
const files = await appService.readDirectory(dataDir, 'None');
|
||||
setFilesToMigrate(files);
|
||||
setCurrentDirFileCount(files.length.toLocaleString());
|
||||
setCurrentDirFileSize(files.reduce((acc, file) => acc + file.size, 0));
|
||||
} catch (error) {
|
||||
console.error('Error loading current data directory:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadAndroidDirs = async () => {
|
||||
const sdcardDirs = [
|
||||
{ path: '/storage/emulated/0', label: '/sdcard' },
|
||||
{ path: '/storage/emulated/0/Books', label: '/sdcard/Books' },
|
||||
{ path: '/storage/emulated/0/Documents', label: '/sdcard/Documents' },
|
||||
{ path: '/storage/emulated/0/Download', label: '/sdcard/Download' },
|
||||
];
|
||||
try {
|
||||
if (appService?.isAndroidApp) {
|
||||
const localDocumentDir = await documentDir();
|
||||
setAndroidNewDirs([
|
||||
// For Google Play version we won't request permission to access root of /sdcard
|
||||
...(appService?.distChannel === 'playstore' ? [] : sdcardDirs),
|
||||
{ path: localDocumentDir, label: '/sdcard/APPDATA/Documents' },
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading app local data directory:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectNewDir = async () => {
|
||||
setMigrationStatus('selecting');
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const selectedDir = await appService?.selectDirectory?.('write');
|
||||
if (selectedDir) {
|
||||
const newDataDir = await join(selectedDir, DATA_SUBDIR);
|
||||
await appService?.createDir(newDataDir, 'None', true);
|
||||
setNewDataDir(newDataDir);
|
||||
setMigrationStatus('idle');
|
||||
} else {
|
||||
setMigrationStatus('idle');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting directory:', error);
|
||||
setErrorMessage(_('Failed to select directory'));
|
||||
setMigrationStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectedNewDir = async (dir: string) => {
|
||||
setErrorMessage('');
|
||||
|
||||
if (!dir.includes('Android/data')) {
|
||||
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
|
||||
if (permission.manageStorage !== 'granted') {
|
||||
permission = await invoke<Permissions>(
|
||||
'plugin:native-bridge|request_manage_storage_permission',
|
||||
);
|
||||
}
|
||||
if (permission.manageStorage !== 'granted') return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newDataDir = await join(dir, DATA_SUBDIR);
|
||||
await appService?.createDir(newDataDir, 'None', true);
|
||||
setNewDataDir(newDataDir);
|
||||
setMigrationStatus('idle');
|
||||
} catch (error) {
|
||||
console.error('Error selecting directory:', error);
|
||||
setErrorMessage(_('Failed to select directory'));
|
||||
setMigrationStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartMigration = async () => {
|
||||
if (!appService || !currentDataDir || !newDataDir || !filesToMigrate.length) return;
|
||||
|
||||
setMigrationStatus('migrating');
|
||||
setErrorMessage('');
|
||||
setMigrationProgress({ current: 0, total: 0 });
|
||||
|
||||
try {
|
||||
if (newDataDir === currentDataDir) {
|
||||
throw new Error(_('The new data directory must be different from the current one.'));
|
||||
}
|
||||
|
||||
// Copy all files to new location
|
||||
for (let i = 0; i < filesToMigrate.length; i++) {
|
||||
const file = filesToMigrate[i]!;
|
||||
setMigrationProgress({
|
||||
current: i + 1,
|
||||
total: filesToMigrate.length,
|
||||
currentFile: file.path,
|
||||
});
|
||||
|
||||
const srcPath = await join(currentDataDir, file.path);
|
||||
const destPath = await join(newDataDir, file.path);
|
||||
await appService.copyFile(srcPath, destPath, 'None');
|
||||
}
|
||||
|
||||
// Verify all files copied
|
||||
const filesMigrated = await appService.readDirectory(newDataDir, 'None');
|
||||
for (const file of filesToMigrate) {
|
||||
if (!filesMigrated.find((f) => f.path === file.path && f.size === file.size)) {
|
||||
throw new Error(`File ${file.path} failed to copy.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old data directory
|
||||
await appService.deleteDir(currentDataDir, 'None', true);
|
||||
|
||||
// Update settings for new data directory
|
||||
const customRootDir = getDirPath(newDataDir);
|
||||
await appService.setCustomRootDir(customRootDir);
|
||||
settings.customRootDir = customRootDir;
|
||||
settings.localBooksDir = await appService.resolveFilePath('', 'Books');
|
||||
setSettings({ ...settings });
|
||||
await saveSettings(envConfig, settings);
|
||||
|
||||
// Finalize migration
|
||||
setMigrationStatus('completed');
|
||||
setCurrentDataDir(newDataDir);
|
||||
setFilesToMigrate([]);
|
||||
setCurrentDirFileCount('');
|
||||
setCurrentDirFileSize(0);
|
||||
loadCurrentDataDir();
|
||||
} catch (error) {
|
||||
console.error('Error migrating data:', error);
|
||||
setErrorMessage(_('Migration failed: {{error}}', { error: error || 'Unknown error' }));
|
||||
setMigrationStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (migrationStatus === 'migrating') {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
setNewDataDir('');
|
||||
setMigrationStatus('idle');
|
||||
setErrorMessage('');
|
||||
setMigrationProgress({ current: 0, total: 0 });
|
||||
};
|
||||
|
||||
const handleRestartApp = () => {
|
||||
relaunch();
|
||||
};
|
||||
|
||||
const handleRevealDir = (dataDir: string) => {
|
||||
if (dataDir && appService?.isDesktopApp) {
|
||||
revealItemInDir(dataDir);
|
||||
}
|
||||
};
|
||||
|
||||
const progressPercentage =
|
||||
migrationProgress.total > 0
|
||||
? Math.round((migrationProgress.current / migrationProgress.total) * 100)
|
||||
: 0;
|
||||
|
||||
const canStartMigration =
|
||||
newDataDir && newDataDir !== currentDataDir && migrationStatus === 'idle';
|
||||
|
||||
const osPlatform = getOSPlatform();
|
||||
const fileRevealLabel =
|
||||
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id='migrate_data_dir_window'
|
||||
isOpen={isOpen}
|
||||
title={_('Change Data Location')}
|
||||
onClose={handleClose}
|
||||
boxClassName='sm:!w-[520px] sm:!max-w-screen-sm sm:h-auto'
|
||||
>
|
||||
{isOpen && (
|
||||
<div className='migrate-data-dir-content flex flex-col gap-6 px-6 py-4'>
|
||||
{/* Current Data Directory */}
|
||||
<div className='space-y-2'>
|
||||
<h3 className='text-base-content text-sm font-semibold'>
|
||||
{_('Current Data Location')}
|
||||
</h3>
|
||||
<button
|
||||
title={_(fileRevealLabel)}
|
||||
className='bg-base-200 flex w-full items-center gap-2 rounded-lg p-3'
|
||||
onClick={() => handleRevealDir(currentDataDir)}
|
||||
>
|
||||
<RiFolderOpenLine className='text-base-content/70 h-4 w-4 flex-shrink-0' />
|
||||
<span className='text-base-content/80 break-all text-start font-mono text-sm'>
|
||||
{currentDataDir || _('Loading...')}
|
||||
</span>
|
||||
</button>
|
||||
{currentDirFileCount ? (
|
||||
<div className='flex space-x-4'>
|
||||
<p className='text-base-content/60 text-xs'>
|
||||
{_('File count: {{size}}', { size: currentDirFileCount })}
|
||||
</p>
|
||||
<p className='text-base-content/60 text-xs'>
|
||||
{_('Total size: {{size}}', { size: formatBytes(currentDirFileSize) })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-base-content/60 text-xs'>{_('Calculating file info...')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New Data Directory Selection */}
|
||||
<div className='space-y-3'>
|
||||
<h3 className='text-base-content text-sm font-semibold'>{_('New Data Location')}</h3>
|
||||
|
||||
{newDataDir && (
|
||||
<button
|
||||
title={_(fileRevealLabel)}
|
||||
className='bg-primary/10 border-primary/20 flex w-full items-center gap-2 rounded-lg border p-3'
|
||||
onClick={() => handleRevealDir(newDataDir)}
|
||||
>
|
||||
<RiFolderOpenLine className='text-primary h-4 w-4 flex-shrink-0' />
|
||||
<span className='text-primary break-all text-start font-mono text-sm'>
|
||||
{newDataDir}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{appService?.isAndroidApp ? (
|
||||
<Dropdown
|
||||
label={_('Choose New Folder')}
|
||||
className='dropdown-bottom flex w-full justify-center'
|
||||
buttonClassName='btn btn-ghost btn-outline w-full'
|
||||
toggleButton={
|
||||
<div>{newDataDir ? _('Choose Different Folder') : _('Choose New Folder')}</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'folder-menu dropdown-content dropdown-center no-triangle',
|
||||
'border-base-300 !bg-base-200 z-20 mt-1 max-w-[90vw] shadow-2xl',
|
||||
)}
|
||||
>
|
||||
{androidNewDirs.map((dir) => (
|
||||
<MenuItem
|
||||
key={dir.path}
|
||||
toggled={newDataDir.split(`/${DATA_SUBDIR}`)[0] === dir.path}
|
||||
transient
|
||||
label={dir.label}
|
||||
onClick={() => handleSelectedNewDir(dir.path)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<button
|
||||
className='btn btn-outline btn-sm w-full'
|
||||
onClick={handleSelectNewDir}
|
||||
disabled={migrationStatus === 'migrating' || migrationStatus === 'selecting'}
|
||||
>
|
||||
{migrationStatus === 'selecting' && (
|
||||
<RiLoader2Line className='h-4 w-4 animate-spin' />
|
||||
)}
|
||||
{newDataDir ? _('Choose Different Folder') : _('Choose New Folder')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Migration Progress */}
|
||||
{migrationStatus === 'migrating' && (
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<RiLoader2Line className='text-primary h-4 w-4 animate-spin' />
|
||||
<span className='text-base-content text-sm font-medium'>
|
||||
{_('Migrating data...')}
|
||||
</span>
|
||||
<span className='text-base-content/70 text-sm'>{progressPercentage}%</span>
|
||||
</div>
|
||||
|
||||
<div className='bg-base-200 h-2 w-full rounded-full'>
|
||||
<div
|
||||
className='bg-primary h-2 rounded-full transition-all duration-300'
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{migrationProgress.currentFile && (
|
||||
<p
|
||||
className='text-base-content/60 overflow-hidden font-mono text-xs'
|
||||
style={{
|
||||
direction: 'rtl',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{_('Copying: {{file}}', { file: migrationProgress.currentFile })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className='text-base-content/60 text-xs'>
|
||||
{_('{{current}} of {{total}} files', {
|
||||
current: migrationProgress.current.toLocaleString(),
|
||||
total: migrationProgress.total.toLocaleString(),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success State */}
|
||||
{migrationStatus === 'completed' && (
|
||||
<div className='space-y-3'>
|
||||
<div className='text-success flex items-center gap-2'>
|
||||
<RiCheckboxCircleFill className='h-5 w-5' />
|
||||
<span className='font-medium'>{_('Migration completed successfully!')}</span>
|
||||
</div>
|
||||
<div className='bg-success/10 border-success/20 rounded-lg border p-3'>
|
||||
<p className='text-success/80 text-sm'>
|
||||
{_(
|
||||
'Your data has been moved to the new location. Please restart the application to complete the process.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{migrationStatus === 'error' && errorMessage && (
|
||||
<div className='space-y-2'>
|
||||
<div className='text-error flex items-center gap-2'>
|
||||
<RiErrorWarningFill className='h-5 w-5' />
|
||||
<span className='font-medium'>{_('Migration failed')}</span>
|
||||
</div>
|
||||
<div className='bg-error/10 border-error/20 rounded-lg border p-3'>
|
||||
<p className='text-error/80 break-all text-sm'>{errorMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning */}
|
||||
{canStartMigration && (
|
||||
<div className='bg-warning/10 border-warning/20 rounded-lg border p-3'>
|
||||
<div className='flex items-start gap-2'>
|
||||
<RiErrorWarningFill className='text-warning mt-0.5 h-4 w-4 flex-shrink-0' />
|
||||
<div className='space-y-1'>
|
||||
<p className='text-base-content text-sm font-medium'>{_('Important Notice')}</p>
|
||||
<p className='text-base-content/80 text-sm'>
|
||||
{_(
|
||||
'This will move all your app data to the new location. Make sure the destination has enough free space.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className='flex gap-3 pt-2'>
|
||||
{migrationStatus === 'completed' ? (
|
||||
<>
|
||||
<button className='btn btn-outline flex-1' onClick={handleClose}>
|
||||
{_('Close')}
|
||||
</button>
|
||||
<button className='btn btn-primary flex-1' onClick={handleRestartApp}>
|
||||
{_('Restart App')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className='btn btn-outline flex-1'
|
||||
onClick={handleClose}
|
||||
disabled={migrationStatus === 'migrating'}
|
||||
>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-primary flex-1'
|
||||
onClick={handleStartMigration}
|
||||
disabled={!canStartMigration || migrationStatus !== 'idle'}
|
||||
>
|
||||
{migrationStatus === 'migrating' && (
|
||||
<RiLoader2Line className='h-4 w-4 animate-spin' />
|
||||
)}
|
||||
{_('Start Migration')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PiUserCircle } from 'react-icons/pi';
|
||||
import { PiUserCircleCheck } from 'react-icons/pi';
|
||||
import { PiUserCircle, PiUserCircleCheck, PiGear } from 'react-icons/pi';
|
||||
import { PiSun, PiMoon } from 'react-icons/pi';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { invoke, PermissionState } from '@tauri-apps/api/core';
|
||||
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
@@ -19,6 +18,8 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
|
||||
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { setMigrateDataDirDialogVisible } from '@/app/library/components/MigrateDataWindow';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Quota from '@/components/Quota';
|
||||
@@ -28,6 +29,10 @@ interface SettingsMenuProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
postNotification: PermissionState;
|
||||
}
|
||||
|
||||
const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -36,6 +41,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const { userPlan, quotas } = useQuotaStats(true);
|
||||
const { themeMode, setThemeMode } = useThemeStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
|
||||
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
|
||||
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
|
||||
@@ -46,6 +52,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
settings.autoImportBooksOnOpen,
|
||||
);
|
||||
const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled);
|
||||
const [alwaysInForeground, setAlwaysInForeground] = useState(settings.alwaysInForeground);
|
||||
const iconSize = useResponsiveSize(16);
|
||||
|
||||
const showAboutReadest = () => {
|
||||
@@ -162,6 +169,35 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const handleSetRootDir = () => {
|
||||
setMigrateDataDirDialogVisible(true);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const openSettingsDialog = () => {
|
||||
setIsDropdownOpen?.(false);
|
||||
setFontLayoutSettingsDialogOpen(true);
|
||||
};
|
||||
|
||||
const toggleAlwaysInForeground = async () => {
|
||||
const requestAlwaysInForeground = !settings.alwaysInForeground;
|
||||
|
||||
if (requestAlwaysInForeground) {
|
||||
let permission = await invoke<Permissions>('plugin:native-tts|checkPermissions');
|
||||
if (permission.postNotification !== 'granted') {
|
||||
permission = await invoke<Permissions>('plugin:native-tts|requestPermissions', {
|
||||
permissions: ['postNotification'],
|
||||
});
|
||||
}
|
||||
if (permission.postNotification !== 'granted') return;
|
||||
}
|
||||
|
||||
settings.alwaysInForeground = requestAlwaysInForeground;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setAlwaysInForeground(settings.alwaysInForeground);
|
||||
};
|
||||
|
||||
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
|
||||
const userFullName = user?.user_metadata?.['full_name'];
|
||||
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
|
||||
@@ -174,7 +210,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
|
||||
return (
|
||||
<Menu
|
||||
label={_('Settings Menu')}
|
||||
className={clsx(
|
||||
'settings-menu dropdown-content no-triangle border-base-100',
|
||||
'z-20 mt-2 max-w-[90vw] shadow-2xl',
|
||||
@@ -257,12 +292,30 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
toggled={isScreenWakeLock}
|
||||
onClick={toggleScreenWakeLock}
|
||||
/>
|
||||
{appService?.isAndroidApp && (
|
||||
<MenuItem
|
||||
label={_(_('Background Read Aloud'))}
|
||||
toggled={alwaysInForeground}
|
||||
onClick={toggleAlwaysInForeground}
|
||||
/>
|
||||
)}
|
||||
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
|
||||
<MenuItem
|
||||
label={themeModeLabel}
|
||||
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
|
||||
Icon={themeMode === 'dark' ? PiMoon : themeMode === 'light' ? PiSun : TbSunMoon}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<MenuItem label={_('Settings')} Icon={PiGear} onClick={openSettingsDialog} />
|
||||
{appService?.canCustomizeRootDir && (
|
||||
<>
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
<MenuItem label={_('Advanced Settings')}>
|
||||
<ul className='flex flex-col'>
|
||||
<MenuItem label={_('Change Data Location')} noIcon onClick={handleSetRootDir} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
{user && userPlan === 'free' && !appService?.isIOSApp && (
|
||||
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
|
||||
|
||||
@@ -92,10 +92,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
label={_('View Menu')}
|
||||
className='view-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
|
||||
>
|
||||
<Menu className='view-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'>
|
||||
{viewOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
|
||||
@@ -38,21 +38,24 @@ import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
|
||||
import { SelectedFile, useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import {
|
||||
tauriHandleClose,
|
||||
tauriHandleSetAlwaysOnTop,
|
||||
tauriHandleToggleFullScreen,
|
||||
tauriQuitApp,
|
||||
} from '@/utils/window';
|
||||
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { BookDetailModal } from '@/components/metadata';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
import { MigrateDataWindow } from './components/MigrateDataWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from './components/LibraryHeader';
|
||||
import Bookshelf from './components/Bookshelf';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import DropIndicator from '@/components/DropIndicator';
|
||||
import SettingsDialog from '@/components/settings/SettingsDialog';
|
||||
|
||||
const LibraryPageWithSearchParams = () => {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -76,6 +79,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
const { selectFiles } = useFileSelector(appService, _);
|
||||
const { safeAreaInsets: insets, isRoundedWindow } = useThemeStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const { isFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
@@ -88,6 +93,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
}>({});
|
||||
const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState<string[] | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const demoBooks = useDemoBooks();
|
||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||
const containerRef: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
@@ -112,11 +118,22 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
await tauriHandleToggleFullScreen();
|
||||
}
|
||||
},
|
||||
onCloseWindow: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriHandleClose();
|
||||
}
|
||||
},
|
||||
onQuitApp: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriQuitApp();
|
||||
}
|
||||
},
|
||||
onOpenFontLayoutSettings: () => {
|
||||
setFontLayoutSettingsDialogOpen(true);
|
||||
},
|
||||
onOpenBooks: () => {
|
||||
handleImportBooks();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -402,41 +419,56 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
|
||||
const importBooks = async (files: SelectedFile[]) => {
|
||||
setLoading(true);
|
||||
const failedFiles = [];
|
||||
const { library } = useLibraryStore.getState();
|
||||
const failedImports: Array<{ filename: string; errorMessage: string }> = [];
|
||||
const errorMap: [string, string][] = [
|
||||
['No chapters detected.', _('No chapters detected.')],
|
||||
['Failed to parse EPUB.', _('Failed to parse the EPUB file.')],
|
||||
['Unsupported format.', _('This book format is not supported.')],
|
||||
];
|
||||
const { library } = useLibraryStore.getState();
|
||||
for (const selectedFile of files) {
|
||||
|
||||
const processFile = async (selectedFile: SelectedFile) => {
|
||||
const file = selectedFile.file || selectedFile.path;
|
||||
if (!file) continue;
|
||||
if (!file) return;
|
||||
try {
|
||||
const book = await appService?.importBook(file, library);
|
||||
setLibrary([...library]);
|
||||
if (user && book && !book.uploadedAt && settings.autoUpload) {
|
||||
console.log('Uploading book:', book.title);
|
||||
handleBookUpload(book);
|
||||
handleBookUpload(book, false);
|
||||
}
|
||||
} catch (error) {
|
||||
const filename = typeof file === 'string' ? file : file.name;
|
||||
const baseFilename = getFilename(filename);
|
||||
failedFiles.push(baseFilename);
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? errorMap.find(([substring]) => error.message.includes(substring))?.[1] || ''
|
||||
: '';
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message:
|
||||
_('Failed to import book(s): {{filenames}}', {
|
||||
filenames: listFormater(false).format(failedFiles),
|
||||
}) + (errorMessage ? `\n${errorMessage}` : ''),
|
||||
type: 'error',
|
||||
});
|
||||
failedImports.push({ filename: baseFilename, errorMessage });
|
||||
console.error('Failed to import book:', filename, error);
|
||||
}
|
||||
};
|
||||
|
||||
const concurrency = 4;
|
||||
for (let i = 0; i < files.length; i += concurrency) {
|
||||
const batch = files.slice(i, i + concurrency);
|
||||
await Promise.all(batch.map(processFile));
|
||||
}
|
||||
pushLibrary();
|
||||
|
||||
if (failedImports.length > 0) {
|
||||
const filenames = failedImports.map((f) => f.filename);
|
||||
const errorMessage = failedImports.find((f) => f.errorMessage)?.errorMessage || '';
|
||||
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message:
|
||||
_('Failed to import book(s): {{filenames}}', {
|
||||
filenames: listFormater(false).format(filenames),
|
||||
}) + (errorMessage ? `\n${errorMessage}` : ''),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
setLibrary([...library]);
|
||||
appService?.saveLibraryBooks(library);
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -451,13 +483,18 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
}, 500);
|
||||
|
||||
const handleBookUpload = useCallback(
|
||||
async (book: Book) => {
|
||||
async (book: Book, syncBooks = true) => {
|
||||
try {
|
||||
await appService?.uploadBook(book, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
setBooksTransferProgress((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[book.hash];
|
||||
return updated;
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
pushLibrary();
|
||||
if (syncBooks) pushLibrary();
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
timeout: 2000,
|
||||
@@ -467,6 +504,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
setBooksTransferProgress((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[book.hash];
|
||||
return updated;
|
||||
});
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('Not authenticated') && settings.keepLogin) {
|
||||
settings.keepLogin = false;
|
||||
@@ -524,7 +566,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
);
|
||||
|
||||
const handleBookDelete = (deleteAction: DeleteAction) => {
|
||||
return async (book: Book) => {
|
||||
return async (book: Book, syncBooks = true) => {
|
||||
const deletionMessages = {
|
||||
both: _('Book deleted: {{title}}', { title: book.title }),
|
||||
cloud: _('Deleted cloud backup of the book: {{title}}', { title: book.title }),
|
||||
@@ -538,7 +580,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
try {
|
||||
await appService?.deleteBook(book, deleteAction);
|
||||
await updateBook(envConfig, book);
|
||||
pushLibrary();
|
||||
if (syncBooks) pushLibrary();
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
timeout: 2000,
|
||||
@@ -629,8 +671,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
ref={pageRef}
|
||||
aria-label='Your Library'
|
||||
className={clsx(
|
||||
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
'library-page bg-base-200 text-base-content flex h-[100vh] select-none flex-col overflow-hidden',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
@@ -693,6 +734,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
handleSetSelectMode={handleSetSelectMode}
|
||||
handleShowDetailsBook={handleShowDetailsBook}
|
||||
booksTransferProgress={booksTransferProgress}
|
||||
handlePushLibrary={pushLibrary}
|
||||
/>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
@@ -729,6 +771,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
)}
|
||||
<AboutWindow />
|
||||
<UpdaterWindow />
|
||||
<MigrateDataWindow />
|
||||
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={''} />}
|
||||
<Toast />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Book } from '@/types/book';
|
||||
import { formatAuthors, formatTitle } from '@/utils/book';
|
||||
|
||||
export const createBookFilter = (queryTerm: string | null) => (item: Book) => {
|
||||
if (!queryTerm) return true;
|
||||
if (item.deletedAt) return false;
|
||||
const searchTerm = new RegExp(queryTerm, 'i');
|
||||
const title = formatTitle(item.title);
|
||||
const authors = formatAuthors(item.author);
|
||||
return (
|
||||
searchTerm.test(title) ||
|
||||
searchTerm.test(authors) ||
|
||||
searchTerm.test(item.format) ||
|
||||
(item.groupName && searchTerm.test(item.groupName)) ||
|
||||
(item.metadata?.description && searchTerm.test(item.metadata?.description))
|
||||
);
|
||||
};
|
||||
|
||||
export const createBookSorter = (sortBy: string, uiLanguage: string) => (a: Book, b: Book) => {
|
||||
switch (sortBy) {
|
||||
case 'title':
|
||||
const aTitle = formatTitle(a.title);
|
||||
const bTitle = formatTitle(b.title);
|
||||
return aTitle.localeCompare(bTitle, uiLanguage || navigator.language);
|
||||
case 'author':
|
||||
const aAuthors = formatAuthors(a.author, a?.primaryLanguage || 'en', true);
|
||||
const bAuthors = formatAuthors(b.author, b?.primaryLanguage || 'en', true);
|
||||
return aAuthors.localeCompare(bAuthors, uiLanguage || navigator.language);
|
||||
case 'updated':
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||
case 'created':
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
case 'format':
|
||||
return a.format.localeCompare(b.format, uiLanguage || navigator.language);
|
||||
default:
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||
}
|
||||
};
|
||||