forked from akai/readest
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1bc0eb3a4b | |||
| c0bf2d40dd | |||
| e30a39f9cb | |||
| 9727e4e9b8 | |||
| 3380912cdb | |||
| 91a5454b46 | |||
| e0c5acf4c8 | |||
| 7e062be0d0 | |||
| b23876c2b9 | |||
| e9a152683e | |||
| 34f1af727f | |||
| dfc24f3803 | |||
| ec233f7b37 | |||
| bb34b1ba59 | |||
| b1fb477359 | |||
| 3c877a19fc | |||
| b47baa6b74 | |||
| 14c85cbadf | |||
| a88b9139f8 | |||
| 251a4b52c0 | |||
| 1aaeaf5ebe | |||
| 9fd152d727 | |||
| 2571ceede4 | |||
| 932cfa3b9f | |||
| 01fde8573c | |||
| d95656b37b | |||
| 210eda0340 | |||
| 0d8b877d73 | |||
| 79d39b2295 | |||
| e03b0af58a | |||
| 71c2143cba | |||
| 70a7f5be19 | |||
| d2242423f5 | |||
| 782e9c8b20 | |||
| 55b2c678a4 | |||
| 48d754c184 | |||
| ff0443ace7 |
@@ -19,6 +19,7 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -48,7 +49,7 @@ jobs:
|
||||
version: 10.14.0
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -72,6 +73,11 @@ jobs:
|
||||
run: |
|
||||
pnpm test -- --watch=false
|
||||
|
||||
- name: run lint
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm lint
|
||||
|
||||
- name: build the web App
|
||||
if: matrix.config.platform == 'web'
|
||||
working-directory: apps/readest-app
|
||||
|
||||
@@ -19,12 +19,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
- name: get version
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./apps/readest-app/package.json').version")" >> $GITHUB_ENV
|
||||
- name: get release
|
||||
id: get-release
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.getLatestRelease({
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
core.setOutput('release_tag', data.tag_name);
|
||||
- name: get release notes
|
||||
id: get-release-notes
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
steps:
|
||||
- name: update release
|
||||
id: update-release
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
release_id: ${{ needs.get-release.outputs.release_id }}
|
||||
release_tag: ${{ needs.get-release.outputs.release_tag }}
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
version: 10.14.0
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -153,7 +153,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 +200,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 +288,18 @@ 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 }}
|
||||
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 }}
|
||||
|
||||
Generated
+572
-554
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
@@ -45,23 +45,23 @@
|
||||
|
||||
<div align="left">✅ Implemented</div>
|
||||
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
|
||||
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ |
|
||||
| **Translate with Yandex** | Instantly translate text or books using Yandex Translate. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
|
||||
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ |
|
||||
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
|
||||
## Planned Features
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
|
||||
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import path from 'node:path';
|
||||
import js from '@eslint/js';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
export default [...compat.extends("next/core-web-vitals", "next/typescript")];
|
||||
|
||||
export default [
|
||||
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||
{
|
||||
plugins: {
|
||||
'jsx-a11y': jsxA11y,
|
||||
},
|
||||
rules: {
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.78",
|
||||
"version": "0.9.82",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -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,11 +120,13 @@
|
||||
"@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",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
"Search...": "بحث...",
|
||||
"Select Book": "تحديد الكتاب",
|
||||
"Select Books": "تحديد الكتب",
|
||||
"Select Multiple Books": "تحديد عدة كتب",
|
||||
"Sepia": "بني داكن",
|
||||
"Serif Font": "خط مذيل (Serif)",
|
||||
"Show Book Details": "عرض تفاصيل الكتاب",
|
||||
@@ -117,6 +116,7 @@
|
||||
"Book Details": "تفاصيل الكتاب",
|
||||
"From Local File": "من ملف محلي",
|
||||
"TOC": "جدول المحتويات",
|
||||
"Table of Contents": "جدول المحتويات",
|
||||
"Book uploaded: {{title}}": "تم تحميل الكتاب: {{title}}",
|
||||
"Failed to upload book: {{title}}": "فشل تحميل الكتاب: {{title}}",
|
||||
"Book downloaded: {{title}}": "تم تنزيل الكتاب: {{title}}",
|
||||
@@ -344,7 +344,6 @@
|
||||
"Left Margin (px)": "الهامش الأيسر (بكسل)",
|
||||
"Column Gap (%)": "تباعد الأعمدة (%)",
|
||||
"Always Show Status Bar": "دائمًا إظهار شريط الحالة",
|
||||
"Translation Not Available": "الترجمة غير متاحة",
|
||||
"Custom Content CSS": "CSS مخصص للمحتوى",
|
||||
"Enter CSS for book content styling...": "أدخل CSS لتنسيق محتوى الكتاب...",
|
||||
"Custom Reader UI CSS": "CSS مخصص لواجهة القارئ",
|
||||
@@ -538,5 +537,83 @@
|
||||
"Play": "تشغيل",
|
||||
"Next Sentence": "الجملة التالية",
|
||||
"Next Paragraph": "الفقرة التالية",
|
||||
"Separate Cover Page": "صفحة غلاف منفصلة"
|
||||
"Separate Cover Page": "صفحة غلاف منفصلة",
|
||||
"Resize Notebook": "تغيير حجم المفكرة",
|
||||
"Resize Sidebar": "تغيير حجم الشريط الجانبي",
|
||||
"Get Help from the Readest Community": "الحصول على المساعدة من مجتمع ريديست",
|
||||
"Remove cover image": "إزالة صورة الغلاف",
|
||||
"Bookshelf": "رف الكتب",
|
||||
"View Menu": "عرض القائمة",
|
||||
"Settings Menu": "إعدادات القائمة",
|
||||
"View account details and quota": "عرض تفاصيل الحساب والحصة المخصصة",
|
||||
"Library Header": "رأس المكتبة",
|
||||
"Book Content": "محتوى الكتاب",
|
||||
"Footer Bar": "شريط التذييل",
|
||||
"Header Bar": "شريط الرأس",
|
||||
"View Options": "خيارات العرض",
|
||||
"Book Menu": "قائمة الكتاب",
|
||||
"Search Options": "خيارات البحث",
|
||||
"Close": "إغلاق",
|
||||
"Delete Book Options": "خيارات حذف الكتاب",
|
||||
"ON": "تشغيل",
|
||||
"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": "تكبير أو استعادة",
|
||||
"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": "القراءة بصوت عالٍ"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"Search Books...": "বই খুঁজুন...",
|
||||
"Clear Search": "খোঁজ পরিষ্কার করুন",
|
||||
"Import Books": "বই আমদানি করুন",
|
||||
"Select Multiple Books": "একাধিক বই নির্বাচন করুন",
|
||||
"Select Books": "বই নির্বাচন করুন",
|
||||
"Deselect": "অনির্বাচিত করুন",
|
||||
"Select All": "সব নির্বাচন করুন",
|
||||
@@ -324,9 +323,9 @@
|
||||
"Match Whole Words": "পূর্ণ শব্দ মিলান",
|
||||
"Match Diacritics": "ডায়াক্রিটিক্স মিলান",
|
||||
"TOC": "সূচিপত্র",
|
||||
"Table of Contents": "সূচিপত্র",
|
||||
"Sidebar": "সাইডবার",
|
||||
"Disable Translation": "অনুবাদ নিষ্ক্রিয়",
|
||||
"Translation Not Available": "অনুবাদ উপলব্ধ নয়",
|
||||
"TTS not supported for PDF": "PDF এর জন্য TTS সমর্থিত নয়",
|
||||
"TTS not supported for this document": "এই ডকুমেন্টের জন্য TTS সমর্থিত নয়",
|
||||
"No Timeout": "কোন টাইমআউট নেই",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "চালান",
|
||||
"Next Sentence": "পরবর্তী বাক্য",
|
||||
"Next Paragraph": "পরবর্তী অনুচ্ছেদ",
|
||||
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা"
|
||||
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা",
|
||||
"Resize Notebook": "নোটবুকের আকার পরিবর্তন",
|
||||
"Resize Sidebar": "সাইডবারের আকার পরিবর্তন",
|
||||
"Get Help from the Readest Community": "রিডেস্ট কমিউনিটি থেকে সাহায্য নিন",
|
||||
"Remove cover image": "মলাট ছবি সরান",
|
||||
"Bookshelf": "বইয়ের তাক",
|
||||
"View Menu": "মেনু দেখুন",
|
||||
"Settings Menu": "সেটিংস মেনু",
|
||||
"View account details and quota": "অ্যাকাউন্টের বিবরণ এবং কোটার তথ্য দেখুন",
|
||||
"Library Header": "লাইব্রেরি হেডার",
|
||||
"Book Content": "বইয়ের বিষয়বস্তু",
|
||||
"Footer Bar": "ফুটার বার",
|
||||
"Header Bar": "হেডার বার",
|
||||
"View Options": "দেখার অপশন",
|
||||
"Book Menu": "বইয়ের মেনু",
|
||||
"Search Options": "অনুসন্ধানের অপশন",
|
||||
"Close": "বন্ধ করুন",
|
||||
"Delete Book Options": "বই মুছে ফেলার অপশন",
|
||||
"ON": "চালু",
|
||||
"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": "বৃহৎ করুন বা পুনরুদ্ধার করুন",
|
||||
"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": "উচ্চস্বরে পড়ুন"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "འཚོལ་བཤེར།...",
|
||||
"Select Book": "དཔེ་དེབ་འདེམས་པ།",
|
||||
"Select Books": "དཔེ་དེབ་འདེམས་པ།",
|
||||
"Select Multiple Books": "དཔེ་དེབ་མང་པོ་འདེམས་པ།",
|
||||
"Sepia": "རྙིང་པའི་ཉམས་འགྱུར།",
|
||||
"Serif Font": "མཐའ་ཐིག་ཡོད་པའི་ཡིག་གཟུགས།",
|
||||
"Show Book Details": "དཔེ་དེབ་ཀྱི་གནས་ཚུལ་རྒྱས་པ་མངོན་པ།",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "དཔེ་དེབ་ཀྱི་གནས་ཚུལ་རྒྱས་པ།",
|
||||
"From Local File": "ས་གནས་ཀྱི་ཡིག་ཆ་ནས་ནང་འདྲེན།",
|
||||
"TOC": "དཀར་ཆག",
|
||||
"Table of Contents": "དཀར་ཆག",
|
||||
"Book uploaded: {{title}}": "དཔེ་དེབ་སྤྲད་ཟིན། {{title}}",
|
||||
"Failed to upload book: {{title}}": "དཔེ་དེབ་སྤྲད་མ་ཐུབ། {{title}}",
|
||||
"Book downloaded: {{title}}": "དཔེ་དེབ་ཕབ་ལེན་བྱས་ཟིན། {{title}}",
|
||||
@@ -329,7 +329,6 @@
|
||||
"Left Margin (px)": "གཡོན་ངོས་ཀྱི་བར་ཐག",
|
||||
"Column Gap (%)": "གྲལ་ཐིག་བར་གྱི་བར་ཐག",
|
||||
"Always Show Status Bar": "རྟག་ཏུ་གནས་ཚུལ་གྱི་གྲལ་ཐིག་མངོན་པ།",
|
||||
"Translation Not Available": "སྒྱུར་བཅོས་སྤྱོད་ཐུབ་མེད།",
|
||||
"Custom Content CSS": "རང་འགུལ་གྱི་ནང་དོན་ CSS",
|
||||
"Enter CSS for book content styling...": "དཔེ་དེབ་ཀྱི་ནང་དོན་གྱི་རྣམ་པའི་ CSS ནང་འཇུག་བྱོས།...",
|
||||
"Custom Reader UI CSS": "རང་འགུལ་གྱི་མཐུད་ངོས་ CSS",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "གཏོང་བ།",
|
||||
"Next Sentence": "རྗེས་ཀྱི་ཚིག་གྲུབ།",
|
||||
"Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།",
|
||||
"Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།"
|
||||
"Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།",
|
||||
"Resize Notebook": "དེབ་སྣོད་གཅོད་སྒྲིག",
|
||||
"Resize Sidebar": "གཡས་སྒོ་གཅོད་སྒྲིག",
|
||||
"Get Help from the Readest Community": "Readest མཛའ་བརྩེའི་རོགས་རམ་ལོངས་སུ་རོགས་རམ་ཐོབ།",
|
||||
"Remove cover image": "སྟོང་ངོས་ཀྱི་པར་རིས་ཕྱིར་འཐེན།",
|
||||
"Bookshelf": "དེབ་སྡེབས་",
|
||||
"View Menu": "མ་ལག་བལྟ་བ",
|
||||
"Settings Menu": "སྒྲིག་སྟངས་མ་ལག",
|
||||
"View account details and quota": "རྩིས་ཐོའི་རྒྱུ་ཆ་དང་ཚད་བརྡ་བལྟ་བ",
|
||||
"Library Header": "དེབ་མཛོད་མགོ་ཡིག",
|
||||
"Book Content": "དེབ་ནང་དོན",
|
||||
"Footer Bar": "མཇུག་སྒམ་ཕྱོགས",
|
||||
"Header Bar": "མགོ་སྒམ་ཕྱོགས",
|
||||
"View Options": "བལྟ་བའི་གདམ་ཁ",
|
||||
"Book Menu": "དེབ་ཀྱི་མ་ལག",
|
||||
"Search Options": "འཚོལ་བའི་གདམ་ཁ",
|
||||
"Close": "ཁ་རྒྱག",
|
||||
"Delete Book Options": "དེབ་བཏོན་གཏང་གི་གདམ་ཁ",
|
||||
"ON": "འབྱུང་ཁུངས",
|
||||
"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": "ཤོག་ངོས་ {{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": "ཀློག་བྱེད།"
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
"Search...": "Suchen...",
|
||||
"Select Book": "Buch auswählen",
|
||||
"Select Books": "Bücher auswählen",
|
||||
"Select Multiple Books": "Mehrere Bücher auswählen",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Serifenschrift",
|
||||
"Show Book Details": "Buchdetails anzeigen",
|
||||
@@ -117,6 +116,7 @@
|
||||
"Book Details": "Buchdetails",
|
||||
"From Local File": "Aus lokaler Datei",
|
||||
"TOC": "Inhaltsverzeichnis",
|
||||
"Table of Contents": "Inhaltsverzeichnis",
|
||||
"Book uploaded: {{title}}": "Buch hochgeladen: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Fehler beim Hochladen des Buches: {{title}}",
|
||||
"Book downloaded: {{title}}": "Buch heruntergeladen: {{title}}",
|
||||
@@ -332,7 +332,6 @@
|
||||
"Left Margin (px)": "Linker Rand (px)",
|
||||
"Column Gap (%)": "Spaltenabstand (%)",
|
||||
"Always Show Status Bar": "Statusleiste immer anzeigen",
|
||||
"Translation Not Available": "Übersetzung nicht verfügbar",
|
||||
"Custom Content CSS": "Benutzerdefiniertes Inhalts-CSS",
|
||||
"Enter CSS for book content styling...": "CSS für die Buchinhaltsgestaltung eingeben...",
|
||||
"Custom Reader UI CSS": "Benutzerdefiniertes CSS für die Leseroberfläche",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "Wiedergabe",
|
||||
"Next Sentence": "Nächster Satz",
|
||||
"Next Paragraph": "Nächster Absatz",
|
||||
"Separate Cover Page": "Titelseite separat anzeigen"
|
||||
"Separate Cover Page": "Titelseite separat anzeigen",
|
||||
"Resize Notebook": "Notizbuchgröße ändern",
|
||||
"Resize Sidebar": "Seitenleisten-Größe ändern",
|
||||
"Get Help from the Readest Community": "Hilfe von der Readest-Community erhalten",
|
||||
"Remove cover image": "Coverbild entfernen",
|
||||
"Bookshelf": "Bücherregal",
|
||||
"View Menu": "Menü anzeigen",
|
||||
"Settings Menu": "Einstellungsmenü",
|
||||
"View account details and quota": "Kontodetails und Kontingent anzeigen",
|
||||
"Library Header": "Bibliothekskopf",
|
||||
"Book Content": "Buchinhalt",
|
||||
"Footer Bar": "Fußzeile",
|
||||
"Header Bar": "Kopfzeile",
|
||||
"View Options": "Ansichtsoptionen",
|
||||
"Book Menu": "Buchmenü",
|
||||
"Search Options": "Suchoptionen",
|
||||
"Close": "Schließen",
|
||||
"Delete Book Options": "Buch-Löschoptionen",
|
||||
"ON": "EIN",
|
||||
"OFF": "AUS",
|
||||
"Reading Progress": "Lesefortschritt",
|
||||
"Page Margin": "Seitenrand",
|
||||
"Remove Bookmark": "Lesezeichen entfernen",
|
||||
"Add Bookmark": "Lesezeichen hinzufügen",
|
||||
"Books Content": "Buchinhalt",
|
||||
"Jump to Location": "Zu Standort springen",
|
||||
"Unpin Notebook": "Notizbuch lösen",
|
||||
"Pin Notebook": "Notizbuch anheften",
|
||||
"Hide Search Bar": "Suchleiste ausblenden",
|
||||
"Show Search Bar": "Suchleiste einblenden",
|
||||
"On {{current}} of {{total}} page": "Auf Seite {{current}} von {{total}}",
|
||||
"Section Title": "Abschnittsüberschrift",
|
||||
"Decrease": "Verringern",
|
||||
"Increase": "Erhöhen",
|
||||
"Settings Panels": "Einstellungsfenster",
|
||||
"Settings": "Einstellungen",
|
||||
"Unpin Sidebar": "Seitenleiste lösen",
|
||||
"Pin Sidebar": "Seitenleiste anheften",
|
||||
"Toggle Sidebar": "Seitenleiste umschalten",
|
||||
"Toggle Translation": "Übersetzung umschalten",
|
||||
"Translation Disabled": "Übersetzung deaktiviert",
|
||||
"Minimize": "Minimieren",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Αναζήτηση...",
|
||||
"Select Book": "Επιλογή βιβλίου",
|
||||
"Select Books": "Επιλογή βιβλίων",
|
||||
"Select Multiple Books": "Επιλογή πολλαπλών βιβλίων",
|
||||
"Sepia": "Σέπια",
|
||||
"Serif Font": "Γραμματοσειρά Serif",
|
||||
"Show Book Details": "Εμφάνιση λεπτομερειών βιβλίου",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Λεπτομέρειες βιβλίου",
|
||||
"From Local File": "Από τοπικό αρχείο",
|
||||
"TOC": "Πίνακας περιεχομένων",
|
||||
"Table of Contents": "Πίνακας περιεχομένων",
|
||||
"Book uploaded: {{title}}": "Το βιβλίο με τίτλο {{title}} ανέβηκε",
|
||||
"Failed to upload book: {{title}}": "Αποτυχία ανέβασμα βιβλίου: {{title}}",
|
||||
"Book downloaded: {{title}}": "Το βιβλίο με τίτλο {{title}} κατέβηκε",
|
||||
@@ -333,7 +333,6 @@
|
||||
"Left Margin (px)": "Αριστερό περιθώριο (px)",
|
||||
"Column Gap (%)": "Απόσταση στηλών (%)",
|
||||
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης",
|
||||
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη",
|
||||
"Custom Content CSS": "Προσαρμοσμένο CSS περιεχομένου",
|
||||
"Enter CSS for book content styling...": "Εισαγάγετε CSS για στυλ περιεχομένου βιβλίου...",
|
||||
"Custom Reader UI CSS": "Προσαρμοσμένο CSS διεπαφής αναγνώστη",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "Αναπαραγωγή",
|
||||
"Next Sentence": "Επόμενη πρόταση",
|
||||
"Next Paragraph": "Επόμενη παράγραφος",
|
||||
"Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου"
|
||||
"Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου",
|
||||
"Resize Notebook": "Αλλαγή μεγέθους σημειωματάριου",
|
||||
"Resize Sidebar": "Αλλαγή μεγέθους πλαϊνής γραμμής",
|
||||
"Get Help from the Readest Community": "Λάβετε βοήθεια από την κοινότητα Readest",
|
||||
"Remove cover image": "Αφαίρεση εικόνας εξωφύλλου",
|
||||
"Bookshelf": "Ράφι βιβλίων",
|
||||
"View Menu": "Προβολή μενού",
|
||||
"Settings Menu": "Μενού ρυθμίσεων",
|
||||
"View account details and quota": "Προβολή στοιχείων λογαριασμού και ορίου",
|
||||
"Library Header": "Κεφαλίδα βιβλιοθήκης",
|
||||
"Book Content": "Περιεχόμενο βιβλίου",
|
||||
"Footer Bar": "Γραμμή υποσέλιδου",
|
||||
"Header Bar": "Γραμμή κεφαλίδας",
|
||||
"View Options": "Επιλογές προβολής",
|
||||
"Book Menu": "Μενού βιβλίου",
|
||||
"Search Options": "Επιλογές αναζήτησης",
|
||||
"Close": "Κλείσιμο",
|
||||
"Delete Book Options": "Επιλογές διαγραφής βιβλίου",
|
||||
"ON": "ΕΝΕΡΓΟΠΟΙΗΣΗ",
|
||||
"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": "Μεγιστοποίηση ή Επαναφορά",
|
||||
"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": "Ανάγνωση"
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@
|
||||
"Search...": "Buscar...",
|
||||
"Select Book": "Seleccionar libro",
|
||||
"Select Books": "Seleccionar libros",
|
||||
"Select Multiple Books": "Seleccionar varios libros",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Fuente serif",
|
||||
"Show Book Details": "Mostrar detalles del libro",
|
||||
@@ -145,6 +144,7 @@
|
||||
"Book Details": "Detalles del libro",
|
||||
"From Local File": "Desde archivo local",
|
||||
"TOC": "Índice",
|
||||
"Table of Contents": "Índice",
|
||||
"Book uploaded: {{title}}": "Libro subido: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Error al subir libro: {{title}}",
|
||||
"Book downloaded: {{title}}": "Libro descargado: {{title}}",
|
||||
@@ -363,7 +363,6 @@
|
||||
"Left Margin (px)": "Margen izquierdo (px)",
|
||||
"Column Gap (%)": "Espacio entre columnas (%)",
|
||||
"Always Show Status Bar": "Mostrar siempre la barra de estado",
|
||||
"Translation Not Available": "Traducción no disponible",
|
||||
"Custom Content CSS": "CSS del contenido",
|
||||
"Enter CSS for book content styling...": "Introduce CSS para el contenido del libro...",
|
||||
"Custom Reader UI CSS": "CSS de la interfaz",
|
||||
@@ -526,5 +525,83 @@
|
||||
"Play": "Reproducir",
|
||||
"Next Sentence": "Siguiente oración",
|
||||
"Next Paragraph": "Siguiente párrafo",
|
||||
"Separate Cover Page": "Portada separada"
|
||||
"Separate Cover Page": "Portada separada",
|
||||
"Resize Notebook": "Redimensionar cuaderno",
|
||||
"Resize Sidebar": "Redimensionar barra lateral",
|
||||
"Get Help from the Readest Community": "Obtén ayuda de la comunidad de Readest",
|
||||
"Remove cover image": "Eliminar imagen de portada",
|
||||
"Bookshelf": "Estantería",
|
||||
"View Menu": "Menú de Vista",
|
||||
"Settings Menu": "Menú de Configuración",
|
||||
"View account details and quota": "Ver detalles de la cuenta y cuota",
|
||||
"Library Header": "Encabezado de Biblioteca",
|
||||
"Book Content": "Contenido del Libro",
|
||||
"Footer Bar": "Barra de Pie de Página",
|
||||
"Header Bar": "Barra de Encabezado",
|
||||
"View Options": "Opciones de Vista",
|
||||
"Book Menu": "Menú de Libro",
|
||||
"Search Options": "Opciones de Búsqueda",
|
||||
"Close": "Cerrar",
|
||||
"Delete Book Options": "Opciones de Eliminar Libro",
|
||||
"ON": "ENCENDIDO",
|
||||
"OFF": "APAGADO",
|
||||
"Reading Progress": "Progreso de Lectura",
|
||||
"Page Margin": "Márgenes de Página",
|
||||
"Remove Bookmark": "Eliminar Marcador",
|
||||
"Add Bookmark": "Agregar Marcador",
|
||||
"Books Content": "Contenido de Libros",
|
||||
"Jump to Location": "Saltar a Ubicación",
|
||||
"Unpin Notebook": "Desanclar Cuaderno",
|
||||
"Pin Notebook": "Anclar Cuaderno",
|
||||
"Hide Search Bar": "Ocultar Barra de Búsqueda",
|
||||
"Show Search Bar": "Mostrar Barra de Búsqueda",
|
||||
"On {{current}} of {{total}} page": "En {{current}} de {{total}} página",
|
||||
"Section Title": "Título de Sección",
|
||||
"Decrease": "Disminuir",
|
||||
"Increase": "Aumentar",
|
||||
"Settings Panels": "Paneles de Configuración",
|
||||
"Settings": "Configuraciones",
|
||||
"Unpin Sidebar": "Desanclar Barra Lateral",
|
||||
"Pin Sidebar": "Anclar Barra Lateral",
|
||||
"Toggle Sidebar": "Alternar Barra Lateral",
|
||||
"Toggle Translation": "Alternar Traducción",
|
||||
"Translation Disabled": "Traducción Deshabilitada",
|
||||
"Minimize": "Minimizar",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Rechercher...",
|
||||
"Select Book": "Sélectionner un livre",
|
||||
"Select Books": "Sélectionner des livres",
|
||||
"Select Multiple Books": "Sélectionner plusieurs livres",
|
||||
"Sepia": "Sépia",
|
||||
"Serif Font": "Police avec empattement",
|
||||
"Show Book Details": "Afficher les détails du livre",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Détails du livre",
|
||||
"From Local File": "Depuis un fichier local",
|
||||
"TOC": "Table des matières",
|
||||
"Table of Contents": "Table des matières",
|
||||
"Book uploaded: {{title}}": "Livre téléchargé : {{title}}",
|
||||
"Failed to upload book: {{title}}": "Échec du téléchargement du livre : {{title}}",
|
||||
"Book downloaded: {{title}}": "Livre téléchargé : {{title}}",
|
||||
@@ -335,7 +335,6 @@
|
||||
"Left Margin (px)": "Marge gauche (px)",
|
||||
"Column Gap (%)": "Espacement des colonnes (%)",
|
||||
"Always Show Status Bar": "Afficher toujours la barre d'état",
|
||||
"Translation Not Available": "Traduction non disponible",
|
||||
"Custom Content CSS": "CSS du contenu",
|
||||
"Enter CSS for book content styling...": "Saisissez le CSS pour le contenu du livre...",
|
||||
"Custom Reader UI CSS": "CSS de l’interface",
|
||||
@@ -526,5 +525,83 @@
|
||||
"Play": "Lecture",
|
||||
"Next Sentence": "Phrase suivante",
|
||||
"Next Paragraph": "Paragraphe suivant",
|
||||
"Separate Cover Page": "Page de couverture séparée"
|
||||
"Separate Cover Page": "Page de couverture séparée",
|
||||
"Resize Notebook": "Redimensionner le Carnet",
|
||||
"Resize Sidebar": "Redimensionner la Barre Latérale",
|
||||
"Get Help from the Readest Community": "Obtenir de l'aide de la communauté Readest",
|
||||
"Remove cover image": "Supprimer l'image de couverture",
|
||||
"Bookshelf": "Étagère à livres",
|
||||
"View Menu": "Menu d'affichage",
|
||||
"Settings Menu": "Menu des paramètres",
|
||||
"View account details and quota": "Voir les détails du compte et le quota",
|
||||
"Library Header": "En-tête de la bibliothèque",
|
||||
"Book Content": "Contenu du livre",
|
||||
"Footer Bar": "Barre de pied de page",
|
||||
"Header Bar": "Barre d'en-tête",
|
||||
"View Options": "Options d'affichage",
|
||||
"Book Menu": "Menu du livre",
|
||||
"Search Options": "Options de recherche",
|
||||
"Close": "Fermer",
|
||||
"Delete Book Options": "Options de suppression du livre",
|
||||
"ON": "ACTIVER",
|
||||
"OFF": "DÉSACTIVER",
|
||||
"Reading Progress": "Progression de lecture",
|
||||
"Page Margin": "Marge de page",
|
||||
"Remove Bookmark": "Supprimer le Marque-page",
|
||||
"Add Bookmark": "Ajouter un Marque-page",
|
||||
"Books Content": "Contenu des Livres",
|
||||
"Jump to Location": "Aller à l'Emplacement",
|
||||
"Unpin Notebook": "Détacher le Carnet",
|
||||
"Pin Notebook": "Épingler le Carnet",
|
||||
"Hide Search Bar": "Masquer la Barre de Recherche",
|
||||
"Show Search Bar": "Afficher la Barre de Recherche",
|
||||
"On {{current}} of {{total}} page": "Sur {{current}} de {{total}} page",
|
||||
"Section Title": "Titre de Section",
|
||||
"Decrease": "Diminuer",
|
||||
"Increase": "Augmenter",
|
||||
"Settings Panels": "Panneaux de Configuration",
|
||||
"Settings": "Paramètres",
|
||||
"Unpin Sidebar": "Détacher la Barre Latérale",
|
||||
"Pin Sidebar": "Épingler la Barre Latérale",
|
||||
"Toggle Sidebar": "Alternar la Barre Latérale",
|
||||
"Toggle Translation": "Alternar Traduction",
|
||||
"Translation Disabled": "Traduction Désactivée",
|
||||
"Minimize": "Minimiser",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "खोजें...",
|
||||
"Select Book": "पुस्तक चुनें",
|
||||
"Select Books": "पुस्तकें चुनें",
|
||||
"Select Multiple Books": "कई पुस्तकें चुनें",
|
||||
"Sepia": "सेपिया",
|
||||
"Serif Font": "सेरिफ फ़ॉन्ट",
|
||||
"Show Book Details": "पुस्तक विवरण दिखाएं",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "पुस्तक विवरण",
|
||||
"From Local File": "स्थानीय फ़ाइल से",
|
||||
"TOC": "सामग्री",
|
||||
"Table of Contents": "सामग्री",
|
||||
"Book uploaded: {{title}}": "पुस्तक अपलोड की गई: {{title}}",
|
||||
"Failed to upload book: {{title}}": "पुस्तक अपलोड करने में विफल: {{title}}",
|
||||
"Book downloaded: {{title}}": "पुस्तक डाउनलोड की गई: {{title}}",
|
||||
@@ -333,7 +333,6 @@
|
||||
"Left Margin (px)": "बायां हाशिया (px)",
|
||||
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
|
||||
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं",
|
||||
"Translation Not Available": "अनुवाद उपलब्ध नहीं है",
|
||||
"Custom Content CSS": "सामग्री का CSS",
|
||||
"Enter CSS for book content styling...": "पुस्तक सामग्री के लिए CSS दर्ज करें...",
|
||||
"Custom Reader UI CSS": "इंटरफ़ेस का CSS",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "चलाएं",
|
||||
"Next Sentence": "अगला वाक्य",
|
||||
"Next Paragraph": "अगला पैराग्राफ",
|
||||
"Separate Cover Page": "अलग कवर पृष्ठ"
|
||||
"Separate Cover Page": "अलग कवर पृष्ठ",
|
||||
"Resize Notebook": "नोटबुक का आकार बदलें",
|
||||
"Resize Sidebar": "साइडबार का आकार बदलें",
|
||||
"Get Help from the Readest Community": "Readest समुदाय से मदद लें",
|
||||
"Remove cover image": "कवर इमेज हटाएं",
|
||||
"Bookshelf": "पुस्तक शेल्फ",
|
||||
"View Menu": "मेनू देखें",
|
||||
"Settings Menu": "सेटिंग्स मेनू",
|
||||
"View account details and quota": "खाता विवरण और कोटा देखें",
|
||||
"Library Header": "पुस्तकालय शीर्षक",
|
||||
"Book Content": "पुस्तक की सामग्री",
|
||||
"Footer Bar": "फुटर बार",
|
||||
"Header Bar": "हेडर बार",
|
||||
"View Options": "देखने के विकल्प",
|
||||
"Book Menu": "पुस्तक मेनू",
|
||||
"Search Options": "खोज विकल्प",
|
||||
"Close": "बंद करें",
|
||||
"Delete Book Options": "पुस्तक हटाने के विकल्प",
|
||||
"ON": "चालू",
|
||||
"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": "अधिकतम या पुनर्स्थापित करें",
|
||||
"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": "उच्च स्वर में पढ़ें"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Cari...",
|
||||
"Select Book": "Pilih Buku",
|
||||
"Select Books": "Pilih buku",
|
||||
"Select Multiple Books": "Pilih beberapa buku",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Font Serif",
|
||||
"Show Book Details": "Tampilkan Detail Buku",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Detail Buku",
|
||||
"From Local File": "Dari file lokal",
|
||||
"TOC": "Daftar Isi",
|
||||
"Table of Contents": "Daftar Isi",
|
||||
"Book uploaded: {{title}}": "Buku diunggah: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Gagal mengunggah buku: {{title}}",
|
||||
"Book downloaded: {{title}}": "Buku diunduh: {{title}}",
|
||||
@@ -330,7 +330,6 @@
|
||||
"Left Margin (px)": "Margin Kiri (px)",
|
||||
"Column Gap (%)": "Jarak Antar Kolom (%)",
|
||||
"Always Show Status Bar": "Selalu Tampilkan Bilah Status",
|
||||
"Translation Not Available": "Terjemahan Tidak Tersedia",
|
||||
"Custom Content CSS": "CSS konten",
|
||||
"Enter CSS for book content styling...": "Masukkan CSS untuk gaya konten buku...",
|
||||
"Custom Reader UI CSS": "CSS antarmuka",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "Putar",
|
||||
"Next Sentence": "Kalimat Berikutnya",
|
||||
"Next Paragraph": "Paragraf Berikutnya",
|
||||
"Separate Cover Page": "Halaman Sampul Terpisah"
|
||||
"Separate Cover Page": "Halaman Sampul Terpisah",
|
||||
"Resize Notebook": "Ubah Ukuran Buku Catatan",
|
||||
"Resize Sidebar": "Ubah Ukuran Bilah Samping",
|
||||
"Get Help from the Readest Community": "Dapatkan Bantuan dari Komunitas Readest",
|
||||
"Remove cover image": "Hapus gambar sampul",
|
||||
"Bookshelf": "Rak Buku",
|
||||
"View Menu": "Lihat Menu",
|
||||
"Settings Menu": "Menu Pengaturan",
|
||||
"View account details and quota": "Lihat detail akun dan kuota",
|
||||
"Library Header": "Header Perpustakaan",
|
||||
"Book Content": "Isi Buku",
|
||||
"Footer Bar": "Bilah Footer",
|
||||
"Header Bar": "Bilah Header",
|
||||
"View Options": "Opsi Tampilan",
|
||||
"Book Menu": "Menu Buku",
|
||||
"Search Options": "Opsi Pencarian",
|
||||
"Close": "Tutup",
|
||||
"Delete Book Options": "Opsi Hapus Buku",
|
||||
"ON": "AKTIF",
|
||||
"OFF": "TIDAK AKTIF",
|
||||
"Reading Progress": "Progres Membaca",
|
||||
"Page Margin": "Margin Halaman",
|
||||
"Remove Bookmark": "Hapus Penanda",
|
||||
"Add Bookmark": "Tambahkan Penanda",
|
||||
"Books Content": "Isi Buku",
|
||||
"Jump to Location": "Lompat ke Lokasi",
|
||||
"Unpin Notebook": "Batal Pin Notebook",
|
||||
"Pin Notebook": "Pin Notebook",
|
||||
"Hide Search Bar": "Sembunyikan Bilah Pencarian",
|
||||
"Show Search Bar": "Tampilkan Bilah Pencarian",
|
||||
"On {{current}} of {{total}} page": "Pada {{current}} dari {{total}} halaman",
|
||||
"Section Title": "Judul Bagian",
|
||||
"Decrease": "Kecilkan",
|
||||
"Increase": "Besarakan",
|
||||
"Settings Panels": "Panel Pengaturan",
|
||||
"Settings": "Pengaturan",
|
||||
"Unpin Sidebar": "Batal Pin Bilah Samping",
|
||||
"Pin Sidebar": "Pin Bilah Samping",
|
||||
"Toggle Sidebar": "Toggel Bilah Samping",
|
||||
"Toggle Translation": "Toggel Terjemahan",
|
||||
"Translation Disabled": "Terjemahan Dinonaktifkan",
|
||||
"Minimize": "Minimalkan",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Cerca...",
|
||||
"Select Book": "Seleziona libro",
|
||||
"Select Books": "Seleziona libri",
|
||||
"Select Multiple Books": "Seleziona più libri",
|
||||
"Sepia": "Seppia",
|
||||
"Serif Font": "Font serif",
|
||||
"Show Book Details": "Mostra dettagli libro",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Dettagli libro",
|
||||
"From Local File": "Da file locale",
|
||||
"TOC": "Sommario",
|
||||
"Table of Contents": "Sommario",
|
||||
"Book uploaded: {{title}}": "Libro caricato: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Caricamento libro non riuscito: {{title}}",
|
||||
"Book downloaded: {{title}}": "Libro scaricato: {{title}}",
|
||||
@@ -336,7 +336,6 @@
|
||||
"Left Margin (px)": "Margine Sinistro (px)",
|
||||
"Column Gap (%)": "Spazio tra Colonne (%)",
|
||||
"Always Show Status Bar": "Mostra sempre la barra di stato",
|
||||
"Translation Not Available": "Traduzione non disponibile",
|
||||
"Custom Content CSS": "CSS contenuto",
|
||||
"Enter CSS for book content styling...": "Inserisci il CSS per il contenuto del libro...",
|
||||
"Custom Reader UI CSS": "CSS interfaccia",
|
||||
@@ -526,5 +525,83 @@
|
||||
"Play": "Riproduci",
|
||||
"Next Sentence": "Frase successiva",
|
||||
"Next Paragraph": "Paragrafo successivo",
|
||||
"Separate Cover Page": "Pagina di copertina separata"
|
||||
"Separate Cover Page": "Pagina di copertina separata",
|
||||
"Resize Notebook": "Ridimensiona quaderno",
|
||||
"Resize Sidebar": "Ridimensiona barra laterale",
|
||||
"Get Help from the Readest Community": "Ottieni aiuto dalla community di Readest",
|
||||
"Remove cover image": "Rimuovi immagine di copertina",
|
||||
"Bookshelf": "Scaffale dei libri",
|
||||
"View Menu": "Visualizza menu",
|
||||
"Settings Menu": "Menu impostazioni",
|
||||
"View account details and quota": "Visualizza dettagli account e quota",
|
||||
"Library Header": "Intestazione biblioteca",
|
||||
"Book Content": "Contenuto del libro",
|
||||
"Footer Bar": "Barra piè di pagina",
|
||||
"Header Bar": "Barra intestazione",
|
||||
"View Options": "Opzioni di visualizzazione",
|
||||
"Book Menu": "Menu libro",
|
||||
"Search Options": "Opzioni di ricerca",
|
||||
"Close": "Chiudi",
|
||||
"Delete Book Options": "Opzioni di eliminazione libro",
|
||||
"ON": "ATTIVO",
|
||||
"OFF": "DISATTIVO",
|
||||
"Reading Progress": "Progresso di lettura",
|
||||
"Page Margin": "Margine di pagina",
|
||||
"Remove Bookmark": "Rimuovi Segnalibro",
|
||||
"Add Bookmark": "Aggiungi Segnalibro",
|
||||
"Books Content": "Contenuto dei Libri",
|
||||
"Jump to Location": "Salta alla Posizione",
|
||||
"Unpin Notebook": "Sblocca Quaderno",
|
||||
"Pin Notebook": "Blocca Quaderno",
|
||||
"Hide Search Bar": "Nascondi Barra di Ricerca",
|
||||
"Show Search Bar": "Mostra Barra di Ricerca",
|
||||
"On {{current}} of {{total}} page": "A pagina {{current}} di {{total}}",
|
||||
"Section Title": "Titolo Sezione",
|
||||
"Decrease": "Diminuisci",
|
||||
"Increase": "Aumenta",
|
||||
"Settings Panels": "Pannelli di Impostazione",
|
||||
"Settings": "Impostazioni",
|
||||
"Unpin Sidebar": "Sblocca Bilah Samping",
|
||||
"Pin Sidebar": "Blocca Bilah Samping",
|
||||
"Toggle Sidebar": "Toggel Bilah Samping",
|
||||
"Toggle Translation": "Toggel Terjemahan",
|
||||
"Translation Disabled": "Terjemahan Dinonaktifkan",
|
||||
"Minimize": "Minimalkan",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "検索...",
|
||||
"Select Book": "書籍を選択",
|
||||
"Select Books": "書籍を選択",
|
||||
"Select Multiple Books": "複数の書籍を選択",
|
||||
"Sepia": "セピア",
|
||||
"Serif Font": "セリフ体",
|
||||
"Show Book Details": "書籍の詳細を表示",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "書籍の詳細",
|
||||
"From Local File": "ローカルファイルから",
|
||||
"TOC": "目次",
|
||||
"Table of Contents": "目次",
|
||||
"Book uploaded: {{title}}": "書籍がアップロードされました:{{title}}",
|
||||
"Failed to upload book: {{title}}": "書籍のアップロードに失敗しました:{{title}}",
|
||||
"Book downloaded: {{title}}": "書籍がダウンロードされました:{{title}}",
|
||||
@@ -330,7 +330,6 @@
|
||||
"Left Margin (px)": "左マージン (px)",
|
||||
"Column Gap (%)": "列間の間隔 (%)",
|
||||
"Always Show Status Bar": "ステータスバーを常に表示",
|
||||
"Translation Not Available": "翻訳は利用できません",
|
||||
"Custom Content CSS": "コンテンツCSS",
|
||||
"Enter CSS for book content styling...": "本のコンテンツ用CSSを入力...",
|
||||
"Custom Reader UI CSS": "リーダーUIのCSS",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "再生",
|
||||
"Next Sentence": "次の文",
|
||||
"Next Paragraph": "次の段落",
|
||||
"Separate Cover Page": "別の表紙ページ"
|
||||
"Separate Cover Page": "別の表紙ページ",
|
||||
"Resize Notebook": "ノートのサイズ変更",
|
||||
"Resize Sidebar": "サイドバーのサイズ変更",
|
||||
"Get Help from the Readest Community": "Readestコミュニティからヘルプを得る",
|
||||
"Remove cover image": "表紙画像を削除",
|
||||
"Bookshelf": "本棚",
|
||||
"View Menu": "メニューを表示",
|
||||
"Settings Menu": "設定メニュー",
|
||||
"View account details and quota": "アカウントの詳細とクォータを表示",
|
||||
"Library Header": "ライブラリヘッダー",
|
||||
"Book Content": "本の内容",
|
||||
"Footer Bar": "フッターバー",
|
||||
"Header Bar": "ヘッダーバー",
|
||||
"View Options": "表示オプション",
|
||||
"Book Menu": "本のメニュー",
|
||||
"Search Options": "検索オプション",
|
||||
"Close": "閉じる",
|
||||
"Delete Book Options": "本の削除オプション",
|
||||
"ON": "オン",
|
||||
"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": "最大化または復元",
|
||||
"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": "読み上げ"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "검색...",
|
||||
"Select Book": "책 선택",
|
||||
"Select Books": "책 선택",
|
||||
"Select Multiple Books": "여러 책 선택",
|
||||
"Sepia": "세피아",
|
||||
"Serif Font": "세리프체",
|
||||
"Show Book Details": "책 세부 정보 표시",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "책 세부 정보",
|
||||
"From Local File": "로컬 파일에서",
|
||||
"TOC": "목차",
|
||||
"Table of Contents": "목차",
|
||||
"Book uploaded: {{title}}": "책 업로드됨: {{title}}",
|
||||
"Failed to upload book: {{title}}": "책 업로드 실패: {{title}}",
|
||||
"Book downloaded: {{title}}": "책 다운로드됨: {{title}}",
|
||||
@@ -330,7 +330,6 @@
|
||||
"Left Margin (px)": "왼쪽 여백 (px)",
|
||||
"Column Gap (%)": "열 간격 (%)",
|
||||
"Always Show Status Bar": "상태 표시줄 항상 표시",
|
||||
"Translation Not Available": "번역을 사용할 수 없음",
|
||||
"Custom Content CSS": "콘텐츠 CSS",
|
||||
"Enter CSS for book content styling...": "도서 콘텐츠 스타일을 위한 CSS 입력...",
|
||||
"Custom Reader UI CSS": "리더 UI CSS",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "재생",
|
||||
"Next Sentence": "다음 문장",
|
||||
"Next Paragraph": "다음 단락",
|
||||
"Separate Cover Page": "별도의 표지 페이지"
|
||||
"Separate Cover Page": "별도의 표지 페이지",
|
||||
"Resize Notebook": "노트북 크기 조정",
|
||||
"Resize Sidebar": "사이드바 크기 조정",
|
||||
"Get Help from the Readest Community": "Readest 커뮤니티에서 도움 받기",
|
||||
"Remove cover image": "표지 이미지 제거",
|
||||
"Bookshelf": "책장",
|
||||
"View Menu": "메뉴 보기",
|
||||
"Settings Menu": "설정 메뉴",
|
||||
"View account details and quota": "계정 세부정보 및 할당량 보기",
|
||||
"Library Header": "라이브러리 헤더",
|
||||
"Book Content": "도서 내용",
|
||||
"Footer Bar": "푸터 바",
|
||||
"Header Bar": "헤더 바",
|
||||
"View Options": "보기 옵션",
|
||||
"Book Menu": "도서 메뉴",
|
||||
"Search Options": "검색 옵션",
|
||||
"Close": "닫기",
|
||||
"Delete Book Options": "도서 삭제 옵션",
|
||||
"ON": "켜기",
|
||||
"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": "최대화 또는 복원",
|
||||
"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": "음성 읽기"
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
"Search Books...": "Boeken zoeken...",
|
||||
"Clear Search": "Zoekopdracht wissen",
|
||||
"Import Books": "Boeken importeren",
|
||||
"Select Multiple Books": "Meerdere boeken selecteren",
|
||||
"Select Books": "Boeken selecteren",
|
||||
"Logged in as {{userDisplayName}}": "Ingelogd als {{userDisplayName}}",
|
||||
"Logged in": "Ingelogd",
|
||||
@@ -211,6 +210,7 @@
|
||||
"Match Whole Words": "Hele woorden",
|
||||
"Match Diacritics": "Diakritische tekens",
|
||||
"TOC": "Inhoudsopgave",
|
||||
"Table of Contents": "Inhoudsopgave",
|
||||
"Sidebar": "Zijbalk",
|
||||
"TTS not supported for PDF": "TTS niet ondersteund voor PDF",
|
||||
"No Timeout": "Geen time-out",
|
||||
@@ -333,7 +333,6 @@
|
||||
"Left Margin (px)": "Linker marge (px)",
|
||||
"Column Gap (%)": "Kolomafstand (%)",
|
||||
"Always Show Status Bar": "Statusbalk altijd weergeven",
|
||||
"Translation Not Available": "Vertaling niet beschikbaar",
|
||||
"Custom Content CSS": "Aangepaste inhoud-CSS",
|
||||
"Enter CSS for book content styling...": "Voer CSS in voor de opmaak van de boekinhoud...",
|
||||
"Custom Reader UI CSS": "Aangepaste interface-CSS",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "Afspelen",
|
||||
"Next Sentence": "Volgende zin",
|
||||
"Next Paragraph": "Volgende alinea",
|
||||
"Separate Cover Page": "Afzonderlijke omslagpagina"
|
||||
"Separate Cover Page": "Afzonderlijke omslagpagina",
|
||||
"Resize Notebook": "Formaat Notitieblok Aanpassen",
|
||||
"Resize Sidebar": "Formaat Zijbalk Aanpassen",
|
||||
"Get Help from the Readest Community": "Hulp krijgen van de Readest Community",
|
||||
"Remove cover image": "Omslagafbeelding verwijderen",
|
||||
"Bookshelf": "Boekenkast",
|
||||
"View Menu": "Menu bekijken",
|
||||
"Settings Menu": "Instellingenmenu",
|
||||
"View account details and quota": "Accountgegevens en quotum bekijken",
|
||||
"Library Header": "Bibliotheekkop",
|
||||
"Book Content": "Boekinhoud",
|
||||
"Footer Bar": "Voettekstbalk",
|
||||
"Header Bar": "Koptekstbalk",
|
||||
"View Options": "Weergaveopties",
|
||||
"Book Menu": "Boekmenu",
|
||||
"Search Options": "Zoekopties",
|
||||
"Close": "Sluiten",
|
||||
"Delete Book Options": "Boekverwijderopties",
|
||||
"ON": "AAN",
|
||||
"OFF": "UIT",
|
||||
"Reading Progress": "Leesvoortgang",
|
||||
"Page Margin": "Pagina Marges",
|
||||
"Remove Bookmark": "Bladwijzer Verwijderen",
|
||||
"Add Bookmark": "Bladwijzer Toevoegen",
|
||||
"Books Content": "Boekinhoud",
|
||||
"Jump to Location": "Ga naar Locatie",
|
||||
"Unpin Notebook": "Notitieblok Losmaken",
|
||||
"Pin Notebook": "Notitieblok Vastmaken",
|
||||
"Hide Search Bar": "Verberg Zoekbalk",
|
||||
"Show Search Bar": "Toon Zoekbalk",
|
||||
"On {{current}} of {{total}} page": "Op {{current}} van {{total}} pagina",
|
||||
"Section Title": "Sectietitel",
|
||||
"Decrease": "Verkleinen",
|
||||
"Increase": "Vergroten",
|
||||
"Settings Panels": "Instellingenpanelen",
|
||||
"Settings": "Instellingen",
|
||||
"Unpin Sidebar": "Zijbalk Losmaken",
|
||||
"Pin Sidebar": "Zijbalk Vastmaken",
|
||||
"Toggle Sidebar": "Zijbalk Wisselen",
|
||||
"Toggle Translation": "Vertaling Wisselen",
|
||||
"Translation Disabled": "Vertaling Uitgeschakeld",
|
||||
"Minimize": "Minimaliseren",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Szukaj...",
|
||||
"Select Book": "Wybierz książkę",
|
||||
"Select Books": "Wybierz książki",
|
||||
"Select Multiple Books": "Wybierz wiele książek",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Czcionka szeryfowa",
|
||||
"Show Book Details": "Pokaż szczegóły książki",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Szczegóły książki",
|
||||
"From Local File": "Z pliku lokalnego",
|
||||
"TOC": "Spis treści",
|
||||
"Table of Contents": "Spis treści",
|
||||
"Book uploaded: {{title}}": "Książka przesłana: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Nie udało się przesłać książki: {{title}}",
|
||||
"Book downloaded: {{title}}": "Książka pobrana: {{title}}",
|
||||
@@ -339,7 +339,6 @@
|
||||
"Left Margin (px)": "Lewy margines (px)",
|
||||
"Column Gap (%)": "Odstęp między kolumnami (%)",
|
||||
"Always Show Status Bar": "Zawsze pokazuj pasek stanu",
|
||||
"Translation Not Available": "Brak dostępnego tłumaczenia",
|
||||
"Custom Content CSS": "CSS treści",
|
||||
"Enter CSS for book content styling...": "Wprowadź CSS dla stylu treści książki...",
|
||||
"Custom Reader UI CSS": "CSS interfejsu",
|
||||
@@ -530,5 +529,83 @@
|
||||
"Play": "Odtwórz",
|
||||
"Next Sentence": "Następne zdanie",
|
||||
"Next Paragraph": "Następny akapit",
|
||||
"Separate Cover Page": "Osobna strona okładki"
|
||||
"Separate Cover Page": "Osobna strona okładki",
|
||||
"Resize Notebook": "Zmień rozmiar notatnika",
|
||||
"Resize Sidebar": "Zmień rozmiar paska bocznego",
|
||||
"Get Help from the Readest Community": "Uzyskaj pomoc od społeczności Readest",
|
||||
"Remove cover image": "Usuń obraz okładki",
|
||||
"Bookshelf": "Półka na książki",
|
||||
"View Menu": "Pokaż menu",
|
||||
"Settings Menu": "Menu ustawień",
|
||||
"View account details and quota": "Zobacz szczegóły konta i limit",
|
||||
"Library Header": "Nagłówek biblioteki",
|
||||
"Book Content": "Zawartość książki",
|
||||
"Footer Bar": "Pasek stopki",
|
||||
"Header Bar": "Pasek nagłówka",
|
||||
"View Options": "Opcje widoku",
|
||||
"Book Menu": "Menu książki",
|
||||
"Search Options": "Opcje wyszukiwania",
|
||||
"Close": "Zamknij",
|
||||
"Delete Book Options": "Opcje usuwania książki",
|
||||
"ON": "WŁĄCZONE",
|
||||
"OFF": "WYŁĄCZONE",
|
||||
"Reading Progress": "Postęp czytania",
|
||||
"Page Margin": "Margines strony",
|
||||
"Remove Bookmark": "Usuń zakładkę",
|
||||
"Add Bookmark": "Dodaj zakładkę",
|
||||
"Books Content": "Zawartość książek",
|
||||
"Jump to Location": "Przejdź do lokalizacji",
|
||||
"Unpin Notebook": "Odczep notatnik",
|
||||
"Pin Notebook": "Przypnij notatnik",
|
||||
"Hide Search Bar": "Ukryj pasek wyszukiwania",
|
||||
"Show Search Bar": "Pokaż pasek wyszukiwania",
|
||||
"On {{current}} of {{total}} page": "Na {{current}} z {{total}} strony",
|
||||
"Section Title": "Tytuł sekcji",
|
||||
"Decrease": "Zmniejsz",
|
||||
"Increase": "Zwiększ",
|
||||
"Settings Panels": "Panele ustawień",
|
||||
"Settings": "Ustawienia",
|
||||
"Unpin Sidebar": "Odczep pasek boczny",
|
||||
"Pin Sidebar": "Przypnij pasek boczny",
|
||||
"Toggle Sidebar": "Włącz/Wyłącz pasek boczny",
|
||||
"Toggle Translation": "Włącz/Wyłącz tłumaczenie",
|
||||
"Translation Disabled": "Tłumaczenie wyłączone",
|
||||
"Minimize": "Minimalizuj",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Pesquisar...",
|
||||
"Select Book": "Selecionar Livro",
|
||||
"Select Books": "Selecionar livros",
|
||||
"Select Multiple Books": "Selecionar múltiplos livros",
|
||||
"Sepia": "Sépia",
|
||||
"Serif Font": "Fonte Serif",
|
||||
"Show Book Details": "Mostrar Detalhes do Livro",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Detalhes do Livro",
|
||||
"From Local File": "Do arquivo local",
|
||||
"TOC": "Índice",
|
||||
"Table of Contents": "Índice",
|
||||
"Book uploaded: {{title}}": "Livro enviado: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Falha ao enviar livro: {{title}}",
|
||||
"Book downloaded: {{title}}": "Livro baixado: {{title}}",
|
||||
@@ -336,7 +336,6 @@
|
||||
"Left Margin (px)": "Margem esquerda (px)",
|
||||
"Column Gap (%)": "Espaço entre colunas (%)",
|
||||
"Always Show Status Bar": "Exibir sempre a barra de status",
|
||||
"Translation Not Available": "Tradução não disponível",
|
||||
"Custom Content CSS": "CSS do conteúdo",
|
||||
"Enter CSS for book content styling...": "Insira o CSS para o estilo do conteúdo do livro...",
|
||||
"Custom Reader UI CSS": "CSS da interface",
|
||||
@@ -526,5 +525,83 @@
|
||||
"Play": "Reproduzir",
|
||||
"Next Sentence": "Próxima frase",
|
||||
"Next Paragraph": "Próximo parágrafo",
|
||||
"Separate Cover Page": "Páginas de Capa Separadas"
|
||||
"Separate Cover Page": "Páginas de Capa Separadas",
|
||||
"Resize Notebook": "Redimensionar Caderno",
|
||||
"Resize Sidebar": "Redimensionar Barra Lateral",
|
||||
"Get Help from the Readest Community": "Obter ajuda da comunidade Readest",
|
||||
"Remove cover image": "Remover imagem da capa",
|
||||
"Bookshelf": "Estante de Livros",
|
||||
"View Menu": "Ver Menu",
|
||||
"Settings Menu": "Menu de Configurações",
|
||||
"View account details and quota": "Ver detalhes da conta e cota",
|
||||
"Library Header": "Cabeçalho da Biblioteca",
|
||||
"Book Content": "Conteúdo do Livro",
|
||||
"Footer Bar": "Barra de Rodapé",
|
||||
"Header Bar": "Barra de Cabeçalho",
|
||||
"View Options": "Opções de Visualização",
|
||||
"Book Menu": "Menu do Livro",
|
||||
"Search Options": "Opções de Pesquisa",
|
||||
"Close": "Fechar",
|
||||
"Delete Book Options": "Opções de Exclusão de Livro",
|
||||
"ON": "LIGADO",
|
||||
"OFF": "DESLIGADO",
|
||||
"Reading Progress": "Progresso de Leitura",
|
||||
"Page Margin": "Margem da Página",
|
||||
"Remove Bookmark": "Remover Marcador",
|
||||
"Add Bookmark": "Adicionar Marcador",
|
||||
"Books Content": "Conteúdo dos Livros",
|
||||
"Jump to Location": "Ir para Localização",
|
||||
"Unpin Notebook": "Desafixar Caderno",
|
||||
"Pin Notebook": "Fixar Caderno",
|
||||
"Hide Search Bar": "Ocultar Barra de Pesquisa",
|
||||
"Show Search Bar": "Mostrar Barra de Pesquisa",
|
||||
"On {{current}} of {{total}} page": "Na {{current}} de {{total}} página",
|
||||
"Section Title": "Título da Seção",
|
||||
"Decrease": "Diminuir",
|
||||
"Increase": "Aumentar",
|
||||
"Settings Panels": "Painéis de Configurações",
|
||||
"Settings": "Configurações",
|
||||
"Unpin Sidebar": "Desafixar Barra Lateral",
|
||||
"Pin Sidebar": "Fixar Barra Lateral",
|
||||
"Toggle Sidebar": "Alternar Barra Lateral",
|
||||
"Toggle Translation": "Alternar Tradução",
|
||||
"Translation Disabled": "Tradução Desativada",
|
||||
"Minimize": "Minimizar",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Поиск...",
|
||||
"Select Book": "Выбрать книгу",
|
||||
"Select Books": "Выбрать книги",
|
||||
"Select Multiple Books": "Выбрать несколько книг",
|
||||
"Sepia": "Сепия",
|
||||
"Serif Font": "Шрифт с засечками",
|
||||
"Show Book Details": "Показать детали книги",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Детали книги",
|
||||
"From Local File": "Из локального файла",
|
||||
"TOC": "Содержание",
|
||||
"Table of Contents": "Содержание",
|
||||
"Book uploaded: {{title}}": "Книга загружена: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Не удалось загрузить книгу: {{title}}",
|
||||
"Book downloaded: {{title}}": "Книга скачана: {{title}}",
|
||||
@@ -339,7 +339,6 @@
|
||||
"Left Margin (px)": "Левый отступ (пикс)",
|
||||
"Column Gap (%)": "Промежуток между колонками (%)",
|
||||
"Always Show Status Bar": "Всегда показывать панель состояния",
|
||||
"Translation Not Available": "Перевод недоступен",
|
||||
"Custom Content CSS": "CSS содержимого",
|
||||
"Enter CSS for book content styling...": "Введите CSS для оформления содержимого книги...",
|
||||
"Custom Reader UI CSS": "CSS интерфейса",
|
||||
@@ -530,5 +529,83 @@
|
||||
"Play": "Воспроизвести",
|
||||
"Next Sentence": "Следующее предложение",
|
||||
"Next Paragraph": "Следующий абзац",
|
||||
"Separate Cover Page": "Отдельная страница обложки"
|
||||
"Separate Cover Page": "Отдельная страница обложки",
|
||||
"Resize Notebook": "Изменить размер блокнота",
|
||||
"Resize Sidebar": "Изменить размер боковой панели",
|
||||
"Get Help from the Readest Community": "Получить помощь от сообщества Readest",
|
||||
"Remove cover image": "Удалить изображение обложки",
|
||||
"Bookshelf": "Книжная полка",
|
||||
"View Menu": "Просмотр меню",
|
||||
"Settings Menu": "Меню настроек",
|
||||
"View account details and quota": "Просмотр данных аккаунта и квоты",
|
||||
"Library Header": "Заголовок библиотеки",
|
||||
"Book Content": "Содержание книги",
|
||||
"Footer Bar": "Нижняя панель",
|
||||
"Header Bar": "Верхняя панель",
|
||||
"View Options": "Параметры просмотра",
|
||||
"Book Menu": "Меню книги",
|
||||
"Search Options": "Параметры поиска",
|
||||
"Close": "Закрыть",
|
||||
"Delete Book Options": "Параметры удаления книги",
|
||||
"ON": "ВКЛ",
|
||||
"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": "Максимизировать или восстановить",
|
||||
"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": "Читать вслух"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"Search Books...": "පොත් සොයන්න...",
|
||||
"Clear Search": "සෙවීම මකන්න",
|
||||
"Import Books": "පොත් ආයාත කරන්න",
|
||||
"Select Multiple Books": "පොත් කිහිපයක් තෝරන්න",
|
||||
"Select Books": "පොත් තෝරන්න",
|
||||
"Deselect": "අනිශ්චිත කරන්න",
|
||||
"Select All": "සියල්ල තෝරන්න",
|
||||
@@ -324,9 +323,9 @@
|
||||
"Match Whole Words": "සම්පූර්ණ වචන ගැළපීම",
|
||||
"Match Diacritics": "ධ්වනි ලකුණු ගැළපීම",
|
||||
"TOC": "සූචිය",
|
||||
"Table of Contents": "සූචිය",
|
||||
"Sidebar": "පැති තීරුව",
|
||||
"Disable Translation": "පරිවර්තනය අබල කරන්න",
|
||||
"Translation Not Available": "පරිවර්තනය ලබා ගත නොහැක",
|
||||
"TTS not supported for PDF": "PDF සඳහා TTS සහාය නොදක්වයි",
|
||||
"TTS not supported for this document": "මෙම ලේඛනය සඳහා TTS සහාය නොදක්වයි",
|
||||
"No Timeout": "කාල සීමාවක් නැත",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "වාදනය කරන්න",
|
||||
"Next Sentence": "මීළඟ වාක්යය",
|
||||
"Next Paragraph": "මීළඟ ඡේදය",
|
||||
"Separate Cover Page": "වෙන් වූ ආවරණ පිටුව"
|
||||
"Separate Cover Page": "වෙන් වූ ආවරණ පිටුව",
|
||||
"Resize Notebook": "සටහන් පොතේ ප්රමාණය වෙනස් කරන්න",
|
||||
"Resize Sidebar": "පැති තීරුවේ ප්රමාණය වෙනස් කරන්න",
|
||||
"Get Help from the Readest Community": "Readest ප්රජාවෙන් උදව් ලබා ගන්න",
|
||||
"Remove cover image": "ආවරණ රූපය ඉවත් කරන්න",
|
||||
"Bookshelf": "පොත් රාක්කය",
|
||||
"View Menu": "මෙනුව බලන්න",
|
||||
"Settings Menu": "සැකසුම් මෙනුව",
|
||||
"View account details and quota": "ගිණුම් විස්තර සහ කොටස බලන්න",
|
||||
"Library Header": "පුස්තකාල ශීර්ෂය",
|
||||
"Book Content": "පොතේ අන්තර්ගතය",
|
||||
"Footer Bar": "පාදක තීරුව",
|
||||
"Header Bar": "ශීර්ෂ තීරුව",
|
||||
"View Options": "දැක්ම විකල්ප",
|
||||
"Book Menu": "පොත් මෙනුව",
|
||||
"Search Options": "සෙවුම් විකල්ප",
|
||||
"Close": "වසන්න",
|
||||
"Delete Book Options": "පොත මකන විකල්ප",
|
||||
"ON": "ඇක්ටිව්",
|
||||
"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": "ඉහළ නැංවීම හෝ ප්රතිසංස්කරණය",
|
||||
"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": "පවසා කියවීම"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"Search Books...": "புத்தகங்களைத் தேடவும்...",
|
||||
"Clear Search": "தேடலை அழிக்கவும்",
|
||||
"Import Books": "புத்தகங்களை இறக்குமதி செய்யவும்",
|
||||
"Select Multiple Books": "பல புத்தகங்களை தேர்வு செய்யவும்",
|
||||
"Select Books": "புத்தகங்களை தேர்வு செய்யவும்",
|
||||
"Deselect": "தேர்வுநீக்கவும்",
|
||||
"Select All": "அனைத்தையும் தேர்வு செய்யவும்",
|
||||
@@ -324,9 +323,9 @@
|
||||
"Match Whole Words": "முழு வார்த்தைகள் பொருத்தம்",
|
||||
"Match Diacritics": "உச்சரிப்பு குறிகள் பொருத்தம்",
|
||||
"TOC": "உள்ளடக்கம்",
|
||||
"Table of Contents": "உள்ளடக்கம்",
|
||||
"Sidebar": "பக்கப்பட்டி",
|
||||
"Disable Translation": "மொழிபெயர்ப்பை முடக்கவும்",
|
||||
"Translation Not Available": "மொழிபெயர்ப்பு கிடைக்கவில்லை",
|
||||
"TTS not supported for PDF": "PDF க்கு TTS ஆதரிக்கப்படவில்லை",
|
||||
"TTS not supported for this document": "இந்த ஆவணத்திற்கு TTS ஆதரிக்கப்படவில்லை",
|
||||
"No Timeout": "நேர வரம்பு இல்லை",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "இயக்கு",
|
||||
"Next Sentence": "அடுத்த வாக்கியம்",
|
||||
"Next Paragraph": "அடுத்த பத்தி",
|
||||
"Separate Cover Page": "தனித்த அட்டை பக்கம்"
|
||||
"Separate Cover Page": "தனித்த அட்டை பக்கம்",
|
||||
"Resize Notebook": "குறிப்பேட்டை மீட்டமைக்கவும்",
|
||||
"Resize Sidebar": "பக்கப்பட்டியை மீட்டமைக்கவும்",
|
||||
"Get Help from the Readest Community": "Readest சமூகத்திலிருந்து உதவி பெறவும்",
|
||||
"Remove cover image": "அட்டை படத்தை நீக்கவும்",
|
||||
"Bookshelf": "புத்தக அலமாரி",
|
||||
"View Menu": "மெனுவை பார்க்கவும்",
|
||||
"Settings Menu": "அமைப்புகள் மெனு",
|
||||
"View account details and quota": "கணக்கு விவரங்கள் மற்றும் ஒதுக்கீட்டை பார்க்கவும்",
|
||||
"Library Header": "நூலக தலைப்பு",
|
||||
"Book Content": "புத்தக உள்ளடக்கம்",
|
||||
"Footer Bar": "அடிக்குறிப்பு பட்டை",
|
||||
"Header Bar": "தலைப்பு பட்டை",
|
||||
"View Options": "காணும் விருப்பங்கள்",
|
||||
"Book Menu": "புத்தக மெனு",
|
||||
"Search Options": "தேடல் விருப்பங்கள்",
|
||||
"Close": "மூடு",
|
||||
"Delete Book Options": "புத்தகத்தை நீக்கும் விருப்பங்கள்",
|
||||
"ON": "செயல்படுத்தவும்",
|
||||
"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": "அதிகரிக்கவும் அல்லது மீட்டமைக்கவும்",
|
||||
"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": "உரை வாசிப்பு"
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"Search Books...": "ค้นหาหนังสือ...",
|
||||
"Clear Search": "ล้างการค้นหา",
|
||||
"Import Books": "นำเข้าหนังสือ",
|
||||
"Select Multiple Books": "เลือกหลายเล่ม",
|
||||
"Select Books": "เลือกหนังสือ",
|
||||
"Deselect": "ยกเลิกเลือก",
|
||||
"Select All": "เลือกทั้งหมด",
|
||||
@@ -274,9 +273,9 @@
|
||||
"Match Whole Words": "ตรงทั้งคำ",
|
||||
"Match Diacritics": "ตรงตามเครื่องหมายวรรณยุกต์",
|
||||
"TOC": "สารบัญ",
|
||||
"Table of Contents": "สารบัญ",
|
||||
"Sidebar": "แถบข้าง",
|
||||
"Disable Translation": "ปิดการแปล",
|
||||
"Translation Not Available": "ไม่มีการแปล",
|
||||
"TTS not supported for PDF": "ไม่รองรับ TTS สำหรับ PDF",
|
||||
"No Timeout": "ไม่จำกัดเวลา",
|
||||
"{{value}} minute": "{{value}} นาที",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "เล่น",
|
||||
"Next Sentence": "ประโยคถัดไป",
|
||||
"Next Paragraph": "ย่อหน้าถัดไป",
|
||||
"Separate Cover Page": "หน้าปกแยกต่างหาก"
|
||||
"Separate Cover Page": "หน้าปกแยกต่างหาก",
|
||||
"Resize Notebook": "ปรับขนาดสมุดบันทึก",
|
||||
"Resize Sidebar": "ปรับขนาดแถบข้าง",
|
||||
"Get Help from the Readest Community": "รับความช่วยเหลือจากชุมชน Readest",
|
||||
"Remove cover image": "ลบรูปปก",
|
||||
"Bookshelf": "ชั้นวางหนังสือ",
|
||||
"View Menu": "ดูเมนู",
|
||||
"Settings Menu": "เมนูการตั้งค่า",
|
||||
"View account details and quota": "ดูรายละเอียดบัญชีและโควต้า",
|
||||
"Library Header": "ส่วนหัวของห้องสมุด",
|
||||
"Book Content": "เนื้อหาหนังสือ",
|
||||
"Footer Bar": "แถบด้านล่าง",
|
||||
"Header Bar": "แถบส่วนหัว",
|
||||
"View Options": "ตัวเลือกการดู",
|
||||
"Book Menu": "เมนูหนังสือ",
|
||||
"Search Options": "ตัวเลือกการค้นหา",
|
||||
"Close": "ปิด",
|
||||
"Delete Book Options": "ตัวเลือกการลบหนังสือ",
|
||||
"ON": "เปิด",
|
||||
"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": "ขยายหรือคืนค่า",
|
||||
"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": "อ่านออกเสียง"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Ara...",
|
||||
"Select Book": "Kitap Seç",
|
||||
"Select Books": "Kitapları seç",
|
||||
"Select Multiple Books": "Birden fazla kitap seç",
|
||||
"Sepia": "Sepya",
|
||||
"Serif Font": "Serif Yazı Tipi",
|
||||
"Show Book Details": "Kitap Detaylarını Göster",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Kitap Detayları",
|
||||
"From Local File": "Yerel dosyadan",
|
||||
"TOC": "İçindekiler",
|
||||
"Table of Contents": "İçindekiler",
|
||||
"Book uploaded: {{title}}": "Kitap yüklendi: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Kitap yüklenemedi: {{title}}",
|
||||
"Book downloaded: {{title}}": "Kitap indirildi: {{title}}",
|
||||
@@ -333,7 +333,6 @@
|
||||
"Left Margin (px)": "Sol Kenar Boşluğu (px)",
|
||||
"Column Gap (%)": "Sütun Aralığı (%)",
|
||||
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster",
|
||||
"Translation Not Available": "Çeviri Mevcut Değil",
|
||||
"Custom Content CSS": "Özel İçerik CSS'si",
|
||||
"Enter CSS for book content styling...": "Kitap içeriği stili için CSS girin...",
|
||||
"Custom Reader UI CSS": "Özel Okuyucu Arayüzü CSS'si",
|
||||
@@ -522,5 +521,83 @@
|
||||
"Play": "Oynat",
|
||||
"Next Sentence": "Sonraki Cümle",
|
||||
"Next Paragraph": "Sonraki Paragraf",
|
||||
"Separate Cover Page": "Ayrı Kapak Sayfası"
|
||||
"Separate Cover Page": "Ayrı Kapak Sayfası",
|
||||
"Resize Notebook": "Defteri Yeniden Boyutlandır",
|
||||
"Resize Sidebar": "Kenar Çubuğunu Yeniden Boyutlandır",
|
||||
"Get Help from the Readest Community": "Readest Topluluğundan Yardım Alın",
|
||||
"Remove cover image": "Kapak resmini kaldır",
|
||||
"Bookshelf": "Kitaplık",
|
||||
"View Menu": "Menüyü Görüntüle",
|
||||
"Settings Menu": "Ayarlar Menüsü",
|
||||
"View account details and quota": "Hesap detaylarını ve kotayı görüntüle",
|
||||
"Library Header": "Kütüphane Başlığı",
|
||||
"Book Content": "Kitap İçeriği",
|
||||
"Footer Bar": "Alt Çubuk",
|
||||
"Header Bar": "Üst Çubuk",
|
||||
"View Options": "Görünüm Seçenekleri",
|
||||
"Book Menu": "Kitap Menüsü",
|
||||
"Search Options": "Arama Seçenekleri",
|
||||
"Close": "Kapat",
|
||||
"Delete Book Options": "Kitap Silme Seçenekleri",
|
||||
"ON": "AÇIK",
|
||||
"OFF": "KAPALI",
|
||||
"Reading Progress": "Okuma İlerlemesi",
|
||||
"Page Margin": "Sayfa Kenar Boşluğu",
|
||||
"Remove Bookmark": "Yer İşareti Kaldır",
|
||||
"Add Bookmark": "Yer İşareti Ekle",
|
||||
"Books Content": "Kitap İçeriği",
|
||||
"Jump to Location": "Konuma Atlama",
|
||||
"Unpin Notebook": "Defteri Sabitlemeyi Kaldır",
|
||||
"Pin Notebook": "Defteri Sabitle",
|
||||
"Hide Search Bar": "Arama Çubuğunu Gizle",
|
||||
"Show Search Bar": "Arama Çubuğunu Göster",
|
||||
"On {{current}} of {{total}} page": "{{current}} / {{total}} sayfada",
|
||||
"Section Title": "Bölüm Başlığı",
|
||||
"Decrease": "Azalt",
|
||||
"Increase": "Artır",
|
||||
"Settings Panels": "Ayarlar Panelleri",
|
||||
"Settings": "Ayarlar",
|
||||
"Unpin Sidebar": "Kenar Çubuğunu Sabitlemeyi Kaldır",
|
||||
"Pin Sidebar": "Kenar Çubuğunu Sabitle",
|
||||
"Toggle Sidebar": "Kenar Çubuğunu Değiştir",
|
||||
"Toggle Translation": "Çeviriyi Değiştir",
|
||||
"Translation Disabled": "Çeviri Devre Dışı",
|
||||
"Minimize": "Küçült",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
"Fast": "Швидко",
|
||||
"Font": "Шрифт",
|
||||
"Font & Layout": "Шрифт та макет",
|
||||
"Font Face": "Накреслення шрифту",
|
||||
"Font Face": "Налаштування шрифту",
|
||||
"Font Family": "Сімейство шрифтів",
|
||||
"Font Size": "Розмір шрифту",
|
||||
"Full Justification": "Повне вирівнювання",
|
||||
"Global Settings": "Глобальні налаштування",
|
||||
"Global Settings": "Ґлобальні налаштування",
|
||||
"Go Back": "Назад",
|
||||
"Go Forward": "Вперед",
|
||||
"Grass": "Трав'яний",
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Пошук...",
|
||||
"Select Book": "Вибрати книгу",
|
||||
"Select Books": "Вибрати книги",
|
||||
"Select Multiple Books": "Вибрати кілька книг",
|
||||
"Sepia": "Сепія",
|
||||
"Serif Font": "Шрифт із засічками",
|
||||
"Show Book Details": "Показати деталі книги",
|
||||
@@ -114,10 +113,11 @@
|
||||
"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": "Зміст",
|
||||
"Table of Contents": "Зміст",
|
||||
"Book uploaded: {{title}}": "Книгу завантажено: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Не вдалося завантажити книгу: {{title}}",
|
||||
"Book downloaded: {{title}}": "Книгу завантажено: {{title}}",
|
||||
@@ -237,7 +237,7 @@
|
||||
"Header & Footer": "Заголовок і нижній колонтитул",
|
||||
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
|
||||
"A new version of Readest is available!": "Доступна нова версія Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлено {{currentVersion}}).",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Нова версія Readest {{newVersion}} доступна. (Поточна {{currentVersion}}).",
|
||||
"Download and install now?": "Завантажити та встановити зараз?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Завантаженно {{downloaded}} з {{contentLength}}",
|
||||
"Download finished": "Завантаження завершено",
|
||||
@@ -339,7 +339,6 @@
|
||||
"Left Margin (px)": "Лівий відступ (px)",
|
||||
"Column Gap (%)": "Проміжок між стовпцями (%)",
|
||||
"Always Show Status Bar": "Завжди показувати панель стану",
|
||||
"Translation Not Available": "Переклад недоступний",
|
||||
"Custom Content CSS": "Користуацький CSS вмісту",
|
||||
"Enter CSS for book content styling...": "Введіть CSS для стилізації вмісту книги...",
|
||||
"Custom Reader UI CSS": "CSS інтерфейсу",
|
||||
@@ -490,7 +489,7 @@
|
||||
"File Content (recommended)": "Вміст файлу (рекомендується)",
|
||||
"File Name": "Ім'я файлу",
|
||||
"Device Name": "Ім'я пристрою",
|
||||
"Connect to your KOReader Sync server.": "Підключіться до свого сервера синхронізації KOReader.",
|
||||
"Connect to your KOReader Sync server.": "Під'єдайте свій сервер синхронізації KOReader.",
|
||||
"Server URL": "URL сервера",
|
||||
"Username": "Ім'я користувача",
|
||||
"Your Username": "Ваше ім'я користувача",
|
||||
@@ -500,8 +499,8 @@
|
||||
"Sync Conflict": "Конфлікт синхронізації",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "Синхронізувати прогрес читання із \"{{deviceName}}\"?",
|
||||
"another device": "інший пристрій",
|
||||
"Local Progress": "Локальний прогрес",
|
||||
"Remote Progress": "Прогрес у хмарі",
|
||||
"Local Progress": "Локальний проґрес",
|
||||
"Remote Progress": "Проґрес у хмарі",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "Сторінка {{page}} з {{total}} ({{percentage}}%)",
|
||||
"Current position": "Поточна позиція",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Приблизно сторінка {{page}} із {{total}} ({{percentage}}%)",
|
||||
@@ -509,26 +508,104 @@
|
||||
"Failed to connect": "Не вдалося під'єднатися",
|
||||
"Sync Server Connected": "Сервер синхронізації під'єднано",
|
||||
"Sync as {{userDisplayName}}": "Синхронізувати як {{userDisplayName}}",
|
||||
"Custom Fonts": "Користувацькі Шрифти",
|
||||
"Cancel Delete": "Скасувати Видалення",
|
||||
"Import Font": "Імпортувати Шрифт",
|
||||
"Delete Font": "Видалити Шрифт",
|
||||
"Custom Fonts": "Користувацькі шрифти",
|
||||
"Cancel Delete": "Скасувати видалення",
|
||||
"Import Font": "Імпортувати шрифт",
|
||||
"Delete Font": "Видалити шрифт",
|
||||
"Tips": "Поради",
|
||||
"Custom fonts can be selected from the Font Face menu": "Користувацькі шрифти можна вибрати з меню Шрифт",
|
||||
"Manage Custom Fonts": "Керування Користувацькими Шрифтами",
|
||||
"Select Files": "Вибрати Файли",
|
||||
"Select Image": "Вибрати Зображення",
|
||||
"Select Video": "Вибрати Відео",
|
||||
"Select Audio": "Вибрати Аудіо",
|
||||
"Select Fonts": "Вибрати Шрифти",
|
||||
"Custom fonts can be selected from the Font Face menu": "Користувацькі шрифти можна вибрати з меню \"Налаштування шрифту\"",
|
||||
"Manage Custom Fonts": "Керування користувацькими шрифтами",
|
||||
"Select Files": "Вибрати файли",
|
||||
"Select Image": "Вибрати зображення",
|
||||
"Select Video": "Вибрати відео",
|
||||
"Select Audio": "Вибрати аудіо",
|
||||
"Select Fonts": "Вибрати шифти",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Підтримувані формати шрифтів: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Надіслати прогрес",
|
||||
"Pull Progress": "Отримати прогрес",
|
||||
"Push Progress": "Надіслати проґрес",
|
||||
"Pull Progress": "Отримати проґрес",
|
||||
"Previous Paragraph": "Попередній абзац",
|
||||
"Previous Sentence": "Попереднє речення",
|
||||
"Pause": "Пауза",
|
||||
"Play": "Відтворити",
|
||||
"Next Sentence": "Наступне речення",
|
||||
"Next Paragraph": "Наступний абзац",
|
||||
"Separate Cover Page": "Окрема сторінка обкладинки"
|
||||
"Separate Cover Page": "Окрема сторінка обкладинки",
|
||||
"Resize Notebook": "Змінити розмір нотаток",
|
||||
"Resize Sidebar": "Змінити розмір бічної панелі",
|
||||
"Get Help from the Readest Community": "Отримати допомогу від спільноти Readest",
|
||||
"Remove cover image": "Видалити зображення обкладинки",
|
||||
"Bookshelf": "Книжкова полиця",
|
||||
"View Menu": "Переглянути меню",
|
||||
"Settings Menu": "Меню налаштувань",
|
||||
"View account details and quota": "Переглянути деталі акаунта та квоту",
|
||||
"Library Header": "Заголовок бібліотеки",
|
||||
"Book Content": "Зміст книги",
|
||||
"Footer Bar": "Нижня панель",
|
||||
"Header Bar": "Верхня панель",
|
||||
"View Options": "Параметри перегляду",
|
||||
"Book Menu": "Меню книги",
|
||||
"Search Options": "Параметри пошуку",
|
||||
"Close": "Закрити",
|
||||
"Delete Book Options": "Параметри видалення книги",
|
||||
"ON": "УВІМКНЕННО",
|
||||
"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": "Збільшити або відновити",
|
||||
"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": "Читати вголос"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "Tìm kiếm...",
|
||||
"Select Book": "Chọn sách",
|
||||
"Select Books": "Chọn sách",
|
||||
"Select Multiple Books": "Chọn nhiều sách",
|
||||
"Sepia": "Nâu cổ",
|
||||
"Serif Font": "Phông chữ có chân",
|
||||
"Show Book Details": "Hiển thị chi tiết sách",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "Chi tiết sách",
|
||||
"From Local File": "Từ tệp địa phương",
|
||||
"TOC": "Mục lục",
|
||||
"Table of Contents": "Mục lục",
|
||||
"Book uploaded: {{title}}": "Sách đã tải lên: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Không thể tải lên sách: {{title}}",
|
||||
"Book downloaded: {{title}}": "Sách đã tải về: {{title}}",
|
||||
@@ -330,7 +330,6 @@
|
||||
"Left Margin (px)": "Lề trái (px)",
|
||||
"Column Gap (%)": "Khoảng cách cột (%)",
|
||||
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái",
|
||||
"Translation Not Available": "Không có bản dịch",
|
||||
"Custom Content CSS": "CSS nội dung",
|
||||
"Enter CSS for book content styling...": "Nhập CSS cho phần nội dung sách...",
|
||||
"Custom Reader UI CSS": "CSS giao diện đọc",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "Phát",
|
||||
"Next Sentence": "Câu tiếp theo",
|
||||
"Next Paragraph": "Đoạn tiếp theo",
|
||||
"Separate Cover Page": "Trang Bìa Riêng"
|
||||
"Separate Cover Page": "Trang Bìa Riêng",
|
||||
"Resize Notebook": "Thay Đổi Kích Thước Sổ Ghi Chép",
|
||||
"Resize Sidebar": "Thay Đổi Kích Thước Thanh Bên",
|
||||
"Get Help from the Readest Community": "Nhận Trợ Giúp từ Cộng Đồng Readest",
|
||||
"Remove cover image": "Xóa ảnh bìa",
|
||||
"Bookshelf": "Kệ sách",
|
||||
"View Menu": "Xem menu",
|
||||
"Settings Menu": "Menu cài đặt",
|
||||
"View account details and quota": "Xem chi tiết tài khoản và hạn mức",
|
||||
"Library Header": "Tiêu đề thư viện",
|
||||
"Book Content": "Nội dung sách",
|
||||
"Footer Bar": "Thanh chân trang",
|
||||
"Header Bar": "Thanh đầu trang",
|
||||
"View Options": "Tùy chọn xem",
|
||||
"Book Menu": "Menu sách",
|
||||
"Search Options": "Tùy chọn tìm kiếm",
|
||||
"Close": "Đóng",
|
||||
"Delete Book Options": "Tùy chọn xóa sách",
|
||||
"ON": "BẬT",
|
||||
"OFF": "TẮT",
|
||||
"Reading Progress": "Tiến độ đọc",
|
||||
"Page Margin": "Lề trang",
|
||||
"Remove Bookmark": "Xóa đánh dấu",
|
||||
"Add Bookmark": "Thêm đánh dấu",
|
||||
"Books Content": "Nội dung sách",
|
||||
"Jump to Location": "Nhảy đến vị trí",
|
||||
"Unpin Notebook": "Bỏ ghim sổ tay",
|
||||
"Pin Notebook": "Ghim sổ tay",
|
||||
"Hide Search Bar": "Ẩn thanh tìm kiếm",
|
||||
"Show Search Bar": "Hiện thanh tìm kiếm",
|
||||
"On {{current}} of {{total}} page": "Trên {{current}} của {{total}} trang",
|
||||
"Section Title": "Tiêu đề phần",
|
||||
"Decrease": "Giảm",
|
||||
"Increase": "Tăng",
|
||||
"Settings Panels": "Các bảng điều khiển cài đặt",
|
||||
"Settings": "Cài đặt",
|
||||
"Unpin Sidebar": "Bỏ ghim thanh bên",
|
||||
"Pin Sidebar": "Ghim thanh bên",
|
||||
"Toggle Sidebar": "Chuyển đổi thanh bên",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "搜索...",
|
||||
"Select Book": "选择书籍",
|
||||
"Select Books": "选择书籍",
|
||||
"Select Multiple Books": "选择多本书籍",
|
||||
"Sepia": "旧韵",
|
||||
"Serif Font": "衬线字体",
|
||||
"Show Book Details": "显示书籍详情",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "书籍详情",
|
||||
"From Local File": "从本地文件导入",
|
||||
"TOC": "目录",
|
||||
"Table of Contents": "目录",
|
||||
"Book uploaded: {{title}}": "书籍已上传:{{title}}",
|
||||
"Failed to upload book: {{title}}": "上传书籍失败:{{title}}",
|
||||
"Book downloaded: {{title}}": "书籍已下载:{{title}}",
|
||||
@@ -330,7 +330,6 @@
|
||||
"Left Margin (px)": "左边距",
|
||||
"Column Gap (%)": "列间距",
|
||||
"Always Show Status Bar": "始终显示状态栏",
|
||||
"Translation Not Available": "翻译不可用",
|
||||
"Custom Content CSS": "自定义内容 CSS",
|
||||
"Enter CSS for book content styling...": "输入书籍内容样式的CSS…",
|
||||
"Custom Reader UI CSS": "自定义界面 CSS",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "播放",
|
||||
"Next Sentence": "下一句",
|
||||
"Next Paragraph": "下一段",
|
||||
"Separate Cover Page": "单独封面页"
|
||||
"Separate Cover Page": "单独封面页",
|
||||
"Resize Notebook": "调整笔记本大小",
|
||||
"Resize Sidebar": "调整侧边栏大小",
|
||||
"Get Help from the Readest Community": "从 Readest 社区获取帮助",
|
||||
"Remove cover image": "移除封面图片",
|
||||
"Bookshelf": "书架",
|
||||
"View Menu": "查看菜单",
|
||||
"Settings Menu": "设置菜单",
|
||||
"View account details and quota": "查看账户详情和配额",
|
||||
"Library Header": "图书馆标题",
|
||||
"Book Content": "书籍内容",
|
||||
"Footer Bar": "底部栏",
|
||||
"Header Bar": "顶部栏",
|
||||
"View Options": "查看选项",
|
||||
"Book Menu": "书籍菜单",
|
||||
"Search Options": "搜索选项",
|
||||
"Close": "关闭",
|
||||
"Delete Book Options": "删除书籍选项",
|
||||
"ON": "开启",
|
||||
"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": "最大化或还原",
|
||||
"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": "语音朗读"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Search...": "搜索...",
|
||||
"Select Book": "選擇書籍",
|
||||
"Select Books": "選擇書籍",
|
||||
"Select Multiple Books": "選擇多本書籍",
|
||||
"Sepia": "舊韻",
|
||||
"Serif Font": "襯線字體",
|
||||
"Show Book Details": "顯示書籍詳情",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Book Details": "書籍詳情",
|
||||
"From Local File": "從本地文件導入",
|
||||
"TOC": "目錄",
|
||||
"Table of Contents": "目錄",
|
||||
"Book uploaded: {{title}}": "書籍已上傳:{{title}}",
|
||||
"Failed to upload book: {{title}}": "書籍上傳失敗:{{title}}",
|
||||
"Book downloaded: {{title}}": "書籍已下載:{{title}}",
|
||||
@@ -330,7 +330,6 @@
|
||||
"Left Margin (px)": "左邊距",
|
||||
"Column Gap (%)": "欄間距",
|
||||
"Always Show Status Bar": "始終顯示狀態欄",
|
||||
"Translation Not Available": "翻譯不可用",
|
||||
"Custom Content CSS": "自訂內容 CSS",
|
||||
"Enter CSS for book content styling...": "輸入書籍內容樣式的CSS…",
|
||||
"Custom Reader UI CSS": "自訂介面 CSS",
|
||||
@@ -518,5 +517,83 @@
|
||||
"Play": "播放",
|
||||
"Next Sentence": "下一句",
|
||||
"Next Paragraph": "下一段",
|
||||
"Separate Cover Page": "單獨封面頁"
|
||||
"Separate Cover Page": "單獨封面頁",
|
||||
"Resize Notebook": "調整筆記本大小",
|
||||
"Resize Sidebar": "調整側邊欄大小",
|
||||
"Get Help from the Readest Community": "從 Readest 社群獲取幫助",
|
||||
"Remove cover image": "移除封面圖片",
|
||||
"Bookshelf": "書架",
|
||||
"View Menu": "檢視選單",
|
||||
"Settings Menu": "設定選單",
|
||||
"View account details and quota": "檢視帳戶詳細資料與配額",
|
||||
"Library Header": "圖書館標題",
|
||||
"Book Content": "書籍內容",
|
||||
"Footer Bar": "頁腳欄",
|
||||
"Header Bar": "標頭欄",
|
||||
"View Options": "檢視選項",
|
||||
"Book Menu": "書籍選單",
|
||||
"Search Options": "搜尋選項",
|
||||
"Close": "關閉",
|
||||
"Delete Book Options": "刪除書籍選項",
|
||||
"ON": "開啟",
|
||||
"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": "最大化或還原",
|
||||
"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": "朗讀"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
{
|
||||
"releases": {
|
||||
"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": [
|
||||
"Accessibility: Added support for popular screen readers on all platforms, making books accessible to readers with visual impairments",
|
||||
"Sync: Progress is now synced across different versions of the same book by aggregated metadata",
|
||||
"Library: Groups are now displayed in list view for easier organization",
|
||||
"Library: Added delete confirmation when removing books from the context menu",
|
||||
"Fonts: Added support for fonts with subfamilies when importing TTF/OTF font files",
|
||||
"Layout: Prevented layout shift in Android when the virtual keyboard appears"
|
||||
]
|
||||
},
|
||||
"0.9.78": {
|
||||
"date": "2025-09-06",
|
||||
"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,12 @@
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="com.readest.native_tts.MediaForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
+2
-2
@@ -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")
|
||||
|
||||
+62
-1
@@ -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
|
||||
@@ -67,7 +74,11 @@ interface KeyDownInterceptor {
|
||||
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 +86,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 +414,53 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
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,9 @@ const COMMANDS: &[&str] = &[
|
||||
"iap_restore_purchases",
|
||||
"get_system_color_scheme",
|
||||
"get_safe_area_insets",
|
||||
"request_manage_storage_permission",
|
||||
"checkPermissions",
|
||||
"requestPermissions",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
+13
@@ -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"]
|
||||
+13
@@ -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"]
|
||||
+13
@@ -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"]
|
||||
+81
@@ -20,6 +20,9 @@ Default permissions for the plugin
|
||||
- `allow-iap-restore-purchases`
|
||||
- `allow-get-system-color-scheme`
|
||||
- `allow-get-safe-area-insets`
|
||||
- `allow-request-manage-storage-permission`
|
||||
- `allow-checkPermissions`
|
||||
- `allow-requestPermissions`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -85,6 +88,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>
|
||||
@@ -397,6 +426,58 @@ 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-system-ui-visibility`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -17,4 +17,7 @@ permissions = [
|
||||
"allow-iap-restore-purchases",
|
||||
"allow-get-system-color-scheme",
|
||||
"allow-get-safe-area-insets",
|
||||
"allow-request-manage-storage-permission",
|
||||
"allow-checkPermissions",
|
||||
"allow-requestPermissions",
|
||||
]
|
||||
|
||||
+38
-2
@@ -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",
|
||||
@@ -462,6 +474,30 @@
|
||||
"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_system_ui_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -487,10 +523,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-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-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -126,3 +126,10 @@ pub(crate) async fn get_safe_area_insets<R: Runtime>(
|
||||
) -> Result<GetSafeAreaInsetsResponse> {
|
||||
app.native_bridge().get_safe_area_insets()
|
||||
}
|
||||
|
||||
#[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,10 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_safe_area_insets(&self) -> crate::Result<GetSafeAreaInsetsResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::iap_restore_purchases,
|
||||
commands::get_system_color_scheme,
|
||||
commands::get_safe_area_insets,
|
||||
commands::request_manage_storage_permission,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -169,3 +169,13 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
.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,9 @@ pub struct GetSafeAreaInsetsResponse {
|
||||
pub left: f64,
|
||||
pub right: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestManageStoragePermissionResponse {
|
||||
pub manage_storage: String, // "granted", "denied", or "prompt"
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("androidx.media3:media3-common:1.2.0")
|
||||
implementation("androidx.media3:media3-session:1.2.0")
|
||||
implementation("androidx.media3:media3-exoplayer:1.2.0")
|
||||
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")
|
||||
|
||||
+379
-7
@@ -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,18 @@ 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.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
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 +36,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 +81,117 @@ 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: Double? = null
|
||||
var duration: Double? = null
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
class MediaForegroundService : Service() {
|
||||
companion object {
|
||||
const val NOTIFICATION_ID = 1001
|
||||
const val CHANNEL_ID = "tts_media_playback_channel"
|
||||
const val ACTION_START = "START_FOREGROUND_SERVICE"
|
||||
const val ACTION_STOP = "STOP_FOREGROUND_SERVICE"
|
||||
|
||||
var pluginInstance: NativeTTSPlugin? = null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_START -> {
|
||||
startForegroundService()
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
stopForegroundService()
|
||||
}
|
||||
}
|
||||
return START_STICKY // Restart if killed
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
NativeTTSPlugin.NOTIFICATION_TITLE,
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
).apply {
|
||||
description = NativeTTSPlugin.NOTIFICATION_TEXT
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(NativeTTSPlugin.FOREGROUND_SERVICE_TITLE)
|
||||
.setContentText(NativeTTSPlugin.FOREGROUND_SERVICE_TEXT)
|
||||
.setSmallIcon(android.R.drawable.ic_media_play)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
Log.d("MediaForegroundService", "Foreground service started")
|
||||
}
|
||||
|
||||
private fun stopForegroundService() {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
Log.d("MediaForegroundService", "Foreground service stopped")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d("MediaForegroundService", "Service destroyed")
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -78,7 +204,16 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
|
||||
private val speakingJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
private var player: Player? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
private var isMediaSessionActive = false
|
||||
private var isForegroundServiceRunning = false
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
init {
|
||||
MediaForegroundService.pluginInstance = this
|
||||
}
|
||||
|
||||
@Command
|
||||
fun init(invoke: Invoke) {
|
||||
@@ -373,13 +508,250 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
invoke.reject("Exception getting voices: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private class WebViewPlayer(player: Player) : ForwardingPlayer(player) {
|
||||
var eventTrigger: ((String, JSObject) -> Unit)? = null
|
||||
|
||||
override fun play() {
|
||||
eventTrigger?.invoke("media-session-play", JSObject())
|
||||
if (!this.playWhenReady) {
|
||||
this.playWhenReady = true
|
||||
} else {
|
||||
this.playWhenReady = false
|
||||
}
|
||||
}
|
||||
override fun pause() {
|
||||
eventTrigger?.invoke("media-session-pause", JSObject())
|
||||
if (!this.playWhenReady) {
|
||||
this.playWhenReady = true
|
||||
} else {
|
||||
this.playWhenReady = false
|
||||
}
|
||||
}
|
||||
override fun seekToNext() {
|
||||
eventTrigger?.invoke("media-session-next", JSObject())
|
||||
}
|
||||
override fun seekToPrevious() {
|
||||
eventTrigger?.invoke("media-session-previous", JSObject())
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeMediaSession() {
|
||||
var basePlayer = ExoPlayer.Builder(activity).build()
|
||||
val webViewPlayer = WebViewPlayer(basePlayer)
|
||||
webViewPlayer.eventTrigger = { event, data -> trigger(event, data) }
|
||||
player = webViewPlayer
|
||||
|
||||
mediaSession = MediaSession.Builder(activity, player!!)
|
||||
.setCallback(object : MediaSession.Callback {
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val builder = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
builder.setAvailablePlayerCommands(
|
||||
Player.Commands.Builder()
|
||||
.add(Player.COMMAND_PLAY_PAUSE)
|
||||
.add(Player.COMMAND_SEEK_TO_NEXT)
|
||||
.add(Player.COMMAND_SEEK_TO_PREVIOUS)
|
||||
.build()
|
||||
)
|
||||
return builder.build()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun deinitializeMediaSession() {
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
player?.release()
|
||||
player = null
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
try {
|
||||
val intent = Intent(activity, MediaForegroundService::class.java).apply {
|
||||
action = MediaForegroundService.ACTION_START
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intent)
|
||||
} else {
|
||||
activity.startService(intent)
|
||||
}
|
||||
Log.d(TAG, "Foreground service started")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start foreground service", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundService() {
|
||||
try {
|
||||
val intent = Intent(activity, MediaForegroundService::class.java).apply {
|
||||
action = MediaForegroundService.ACTION_STOP
|
||||
}
|
||||
activity.stopService(intent)
|
||||
Log.d(TAG, "Foreground service stopped")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to stop foreground service", e)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentArtworkByteArray: ByteArray? = 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 ?: ""
|
||||
val artworkUrl = args.artwork ?: ""
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val metadataBuilder = MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setAlbumTitle(album)
|
||||
.apply {
|
||||
if (artworkUrl.isNotEmpty()) {
|
||||
loadArtworkFromUrl(artworkUrl)?.let { bitmap ->
|
||||
java.io.ByteArrayOutputStream().use { stream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
currentArtworkByteArray = stream.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
currentArtworkByteArray?.let {
|
||||
setArtworkData(it, MediaMetadata.PICTURE_TYPE_FRONT_COVER)
|
||||
}
|
||||
}
|
||||
|
||||
val mediaItem: MediaItem = MediaItem.Builder()
|
||||
.setMediaId("silent_media")
|
||||
.setUri("data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQQAAAAAAA==")
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build()
|
||||
|
||||
player?.setMediaItems(listOf(mediaItem, mediaItem))
|
||||
player?.prepare()
|
||||
player?.playWhenReady = true
|
||||
player?.setRepeatMode(Player.REPEAT_MODE_ALL)
|
||||
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
Log.e("NativeBridgePlugin", "Failed to update metadata: ${e.message}")
|
||||
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.0
|
||||
|
||||
try {
|
||||
player?.let { player ->
|
||||
if (position > 0) {
|
||||
player.seekTo((position * 1000).toLong())
|
||||
}
|
||||
player.playWhenReady = isPlaying
|
||||
}
|
||||
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
|
||||
val keepAppInForeground = args.keepAppInForeground ?: false
|
||||
|
||||
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 {
|
||||
if (active && !isMediaSessionActive) {
|
||||
initializeMediaSession()
|
||||
if (keepAppInForeground) {
|
||||
startForegroundService()
|
||||
isForegroundServiceRunning = true
|
||||
}
|
||||
isMediaSessionActive = true
|
||||
Log.d(TAG, "Media session activated with foreground service")
|
||||
} else if (!active && isMediaSessionActive) {
|
||||
deinitializeMediaSession()
|
||||
if (isForegroundServiceRunning) {
|
||||
stopForegroundService()
|
||||
isForegroundServiceRunning = false
|
||||
}
|
||||
isMediaSessionActive = false
|
||||
Log.d(TAG, "Media session deactivated and foreground service stopped")
|
||||
}
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to set media session active state: ${e.message}")
|
||||
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 {
|
||||
if (isMediaSessionActive) {
|
||||
if (isForegroundServiceRunning) {
|
||||
stopForegroundService()
|
||||
isForegroundServiceRunning = false
|
||||
}
|
||||
isMediaSessionActive = false
|
||||
}
|
||||
|
||||
deinitializeMediaSession()
|
||||
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
eventChannels.values.forEach { it.close() }
|
||||
eventChannels.clear()
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
speakingJobs.clear()
|
||||
|
||||
MediaForegroundService.pluginInstance = null
|
||||
|
||||
Log.d(TAG, "Plugin destroyed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during plugin destruction", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
+13
@@ -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"]
|
||||
+13
@@ -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"]
|
||||
+13
@@ -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"]
|
||||
+13
@@ -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"]
|
||||
+13
@@ -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"]
|
||||
+135
@@ -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",
|
||||
]
|
||||
|
||||
+62
-2
@@ -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()
|
||||
@@ -268,7 +284,9 @@ pub fn run() {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.transparent(true);
|
||||
builder = builder
|
||||
.transparent(true)
|
||||
.background_color(tauri::window::Color(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
builder
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"$APPDATA/**/*",
|
||||
"$APPCACHE/**/*",
|
||||
"$TEMP/**/*",
|
||||
"**/Readest/**/*",
|
||||
"/private/var/mobile/Containers/Data/Application/**/*"
|
||||
],
|
||||
"deny": []
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function AuthPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { isDarkMode, safeAreaInsets } = useThemeStore();
|
||||
const { isDarkMode, safeAreaInsets, isRoundedWindow } = useThemeStore();
|
||||
const { isTrafficLightVisible } = useTrafficLightStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const [port, setPort] = useState<number | null>(null);
|
||||
@@ -344,10 +344,8 @@ 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',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
'bg-base-100 inset-0 flex h-[100vh] select-none flex-col items-center overflow-hidden',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -364,6 +362,7 @@ export default function AuthPage() {
|
||||
)}
|
||||
>
|
||||
<button
|
||||
aria-label={_('Go Back')}
|
||||
onClick={handleGoBack}
|
||||
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
|
||||
>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function ResetPasswordPage() {
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen items-center justify-center'>
|
||||
<div className='w-full max-w-md'>
|
||||
<div className='w-full max-w-md p-8'>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
view='update_password'
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -41,20 +41,9 @@ export const viewport = {
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
const themeMode = localStorage.getItem('themeMode');
|
||||
const themeColor = localStorage.getItem('themeColor');
|
||||
if (themeMode && themeColor) {
|
||||
document.documentElement.setAttribute('data-theme', \`\${themeColor}-\${themeMode}\`);
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<meta
|
||||
name='viewport'
|
||||
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover'
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Book } from '@/types/book';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLogin } from '@/utils/nav';
|
||||
@@ -40,24 +41,22 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
handleBookDownload,
|
||||
showBookDetailsModal,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { appService } = useEnv();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
const stopEvent = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role='none'
|
||||
className={clsx(
|
||||
'book-item flex',
|
||||
mode === 'grid' && 'h-full flex-col justify-end',
|
||||
mode === 'list' && 'h-28 flex-row gap-4 overflow-hidden',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -115,13 +114,12 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
<div className='flex items-center justify-center gap-x-2'>
|
||||
{!appService?.isMobile && (
|
||||
<button
|
||||
aria-label={_('Show Book Details')}
|
||||
className='show-detail-button -m-2 p-2 sm:opacity-0 sm:group-hover:opacity-100'
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => showBookDetailsModal(book)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={() => {
|
||||
showBookDetailsModal(book);
|
||||
}}
|
||||
>
|
||||
<div className='pt-[2px] sm:pt-[1px]'>
|
||||
<LiaInfoCircleSolid size={iconSize15} />
|
||||
@@ -146,11 +144,7 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
(!book.uploadedAt || (book.uploadedAt && !book.downloadedAt)) && (
|
||||
<button
|
||||
className='show-cloud-button -m-2 p-2'
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={() => {
|
||||
if (!user) {
|
||||
navigateToLogin(router);
|
||||
|
||||
@@ -9,17 +9,19 @@ import { Book, BooksGroup } from '@/types/book';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useAutoFocus } from '@/hooks/useAutoFocus';
|
||||
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 { eventDispatcher } from '@/utils/event';
|
||||
import { isMd5 } from '@/utils/md5';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import ModalPortal from '@/components/ModalPortal';
|
||||
import BookshelfItem, { generateGridItems, generateListItems } from './BookshelfItem';
|
||||
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
|
||||
import GroupingModal from './GroupingModal';
|
||||
|
||||
interface BookshelfProps {
|
||||
@@ -57,6 +59,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
|
||||
const [bookIdsToDelete, setBookIdsToDelete] = useState<string[]>([]);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
|
||||
const [showGroupingModal, setShowGroupingModal] = useState(false);
|
||||
const [queryTerm, setQueryTerm] = useState<string | null>(null);
|
||||
@@ -72,8 +75,9 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
|
||||
const allBookshelfItems =
|
||||
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
|
||||
const allBookshelfItems = generateBookshelfItems(libraryBooks);
|
||||
|
||||
const autofocusRef = useAutoFocus<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (isImportingBook.current) return;
|
||||
@@ -190,8 +194,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
|
||||
const getBooksToDelete = () => {
|
||||
const booksToDelete: Book[] = [];
|
||||
const selectedBooks = getSelectedBooks();
|
||||
selectedBooks.forEach((id) => {
|
||||
bookIdsToDelete.forEach((id) => {
|
||||
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
|
||||
if (book && !book.deletedAt) {
|
||||
booksToDelete.push(book);
|
||||
@@ -211,6 +214,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
};
|
||||
|
||||
const deleteSelectedBooks = () => {
|
||||
setBookIdsToDelete(getSelectedBooks());
|
||||
setShowSelectModeActions(false);
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
@@ -220,6 +224,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
setShowGroupingModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteBooksIntent = (event: CustomEvent) => {
|
||||
const { ids } = event.detail;
|
||||
setBookIdsToDelete(ids);
|
||||
setShowSelectModeActions(false);
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
|
||||
const bookFilter = (item: Book, queryTerm: string) => {
|
||||
if (item.deletedAt) return false;
|
||||
const searchTerm = new RegExp(queryTerm, 'i');
|
||||
@@ -296,17 +307,28 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSelectMode, isSelectAll, isSelectNone]);
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('delete-books', handleDeleteBooksIntent);
|
||||
return () => {
|
||||
eventDispatcher.off('delete-books', handleDeleteBooksIntent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const selectedBooks = getSelectedBooks();
|
||||
|
||||
return (
|
||||
<div className='bookshelf'>
|
||||
<div
|
||||
ref={autofocusRef}
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
'bookshelf-items transform-wrapper',
|
||||
'bookshelf-items transform-wrapper focus:outline-none',
|
||||
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
|
||||
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
|
||||
viewMode === 'list' && 'flex flex-col',
|
||||
)}
|
||||
role='main'
|
||||
aria-label={_('Bookshelf')}
|
||||
>
|
||||
{filteredBookshelfItems.map((item) => (
|
||||
<BookshelfItem
|
||||
@@ -331,15 +353,20 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
/>
|
||||
))}
|
||||
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
<div
|
||||
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',
|
||||
)}
|
||||
role='button'
|
||||
onClick={handleImportBooks}
|
||||
>
|
||||
<PiPlus className='size-10' color='gray' />
|
||||
<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>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLongPress } from '@/hooks/useLongPress';
|
||||
import { Menu, MenuItem } from '@tauri-apps/api/menu';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
@@ -20,7 +21,7 @@ import GroupItem from './GroupItem';
|
||||
|
||||
export type BookshelfItem = Book | BooksGroup;
|
||||
|
||||
export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
|
||||
if (book.deletedAt) return acc;
|
||||
book.groupId = book.groupId || BOOK_UNGROUPED_ID;
|
||||
@@ -49,10 +50,6 @@ export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
export const generateListItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
return books.filter((book) => !book.deletedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
export const generateGroupsList = (items: Book[]): BookGroupType[] => {
|
||||
return items
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
@@ -98,7 +95,6 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
toggleSelection,
|
||||
handleBookUpload,
|
||||
handleBookDownload,
|
||||
handleBookDelete,
|
||||
handleSetSelectMode,
|
||||
handleShowDetailsBook,
|
||||
}) => {
|
||||
@@ -216,7 +212,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const deleteBookMenuItem = await MenuItem.new({
|
||||
text: _('Delete'),
|
||||
action: async () => {
|
||||
await handleBookDelete(book);
|
||||
eventDispatcher.dispatch('delete-books', { ids: [book.hash] });
|
||||
},
|
||||
});
|
||||
const menu = await Menu.new();
|
||||
@@ -245,9 +241,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const deleteGroupMenuItem = await MenuItem.new({
|
||||
text: _('Delete'),
|
||||
action: async () => {
|
||||
for (const book of group.books) {
|
||||
await handleBookDelete(book);
|
||||
}
|
||||
eventDispatcher.dispatch('delete-books', { ids: [group.id] });
|
||||
},
|
||||
});
|
||||
const menu = await Menu.new();
|
||||
@@ -296,7 +290,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
groupContextMenuHandler(item as BooksGroup);
|
||||
}
|
||||
}, 100),
|
||||
[itemSelected],
|
||||
[itemSelected, settings.localBooksDir],
|
||||
);
|
||||
|
||||
const { pressing, handlers } = useLongPress(
|
||||
@@ -318,22 +312,37 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
[isSelectMode, handleSelectItem, handleOpenItem, handleContextMenu],
|
||||
);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleOpenItem();
|
||||
}
|
||||
if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {
|
||||
e.preventDefault();
|
||||
handleContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'group',
|
||||
'visible-focus-inset-2 group',
|
||||
mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
|
||||
appService?.isMobileApp && 'no-context-menu',
|
||||
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
|
||||
pressing && mode === 'grid' ? 'scale-95' : 'scale-100',
|
||||
)}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={'format' in item ? item.title : item.name}
|
||||
style={{
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
<div className='flex h-full flex-col justify-end'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
mode={mode}
|
||||
@@ -347,7 +356,12 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} groupSelected={itemSelected} />
|
||||
<GroupItem
|
||||
mode={mode}
|
||||
group={item}
|
||||
isSelectMode={isSelectMode}
|
||||
groupSelected={itemSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,195 @@
|
||||
import clsx from 'clsx';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { MdCheckCircle, MdCheckCircleOutline, MdChevronRight, MdChevronLeft } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { BooksGroup } from '@/types/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import BookCover from '@/components/BookCover';
|
||||
|
||||
interface GroupItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
group: BooksGroup;
|
||||
isSelectMode: boolean;
|
||||
groupSelected: boolean;
|
||||
}
|
||||
|
||||
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelected }) => {
|
||||
const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupSelected }) => {
|
||||
const { appService } = useEnv();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(false);
|
||||
|
||||
const checkScrollArrows = () => {
|
||||
if (mode === 'list' && scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const hasOverflow = container.scrollWidth > container.clientWidth;
|
||||
|
||||
if (hasOverflow) {
|
||||
const isAtStart = container.scrollLeft <= 5;
|
||||
const isAtEnd = container.scrollLeft >= container.scrollWidth - container.clientWidth - 5;
|
||||
setShowLeftArrow(!isAtStart);
|
||||
setShowRightArrow(!isAtEnd);
|
||||
} else {
|
||||
setShowLeftArrow(false);
|
||||
setShowRightArrow(false);
|
||||
}
|
||||
} else {
|
||||
setShowLeftArrow(false);
|
||||
setShowRightArrow(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkScrollArrows();
|
||||
if (mode === 'list' && scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
setTimeout(() => {
|
||||
container.style.transform = 'translateZ(0)';
|
||||
container.scrollLeft = 0;
|
||||
}, 0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mode, group.books.length, scrollContainerRef.current]);
|
||||
|
||||
const handleScroll = () => {
|
||||
checkScrollArrows();
|
||||
};
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const scrollAmount = container.clientWidth * 0.5;
|
||||
const currentScroll = container.scrollLeft;
|
||||
const targetScroll = Math.max(0, currentScroll - scrollAmount);
|
||||
container.scrollTo({ left: targetScroll, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const scrollRight = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const scrollAmount = container.clientWidth * 0.5;
|
||||
const currentScroll = container.scrollLeft;
|
||||
const maxScroll = container.scrollWidth - container.clientWidth;
|
||||
const targetScroll = Math.min(maxScroll, currentScroll + scrollAmount);
|
||||
container.scrollTo({ left: targetScroll, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const stopEvent = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleLeftArrowClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
scrollLeft();
|
||||
};
|
||||
|
||||
const handleRightArrowClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
scrollRight();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'group-item flex h-full flex-col justify-end',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
>
|
||||
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden p-2 shadow-md'>
|
||||
<div className='grid w-full grid-cols-2 grid-rows-2 gap-1 overflow-hidden'>
|
||||
{group.books.slice(0, 4).map((book) => (
|
||||
<div key={book.hash} className='relative aspect-[28/41] h-full w-full'>
|
||||
<BookCover book={book} isPreview />
|
||||
<div className={clsx('group-item', appService?.hasContextMenu ? 'cursor-pointer' : '')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex overflow-hidden',
|
||||
mode === 'grid' && 'bg-base-100 aspect-[28/41] items-center justify-center p-2 shadow-md',
|
||||
mode === 'list' && 'h-32 items-center justify-start gap-4 py-2',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
mode === 'grid' && 'flex h-full w-full',
|
||||
mode === 'list' && 'relative min-w-0 max-w-[85%]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={mode === 'list' ? scrollContainerRef : undefined}
|
||||
className={clsx(
|
||||
mode === 'grid' && 'grid w-full grid-cols-2 grid-rows-2 gap-1 overflow-hidden',
|
||||
mode === 'list' && 'flex h-28 gap-2 overflow-x-auto overflow-y-hidden',
|
||||
)}
|
||||
style={
|
||||
mode === 'list'
|
||||
? {
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'scroll-position',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onScroll={mode === 'list' ? handleScroll : undefined}
|
||||
>
|
||||
{group.books.slice(0, mode === 'grid' ? 4 : undefined).map((book) => (
|
||||
<div
|
||||
key={book.hash}
|
||||
className={clsx(
|
||||
'relative aspect-[28/41] h-full',
|
||||
mode === 'grid' && 'w-full',
|
||||
mode === 'list' && 'flex-shrink-0',
|
||||
)}
|
||||
>
|
||||
<BookCover book={book} isPreview />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{mode === 'list' && showLeftArrow && (
|
||||
<div className='absolute left-[-0.5px] top-0 h-full w-12'>
|
||||
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-r to-transparent'></div>
|
||||
<button
|
||||
onClick={handleLeftArrowClick}
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
className='absolute left-2 top-1/2 -translate-y-1/2 transition-all duration-200 hover:scale-110'
|
||||
>
|
||||
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-sm transition-colors duration-200'>
|
||||
<MdChevronLeft
|
||||
size={16}
|
||||
className='text-base-content/50 hover:text-base-content/70'
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
{mode === 'list' && showRightArrow && (
|
||||
<div className='absolute right-[-0.5px] top-0 h-full w-12'>
|
||||
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-l to-transparent'></div>
|
||||
<button
|
||||
onClick={handleRightArrowClick}
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
className='absolute right-2 top-1/2 -translate-y-1/2 transition-all duration-200 hover:scale-110'
|
||||
>
|
||||
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-sm transition-colors duration-200'>
|
||||
<MdChevronRight
|
||||
size={16}
|
||||
className='text-base-content/50 hover:text-base-content/70'
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{mode === 'list' && (
|
||||
<div className='text-base-content/75 w-28 min-w-24 max-w-40 overflow-hidden text-ellipsis text-base font-semibold'>
|
||||
{group.name}
|
||||
</div>
|
||||
)}
|
||||
{groupSelected && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
@@ -43,14 +203,16 @@ const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelecte
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={clsx('flex w-full flex-col pt-2')}>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
|
||||
{group.name}
|
||||
</h4>
|
||||
{mode === 'grid' && (
|
||||
<div className={clsx('flex w-full flex-col pt-2')}>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
|
||||
{group.name}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='placeholder' style={{ height: `${iconSize15}px` }}></div>
|
||||
</div>
|
||||
<div className='placeholder' style={{ height: `${iconSize15}px` }}></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,29 +150,26 @@ const GroupingModal: React.FC<GroupingModalProps> = ({
|
||||
<h2 className='text-center text-lg font-bold'>{_('Group Books')}</h2>
|
||||
<div className={clsx('mt-4 grid grid-cols-1 gap-2 text-base md:grid-cols-2')}>
|
||||
{isSelectedBooksHasGroup && (
|
||||
<div
|
||||
<button
|
||||
onClick={handleRemoveFromGroup}
|
||||
role='button'
|
||||
className='flex items-center space-x-2 p-2 text-blue-500'
|
||||
>
|
||||
<HiOutlineFolderRemove size={iconSize} />
|
||||
<span>{_('Remove From Group')}</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
<button
|
||||
onClick={handleCreateGroup}
|
||||
role='button'
|
||||
className='flex items-center space-x-2 p-2 text-blue-500'
|
||||
>
|
||||
<HiOutlineFolderAdd size={iconSize} />
|
||||
<span>{_('Create New Group')}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{showInput && (
|
||||
<div className='mt-4 flex items-center gap-2'>
|
||||
<input
|
||||
type='text'
|
||||
autoFocus
|
||||
ref={editorRef}
|
||||
value={editGroupName}
|
||||
onChange={(e) => setEditGroupName(e.target.value)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Menu from '@/components/Menu';
|
||||
|
||||
interface ImportMenuProps {
|
||||
setIsDropdownOpen?: (open: boolean) => void;
|
||||
@@ -18,15 +19,14 @@ const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBook
|
||||
};
|
||||
|
||||
return (
|
||||
<ul
|
||||
tabIndex={-1}
|
||||
<Menu
|
||||
className={clsx(
|
||||
'dropdown-content bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow',
|
||||
'dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow',
|
||||
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
|
||||
)}
|
||||
>
|
||||
<MenuItem label={_('From Local File')} onClick={handleImportBooks} />
|
||||
</ul>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -172,34 +172,27 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
)}
|
||||
<span className='bg-base-content/50 mx-2 h-4 w-[0.5px]'></span>
|
||||
<Dropdown
|
||||
label={_('Import Books')}
|
||||
className={clsx(
|
||||
'exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center',
|
||||
appService?.isMobile ? 'dropdown-end' : 'dropdown-center',
|
||||
)}
|
||||
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center'
|
||||
toggleButton={
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Import Books')}>
|
||||
<PiPlus className='m-0.5 h-5 w-5' />
|
||||
</div>
|
||||
}
|
||||
toggleButton={<PiPlus role='none' className='m-0.5 h-5 w-5' />}
|
||||
>
|
||||
<ImportMenu onImportBooks={onImportBooks} />
|
||||
</Dropdown>
|
||||
{appService?.isMobile ? null : (
|
||||
<button
|
||||
onClick={onToggleSelectMode}
|
||||
aria-label={_('Select Multiple Books')}
|
||||
aria-label={_('Select Books')}
|
||||
title={_('Select Books')}
|
||||
className='h-6'
|
||||
>
|
||||
<div
|
||||
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
|
||||
data-tip={_('Select Books')}
|
||||
>
|
||||
<PiSelectionAllDuotone
|
||||
role='button'
|
||||
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
|
||||
/>
|
||||
</div>
|
||||
<PiSelectionAllDuotone
|
||||
role='button'
|
||||
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -224,16 +217,18 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
) : (
|
||||
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
|
||||
<Dropdown
|
||||
label={_('View Menu')}
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
|
||||
toggleButton={<PiDotsThreeCircle role='none' size={iconSize18} />}
|
||||
>
|
||||
<ViewMenu />
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
label={_('Settings Menu')}
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={iconSize18} />}
|
||||
toggleButton={<MdOutlineMenu role='none' size={iconSize18} />}
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -21,12 +21,16 @@ const ReadingProgress: React.FC<ReadingProgressProps> = memo(
|
||||
({ book }) => {
|
||||
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
|
||||
|
||||
if (progressPercentage === null) {
|
||||
if (progressPercentage === null || Number.isNaN(progressPercentage)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='text-neutral-content/70 flex justify-between text-xs'>
|
||||
<div
|
||||
className='text-neutral-content/70 flex justify-between text-xs'
|
||||
role='status'
|
||||
aria-label={`${progressPercentage}%`}
|
||||
>
|
||||
<span>{progressPercentage}%</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +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 { MdCheck } from 'react-icons/md';
|
||||
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';
|
||||
@@ -20,14 +18,21 @@ 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';
|
||||
import Menu from '@/components/Menu';
|
||||
|
||||
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 = () => {
|
||||
@@ -104,7 +111,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsAlwaysShowStatusBar(settings.alwaysShowStatusBar);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const toggleAutoUploadBooks = () => {
|
||||
@@ -163,13 +169,47 @@ 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;
|
||||
const themeModeLabel =
|
||||
themeMode === 'dark'
|
||||
? _('Dark Mode')
|
||||
: themeMode === 'light'
|
||||
? _('Light Mode')
|
||||
: _('Auto Mode');
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
<Menu
|
||||
className={clsx(
|
||||
'settings-menu dropdown-content no-triangle border-base-100',
|
||||
'z-20 mt-2 max-w-[90vw] shadow-2xl',
|
||||
@@ -183,6 +223,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
: _('Logged in')
|
||||
}
|
||||
labelClass='!max-w-40'
|
||||
aria-label={_('View account details and quota')}
|
||||
Icon={
|
||||
avatarUrl ? (
|
||||
<UserAvatar url={avatarUrl} size={iconSize} DefaultIcon={PiUserCircleCheck} />
|
||||
@@ -191,10 +232,10 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
)
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
<div onClick={handleUserProfile} className='cursor-pointer'>
|
||||
<ul className='flex flex-col'>
|
||||
<button onClick={handleUserProfile} className='w-full'>
|
||||
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
|
||||
</div>
|
||||
</button>
|
||||
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
@@ -203,71 +244,79 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Auto Upload Books to Cloud')}
|
||||
Icon={isAutoUpload ? MdCheck : undefined}
|
||||
toggled={isAutoUpload}
|
||||
onClick={toggleAutoUploadBooks}
|
||||
/>
|
||||
{isTauriAppPlatform() && !appService?.isMobile && (
|
||||
<MenuItem
|
||||
label={_('Auto Import on File Open')}
|
||||
Icon={isAutoImportBooksOnOpen ? MdCheck : undefined}
|
||||
toggled={isAutoImportBooksOnOpen}
|
||||
onClick={toggleAutoImportBooksOnOpen}
|
||||
/>
|
||||
)}
|
||||
{isTauriAppPlatform() && (
|
||||
<MenuItem
|
||||
label={_('Open Last Book on Start')}
|
||||
Icon={isOpenLastBooks ? MdCheck : undefined}
|
||||
toggled={isOpenLastBooks}
|
||||
onClick={toggleOpenLastBooks}
|
||||
/>
|
||||
)}
|
||||
{appService?.hasUpdater && (
|
||||
<MenuItem
|
||||
label={_('Check Updates on Start')}
|
||||
Icon={isAutoCheckUpdates ? MdCheck : undefined}
|
||||
toggled={isAutoCheckUpdates}
|
||||
onClick={toggleAutoCheckUpdates}
|
||||
/>
|
||||
)}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
{appService?.hasWindow && (
|
||||
<MenuItem
|
||||
label={_('Open Book in New Window')}
|
||||
Icon={settings.openBookInNewWindow ? MdCheck : undefined}
|
||||
toggled={settings.openBookInNewWindow}
|
||||
onClick={toggleOpenInNewWindow}
|
||||
/>
|
||||
)}
|
||||
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
|
||||
{appService?.hasWindow && (
|
||||
<MenuItem
|
||||
label={_('Always on Top')}
|
||||
Icon={isAlwaysOnTop ? MdCheck : undefined}
|
||||
onClick={toggleAlwaysOnTop}
|
||||
/>
|
||||
<MenuItem label={_('Always on Top')} toggled={isAlwaysOnTop} onClick={toggleAlwaysOnTop} />
|
||||
)}
|
||||
{appService?.isMobileApp && (
|
||||
<MenuItem
|
||||
label={_('Always Show Status Bar')}
|
||||
Icon={isAlwaysShowStatusBar ? MdCheck : undefined}
|
||||
toggled={isAlwaysShowStatusBar}
|
||||
onClick={toggleAlwaysShowStatusBar}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Keep Screen Awake')}
|
||||
Icon={isScreenWakeLock ? MdCheck : undefined}
|
||||
toggled={isScreenWakeLock}
|
||||
onClick={toggleScreenWakeLock}
|
||||
/>
|
||||
{appService?.isAndroidApp && (
|
||||
<MenuItem
|
||||
label={_(_('Background Read Aloud'))}
|
||||
toggled={alwaysInForeground}
|
||||
onClick={toggleAlwaysInForeground}
|
||||
/>
|
||||
)}
|
||||
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
|
||||
<MenuItem
|
||||
label={
|
||||
themeMode === 'dark'
|
||||
? _('Dark Mode')
|
||||
: themeMode === 'light'
|
||||
? _('Light Mode')
|
||||
: _('Auto Mode')
|
||||
}
|
||||
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
|
||||
label={themeModeLabel}
|
||||
Icon={themeMode === 'dark' ? PiMoon : themeMode === 'light' ? PiSun : TbSunMoon}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
<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} />
|
||||
)}
|
||||
@@ -276,10 +325,10 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
<MenuItem
|
||||
label={_('Help improve Readest')}
|
||||
description={isTelemetryEnabled ? _('Sharing anonymized statistics') : ''}
|
||||
Icon={isTelemetryEnabled ? MdCheck : undefined}
|
||||
toggled={isTelemetryEnabled}
|
||||
onClick={toggleTelemetry}
|
||||
/>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Menu from '@/components/Menu';
|
||||
|
||||
interface ViewMenuProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
@@ -92,20 +92,17 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='settings-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}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={viewMode === option.value ? MdCheck : undefined}
|
||||
toggled={viewMode === option.value}
|
||||
onClick={() => handleSetViewMode(option.value as LibraryViewModeType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
<MenuItem
|
||||
label={_('Book Covers')}
|
||||
buttonClass='h-8'
|
||||
@@ -117,11 +114,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={coverFit === option.value ? MdCheck : undefined}
|
||||
toggled={coverFit === option.value}
|
||||
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
<MenuItem
|
||||
label={_('Sort by...')}
|
||||
buttonClass='h-8'
|
||||
@@ -133,21 +130,21 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={sortBy === option.value ? MdCheck : undefined}
|
||||
toggled={sortBy === option.value}
|
||||
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
{sortingOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value.toString()}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={isAscending === option.value ? MdCheck : undefined}
|
||||
toggled={isAscending === option.value}
|
||||
onClick={() => handleSetSortAscending(option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -43,16 +43,18 @@ import {
|
||||
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();
|
||||
@@ -74,8 +76,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
} = useLibraryStore();
|
||||
const _ = useTranslation();
|
||||
const { selectFiles } = useFileSelector(appService, _);
|
||||
const { safeAreaInsets: insets } = useThemeStore();
|
||||
const { safeAreaInsets: insets, isRoundedWindow } = useThemeStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const { isFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
@@ -88,6 +91,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);
|
||||
@@ -619,7 +623,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
};
|
||||
|
||||
if (!appService || !insets || checkOpenWithBooks || checkLastOpenBooks) {
|
||||
return <div className='bg-base-200 h-[100vh]' />;
|
||||
return <div className={clsx('h-[100vh]', !appService?.isLinuxApp && 'bg-base-200')} />;
|
||||
}
|
||||
|
||||
const showBookshelf = libraryLoaded || libraryBooks.length > 0;
|
||||
@@ -627,14 +631,18 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
return (
|
||||
<div
|
||||
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',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
'library-page bg-base-200 text-base-content flex h-[100vh] select-none flex-col overflow-hidden',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
<div className='top-0 z-40 w-full'>
|
||||
<div
|
||||
className='top-0 z-40 w-full'
|
||||
role='banner'
|
||||
tabIndex={-1}
|
||||
aria-label={_('Library Header')}
|
||||
>
|
||||
<LibraryHeader
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
@@ -653,6 +661,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
(libraryBooks.some((book) => !book.deletedAt) ? (
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
aria-label=''
|
||||
ref={osRef}
|
||||
className='flex-grow'
|
||||
options={{ scrollbars: { autoHide: 'scroll' } }}
|
||||
@@ -723,6 +732,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
)}
|
||||
<AboutWindow />
|
||||
<UpdaterWindow />
|
||||
<MigrateDataWindow />
|
||||
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={''} />}
|
||||
<Toast />
|
||||
</div>
|
||||
);
|
||||
@@ -730,7 +741,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
|
||||
const LibraryPage = () => {
|
||||
return (
|
||||
<Suspense fallback={<div className='bg-base-200 h-[100vh]' />}>
|
||||
<Suspense fallback={<div className='h-[100vh]' />}>
|
||||
<LibraryPageWithSearchParams />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -103,8 +103,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
)
|
||||
}
|
||||
onClick={toggleBookmark}
|
||||
tooltip={_('Bookmark')}
|
||||
tooltipDirection='bottom'
|
||||
label={isBookmarked ? _('Remove Bookmark') : _('Add Bookmark')}
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,15 +7,16 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getGridTemplate, getInsetEdges } from '@/utils/grid';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
import SettingsDialog from '@/components/settings/SettingsDialog';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FooterBar from './FooterBar';
|
||||
import ProgressInfoView from './ProgressInfo';
|
||||
import Ribbon from './Ribbon';
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
import Annotator from './annotator/Annotator';
|
||||
import FootnotePopup from './FootnotePopup';
|
||||
import HintInfo from './HintInfo';
|
||||
@@ -27,12 +28,13 @@ interface BooksGridProps {
|
||||
}
|
||||
|
||||
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { setGridInsets, hoveredBookKey } = useReaderStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const { isFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
|
||||
const { safeAreaInsets: screenInsets } = useThemeStore();
|
||||
const aspectRatio = window.innerWidth / window.innerHeight;
|
||||
@@ -75,6 +77,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
gridTemplateColumns: gridTemplate.columns,
|
||||
gridTemplateRows: gridTemplate.rows,
|
||||
}}
|
||||
role='main'
|
||||
aria-label={_('Books Content')}
|
||||
>
|
||||
{bookKeys.map((bookKey, index) => {
|
||||
const bookData = getBookData(bookKey);
|
||||
@@ -116,7 +120,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
isTopLeft={index === 0}
|
||||
isHoveredAnim={bookKeys.length > 2}
|
||||
onCloseBook={onCloseBook}
|
||||
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
<FoliateViewer
|
||||
@@ -181,7 +184,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
{showFooter && (
|
||||
<ProgressInfoView
|
||||
bookKey={bookKey}
|
||||
bookFormat={book.format}
|
||||
section={section}
|
||||
pageinfo={pageinfo}
|
||||
timeinfo={timeinfo}
|
||||
@@ -200,7 +202,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
isHoveredAnim={false}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
|
||||
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { usePagination } from '../hooks/usePagination';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { useProgressSync } from '../hooks/useProgressSync';
|
||||
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
|
||||
import { useAutoFocus } from '@/hooks/useAutoFocus';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useKOSync } from '../hooks/useKOSync';
|
||||
import {
|
||||
applyFixedlayoutStyles,
|
||||
@@ -45,6 +47,7 @@ import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
import { removeTabIndex } from '@/utils/a11y';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import KOSyncConflictResolver from './KOSyncResolver';
|
||||
|
||||
@@ -61,11 +64,12 @@ const FoliateViewer: React.FC<{
|
||||
gridInsets: Insets;
|
||||
contentInsets: Insets;
|
||||
}> = ({ bookKey, bookDoc, config, gridInsets, contentInsets: insets }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService, envConfig } = useEnv();
|
||||
const { themeCode, isDarkMode } = useThemeStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { loadCustomFonts, getLoadedFonts } = useCustomFontStore();
|
||||
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
|
||||
const { getView, setView: setFoliateView, setViewInited, setProgress } = useReaderStore();
|
||||
const { getViewState, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
@@ -80,6 +84,8 @@ const FoliateViewer: React.FC<{
|
||||
const [loading, setLoading] = useState(false);
|
||||
const docLoaded = useRef(false);
|
||||
|
||||
useAutoFocus<HTMLDivElement>({ ref: containerRef });
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setToastMessage(''), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
@@ -160,6 +166,8 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
applyImageStyle(detail.doc);
|
||||
|
||||
removeTabIndex(detail.doc);
|
||||
|
||||
// Inline scripts in tauri platforms are not executed by default
|
||||
if (viewSettings.allowScript && isTauriAppPlatform()) {
|
||||
evalInlineScripts(detail.doc);
|
||||
@@ -222,7 +230,7 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const { handlePageFlip, handleContinuousScroll } = usePagination(bookKey, viewRef, containerRef);
|
||||
const mouseHandlers = useMouseEvent(bookKey, handlePageFlip, handleContinuousScroll);
|
||||
const touchHandlers = useTouchEvent(bookKey, handleContinuousScroll);
|
||||
const touchHandlers = useTouchEvent(bookKey, handlePageFlip, handleContinuousScroll);
|
||||
|
||||
useFoliateEvents(viewRef.current, {
|
||||
onLoad: docLoadHandler,
|
||||
@@ -300,6 +308,7 @@ const FoliateViewer: React.FC<{
|
||||
if (bookDoc?.rendition?.layout === 'pre-paginated') {
|
||||
view.renderer.setAttribute('zoom', viewSettings.zoomMode);
|
||||
view.renderer.setAttribute('spread', viewSettings.spreadMode);
|
||||
view.renderer.setAttribute('scale-factor', viewSettings.zoomLevel);
|
||||
} else {
|
||||
view.renderer.setAttribute('max-column-count', maxColumnCount);
|
||||
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
|
||||
@@ -313,6 +322,7 @@ const FoliateViewer: React.FC<{
|
||||
} else {
|
||||
await view.goToFraction(0);
|
||||
}
|
||||
setViewInited(bookKey, true);
|
||||
};
|
||||
|
||||
openBook();
|
||||
@@ -405,7 +415,10 @@ const FoliateViewer: React.FC<{
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='foliate-viewer h-[100%] w-[100%]'
|
||||
tabIndex={-1}
|
||||
role='document'
|
||||
aria-label={_('Book Content')}
|
||||
className='foliate-viewer h-[100%] w-[100%] focus:outline-none'
|
||||
{...mouseHandlers}
|
||||
{...touchHandlers}
|
||||
/>
|
||||
|
||||
@@ -18,9 +18,9 @@ import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { saveViewSettings } from '@/helpers/viewSettings';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { viewPagination } from '../hooks/usePagination';
|
||||
import { saveViewSettings } from '../utils/viewSettingsHelper';
|
||||
import { PageInfo } from '@/types/book';
|
||||
import { Insets } from '@/types/misc';
|
||||
import Button from '@/components/Button';
|
||||
@@ -173,6 +173,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
role='none'
|
||||
className={clsx(
|
||||
'absolute bottom-0 left-0 z-10 hidden w-full sm:flex sm:h-[52px]',
|
||||
// show scroll bar when vertical and scrolled in desktop
|
||||
@@ -182,8 +183,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
/>
|
||||
<div
|
||||
role='group'
|
||||
aria-label={_('Footer Bar')}
|
||||
className={clsx(
|
||||
'footer-bar shadow-xs bottom-0 z-30 flex w-full flex-col',
|
||||
'footer-bar shadow-xs bottom-0 z-10 flex w-full flex-col',
|
||||
'sm:h-[52px] sm:justify-center',
|
||||
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
|
||||
'transition-[opacity,transform] duration-300',
|
||||
@@ -199,8 +202,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: `pointer-events-none translate-y-full opacity-0 sm:translate-y-0`,
|
||||
)}
|
||||
dir={viewSettings?.rtl ? 'rtl' : 'ltr'}
|
||||
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => window.innerWidth >= 640 && setHoveredBookKey('')}
|
||||
aria-hidden={!isVisible}
|
||||
>
|
||||
{/* Mobile footer bar */}
|
||||
<div
|
||||
@@ -216,6 +219,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
label={_('Reading Progress')}
|
||||
heightPx={sliderHeight}
|
||||
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
|
||||
initialValue={progressValid ? progressFraction * 100 : 0}
|
||||
@@ -226,34 +230,34 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
tooltip={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
label={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
|
||||
tooltip={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
label={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
tooltip={_('Go Back')}
|
||||
label={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
tooltip={_('Go Forward')}
|
||||
label={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
label={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
label={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,6 +273,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
label={_('Font Size')}
|
||||
initialValue={viewSettings?.defaultFontSize ?? 16}
|
||||
bubbleLabel={`${viewSettings?.defaultFontSize ?? 16}`}
|
||||
minLabel='A'
|
||||
@@ -281,6 +286,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
/>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
label={_('Page Margin')}
|
||||
initialValue={getMarginProgressValue(
|
||||
viewSettings?.marginTopPx ?? 44,
|
||||
viewSettings?.gapPercent ?? 5,
|
||||
@@ -292,6 +298,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
onChange={handleMarginChange}
|
||||
/>
|
||||
<Slider
|
||||
label={_('Line Spacing')}
|
||||
initialValue={(viewSettings?.lineHeight ?? 1.6) * 10}
|
||||
bubbleElement={<RxLineHeight size={marginIconSize} />}
|
||||
minLabel={_('Small')}
|
||||
@@ -311,15 +318,22 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
label={_('Table of Contents')}
|
||||
icon={<TOCIcon size={tocIconSize} className='' />}
|
||||
onClick={() => handleSetActionTab('toc')}
|
||||
/>
|
||||
<Button icon={<NoteIcon className='' />} onClick={() => handleSetActionTab('note')} />
|
||||
<Button
|
||||
label={_('Notes')}
|
||||
icon={<NoteIcon className='' />}
|
||||
onClick={() => handleSetActionTab('note')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Reading Progress')}
|
||||
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
|
||||
onClick={() => handleSetActionTab('progress')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Font & Layout')}
|
||||
icon={
|
||||
<FontIcon
|
||||
size={fontIconSize}
|
||||
@@ -329,6 +343,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
onClick={() => handleSetActionTab('font')}
|
||||
/>
|
||||
<Button
|
||||
label={_('Speak')}
|
||||
icon={<TTSIcon className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={() => handleSetActionTab('tts')}
|
||||
/>
|
||||
@@ -343,33 +358,40 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
tooltip={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
label={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
|
||||
tooltip={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
label={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
tooltip={_('Go Back')}
|
||||
label={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
tooltip={_('Go Forward')}
|
||||
label={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<span className='mx-2 text-center text-sm'>
|
||||
{progressValid ? `${Math.round(progressFraction * 100)}%` : ''}
|
||||
</span>
|
||||
{progressValid && (
|
||||
<span
|
||||
title={_('Reading Progress')}
|
||||
aria-label={_('Reading Progress') + ': ' + `${Math.round(progressFraction * 100)}%`}
|
||||
className='mx-2 text-center text-sm'
|
||||
>
|
||||
<span aria-hidden='true'>{`${Math.round(progressFraction * 100)}%`}</span>
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type='range'
|
||||
className='text-base-content mx-2 min-w-0 flex-1'
|
||||
min={0}
|
||||
max={100}
|
||||
aria-label={_('Jump to Location')}
|
||||
value={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) =>
|
||||
handleProgressChange(parseInt((e.target as HTMLInputElement).value, 10))
|
||||
@@ -378,17 +400,17 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<Button
|
||||
icon={<FaHeadphones className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={handleSpeakText}
|
||||
tooltip={_('Speak')}
|
||||
label={_('Speak')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
label={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
label={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { mountAdditionalFonts } from '@/styles/fonts';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { isCJKLang } from '@/utils/lang';
|
||||
import { Overlay } from '@/components/Overlay';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
interface FootnotePopupProps {
|
||||
@@ -226,13 +227,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showPopup && (
|
||||
<div
|
||||
className='fixed inset-0'
|
||||
onClick={handleDismissPopup}
|
||||
onContextMenu={handleDismissPopup}
|
||||
/>
|
||||
)}
|
||||
{showPopup && <Overlay onDismiss={handleDismissPopup} />}
|
||||
<Popup
|
||||
width={responsiveWidth}
|
||||
height={responsiveHeight}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import { Insets } from '@/types/misc';
|
||||
@@ -7,6 +7,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
@@ -25,7 +26,6 @@ interface HeaderBarProps {
|
||||
isHoveredAnim: boolean;
|
||||
gridInsets: Insets;
|
||||
onCloseBook: (bookKey: string) => void;
|
||||
onSetSettingsDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
@@ -35,8 +35,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
isHoveredAnim,
|
||||
gridInsets,
|
||||
onCloseBook,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
@@ -83,6 +83,16 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService, isSideBarVisible, hoveredBookKey]);
|
||||
|
||||
// Check if mouse is outside header area to avoid false positive event of MouseLeave when clicking inside header on Windows
|
||||
const isMouseOutsideHeader = useCallback((clientX: number, clientY: number) => {
|
||||
if (!headerRef.current) return true;
|
||||
|
||||
const rect = headerRef.current.getBoundingClientRect();
|
||||
return (
|
||||
clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom
|
||||
);
|
||||
}, []);
|
||||
|
||||
const isHeaderVisible = hoveredBookKey === bookKey || isDropdownOpen;
|
||||
const trafficLightInHeader =
|
||||
appService?.hasTrafficLight && !trafficLightInFullscreen && !isSideBarVisible && isTopLeft;
|
||||
@@ -95,6 +105,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role='none'
|
||||
className={clsx('absolute top-0 z-10 h-11 w-full')}
|
||||
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
@@ -111,6 +122,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
/>
|
||||
<div
|
||||
ref={headerRef}
|
||||
role='group'
|
||||
aria-label={_('Header Bar')}
|
||||
className={clsx(
|
||||
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
|
||||
`shadow-xs transition-[opacity,margin-top] duration-300`,
|
||||
@@ -126,7 +139,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
? `${Math.max(gridInsets.top, statusBarHeight)}px`
|
||||
: `${gridInsets.top}px`,
|
||||
}}
|
||||
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={(e) => {
|
||||
if (!appService?.isMobile && isMouseOutsideHeader(e.clientX, e.clientY)) {
|
||||
setHoveredBookKey('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='bg-base-100 sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4 pe-2'>
|
||||
<div className='hidden sm:flex'>
|
||||
@@ -137,31 +155,35 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
</div>
|
||||
|
||||
<div
|
||||
role='contentinfo'
|
||||
aria-label={_('Title') + ' - ' + bookTitle}
|
||||
className={clsx(
|
||||
'header-title z-15 bg-base-100 pointer-events-none hidden flex-1 items-center justify-center sm:flex',
|
||||
!windowButtonVisible && 'absolute inset-0',
|
||||
)}
|
||||
>
|
||||
<h2
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={clsx(
|
||||
'line-clamp-1 text-center text-xs font-semibold',
|
||||
!windowButtonVisible && 'max-w-[50%]',
|
||||
)}
|
||||
>
|
||||
{bookTitle}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4 ps-2'>
|
||||
<SettingsToggler />
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
label={_('View Options')}
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
|
||||
onToggle={handleToggleDropdown}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
|
||||
<ViewMenu bookKey={bookKey} />
|
||||
</Dropdown>
|
||||
|
||||
<WindowButtons
|
||||
|
||||
@@ -215,126 +215,128 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
title={_('KOReader Sync Settings')}
|
||||
boxClassName='sm:!min-w-[520px] sm:h-auto'
|
||||
>
|
||||
<div className='mb-4 mt-0 flex flex-col gap-4 p-2 sm:p-4'>
|
||||
{isConfigured ? (
|
||||
<>
|
||||
<div className='text-center'>
|
||||
<p className='text-base-content/80 text-sm'>
|
||||
{_('Sync as {{userDisplayName}}', {
|
||||
userDisplayName: settings.kosync.username,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex h-14 items-center justify-between'>
|
||||
<span className='text-base-content/80'>{_('Sync Server Connected')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={settings.kosync.enabled}
|
||||
onChange={() => handleDisconnect()}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Sync Strategy')}</span>
|
||||
</label>
|
||||
<StyledSelect
|
||||
value={settings.kosync.strategy}
|
||||
onChange={handleStrategyChange}
|
||||
options={[
|
||||
{ value: 'prompt', label: _('Ask on conflict') },
|
||||
{ value: 'silent', label: _('Always use latest') },
|
||||
{ value: 'send', label: _('Send changes only') },
|
||||
{ value: 'receive', label: _('Receive changes only') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Checksum Method')}</span>
|
||||
</label>
|
||||
<StyledSelect
|
||||
value={settings.kosync.checksumMethod}
|
||||
onChange={handleChecksumMethodChange}
|
||||
options={[
|
||||
{ value: 'binary', label: _('File Content (recommended)') },
|
||||
{ value: 'filename', label: _('File Name'), disabled: true },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Device Name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={osName ? `Readest (${osName})` : 'Readest'}
|
||||
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
|
||||
value={deviceName}
|
||||
onChange={handleDeviceNameChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className='text-base-content/70 text-center text-sm'>
|
||||
{_('Connect to your KOReader Sync server.')}
|
||||
</p>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Server URL')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='https://koreader.sync.server'
|
||||
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
|
||||
spellCheck='false'
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<form className='flex flex-col gap-4'>
|
||||
{isOpen && (
|
||||
<div className='mb-4 mt-0 flex flex-col gap-4 p-2 sm:p-4'>
|
||||
{isConfigured ? (
|
||||
<>
|
||||
<div className='text-center'>
|
||||
<p className='text-base-content/80 text-sm'>
|
||||
{_('Sync as {{userDisplayName}}', {
|
||||
userDisplayName: settings.kosync.username,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex h-14 items-center justify-between'>
|
||||
<span className='text-base-content/80'>{_('Sync Server Connected')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={settings.kosync.enabled}
|
||||
onChange={() => handleDisconnect()}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Username')}</span>
|
||||
<span className='label-text font-medium'>{_('Sync Strategy')}</span>
|
||||
</label>
|
||||
<StyledSelect
|
||||
value={settings.kosync.strategy}
|
||||
onChange={handleStrategyChange}
|
||||
options={[
|
||||
{ value: 'prompt', label: _('Ask on conflict') },
|
||||
{ value: 'silent', label: _('Always use latest') },
|
||||
{ value: 'send', label: _('Send changes only') },
|
||||
{ value: 'receive', label: _('Receive changes only') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Checksum Method')}</span>
|
||||
</label>
|
||||
<StyledSelect
|
||||
value={settings.kosync.checksumMethod}
|
||||
onChange={handleChecksumMethodChange}
|
||||
options={[
|
||||
{ value: 'binary', label: _('File Content (recommended)') },
|
||||
{ value: 'filename', label: _('File Name'), disabled: true },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Device Name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={_('Your Username')}
|
||||
placeholder={osName ? `Readest (${osName})` : 'Readest'}
|
||||
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
|
||||
spellCheck='false'
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete='username'
|
||||
value={deviceName}
|
||||
onChange={handleDeviceNameChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className='text-base-content/70 text-center text-sm'>
|
||||
{_('Connect to your KOReader Sync server.')}
|
||||
</p>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Password')}</span>
|
||||
<span className='label-text font-medium'>{_('Server URL')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
placeholder={_('Your Password')}
|
||||
type='text'
|
||||
placeholder='https://koreader.sync.server'
|
||||
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete='current-password'
|
||||
spellCheck='false'
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<button
|
||||
className='btn btn-primary mt-2 h-12 min-h-12 w-full'
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || !url || !username || !password}
|
||||
>
|
||||
{isConnecting ? <span className='loading loading-spinner'></span> : _('Connect')}
|
||||
</button>
|
||||
{connectionStatus && (
|
||||
<div className='text-error h-4 text-center text-sm'>{connectionStatus}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<form className='flex flex-col gap-4'>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Username')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={_('Your Username')}
|
||||
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
|
||||
spellCheck='false'
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete='username'
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Password')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
placeholder={_('Your Password')}
|
||||
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete='current-password'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<button
|
||||
className='btn btn-primary mt-2 h-12 min-h-12 w-full'
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || !url || !username || !password}
|
||||
>
|
||||
{isConnecting ? <span className='loading loading-spinner'></span> : _('Connect')}
|
||||
</button>
|
||||
{connectionStatus && (
|
||||
<div className='text-error h-4 text-center text-sm'>{connectionStatus}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,8 +42,7 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
tooltip={_('Notebook')}
|
||||
tooltipDirection='bottom'
|
||||
label={_('Notebook')}
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@ import { PageInfo, TimeInfo } from '@/types/book';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { formatReadingProgress } from '@/utils/progress';
|
||||
|
||||
interface PageInfoProps {
|
||||
bookKey: string;
|
||||
bookFormat: string;
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
timeinfo?: TimeInfo;
|
||||
@@ -20,7 +20,6 @@ interface PageInfoProps {
|
||||
|
||||
const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
bookKey,
|
||||
bookFormat,
|
||||
section,
|
||||
pageinfo,
|
||||
timeinfo,
|
||||
@@ -30,8 +29,10 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
@@ -46,9 +47,10 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
: '{current} / {total}'
|
||||
: '{percent}%';
|
||||
|
||||
const progressInfo = ['PDF', 'CBZ'].includes(bookFormat)
|
||||
? formatReadingProgress(section?.current, section?.total, formatTemplate)
|
||||
: formatReadingProgress(pageinfo?.current, pageinfo?.total, formatTemplate);
|
||||
const pageProgress = bookData?.isFixedLayout ? section : pageinfo;
|
||||
const progressInfo = bookData?.isFixedLayout
|
||||
? formatReadingProgress(pageProgress?.current, pageProgress?.total, formatTemplate)
|
||||
: formatReadingProgress(pageProgress?.current, pageProgress?.total, formatTemplate);
|
||||
|
||||
const timeLeft = timeinfo
|
||||
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
|
||||
@@ -65,6 +67,18 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
isVertical ? 'writing-vertical-rl' : 'w-full',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
aria-label={[
|
||||
pageProgress
|
||||
? _('On {{current}} of {{total}} page', {
|
||||
current: pageProgress.current + 1,
|
||||
total: pageProgress.total,
|
||||
})
|
||||
: '',
|
||||
timeLeft,
|
||||
pageLeft,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ')}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
@@ -83,6 +97,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
}
|
||||
>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={clsx(
|
||||
'flex items-center justify-center',
|
||||
isVertical ? 'h-full' : 'h-[52px] w-full',
|
||||
|
||||
@@ -39,12 +39,12 @@ Z-Index Layering Guide:
|
||||
• Floats above the content but below global dialogs.
|
||||
40 – TTS Bar
|
||||
• Mini controls for TTS playback on top of the TTS Control.
|
||||
30 – Footbar (TTS Control)
|
||||
• Persistent bottom controls and the TTS icon/panel.
|
||||
30 – TTS Control
|
||||
• Persistent TTS icon/panel.
|
||||
20 – Menu / Sidebar / Notebook (Pinned)
|
||||
• Docked navigation or note views.
|
||||
10 – Headerbar / Ribbon
|
||||
• Top toolbar and ribbon elements.
|
||||
10 – Headerbar / Footbar / Ribbon
|
||||
• Top toolbar, bottom footbar and ribbon elements.
|
||||
0 – Base Content
|
||||
• Main reading area or background content.
|
||||
*/
|
||||
@@ -57,7 +57,8 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isSideBarVisible, getIsSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, getIsNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { isDarkMode, systemUIAlwaysHidden, isRoundedWindow } = useThemeStore();
|
||||
const { showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
@@ -140,10 +141,8 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
return libraryLoaded && settings.globalReadSettings ? (
|
||||
<div
|
||||
className={clsx(
|
||||
`reader-page bg-base-100 text-base-content select-none overflow-hidden`,
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
'reader-page bg-base-100 text-base-content h-[100vh] select-none overflow-hidden',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<div className='h-[100vh]'></div>}>
|
||||
@@ -155,7 +154,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
</Suspense>
|
||||
</div>
|
||||
) : (
|
||||
<div className='bg-base-100 h-[100vh]'></div>
|
||||
<div className={clsx('h-[100vh]', !appService?.isLinuxApp && 'bg-base-100')}></div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
@@ -185,7 +184,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
return (
|
||||
loading && (
|
||||
<div className={clsx('hero hero-content', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
|
||||
<div className='hero hero-content h-[100vh]'>
|
||||
<Spinner loading={true} />
|
||||
</div>
|
||||
)
|
||||
@@ -193,7 +192,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('reader-content flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
|
||||
<div className='reader-content flex h-[100vh]'>
|
||||
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<Notebook />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user