forked from akai/readest
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fde9955b2 | |||
| 3875c8f239 | |||
| 7bd1b03652 | |||
| 73878c14e3 | |||
| bc2c3a2c9a | |||
| 460d37c656 | |||
| fff9e3221e | |||
| f77fa17cbc | |||
| d4565f7d60 | |||
| e8b1976f4f | |||
| 6b4bb64885 | |||
| fc0c42f7db | |||
| 41e5f27d4d | |||
| a042b4c739 | |||
| a1c1a9f983 | |||
| 423ae77160 | |||
| 05c61a4b1a | |||
| ce8f06b7fb | |||
| f815844577 | |||
| 7fe4d38c51 | |||
| 43edcb4e9e | |||
| e259b29afa | |||
| cd5f57a004 | |||
| 8c0cdb0e22 | |||
| c0df9f1cb6 | |||
| cb1ddaeedb | |||
| 4447dd7b9a | |||
| dcb7602837 | |||
| 60e48742fe | |||
| dd43569778 | |||
| 8433dd6950 | |||
| 379802f580 | |||
| 2dcb5d7c1e | |||
| 98870d9f3e | |||
| 31aeb2289c | |||
| 0f66ecbba1 | |||
| f0d3c9dd4e | |||
| 92aae340d9 | |||
| 3cf40039ee | |||
| 7ed4937d9d | |||
| b3593c0365 | |||
| 6235202066 | |||
| b3d1085ddf | |||
| b2a4ddae60 | |||
| 42f50af27b | |||
| 07f74d8858 | |||
| 9477789db1 | |||
| 8a7d0e1b0d | |||
| 261ce95ed1 | |||
| c5a3b44cbf | |||
| ed68d25b15 | |||
| f7f8872a13 | |||
| be9802a936 | |||
| 84328dcfb2 | |||
| 89d48c72b0 | |||
| 467e80cd4b | |||
| 20955ec294 | |||
| 3cae183f51 | |||
| 99319ab4d1 | |||
| 658969e97a | |||
| 28c6c1ec58 | |||
| d8cc220c18 | |||
| 97d7eb659c | |||
| 9766ec0125 | |||
| e46d01448b | |||
| bb8bf99b40 | |||
| f8ac30adf1 | |||
| 69d418aa61 | |||
| 95af3bd3d1 | |||
| 5bdc29fe84 | |||
| 56f4b275f4 | |||
| 00b46dd8bb | |||
| 8ce89ca8f6 | |||
| 648d9ec260 | |||
| ebe1c10c84 | |||
| f2309ef496 | |||
| 8d71593f39 | |||
| 09e3c30a2a | |||
| eee4cbeaca | |||
| 00deca5029 | |||
| c7c20fec55 | |||
| 57e22dc7b6 | |||
| debd60fb70 | |||
| dfa17c1c0b | |||
| 2a26f8b04a | |||
| db955d2c8c | |||
| 51602a77fb | |||
| 48da5ab546 | |||
| 363b98dd3b | |||
| 43532a0ee8 | |||
| 03b2af1f97 | |||
| 3f8d03ae28 | |||
| 146a71fdef | |||
| 734e22d92c | |||
| a77986c0e6 | |||
| 4e7f9c49b0 | |||
| c51c95b883 | |||
| 62081bebfd | |||
| 15394d17d8 | |||
| 5d1deadc1e | |||
| b497ddacac | |||
| a9965fd52c | |||
| 87d610bb62 | |||
| dbc4651141 | |||
| dad2dfd078 | |||
| 1fe188814d | |||
| 6ec3a5b332 | |||
| 1acba2e08c | |||
| 32f9346f84 | |||
| 69c503026a | |||
| 84fc4cec5e | |||
| 5c9c11fafd | |||
| 77fa57438e | |||
| e4d217f3aa | |||
| c7a583c53a | |||
| 6c86917098 | |||
| 7c21f48d68 | |||
| 5820191c26 | |||
| b37a804192 | |||
| ebdfc39ef6 | |||
| 9f0d8b5c12 | |||
| a267671671 | |||
| aa62ebd3f0 | |||
| cc85e5c304 | |||
| b0cc0b6f1a | |||
| 6b033f7509 | |||
| 5c0670031a | |||
| 35735cd931 | |||
| 699a01c78f | |||
| 033e161455 | |||
| e6428656c2 | |||
| 68c3c45da3 | |||
| 36e87d4eb6 | |||
| 7f5059a8ea | |||
| 8dc04f4095 | |||
| 6a48f3feb4 | |||
| facf5fba75 | |||
| 9700942e24 | |||
| e238b49fbf | |||
| 32dfadf182 | |||
| 66744108e2 | |||
| 401f28847c | |||
| 9674b45cf0 | |||
| a4bc88d305 | |||
| b6b8b17760 | |||
| 14810d6737 | |||
| ed6c92a65a | |||
| 20d7a1185e | |||
| 32bcfda9ad |
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: ['readest']
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -177,6 +177,11 @@ jobs:
|
||||
pnpm tauri icon ../../data/icons/readest-book.png
|
||||
git checkout .
|
||||
|
||||
MANIFEST="src-tauri/gen/android/app/src/main/AndroidManifest.xml"
|
||||
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST" || \
|
||||
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
|
||||
|
||||
pushd src-tauri/gen/android
|
||||
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
|
||||
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Upload Release Assets to R2
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag name (e.g., v1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
upload-to-r2:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RELEASE_R2_BUCKET: readest-releases
|
||||
RELEASE_R2_ACCOUNT_ID: ${{ secrets.RELEASE_R2_ACCOUNT_ID }}
|
||||
RELEASE_R2_ACCESS_KEY_ID: ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
|
||||
RELEASE_R2_SECRET_ACCESS_KEY: ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Download release assets
|
||||
run: |
|
||||
gh release download "${{ inputs.tag }}" --repo readest/readest --dir ./release-assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install rclone
|
||||
run: curl https://rclone.org/install.sh | sudo bash
|
||||
|
||||
- name: Configure rclone
|
||||
run: |
|
||||
mkdir -p ~/.config/rclone
|
||||
cat > ~/.config/rclone/rclone.conf <<EOF
|
||||
[r2]
|
||||
type = s3
|
||||
provider = Cloudflare
|
||||
access_key_id = $RELEASE_R2_ACCESS_KEY_ID
|
||||
secret_access_key = $RELEASE_R2_SECRET_ACCESS_KEY
|
||||
endpoint = https://${RELEASE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||
EOF
|
||||
|
||||
- name: Modify latest.json download URLs
|
||||
run: |
|
||||
GITHUB_BASE_URL="https://github.com/readest/readest/releases/download"
|
||||
READEST_BASE_URL="https://download.readest.com/releases"
|
||||
sed -i "s#${GITHUB_BASE_URL}#${READEST_BASE_URL}#g" ./release-assets/latest.json
|
||||
|
||||
- name: Upload to R2
|
||||
run: |
|
||||
mkdir releases
|
||||
mv ./release-assets/latest.json releases
|
||||
mv ./release-assets/release-notes.json releases
|
||||
rclone copy ./release-assets r2:${RELEASE_R2_BUCKET}/releases/${{ inputs.tag }}/
|
||||
rclone copy ./releases r2:${RELEASE_R2_BUCKET}/releases/
|
||||
Vendored
+9
@@ -4,4 +4,13 @@
|
||||
"packages/tauri/Cargo.toml",
|
||||
"apps/readest-app/src-tauri/Cargo.toml"
|
||||
],
|
||||
// "editor.formatOnSave": true, // uncomment to add format on save
|
||||
"typescript.inlayHints.parameterNames.enabled": "all",
|
||||
"typescript.inlayHints.variableTypes.enabled": true,
|
||||
"typescript.inlayHints.propertyDeclarationTypes.enabled": true,
|
||||
"typescript.inlayHints.functionLikeReturnTypes.enabled": true,
|
||||
"typescript.inlayHints.enumMemberValues.enabled": true,
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.format.enable": false,
|
||||
}
|
||||
Generated
+591
-669
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@
|
||||
[![][badge-discord]][link-discord]
|
||||
[![AGPL Licence][badge-license]](LICENSE)
|
||||
[![Latest release][badge-release]][link-gh-releases]
|
||||
[![Donate][badge-donate]][link-donate]
|
||||
<br>
|
||||
[![Last commit][badge-last-commit]][link-gh-commits]
|
||||
[![Commits][badge-commit-activity]][link-gh-pulse]
|
||||
|
||||
@@ -27,6 +29,7 @@
|
||||
<a href="#downloads">Downloads</a> •
|
||||
<a href="#getting-started">Getting Started</a> •
|
||||
<a href="#troubleshooting">Troubleshooting</a> •
|
||||
<a href="#support">Support</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
@@ -48,13 +51,14 @@
|
||||
| **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. | ✅ |
|
||||
| **Translate with DeepL** | Translate selected text instantly using DeepL for accurate translations. | ✅ |
|
||||
| **[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. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enable text-to-speech functionality for a more accessible reading experience. | ✅ |
|
||||
| **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ |
|
||||
| **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
|
||||
|
||||
@@ -63,8 +67,8 @@
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
|
||||
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
|
||||
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
@@ -78,15 +82,15 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +98,7 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||
The Readest app is available for download! 🥳 🚀
|
||||
|
||||
- macOS / iOS / iPadOS : Search for "Readest" on the [App Store][link-appstore], also available on TestFlight for beta test (send your Apple ID to readestapp@gmail.com to request access).
|
||||
- macOS / iOS / iPadOS : Search for "Readest" on the [App Store][link-appstore], also available on TestFlight for beta test (send your Apple ID to <readestapp@gmail.com> to request access).
|
||||
- Windows / Linux / Android: Visit [https://readest.com][link-website] or the [Releases on GitHub][link-gh-releases].
|
||||
- Web: Visit [https://web.readest.com][link-web-readest].
|
||||
|
||||
@@ -242,6 +246,20 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
|
||||
</p>
|
||||
</a>
|
||||
|
||||
## Support
|
||||
|
||||
If Readest has been useful to you, consider supporting its development. Your contribution helps us squash bugs faster, improve performance, and keep building great features.
|
||||
|
||||
### How to Donate
|
||||
|
||||
1. **GitHub Sponsors**
|
||||
Back the project directly on GitHub:
|
||||
👉 [https://github.com/sponsors/readest](https://github.com/sponsors/readest)
|
||||
|
||||
2. **Crypto Donations**
|
||||
Prefer crypto? You can donate here:
|
||||
👉 [https://donate.readest.com/](https://donate.readest.com/)
|
||||
|
||||
## License
|
||||
|
||||
Readest is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file for details.
|
||||
@@ -253,6 +271,7 @@ The following libraries and frameworks are used in this software:
|
||||
- [fflate](https://github.com/101arrowz/fflate), which is MIT licensed.
|
||||
- [PDF.js](https://github.com/mozilla/pdf.js), which is licensed under Apache License 2.0.
|
||||
- [daisyUI](https://github.com/saadeghi/daisyui), which is MIT licensed.
|
||||
- [marked](https://github.com/markedjs/marked), which is MIT licensed.
|
||||
- [next.js](https://github.com/vercel/next.js), which is MIT licensed.
|
||||
- [react-icons](https://github.com/react-icons/react-icons), which has various open-source licenses.
|
||||
- [react](https://github.com/facebook/react), which is MIT licensed.
|
||||
@@ -275,6 +294,8 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
|
||||
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
|
||||
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
|
||||
[badge-donate]: https://donate.readest.com/badge.svg
|
||||
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
|
||||
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
|
||||
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
|
||||
[link-web-readest]: https://web.readest.com
|
||||
|
||||
@@ -2,5 +2,8 @@ PDFJS_BUILD_PATH=../../packages/foliate-js/node_modules/pdfjs-dist/legacy/build
|
||||
PDFJS_FONTS_PATH=../../packages/foliate-js/node_modules/pdfjs-dist
|
||||
PDFJS_STYLE_PATH=../../packages/foliate-js/vendor/pdfjs
|
||||
|
||||
NEXT_PUBLIC_DEV_SUPABASE_URL=https://gxkhxxxeapexynpouyjz.supabase.co
|
||||
NEXT_PUBLIC_DEV_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd4a2h4eHhlYXBleHlucG91eWp6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ0MzAwNTksImV4cCI6MjA1MDAwNjA1OX0.jhinkQsimQoidsg_U59YD5ROw4PmMJQNKuyXbr4TiQA
|
||||
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64="aHR0cHM6Ly91cy5pLnBvc3Rob2cuY29t"
|
||||
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThpTmRSNTNsR1A3VFM3VGh4S08="
|
||||
|
||||
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
|
||||
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
|
||||
|
||||
@@ -6,6 +6,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
|
||||
NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824
|
||||
|
||||
NEXT_PUBLIC_API_BASE_URL=https://your-api-base-url.com
|
||||
|
||||
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY
|
||||
|
||||
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import withPWAInit from '@ducanh2912/next-pwa';
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
|
||||
const isDev = process.env['NODE_ENV'] === 'development';
|
||||
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
|
||||
|
||||
if (isDev) {
|
||||
initOpenNextCloudflareForDev();
|
||||
}
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Ensure Next.js uses SSG instead of SSR
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
// @ts-nocheck
|
||||
// default open-next.config.ts file created by @opennextjs/cloudflare
|
||||
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
|
||||
import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
|
||||
|
||||
import type { OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
|
||||
import cache from '@opennextjs/cloudflare/kv-cache';
|
||||
|
||||
const config: OpenNextConfig = {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: 'cloudflare-node',
|
||||
converter: 'edge',
|
||||
// set `incrementalCache` to "dummy" to disable KV cache
|
||||
incrementalCache: async () => cache,
|
||||
tagCache: 'dummy',
|
||||
queue: 'dummy',
|
||||
},
|
||||
},
|
||||
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: 'cloudflare-edge',
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default defineCloudflareConfig({
|
||||
incrementalCache: r2IncrementalCache,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.43",
|
||||
"version": "0.9.61",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -30,9 +30,11 @@
|
||||
"build-ios-appstore": "dotenv -e .env.ios-appstore.local -- tauri ios build --export-method app-store-connect",
|
||||
"release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh",
|
||||
"release-ios-appstore": "dotenv -e .env.ios-appstore.local -- bash scripts/release-ios-appstore.sh",
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare && wrangler dev",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare && wrangler deploy",
|
||||
"release-google-play": "dotenv -e .env.google-play.local -- bash scripts/release-google-play.sh",
|
||||
"config-wrangler": "sed -i \"s/\\${TRANSLATIONS_KV_ID}/$TRANSLATIONS_KV_ID/g\" wrangler.toml",
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -40,54 +42,60 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.735.0",
|
||||
"@ducanh2912/next-pwa": "^10.2.9",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.3.1",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.47.7",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-fs": "^2.2.1",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.4",
|
||||
"@tauri-apps/plugin-http": "^2.4.3",
|
||||
"@tauri-apps/plugin-log": "^2.4.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@supabase/supabase-js": "^2.49.10",
|
||||
"@tauri-apps/api": "2.6.0",
|
||||
"@tauri-apps/plugin-cli": "^2.3.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.5",
|
||||
"@tauri-apps/plugin-http": "^2.4.4",
|
||||
"@tauri-apps/plugin-log": "^2.5.1",
|
||||
"@tauri-apps/plugin-opener": "^2.3.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.2",
|
||||
"@tauri-apps/plugin-shell": "~2.2.2",
|
||||
"@tauri-apps/plugin-updater": "^2.8.1",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"clsx": "^2.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"cssbeautify": "^0.3.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"foliate-js": "workspace:*",
|
||||
"highlight.js": "^11.11.1",
|
||||
"i18next": "^24.2.0",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "15.2.4",
|
||||
"posthog-js": "^1.205.0",
|
||||
"marked": "^15.0.12",
|
||||
"next": "15.3.3",
|
||||
"posthog-js": "^1.246.0",
|
||||
"react": "19.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-window": "^1.8.11",
|
||||
"semver": "^7.7.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/cloudflare": "^0.5.12",
|
||||
"@tauri-apps/cli": "2.5.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.6.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
@@ -105,6 +113,6 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"wrangler": "^4.4.0"
|
||||
"wrangler": "^4.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"relation": [
|
||||
"delegate_permission/common.handle_all_urls",
|
||||
"delegate_permission/common.get_login_creds"
|
||||
],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.bilingify.readest",
|
||||
"sha256_cert_fingerprints": [
|
||||
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82"
|
||||
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82",
|
||||
"E0:E7:60:55:80:8D:3A:DE:A0:D1:CF:7C:20:85:40:A3:DD:4B:E6:4D:17:5C:0F:DE:26:57:7D:9C:5B:29:5F:51"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/_next/static/*
|
||||
Cache-Control: public,max-age=31536000,immutable
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "الرسوم المتحركة",
|
||||
"Annotate": "إضافة ملاحظة",
|
||||
"Apply": "تطبيق",
|
||||
"Are you sure to delete the selected books?": "هل أنت متأكد من رغبتك في حذف الكتب المحددة؟",
|
||||
"Auto Mode": "الوضع التلقائي",
|
||||
"Behavior": "السلوك",
|
||||
"Book": "الكتاب",
|
||||
@@ -19,8 +18,7 @@
|
||||
"Confirm Deletion": "تأكيد الحذف",
|
||||
"Copied to notebook": "تم النسخ إلى المفكرة",
|
||||
"Copy": "نسخ",
|
||||
"Custom CSS": "CSS مخصص",
|
||||
"Dark Mode": "الوضع المظلم",
|
||||
"Dark Mode": "الوضع الداكن",
|
||||
"Default": "الافتراضي",
|
||||
"Default Font": "الخط الافتراضي",
|
||||
"Default Font Size": "حجم الخط الافتراضي",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "القاموس",
|
||||
"Download Readest": "تحميل ريديست",
|
||||
"Edit": "تحرير",
|
||||
"Enter your custom CSS here...": "أدخل الـ CSS المخصص هنا...",
|
||||
"Excerpts": "مقتطفات",
|
||||
"Failed to import book(s): {{filenames}}": "فشل استيراد الكتاب/الكتب: {{filenames}}",
|
||||
"Fast": "سريع",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "مطابقة الكلمات الكاملة",
|
||||
"Maximum Number of Columns": "العدد الأقصى للأعمدة",
|
||||
"Minimum Font Size": "الحد الأدنى لحجم الخط",
|
||||
"Misc": "أخرى",
|
||||
"Monospace Font": "خط أحادي المسافة (Monospace)",
|
||||
"More Info": "مزيد من المعلومات",
|
||||
"Nord": "نورد",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "المكتبة الخاصة بك",
|
||||
"TTS not supported for PDF": "القراءة الصوتية غير مدعومة لملفات PDF",
|
||||
"Override Book Font": "تجاوز خط الكتاب",
|
||||
"Vertical Margins (px)": "هوامش عمودية (بكسل)",
|
||||
"Horizontal Margins (%)": "هوامش أفقية (%)",
|
||||
"Apply to All Books": "تطبيق على جميع الكتب",
|
||||
"Apply to This Book": "تطبيق على هذا الكتاب",
|
||||
"Unable to fetch the translation. Try again later.": "تعذر جلب الترجمة. حاول مرة أخرى لاحقًا.",
|
||||
@@ -188,28 +182,27 @@
|
||||
"Sign in with Apple": "تسجيل الدخول عبر Apple",
|
||||
"Sign in with GitHub": "تسجيل الدخول عبر GitHub",
|
||||
"Account": "الحساب",
|
||||
"Cloud Storage": "التخزين السحابي",
|
||||
"Failed to delete user. Please try again later.": "فشل حذف المستخدم. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"Unlimited Offline/Online Reading": "قراءة غير محدودة بدون اتصال/متصلة بالإنترنت",
|
||||
"Unlimited Cloud Sync Devices": "أجهزة مزامنة سحابية غير محدودة",
|
||||
"Unlimited Offline/Online Reading": "القراءة بدون حدود للكتب المتوفر على الجهاز أو عبر الانترنت",
|
||||
"Unlimited Cloud Sync Devices": "مزامنة عبر السحابة لعدد غير محدود من الأجهزة",
|
||||
"Essential Text-to-Speech Voices": "أصوات أساسية لتحويل النص إلى كلام",
|
||||
"DeepL Free Access": "وصول مجاني إلى DeepL",
|
||||
"DeepL Free Access": "وصول مجاني لـ DeepL",
|
||||
"Community Support": "دعم المجتمع",
|
||||
"500 MB Cloud Sync Space": "مساحة مزامنة سحابية 500 ميجابايت",
|
||||
"Includes All Free Tier Benefits": "يتضمن جميع مزايا الفئة المجانية",
|
||||
"500 MB Cloud Sync Space": "مزامنة سحابية بمساحة تخزين 500 ميجابايت",
|
||||
"Includes All Free Tier Benefits": "تشمل جميع مزايا الفئة المجانية",
|
||||
"AI Summaries": "ملخصات الذكاء الاصطناعي",
|
||||
"AI Translations": "ترجمات الذكاء الاصطناعي",
|
||||
"Priority Support": "دعم ذو أولوية",
|
||||
"DeepL Pro Access": "وصول إلى DeepL Pro",
|
||||
"DeepL Pro Access": "وصول لميزات DeepL Pro",
|
||||
"Expanded Text-to-Speech Voices": "أصوات موسعة لتحويل النص إلى كلام",
|
||||
"2000 MB Cloud Sync Space": "مساحة مزامنة سحابية 2000 ميجابايت",
|
||||
"Includes All Plus Tier Benefits": "يتضمن جميع مزايا فئة Plus",
|
||||
"2000 MB Cloud Sync Space": "مزامنة سحابية بمساحة تخزين 2000 ميجابايت",
|
||||
"Includes All Plus Tier Benefits": "تشمل جميع مزايا فئة Plus",
|
||||
"Unlimited AI Summaries": "ملخصات ذكاء اصطناعي غير محدودة",
|
||||
"Unlimited AI Translations": "ترجمات ذكاء اصطناعي غير محدودة",
|
||||
"Unlimited DeepL Pro Access": "وصول غير محدود إلى DeepL Pro",
|
||||
"Unlimited DeepL Pro Access": "وصول غير محدود لميزات DeepL Pro",
|
||||
"Advanced AI Tools": "أدوات ذكاء اصطناعي متقدمة",
|
||||
"Early Feature Access": "وصول مبكر للميزات",
|
||||
"10 GB Cloud Sync Space": "مساحة مزامنة سحابية 10 جيجابايت",
|
||||
"10 GB Cloud Sync Space": "مزامنة سحابية بمساحة تخزين 10 جيجابايت",
|
||||
"Loading profile...": "جارٍ تحميل الملف الشخصي...",
|
||||
"Features": "الميزات",
|
||||
"Delete Account": "حذف الحساب",
|
||||
@@ -225,7 +218,7 @@
|
||||
"Continuous Scroll": "التمرير المستمر",
|
||||
"Fullscreen": "ملء الشاشة",
|
||||
"No supported files found. Supported formats: {{formats}}": "لم يتم العثور على ملفات مدعومة. الصيغ المدعومة: {{formats}}",
|
||||
"Drop to Import Books": "اسحب لاستيراد الكتب",
|
||||
"Drop to Import Books": "قم بالسحب والإسقاط لاستيراد الكتب",
|
||||
"Custom": "مخصص",
|
||||
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "العالم مسرح،\nوالناس فيه ممثلون؛\nلهم مخارج ومداخل،\nوالإنسان في حياته يلعب دورًا كثيرًا،\nوأفعاله تمر بسبع مراحل.\n\n— ويليام شكسبير",
|
||||
"Custom Theme": "سمة مخصصة",
|
||||
@@ -238,8 +231,8 @@
|
||||
"Double Border": "حدود مزدوجة",
|
||||
"Border Color": "لون الحدود",
|
||||
"Border Frame": "إطار الحدود",
|
||||
"Show Header": "عرض الرأس",
|
||||
"Show Footer": "عرض التذييل",
|
||||
"Show Header": "إظهار الترويسة",
|
||||
"Show Footer": "إظهار التذييل",
|
||||
"Small": "صغير",
|
||||
"Large": "كبير",
|
||||
"Auto": "تلقائي",
|
||||
@@ -254,24 +247,24 @@
|
||||
"Auto Import on File Open": "استيراد تلقائي عند فتح الملف",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "لم يتم التعرف على فصول.",
|
||||
"No chapters detected.": "لم يتم التعرف على أي فصول.",
|
||||
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
|
||||
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم.",
|
||||
"This book format is not supported.": "تنسيق الكتاب هذا غير مدعوم.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "تعذر جلب الترجمة. يرجى تسجيل الدخول أولاً ثم المحاولة مرة أخرى.",
|
||||
"Group": "تجميع",
|
||||
"Always on Top": "دائمًا في المقدمة",
|
||||
"No Timeout": "لا مهلة",
|
||||
"No Timeout": "بدون مهلة",
|
||||
"{{value}} minute": "{{value}} دقيقة",
|
||||
"{{value}} minutes": "{{value}} دقائق",
|
||||
"{{value}} hour": "{{value}} ساعة",
|
||||
"{{value}} hours": "{{value}} ساعات",
|
||||
"CJK Font": "خط CJK",
|
||||
"Clear Search": "مسح البحث",
|
||||
"Header & Footer": "الرأس والتذييل",
|
||||
"Header & Footer": "الترويسة والتذييل",
|
||||
"Apply also in Scrolled Mode": "تطبيق أيضًا في وضع التمرير",
|
||||
"A new version of Readest is Available!": "إصدار جديد من ريديست متاح!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "إصدار ريديست {{newVersion}} متاح (الإصدار المثبت {{currentVersion}}).",
|
||||
"Download and install now?": "هل ترغب في تنزيل وتثبيت الآن؟",
|
||||
"A new version of Readest is available!": "يتوفر إصدار جديد من Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "إصدار {{newVersion}} من ريديست متاح للتنزيل (الإصدار المثبت حاليًا {{currentVersion}}).",
|
||||
"Download and install now?": "هل ترغب في تنزيله وتثبيته الآن؟",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
|
||||
"Download finished": "اكتمل التنزيل",
|
||||
"DOWNLOAD & INSTALL": "تنزيل وتثبيت",
|
||||
@@ -298,19 +291,104 @@
|
||||
"Swap Clicks Area": "تبديل منطقة النقرات",
|
||||
"Screen": "الشاشة",
|
||||
"Orientation": "الاتجاه",
|
||||
"Portrait": "العمودي",
|
||||
"Landscape": "الأفقي",
|
||||
"Portrait": "عمودي",
|
||||
"Landscape": "أفقي",
|
||||
"Open Last Book on Start": "فتح آخر كتاب عند البدء",
|
||||
"Checking for updates...": "جارٍ التحقق من التحديثات...",
|
||||
"Error checking for updates": "خطأ في التحقق من التحديثات",
|
||||
"Details": "التفاصيل",
|
||||
"Cancel Selection": "إلغاء التحديد",
|
||||
"File Size:": "حجم الملف:",
|
||||
"Auto Detect": "الكشف التلقائي",
|
||||
"Touch": "اللمس",
|
||||
"Scrolling Overlap (px)": "تداخل التمرير (بكسل)",
|
||||
"Next Section": "القسم التالي",
|
||||
"Previous Section": "القسم السابق",
|
||||
"Next Page": "الصفحة التالية",
|
||||
"Previous Page": "الصفحة السابقة"
|
||||
"Previous Page": "الصفحة السابقة",
|
||||
"Are you sure to delete {{count}} selected book(s)?_zero": "هل أنت متأكد من حذف الكتب المحددة؟",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "هل أنت متأكد من حذف الكتاب المحدد؟",
|
||||
"Are you sure to delete {{count}} selected book(s)?_two": "هل أنت متأكد من حذف الكتابين المحددين؟",
|
||||
"Are you sure to delete {{count}} selected book(s)?_few": "هل أنت متأكد من حذف {{count}} كتب محددة؟",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "هل أنت متأكد من حذف {{count}} كتابًا محددًا؟",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "هل أنت متأكد من حذف {{count}} من الكتب المحددة؟",
|
||||
"Are you sure to delete the selected book?": "هل أنت متأكد من حذف الكتاب المحدد؟",
|
||||
"Deselect": "إلغاء التحديد",
|
||||
"Select All": "تحديد الكل",
|
||||
"No translation available.": "لا توجد ترجمة متاحة.",
|
||||
"Translated by {{provider}}.": "تمت الترجمة بواسطة {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "ترجمة Google",
|
||||
"Azure Translator": "مترجم Azure",
|
||||
"Invert Image In Dark Mode": "عكس ألوان الصورة في الوضع الداكن",
|
||||
"Help improve Readest": "ساعد في تحسين ريديست",
|
||||
"Sharing anonymized statistics": "مشاركة إحصائيات مجهولة الهوية",
|
||||
"Interface Language": "لغة الواجهة",
|
||||
"Translation": "الترجمة",
|
||||
"Enable Translation": "تمكين الترجمة",
|
||||
"Translation Service": "خدمة الترجمة",
|
||||
"Translate To": "ترجمة إلى",
|
||||
"Disable Translation": "تعطيل الترجمة",
|
||||
"Scroll": "تمرير",
|
||||
"Overlap Pixels": "تداخل البكسلات",
|
||||
"Click": "نقر",
|
||||
"System Language": "لغة النظام",
|
||||
"Security": "الأمان",
|
||||
"Allow JavaScript": "السماح بـ JavaScript",
|
||||
"Enable only if you trust the file.": "قم بالتمكين فقط في حال وثوقك بالملف.",
|
||||
"Sort TOC by Page": "ترتيب جدول المحتويات حسب الصفحة",
|
||||
"Search in {{count}} Book(s)..._zero": "بحث في الكتب...",
|
||||
"Search in {{count}} Book(s)..._one": "بحث في كتاب...",
|
||||
"Search in {{count}} Book(s)..._two": "بحث في كتابين...",
|
||||
"Search in {{count}} Book(s)..._few": "بحث في {{count}} كتب...",
|
||||
"Search in {{count}} Book(s)..._many": "بحث في {{count}} كتابًا...",
|
||||
"Search in {{count}} Book(s)..._other": "بحث في {{count}} من الكتب...",
|
||||
"No notes match your search": "لا توجد ملاحظات تطابق بحثك",
|
||||
"Search notes and excerpts...": "بحث في الملاحظات والمقتطفات...",
|
||||
"Sign in to Sync": "قم بتسجيل الدخول للمزامنة",
|
||||
"Synced at {{time}}": "تمت المزامنة في {{time}}",
|
||||
"Never synced": "لم تتم المزامنة بعد",
|
||||
"Show Remaining Time": "إظهار الوقت المتبقي",
|
||||
"Show Page Number": "إظهار رقم الصفحة",
|
||||
"{{time}} min left in chapter": "{{time}} دقيقة متبقية حتى نهاية الفصل",
|
||||
"Override Book Color": "تجاوز لون الكتاب",
|
||||
"Login Required": "يتطلب الأمر تسجيل الدخول",
|
||||
"Quota Exceeded": "تجاوزت الحصة المخصصة",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "تم استخدام ما نسبته {{percentage}}% من إجمالي أحرف الترجمة المسموح بها يوميًا.",
|
||||
"Translation Characters": "أحرف الترجمة",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "تم استنفاد حصة الترجمة اليومية. قم بتحديد خدمة ترجمة أخرى للاستمرار.",
|
||||
"{{engine}}: {{count}} voices_zero": "{{engine}}: لا توجد أصوات",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: صوت واحد",
|
||||
"{{engine}}: {{count}} voices_two": "{{engine}}: صوتان",
|
||||
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} أصوات",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} صوتاً",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} صوت",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "حدث خطأ ما. لا تقلق، لقد تم إبلاغ فريقنا ونحن نعمل على إصلاحه.",
|
||||
"Error Details:": "تفاصيل الخطأ:",
|
||||
"Try Again": "حاول مرة أخرى",
|
||||
"Need help?": "تحتاج مساعدة؟",
|
||||
"Contact Support": "اتصل بالدعم الفني",
|
||||
"Code Highlighting": "تمييز الكود",
|
||||
"Enable Highlighting": "تمكين التمييز",
|
||||
"Code Language": "لغة الكود",
|
||||
"Top Margin (px)": "الهامش العلوي (بكسل)",
|
||||
"Bottom Margin (px)": "الهامش السفلي (بكسل)",
|
||||
"Right Margin (px)": "الهامش الأيمن (بكسل)",
|
||||
"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 مخصص لواجهة القارئ",
|
||||
"Enter CSS for reader interface styling...": "أدخل CSS لتنسيق واجهة القارئ...",
|
||||
"Crop": "قص",
|
||||
"Book Covers": "أغلفة الكتب",
|
||||
"Fit": "ملاءمة",
|
||||
"Reset {{settings}}": "إعادة تعيين {{settings}}",
|
||||
"Reset Settings": "إعادة تعيين الإعدادات",
|
||||
"{{count}} pages left in chapter_zero": "لم يتبق أي صفحات في هذا الفصل",
|
||||
"{{count}} pages left in chapter_one": "تبقّت صفحة واحدة في هذا الفصل",
|
||||
"{{count}} pages left in chapter_two": "تبقّت صفحتان في هذا الفصل",
|
||||
"{{count}} pages left in chapter_few": "تبقّت {{count}} صفحات في هذا الفصل",
|
||||
"{{count}} pages left in chapter_many": "تبقّت {{count}} صفحة في هذا الفصل",
|
||||
"{{count}} pages left in chapter_other": "تبقّى {{count}} صفحة في هذا الفصل",
|
||||
"Show Remaining Pages": "عرض الصفحات المتبقية"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animation",
|
||||
"Annotate": "Annotieren",
|
||||
"Apply": "Anwenden",
|
||||
"Are you sure to delete the selected books?": "Möchten Sie die ausgewählten Bücher wirklich löschen?",
|
||||
"Auto Mode": "Automatischer Modus",
|
||||
"Behavior": "Verhalten",
|
||||
"Book": "Buch",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Löschen bestätigen",
|
||||
"Copied to notebook": "In Notizbuch kopiert",
|
||||
"Copy": "Kopieren",
|
||||
"Custom CSS": "Benutzerdefiniertes CSS",
|
||||
"Dark Mode": "Dunkelmodus",
|
||||
"Default": "Standard",
|
||||
"Default Font": "Standardschriftart",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Wörterbuch",
|
||||
"Download Readest": "Readest herunterladen",
|
||||
"Edit": "Bearbeiten",
|
||||
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
|
||||
"Excerpts": "Auszüge",
|
||||
"Failed to import book(s): {{filenames}}": "Fehler beim Importieren von Buch/Büchern: {{filenames}}",
|
||||
"Fast": "Schnell",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Ganze Wörter",
|
||||
"Maximum Number of Columns": "Maximale Spaltenanzahl",
|
||||
"Minimum Font Size": "Minimale Schriftgröße",
|
||||
"Misc": "Sonstiges",
|
||||
"Monospace Font": "Monospace-Schriftart",
|
||||
"More Info": "Weitere Informationen",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Ihre Bibliothek",
|
||||
"TTS not supported for PDF": "TTS wird für PDF nicht unterstützt",
|
||||
"Override Book Font": "Buch-Schriftart überschreiben",
|
||||
"Vertical Margins (px)": "Vertikale Ränder (px)",
|
||||
"Horizontal Margins (%)": "Horizontale Ränder (%)",
|
||||
"Apply to All Books": "Auf alle Bücher anwenden",
|
||||
"Apply to This Book": "Auf dieses Buch anwenden",
|
||||
"Unable to fetch the translation. Try again later.": "Übersetzung konnte nicht abgerufen werden. Versuchen Sie es später erneut.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Mit Apple anmelden",
|
||||
"Sign in with GitHub": "Mit GitHub anmelden",
|
||||
"Account": "Konto",
|
||||
"Cloud Storage": "Cloud-Speicher",
|
||||
"Failed to delete user. Please try again later.": "Benutzer konnte nicht gelöscht werden. Bitte versuchen Sie es später erneut.",
|
||||
"Unlimited Offline/Online Reading": "Unbegrenztes Offline/Online Lesen",
|
||||
"Unlimited Cloud Sync Devices": "Unbegrenzte Cloud-Synchronisierungsgeräte",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Suche löschen",
|
||||
"Header & Footer": "Kopf- & Fußzeile",
|
||||
"Apply also in Scrolled Mode": "Auch im Scroll-Modus anwenden",
|
||||
"A new version of Readest is Available!": "Eine neue Version von Readest ist verfügbar!",
|
||||
"A new version of Readest is available!": "Eine neue Version von Readest ist verfügbar!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} ist verfügbar (installierte Version {{currentVersion}}).",
|
||||
"Download and install now?": "Jetzt herunterladen und installieren?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Herunterladen {{downloaded}} von {{contentLength}}",
|
||||
@@ -304,13 +297,82 @@
|
||||
"Checking for updates...": "Auf Updates prüfen...",
|
||||
"Error checking for updates": "Fehler beim Überprüfen auf Updates",
|
||||
"Details": "Details",
|
||||
"Cancel Selection": "Auswahl abbrechen",
|
||||
"File Size:": "Dateigröße:",
|
||||
"Auto Detect": "Automatisch erkennen",
|
||||
"Touch": "Berührung",
|
||||
"Scrolling Overlap (px)": "Scroll-Überlappung (px)",
|
||||
"Next Section": "Nächster Abschnitt",
|
||||
"Previous Section": "Vorheriger Abschnitt",
|
||||
"Next Page": "Nächste Seite",
|
||||
"Previous Page": "Vorherige Seite"
|
||||
"Previous Page": "Vorherige Seite",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Möchtest du das ausgewählte Buch wirklich löschen?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Möchtest du die {{count}} ausgewählten Bücher wirklich löschen?",
|
||||
"Are you sure to delete the selected book?": "Möchtest du das ausgewählte Buch wirklich löschen?",
|
||||
"Deselect": "Aufheben",
|
||||
"Select All": "Alle",
|
||||
"No translation available.": "Keine Übersetzung verfügbar.",
|
||||
"Translated by {{provider}}.": "Übersetzt von {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Übersetzer",
|
||||
"Azure Translator": "Azure Übersetzer",
|
||||
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln",
|
||||
"Help improve Readest": "Readest verbessern helfen",
|
||||
"Sharing anonymized statistics": "Anonymisierte Statistiken teilen",
|
||||
"Interface Language": "Benutzeroberflächensprache",
|
||||
"Translation": "Übersetzung",
|
||||
"Enable Translation": "Übersetzung aktivieren",
|
||||
"Translation Service": "Übersetzungsdienst",
|
||||
"Translate To": "Übersetzen nach",
|
||||
"Disable Translation": "Übersetzung deaktivieren",
|
||||
"Scroll": "Scrollen",
|
||||
"Overlap Pixels": "Überlappungspixel",
|
||||
"Click": "Tippen",
|
||||
"System Language": "Systemsprache",
|
||||
"Security": "Sicherheit",
|
||||
"Allow JavaScript": "JavaScript zulassen",
|
||||
"Enable only if you trust the file.": "Nur aktivieren, wenn Sie der Datei vertrauen.",
|
||||
"Sort TOC by Page": "TOC nach Seite sortieren",
|
||||
"Search in {{count}} Book(s)..._one": "Suche in {{count}} Buch...",
|
||||
"Search in {{count}} Book(s)..._other": "Suche in {{count}} Büchern...",
|
||||
"No notes match your search": "Noch keine Notizen gefunden",
|
||||
"Search notes and excerpts...": "Notizen und Auszüge suchen...",
|
||||
"Sign in to Sync": "Anmelden zum Synchronisieren",
|
||||
"Synced at {{time}}": "Synchronisiert um {{time}}",
|
||||
"Never synced": "Nie synchronisiert",
|
||||
"Show Remaining Time": "Verbleibende Zeit anzeigen",
|
||||
"Show Page Number": "Seitenzahl anzeigen",
|
||||
"{{time}} min left in chapter": "{{time}} min verbleibend im Kapitel",
|
||||
"Override Book Color": "Farbe des Buches überschreiben",
|
||||
"Login Required": "Anmeldung nötig",
|
||||
"Quota Exceeded": "Limit überschritten",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% der täglichen Übersetzungszeichen verwendet.",
|
||||
"Translation Characters": "Übersetzungszeichen",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Das tägliche Übersetzungslimit wurde erreicht. Wählen Sie einen anderen Übersetzungsdienst, um fortzufahren.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} Stimme",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} Stimmen",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Etwas ist schief gelaufen. Keine Sorge, unser Team wurde benachrichtigt und arbeitet an einer Lösung.",
|
||||
"Error Details:": "Fehlerdetails:",
|
||||
"Try Again": "Erneut versuchen",
|
||||
"Need help?": "Brauchen Sie Hilfe?",
|
||||
"Contact Support": "Support kontaktieren",
|
||||
"Code Highlighting": "Code-Hervorhebung",
|
||||
"Enable Highlighting": "Code-Hervorhebung aktivieren",
|
||||
"Code Language": "Programmiersprache",
|
||||
"Top Margin (px)": "Oberer Rand (px)",
|
||||
"Bottom Margin (px)": "Unterer Rand (px)",
|
||||
"Right Margin (px)": "Rechter Rand (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "CSS für die Leseroberfläche eingeben...",
|
||||
"Crop": "Zuschneiden",
|
||||
"Book Covers": "Buchcover",
|
||||
"Fit": "Anpassen",
|
||||
"Reset {{settings}}": "{{settings}} zurücksetzen",
|
||||
"Reset Settings": "Einstellungen zurücksetzen",
|
||||
"{{count}} pages left in chapter_one": "{{count}} Seite verbleibend im Kapitel",
|
||||
"{{count}} pages left in chapter_other": "{{count}} Seiten verbleibend im Kapitel",
|
||||
"Show Remaining Pages": "Verbleibende Seiten anzeigen"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Κινούμενα σχέδια",
|
||||
"Annotate": "Σχολιασμός",
|
||||
"Apply": "Εφαρμογή",
|
||||
"Are you sure to delete the selected books?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα βιβλία;",
|
||||
"Auto Mode": "Αυτόματη λειτουργία",
|
||||
"Behavior": "Συμπεριφορά",
|
||||
"Book": "Βιβλίο",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Επιβεβαίωση διαγραφής",
|
||||
"Copied to notebook": "Αντιγράφηκε στο σημειωματάριο",
|
||||
"Copy": "Αντιγραφή",
|
||||
"Custom CSS": "Προσαρμοσμένο CSS",
|
||||
"Dark Mode": "Σκοτεινή λειτουργία",
|
||||
"Default": "Προεπιλογή",
|
||||
"Default Font": "Προεπιλεγμένη γραμματοσειρά",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Λεξικό",
|
||||
"Download Readest": "Λήψη Readest",
|
||||
"Edit": "Επεξεργασία",
|
||||
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
|
||||
"Excerpts": "Αποσπάσματα",
|
||||
"Failed to import book(s): {{filenames}}": "Αποτυχία εισαγωγής βιβλίου(ων): {{filenames}}",
|
||||
"Fast": "Γρήγορα",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Ταίριασμα ολόκληρων λέξεων",
|
||||
"Maximum Number of Columns": "Μέγιστος αριθμός στηλών",
|
||||
"Minimum Font Size": "Ελάχιστο μέγεθος γραμματοσειράς",
|
||||
"Misc": "Διάφορα",
|
||||
"Monospace Font": "Γραμματοσειρά σταθερού πλάτους",
|
||||
"More Info": "Περισσότερες πληροφορίες",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Η βιβλιοθήκη σας",
|
||||
"TTS not supported for PDF": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται για αρχεία PDF",
|
||||
"Override Book Font": "Παράκαμψη γραμματοσειράς βιβλίου",
|
||||
"Vertical Margins (px)": "Κάθετα περιθώρια (px)",
|
||||
"Horizontal Margins (%)": "Οριζόντια περιθώρια (%)",
|
||||
"Apply to All Books": "Εφαρμογή σε όλα τα βιβλία",
|
||||
"Apply to This Book": "Εφαρμογή σε αυτό το βιβλίο",
|
||||
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Σύνδεση με Apple",
|
||||
"Sign in with GitHub": "Σύνδεση με GitHub",
|
||||
"Account": "Λογαριασμός",
|
||||
"Cloud Storage": "Αποθηκευτικός χώρος Cloud",
|
||||
"Failed to delete user. Please try again later.": "Αποτυχία διαγραφής χρήστη. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"Unlimited Offline/Online Reading": "Απεριόριστη ανάγνωση εκτός/εντός σύνδεσης",
|
||||
"Unlimited Cloud Sync Devices": "Απεριόριστες συσκευές συγχρονισμού Cloud",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Καθαρισμός αναζήτησης",
|
||||
"Header & Footer": "Κεφαλίδα & Υποσέλιδο",
|
||||
"Apply also in Scrolled Mode": "Εφαρμογή και σε λειτουργία κύλισης",
|
||||
"A new version of Readest is Available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
|
||||
"A new version of Readest is available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Η έκδοση Readest {{newVersion}} είναι διαθέσιμη (έκδοση που είναι εγκατεστημένη {{currentVersion}}).",
|
||||
"Download and install now?": "Θέλετε να την κατεβάσετε και να την εγκαταστήσετε τώρα;",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
|
||||
@@ -304,13 +297,82 @@
|
||||
"Checking for updates...": "Έλεγχος για ενημερώσεις...",
|
||||
"Error checking for updates": "Σφάλμα κατά τον έλεγχο για ενημερώσεις",
|
||||
"Details": "Πληρ.",
|
||||
"Cancel Selection": "Ακύρωση επιλογής",
|
||||
"File Size:": "Μέγεθος αρχείου:",
|
||||
"Auto Detect": "Αυτόματη ανίχνευση",
|
||||
"Touch": "Αφή",
|
||||
"Scrolling Overlap (px)": "Επικάλυψη κύλισης (px)",
|
||||
"Next Section": "Επόμενη ενότητα",
|
||||
"Previous Section": "Προηγούμενη ενότητα",
|
||||
"Next Page": "Επόμενη σελίδα",
|
||||
"Previous Page": "Προηγούμενη σελίδα"
|
||||
"Previous Page": "Προηγούμενη σελίδα",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Είστε σίγουροι ότι θέλετε να διαγράψετε το επιλεγμένο βιβλίο;",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Είστε σίγουροι ότι θέλετε να διαγράψετε τα {{count}} επιλεγμένα βιβλία;",
|
||||
"Are you sure to delete the selected book?": "Είστε σίγουροι ότι θέλετε να διαγράψετε το επιλεγμένο βιβλίο;",
|
||||
"Deselect": "Άρση",
|
||||
"Select All": "Όλα",
|
||||
"No translation available.": "Δεν είναι διαθέσιμη καμία μετάφραση.",
|
||||
"Translated by {{provider}}.": "Μεταφρασμένο από {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Μετάφραση Google",
|
||||
"Azure Translator": "Μεταφραστής Azure",
|
||||
"Invert Image In Dark Mode": "Αντιστροφή σε σκούρο",
|
||||
"Help improve Readest": "Βοηθήστε στη βελτίωση του Readest",
|
||||
"Sharing anonymized statistics": "Μοιράζομαι ανώνυμες στατιστικές",
|
||||
"Interface Language": "Γλώσσα διεπαφής",
|
||||
"Translation": "Μετάφραση",
|
||||
"Enable Translation": "Ενεργοποίηση μετάφρασης",
|
||||
"Translation Service": "Υπηρεσία μετάφρασης",
|
||||
"Translate To": "Μετάφραση σε",
|
||||
"Disable Translation": "Απενεργοποίηση μετάφρασης",
|
||||
"Scroll": "Κύλιση",
|
||||
"Overlap Pixels": "Επικάλυψη εικονοστοιχείων",
|
||||
"Click": "Κλικ",
|
||||
"System Language": "Γλώσσα συστήματος",
|
||||
"Security": "Ασφάλεια",
|
||||
"Allow JavaScript": "Επιτρέψτε JavaScript",
|
||||
"Enable only if you trust the file.": "Ενεργοποιήστε μόνο εάν εμπιστεύεστε το αρχείο.",
|
||||
"Sort TOC by Page": "Ταξινόμηση TOC κατά σελίδα",
|
||||
"Search in {{count}} Book(s)..._one": "Αναζήτηση σε {{count}} Βιβλίο...",
|
||||
"Search in {{count}} Book(s)..._other": "Αναζήτηση σε {{count}} Βιβλία...",
|
||||
"No notes match your search": "Καμία σημείωση δεν βρέθηκε",
|
||||
"Search notes and excerpts...": "Αναζήτηση σημειώσεων...",
|
||||
"Sign in to Sync": "Σύνδεση για συγχρονισμό",
|
||||
"Synced at {{time}}": "Συγχρονίστηκε στις {{time}}",
|
||||
"Never synced": "Ποτέ δεν συγχρονίστηκε",
|
||||
"Show Remaining Time": "Εμφάνιση υπολειπόμενου χρόνου",
|
||||
"Show Page Number": "Εμφάνιση αριθμού σελίδας",
|
||||
"{{time}} min left in chapter": "{{time}} λεπτά απομένουν στο κεφάλαιο",
|
||||
"Override Book Color": "Παράκαμψη χρώματος βιβλίου",
|
||||
"Login Required": "Απαιτείται σύνδεση",
|
||||
"Quota Exceeded": "Υπέρβαση ορίου",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% των ημερήσιων χαρακτήρων μετάφρασης χρησιμοποιήθηκαν.",
|
||||
"Translation Characters": "Χαρακτήρες μετάφρασης",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Έχετε φτάσει το ημερήσιο όριο χαρακτήρων μετάφρασης. Επιλέξτε άλλη υπηρεσία μετάφρασης για να συνεχίσετε.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} φωνή",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} φωνές",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Κάτι πήγε στραβά. Μην ανησυχείτε, η ομάδα μας έχει ενημερωθεί και εργαζόμαστε για μια λύση.",
|
||||
"Error Details:": "Λεπτομέρειες σφάλματος:",
|
||||
"Try Again": "Δοκιμάστε ξανά",
|
||||
"Need help?": "Χρειάζεστε βοήθεια;",
|
||||
"Contact Support": "Επικοινωνήστε με την υποστήριξη",
|
||||
"Code Highlighting": "Επισήμανση κώδικα",
|
||||
"Enable Highlighting": "Ενεργοποίηση επισήμανσης",
|
||||
"Code Language": "Γλώσσα κώδικα",
|
||||
"Top Margin (px)": "Πάνω περιθώριο (px)",
|
||||
"Bottom Margin (px)": "Κάτω περιθώριο (px)",
|
||||
"Right Margin (px)": "Δεξί περιθώριο (px)",
|
||||
"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 διεπαφής αναγνώστη",
|
||||
"Enter CSS for reader interface styling...": "Εισαγάγετε CSS για στυλ διεπαφής αναγνώστη...",
|
||||
"Crop": "Περικοπή",
|
||||
"Book Covers": "Εξώφυλλα βιβλίων",
|
||||
"Fit": "Προσαρμογή",
|
||||
"Reset {{settings}}": "Επαναφορά {{settings}}",
|
||||
"Reset Settings": "Επαναφορά ρυθμίσεων",
|
||||
"{{count}} pages left in chapter_one": "Μένει {{count}} σελίδα",
|
||||
"{{count}} pages left in chapter_other": "Μένουν {{count}} σελίδες",
|
||||
"Show Remaining Pages": "Εμφάνιση υπολοίπων"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC"
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Are you sure to delete {{count}} selected book?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Are you sure to delete {{count}} selected books?",
|
||||
"Search in {{count}} Book(s)..._one": "Search in {{count}} book...",
|
||||
"Search in {{count}} Book(s)..._other": "Search in {{count}} books...",
|
||||
"{{count}} pages left in chapter_one": "{{count}} page left in chapter",
|
||||
"{{count}} pages left in chapter_other": "{{count}} pages left in chapter"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animación",
|
||||
"Annotate": "Anotar",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "¿Estás seguro de eliminar los libros seleccionados?",
|
||||
"Auto Mode": "Modo automático",
|
||||
"Behavior": "Comportamiento",
|
||||
"Book": "Libro",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Confirmar eliminación",
|
||||
"Copied to notebook": "Copiado al cuaderno",
|
||||
"Copy": "Copiar",
|
||||
"Custom CSS": "CSS personalizado",
|
||||
"Dark Mode": "Modo oscuro",
|
||||
"Default": "Predeterminado",
|
||||
"Default Font": "Fuente predeterminada",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Diccionario",
|
||||
"Download Readest": "Descargar Readest",
|
||||
"Edit": "Editar",
|
||||
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
|
||||
"Excerpts": "Extractos",
|
||||
"Failed to import book(s): {{filenames}}": "Error al importar libro(s): {{filenames}}",
|
||||
"Fast": "Rápido",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Coincidir palabras completas",
|
||||
"Maximum Number of Columns": "Número máximo de columnas",
|
||||
"Minimum Font Size": "Tamaño de fuente mínimo",
|
||||
"Misc": "Varios",
|
||||
"Monospace Font": "Fuente monoespaciada",
|
||||
"More Info": "Más información",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Tu biblioteca",
|
||||
"TTS not supported for PDF": "TTS no es compatible con PDF",
|
||||
"Override Book Font": "Sobrescribir fuente del libro",
|
||||
"Vertical Margins (px)": "Márgenes verticales (px)",
|
||||
"Horizontal Margins (%)": "Márgenes horizontales (%)",
|
||||
"Apply to All Books": "Aplicar a todos los libros",
|
||||
"Apply to This Book": "Aplicar a este libro",
|
||||
"Unable to fetch the translation. Try again later.": "No se puede obtener la traducción. Inténtalo de nuevo más tarde.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Iniciar sesión con Apple",
|
||||
"Sign in with GitHub": "Iniciar sesión con GitHub",
|
||||
"Account": "Cuenta",
|
||||
"Cloud Storage": "Almacenamiento en la nube",
|
||||
"Failed to delete user. Please try again later.": "Error al eliminar usuario. Por favor, inténtelo de nuevo más tarde.",
|
||||
"Unlimited Offline/Online Reading": "Lectura sin límites en línea/sin conexión",
|
||||
"Unlimited Cloud Sync Devices": "Sincronización ilimitada de dispositivos en la nube",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Limpiar búsqueda",
|
||||
"Header & Footer": "Encabezado y pie de página",
|
||||
"Apply also in Scrolled Mode": "Aplicar también en modo desplazamiento",
|
||||
"A new version of Readest is Available!": "¡Una nueva versión de Readest está disponible!",
|
||||
"A new version of Readest is available!": "¡Una nueva versión de Readest está disponible!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponible (versión instalada {{currentVersion}}).",
|
||||
"Download and install now?": "Descargar e instalar ahora?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Descargando {{downloaded}} de {{contentLength}}",
|
||||
@@ -304,13 +297,86 @@
|
||||
"Checking for updates...": "Comprobando actualizaciones...",
|
||||
"Error checking for updates": "Error al comprobar actualizaciones",
|
||||
"Details": "Detalles",
|
||||
"Cancel Selection": "Cancelar selección",
|
||||
"File Size:": "Peso del archivo:",
|
||||
"Auto Detect": "Detección automática",
|
||||
"Touch": "Toque",
|
||||
"Scrolling Overlap (px)": "Superposición de desplazamiento (px)",
|
||||
"Next Section": "Siguiente sección",
|
||||
"Previous Section": "Sección anterior",
|
||||
"Next Page": "Página siguiente",
|
||||
"Previous Page": "Página anterior"
|
||||
"Previous Page": "Página anterior",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "¿Seguro que quieres eliminar el libro seleccionado?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "¿Seguro que quieres eliminar los {{count}} libros seleccionados?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "¿Seguro que quieres eliminar los {{count}} libros seleccionados?",
|
||||
"Are you sure to delete the selected book?": "¿Seguro que quieres eliminar el libro seleccionado?",
|
||||
"Deselect": "Quitar",
|
||||
"Select All": "Todo",
|
||||
"No translation available.": "No hay traducción disponible.",
|
||||
"Translated by {{provider}}.": "Traducido por {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Invertir imagen en oscuro",
|
||||
"Help improve Readest": "Ayuda a mejorar Readest",
|
||||
"Sharing anonymized statistics": "Compartiendo estadísticas anónimas",
|
||||
"Interface Language": "Idioma de la interfaz",
|
||||
"Translation": "Traducción",
|
||||
"Enable Translation": "Habilitar traducción",
|
||||
"Translation Service": "Servicio de traducción",
|
||||
"Translate To": "Traducir a",
|
||||
"Disable Translation": "Deshabilitar traducción",
|
||||
"Scroll": "Desplazar",
|
||||
"Overlap Pixels": "Superponer píxeles",
|
||||
"Click": "Click",
|
||||
"System Language": "Idioma del sistema",
|
||||
"Security": "Seguridad",
|
||||
"Allow JavaScript": "Permitir JavaScript",
|
||||
"Enable only if you trust the file.": "Permitir solo si confías en el archivo.",
|
||||
"Sort TOC by Page": "Ordenar TOC por página",
|
||||
"Search in {{count}} Book(s)..._one": "Buscar en {{count}} libro...",
|
||||
"Search in {{count}} Book(s)..._many": "Buscar en {{count}} libros...",
|
||||
"Search in {{count}} Book(s)..._other": "Buscar en {{count}} libros...",
|
||||
"No notes match your search": "No se encontraron notas",
|
||||
"Search notes and excerpts...": "Buscar notas...",
|
||||
"Sign in to Sync": "Inicia sesión para sincronizar",
|
||||
"Synced at {{time}}": "Sincronizado a las {{time}}",
|
||||
"Never synced": "Nunca sincronizado",
|
||||
"Show Remaining Time": "Mostrar tiempo restante",
|
||||
"Show Page Number": "Mostrar número de página",
|
||||
"{{time}} min left in chapter": "{{time}} min restantes en el capítulo",
|
||||
"Override Book Color": "Sobre escribir color del libro",
|
||||
"Login Required": "Iniciar sesión",
|
||||
"Quota Exceeded": "Límite superado",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% de caracteres de traducción diarios utilizados.",
|
||||
"Translation Characters": "Caracteres de traducción",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Cuota diaria de traducción alcanzada. Selecciona otro servicio de traducción para continuar.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} voz",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} voces",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} voces",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Algo salió mal. No te preocupes, nuestro equipo ha sido notificado y estamos trabajando en una solución.",
|
||||
"Error Details:": "Detalles del error:",
|
||||
"Try Again": "Intentar de nuevo",
|
||||
"Need help?": "¿Necesitas ayuda?",
|
||||
"Contact Support": "Contactar con soporte",
|
||||
"Code Highlighting": "Resaltado de código",
|
||||
"Enable Highlighting": "Activar resaltado",
|
||||
"Code Language": "Lenguaje de código",
|
||||
"Top Margin (px)": "Margen superior (px)",
|
||||
"Bottom Margin (px)": "Margen inferior (px)",
|
||||
"Right Margin (px)": "Margen derecho (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Introduce CSS para la interfaz de lectura...",
|
||||
"Crop": "Recortar",
|
||||
"Book Covers": "Portadas de libros",
|
||||
"Fit": "Encajar",
|
||||
"Reset {{settings}}": "Restablecer {{settings}}",
|
||||
"Reset Settings": "Restablecer configuración",
|
||||
"{{count}} pages left in chapter_one": "{{count}} página restante en el capítulo",
|
||||
"{{count}} pages left in chapter_many": "{{count}} páginas restantes en el capítulo",
|
||||
"{{count}} pages left in chapter_other": "{{count}} páginas restantes en el capítulo",
|
||||
"Show Remaining Pages": "Mostrar resto"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animation",
|
||||
"Annotate": "Annoter",
|
||||
"Apply": "Appliquer",
|
||||
"Are you sure to delete the selected books?": "Êtes-vous sûr de vouloir supprimer les livres sélectionnés ?",
|
||||
"Auto Mode": "Mode automatique",
|
||||
"Behavior": "Comportement",
|
||||
"Book": "Livre",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Confirmer la suppression",
|
||||
"Copied to notebook": "Copié dans le carnet de notes",
|
||||
"Copy": "Copier",
|
||||
"Custom CSS": "CSS personnalisé",
|
||||
"Dark Mode": "Mode sombre",
|
||||
"Default": "Par défaut",
|
||||
"Default Font": "Police par défaut",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Dictionnaire",
|
||||
"Download Readest": "Télécharger Readest",
|
||||
"Edit": "Modifier",
|
||||
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
|
||||
"Excerpts": "Extraits",
|
||||
"Failed to import book(s): {{filenames}}": "Échec de l'importation du/des livre(s) : {{filenames}}",
|
||||
"Fast": "Rapide",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Mots entiers",
|
||||
"Maximum Number of Columns": "Nombre maximal de colonnes",
|
||||
"Minimum Font Size": "Taille minimale de police",
|
||||
"Misc": "Divers",
|
||||
"Monospace Font": "Police monospace",
|
||||
"More Info": "Plus d'informations",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Votre bibliothèque",
|
||||
"TTS not supported for PDF": "La synthèse vocale n'est pas prise en charge pour les fichiers PDF",
|
||||
"Override Book Font": "Remplacer la police du livre",
|
||||
"Vertical Margins (px)": "Marges verticales (px)",
|
||||
"Horizontal Margins (%)": "Marges horizontales (%)",
|
||||
"Apply to All Books": "Appliquer à tous les livres",
|
||||
"Apply to This Book": "Appliquer à ce livre",
|
||||
"Unable to fetch the translation. Try again later.": "Impossible de récupérer la traduction. Réessayez plus tard.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Se connecter avec Apple",
|
||||
"Sign in with GitHub": "Se connecter avec GitHub",
|
||||
"Account": "Compte",
|
||||
"Cloud Storage": "Stockage cloud",
|
||||
"Failed to delete user. Please try again later.": "Échec de la suppression de l'utilisateur. Veuillez réessayer plus tard.",
|
||||
"Unlimited Offline/Online Reading": "Lecture illimitée hors ligne/en ligne",
|
||||
"Unlimited Cloud Sync Devices": "Synchronisation cloud sur appareils illimitée",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Effacer la recherche",
|
||||
"Header & Footer": "En-tête et pied de page",
|
||||
"Apply also in Scrolled Mode": "Appliquer également en mode défilement",
|
||||
"A new version of Readest is Available!": "Avis de mise à jour",
|
||||
"A new version of Readest is available!": "Avis de mise à jour",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} est disponible (version installée {{currentVersion}}).",
|
||||
"Download and install now?": "Télécharger et installer maintenant ?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Téléchargement {{downloaded}} sur {{contentLength}}",
|
||||
@@ -304,13 +297,86 @@
|
||||
"Checking for updates...": "Vérification des mises à jour...",
|
||||
"Error checking for updates": "Erreur lors de la vérification des mises à jour",
|
||||
"Details": "Détails",
|
||||
"Cancel Selection": "Annuler la sélection",
|
||||
"File Size:": "Taille du fichier :",
|
||||
"Auto Detect": "Détection automatique",
|
||||
"Touch": "Touch",
|
||||
"Scrolling Overlap (px)": "Chevauchement de défilement (px)",
|
||||
"Next Section": "Section suivante",
|
||||
"Previous Section": "Section précédente",
|
||||
"Next Page": "Page suivante",
|
||||
"Previous Page": "Page précédente"
|
||||
"Previous Page": "Page précédente",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Supprimer le livre sélectionné ?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "Supprimer les {{count}} livres sélectionnés ?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Supprimer les {{count}} livres sélectionnés ?",
|
||||
"Are you sure to delete the selected book?": "Supprimer le livre sélectionné ?",
|
||||
"Deselect": "Annuler",
|
||||
"Select All": "Tous",
|
||||
"No translation available.": "Aucune traduction disponible.",
|
||||
"Translated by {{provider}}.": "Traduit par {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Traduction",
|
||||
"Azure Translator": "Traducteur Azure",
|
||||
"Invert Image In Dark Mode": "Inverser l'image en mode sombre",
|
||||
"Help improve Readest": "Contribuer à améliorer Readest",
|
||||
"Sharing anonymized statistics": "Partage de statistiques anonymisées",
|
||||
"Interface Language": "Langue de l'interface",
|
||||
"Translation": "Traduction",
|
||||
"Enable Translation": "Activer la traduction",
|
||||
"Translation Service": "Service de traduction",
|
||||
"Translate To": "Traduire vers",
|
||||
"Disable Translation": " désactiver la traduction",
|
||||
"Scroll": "Défilement",
|
||||
"Overlap Pixels": "Pixels de chevauchement",
|
||||
"Click": "Cliquer",
|
||||
"System Language": "Langue du système",
|
||||
"Security": "Sécurité",
|
||||
"Allow JavaScript": "Autoriser JavaScript",
|
||||
"Enable only if you trust the file.": "Activer uniquement si vous faites confiance au fichier.",
|
||||
"Sort TOC by Page": "Tris la table des matières par page",
|
||||
"Search in {{count}} Book(s)..._one": "Rechercher dans {{count}} Livre...",
|
||||
"Search in {{count}} Book(s)..._many": "Rechercher dans {{count}} Livres...",
|
||||
"Search in {{count}} Book(s)..._other": "Rechercher dans {{count}} Livres...",
|
||||
"No notes match your search": "Aucune note trouvée",
|
||||
"Search notes and excerpts...": "Rechercher des notes...",
|
||||
"Sign in to Sync": "Connectez-vous pour synchroniser",
|
||||
"Synced at {{time}}": "Synchronisé à {{time}}",
|
||||
"Never synced": "Jamais synchronisé",
|
||||
"Show Remaining Time": "Afficher le temps restant",
|
||||
"Show Page Number": "Afficher le numéro de page",
|
||||
"{{time}} min left in chapter": "{{time}} min restant dans le chapitre",
|
||||
"Override Book Color": "Remplacer la couleur du livre",
|
||||
"Login Required": "Connexion requise",
|
||||
"Quota Exceeded": "Quota dépassé",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% des caractères de traduction quotidiens utilisés.",
|
||||
"Translation Characters": "Caractères de traduction",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Quota de traduction quotidien atteint. Sélectionnez un autre service de traduction pour continuer.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}} : {{count}} voix",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}} : {{count}} voix",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}} : {{count}} voix",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Quelque chose s'est mal passé. Ne vous inquiétez pas, notre équipe a été informée et nous travaillons sur une solution.",
|
||||
"Error Details:": "Erreur :",
|
||||
"Try Again": "Réessayer",
|
||||
"Need help?": "Besoin d'aide ?",
|
||||
"Contact Support": "Contacter le support",
|
||||
"Code Highlighting": "Coloration syntaxique",
|
||||
"Enable Highlighting": "Activer la coloration syntaxique",
|
||||
"Code Language": "Langage de programmation",
|
||||
"Top Margin (px)": "Marge supérieure (px)",
|
||||
"Bottom Margin (px)": "Marge inférieure (px)",
|
||||
"Right Margin (px)": "Marge droite (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Saisissez le CSS pour l’interface de lecture...",
|
||||
"Crop": "Rogner",
|
||||
"Book Covers": "Couvertures de livres",
|
||||
"Fit": "Adapter",
|
||||
"Reset {{settings}}": "Réinitialiser {{settings}}",
|
||||
"Reset Settings": "Réinitialiser les paramètres",
|
||||
"{{count}} pages left in chapter_one": "{{count}} page restant dans le chapitre",
|
||||
"{{count}} pages left in chapter_many": "{{count}} pages restantes dans le chapitre",
|
||||
"{{count}} pages left in chapter_other": "{{count}} pages restantes dans le chapitre",
|
||||
"Show Remaining Pages": "Voir restantes"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "एनीमेशन",
|
||||
"Annotate": "टिप्पणी",
|
||||
"Apply": "लागू करें",
|
||||
"Are you sure to delete the selected books?": "क्या आप चयनित पुस्तकों को हटाना चाहते हैं?",
|
||||
"Auto Mode": "स्वचालित मोड",
|
||||
"Behavior": "व्यवहार",
|
||||
"Book": "पुस्तक",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "हटाने की पुष्टि करें",
|
||||
"Copied to notebook": "नोटबुक में कॉपी किया गया",
|
||||
"Copy": "कॉपी",
|
||||
"Custom CSS": "कस्टम CSS",
|
||||
"Dark Mode": "डार्क मोड",
|
||||
"Default": "डिफ़ॉल्ट",
|
||||
"Default Font": "डिफ़ॉल्ट फ़ॉन्ट",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "शब्दकोश",
|
||||
"Download Readest": "Readest डाउनलोड करें",
|
||||
"Edit": "संपादित करें",
|
||||
"Enter your custom CSS here...": "अपना कस्टम CSS यहां दर्ज करें...",
|
||||
"Excerpts": "अंश",
|
||||
"Failed to import book(s): {{filenames}}": "पुस्तक(ें) आयात करने में विफल: {{filenames}}",
|
||||
"Fast": "तेज़",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "पूरे शब्द मैच करें",
|
||||
"Maximum Number of Columns": "कॉलम की अधिकतम संख्या",
|
||||
"Minimum Font Size": "न्यूनतम फ़ॉन्ट साइज़",
|
||||
"Misc": "विविध",
|
||||
"Monospace Font": "मोनोस्पेस फ़ॉन्ट",
|
||||
"More Info": "अधिक जानकारी",
|
||||
"Nord": "नॉर्ड",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "आपकी लाइब्रेरी",
|
||||
"TTS not supported for PDF": "PDF के लिए TTS समर्थित नहीं है",
|
||||
"Override Book Font": "पुस्तक फ़ॉन्ट ओवरराइड करें",
|
||||
"Vertical Margins (px)": "ऊर्ध्वाधर मार्जिन (px)",
|
||||
"Horizontal Margins (%)": "क्षैतिज मार्जिन (%)",
|
||||
"Apply to All Books": "सभी पुस्तकों पर लागू करें",
|
||||
"Apply to This Book": "इस पुस्तक पर लागू करें",
|
||||
"Unable to fetch the translation. Try again later.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Apple के साथ साइन इन करें",
|
||||
"Sign in with GitHub": "GitHub के साथ साइन इन करें",
|
||||
"Account": "खाता",
|
||||
"Cloud Storage": "क्लाउड स्टोरेज",
|
||||
"Failed to delete user. Please try again later.": "उपयोगकर्ता को हटाने में विफल। कृपया बाद में पुनः प्रयास करें।",
|
||||
"Unlimited Offline/Online Reading": "असीमित ऑफलाइन/ऑनलाइन पढ़ना",
|
||||
"Unlimited Cloud Sync Devices": "असीमित क्लाउड सिंक उपकरण",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "खोज साफ करें",
|
||||
"Header & Footer": "हेडर और फ़ुटर",
|
||||
"Apply also in Scrolled Mode": "स्क्रॉल मोड में भी लागू करें",
|
||||
"A new version of Readest is Available!": "Readest का एक नया संस्करण उपलब्ध है!",
|
||||
"A new version of Readest is available!": "Readest का एक नया संस्करण उपलब्ध है!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} उपलब्ध है (स्थापित संस्करण {{currentVersion}})।",
|
||||
"Download and install now?": "अब डाउनलोड और इंस्टॉल करें?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
|
||||
@@ -304,13 +297,82 @@
|
||||
"Checking for updates...": "अपडेट की जांच कर रहा है...",
|
||||
"Error checking for updates": "अपडेट की जांच करते समय त्रुटि",
|
||||
"Details": "विवरण",
|
||||
"Cancel Selection": "चयन रद्द करें",
|
||||
"File Size:": "फ़ाइल आकार:",
|
||||
"Auto Detect": "स्वचालित पहचान",
|
||||
"Touch": "स्पर्श",
|
||||
"Scrolling Overlap (px)": "स्क्रॉलिंग ओवरलैप (px)",
|
||||
"Next Section": "अगला अनुभाग",
|
||||
"Previous Section": "पिछला अनुभाग",
|
||||
"Next Page": "अगला पृष्ठ",
|
||||
"Previous Page": "पिछला पृष्ठ"
|
||||
"Previous Page": "पिछला पृष्ठ",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "क्या आप चयनित पुस्तक हटाना चाहते हैं?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "क्या आप चयनित {{count}} पुस्तकें हटाना चाहते हैं?",
|
||||
"Are you sure to delete the selected book?": "क्या आप चयनित पुस्तक हटाना चाहते हैं?",
|
||||
"Deselect": "रद्द करें",
|
||||
"Select All": "सब चुनें",
|
||||
"No translation available.": "कोई अनुवाद उपलब्ध नहीं है।",
|
||||
"Translated by {{provider}}.": "{{provider}} द्वारा अनुवादित।",
|
||||
"DeepL": "डीपएल",
|
||||
"Google Translate": "गूगल अनुवाद",
|
||||
"Azure Translator": "एज़्योर अनुवादक",
|
||||
"Invert Image In Dark Mode": "अंधेरे में छवि को उलटें",
|
||||
"Help improve Readest": "Readest में सुधार करने में मदद करें",
|
||||
"Sharing anonymized statistics": "सांख्यिकी साझा करना",
|
||||
"Interface Language": "इंटरफ़ेस भाषा",
|
||||
"Translation": "अनुवाद",
|
||||
"Enable Translation": "अनुवाद सक्षम करें",
|
||||
"Translation Service": "अनुवाद सेवा",
|
||||
"Translate To": "अनुवाद करें",
|
||||
"Disable Translation": "अनुवाद अक्षम करें",
|
||||
"Scroll": "स्क्रॉल",
|
||||
"Overlap Pixels": "ओवरलैप पिक्सेल",
|
||||
"Click": "क्लिक करें",
|
||||
"System Language": "सिस्टम भाषा",
|
||||
"Security": "सुरक्षा",
|
||||
"Allow JavaScript": "जावास्क्रिप्ट की अनुमति दें",
|
||||
"Enable only if you trust the file.": "केवल तभी सक्षम करें जब आप फ़ाइल पर भरोसा करते हों।",
|
||||
"Sort TOC by Page": "सामग्री को पृष्ठ द्वारा क्रमबद्ध करें",
|
||||
"Search in {{count}} Book(s)..._one": "{{count}} पुस्तक में खोजें...",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}} पुस्तकों में खोजें...",
|
||||
"No notes match your search": "कोई नोट्स नहीं मिले",
|
||||
"Search notes and excerpts...": "नोट्स खोजें...",
|
||||
"Sign in to Sync": "सिंक करने के लिए साइन इन करें",
|
||||
"Synced at {{time}}": "{{time}} पर सिंक किया गया",
|
||||
"Never synced": "कभी सिंक नहीं किया गया",
|
||||
"Show Remaining Time": "शेष समय दिखाएं",
|
||||
"Show Page Number": "पृष्ठ संख्या दिखाएं",
|
||||
"{{time}} min left in chapter": "{{time}} मिनट शेष अध्याय में",
|
||||
"Override Book Color": "पुस्तक रंग ओवरराइड करें",
|
||||
"Login Required": "लॉगिन ज़रूरी",
|
||||
"Quota Exceeded": "सीमा पार",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% दैनिक अनुवाद वर्णों का उपयोग किया गया है।",
|
||||
"Translation Characters": "अनुवाद वर्ण",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "दैनिक अनुवाद कोटा पूरा हो गया है। आगे बढ़ने के लिए कोई अन्य अनुवाद सेवा चुनें।",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} आवाज़",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} आवाज़ें",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "कुछ गलत हो गया। चिंता न करें, हमारी टीम को सूचित कर दिया गया है और हम एक समाधान पर काम कर रहे हैं।",
|
||||
"Error Details:": "त्रुटि विवरण:",
|
||||
"Try Again": "फिर से प्रयास करें",
|
||||
"Need help?": "मदद चाहिए?",
|
||||
"Contact Support": "सहायता से संपर्क करें",
|
||||
"Code Highlighting": "कोड हाइलाइटिंग",
|
||||
"Enable Highlighting": "हाइलाइटिंग सक्षम करें",
|
||||
"Code Language": "कोड भाषा",
|
||||
"Top Margin (px)": "ऊपरी हाशिया (px)",
|
||||
"Bottom Margin (px)": "निचला हाशिया (px)",
|
||||
"Right Margin (px)": "दायां हाशिया (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "रीडर इंटरफ़ेस के लिए CSS दर्ज करें...",
|
||||
"Crop": "काटें",
|
||||
"Book Covers": "पुस्तक कवर",
|
||||
"Fit": "फिट",
|
||||
"Reset {{settings}}": "रीसेट {{settings}}",
|
||||
"Reset Settings": "सेटिंग्स रीसेट करें",
|
||||
"{{count}} pages left in chapter_one": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
|
||||
"{{count}} pages left in chapter_other": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
|
||||
"Show Remaining Pages": "शेष पृष्ठ दिखाएँ"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animasi",
|
||||
"Annotate": "Anotasi",
|
||||
"Apply": "Terapkan",
|
||||
"Are you sure to delete the selected books?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Auto Mode": "Mode Otomatis",
|
||||
"Behavior": "Perilaku",
|
||||
"Book": "Buku",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Konfirmasi Penghapusan",
|
||||
"Copied to notebook": "Disalin ke buku catatan",
|
||||
"Copy": "Salin",
|
||||
"Custom CSS": "CSS Kustom",
|
||||
"Dark Mode": "Mode Gelap",
|
||||
"Default": "Default",
|
||||
"Default Font": "Font Default",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Kamus",
|
||||
"Download Readest": "Unduh Readest",
|
||||
"Edit": "Edit",
|
||||
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
|
||||
"Excerpts": "Kutipan",
|
||||
"Failed to import book(s): {{filenames}}": "Gagal mengimpor buku: {{filenames}}",
|
||||
"Fast": "Cepat",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Cocokkan Kata Utuh",
|
||||
"Maximum Number of Columns": "Jumlah Kolom Maksimum",
|
||||
"Minimum Font Size": "Ukuran Font Minimum",
|
||||
"Misc": "Lain-lain",
|
||||
"Monospace Font": "Font Monospace",
|
||||
"More Info": "Info Lebih Lanjut",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Perpustakaan Anda",
|
||||
"TTS not supported for PDF": "TTS tidak didukung untuk PDF",
|
||||
"Override Book Font": "Ganti Font Buku",
|
||||
"Vertical Margins (px)": "Margin Vertikal (px)",
|
||||
"Horizontal Margins (%)": "Margin Horizontal (%)",
|
||||
"Apply to All Books": "Terapkan ke semua buku",
|
||||
"Apply to This Book": "Terapkan ke buku ini",
|
||||
"Unable to fetch the translation. Try again later.": "Tidak dapat mengambil terjemahan. Coba lagi nanti.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Masuk dengan Apple",
|
||||
"Sign in with GitHub": "Masuk dengan GitHub",
|
||||
"Account": "Akun",
|
||||
"Cloud Storage": "Penyimpanan Cloud",
|
||||
"Failed to delete user. Please try again later.": "Gagal menghapus pengguna. Silakan coba lagi nanti.",
|
||||
"Unlimited Offline/Online Reading": "Membaca Offline/Online Tanpa Batas",
|
||||
"Unlimited Cloud Sync Devices": "Perangkat Sinkronisasi Cloud Tanpa Batas",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Hapus Pencarian",
|
||||
"Header & Footer": "Header & Footer",
|
||||
"Apply also in Scrolled Mode": "Terapkan juga di Mode Gulir",
|
||||
"A new version of Readest is Available!": "Versi baru Readest Tersedia!",
|
||||
"A new version of Readest is available!": "Versi baru Readest Tersedia!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} tersedia (versi terinstal {{currentVersion}}).",
|
||||
"Download and install now?": "Unduh dan instal sekarang?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Mengunduh {{downloaded}} dari {{contentLength}}",
|
||||
@@ -304,13 +297,78 @@
|
||||
"Checking for updates...": "Memeriksa pembaruan...",
|
||||
"Error checking for updates": "Kesalahan saat memeriksa pembaruan",
|
||||
"Details": "Detail",
|
||||
"Cancel Selection": "Batalkan Pilihan",
|
||||
"File Size:": "Ukuran File:",
|
||||
"Auto Detect": "Deteksi Otomatis",
|
||||
"Touch": "Sentuh",
|
||||
"Scrolling Overlap (px)": "Overlapping Gulir (px)",
|
||||
"Next Section": "Bagian Selanjutnya",
|
||||
"Previous Section": "Bagian Sebelumnya",
|
||||
"Next Page": "Halaman Berikutnya",
|
||||
"Previous Page": "Halaman Sebelumnya"
|
||||
"Previous Page": "Halaman Sebelumnya",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Apakah Anda yakin ingin menghapus {{count}} buku yang dipilih?",
|
||||
"Are you sure to delete the selected book?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Deselect": "Batal",
|
||||
"Select All": "Pilih Semua",
|
||||
"No translation available.": "Tidak ada terjemahan yang tersedia.",
|
||||
"Translated by {{provider}}.": "Terjemahan oleh {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Inversi Gambar di Gelap",
|
||||
"Help improve Readest": "Bantu tingkatkan Readest",
|
||||
"Sharing anonymized statistics": "Berbagi statistik anonim",
|
||||
"Interface Language": "Bahasa Antarmuka",
|
||||
"Translation": "Terjemahan",
|
||||
"Enable Translation": "Aktifkan Terjemahan",
|
||||
"Translation Service": "Layanan Terjemahan",
|
||||
"Translate To": "Terjemahkan Ke",
|
||||
"Disable Translation": "Nonaktifkan Terjemahan",
|
||||
"Scroll": "Scroll",
|
||||
"Overlap Pixels": "Overlap Piksel",
|
||||
"Click": "Klik",
|
||||
"System Language": "Bahasa Sistem",
|
||||
"Security": "Keamanan",
|
||||
"Allow JavaScript": "Izinkan JavaScript",
|
||||
"Enable only if you trust the file.": "Aktifkan hanya jika Anda mempercayai file tersebut.",
|
||||
"Sort TOC by Page": "Urutkan TOC berdasarkan Halaman",
|
||||
"Search in {{count}} Book(s)..._other": "Cari di {{count}} Buku...",
|
||||
"No notes match your search": "Tidak ada catatan ditemukan",
|
||||
"Search notes and excerpts...": "Cari catatan...",
|
||||
"Sign in to Sync": "Masuk untuk Sinkronisasi",
|
||||
"Synced at {{time}}": "Disinkronkan pada {{time}}",
|
||||
"Never synced": "Belum pernah disinkronkan",
|
||||
"Show Remaining Time": "Perlihatkan Waktu Tersisa",
|
||||
"Show Page Number": "Perlihatkan Nomor Halaman",
|
||||
"{{time}} min left in chapter": "{{time}} menit tersisa di bab",
|
||||
"Override Book Color": "Ubah Warna Buku",
|
||||
"Login Required": "Perlu masuk",
|
||||
"Quota Exceeded": "Kuota terlampaui",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dari Karakter Terjemahan Harian Digunakan.",
|
||||
"Translation Characters": "Karakter Terjemahan",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Kuota terjemahan harian tercapai. Pilih layanan terjemahan lain untuk melanjutkan.",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} suara",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Terjadi kesalahan. Jangan khawatir, tim kami telah diberitahu dan kami sedang bekerja untuk memperbaikinya.",
|
||||
"Error Details:": "Rincian Kesalahan:",
|
||||
"Try Again": "Coba Lagi",
|
||||
"Need help?": "Butuh bantuan?",
|
||||
"Contact Support": "Hubungi Dukungan",
|
||||
"Code Highlighting": "Penyorotan Kode",
|
||||
"Enable Highlighting": "Aktifkan Penyorotan",
|
||||
"Code Language": "Bahasa Kode",
|
||||
"Top Margin (px)": "Margin Atas (px)",
|
||||
"Bottom Margin (px)": "Margin Bawah (px)",
|
||||
"Right Margin (px)": "Margin Kanan (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Masukkan CSS untuk gaya antarmuka pembaca...",
|
||||
"Crop": "Potong",
|
||||
"Book Covers": "Sampul Buku",
|
||||
"Fit": "Pas",
|
||||
"Reset {{settings}}": "Atur Ulang {{settings}}",
|
||||
"Reset Settings": "Atur Ulang Pengaturan",
|
||||
"{{count}} pages left in chapter_other": "{{count}} halaman tersisa di bab ini",
|
||||
"Show Remaining Pages": "Tampilkan halaman tersisa"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animazione",
|
||||
"Annotate": "Annota",
|
||||
"Apply": "Applica",
|
||||
"Are you sure to delete the selected books?": "Sei sicuro di voler eliminare i libri selezionati?",
|
||||
"Auto Mode": "Modalità automatica",
|
||||
"Behavior": "Comportamento",
|
||||
"Book": "Libro",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Conferma eliminazione",
|
||||
"Copied to notebook": "Copiato nel quaderno",
|
||||
"Copy": "Copia",
|
||||
"Custom CSS": "CSS personalizzato",
|
||||
"Dark Mode": "Modalità scura",
|
||||
"Default": "Predefinito",
|
||||
"Default Font": "Font predefinito",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Dizionario",
|
||||
"Download Readest": "Scarica Readest",
|
||||
"Edit": "Modifica",
|
||||
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
|
||||
"Excerpts": "Estratti",
|
||||
"Failed to import book(s): {{filenames}}": "Impossibile importare il/i libro/i: {{filenames}}",
|
||||
"Fast": "Veloce",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Parole intere",
|
||||
"Maximum Number of Columns": "Numero massimo di colonne",
|
||||
"Minimum Font Size": "Dimensione minima font",
|
||||
"Misc": "Varie",
|
||||
"Monospace Font": "Font monospazio",
|
||||
"More Info": "Maggiori informazioni",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "La tua biblioteca",
|
||||
"TTS not supported for PDF": "TTS non supportato per PDF",
|
||||
"Override Book Font": "Sovrascrivi font libro",
|
||||
"Vertical Margins (px)": "Margini verticali (px)",
|
||||
"Horizontal Margins (%)": "Margini orizzontali (%)",
|
||||
"Apply to All Books": "Applica a tutti i libri",
|
||||
"Apply to This Book": "Applica a questo libro",
|
||||
"Unable to fetch the translation. Try again later.": "Impossibile recuperare la traduzione. Riprova più tardi.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Accedi con Apple",
|
||||
"Sign in with GitHub": "Accedi con GitHub",
|
||||
"Account": "Account",
|
||||
"Cloud Storage": "Archiviazione Cloud",
|
||||
"Failed to delete user. Please try again later.": "Impossibile eliminare l'utente. Riprova più tardi.",
|
||||
"Unlimited Offline/Online Reading": "Lettura offline/online illimitata",
|
||||
"Unlimited Cloud Sync Devices": "Sincronizzazione cloud su dispositivi illimitati",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Cancella ricerca",
|
||||
"Header & Footer": "Intestazione e piè di pagina",
|
||||
"Apply also in Scrolled Mode": "Applica anche in modalità scorrimento",
|
||||
"A new version of Readest is Available!": "È disponibile una nuova versione di Readest!",
|
||||
"A new version of Readest is available!": "È disponibile una nuova versione di Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} è disponibile (versione installata {{currentVersion}}).",
|
||||
"Download and install now?": "Scarica e installa ora?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Scaricando {{downloaded}} di {{contentLength}}",
|
||||
@@ -304,13 +297,86 @@
|
||||
"Checking for updates...": "Controllo aggiornamenti...",
|
||||
"Error checking for updates": "Errore durante il controllo degli aggiornamenti",
|
||||
"Details": "Dettagli",
|
||||
"Cancel Selection": "Annulla selezione",
|
||||
"File Size:": "Dimensione file:",
|
||||
"Auto Detect": "Rilevamento automatico",
|
||||
"Touch": "Touch",
|
||||
"Scrolling Overlap (px)": "Overlap scorrimento (px)",
|
||||
"Next Section": "Sezione successiva",
|
||||
"Previous Section": "Sezione precedente",
|
||||
"Next Page": "Pagina successiva",
|
||||
"Previous Page": "Pagina precedente"
|
||||
"Previous Page": "Pagina precedente",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Sei sicuro di voler eliminare {{count}} libro selezionato?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "Sei sicuro di voler eliminare {{count}} libri selezionati?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Sei sicuro di voler eliminare {{count}} libri selezionati?",
|
||||
"Are you sure to delete the selected book?": "Sei sicuro di voler eliminare il libro selezionato?",
|
||||
"Deselect": "Annulla",
|
||||
"Select All": "Tutti",
|
||||
"No translation available.": "Nessuna traduzione disponibile.",
|
||||
"Translated by {{provider}}.": "Tradotto da {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Traduttore Azure",
|
||||
"Invert Image In Dark Mode": "Inverti immagine in scuro",
|
||||
"Help improve Readest": "Aiuta a migliorare Readest",
|
||||
"Sharing anonymized statistics": "Condivisione di statistiche anonime",
|
||||
"Interface Language": "Lingua dell'interfaccia",
|
||||
"Translation": "Traduzione",
|
||||
"Enable Translation": "Abilita traduzione",
|
||||
"Translation Service": "Servizio di traduzione",
|
||||
"Translate To": "Traduci in",
|
||||
"Disable Translation": "Disabilita traduzione",
|
||||
"Scroll": "Scorri",
|
||||
"Overlap Pixels": "Pixel di sovrapposizione",
|
||||
"Click": "Fai clic",
|
||||
"System Language": "Lingua di sistema",
|
||||
"Security": "Security",
|
||||
"Allow JavaScript": "Consenti JavaScript",
|
||||
"Enable only if you trust the file.": "Abilita solo se ti fidi del file.",
|
||||
"Sort TOC by Page": "Ordina sommario per pagina",
|
||||
"Search in {{count}} Book(s)..._one": "Cerca in {{count}} libro...",
|
||||
"Search in {{count}} Book(s)..._many": "Cerca in {{count}} libri...",
|
||||
"Search in {{count}} Book(s)..._other": "Cerca in {{count}} libri...",
|
||||
"No notes match your search": "Nessuna nota trovata",
|
||||
"Search notes and excerpts...": "Cerca nelle note...",
|
||||
"Sign in to Sync": "Accedi per sincronizzare",
|
||||
"Synced at {{time}}": "Sincronizzato alle {{time}}",
|
||||
"Never synced": "Mai sincronizzato",
|
||||
"Show Remaining Time": "Mostra tempo rimanente",
|
||||
"Show Page Number": "Mostra numero pagina",
|
||||
"{{time}} min left in chapter": "{{time}} min rimasti nel capitolo",
|
||||
"Override Book Color": "Override colore libro",
|
||||
"Login Required": "Accesso richiesto",
|
||||
"Quota Exceeded": "Limite superato",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dei caratteri di traduzione giornalieri utilizzati.",
|
||||
"Translation Characters": "Caratteri di traduzione",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Quota di traduzione giornaliera raggiunta. Seleziona un altro servizio di traduzione per procedere.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} voce",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} voci",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} voci",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Qualcosa è andato storto. Non preoccuparti, il nostro team è stato avvisato e stiamo lavorando a una soluzione.",
|
||||
"Error Details:": "Dettagli errore:",
|
||||
"Try Again": "Riprova",
|
||||
"Need help?": "Hai bisogno di aiuto?",
|
||||
"Contact Support": "Contatta il supporto",
|
||||
"Code Highlighting": "Evidenziazione codice",
|
||||
"Enable Highlighting": "Abilita evidenziazione",
|
||||
"Code Language": "Tipo di codice",
|
||||
"Top Margin (px)": "Margine Superiore (px)",
|
||||
"Bottom Margin (px)": "Margine Inferiore (px)",
|
||||
"Right Margin (px)": "Margine Destro (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Inserisci il CSS per l'interfaccia di lettura...",
|
||||
"Crop": "Ritaglia",
|
||||
"Book Covers": "Copertine libri",
|
||||
"Fit": "Adatta",
|
||||
"Reset {{settings}}": "Reimposta {{settings}}",
|
||||
"Reset Settings": "Reimposta impostazioni",
|
||||
"{{count}} pages left in chapter_one": "{{count}} pagina rimasta nel capitolo",
|
||||
"{{count}} pages left in chapter_many": "{{count}} pagine rimaste nel capitolo",
|
||||
"{{count}} pages left in chapter_other": "{{count}} pagine rimaste nel capitolo",
|
||||
"Show Remaining Pages": "Mostra pagine rimanenti"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "アニメーション",
|
||||
"Annotate": "注釈",
|
||||
"Apply": "適用",
|
||||
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
|
||||
"Auto Mode": "自動モード",
|
||||
"Behavior": "動作",
|
||||
"Book": "書籍",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "削除の確認",
|
||||
"Copied to notebook": "ノートブックにコピー",
|
||||
"Copy": "コピー",
|
||||
"Custom CSS": "カスタムCSS",
|
||||
"Dark Mode": "ダークモード",
|
||||
"Default": "デフォルト",
|
||||
"Default Font": "デフォルトフォント",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "辞書",
|
||||
"Download Readest": "Readestをダウンロード",
|
||||
"Edit": "編集",
|
||||
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
|
||||
"Excerpts": "抜粋",
|
||||
"Failed to import book(s): {{filenames}}": "書籍のインポートに失敗しました:{{filenames}}",
|
||||
"Fast": "高速",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "単語全体を一致",
|
||||
"Maximum Number of Columns": "最大列数",
|
||||
"Minimum Font Size": "最小フォントサイズ",
|
||||
"Misc": "その他",
|
||||
"Monospace Font": "等幅フォント",
|
||||
"More Info": "詳細情報",
|
||||
"Nord": "ノルド",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "ライブラリー",
|
||||
"TTS not supported for PDF": "PDFではTTSがサポートされていません",
|
||||
"Override Book Font": "書籍フォントを上書き",
|
||||
"Vertical Margins (px)": "縦マージン(px)",
|
||||
"Horizontal Margins (%)": "横マージン(%)",
|
||||
"Apply to All Books": "すべての書籍に適用",
|
||||
"Apply to This Book": "この書籍に適用",
|
||||
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Appleでサインイン",
|
||||
"Sign in with GitHub": "GitHubでサインイン",
|
||||
"Account": "アカウント",
|
||||
"Cloud Storage": "クラウドストレージ",
|
||||
"Failed to delete user. Please try again later.": "ユーザーの削除に失敗しました。後ほど再試行してください。",
|
||||
"Unlimited Offline/Online Reading": "無制限のオフライン/オンライン読書",
|
||||
"Unlimited Cloud Sync Devices": "無制限のクラウド同期デバイス",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "検索をクリア",
|
||||
"Header & Footer": "ヘッダーとフッター",
|
||||
"Apply also in Scrolled Mode": "スクロールモードでも適用",
|
||||
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
|
||||
"A new version of Readest is available!": "新しいバージョンのReadestが利用可能です!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}が利用可能です(インストールされたバージョン{{currentVersion}})。",
|
||||
"Download and install now?": "今すぐダウンロードしてインストールしますか?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
|
||||
@@ -304,13 +297,78 @@
|
||||
"Checking for updates...": "更新を確認中...",
|
||||
"Error checking for updates": "更新の確認中にエラーが発生しました",
|
||||
"Details": "詳細",
|
||||
"Cancel Selection": "選択をキャンセル",
|
||||
"File Size:": "ファイルサイズ:",
|
||||
"Auto Detect": "自動検出",
|
||||
"Touch": "タッチ",
|
||||
"Scrolling Overlap (px)": "スクロール重複(px)",
|
||||
"Next Section": "次のセクション",
|
||||
"Previous Section": "前のセクション",
|
||||
"Next Page": "次のページ",
|
||||
"Previous Page": "前のページ"
|
||||
"Previous Page": "前のページ",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "選択した{{count}}冊の本を削除してもよろしいですか?",
|
||||
"Are you sure to delete the selected book?": "選択した本を削除してもよろしいですか?",
|
||||
"Deselect": "解除",
|
||||
"Select All": "全選択",
|
||||
"No translation available.": "翻訳はありません。",
|
||||
"Translated by {{provider}}.": "{{provider}}によって翻訳されました。",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google翻訳",
|
||||
"Azure Translator": "Azure翻訳",
|
||||
"Invert Image In Dark Mode": "ダークモードで画像を反転",
|
||||
"Help improve Readest": "Readestを改善する手助け",
|
||||
"Sharing anonymized statistics": "匿名化された統計情報を共有",
|
||||
"Interface Language": "インターフェースの言語",
|
||||
"Translation": "翻訳",
|
||||
"Enable Translation": "翻訳を有効にする",
|
||||
"Translation Service": "翻訳サービス",
|
||||
"Translate To": "翻訳先",
|
||||
"Disable Translation": "翻訳を無効にする",
|
||||
"Scroll": "スクロール",
|
||||
"Overlap Pixels": "重なりピクセル",
|
||||
"Click": "クリック",
|
||||
"System Language": "システム言語",
|
||||
"Security": "セキュリティ",
|
||||
"Allow JavaScript": "JavaScriptを許可",
|
||||
"Enable only if you trust the file.": "ファイルを信頼する場合にのみ有効にしてください。",
|
||||
"Sort TOC by Page": "目次をページで並べ替え",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}}冊の本を検索...",
|
||||
"No notes match your search": "ノートが見つかりません",
|
||||
"Search notes and excerpts...": "ノートを検索...",
|
||||
"Sign in to Sync": "同期するにはサインイン",
|
||||
"Synced at {{time}}": "{{time}} に同期済み",
|
||||
"Never synced": "まだ同期されていません",
|
||||
"Show Remaining Time": "残り時間を表示",
|
||||
"Show Page Number": "ページ番号を表示",
|
||||
"{{time}} min left in chapter": "{{time}}分残り",
|
||||
"Override Book Color": "書籍の色を上書き",
|
||||
"Login Required": "ログイン必須",
|
||||
"Quota Exceeded": "上限超過",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "日次翻訳文字数の使用量は{{percentage}}%です。",
|
||||
"Translation Characters": "翻訳文字数",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "日次翻訳クォータに達しました。続行するには別の翻訳サービスを選択してください。",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}}音声",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "何か問題が発生しました。心配しないでください。私たちのチームが通知を受けており、修正に取り組んでいます。",
|
||||
"Error Details:": "エラーの詳細:",
|
||||
"Try Again": "再試行",
|
||||
"Need help?": "ヘルプが必要ですか?",
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Code Highlighting": "コードハイライト",
|
||||
"Enable Highlighting": "ハイライトを有効にする",
|
||||
"Code Language": "コード言語",
|
||||
"Top Margin (px)": "上マージン (px)",
|
||||
"Bottom Margin (px)": "下マージン (px)",
|
||||
"Right Margin (px)": "右マージン (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "リーダーインターフェース用CSSを入力...",
|
||||
"Crop": "トリミング",
|
||||
"Book Covers": "書籍の表紙",
|
||||
"Fit": "フィット",
|
||||
"Reset {{settings}}": "{{settings}}をリセット",
|
||||
"Reset Settings": "設定をリセット",
|
||||
"{{count}} pages left in chapter_other": "{{count}} ページ残り",
|
||||
"Show Remaining Pages": "残りを表示"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "애니메이션",
|
||||
"Annotate": "주석",
|
||||
"Apply": "적용",
|
||||
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Auto Mode": "자동 모드",
|
||||
"Behavior": "동작",
|
||||
"Book": "책",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "삭제 확인",
|
||||
"Copied to notebook": "노트북에 복사됨",
|
||||
"Copy": "복사",
|
||||
"Custom CSS": "사용자 정의 CSS",
|
||||
"Dark Mode": "다크 모드",
|
||||
"Default": "기본값",
|
||||
"Default Font": "기본 글꼴",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "사전",
|
||||
"Download Readest": "Readest 다운로드",
|
||||
"Edit": "편집",
|
||||
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
|
||||
"Excerpts": "발췌",
|
||||
"Failed to import book(s): {{filenames}}": "책 가져오기 실패: {{filenames}}",
|
||||
"Fast": "빠름",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "전체 단어 일치",
|
||||
"Maximum Number of Columns": "최대 열 수",
|
||||
"Minimum Font Size": "최소 글꼴 크기",
|
||||
"Misc": "기타",
|
||||
"Monospace Font": "고정폭 글꼴",
|
||||
"More Info": "추가 정보",
|
||||
"Nord": "노드",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "내 서재",
|
||||
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음",
|
||||
"Override Book Font": "책 글꼴 덮어쓰기",
|
||||
"Vertical Margins (px)": "수직 여백 (px)",
|
||||
"Horizontal Margins (%)": "수평 여백 (%)",
|
||||
"Apply to All Books": "모든 책에 적용",
|
||||
"Apply to This Book": "이 책에 적용",
|
||||
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Apple로 로그인",
|
||||
"Sign in with GitHub": "GitHub로 로그인",
|
||||
"Account": "계정",
|
||||
"Cloud Storage": "클라우드 스토리지",
|
||||
"Failed to delete user. Please try again later.": "사용자 삭제에 실패했습니다. 나중에 다시 시도해 주세요.",
|
||||
"Unlimited Offline/Online Reading": "무제한 오프라인/온라인 읽기",
|
||||
"Unlimited Cloud Sync Devices": "무제한 클라우드 동기화 기기",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "검색 지우기",
|
||||
"Header & Footer": "헤더 및 푸터",
|
||||
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
|
||||
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
|
||||
"A new version of Readest is available!": "Readest의 새 버전이 있습니다!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}가 사용 가능 (설치된 버전 {{currentVersion}}).",
|
||||
"Download and install now?": "지금 다운로드하고 설치하시겠습니까?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
|
||||
@@ -304,13 +297,78 @@
|
||||
"Checking for updates...": "업데이트 확인 중...",
|
||||
"Error checking for updates": "업데이트 확인 중 오류 발생",
|
||||
"Details": "세부정보",
|
||||
"Cancel Selection": "선택 취소",
|
||||
"File Size:": "파일 크기:",
|
||||
"Auto Detect": "자동 감지",
|
||||
"Touch": "터치",
|
||||
"Scrolling Overlap (px)": "스크롤 겹침 (px)",
|
||||
"Next Section": "다음 섹션",
|
||||
"Previous Section": "이전 섹션",
|
||||
"Next Page": "다음 페이지",
|
||||
"Previous Page": "이전 페이지"
|
||||
"Previous Page": "이전 페이지",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "선택한 {{count}}권의 책을 삭제하시겠습니까?",
|
||||
"Are you sure to delete the selected book?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Deselect": "해제",
|
||||
"Select All": "전체선택",
|
||||
"No translation available.": "번역이 없습니다.",
|
||||
"Translated by {{provider}}.": "{{provider}}에 의해 번역됨.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "구글 번역",
|
||||
"Azure Translator": "Azure 번역기",
|
||||
"Invert Image In Dark Mode": "어두운 곳에서 이미지 반전",
|
||||
"Help improve Readest": "Readest 개선에 도움을 주세요",
|
||||
"Sharing anonymized statistics": "익명화된 통계 공유",
|
||||
"Interface Language": "인터페이스 언어",
|
||||
"Translation": "번역",
|
||||
"Enable Translation": "번역 활성화",
|
||||
"Translation Service": "번역 서비스",
|
||||
"Translate To": "번역 대상 언어",
|
||||
"Disable Translation": "번역 비활성화",
|
||||
"Scroll": "스크롤",
|
||||
"Overlap Pixels": "겹치는 픽셀",
|
||||
"Click": "클릭",
|
||||
"System Language": "시스템 언어",
|
||||
"Security": "보안",
|
||||
"Allow JavaScript": "JavaScript 허용",
|
||||
"Enable only if you trust the file.": "파일을 신뢰하는 경우에만 활성화하세요.",
|
||||
"Sort TOC by Page": "목차를 페이지별로 정렬",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}}권의 책에서 검색...",
|
||||
"No notes match your search": "일치하는 노트가 없습니다",
|
||||
"Search notes and excerpts...": "노트 검색...",
|
||||
"Sign in to Sync": "동기화를 위해 로그인하세요",
|
||||
"Synced at {{time}}": "{{time}}에 동기화됨",
|
||||
"Never synced": "동기화된 적 없음",
|
||||
"Show Remaining Time": "남은 시간 표시",
|
||||
"Show Page Number": "페이지 번호 표시",
|
||||
"{{time}} min left in chapter": "{{time}}분 남음",
|
||||
"Override Book Color": "책 색상 덮어쓰기",
|
||||
"Login Required": "로그인 필요",
|
||||
"Quota Exceeded": "한도 초과",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "일일 번역 문자 사용량 {{percentage}}%.",
|
||||
"Translation Characters": "번역 문자",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "일일 번역 할당량에 도달했습니다. 계속 진행하려면 다른 번역 서비스를 선택하세요.",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}}개의 음성",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "문제가 발생했습니다. 걱정하지 마세요, 저희 팀이 이미 알림을 받았으며 해결 작업을 진행 중입니다.",
|
||||
"Error Details:": "오류 세부사항:",
|
||||
"Try Again": "다시 시도",
|
||||
"Need help?": "도움이 필요하신가요?",
|
||||
"Contact Support": "지원팀에 문의",
|
||||
"Code Highlighting": "코드 하이라이팅",
|
||||
"Enable Highlighting": "하이라이팅 활성화",
|
||||
"Code Language": "코드 언어",
|
||||
"Top Margin (px)": "위쪽 여백 (px)",
|
||||
"Bottom Margin (px)": "아래쪽 여백 (px)",
|
||||
"Right Margin (px)": "오른쪽 여백 (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "리더 인터페이스 스타일용 CSS 입력...",
|
||||
"Crop": "자르기",
|
||||
"Book Covers": "책 표지",
|
||||
"Fit": "맞춤",
|
||||
"Reset {{settings}}": "{{settings}} 재설정",
|
||||
"Reset Settings": "설정 재설정",
|
||||
"{{count}} pages left in chapter_other": "남은 페이지 {{count}}장",
|
||||
"Show Remaining Pages": "남은 페이지 보기"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"Delete": "Verwijderen",
|
||||
"Cancel": "Annuleren",
|
||||
"Confirm Deletion": "Verwijdering bevestigen",
|
||||
"Are you sure to delete the selected books?": "Weet u zeker dat u de geselecteerde boeken wilt verwijderen?",
|
||||
"Deselect Book": "Boek deselecteren",
|
||||
"Select Book": "Boek selecteren",
|
||||
"Show Book Details": "Boekdetails weergeven",
|
||||
@@ -58,7 +57,6 @@
|
||||
"Import Books": "Boeken importeren",
|
||||
"Select Multiple Books": "Meerdere boeken selecteren",
|
||||
"Select Books": "Boeken selecteren",
|
||||
"Cancel Selection": "Selectie annuleren",
|
||||
"Storage": "Opslag",
|
||||
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% van cloudopslag gebruikt.",
|
||||
"Logged in as {{userDisplayName}}": "Ingelogd als {{userDisplayName}}",
|
||||
@@ -171,8 +169,6 @@
|
||||
"Hyphenation": "Woordafbreking",
|
||||
"Override Book Layout": "Boekopmaak overschrijven",
|
||||
"Page": "Pagina",
|
||||
"Vertical Margins (px)": "Verticale marges (px)",
|
||||
"Horizontal Margins (%)": "Horizontale marges (%)",
|
||||
"Maximum Number of Columns": "Maximum aantal kolommen",
|
||||
"Maximum Column Height": "Maximale kolomhoogte",
|
||||
"Maximum Column Width": "Maximale kolombreedte",
|
||||
@@ -193,13 +189,10 @@
|
||||
"Volume Keys for Page Flip": "Volumetoetsen voor pagina omslaan",
|
||||
"Clicks for Page Flip": "Klikken voor pagina omslaan",
|
||||
"Swap Clicks Area": "Klikgebieden omwisselen",
|
||||
"Custom CSS": "Aangepaste CSS",
|
||||
"Enter your custom CSS here...": "Voer hier uw aangepaste CSS in...",
|
||||
"Apply": "Toepassen",
|
||||
"Font": "Lettertype",
|
||||
"Layout": "Opmaak",
|
||||
"Color": "Kleur",
|
||||
"Misc": "Overig",
|
||||
"(from 'As You Like It', Act II)": "(uit 'As You Like It', Akte II)",
|
||||
"Custom Theme": "Aangepast thema",
|
||||
"Theme Name": "Themanaam",
|
||||
@@ -231,7 +224,6 @@
|
||||
"Slow": "Langzaam",
|
||||
"Fast": "Snel",
|
||||
"Reading Progress Synced": "Leesvoortgang gesynchroniseerd",
|
||||
"Cloud Storage": "Cloudopslag",
|
||||
"Failed to delete user. Please try again later.": "Gebruiker verwijderen mislukt. Probeer het later opnieuw.",
|
||||
"Free Tier": "Gratis niveau",
|
||||
"Community Support": "Community-ondersteuning",
|
||||
@@ -281,7 +273,7 @@
|
||||
"Description:": "Beschrijving:",
|
||||
"No description available": "Geen beschrijving beschikbaar",
|
||||
"Drop to Import Books": "Sleep om boeken te importeren",
|
||||
"A new version of Readest is Available!": "Een nieuwe versie van Readest is beschikbaar!",
|
||||
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} is beschikbaar (geïnstalleerde versie {{currentVersion}}).",
|
||||
"Download and install now?": "Nu downloaden en installeren?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Downloaden {{downloaded}} van {{contentLength}}",
|
||||
@@ -307,10 +299,80 @@
|
||||
"Reveal in Folder": "Tonen in map",
|
||||
"Don’t have an account? Sign up": "Nog geen account? Registreer u",
|
||||
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "De hele wereld is een toneel,\nEn alle mannen en vrouwen slechts spelers;\nZij hebben hun uitgangen en hun entrees,\nEn één man speelt in zijn tijd vele rollen,\nZijn daden zijnde zeven leeftijden.\n\n— William Shakespeare",
|
||||
"Touch": "Raak",
|
||||
"Scrolling Overlap (px)": "Scroll-overlap (px)",
|
||||
"Next Section": "Volgende sectie",
|
||||
"Previous Section": "Vorige sectie",
|
||||
"Next Page": "Volgende pagina",
|
||||
"Previous Page": "Vorige pagina"
|
||||
"Previous Page": "Vorige pagina",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Weet je zeker dat je {{count}} geselecteerd boek wilt verwijderen?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Weet je zeker dat je {{count}} geselecteerde boeken wilt verwijderen?",
|
||||
"Are you sure to delete the selected book?": "Weet je zeker dat je het geselecteerde boek wilt verwijderen?",
|
||||
"Deselect": "Deselecteer",
|
||||
"Select All": "Alles",
|
||||
"No translation available.": "Geen vertaling beschikbaar.",
|
||||
"Translated by {{provider}}.": "Vertaald door {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Vertalen",
|
||||
"Azure Translator": "Azure Vertaler",
|
||||
"Invert Image In Dark Mode": "Inverteer afbeelding in het donker",
|
||||
"Help improve Readest": "Help Readest verbeteren",
|
||||
"Sharing anonymized statistics": "Anonieme statistieken delen",
|
||||
"Interface Language": "Interface-taal",
|
||||
"Translation": "Vertaling",
|
||||
"Enable Translation": "Vertaling inschakelen",
|
||||
"Translation Service": "Vertaalservice",
|
||||
"Translate To": "Vertalen naar",
|
||||
"Disable Translation": "Vertaling uitschakelen",
|
||||
"Scroll": "Scrollen",
|
||||
"Overlap Pixels": "Overlap pixels",
|
||||
"Click": "Klik",
|
||||
"System Language": "Systemtaal",
|
||||
"Security": "Beveiliging",
|
||||
"Allow JavaScript": "JavaScript toestaan",
|
||||
"Enable only if you trust the file.": "Schakel alleen in als u het bestand vertrouwt.",
|
||||
"Sort TOC by Page": "__STRING_NOT_TRANSLATED__",
|
||||
"Search in {{count}} Book(s)..._one": "Zoeken in {{count}} Boek...",
|
||||
"Search in {{count}} Book(s)..._other": "Zoeken in {{count}} Boeken...",
|
||||
"No notes match your search": "Geen notities gevonden",
|
||||
"Search notes and excerpts...": "Zoek notities...",
|
||||
"Sign in to Sync": "Meld je aan om te synchroniseren",
|
||||
"Synced at {{time}}": "Gesynchroniseerd om {{time}}",
|
||||
"Never synced": "Nooit gesynchroniseerd",
|
||||
"Show Remaining Time": "Toon resterende tijd",
|
||||
"Show Page Number": "Toon paginanummer",
|
||||
"{{time}} min left in chapter": "Nog {{time}} min in hoofdstuk",
|
||||
"Override Book Color": "Boekkleur overschrijven",
|
||||
"Login Required": "Inloggen vereist",
|
||||
"Quota Exceeded": "Limiet overschreden",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% van de dagelijkse vertaaltekens gebruikt.",
|
||||
"Translation Characters": "Vertaaltekens",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Dagelijkse vertaalquotum bereikt. Selecteer een andere vertaalservice om door te gaan.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} stem",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} stemmen",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Er is iets misgegaan. Maak je geen zorgen, ons team is op de hoogte gebracht en we werken aan een oplossing.",
|
||||
"Error Details:": "Foutdetails:",
|
||||
"Try Again": "Probeer opnieuw",
|
||||
"Need help?": "Heb je hulp nodig?",
|
||||
"Contact Support": "Neem contact op met ondersteuning",
|
||||
"Code Highlighting": "Code markering",
|
||||
"Enable Highlighting": "Markering inschakelen",
|
||||
"Code Language": "Code taal",
|
||||
"Top Margin (px)": "Bovenmarge (px)",
|
||||
"Bottom Margin (px)": "Ondermarge (px)",
|
||||
"Right Margin (px)": "Rechter marge (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Voer CSS in voor de opmaak van de leesinterface...",
|
||||
"Crop": "Bijsnijden",
|
||||
"Book Covers": "Boekomslagen",
|
||||
"Fit": "Passend maken",
|
||||
"Reset {{settings}}": "Reset {{settings}}",
|
||||
"Reset Settings": "Instellingen resetten",
|
||||
"{{count}} pages left in chapter_one": "Nog {{count}} pagina in hoofdstuk",
|
||||
"{{count}} pages left in chapter_other": "Nog {{count}} pagina’s in hoofdstuk",
|
||||
"Show Remaining Pages": "Toon resterende pagina’s"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animacja",
|
||||
"Annotate": "Adnotacja",
|
||||
"Apply": "Zastosuj",
|
||||
"Are you sure to delete the selected books?": "Czy na pewno chcesz usunąć wybrane książki?",
|
||||
"Auto Mode": "Tryb automatyczny",
|
||||
"Behavior": "Zachowanie",
|
||||
"Book": "Książka",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Potwierdź usunięcie",
|
||||
"Copied to notebook": "Skopiowano do notatnika",
|
||||
"Copy": "Kopiuj",
|
||||
"Custom CSS": "Własny CSS",
|
||||
"Dark Mode": "Tryb ciemny",
|
||||
"Default": "Domyślne",
|
||||
"Default Font": "Domyślna czcionka",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Słownik",
|
||||
"Download Readest": "Pobierz Readest",
|
||||
"Edit": "Edytuj",
|
||||
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
|
||||
"Excerpts": "Fragmenty",
|
||||
"Failed to import book(s): {{filenames}}": "Nie udało się zaimportować książek: {{filenames}}",
|
||||
"Fast": "Szybko",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Uwzględnij całe słowa",
|
||||
"Maximum Number of Columns": "Maksymalna liczba kolumn",
|
||||
"Minimum Font Size": "Minimalny rozmiar czcionki",
|
||||
"Misc": "Różne",
|
||||
"Monospace Font": "Czcionka o stałej szerokości",
|
||||
"More Info": "Więcej informacji",
|
||||
"Nord": "Północny",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Twoja biblioteka",
|
||||
"TTS not supported for PDF": "TTS nie jest obsługiwane dla plików PDF",
|
||||
"Override Book Font": "Nadpisz czcionkę książki",
|
||||
"Vertical Margins (px)": "Marginesy pionowe (px)",
|
||||
"Horizontal Margins (%)": "Marginesy poziome (%)",
|
||||
"Apply to All Books": "Zastosuj do wszystkich książek",
|
||||
"Apply to This Book": "Zastosuj do tej książki",
|
||||
"Unable to fetch the translation. Try again later.": "Nie udało się pobrać tłumaczenia. Spróbuj ponownie później.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Zaloguj się za pomocą Apple",
|
||||
"Sign in with GitHub": "Zaloguj się za pomocą GitHub",
|
||||
"Account": "Konto",
|
||||
"Cloud Storage": "Przestrzeń w chmurze",
|
||||
"Failed to delete user. Please try again later.": "Nie udało się usunąć użytkownika. Spróbuj ponownie później.",
|
||||
"Unlimited Offline/Online Reading": "Nieograniczone czytanie offline/online",
|
||||
"Unlimited Cloud Sync Devices": "Nieograniczona liczba urządzeń synchronizowanych w chmurze",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Wyczyść wyszukiwanie",
|
||||
"Header & Footer": "Nagłówek i stopka",
|
||||
"Apply also in Scrolled Mode": "Zastosuj również w trybie przewijania",
|
||||
"A new version of Readest is Available!": "Nowa wersja Readest jest dostępna!",
|
||||
"A new version of Readest is available!": "Nowa wersja Readest jest dostępna!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} jest dostępny (zainstalowana wersja {{currentVersion}}).",
|
||||
"Download and install now?": "Pobierz i zainstaluj teraz?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Pobieranie {{downloaded}} z {{contentLength}}",
|
||||
@@ -304,13 +297,90 @@
|
||||
"Checking for updates...": "Sprawdzanie aktualizacji...",
|
||||
"Error checking for updates": "Błąd podczas sprawdzania aktualizacji",
|
||||
"Details": "Szczegóły",
|
||||
"Cancel Selection": "Anuluj zaznaczenie",
|
||||
"File Size:": "Rozmiar pliku:",
|
||||
"Auto Detect": "Automatyczne wykrywanie",
|
||||
"Touch": "Dotyk",
|
||||
"Scrolling Overlap (px)": "Nakładanie przewijania (px)",
|
||||
"Next Section": "Następna sekcja",
|
||||
"Previous Section": "Poprzednia sekcja",
|
||||
"Next Page": "Następna strona",
|
||||
"Previous Page": "Poprzednia strona"
|
||||
"Previous Page": "Poprzednia strona",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Czy na pewno chcesz usunąć {{count}} wybraną książkę?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_few": "Czy na pewno chcesz usunąć {{count}} wybrane książki?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "Czy na pewno chcesz usunąć {{count}} wybranych książek?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Czy na pewno chcesz usunąć {{count}} wybranych książek?",
|
||||
"Are you sure to delete the selected book?": "Czy na pewno chcesz usunąć wybraną książkę?",
|
||||
"Deselect": "Odznacz",
|
||||
"Select All": "Zaznacz",
|
||||
"No translation available.": "Brak dostępnego tłumaczenia.",
|
||||
"Translated by {{provider}}.": "Przetłumaczone przez {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Tłumacz Google",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Odwróć obraz w ciemności",
|
||||
"Help improve Readest": "Pomóż ulepszyć Readest",
|
||||
"Sharing anonymized statistics": "Udostępnianie zanonimizowanych statystyk",
|
||||
"Interface Language": "Język interfejsu",
|
||||
"Translation": "Tłumaczenie",
|
||||
"Enable Translation": "Włącz tłumaczenie",
|
||||
"Translation Service": "Usługa tłumaczenia",
|
||||
"Translate To": "Przetłumacz na",
|
||||
"Disable Translation": "Wyłącz tłumaczenie",
|
||||
"Scroll": "Przewijaj",
|
||||
"Overlap Pixels": "Przesunięcie pikseli",
|
||||
"Click": "Kliknij",
|
||||
"System Language": "Język systemu",
|
||||
"Security": "Bezpieczeństwo",
|
||||
"Allow JavaScript": "Zezwól na JavaScript",
|
||||
"Enable only if you trust the file.": "Zezwól tylko wtedy, gdy ufasz plikowi.",
|
||||
"Sort TOC by Page": "Sortuj spis treści według strony",
|
||||
"Search in {{count}} Book(s)..._one": "Szukaj w {{count}} książce...",
|
||||
"Search in {{count}} Book(s)..._few": "Szukaj w {{count}} książkach...",
|
||||
"Search in {{count}} Book(s)..._many": "Szukaj w {{count}} książkach...",
|
||||
"Search in {{count}} Book(s)..._other": "Szukaj w {{count}} książkach...",
|
||||
"No notes match your search": "Nie znaleziono notatek",
|
||||
"Search notes and excerpts...": "Szukaj notatek...",
|
||||
"Sign in to Sync": "Zaloguj się",
|
||||
"Synced at {{time}}": "Sync: {{time}}",
|
||||
"Never synced": "Brak synchronizacji",
|
||||
"Show Remaining Time": "Pokazuj pozostały czas",
|
||||
"Show Page Number": "Pokaż numer strony",
|
||||
"{{time}} min left in chapter": "{{time}} min pozostało do końca rozdziału",
|
||||
"Override Book Color": "Nadpisz kolor książki",
|
||||
"Login Required": "Wymagane logowanie",
|
||||
"Quota Exceeded": "Limit przekroczony",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% użytych znaków tłumaczenia dziennego.",
|
||||
"Translation Characters": "Znaki tłumaczenia",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Przekroczono dzienny limit znaków tłumaczenia. Wybierz inną usługę tłumaczenia, aby kontynuować.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} głos",
|
||||
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} głosy",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} głosów",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} głosu",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Coś poszło nie tak. Nie martw się, nasz zespół został powiadomiony i pracujemy nad naprawą.",
|
||||
"Error Details:": "Informacje o błędzie:",
|
||||
"Try Again": "Spróbuj ponownie",
|
||||
"Need help?": "Potrzebujesz pomocy?",
|
||||
"Contact Support": "Skontaktuj się z pomocą techniczną",
|
||||
"Code Highlighting": "Podświetlanie kodu",
|
||||
"Enable Highlighting": "Zezwól na podświetlanie",
|
||||
"Code Language": "Język kodu",
|
||||
"Top Margin (px)": "Górny margines (px)",
|
||||
"Bottom Margin (px)": "Dolny margines (px)",
|
||||
"Right Margin (px)": "Prawy margines (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Wprowadź CSS dla stylu interfejsu czytnika...",
|
||||
"Crop": "Przytnij",
|
||||
"Book Covers": "Okładki książek",
|
||||
"Fit": "Pasuje",
|
||||
"Reset {{settings}}": "Zresetuj {{settings}}",
|
||||
"Reset Settings": "Zresetuj ustawienia",
|
||||
"{{count}} pages left in chapter_one": "Pozostała {{count}} strona w rozdziale",
|
||||
"{{count}} pages left in chapter_few": "Pozostały {{count}} strony w rozdziale",
|
||||
"{{count}} pages left in chapter_many": "Pozostało {{count}} stron w rozdziale",
|
||||
"{{count}} pages left in chapter_other": "Pozostało {{count}} stron w rozdziale",
|
||||
"Show Remaining Pages": "Pokaż pozostałe strony"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animação",
|
||||
"Annotate": "Anotar",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "Tem certeza de que deseja excluir os livros selecionados?",
|
||||
"Auto Mode": "Modo Automático",
|
||||
"Behavior": "Comportamento",
|
||||
"Book": "Livro",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Confirmar Exclusão",
|
||||
"Copied to notebook": "Copiado para o caderno",
|
||||
"Copy": "Copiar",
|
||||
"Custom CSS": "CSS Personalizado",
|
||||
"Dark Mode": "Modo Escuro",
|
||||
"Default": "Padrão",
|
||||
"Default Font": "Fonte Padrão",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Dicionário",
|
||||
"Download Readest": "Baixar Readest",
|
||||
"Edit": "Editar",
|
||||
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
|
||||
"Excerpts": "Trechos",
|
||||
"Failed to import book(s): {{filenames}}": "Falha ao importar livro(s): {{filenames}}",
|
||||
"Fast": "Rápido",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Corresponder Palavras Inteiras",
|
||||
"Maximum Number of Columns": "Número Máximo de Colunas",
|
||||
"Minimum Font Size": "Tamanho Mínimo da Fonte",
|
||||
"Misc": "Diversos",
|
||||
"Monospace Font": "Fonte Monoespaçada",
|
||||
"More Info": "Mais Informações",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Sua Biblioteca",
|
||||
"TTS not supported for PDF": "TTS não suportado para PDF",
|
||||
"Override Book Font": "Sobrescrever Fonte do Livro",
|
||||
"Vertical Margins (px)": "Margens Verticais (px)",
|
||||
"Horizontal Margins (%)": "Margens Horizontais (%)",
|
||||
"Apply to All Books": "Aplicar a todos os livros",
|
||||
"Apply to This Book": "Aplicar a este livro",
|
||||
"Unable to fetch the translation. Try again later.": "Não foi possível buscar a tradução. Tente novamente mais tarde.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Entrar com o Apple",
|
||||
"Sign in with GitHub": "Entrar com o GitHub",
|
||||
"Account": "Conta",
|
||||
"Cloud Storage": "Armazenamento na nuvem",
|
||||
"Failed to delete user. Please try again later.": "Falha ao excluir usuário. Por favor, tente novamente mais tarde.",
|
||||
"Unlimited Offline/Online Reading": "Leitura offline/online ilimitada",
|
||||
"Unlimited Cloud Sync Devices": "Sincronização ilimitada de dispositivos na nuvem",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Limpar Pesquisa",
|
||||
"Header & Footer": "Cabeçalho e Rodapé",
|
||||
"Apply also in Scrolled Mode": "Aplicar também no Modo Rolado",
|
||||
"A new version of Readest is Available!": "Uma nova versão do Readest está disponível!",
|
||||
"A new version of Readest is available!": "Uma nova versão do Readest está disponível!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponível (versão instalada {{currentVersion}}).",
|
||||
"Download and install now?": "Baixar e instalar agora?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Baixando {{downloaded}} de {{contentLength}}",
|
||||
@@ -304,13 +297,86 @@
|
||||
"Checking for updates...": "Verificando atualizações...",
|
||||
"Error checking for updates": "Erro ao verificar atualizações",
|
||||
"Details": "Detalhes",
|
||||
"Cancel Selection": "Cancelar Seleção",
|
||||
"File Size:": "Tamanho do arquivo:",
|
||||
"Auto Detect": "Detectar Automaticamente",
|
||||
"Touch": "Toque",
|
||||
"Scrolling Overlap (px)": "Sobreposição de Rolagem (px)",
|
||||
"Next Section": "Próxima seção",
|
||||
"Previous Section": "Seção anterior",
|
||||
"Next Page": "Próxima página",
|
||||
"Previous Page": "Página anterior"
|
||||
"Previous Page": "Página anterior",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Tem certeza que deseja excluir {{count}} livro selecionado?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "Tem certeza que deseja excluir {{count}} livros selecionados?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Tem certeza que deseja excluir {{count}} livros selecionados?",
|
||||
"Are you sure to delete the selected book?": "Tem certeza que deseja excluir o livro selecionado?",
|
||||
"Deselect": "Desmarcar",
|
||||
"Select All": "Marcar tudo",
|
||||
"No translation available.": "Sem tradução disponível.",
|
||||
"Translated by {{provider}}.": "Traduzido por {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Tradutor",
|
||||
"Azure Translator": "Tradutor Azure",
|
||||
"Invert Image In Dark Mode": "Inverter Imagem no Escuro",
|
||||
"Help improve Readest": "Ajude a melhorar o Readest",
|
||||
"Sharing anonymized statistics": "Compartilhando estatísticas anonimizadas",
|
||||
"Interface Language": "Idioma da interface",
|
||||
"Translation": "Tradução",
|
||||
"Enable Translation": "Ativar tradução",
|
||||
"Translation Service": "Serviço de tradução",
|
||||
"Translate To": "Traduzir para",
|
||||
"Disable Translation": "Desativar tradução",
|
||||
"Scroll": "Rolar",
|
||||
"Overlap Pixels": "Sobreposição de Pixels",
|
||||
"Click": "Clique",
|
||||
"System Language": "Idioma do Sistema",
|
||||
"Security": "Segurança",
|
||||
"Allow JavaScript": "Permitir JavaScript",
|
||||
"Enable only if you trust the file.": "Ativar apenas se você confiar no arquivo.",
|
||||
"Sort TOC by Page": "Ordenar TOC por Página",
|
||||
"Search in {{count}} Book(s)..._one": "Pesquisar em {{count}} Livro...",
|
||||
"Search in {{count}} Book(s)..._many": "Pesquisar em {{count}} Livros...",
|
||||
"Search in {{count}} Book(s)..._other": "Pesquisar em {{count}} Livros...",
|
||||
"No notes match your search": "Nenhuma nota encontrada",
|
||||
"Search notes and excerpts...": "Buscar notas...",
|
||||
"Sign in to Sync": "Entrar",
|
||||
"Synced at {{time}}": "Sincronizado às {{time}}",
|
||||
"Never synced": "Nunca sincronizado",
|
||||
"Show Remaining Time": "Mostrar Tempo Restante",
|
||||
"Show Page Number": "Mostrar Número da Página",
|
||||
"{{time}} min left in chapter": "{{time}} min restantes no capítulo",
|
||||
"Override Book Color": "Substituir Cor do Livro",
|
||||
"Login Required": "Login necessário",
|
||||
"Quota Exceeded": "Limite excedido",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dos caracteres de tradução diários usados.",
|
||||
"Translation Characters": "Caracteres de Tradução",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Quota diária de tradução atingida. Selecione outro serviço de tradução para continuar.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} voz",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} vozes",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} vozes",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Algo deu errado. Não se preocupe, nossa equipe foi notificada e estamos trabalhando em uma correção.",
|
||||
"Error Details:": "Detalhes do Erro:",
|
||||
"Try Again": "Tentar Novamente",
|
||||
"Need help?": "Precisa de ajuda?",
|
||||
"Contact Support": "Contato com Suporte",
|
||||
"Code Highlighting": "Realce de Código",
|
||||
"Enable Highlighting": "Ativar Realce",
|
||||
"Code Language": "Idioma do Código",
|
||||
"Top Margin (px)": "Margem superior (px)",
|
||||
"Bottom Margin (px)": "Margem inferior (px)",
|
||||
"Right Margin (px)": "Margem direita (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Insira o CSS para o estilo da interface de leitura...",
|
||||
"Crop": "Cortar",
|
||||
"Book Covers": "Capas de Livros",
|
||||
"Fit": "Encaixar",
|
||||
"Reset {{settings}}": "Redefinir {{settings}}",
|
||||
"Reset Settings": "Redefinir Configurações",
|
||||
"{{count}} pages left in chapter_one": "Falta {{count}} página neste capítulo",
|
||||
"{{count}} pages left in chapter_many": "Faltam {{count}} páginas neste capítulo",
|
||||
"{{count}} pages left in chapter_other": "Faltam {{count}} páginas neste capítulo",
|
||||
"Show Remaining Pages": "Mostrar páginas restantes"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Анимация",
|
||||
"Annotate": "Аннотация",
|
||||
"Apply": "Применить",
|
||||
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
|
||||
"Auto Mode": "Автоматический режим",
|
||||
"Behavior": "Поведение",
|
||||
"Book": "Книга",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Подтвердить удаление",
|
||||
"Copied to notebook": "Скопировано в блокнот",
|
||||
"Copy": "Копировать",
|
||||
"Custom CSS": "Пользовательский CSS",
|
||||
"Dark Mode": "Тёмная тема",
|
||||
"Default": "По умолчанию",
|
||||
"Default Font": "Шрифт по умолчанию",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Словарь",
|
||||
"Download Readest": "Скачать Readest",
|
||||
"Edit": "Редактировать",
|
||||
"Enter your custom CSS here...": "Введите ваш пользовательский CSS здесь...",
|
||||
"Excerpts": "Отрывки",
|
||||
"Failed to import book(s): {{filenames}}": "Не удалось импортировать книгу(и): {{filenames}}",
|
||||
"Fast": "Быстро",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Только целые слова",
|
||||
"Maximum Number of Columns": "Максимальное количество колонок",
|
||||
"Minimum Font Size": "Минимальный размер шрифта",
|
||||
"Misc": "Разное",
|
||||
"Monospace Font": "Моноширинный шрифт",
|
||||
"More Info": "Подробнее",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Ваша библиотека",
|
||||
"TTS not supported for PDF": "TTS не поддерживается для PDF",
|
||||
"Override Book Font": "Переопределить шрифт книги",
|
||||
"Vertical Margins (px)": "Вертикальные поля (px)",
|
||||
"Horizontal Margins (%)": "Горизонтальные поля (%)",
|
||||
"Apply to All Books": "Применить ко всем книгам",
|
||||
"Apply to This Book": "Применить к этой книге",
|
||||
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Войти через Apple",
|
||||
"Sign in with GitHub": "Войти через GitHub",
|
||||
"Account": "Аккаунт",
|
||||
"Cloud Storage": "Облачное хранилище",
|
||||
"Failed to delete user. Please try again later.": "Не удалось удалить пользователя. Пожалуйста, повторите попытку позже.",
|
||||
"Unlimited Offline/Online Reading": "Безлимитное чтение офлайн/онлайн",
|
||||
"Unlimited Cloud Sync Devices": "Неограниченное количество устройств для облачной синхронизации",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Очистить поиск",
|
||||
"Header & Footer": "Заголовок и нижний колонтитул",
|
||||
"Apply also in Scrolled Mode": "Применить также в прокручиваемом режиме",
|
||||
"A new version of Readest is Available!": "Доступна новая версия Readest!",
|
||||
"A new version of Readest is available!": "Доступна новая версия Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступен (установленная версия {{currentVersion}}).",
|
||||
"Download and install now?": "Скачать и установить сейчас?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
|
||||
@@ -304,13 +297,90 @@
|
||||
"Checking for updates...": "Проверка обновлений...",
|
||||
"Error checking for updates": "Ошибка при проверке обновлений",
|
||||
"Details": "Инфо",
|
||||
"Cancel Selection": "Отменить выбор",
|
||||
"File Size:": "Размер файла:",
|
||||
"Auto Detect": "Автоопределение",
|
||||
"Touch": "Сенсорный экран",
|
||||
"Scrolling Overlap (px)": "Перекрытие прокрутки (px)",
|
||||
"Next Section": "Следующий раздел",
|
||||
"Previous Section": "Предыдущий раздел",
|
||||
"Next Page": "Следующая страница",
|
||||
"Previous Page": "Предыдущая страница"
|
||||
"Previous Page": "Предыдущая страница",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Вы уверены, что хотите удалить {{count}} выбранную книгу?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_few": "Вы уверены, что хотите удалить {{count}} выбранные книги?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "Вы уверены, что хотите удалить {{count}} выбранных книг?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Вы уверены, что хотите удалить {{count}} выбранных книг?",
|
||||
"Are you sure to delete the selected book?": "Вы уверены, что хотите удалить выбранную книгу?",
|
||||
"Deselect": "Снять",
|
||||
"Select All": "Выбрать все",
|
||||
"No translation available.": "Нет доступного перевода.",
|
||||
"Translated by {{provider}}.": "Переведено {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Инвертировать в тёмной теме",
|
||||
"Help improve Readest": "Помогите улучшить Readest",
|
||||
"Sharing anonymized statistics": "Отправка анонимной статистики",
|
||||
"Interface Language": "Язык интерфейса",
|
||||
"Translation": "Перевод",
|
||||
"Enable Translation": "Включить перевод",
|
||||
"Translation Service": "Сервис перевода",
|
||||
"Translate To": "Перевести на",
|
||||
"Disable Translation": "Отключить перевод",
|
||||
"Scroll": "Прокрутка",
|
||||
"Overlap Pixels": "Перекрывающиеся пиксели",
|
||||
"Click": "Клик",
|
||||
"System Language": "Системный язык",
|
||||
"Security": "Безопасность",
|
||||
"Allow JavaScript": "Разрешить JavaScript",
|
||||
"Enable only if you trust the file.": "Включите только если доверяете файлу.",
|
||||
"Sort TOC by Page": "Сортировать содержание по странице",
|
||||
"Search in {{count}} Book(s)..._one": "Поиск в {{count}} книге...",
|
||||
"Search in {{count}} Book(s)..._few": "Поиск в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._many": "Поиск в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._other": "Поиск в {{count}} книгах...",
|
||||
"No notes match your search": "Записи не найдены",
|
||||
"Search notes and excerpts...": "Искать записи...",
|
||||
"Sign in to Sync": "Войти",
|
||||
"Synced at {{time}}": "Синхр. в {{time}}",
|
||||
"Never synced": "Не синхр.",
|
||||
"Show Remaining Time": "Показать оставшееся время",
|
||||
"Show Page Number": "Показать номер страницы",
|
||||
"{{time}} min left in chapter": "{{time}} мин осталось в главе",
|
||||
"Override Book Color": "Переопределить цвет книги",
|
||||
"Login Required": "Требуется вход",
|
||||
"Quota Exceeded": "Лимит превышен",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "Использовано {{percentage}}% от суточного лимита символов перевода.",
|
||||
"Translation Characters": "Символы перевода",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Достигнут суточный лимит символов перевода. Выберите другой сервис перевода для продолжения.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} голос",
|
||||
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} голоса",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} голосов",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} голоса",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Что-то пошло не так. Не волнуйтесь, наша команда уведомлена и работает над исправлением.",
|
||||
"Error Details:": "Подробности ошибки:",
|
||||
"Try Again": "Попробовать снова",
|
||||
"Need help?": "Нужна помощь?",
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Code Highlighting": "Подсветка кода",
|
||||
"Enable Highlighting": "Включить подсветку",
|
||||
"Code Language": "Язык кода",
|
||||
"Top Margin (px)": "Верхний отступ (пикс)",
|
||||
"Bottom Margin (px)": "Нижний отступ (пикс)",
|
||||
"Right Margin (px)": "Правый отступ (пикс)",
|
||||
"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 интерфейса",
|
||||
"Enter CSS for reader interface styling...": "Введите CSS для оформления интерфейса ридера...",
|
||||
"Crop": "Обрезать",
|
||||
"Book Covers": "Обложки книг",
|
||||
"Fit": "Подогнать",
|
||||
"Reset {{settings}}": "Сбросить {{settings}}",
|
||||
"Reset Settings": "Сбросить настройки",
|
||||
"{{count}} pages left in chapter_one": "Осталась {{count}} страница в главе",
|
||||
"{{count}} pages left in chapter_few": "Осталось {{count}} страницы в главе",
|
||||
"{{count}} pages left in chapter_many": "Осталось {{count}} страниц в главе",
|
||||
"{{count}} pages left in chapter_other": "Осталось {{count}} страниц в главе",
|
||||
"Show Remaining Pages": "Показать оставшиеся страницы"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Animasyon",
|
||||
"Annotate": "Not Ekle",
|
||||
"Apply": "Uygula",
|
||||
"Are you sure to delete the selected books?": "Seçili kitapları silmek istediğinizden emin misiniz?",
|
||||
"Auto Mode": "Otomatik Mod",
|
||||
"Behavior": "Davranış",
|
||||
"Book": "Kitap",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Silmeyi Onayla",
|
||||
"Copied to notebook": "Not defterine kopyalandı",
|
||||
"Copy": "Kopyala",
|
||||
"Custom CSS": "Özel CSS",
|
||||
"Dark Mode": "Karanlık Mod",
|
||||
"Default": "Varsayılan",
|
||||
"Default Font": "Varsayılan Yazı Tipi",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Sözlük",
|
||||
"Download Readest": "Readest'i İndir",
|
||||
"Edit": "Düzenle",
|
||||
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
|
||||
"Excerpts": "Alıntılar",
|
||||
"Failed to import book(s): {{filenames}}": "Kitap(lar) içe aktarılamadı: {{filenames}}",
|
||||
"Fast": "Hızlı",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Tam Kelimeleri Eşleştir",
|
||||
"Maximum Number of Columns": "Maksimum Sütun Sayısı",
|
||||
"Minimum Font Size": "Minimum Yazı Boyutu",
|
||||
"Misc": "Diğer",
|
||||
"Monospace Font": "Eş Aralıklı Yazı Tipi",
|
||||
"More Info": "Daha Fazla Bilgi",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Kütüphaneniz",
|
||||
"TTS not supported for PDF": "PDF için TTS desteklenmiyor",
|
||||
"Override Book Font": "Kitap Yazı Tipini Geçersiz Kıl",
|
||||
"Vertical Margins (px)": "Dikey Kenar Boşlukları (px)",
|
||||
"Horizontal Margins (%)": "Yatay Kenar Boşlukları (%)",
|
||||
"Apply to All Books": "Tüm kitaplara uygula",
|
||||
"Apply to This Book": "Bu kitaba uygula",
|
||||
"Unable to fetch the translation. Try again later.": "Çeviri alınamıyor. Daha sonra tekrar deneyin.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Apple ile giriş yap",
|
||||
"Sign in with GitHub": "GitHub ile giriş yap",
|
||||
"Account": "Hesap",
|
||||
"Cloud Storage": "Bulut Depolama",
|
||||
"Failed to delete user. Please try again later.": "Kullanıcı silinemedi. Lütfen daha sonra tekrar deneyin.",
|
||||
"Unlimited Offline/Online Reading": "Sınırsız Çevrimdışı/Çevrimiçi Okuma",
|
||||
"Unlimited Cloud Sync Devices": "Sınırsız Bulut Senkronizasyon Cihazları",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Aramayı Temizle",
|
||||
"Header & Footer": "Başlık ve Altbilgi",
|
||||
"Apply also in Scrolled Mode": "Kaydırılmış Modda da Uygula",
|
||||
"A new version of Readest is Available!": "Yeni bir Readest sürümü mevcut!",
|
||||
"A new version of Readest is available!": "Yeni bir Readest sürümü mevcut!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} mevcut (yüklü sürüm {{currentVersion}}).",
|
||||
"Download and install now?": "Şimdi indirip yükleyelim mi?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "İndiriliyor {{downloaded}}/{{contentLength}}",
|
||||
@@ -304,13 +297,82 @@
|
||||
"Checking for updates...": "Yenilikler kontrol ediliyor...",
|
||||
"Error checking for updates": "Güncellemeleri kontrol ederken hata oluştu",
|
||||
"Details": "Detaylar",
|
||||
"Cancel Selection": "Işaretlemeyi İptal Et",
|
||||
"File Size:": "Dosya Boyutu:",
|
||||
"Auto Detect": "Algıla",
|
||||
"Touch": "Dokunmatik",
|
||||
"Scrolling Overlap (px)": "Kaydırma Üst Üste Binen (px)",
|
||||
"Next Section": "Sonraki Bölüm",
|
||||
"Previous Section": "Önceki Bölüm",
|
||||
"Next Page": "Sonraki Sayfa",
|
||||
"Previous Page": "Önceki Sayfa"
|
||||
"Previous Page": "Önceki Sayfa",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "{{count}} seçili kitabı silmek istediğinize emin misiniz?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "{{count}} seçili kitabı silmek istediğinize emin misiniz?",
|
||||
"Are you sure to delete the selected book?": "Seçili kitabı silmek istediğinize emin misiniz?",
|
||||
"Deselect": "Kaldır",
|
||||
"Select All": "Hepsi",
|
||||
"No translation available.": "Çeviri mevcut değil.",
|
||||
"Translated by {{provider}}.": "{{provider}} tarafından çevrildi.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Çeviri",
|
||||
"Azure Translator": "Azure Çevirmen",
|
||||
"Invert Image In Dark Mode": "Karartılmış Görüntüyü Ters Çevir",
|
||||
"Help improve Readest": "Readest’i geliştirin",
|
||||
"Sharing anonymized statistics": "Anonim verileri paylaş",
|
||||
"Interface Language": "Arayüz Dili",
|
||||
"Translation": "Çeviri",
|
||||
"Enable Translation": "Çeviriyi Etkinleştir",
|
||||
"Translation Service": "Çeviri Hizmeti",
|
||||
"Translate To": "Şuna Çevir",
|
||||
"Disable Translation": "Çeviriyi Devre Dışı Bırak",
|
||||
"Scroll": "Kaydır",
|
||||
"Overlap Pixels": "Örtüşen Pikseller",
|
||||
"Click": "Klik",
|
||||
"System Language": "System Dili",
|
||||
"Security": "Security",
|
||||
"Allow JavaScript": "JavaScript'e İzin Ver",
|
||||
"Enable only if you trust the file.": "Yalnızca dosyaya güveniyorsanız etkinleştirin.",
|
||||
"Sort TOC by Page": "İçindekileri Sayfaya Göre Sırala",
|
||||
"Search in {{count}} Book(s)..._one": "{{count}} kitapta ara...",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}} kitapta ara...",
|
||||
"No notes match your search": "Eşleşen not bulunamadı",
|
||||
"Search notes and excerpts...": "Notlarda ara...",
|
||||
"Sign in to Sync": "Giriş yap",
|
||||
"Synced at {{time}}": "{{time}}'de eşzlendi",
|
||||
"Never synced": "Hiç eşzlenmedi",
|
||||
"Show Remaining Time": "Kalan Süreyi Göster",
|
||||
"Show Page Number": "Sayfa Numarasını Göster",
|
||||
"{{time}} min left in chapter": "{{time}} dakika bölümde kaldı",
|
||||
"Override Book Color": "Kitap Rengini Geçersiz Kıl",
|
||||
"Login Required": "Giriş gerekli",
|
||||
"Quota Exceeded": "Kota aşıldı",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "Çeviri için günlük karakterlerin %{{percentage}}'si kullanıldı.",
|
||||
"Translation Characters": "Çeviri Karakterleri",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Günlük çeviri kotası aşıldı. Devam etmek için başka bir çeviri hizmeti seçin.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} ses",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} sesler",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Bir şeyler yanlış gitti. Endişelenmeyin, ekibimiz bilgilendirildi ve bir çözüm üzerinde çalışıyoruz.",
|
||||
"Error Details:": "Error Detayları:",
|
||||
"Try Again": "Tekrar Dene",
|
||||
"Need help?": "Yardıma mı ihtiyacınız var?",
|
||||
"Contact Support": "Destekle İletişime Geç",
|
||||
"Code Highlighting": "Code Vurgulama",
|
||||
"Enable Highlighting": "Vurgulamayı Etkinleştir",
|
||||
"Code Language": "Code Dili",
|
||||
"Top Margin (px)": "Üst Kenar Boşluğu (px)",
|
||||
"Bottom Margin (px)": "Alt Kenar Boşluğu (px)",
|
||||
"Right Margin (px)": "Sağ Kenar Boşluğu (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Okuyucu arayüzü stili için CSS girin...",
|
||||
"Crop": "Kes",
|
||||
"Book Covers": "Kitap Kapağı",
|
||||
"Fit": "Uygun",
|
||||
"Reset {{settings}}": "{{settings}}'i Sıfırla",
|
||||
"Reset Settings": "Ayarları Sıfırla",
|
||||
"{{count}} pages left in chapter_one": "Bu bölümde {{count}} sayfa kaldı",
|
||||
"{{count}} pages left in chapter_other": "Bu bölümde {{count}} sayfa kaldı",
|
||||
"Show Remaining Pages": "Kalan sayfaları göster"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Анімація",
|
||||
"Annotate": "Анотація",
|
||||
"Apply": "Застосувати",
|
||||
"Are you sure to delete the selected books?": "Ви впевнені, що хочете видалити вибрані книги?",
|
||||
"Auto Mode": "Автоматичний режим",
|
||||
"Behavior": "Поведінка",
|
||||
"Book": "Книга",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Підтвердити видалення",
|
||||
"Copied to notebook": "Скопійовано до нотатника",
|
||||
"Copy": "Копіювати",
|
||||
"Custom CSS": "Користувацький CSS",
|
||||
"Dark Mode": "Темний режим",
|
||||
"Default": "За замовчуванням",
|
||||
"Default Font": "Шрифт за замовчуванням",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Словник",
|
||||
"Download Readest": "Завантажити Readest",
|
||||
"Edit": "Редагувати",
|
||||
"Enter your custom CSS here...": "Введіть ваш користувацький CSS тут...",
|
||||
"Excerpts": "Уривки",
|
||||
"Failed to import book(s): {{filenames}}": "Не вдалося імпортувати книгу(и): {{filenames}}",
|
||||
"Fast": "Швидко",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Шукати цілі слова",
|
||||
"Maximum Number of Columns": "Максимальна кількість колонок",
|
||||
"Minimum Font Size": "Мінімальний розмір шрифту",
|
||||
"Misc": "Різне",
|
||||
"Monospace Font": "Моноширинний шрифт",
|
||||
"More Info": "Більше інформації",
|
||||
"Nord": "Північний",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Ваша бібліотека",
|
||||
"TTS not supported for PDF": "TTS не підтримується для PDF",
|
||||
"Override Book Font": "Перевизначити шрифт книги",
|
||||
"Vertical Margins (px)": "Вертикальні відступи (px)",
|
||||
"Horizontal Margins (%)": "Горизонтальні відступи (%)",
|
||||
"Apply to All Books": "Застосувати до всіх книг",
|
||||
"Apply to This Book": "Застосувати до цієї книги",
|
||||
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Увійти через Apple",
|
||||
"Sign in with GitHub": "Увійти через GitHub",
|
||||
"Account": "Обліковий запис",
|
||||
"Cloud Storage": "Хмарне сховище",
|
||||
"Failed to delete user. Please try again later.": "Не вдалося видалити користувача. Будь ласка, спробуйте пізніше.",
|
||||
"Unlimited Offline/Online Reading": "Необмежене читання офлайн/онлайн",
|
||||
"Unlimited Cloud Sync Devices": "Необмежена кількість пристроїв для хмарної синхронізації",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Очистити пошук",
|
||||
"Header & Footer": "Заголовок і нижній колонтитул",
|
||||
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
|
||||
"A new version of Readest is Available!": "Доступна нова версія Readest!",
|
||||
"A new version of Readest is available!": "Доступна нова версія Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
|
||||
"Download and install now?": "Завантажити та встановити зараз?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
|
||||
@@ -304,13 +297,90 @@
|
||||
"Checking for updates...": "Перевірка наявності оновлень...",
|
||||
"Error checking for updates": "Помилка перевірки наявності оновлень",
|
||||
"Details": "Деталі",
|
||||
"Cancel Selection": "Скасувати вибір",
|
||||
"File Size:": "Розмір файлу:",
|
||||
"Auto Detect": "Авто виявлення",
|
||||
"Touch": "Дотик",
|
||||
"Scrolling Overlap (px)": "Прокрутка перекриття (px)",
|
||||
"Next Section": "Наступний розділ",
|
||||
"Previous Section": "Попередній розділ",
|
||||
"Next Page": "Наступна сторінка",
|
||||
"Previous Page": "Попередня сторінка"
|
||||
"Previous Page": "Попередня сторінка",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Ви впевнені, що хочете видалити {{count}} вибрану книгу?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_few": "Ви впевнені, що хочете видалити {{count}} вибрані книги?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_many": "Ви впевнені, що хочете видалити {{count}} вибраних книг?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Ви впевнені, що хочете видалити {{count}} вибраних книг?",
|
||||
"Are you sure to delete the selected book?": "Ви впевнені, що хочете видалити вибрану книгу?",
|
||||
"Deselect": "Зняти",
|
||||
"Select All": "Усі",
|
||||
"No translation available.": "Переклад недоступний.",
|
||||
"Translated by {{provider}}.": "Перекладено {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Інвертувати зображення в темряві",
|
||||
"Help improve Readest": "Допоможіть покращити Readest",
|
||||
"Sharing anonymized statistics": "Поділ анонімною статистикою",
|
||||
"Interface Language": "Мова інтерфейсу",
|
||||
"Translation": "Переклад",
|
||||
"Enable Translation": "Увімкнути переклад",
|
||||
"Translation Service": "Служба перекладу",
|
||||
"Translate To": "Перекласти на",
|
||||
"Disable Translation": "Вимкнути переклад",
|
||||
"Scroll": "Прокрутка",
|
||||
"Overlap Pixels": "Пікселі перекриття",
|
||||
"Click": "Клацнути",
|
||||
"System Language": "Системна мова",
|
||||
"Security": "Безпека",
|
||||
"Allow JavaScript": "Дозволити JavaScript",
|
||||
"Enable only if you trust the file.": "Увімкніть лише якщо довіряєте файлу.",
|
||||
"Sort TOC by Page": "Сортувати зміст за сторінкою",
|
||||
"Search in {{count}} Book(s)..._one": "Шукати в {{count}} книзі...",
|
||||
"Search in {{count}} Book(s)..._few": "Шукати в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._many": "Шукати в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._other": "Шукати в {{count}} книгах...",
|
||||
"No notes match your search": "Нотаток не знайдено",
|
||||
"Search notes and excerpts...": "Шукати нотатки...",
|
||||
"Sign in to Sync": "Увійти",
|
||||
"Synced at {{time}}": "Синхр. о {{time}}",
|
||||
"Never synced": "Ніколи не синхр.",
|
||||
"Show Remaining Time": "Показати залишок часу",
|
||||
"Show Page Number": "Показати номер сторінки",
|
||||
"{{time}} min left in chapter": "{{time}} хв до кінця розділу",
|
||||
"Override Book Color": "Перевизначити колір книги",
|
||||
"Login Required": "Потрібен вхід",
|
||||
"Quota Exceeded": "Перевищено ліміт",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "Використано {{percentage}}% символів перекладу за день.",
|
||||
"Translation Characters": "Символи перекладу",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Досягнуто добового ліміту перекладу. Виберіть іншу службу перекладу для продовження.",
|
||||
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} голос",
|
||||
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} голоси",
|
||||
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} голосів",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} голоси",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Щось пішло не так. Не хвилюйтеся, наша команда була сповіщена, і ми працюємо над виправленням.",
|
||||
"Error Details:": "Деталі помилки:",
|
||||
"Try Again": "Спробувати ще раз",
|
||||
"Need help?": "Потрібна допомога?",
|
||||
"Contact Support": "Зв'язатися з підтримкою",
|
||||
"Code Highlighting": "Підсвічування коду",
|
||||
"Enable Highlighting": "Увімкнути підсвічування",
|
||||
"Code Language": "Мова коду",
|
||||
"Top Margin (px)": "Верхній відступ (px)",
|
||||
"Bottom Margin (px)": "Нижній відступ (px)",
|
||||
"Right Margin (px)": "Правий відступ (px)",
|
||||
"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 інтерфейсу",
|
||||
"Enter CSS for reader interface styling...": "Введіть CSS для стилізації інтерфейсу рідера...",
|
||||
"Crop": "Обрізати",
|
||||
"Book Covers": "Обкладинки книг",
|
||||
"Fit": "Вписати",
|
||||
"Reset {{settings}}": "Скинути {{settings}}",
|
||||
"Reset Settings": "Скинути налаштування",
|
||||
"{{count}} pages left in chapter_one": "Залишилася {{count}} сторінка у розділі",
|
||||
"{{count}} pages left in chapter_few": "Залишилось {{count}} сторінки у розділі",
|
||||
"{{count}} pages left in chapter_many": "Залишилось {{count}} сторінок у розділі",
|
||||
"{{count}} pages left in chapter_other": "Залишилось {{count}} сторінок у розділі",
|
||||
"Show Remaining Pages": "Показати залишок сторінок"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Hiệu ứng động",
|
||||
"Annotate": "Chú thích",
|
||||
"Apply": "Áp dụng",
|
||||
"Are you sure to delete the selected books?": "Bạn có chắc chắn muốn xóa các sách đã chọn?",
|
||||
"Auto Mode": "Chế độ tự động",
|
||||
"Behavior": "Hành vi",
|
||||
"Book": "Sách",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "Xác nhận xóa",
|
||||
"Copied to notebook": "Đã sao chép vào sổ ghi chú",
|
||||
"Copy": "Sao chép",
|
||||
"Custom CSS": "CSS tùy chỉnh",
|
||||
"Dark Mode": "Chế độ tối",
|
||||
"Default": "Mặc định",
|
||||
"Default Font": "Phông chữ mặc định",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "Từ điển",
|
||||
"Download Readest": "Tải Readest",
|
||||
"Edit": "Chỉnh sửa",
|
||||
"Enter your custom CSS here...": "Nhập CSS tùy chỉnh của bạn vào đây...",
|
||||
"Excerpts": "Trích dẫn",
|
||||
"Failed to import book(s): {{filenames}}": "Không thể nhập sách: {{filenames}}",
|
||||
"Fast": "Nhanh",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "Khớp toàn bộ từ",
|
||||
"Maximum Number of Columns": "Số cột tối đa",
|
||||
"Minimum Font Size": "Cỡ chữ tối thiểu",
|
||||
"Misc": "Khác",
|
||||
"Monospace Font": "Phông chữ đơn cách",
|
||||
"More Info": "Thông tin thêm",
|
||||
"Nord": "Nord",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "Thư viện của bạn",
|
||||
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF",
|
||||
"Override Book Font": "Ghi đè phông chữ sách",
|
||||
"Vertical Margins (px)": "Lề dọc (px)",
|
||||
"Horizontal Margins (%)": "Lề ngang (%)",
|
||||
"Apply to All Books": "Áp dụng cho tất cả sách",
|
||||
"Apply to This Book": "Áp dụng cho sách này",
|
||||
"Unable to fetch the translation. Try again later.": "Không thể tải dịch vụ. Vui lòng thử lại sau.",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "Đăng nhập với Apple",
|
||||
"Sign in with GitHub": "Đăng nhập với GitHub",
|
||||
"Account": "Tài khoản",
|
||||
"Cloud Storage": "Lưu trữ đám mây",
|
||||
"Failed to delete user. Please try again later.": "Không thể xóa người dùng. Vui lòng thử lại sau.",
|
||||
"Unlimited Offline/Online Reading": "Đọc ngoại tuyến/trực tuyến không giới hạn",
|
||||
"Unlimited Cloud Sync Devices": "Đồng bộ hóa thiết bị đám mây không giới hạn",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "Xóa tìm kiếm",
|
||||
"Header & Footer": "Đầu trang & Chân trang",
|
||||
"Apply also in Scrolled Mode": "Áp dụng cũng trong chế độ cuộn",
|
||||
"A new version of Readest is Available!": "Đã có phiên bản mới của Readest!",
|
||||
"A new version of Readest is available!": "Đã có phiên bản mới của Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} đã có sẵn (phiên bản đã cài đặt {{currentVersion}}).",
|
||||
"Download and install now?": "Tải xuống và cài đặt ngay bây giờ?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Đang tải {{downloaded}} trong {{contentLength}}",
|
||||
@@ -304,13 +297,78 @@
|
||||
"Checking for updates...": "Đang kiểm tra cập nhật...",
|
||||
"Error checking for updates": "Lỗi khi kiểm tra cập nhật",
|
||||
"Details": "Chi tiết",
|
||||
"Cancel Selection": "Hủy chọn",
|
||||
"File Size:": "Kích thước tệp:",
|
||||
"Auto Detect": "Phát hiện tự động",
|
||||
"Touch": "Chạm",
|
||||
"Scrolling Overlap (px)": "Chồng chéo cuộn (px)",
|
||||
"Next Section": "Phần tiếp theo",
|
||||
"Previous Section": "Phần trước",
|
||||
"Next Page": "Trang tiếp theo",
|
||||
"Previous Page": "Trang trước"
|
||||
"Previous Page": "Trang trước",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Bạn có chắc muốn xóa {{count}} cuốn sách đã chọn không?",
|
||||
"Are you sure to delete the selected book?": "Bạn có chắc muốn xóa cuốn sách đã chọn không?",
|
||||
"Deselect": "Bỏ chọn",
|
||||
"Select All": "Chọn tất",
|
||||
"No translation available.": "Không có bản dịch nào.",
|
||||
"Translated by {{provider}}.": "Được dịch bởi {{provider}}.",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Dịch",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Đảo ảnh chế độ tối",
|
||||
"Help improve Readest": "Giúp cải thiện Readest",
|
||||
"Sharing anonymized statistics": "Chia sẻ thống kê ẩn danh",
|
||||
"Interface Language": "Ngôn ngữ giao diện",
|
||||
"Translation": "Dịch",
|
||||
"Enable Translation": "Bật dịch",
|
||||
"Translation Service": "Dịch vụ dịch thuật",
|
||||
"Translate To": "Dịch sang",
|
||||
"Disable Translation": "Tắt dịch",
|
||||
"Scroll": "Cuộn",
|
||||
"Overlap Pixels": "Chồng lấn pixel",
|
||||
"Click": "Nhấp",
|
||||
"System Language": "Ngôn ngữ hệ thống",
|
||||
"Security": "An ninh",
|
||||
"Allow JavaScript": "Cho phép JavaScript",
|
||||
"Enable only if you trust the file.": "Chỉ bật nếu bạn tin tưởng tệp.",
|
||||
"Sort TOC by Page": "Sort Mục lục theo Trang",
|
||||
"Search in {{count}} Book(s)..._other": "Tìm kiếm trong {{count}} cuốn sách...",
|
||||
"No notes match your search": "Không tìm thấy ghi chú nào",
|
||||
"Search notes and excerpts...": "Tìm ghi chú...",
|
||||
"Sign in to Sync": "Đăng nhập",
|
||||
"Synced at {{time}}": "Đã đồng bộ lúc {{time}}",
|
||||
"Never synced": "Chưa từng đồng bộ",
|
||||
"Show Remaining Time": "Hiển thị thời gian còn lại",
|
||||
"Show Page Number": "Hiển thị số trang",
|
||||
"{{time}} min left in chapter": "{{time}} phút còn lại trong chương",
|
||||
"Override Book Color": "Đổi màu sách",
|
||||
"Login Required": "Cần đăng nhập",
|
||||
"Quota Exceeded": "Vượt hạn mức",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% số ký tự dịch hàng ngày đã sử dụng.",
|
||||
"Translation Characters": "Những ký tự dịch",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "Đã đạt hạn mức dịch hàng ngày. Chọn dịch vụ dịch khác để tiếp tục.",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} giọng",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Đã xảy ra sự cố. Đừng lo, nhóm của chúng tôi đã được thông báo và chúng tôi đang làm việc để sửa chữa.",
|
||||
"Error Details:": "Chi tiết lỗi:",
|
||||
"Try Again": "Thử lại",
|
||||
"Need help?": "Bạn cần giúp đỡ?",
|
||||
"Contact Support": "Liên hệ hỗ trợ",
|
||||
"Code Highlighting": "Đánh dấu mã",
|
||||
"Enable Highlighting": "Cho phép đánh dấu",
|
||||
"Code Language": "Ngôn ngữ mã",
|
||||
"Top Margin (px)": "Lề trên (px)",
|
||||
"Bottom Margin (px)": "Lề dưới (px)",
|
||||
"Right Margin (px)": "Lề phải (px)",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "Nhập CSS cho giao diện trình đọc...",
|
||||
"Crop": "Cắt",
|
||||
"Book Covers": "Bìa sách",
|
||||
"Fit": "Vừa vặn",
|
||||
"Reset {{settings}}": "Đặt lại {{settings}}",
|
||||
"Reset Settings": "Đặt lại cài đặt",
|
||||
"{{count}} pages left in chapter_other": "Còn {{count}} trang trong chương",
|
||||
"Show Remaining Pages": "Hiển thị trang còn lại"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "动画",
|
||||
"Annotate": "笔记",
|
||||
"Apply": "应用",
|
||||
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
|
||||
"Auto Mode": "自动主题",
|
||||
"Behavior": "行为",
|
||||
"Book": "书籍",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "确认删除",
|
||||
"Copied to notebook": "已复制到笔记本",
|
||||
"Copy": "复制",
|
||||
"Custom CSS": "自定义 CSS",
|
||||
"Dark Mode": "深色主题",
|
||||
"Default": "默认",
|
||||
"Default Font": "默认字体",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "词典",
|
||||
"Download Readest": "下载 Readest",
|
||||
"Edit": "编辑",
|
||||
"Enter your custom CSS here...": "在此处输入您的自定义 CSS...",
|
||||
"Excerpts": "摘录",
|
||||
"Failed to import book(s): {{filenames}}": "导入书籍失败:{{filenames}}",
|
||||
"Fast": "快",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "匹配整个单词",
|
||||
"Maximum Number of Columns": "分栏数",
|
||||
"Minimum Font Size": "最小字号",
|
||||
"Misc": "杂项",
|
||||
"Monospace Font": "等宽字体",
|
||||
"More Info": "更多信息",
|
||||
"Nord": "极光",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "书库",
|
||||
"TTS not supported for PDF": "PDF 文档暂不支持 TTS",
|
||||
"Override Book Font": "覆盖书籍字体",
|
||||
"Vertical Margins (px)": "垂直边距(px)",
|
||||
"Horizontal Margins (%)": "水平边距(%)",
|
||||
"Apply to All Books": "应用于所有书籍",
|
||||
"Apply to This Book": "应用于此书籍",
|
||||
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "使用 Apple 登录",
|
||||
"Sign in with GitHub": "使用 GitHub 登录",
|
||||
"Account": "账户",
|
||||
"Cloud Storage": "云存储",
|
||||
"Failed to delete user. Please try again later.": "删除用户失败。请稍后重试。",
|
||||
"Unlimited Offline/Online Reading": "无限离线和在线阅读",
|
||||
"Unlimited Cloud Sync Devices": "无限云同步设备个数",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "清除搜索",
|
||||
"Header & Footer": "页眉页脚",
|
||||
"Apply also in Scrolled Mode": "应用到滚动模式",
|
||||
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
|
||||
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安装版本 {{currentVersion}})。",
|
||||
"Download and install now?": "立即下载并安装?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
|
||||
@@ -304,13 +297,78 @@
|
||||
"Checking for updates...": "检查更新中...",
|
||||
"Error checking for updates": "检查更新时出错",
|
||||
"Details": "详情",
|
||||
"Cancel Selection": "取消选择",
|
||||
"File Size:": "文件大小",
|
||||
"Auto Detect": "自动检测",
|
||||
"Touch": "触控",
|
||||
"Scrolling Overlap (px)": "滚动重叠(px)",
|
||||
"Next Section": "下一章",
|
||||
"Previous Section": "上一章",
|
||||
"Next Page": "下一页",
|
||||
"Previous Page": "上一页"
|
||||
"Previous Page": "上一页",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "您确定要删除 {{count}} 本选中的书籍吗?",
|
||||
"Are you sure to delete the selected book?": "您确定要删除选中的书籍吗?",
|
||||
"Deselect": "取消选择",
|
||||
"Select All": "全选",
|
||||
"No translation available.": "暂无可用的翻译",
|
||||
"Translated by {{provider}}.": "由 {{provider}} 提供翻译",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "深色主题下图片反色",
|
||||
"Help improve Readest": "帮助改进 Readest",
|
||||
"Sharing anonymized statistics": "分享匿名统计数据",
|
||||
"Interface Language": "界面语言",
|
||||
"Translation": "翻译",
|
||||
"Enable Translation": "启用翻译",
|
||||
"Translation Service": "翻译服务",
|
||||
"Translate To": "翻译到",
|
||||
"Disable Translation": "禁用翻译",
|
||||
"Scroll": "滚动",
|
||||
"Overlap Pixels": "重叠像素",
|
||||
"Click": "点击",
|
||||
"System Language": "系统语言",
|
||||
"Security": "安全",
|
||||
"Allow JavaScript": "允许 JavaScript",
|
||||
"Enable only if you trust the file.": "建议仅在您信任该文件时启用",
|
||||
"Sort TOC by Page": "按页码排序目录",
|
||||
"Search in {{count}} Book(s)..._other": "在 {{count}} 本书籍中搜索...",
|
||||
"No notes match your search": "未找到匹配的笔记",
|
||||
"Search notes and excerpts...": "搜索笔记...",
|
||||
"Sign in to Sync": "登录后同步",
|
||||
"Synced at {{time}}": "同步于:{{time}}",
|
||||
"Never synced": "从未同步",
|
||||
"Show Remaining Time": "显示剩余时间",
|
||||
"Show Page Number": "显示页码",
|
||||
"{{time}} min left in chapter": "本章剩余 {{time}} 分钟",
|
||||
"Override Book Color": "覆盖书籍颜色",
|
||||
"Login Required": "需登录",
|
||||
"Quota Exceeded": "额度不足",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻译字符数",
|
||||
"Translation Characters": "翻译字数",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "已达到每日翻译配额,请选择其他翻译服务继续。",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 种声音",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "出了点问题。请不要担心,我们的团队已经收到通知并正在修复中。",
|
||||
"Error Details:": "错误详情:",
|
||||
"Try Again": "重试",
|
||||
"Need help?": "需要帮助吗?",
|
||||
"Contact Support": "联系客服",
|
||||
"Code Highlighting": "代码高亮",
|
||||
"Enable Highlighting": "启用高亮",
|
||||
"Code Language": "代码语言",
|
||||
"Top Margin (px)": "上边距",
|
||||
"Bottom Margin (px)": "下边距",
|
||||
"Right Margin (px)": "右边距",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "输入阅读器界面样式的CSS…",
|
||||
"Crop": "裁剪",
|
||||
"Book Covers": "书籍封面",
|
||||
"Fit": "适应",
|
||||
"Reset {{settings}}": "重置{{settings}}",
|
||||
"Reset Settings": "重置设置",
|
||||
"{{count}} pages left in chapter_other": "本章剩余 {{count}} 页",
|
||||
"Show Remaining Pages": "显示剩余页数"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "動畫",
|
||||
"Annotate": "筆記",
|
||||
"Apply": "應用",
|
||||
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
|
||||
"Auto Mode": "自動主題",
|
||||
"Behavior": "行為",
|
||||
"Book": "書籍",
|
||||
@@ -19,7 +18,6 @@
|
||||
"Confirm Deletion": "確認刪除",
|
||||
"Copied to notebook": "已複製到筆記本",
|
||||
"Copy": "複製",
|
||||
"Custom CSS": "自定義 CSS",
|
||||
"Dark Mode": "深色主題",
|
||||
"Default": "預設",
|
||||
"Default Font": "預設字體",
|
||||
@@ -29,7 +27,6 @@
|
||||
"Dictionary": "詞典",
|
||||
"Download Readest": "下載 Readest",
|
||||
"Edit": "編輯",
|
||||
"Enter your custom CSS here...": "在此處輸入您的自定義 CSS...",
|
||||
"Excerpts": "摘錄",
|
||||
"Failed to import book(s): {{filenames}}": "導入書籍失敗:{{filenames}}",
|
||||
"Fast": "快",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Match Whole Words": "匹配整個單詞",
|
||||
"Maximum Number of Columns": "分欄數",
|
||||
"Minimum Font Size": "最小字號",
|
||||
"Misc": "雜項",
|
||||
"Monospace Font": "等寬字體",
|
||||
"More Info": "更多信息",
|
||||
"Nord": "極光",
|
||||
@@ -116,8 +112,6 @@
|
||||
"Your Library": "書庫",
|
||||
"TTS not supported for PDF": "PDF 文檔暫不支持 TTS",
|
||||
"Override Book Font": "覆蓋書籍字體",
|
||||
"Vertical Margins (px)": "垂直邊距(px)",
|
||||
"Horizontal Margins (%)": "水平邊距(%)",
|
||||
"Apply to All Books": "應用於所有書籍",
|
||||
"Apply to This Book": "應用於此書籍",
|
||||
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。",
|
||||
@@ -188,7 +182,6 @@
|
||||
"Sign in with Apple": "使用 Apple 登入",
|
||||
"Sign in with GitHub": "使用 GitHub 登入",
|
||||
"Account": "帳戶",
|
||||
"Cloud Storage": "雲端儲存空間",
|
||||
"Failed to delete user. Please try again later.": "刪除使用者失敗。請稍後再試。",
|
||||
"Unlimited Offline/Online Reading": "無限離線/線上閱讀",
|
||||
"Unlimited Cloud Sync Devices": "無限雲端同步裝置",
|
||||
@@ -269,7 +262,7 @@
|
||||
"Clear Search": "清除搜索",
|
||||
"Header & Footer": "頁眉頁腳",
|
||||
"Apply also in Scrolled Mode": "滾動模式同樣適用",
|
||||
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
|
||||
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安裝版本 {{currentVersion}})。",
|
||||
"Download and install now?": "立即下載並安裝?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
|
||||
@@ -304,13 +297,78 @@
|
||||
"Checking for updates...": "檢查更新中...",
|
||||
"Error checking for updates": "檢查更新時出錯",
|
||||
"Details": "詳情",
|
||||
"Cancel Selection": "取消選擇",
|
||||
"File Size:": "檔案大小",
|
||||
"Auto Detect": "自動檢測",
|
||||
"Touch": "觸控",
|
||||
"Scrolling Overlap (px)": "滾動重疊(px)",
|
||||
"Next Section": "下一章",
|
||||
"Previous Section": "上一章",
|
||||
"Next Page": "下一頁",
|
||||
"Previous Page": "上一頁"
|
||||
"Previous Page": "上一頁",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "確定要刪除已選擇的 {{count}} 本書嗎?",
|
||||
"Are you sure to delete the selected book?": "確定要刪除已選擇的書籍嗎?",
|
||||
"Deselect": "取消選取",
|
||||
"Select All": "全選",
|
||||
"No translation available.": "暫無翻譯",
|
||||
"Translated by {{provider}}.": "由 {{provider}} 提供翻譯",
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "深色主題下圖片反相",
|
||||
"Help improve Readest": "幫助改進 Readest",
|
||||
"Sharing anonymized statistics": "分享匿名統計數據",
|
||||
"Interface Language": "介面語言",
|
||||
"Translation": "翻譯",
|
||||
"Enable Translation": "啟用翻譯",
|
||||
"Translation Service": "翻譯服務",
|
||||
"Translate To": "翻譯至",
|
||||
"Disable Translation": "停用翻譯",
|
||||
"Scroll": "滾動",
|
||||
"Overlap Pixels": "重疊像素",
|
||||
"Click": "點擊",
|
||||
"System Language": "系統語言",
|
||||
"Security": "安全",
|
||||
"Allow JavaScript": "允許 JavaScript",
|
||||
"Enable only if you trust the file.": "建議僅在您信任該檔案時啟用",
|
||||
"Sort TOC by Page": "依頁碼排序目錄",
|
||||
"Search in {{count}} Book(s)..._other": "在 {{count}} 本書中搜索...",
|
||||
"No notes match your search": "找不到符合的筆記",
|
||||
"Search notes and excerpts...": "搜尋筆記...",
|
||||
"Sign in to Sync": "登入以同步",
|
||||
"Synced at {{time}}": "同步於:{{time}}",
|
||||
"Never synced": "從未同步",
|
||||
"Show Remaining Time": "顯示剩餘時間",
|
||||
"Show Page Number": "顯示頁碼",
|
||||
"{{time}} min left in chapter": "本章剩餘 {{time}} 分鐘",
|
||||
"Override Book Color": "覆蓋書籍顏色",
|
||||
"Login Required": "需登入",
|
||||
"Quota Exceeded": "額度不足",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻譯字數",
|
||||
"Translation Characters": "翻譯字數",
|
||||
"Daily translation quota reached. Select another translate service to proceed.": "已達到每日翻譯字數限制,請選擇其他翻譯服務繼續。",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 種語音",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "出了點問題。別擔心,我們的團隊已經收到通知,正在努力修復。",
|
||||
"Error Details:": "錯誤詳情:",
|
||||
"Try Again": "再試一次",
|
||||
"Need help?": "需要幫助嗎?",
|
||||
"Contact Support": "聯繫支援",
|
||||
"Code Highlighting": "程式碼高亮",
|
||||
"Enable Highlighting": "啟用高亮",
|
||||
"Code Language": "程式語言",
|
||||
"Top Margin (px)": "上邊距",
|
||||
"Bottom Margin (px)": "下邊距",
|
||||
"Right Margin (px)": "右邊距",
|
||||
"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",
|
||||
"Enter CSS for reader interface styling...": "輸入閱讀器介面樣式的CSS…",
|
||||
"Crop": "裁剪",
|
||||
"Book Covers": "書籍封面",
|
||||
"Fit": "適應",
|
||||
"Reset {{settings}}": "重置{{settings}}",
|
||||
"Reset Settings": "重置設置",
|
||||
"{{count}} pages left in chapter_other": "本章剩餘 {{count}} 頁",
|
||||
"Show Remaining Pages": "顯示剩餘頁數"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,130 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.61": {
|
||||
"date": "2025-06-26",
|
||||
"notes": [
|
||||
"Layout: Fixed view menu width on smaller screens",
|
||||
"Layout: Added option to crop or fit book covers in library view",
|
||||
"Reader: You can now apply custom CSS to the reading interface (UI)",
|
||||
"Reader: Added an option to show remaining pages in the current chapter",
|
||||
"Settings: Added options to reset settings from the config dialog",
|
||||
"Import: Fixed encoded filenames when importing TXT files on iOS",
|
||||
"Linux: Fixed missing app icon on some Linux distributions",
|
||||
"GitHub: Added donation link to support Readest development"
|
||||
]
|
||||
},
|
||||
"0.9.60": {
|
||||
"date": "2025-06-22",
|
||||
"notes": [
|
||||
"Layout has been improved for iPad devices",
|
||||
"Screen orientation changes are now smoother on iOS",
|
||||
"Importing books on iOS during the first launch is now more reliable",
|
||||
"Font size no longer changes unexpectedly in scrolled mode on iOS",
|
||||
"Top and bottom safe insets are now ignored when no header or footer is present",
|
||||
"Interactive books now load correctly on the Desktop platform",
|
||||
"Translation is now disabled when the primary language is undefined",
|
||||
"Android: the task list background color has been updated with current theme color"
|
||||
]
|
||||
},
|
||||
"0.9.59": {
|
||||
"date": "2025-06-19",
|
||||
"notes": [
|
||||
"Layout: Image size now adjusts more accurately by converting screen units to pixels properly",
|
||||
"Layout: Improved header layout on iOS landscape mode — extra spacing has been removed",
|
||||
"Settings: Top and bottom margin settings are now more intuitive and better constrained"
|
||||
]
|
||||
},
|
||||
"0.9.58": {
|
||||
"date": "2025-06-18",
|
||||
"notes": [
|
||||
"TTS: Translation now works even when TTS is playing in the background",
|
||||
"TTS: Press T on your keyboard to quickly toggle TTS on or off",
|
||||
"TTS: Each open book now has its own independent TTS controller",
|
||||
"Annotations: Annotation tools now function properly while TTS is active",
|
||||
"Settings: Margins for top, bottom, left, and right can now be adjusted individually",
|
||||
"Settings: New option to always show the status bar while reading",
|
||||
"Layout: Reader page now adapts better to device safe areas for a more comfortable layout",
|
||||
"UI: Added creation timestamps to annotations and made various interface improvements",
|
||||
"iPad: Multiple columns are now supported in portrait mode",
|
||||
"iPad: Fixed issue where sidebars couldn't be resized by dragging",
|
||||
"iPad: Added support for split-screen mode"
|
||||
]
|
||||
},
|
||||
"0.9.57": {
|
||||
"date": "2025-06-14",
|
||||
"notes": ["TTS: Fixed an issue to detect custom TTS engines on some Android devices"]
|
||||
},
|
||||
"0.9.56": {
|
||||
"date": "2025-06-13",
|
||||
"notes": [
|
||||
"Performance: Improved TOC loading for books with very large chapter lists",
|
||||
"TTS: Added support for native Android TTS engines (e.g., MultiTTS)",
|
||||
"TTS: Fixed an issue where the next chapter would not load properly in the background",
|
||||
"Translation: Improved accuracy and clarity of some Arabic translations",
|
||||
"Reading: Added support for code syntax highlighting in books with code snippets"
|
||||
]
|
||||
},
|
||||
"0.9.55": {
|
||||
"date": "2025-06-07",
|
||||
"notes": [
|
||||
"Sync: Notes deleted on other devices now correctly sync across all platforms",
|
||||
"Layout: Now text justification can be unset in the layout settings",
|
||||
"Translation: Added notification when daily translation quota is exceeded",
|
||||
"Performance: Reduced unnecessary animations on Android to improve efficiency"
|
||||
]
|
||||
},
|
||||
"0.9.53": {
|
||||
"date": "2025-06-05",
|
||||
"notes": [
|
||||
"Reader: New option to customize book foreground and background colors",
|
||||
"Reader: Fixed an issue where images appeared too small in some EPUBs",
|
||||
"EPUB: Improved cover image detection for certain EPUB files",
|
||||
"TTS: Improved background playback stability on iOS devices",
|
||||
"Translation: Introduced a daily DeepL translation quota to ensure long-term availability"
|
||||
]
|
||||
},
|
||||
"0.9.52": {
|
||||
"date": "2025-06-03",
|
||||
"notes": [
|
||||
"Library: The search bar now shows the number of books currently displayed",
|
||||
"Book Details: Book descriptions now support rich HTML formatting",
|
||||
"Notebook: Added support for writing notes using Markdown",
|
||||
"Notebook: You can now search within your notes",
|
||||
"Sync: A sync status item has been added to the View menu",
|
||||
"TOC: You can now sort the table of contents by page number",
|
||||
"Reader: Option added to show remaining time in the current chapter"
|
||||
]
|
||||
},
|
||||
"0.9.51": {
|
||||
"date": "2025-06-01",
|
||||
"notes": [
|
||||
"Translator: Table of contents is now included in translations",
|
||||
"Library Theme: Added light, dark, and system theme options to the library page",
|
||||
"TTS: Now shows the current sentence and chapter in the media session",
|
||||
"TTS: Text-to-speech continues from the last read sentence",
|
||||
"Pagination Fix: Taking notes no longer breaks page turning",
|
||||
"EPUB Scripts: Added a setting to allow JavaScript in EPUB files"
|
||||
]
|
||||
},
|
||||
"0.9.50": {
|
||||
"date": "2025-05-28",
|
||||
"notes": [
|
||||
"Bilingual TTS: You can now choose voices for each language in bilingual text-to-speech mode",
|
||||
"Settings Layout: Improved header layout in the settings dialog for different screen sizes",
|
||||
"CSS Fixes: Corrected anchor text color and fixed image dimensions within paragraphs"
|
||||
]
|
||||
},
|
||||
"0.9.49": {
|
||||
"date": "2025-05-27",
|
||||
"notes": [
|
||||
"Select All in Library: You can now select all books at once when in selection mode",
|
||||
"More Translation Options: Added support for Azure and Google Translate services",
|
||||
"Dark Mode Enhancement: New option to invert image colors for better visibility in dark mode",
|
||||
"Bilingual TTS: Enjoy bilingual text-to-speech for books with content in two languages",
|
||||
"Full Book Translation: Translate books in any language into bilingual ebooks",
|
||||
"Privacy Settings: Added a new option to opt-out of telemetry"
|
||||
]
|
||||
},
|
||||
"0.9.43": {
|
||||
"date": "2025-05-19",
|
||||
"notes": [
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "❌ Failed to extract version from package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Found version: $VERSION"
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
|
||||
|
||||
if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then
|
||||
echo "❌ Invalid version format: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert x.y.z => x * 10000 + y * 1000 + z
|
||||
VERSION_CODE=$((10#$MAJOR * 10000 + 10#$MINOR * 1000 + 10#$PATCH))
|
||||
echo "🔢 Computed versionCode: $VERSION_CODE"
|
||||
|
||||
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
|
||||
|
||||
if [[ ! -f "$PROPERTIES_FILE" ]]; then
|
||||
echo "❌ File not found: $PROPERTIES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
sed "s/^tauri\.android\.versionName=.*/tauri.android.versionName=$VERSION/" "$PROPERTIES_FILE" | \
|
||||
sed "s/^tauri\.android\.versionCode=.*/tauri.android.versionCode=$VERSION_CODE/" > "$tmpfile"
|
||||
mv "$tmpfile" "$PROPERTIES_FILE"
|
||||
|
||||
echo "✅ Updated $PROPERTIES_FILE"
|
||||
|
||||
echo "🚀 Running: pnpm tauri android build"
|
||||
pnpm tauri android build
|
||||
@@ -32,7 +32,7 @@ reqwest = { version = "0.12", default-features = false, features = [
|
||||
] }
|
||||
# FIXME: remove the devtools feature in production
|
||||
tauri = { version = "2.5.1", features = [ "protocol-asset", "devtools"] }
|
||||
tauri-build = "2.1.1"
|
||||
tauri-build = "2.3.0"
|
||||
tauri-plugin-log = "2.4.0"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
@@ -46,6 +46,7 @@ tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2.2.4"
|
||||
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>UIHomeIndicatorAutoHidden</key>
|
||||
<true/>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<false/>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -198,4 +198,4 @@
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -54,13 +54,25 @@
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*.deepl.com"
|
||||
"url": "https://*.readest.com"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/readest/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.deepl.com"
|
||||
},
|
||||
{
|
||||
"url": "https://*.cloudflarestorage.com"
|
||||
},
|
||||
{
|
||||
"url": "https://edge.microsoft.com"
|
||||
},
|
||||
{
|
||||
"url": "https://*.microsofttranslator.com"
|
||||
},
|
||||
{
|
||||
"url": "https://translate.googleapis.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,6 +107,7 @@
|
||||
"haptics:allow-notification-feedback",
|
||||
"haptics:allow-selection-feedback",
|
||||
"native-bridge:default",
|
||||
"native-tts:default",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
@@ -10,14 +9,17 @@
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.readest"
|
||||
android:hardwareAccelerated="true"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:hardwareAccelerated="true"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
@@ -40,15 +42,15 @@
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<data android:pathPattern=".*\\.epub" />
|
||||
<data android:pathPattern=".*\\.pdf" />
|
||||
<data android:pathPattern=".*\\.fb2" />
|
||||
<data android:pathPattern=".*\\.fb2.zip" />
|
||||
<data android:pathPattern=".*\\.mobi" />
|
||||
<data android:pathPattern=".*\\.azw" />
|
||||
<data android:pathPattern=".*\\.cbz" />
|
||||
<data android:pathPattern=".*\\.zip" />
|
||||
<data android:pathPattern=".*\\.txt" />
|
||||
<data android:pathPattern="/.*\\.epub" />
|
||||
<data android:pathPattern="/.*\\.pdf" />
|
||||
<data android:pathPattern="/.*\\.fb2" />
|
||||
<data android:pathPattern="/.*\\.fb2.zip" />
|
||||
<data android:pathPattern="/.*\\.mobi" />
|
||||
<data android:pathPattern="/.*\\.azw" />
|
||||
<data android:pathPattern="/.*\\.cbz" />
|
||||
<data android:pathPattern="/.*\\.zip" />
|
||||
<data android:pathPattern="/.*\\.txt" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="oauth">
|
||||
@@ -68,10 +70,9 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="http" />
|
||||
<data android:host="web.readest.com" />
|
||||
|
||||
</intent-filter>
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
</activity>
|
||||
|
||||
+14
@@ -1,10 +1,14 @@
|
||||
package com.bilingify.readest
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import android.view.KeyEvent
|
||||
import android.webkit.WebView
|
||||
import android.util.Log
|
||||
import android.graphics.Color
|
||||
import android.app.ActivityManager
|
||||
import android.content.res.Configuration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -77,5 +81,15 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
setTaskDescription(
|
||||
ActivityManager.TaskDescription(
|
||||
getString(R.string.app_name),
|
||||
null,
|
||||
Color.TRANSPARENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowSplashScreenBackground">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="31">@null</item>
|
||||
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 87 KiB |
+16
-13
@@ -221,6 +221,7 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
if (visible) {
|
||||
controller.show(WindowInsets.Type.statusBars())
|
||||
controller.hide(WindowInsets.Type.navigationBars())
|
||||
} else {
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
}
|
||||
@@ -236,25 +237,27 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
if (visible) {
|
||||
it.show(WindowInsetsCompat.Type.statusBars())
|
||||
it.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
} else {
|
||||
it.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
decorView.systemUiVisibility = when {
|
||||
visible && !isDarkMode -> View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
visible -> View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
else -> View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
}
|
||||
decorView.systemUiVisibility = buildList {
|
||||
add(View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
|
||||
add(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
add(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
||||
add(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
add(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
||||
|
||||
if (!visible) {
|
||||
add(View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
if (visible && !isDarkMode) {
|
||||
add(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
|
||||
}
|
||||
}.reduce { acc, flag -> acc or flag }
|
||||
}
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
|
||||
+107
-40
@@ -154,9 +154,8 @@ class VolumeKeyHandler: NSObject {
|
||||
class NativeBridgePlugin: Plugin {
|
||||
private var webView: WKWebView?
|
||||
private var authSession: ASWebAuthenticationSession?
|
||||
private var isOrientationLocked = false
|
||||
private var currentOrientationMask: UIInterfaceOrientationMask = .all
|
||||
private var orientationObserver: NSObjectProtocol?
|
||||
private var originalDelegate: UIApplicationDelegate?
|
||||
|
||||
@objc public override func load(webview: WKWebView) {
|
||||
self.webView = webview
|
||||
@@ -175,16 +174,19 @@ class NativeBridgePlugin: Plugin {
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
if let app = UIApplication.value(forKey: "sharedApplication") as? UIApplication {
|
||||
self.originalDelegate = app.delegate
|
||||
app.delegate = self
|
||||
} else {
|
||||
Logger.error("NativeBridgePlugin: Failed to get shared application")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func appDidBecomeActive() {
|
||||
if volumeKeyHandler != nil {
|
||||
activateVolumeKeyInterception()
|
||||
}
|
||||
|
||||
if orientationObserver != nil {
|
||||
self.setupOrientationObserver()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func appDidEnterBackground() {
|
||||
@@ -360,18 +362,11 @@ class NativeBridgePlugin: Plugin {
|
||||
let orientation = args.orientation ?? "auto"
|
||||
switch orientation.lowercased() {
|
||||
case "portrait":
|
||||
self.isOrientationLocked = true
|
||||
self.currentOrientationMask = .portrait
|
||||
self.forceOrientation(.portrait)
|
||||
self.setupOrientationObserver()
|
||||
self.changeOrientation(.portrait)
|
||||
case "landscape":
|
||||
self.isOrientationLocked = true
|
||||
self.currentOrientationMask = .landscape
|
||||
self.forceOrientation(.landscapeRight)
|
||||
self.setupOrientationObserver()
|
||||
self.changeOrientation(.landscape)
|
||||
case "auto":
|
||||
self.isOrientationLocked = false
|
||||
self.currentOrientationMask = .all
|
||||
self.changeOrientation(.all)
|
||||
default:
|
||||
invoke.reject("Invalid orientation mode")
|
||||
return
|
||||
@@ -381,40 +376,49 @@ class NativeBridgePlugin: Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
private func forceOrientation(_ orientation: UIInterfaceOrientation) {
|
||||
private func changeOrientation(_ orientationMask: UIInterfaceOrientationMask) {
|
||||
self.currentOrientationMask = orientationMask
|
||||
if #available(iOS 16.0, *) {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
let orientationMask: UIInterfaceOrientationMask =
|
||||
orientation.isPortrait ? .portrait : .landscape
|
||||
|
||||
for window in windowScene.windows {
|
||||
if let rootVC = window.rootViewController {
|
||||
rootVC.setNeedsUpdateOfSupportedInterfaceOrientations()
|
||||
}
|
||||
}
|
||||
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
|
||||
print("Orientation update error: \(error.localizedDescription)")
|
||||
if orientationMask == .all {
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .all)) { error in
|
||||
logger.error("Orientation update error: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
|
||||
logger.error("Orientation update error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupOrientationObserver() {
|
||||
orientationObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.orientationDidChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
guard let self = self, self.isOrientationLocked else { return }
|
||||
|
||||
if self.currentOrientationMask == .portrait {
|
||||
self.forceOrientation(.portrait)
|
||||
} else if self.currentOrientationMask == .landscape {
|
||||
self.forceOrientation(.landscapeRight)
|
||||
if orientationMask == .all {
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
} else {
|
||||
let specificOrientation: UIInterfaceOrientation
|
||||
if orientationMask.contains(.portrait) {
|
||||
specificOrientation = .portrait
|
||||
} else if orientationMask.contains(.landscape) {
|
||||
let currentOrientation = UIDevice.current.orientation
|
||||
if currentOrientation == .landscapeLeft {
|
||||
specificOrientation = .landscapeRight
|
||||
} else if currentOrientation == .landscapeRight {
|
||||
specificOrientation = .landscapeLeft
|
||||
} else {
|
||||
specificOrientation = .landscapeRight
|
||||
}
|
||||
} else {
|
||||
specificOrientation = .portrait
|
||||
}
|
||||
UIDevice.current.setValue(specificOrientation.rawValue, forKey: "orientation")
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,3 +435,66 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding {
|
||||
return UIApplication.shared.windows.first ?? UIWindow()
|
||||
}
|
||||
}
|
||||
|
||||
extension NativeBridgePlugin: UIApplicationDelegate {
|
||||
public func application(
|
||||
_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?
|
||||
) -> UIInterfaceOrientationMask {
|
||||
return self.currentOrientationMask
|
||||
}
|
||||
|
||||
/*
|
||||
Proxy all application delegate methods to the original delegate:
|
||||
sel!(application:didFinishLaunchingWithOptions:),
|
||||
sel!(application:openURL:options:),
|
||||
sel!(application:continue:restorationHandler:),
|
||||
sel!(applicationDidBecomeActive:),
|
||||
sel!(applicationWillResignActive:),
|
||||
sel!(applicationWillEnterForeground:),
|
||||
sel!(applicationDidEnterBackground:),
|
||||
sel!(applicationWillTerminate:),
|
||||
*/
|
||||
|
||||
public func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
self.originalDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
?? false
|
||||
}
|
||||
|
||||
public func application(
|
||||
_ application: UIApplication, open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
self.originalDelegate?.application?(application, open: url, options: options) ?? false
|
||||
}
|
||||
|
||||
public func application(
|
||||
_ application: UIApplication, continue continueUserActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||||
) -> Bool {
|
||||
self.originalDelegate?.application?(
|
||||
application, continue: continueUserActivity, restorationHandler: restorationHandler) ?? false
|
||||
}
|
||||
|
||||
public func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationDidBecomeActive?(application)
|
||||
}
|
||||
|
||||
public func applicationWillResignActive(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationWillResignActive?(application)
|
||||
}
|
||||
|
||||
public func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationWillEnterForeground?(application)
|
||||
}
|
||||
|
||||
public func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationDidEnterBackground?(application)
|
||||
}
|
||||
|
||||
public func applicationWillTerminate(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationWillTerminate?(application)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea/
|
||||
debug.log
|
||||
package-lock.json
|
||||
.vscode/settings.json
|
||||
yarn.lock
|
||||
|
||||
/.tauri
|
||||
/target
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
|
||||
dist-js
|
||||
dist
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "tauri-plugin-native-tts"
|
||||
version = "0.1.0"
|
||||
authors = [ "You" ]
|
||||
description = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||
links = "tauri-plugin-native-tts"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.5.0" }
|
||||
serde = "1.0"
|
||||
thiserror = "2"
|
||||
schemars = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.2.0", features = ["build"] }
|
||||
schemars = "0.8"
|
||||
@@ -0,0 +1 @@
|
||||
# Tauri Plugin native-tts
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.readest.native_tts"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,31 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
resolutionStrategy {
|
||||
eachPlugin {
|
||||
switch (requested.id.id) {
|
||||
case "com.android.library":
|
||||
useVersion("8.0.2")
|
||||
break
|
||||
case "org.jetbrains.kotlin.android":
|
||||
useVersion("1.8.20")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.readest.native_tts
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.readest.native_tts", appContext.packageName)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
package com.readest.native_tts
|
||||
|
||||
import android.os.Bundle
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.speech.tts.Voice
|
||||
import android.util.Log
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.PluginResult
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.*
|
||||
|
||||
data class TTSVoiceData(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val disabled: Boolean = false
|
||||
)
|
||||
|
||||
data class TTSMessageEvent(
|
||||
val code: String, // 'boundary' | 'error' | 'end'
|
||||
val message: String? = null,
|
||||
val mark: String? = null
|
||||
)
|
||||
|
||||
@InvokeArg
|
||||
class SpeakArgs(
|
||||
val text: String? = "",
|
||||
val preload: Boolean? = false
|
||||
)
|
||||
|
||||
@InvokeArg
|
||||
class SetRateArgs(
|
||||
val rate: Float? = 1.0f
|
||||
)
|
||||
|
||||
@InvokeArg
|
||||
class SetPitchArgs(
|
||||
val pitch: Float? = 1.0f
|
||||
)
|
||||
|
||||
@InvokeArg
|
||||
class SetVoiceArgs(
|
||||
val voice: String? = null
|
||||
)
|
||||
|
||||
@TauriPlugin
|
||||
class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NativeTTSPlugin"
|
||||
private const val CHANNEL_NAME = "tts_events"
|
||||
}
|
||||
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
private var isInitialized = AtomicBoolean(false)
|
||||
private var isPaused = AtomicBoolean(false)
|
||||
private var isSpeaking = AtomicBoolean(false)
|
||||
private var currentRate = AtomicReference<Float>(1.0f)
|
||||
private var currentPitch = AtomicReference<Float>(1.0f)
|
||||
|
||||
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
|
||||
private val speakingJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
@Command
|
||||
fun init(invoke: Invoke) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val success = initializeTTS()
|
||||
val result = JSObject().apply {
|
||||
put("success", success)
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize TTS", e)
|
||||
invoke.reject("Failed to initialize TTS: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initializeTTS(): Boolean = suspendCancellableCoroutine { continuation ->
|
||||
try {
|
||||
val preferredEngine = Settings.Secure.getString(
|
||||
activity.contentResolver,
|
||||
Settings.Secure.TTS_DEFAULT_SYNTH
|
||||
)
|
||||
textToSpeech = TextToSpeech(activity, { status ->
|
||||
when (status) {
|
||||
TextToSpeech.SUCCESS -> {
|
||||
setupTTSListener()
|
||||
isInitialized.set(true)
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
continuation.resume(true) {}
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "TTS initialization failed with status: $status")
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
continuation.resume(false) {}
|
||||
}
|
||||
}
|
||||
}, preferredEngine)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception during TTS initialization", e)
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
continuation.resume(false) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTTSListener() {
|
||||
textToSpeech?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(true)
|
||||
sendEvent(id, TTSMessageEvent("boundary", "start"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDone(utteranceId: String?) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(false)
|
||||
sendEvent(id, TTSMessageEvent("end"))
|
||||
closeEventChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("deprecated in API level 21")
|
||||
override fun onError(utteranceId: String?) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(false)
|
||||
sendEvent(id, TTSMessageEvent("error", "TTS playback error"))
|
||||
closeEventChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(utteranceId: String?, errorCode: Int) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(false)
|
||||
sendEvent(id, TTSMessageEvent("error", "TTS playback error:$errorCode"))
|
||||
closeEventChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
|
||||
utteranceId?.let { id ->
|
||||
sendEvent(id, TTSMessageEvent("boundary", "range", "pos:$start-$end"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Command
|
||||
fun speak(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SpeakArgs::class.java)
|
||||
val text = args.text ?: ""
|
||||
|
||||
if (text.isEmpty()) {
|
||||
invoke.reject("Text cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
val utteranceId = UUID.randomUUID().toString()
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val eventChannel = Channel<TTSMessageEvent>(Channel.UNLIMITED)
|
||||
eventChannels[utteranceId] = eventChannel
|
||||
|
||||
val speakJob = launch {
|
||||
speakText(text, utteranceId, args.preload ?: false)
|
||||
}
|
||||
speakingJobs[utteranceId] = speakJob
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("utteranceId", utteranceId)
|
||||
}
|
||||
invoke.resolve(result)
|
||||
|
||||
// Start sending events to the frontend
|
||||
startEventStream(utteranceId)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start speaking", e)
|
||||
invoke.reject("Failed to start speaking: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun speakText(text: String, utteranceId: String, preload: Boolean) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
textToSpeech?.apply {
|
||||
setSpeechRate(currentRate.get())
|
||||
setPitch(currentPitch.get())
|
||||
}
|
||||
|
||||
val params = Bundle().apply {
|
||||
putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId)
|
||||
}
|
||||
|
||||
val result = textToSpeech?.speak(
|
||||
text,
|
||||
if (preload) TextToSpeech.QUEUE_ADD else TextToSpeech.QUEUE_FLUSH,
|
||||
params,
|
||||
utteranceId
|
||||
)
|
||||
|
||||
if (result != TextToSpeech.SUCCESS) {
|
||||
sendEvent(utteranceId, TTSMessageEvent("error", "Failed to start speech"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
sendEvent(utteranceId, TTSMessageEvent("error", "Exception during speech: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startEventStream(utteranceId: String) {
|
||||
coroutineScope.launch {
|
||||
val channel = eventChannels[utteranceId] ?: return@launch
|
||||
try {
|
||||
for (event in channel) {
|
||||
val eventData = JSObject().apply {
|
||||
put("utteranceId", utteranceId)
|
||||
put("code", event.code)
|
||||
event.message?.let { put("message", it) }
|
||||
event.mark?.let { put("mark", it) }
|
||||
}
|
||||
trigger(CHANNEL_NAME, eventData)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in event stream for $utteranceId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEvent(utteranceId: String, event: TTSMessageEvent) {
|
||||
coroutineScope.launch {
|
||||
eventChannels[utteranceId]?.trySend(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeEventChannel(utteranceId: String) {
|
||||
coroutineScope.launch {
|
||||
eventChannels[utteranceId]?.close()
|
||||
eventChannels.remove(utteranceId)
|
||||
speakingJobs[utteranceId]?.cancel()
|
||||
speakingJobs.remove(utteranceId)
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun pause(invoke: Invoke) {
|
||||
try {
|
||||
if (textToSpeech?.stop() == TextToSpeech.SUCCESS) {
|
||||
isPaused.set(true)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to pause TTS")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception while pausing: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun resume(invoke: Invoke) {
|
||||
try {
|
||||
isPaused.set(false)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception while resuming: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun stop(invoke: Invoke) {
|
||||
try {
|
||||
if (textToSpeech?.stop() == TextToSpeech.SUCCESS) {
|
||||
isSpeaking.set(false)
|
||||
isPaused.set(false)
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
eventChannels.values.forEach { it.close() }
|
||||
speakingJobs.clear()
|
||||
eventChannels.clear()
|
||||
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to stop TTS")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception while stopping: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_rate(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetRateArgs::class.java)
|
||||
try {
|
||||
currentRate.set(args.rate)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting rate: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_pitch(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetPitchArgs::class.java)
|
||||
try {
|
||||
currentPitch.set(args.pitch)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting pitch: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_voice(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetVoiceArgs::class.java)
|
||||
try {
|
||||
val voices = textToSpeech?.voices
|
||||
val targetVoice = voices?.find { it.name == args.voice }
|
||||
|
||||
if (targetVoice != null) {
|
||||
val result = textToSpeech?.setVoice(targetVoice)
|
||||
if (result == TextToSpeech.SUCCESS) {
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to set voice: ${args.voice}")
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Voice not found: ${args.voice}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting voice: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_all_voices(invoke: Invoke) {
|
||||
try {
|
||||
val voices = textToSpeech?.voices?.map { voice ->
|
||||
JSObject().apply {
|
||||
put("id", voice.name)
|
||||
put("name", voice.name)
|
||||
put("lang", voice.locale.toLanguageTag())
|
||||
put("disabled", false)
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("voices", JSONArray(voices))
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting voices: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
eventChannels.values.forEach { it.close() }
|
||||
eventChannels.clear()
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
speakingJobs.clear()
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.readest.native-tts
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
const COMMANDS: &[&str] = &[
|
||||
"init",
|
||||
"speak",
|
||||
"stop",
|
||||
"pause",
|
||||
"resume",
|
||||
"set_rate",
|
||||
"set_pitch",
|
||||
"set_voice",
|
||||
"get_all_voices",
|
||||
"registerListener",
|
||||
"remove_listener",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
@@ -0,0 +1,32 @@
|
||||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-native-tts",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-native-tts",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-native-tts"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-native-tts",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin native-tts
|
||||
|
||||
A description of this package.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
class PingArgs: Decodable {
|
||||
let value: String?
|
||||
}
|
||||
|
||||
class NativeTTSPlugin: Plugin {
|
||||
@objc public func ping(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(PingArgs.self)
|
||||
invoke.resolve(["value": args.value ?? ""])
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_native_tts")
|
||||
func initPlugin() -> Plugin {
|
||||
return NativeTTSPlugin()
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-all-voices"
|
||||
description = "Enables the get_all_voices command without any pre-configured scope."
|
||||
commands.allow = ["get_all_voices"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-all-voices"
|
||||
description = "Denies the get_all_voices command without any pre-configured scope."
|
||||
commands.deny = ["get_all_voices"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-init"
|
||||
description = "Enables the init command without any pre-configured scope."
|
||||
commands.allow = ["init"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-init"
|
||||
description = "Denies the init command without any pre-configured scope."
|
||||
commands.deny = ["init"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-pause"
|
||||
description = "Enables the pause command without any pre-configured scope."
|
||||
commands.allow = ["pause"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-pause"
|
||||
description = "Denies the pause command without any pre-configured scope."
|
||||
commands.deny = ["pause"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-registerListener"
|
||||
description = "Enables the registerListener command without any pre-configured scope."
|
||||
commands.allow = ["registerListener"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-registerListener"
|
||||
description = "Denies the registerListener command without any pre-configured scope."
|
||||
commands.deny = ["registerListener"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-remove-listener"
|
||||
description = "Enables the remove_listener command without any pre-configured scope."
|
||||
commands.allow = ["remove_listener"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-remove-listener"
|
||||
description = "Denies the remove_listener command without any pre-configured scope."
|
||||
commands.deny = ["remove_listener"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-resume"
|
||||
description = "Enables the resume command without any pre-configured scope."
|
||||
commands.allow = ["resume"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-resume"
|
||||
description = "Denies the resume command without any pre-configured scope."
|
||||
commands.deny = ["resume"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-pitch"
|
||||
description = "Enables the set_pitch command without any pre-configured scope."
|
||||
commands.allow = ["set_pitch"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-pitch"
|
||||
description = "Denies the set_pitch command without any pre-configured scope."
|
||||
commands.deny = ["set_pitch"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-rate"
|
||||
description = "Enables the set_rate command without any pre-configured scope."
|
||||
commands.allow = ["set_rate"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-rate"
|
||||
description = "Denies the set_rate command without any pre-configured scope."
|
||||
commands.deny = ["set_rate"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-voice"
|
||||
description = "Enables the set_voice command without any pre-configured scope."
|
||||
commands.allow = ["set_voice"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-voice"
|
||||
description = "Denies the set_voice command without any pre-configured scope."
|
||||
commands.deny = ["set_voice"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-speak"
|
||||
description = "Enables the speak command without any pre-configured scope."
|
||||
commands.allow = ["speak"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-speak"
|
||||
description = "Denies the speak command without any pre-configured scope."
|
||||
commands.deny = ["speak"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-stop"
|
||||
description = "Enables the stop command without any pre-configured scope."
|
||||
commands.allow = ["stop"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-stop"
|
||||
description = "Denies the stop command without any pre-configured scope."
|
||||
commands.deny = ["stop"]
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
## Default Permission
|
||||
|
||||
Default permissions for the plugin
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-init`
|
||||
- `allow-speak`
|
||||
- `allow-stop`
|
||||
- `allow-pause`
|
||||
- `allow-resume`
|
||||
- `allow-set-rate`
|
||||
- `allow-set-pitch`
|
||||
- `allow-set-voice`
|
||||
- `allow-get-all-voices`
|
||||
- `allow-registerListener`
|
||||
- `allow-remove-listener`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-all-voices`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_all_voices command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-get-all-voices`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_all_voices command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-pause`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the pause command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-pause`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the pause command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-registerListener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the registerListener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-registerListener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the registerListener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-remove-listener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the remove_listener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-remove-listener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the remove_listener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-resume`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the resume command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-resume`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the resume command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-pitch`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_pitch command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-pitch`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_pitch command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-rate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_rate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-rate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_rate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-voice`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_voice command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-voice`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_voice command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-speak`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the speak command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-speak`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the speak command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-stop`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the stop command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-stop`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the stop command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,15 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = [
|
||||
"allow-init",
|
||||
"allow-speak",
|
||||
"allow-stop",
|
||||
"allow-pause",
|
||||
"allow-resume",
|
||||
"allow-set-rate",
|
||||
"allow-set-pitch",
|
||||
"allow-set-voice",
|
||||
"allow-get-all-voices",
|
||||
"allow-registerListener",
|
||||
"allow-remove-listener",
|
||||
]
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the get_all_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-all-voices",
|
||||
"markdownDescription": "Enables the get_all_voices command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_all_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-all-voices",
|
||||
"markdownDescription": "Denies the get_all_voices command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-init",
|
||||
"markdownDescription": "Enables the init command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-init",
|
||||
"markdownDescription": "Denies the init command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the pause command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-pause",
|
||||
"markdownDescription": "Enables the pause command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the pause command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-pause",
|
||||
"markdownDescription": "Denies the pause command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the registerListener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-registerListener",
|
||||
"markdownDescription": "Enables the registerListener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the registerListener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-registerListener",
|
||||
"markdownDescription": "Denies the registerListener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the resume command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-resume",
|
||||
"markdownDescription": "Enables the resume command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the resume command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-resume",
|
||||
"markdownDescription": "Denies the resume command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_pitch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-pitch",
|
||||
"markdownDescription": "Enables the set_pitch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_pitch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-pitch",
|
||||
"markdownDescription": "Denies the set_pitch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_rate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-rate",
|
||||
"markdownDescription": "Enables the set_rate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_rate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-rate",
|
||||
"markdownDescription": "Denies the set_rate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_voice command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-voice",
|
||||
"markdownDescription": "Enables the set_voice command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_voice command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-voice",
|
||||
"markdownDescription": "Denies the set_voice command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the speak command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-speak",
|
||||
"markdownDescription": "Enables the speak command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the speak command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-speak",
|
||||
"markdownDescription": "Denies the speak command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the stop command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-stop",
|
||||
"markdownDescription": "Enables the stop command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the stop command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-stop",
|
||||
"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`",
|
||||
"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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use tauri::{command, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
use crate::NativeTtsExt;
|
||||
use crate::Result;
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn init<R: Runtime>(app: AppHandle<R>) -> Result<InitResponse> {
|
||||
app.native_tts().init()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn speak<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: SpeakArgs,
|
||||
) -> Result<SpeakResponse> {
|
||||
app.native_tts().speak(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn pause<R: Runtime>(app: AppHandle<R>) -> Result<()> {
|
||||
app.native_tts().pause()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn resume<R: Runtime>(app: AppHandle<R>) -> Result<()> {
|
||||
app.native_tts().resume()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn stop<R: Runtime>(app: AppHandle<R>) -> Result<()> {
|
||||
app.native_tts().stop()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_rate<R: Runtime>(app: AppHandle<R>, payload: SetRateArgs) -> Result<()> {
|
||||
app.native_tts().set_rate(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_pitch<R: Runtime>(app: AppHandle<R>, payload: SetPitchArgs) -> Result<()> {
|
||||
app.native_tts().set_pitch(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_voice<R: Runtime>(app: AppHandle<R>, payload: SetVoiceArgs) -> Result<()> {
|
||||
app.native_tts().set_voice(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_all_voices<R: Runtime>(app: AppHandle<R>) -> Result<GetVoicesResponse> {
|
||||
app.native_tts().get_all_voices()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<NativeTts<R>> {
|
||||
Ok(NativeTts(app.clone()))
|
||||
}
|
||||
|
||||
/// Access to the native-tts APIs.
|
||||
pub struct NativeTts<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn init(&self) -> crate::Result<InitResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn speak(&self, _args: SpeakArgs) -> crate::Result<SpeakResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn pause(&self) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn resume(&self) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn stop(&self) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_rate(&self, _args: SetRateArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_pitch(&self, _args: SetPitchArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_voice(&self, _args: SetVoiceArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn get_all_voices(&self) -> crate::Result<GetVoicesResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unsupported platform for this plugin")]
|
||||
UnsupportedPlatformError,
|
||||
#[error("Native tts error: {0}")]
|
||||
NativeTTSError(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::NativeTts;
|
||||
#[cfg(mobile)]
|
||||
use mobile::NativeTts;
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the native-tts APIs.
|
||||
pub trait NativeTtsExt<R: Runtime> {
|
||||
fn native_tts(&self) -> &NativeTts<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::NativeTtsExt<R> for T {
|
||||
fn native_tts(&self) -> &NativeTts<R> {
|
||||
self.state::<NativeTts<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("native-tts")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::init,
|
||||
commands::speak,
|
||||
commands::stop,
|
||||
commands::pause,
|
||||
commands::resume,
|
||||
commands::set_rate,
|
||||
commands::set_pitch,
|
||||
commands::set_voice,
|
||||
commands::get_all_voices,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let native_tts = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let native_tts = desktop::init(app, api)?;
|
||||
app.manage(native_tts);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_native_tts);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<NativeTts<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin("com.readest.native_tts", "NativeTTSPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_native_tts)?;
|
||||
Ok(NativeTts(handle))
|
||||
}
|
||||
|
||||
/// Access to the native-tts APIs.
|
||||
pub struct NativeTts<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn init(&self) -> crate::Result<InitResponse> {
|
||||
self.0.run_mobile_plugin("init", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn speak(&self, payload: SpeakArgs) -> crate::Result<SpeakResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("speak", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn pause(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("pause", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn resume(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("resume", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn stop(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("stop", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_rate(&self, payload: SetRateArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_rate", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_pitch(&self, payload: SetPitchArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_pitch", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_voice(&self, payload: SetVoiceArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_voice", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_all_voices(&self) -> crate::Result<GetVoicesResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_all_voices", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TTSVoice {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub lang: String,
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TTSMessageEvent {
|
||||
pub code: String, // 'boundary' | 'error' | 'end'
|
||||
pub message: Option<String>,
|
||||
pub mark: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpeakArgs {
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub preload: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpeakResponse {
|
||||
pub utterance_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetRateArgs {
|
||||
pub rate: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetPitchArgs {
|
||||
pub pitch: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetVoiceArgs {
|
||||
pub voice: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetVoicesResponse {
|
||||
pub voices: Vec<TTSVoice>,
|
||||
}
|
||||
@@ -134,6 +134,7 @@ pub fn run() {
|
||||
.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());
|
||||
|
||||
#[cfg(desktop)]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://*.microsofttranslator.com https://edge.microsoft.com",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://fontsapi.zeoseven.com",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://fontsapi.zeoseven.com",
|
||||
@@ -151,7 +151,10 @@
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
|
||||
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
|
||||
"endpoints": [
|
||||
"https://download.readest.com/releases/latest.json",
|
||||
"https://github.com/readest/readest/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
|
||||
export default function AuthCallback() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const hash = window.location.hash || '';
|
||||
const params = new URLSearchParams(hash.slice(1));
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash || '';
|
||||
const params = new URLSearchParams(hash.slice(1));
|
||||
|
||||
const accessToken = params.get('access_token');
|
||||
const refreshToken = params.get('refresh_token');
|
||||
const type = params.get('type');
|
||||
const next = params.get('next') ?? '/';
|
||||
const error = params.get('error');
|
||||
const errorDescription = params.get('error_description');
|
||||
const errorCode = params.get('error_code');
|
||||
const accessToken = params.get('access_token');
|
||||
const refreshToken = params.get('refresh_token');
|
||||
const type = params.get('type');
|
||||
const next = params.get('next') ?? '/';
|
||||
const error = params.get('error');
|
||||
const errorDescription = params.get('error_description');
|
||||
const errorCode = params.get('error_code');
|
||||
|
||||
handleAuthCallback({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
type,
|
||||
next,
|
||||
error,
|
||||
errorCode,
|
||||
errorDescription,
|
||||
login,
|
||||
navigate: router.push,
|
||||
});
|
||||
handleAuthCallback({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
type,
|
||||
next,
|
||||
error,
|
||||
errorCode,
|
||||
errorDescription,
|
||||
login,
|
||||
navigate: router.push,
|
||||
});
|
||||
}, [login, router]);
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
<span className='loading loading-infinity loading-xl w-20'></span>
|
||||
<span className='loading loading-infinity loading-xl w-20' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,14 +18,13 @@ import { useThemeStore } from '@/store/themeStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { getBaseUrl, isTauriAppPlatform } from '@/services/environment';
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
|
||||
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
|
||||
import { READEST_WEB_BASE_URL } from '@/services/constants';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
|
||||
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
|
||||
@@ -42,7 +41,7 @@ interface ProviderLoginProp {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
|
||||
const WEB_AUTH_CALLBACK = `${getBaseUrl()}/auth/callback`;
|
||||
const DEEPLINK_CALLBACK = 'readest://auth-callback';
|
||||
const USE_APPLE_SIGN_IN = process.env['NEXT_PUBLIC_USE_APPLE_SIGN_IN'] === 'true';
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export default function Error({ error, reset }: ErrorPageProps) {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const [browserInfo, setBrowserInfo] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
posthog.captureException(error);
|
||||
}, [appService, error]);
|
||||
|
||||
const handleGoHome = () => {
|
||||
window.location.href = '/library';
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
handleGoHome();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='hero bg-base-200 min-h-screen'>
|
||||
<div className='hero-content text-center'>
|
||||
<div className='w-full max-w-2xl p-1'>
|
||||
<div className='mb-8 mt-6'>
|
||||
<div className='text-error animate-pulse text-8xl'>⚠️</div>
|
||||
</div>
|
||||
|
||||
<h1 className='text-base-content mb-4 text-5xl font-bold'>Oops!</h1>
|
||||
|
||||
<p className='text-base-content/70 mb-8 text-lg'>
|
||||
{_(
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.",
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className='alert alert-error mb-8'>
|
||||
<div className='flex-col items-start text-left'>
|
||||
<h3 className='mb-2 font-bold'>{_('Error Details:')}</h3>
|
||||
<p className='break-words font-mono text-sm'>{error.message}</p>
|
||||
{browserInfo && <p className='mt-2 font-mono text-sm'>Browser: {browserInfo}</p>}
|
||||
{error.stack && (
|
||||
<p className='mt-2 whitespace-pre-wrap break-all font-mono text-sm'>
|
||||
{error.stack.split('\n').slice(0, 3).join('\n')}
|
||||
</p>
|
||||
)}
|
||||
{error.digest && <p className='mt-2 text-xs opacity-70'>Error ID: {error.digest}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<button onClick={reset} className='btn btn-primary btn-lg'>
|
||||
<svg className='mr-2 h-5 w-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15'
|
||||
/>
|
||||
</svg>
|
||||
{_('Try Again')}
|
||||
</button>
|
||||
|
||||
<div className='flex gap-3'>
|
||||
<button onClick={handleGoBack} className='btn btn-outline flex-1'>
|
||||
<svg className='mr-2 h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M10 19l-7-7m0 0l7-7m-7 7h18'
|
||||
/>
|
||||
</svg>
|
||||
{_('Go Back')}
|
||||
</button>
|
||||
|
||||
<button onClick={handleGoHome} className='btn btn-outline flex-1'>
|
||||
<svg className='mr-2 h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'
|
||||
/>
|
||||
</svg>
|
||||
{_('Your Library')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-base-300 mt-8 border-t pt-6'>
|
||||
<p className='text-base-content/60 text-sm'>
|
||||
{_('Need help?')}{' '}
|
||||
<a href='mailto:support@readest.com' className='link link-primary'>
|
||||
{_('Contact Support')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,15 +11,16 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLogin } from '@/utils/nav';
|
||||
import { formatAuthors } from '@/utils/book';
|
||||
import ReadingProgress from './ReadingProgress';
|
||||
import BookCover from '@/components/BookCover';
|
||||
|
||||
interface BookItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
book: Book;
|
||||
mode: LibraryViewModeType;
|
||||
coverFit: LibraryCoverFitType;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
transferProgress: number | null;
|
||||
@@ -29,8 +30,9 @@ interface BookItemProps {
|
||||
}
|
||||
|
||||
const BookItem: React.FC<BookItemProps> = ({
|
||||
mode,
|
||||
book,
|
||||
mode,
|
||||
coverFit,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
transferProgress,
|
||||
@@ -59,11 +61,11 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
|
||||
'relative flex aspect-[28/41] items-center justify-center',
|
||||
mode === 'list' && 'min-w-20',
|
||||
)}
|
||||
>
|
||||
<BookCover mode={mode} book={book} />
|
||||
<BookCover mode={mode} book={book} coverFit={coverFit} className='drop-shadow-md' />
|
||||
{selectedBooks.includes(book.hash) && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
@@ -102,6 +104,10 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</div>
|
||||
<div
|
||||
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
|
||||
style={{
|
||||
height: `${iconSize15}px`,
|
||||
minHeight: `${iconSize15}px`,
|
||||
}}
|
||||
>
|
||||
{book.progress && <ReadingProgress book={book} />}
|
||||
<div className='flex items-center justify-center gap-x-2'>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-ico
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
@@ -17,12 +17,15 @@ 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 GroupingModal from './GroupingModal';
|
||||
|
||||
interface BookshelfProps {
|
||||
libraryBooks: Book[];
|
||||
isSelectMode: boolean;
|
||||
isSelectAll: boolean;
|
||||
isSelectNone: boolean;
|
||||
handleImportBooks: () => void;
|
||||
handleBookUpload: (book: Book) => Promise<boolean>;
|
||||
handleBookDownload: (book: Book) => Promise<boolean>;
|
||||
@@ -35,6 +38,8 @@ interface BookshelfProps {
|
||||
const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
libraryBooks,
|
||||
isSelectMode,
|
||||
isSelectAll,
|
||||
isSelectNone,
|
||||
handleImportBooks,
|
||||
handleBookUpload,
|
||||
handleBookDownload,
|
||||
@@ -61,21 +66,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const [sortOrder, setSortOrder] = useState(
|
||||
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
|
||||
);
|
||||
const [coverFit, setCoverFit] = useState(searchParams?.get('cover') || settings.libraryCoverFit);
|
||||
const isImportingBook = useRef(false);
|
||||
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const allBookshelfItems =
|
||||
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectMode) {
|
||||
setShowSelectModeActions(true);
|
||||
} else {
|
||||
setSelectedBooks([]);
|
||||
setShowSelectModeActions(false);
|
||||
}
|
||||
}, [isSelectMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isImportingBook.current) return;
|
||||
isImportingBook.current = true;
|
||||
@@ -95,12 +92,22 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importBookUrl, appService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (navBooksGroup) {
|
||||
setCurrentBookshelf(navBooksGroup.books);
|
||||
} else {
|
||||
setCurrentBookshelf(allBookshelfItems);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [libraryBooks, navBooksGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
const group = searchParams?.get('group') || '';
|
||||
const query = searchParams?.get('q') || '';
|
||||
const view = searchParams?.get('view') || settings.libraryViewMode;
|
||||
const sort = searchParams?.get('sort') || settings.librarySortBy;
|
||||
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
|
||||
const cover = searchParams?.get('cover') || settings.libraryCoverFit;
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
if (query) {
|
||||
params.set('q', query);
|
||||
@@ -127,6 +134,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
} else {
|
||||
params.delete('view');
|
||||
}
|
||||
setCoverFit(cover);
|
||||
if (cover === 'crop') {
|
||||
params.delete('cover');
|
||||
}
|
||||
if (sort === 'updated' && order === 'desc' && view === 'grid') {
|
||||
params.delete('sort');
|
||||
params.delete('order');
|
||||
@@ -169,14 +180,22 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
const getBooksToDelete = () => {
|
||||
const booksToDelete: Book[] = [];
|
||||
selectedBooks.forEach((id) => {
|
||||
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
|
||||
if (book && !book.deletedAt) {
|
||||
handleBookDelete(book);
|
||||
booksToDelete.push(book);
|
||||
}
|
||||
}
|
||||
});
|
||||
return booksToDelete;
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
for (const book of getBooksToDelete()) {
|
||||
handleBookDelete(book);
|
||||
}
|
||||
setSelectedBooks([]);
|
||||
setShowDeleteAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
@@ -192,7 +211,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
setShowGroupingModal(true);
|
||||
};
|
||||
|
||||
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
|
||||
const bookFilter = (item: Book, queryTerm: string) => {
|
||||
if (item.deletedAt) return false;
|
||||
const searchTerm = new RegExp(queryTerm, 'i');
|
||||
@@ -222,6 +240,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}
|
||||
};
|
||||
const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1;
|
||||
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
|
||||
const filteredBookshelfItems = currentBookshelfItems
|
||||
.filter((item) => {
|
||||
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
|
||||
@@ -245,6 +264,23 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectMode) {
|
||||
setShowSelectModeActions(true);
|
||||
if (isSelectAll) {
|
||||
setSelectedBooks(
|
||||
filteredBookshelfItems.map((item) => ('hash' in item ? item.hash : item.id)),
|
||||
);
|
||||
} else if (isSelectNone) {
|
||||
setSelectedBooks([]);
|
||||
}
|
||||
} else {
|
||||
setSelectedBooks([]);
|
||||
setShowSelectModeActions(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSelectMode, isSelectAll, isSelectNone]);
|
||||
|
||||
return (
|
||||
<div className='bookshelf'>
|
||||
<div
|
||||
@@ -257,9 +293,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
>
|
||||
{filteredBookshelfItems.map((item, index) => (
|
||||
<BookshelfItem
|
||||
key={`library-item-${index}`}
|
||||
mode={viewMode as LibraryViewModeType}
|
||||
key={`library-item-${'hash' in item ? item.hash : item.id}-${index}`}
|
||||
item={item}
|
||||
mode={viewMode as LibraryViewModeType}
|
||||
coverFit={coverFit as LibraryCoverFitType}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
setLoading={setLoading}
|
||||
@@ -354,7 +391,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
)}
|
||||
</div>
|
||||
{showGroupingModal && (
|
||||
<div>
|
||||
<ModalPortal>
|
||||
<GroupingModal
|
||||
libraryBooks={libraryBooks}
|
||||
selectedBooks={selectedBooks}
|
||||
@@ -367,7 +404,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleSetSelectMode(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{showDeleteAlert && (
|
||||
<div
|
||||
@@ -378,7 +415,9 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
>
|
||||
<Alert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete the selected books?')}
|
||||
message={_('Are you sure to delete {{count}} selected book(s)?', {
|
||||
count: getBooksToDelete().length,
|
||||
})}
|
||||
onCancel={() => {
|
||||
setShowDeleteAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Menu, MenuItem } from '@tauri-apps/api/menu';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getLocalBookFilename } from '@/utils/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import { Book, BookGroupType, BooksGroup } from '@/types/book';
|
||||
@@ -73,6 +73,7 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
|
||||
interface BookshelfItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
item: BookshelfItem;
|
||||
coverFit: LibraryCoverFitType;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
transferProgress: number | null;
|
||||
@@ -88,6 +89,7 @@ interface BookshelfItemProps {
|
||||
const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
mode,
|
||||
item,
|
||||
coverFit,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
transferProgress,
|
||||
@@ -114,6 +116,9 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
|
||||
const makeBookAvailable = async (book: Book) => {
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
if (await appService?.isBookAvailable(book)) {
|
||||
return true;
|
||||
}
|
||||
let available = false;
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 200);
|
||||
try {
|
||||
@@ -273,6 +278,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
<BookItem
|
||||
mode={mode}
|
||||
book={item}
|
||||
coverFit={coverFit}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
transferProgress={transferProgress}
|
||||
|
||||
@@ -140,7 +140,7 @@ const GroupingModal: React.FC<GroupingModalProps> = ({
|
||||
}, [selectedBooks]);
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'>
|
||||
<div className='fixed inset-0 flex items-center justify-center'>
|
||||
<div
|
||||
className={clsx(
|
||||
'modal-box bg-base-100 overflow-y-auto rounded-2xl shadow-xl',
|
||||
|
||||
@@ -11,33 +11,42 @@ import { IoMdCloseCircle } from 'react-icons/io';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SettingsMenu from './SettingsMenu';
|
||||
import ImportMenu from './ImportMenu';
|
||||
import SortMenu from './SortMenu';
|
||||
import ViewMenu from './ViewMenu';
|
||||
|
||||
interface LibraryHeaderProps {
|
||||
isSelectMode: boolean;
|
||||
isSelectAll: boolean;
|
||||
onImportBooks: () => void;
|
||||
onToggleSelectMode: () => void;
|
||||
onSelectAll: () => void;
|
||||
onDeselectAll: () => void;
|
||||
}
|
||||
|
||||
const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
isSelectMode,
|
||||
isSelectAll,
|
||||
onImportBooks,
|
||||
onToggleSelectMode,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { appService } = useEnv();
|
||||
const { statusBarHeight } = useThemeStore();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
const { currentBookshelf } = useLibraryStore();
|
||||
const {
|
||||
isTrafficLightVisible,
|
||||
initializeTrafficLightStore,
|
||||
@@ -50,14 +59,15 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const iconSize18 = useResponsiveSize(18);
|
||||
const iconSize20 = useResponsiveSize(20);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
useShortcuts({
|
||||
onToggleSelectMode,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const throttledUpdateQueryParam = useCallback(
|
||||
throttle((value: string) => {
|
||||
const debouncedUpdateQueryParam = useCallback(
|
||||
debounce((value: string) => {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
if (value) {
|
||||
params.set('q', value);
|
||||
@@ -65,14 +75,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
params.delete('q');
|
||||
}
|
||||
router.push(`?${params.toString()}`);
|
||||
}, 1000),
|
||||
}, 500),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newQuery = e.target.value;
|
||||
setSearchQuery(newQuery);
|
||||
throttledUpdateQueryParam(newQuery);
|
||||
debouncedUpdateQueryParam(newQuery);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,6 +99,12 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
|
||||
const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible;
|
||||
const isInGroupView = !!searchParams?.get('group');
|
||||
const currentBooksCount = currentBookshelf.reduce(
|
||||
(acc, item) => acc + ('books' in item ? item.books.length : 1),
|
||||
0,
|
||||
);
|
||||
|
||||
if (!insets) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -99,8 +115,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
)}
|
||||
style={{
|
||||
marginTop: appService?.hasSafeAreaInset
|
||||
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
|
||||
: '',
|
||||
? `max(${insets.top}px, ${systemUIVisible ? statusBarHeight : 0}px)`
|
||||
: '0px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
|
||||
@@ -124,7 +140,13 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<input
|
||||
type='text'
|
||||
value={searchQuery}
|
||||
placeholder={_('Search Books...')}
|
||||
placeholder={
|
||||
currentBooksCount > 1
|
||||
? _('Search in {{count}} Book(s)...', {
|
||||
count: currentBooksCount,
|
||||
})
|
||||
: _('Search Books...')
|
||||
}
|
||||
onChange={handleSearchChange}
|
||||
spellCheck='false'
|
||||
className={clsx(
|
||||
@@ -140,7 +162,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
type='button'
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
throttledUpdateQueryParam('');
|
||||
debouncedUpdateQueryParam('');
|
||||
}}
|
||||
className='pe-1 text-gray-400 hover:text-gray-600'
|
||||
aria-label={_('Clear Search')}
|
||||
@@ -182,14 +204,21 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{appService?.isMobile && isSelectMode ? (
|
||||
<div className='w-max-[72px] w-min-[72px] flex h-full items-center'>
|
||||
{isSelectMode ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-full items-center',
|
||||
'w-max-[72px] w-min-[72px] sm:w-max-[80px] sm:w-min-[80px]',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={onToggleSelectMode}
|
||||
className='btn btn-ghost text-base-content/85 h-8 min-h-8 w-[72px] p-0'
|
||||
aria-label={_('Cancel Selection')}
|
||||
onClick={isSelectAll ? onDeselectAll : onSelectAll}
|
||||
className='btn btn-ghost text-base-content/85 h-8 min-h-8 w-[72px] p-0 sm:w-[80px]'
|
||||
aria-label={isSelectAll ? _('Deselect') : _('Select All')}
|
||||
>
|
||||
{_('Cancel')}
|
||||
<span className='font-sans text-base font-normal sm:text-sm'>
|
||||
{isSelectAll ? _('Deselect') : _('Select All')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -199,7 +228,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
|
||||
>
|
||||
<SortMenu />
|
||||
<ViewMenu />
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { TbSunMoon } from 'react-icons/tb';
|
||||
import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useQuotaStats } from '@/hooks/useQuotaStats';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { getStoragePlanData } from '@/utils/access';
|
||||
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
|
||||
import { QuotaType } from '@/types/user';
|
||||
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Quota from '@/components/Quota';
|
||||
@@ -29,23 +32,28 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { token, user } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { themeMode, setThemeMode } = useThemeStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
|
||||
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
|
||||
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
|
||||
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
|
||||
const [isAlwaysShowStatusBar, setIsAlwaysShowStatusBar] = useState(settings.alwaysShowStatusBar);
|
||||
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
|
||||
const [isOpenLastBooks, setIsOpenLastBooks] = useState(settings.openLastBooks);
|
||||
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
|
||||
settings.autoImportBooksOnOpen,
|
||||
);
|
||||
const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled);
|
||||
const iconSize = useResponsiveSize(16);
|
||||
|
||||
const { quotas } = useQuotaStats();
|
||||
|
||||
const showAboutReadest = () => {
|
||||
setAboutDialogVisible(true);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const downloadReadest = () => {
|
||||
window.open(DOWNLOAD_READEST_URL, '_blank');
|
||||
setIsDropdownOpen?.(false);
|
||||
@@ -61,6 +69,11 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const cycleThemeMode = () => {
|
||||
const nextMode = themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
|
||||
setThemeMode(nextMode);
|
||||
};
|
||||
|
||||
const handleReloadPage = () => {
|
||||
window.location.reload();
|
||||
setIsDropdownOpen?.(false);
|
||||
@@ -80,6 +93,14 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const toggleAlwaysShowStatusBar = () => {
|
||||
settings.alwaysShowStatusBar = !settings.alwaysShowStatusBar;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsAlwaysShowStatusBar(settings.alwaysShowStatusBar);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const toggleAutoUploadBooks = () => {
|
||||
settings.autoUpload = !settings.autoUpload;
|
||||
setSettings(settings);
|
||||
@@ -119,21 +140,17 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsOpenLastBooks(settings.openLastBooks);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
const storagPlan = getStoragePlanData(token);
|
||||
const storageQuota: QuotaType = {
|
||||
name: _('Storage'),
|
||||
tooltip: _('{{percentage}}% of Cloud Storage Used.', {
|
||||
percentage: Math.round((storagPlan.usage / storagPlan.quota) * 100),
|
||||
}),
|
||||
used: Math.round(storagPlan.usage / 1024 / 1024),
|
||||
total: Math.round(storagPlan.quota / 1024 / 1024),
|
||||
unit: 'MB',
|
||||
};
|
||||
setQuotas([storageQuota]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
const toggleTelemetry = () => {
|
||||
settings.telemetryEnabled = !settings.telemetryEnabled;
|
||||
if (settings.telemetryEnabled) {
|
||||
optInTelemetry();
|
||||
} else {
|
||||
optOutTelemetry();
|
||||
}
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsTelemetryEnabled(settings.telemetryEnabled);
|
||||
};
|
||||
|
||||
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
|
||||
const userFullName = user?.user_metadata?.['full_name'];
|
||||
@@ -164,7 +181,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
<Quota quotas={quotas} className='h-10 pl-3 pr-2' />
|
||||
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
|
||||
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
@@ -206,15 +223,39 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
onClick={toggleAlwaysOnTop}
|
||||
/>
|
||||
)}
|
||||
{appService?.isMobileApp && (
|
||||
<MenuItem
|
||||
label={_('Always Show Status Bar')}
|
||||
Icon={isAlwaysShowStatusBar ? MdCheck : undefined}
|
||||
onClick={toggleAlwaysShowStatusBar}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Keep Screen Awake')}
|
||||
Icon={isScreenWakeLock ? MdCheck : undefined}
|
||||
onClick={toggleScreenWakeLock}
|
||||
/>
|
||||
<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}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
|
||||
<MenuItem
|
||||
label={_('Help improve Readest')}
|
||||
description={isTelemetryEnabled ? _('Sharing anonymized statistics') : ''}
|
||||
Icon={isTelemetryEnabled ? MdCheck : undefined}
|
||||
onClick={toggleTelemetry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+37
-4
@@ -4,15 +4,15 @@ import { MdCheck } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
|
||||
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
interface SortMenuProps {
|
||||
interface ViewMenuProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -22,12 +22,18 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const viewMode = settings.libraryViewMode;
|
||||
const sortBy = settings.librarySortBy;
|
||||
const isAscending = settings.librarySortAscending;
|
||||
const coverFit = settings.libraryCoverFit;
|
||||
|
||||
const viewOptions = [
|
||||
{ label: _('List'), value: 'list' },
|
||||
{ label: _('Grid'), value: 'grid' },
|
||||
];
|
||||
|
||||
const coverFitOptions = [
|
||||
{ label: _('Crop'), value: 'crop' },
|
||||
{ label: _('Fit'), value: 'fit' },
|
||||
];
|
||||
|
||||
const sortByOptions = [
|
||||
{ label: _('Title'), value: 'title' },
|
||||
{ label: _('Author'), value: 'author' },
|
||||
@@ -52,6 +58,17 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleToggleCropCovers = (value: LibraryCoverFitType) => {
|
||||
settings.libraryCoverFit = value;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsDropdownOpen?.(false);
|
||||
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('cover', value);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleSetSortBy = (value: LibrarySortByType) => {
|
||||
settings.librarySortBy = value;
|
||||
setSettings(settings);
|
||||
@@ -89,6 +106,22 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem
|
||||
label={_('Book Covers')}
|
||||
buttonClass='h-8'
|
||||
labelClass='text-sm sm:text-xs'
|
||||
disabled
|
||||
/>
|
||||
{coverFitOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={coverFit === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem
|
||||
label={_('Sort by...')}
|
||||
buttonClass='h-8'
|
||||
@@ -118,4 +151,4 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SortMenu;
|
||||
export default ViewMenu;
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSync } from '@/hooks/useSync';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { SYNC_BOOKS_INTERVAL_SEC } from '@/services/constants';
|
||||
import { Book } from '@/types/book';
|
||||
import { SYNC_BOOKS_INTERVAL_SEC } from '@/services/constants';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
|
||||
export interface UseBooksSyncProps {
|
||||
onSyncStart?: () => void;
|
||||
@@ -38,9 +39,6 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const lastSyncTime = useRef<number>(0);
|
||||
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const getNewBooks = () => {
|
||||
if (!user) return [];
|
||||
const newBooks = library.filter(
|
||||
@@ -49,26 +47,18 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
|
||||
return newBooks;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const now = Date.now();
|
||||
const timeSinceLastSync = now - lastSyncTime.current;
|
||||
if (timeSinceLastSync > SYNC_BOOKS_INTERVAL_SEC * 1000) {
|
||||
lastSyncTime.current = now;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleAutoSync = useCallback(
|
||||
debounce(() => {
|
||||
const newBooks = getNewBooks();
|
||||
syncBooks(newBooks, 'both');
|
||||
} else {
|
||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
|
||||
syncTimeoutRef.current = setTimeout(
|
||||
() => {
|
||||
lastSyncTime.current = Date.now();
|
||||
const newBooks = getNewBooks();
|
||||
syncBooks(newBooks, 'both');
|
||||
syncTimeoutRef.current = null;
|
||||
},
|
||||
SYNC_BOOKS_INTERVAL_SEC * 1000 - timeSinceLastSync,
|
||||
);
|
||||
}
|
||||
}, SYNC_BOOKS_INTERVAL_SEC * 1000),
|
||||
[library, lastSyncedAtBooks],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
handleAutoSync();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [library]);
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ import { usePullToRefresh } from '@/hooks/usePullToRefresh';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useDemoBooks } from './hooks/useDemoBooks';
|
||||
import { useBooksSync } from './hooks/useBooksSync';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { mountAdditionalFonts } from '@/utils/style';
|
||||
import { mountAdditionalFonts } from '@/utils/font';
|
||||
import {
|
||||
tauriHandleSetAlwaysOnTop,
|
||||
tauriHandleToggleFullScreen,
|
||||
@@ -69,12 +69,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
} = useLibraryStore();
|
||||
const _ = useTranslation();
|
||||
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
|
||||
const insets = useSafeAreaInsets();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const { statusBarHeight } = useThemeStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
const [isSelectAll, setIsSelectAll] = useState(false);
|
||||
const [isSelectNone, setIsSelectNone] = useState(false);
|
||||
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
|
||||
const [booksTransferProgress, setBooksTransferProgress] = useState<{
|
||||
[key: string]: number | null;
|
||||
@@ -216,6 +218,13 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pageRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!libraryBooks.some((book) => !book.deletedAt)) {
|
||||
handleSetSelectMode(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [libraryBooks]);
|
||||
|
||||
const processOpenWithFiles = React.useCallback(
|
||||
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
|
||||
const settings = await appService.loadSettings();
|
||||
@@ -360,10 +369,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
['Failed to parse EPUB.', _('Failed to parse the EPUB file.')],
|
||||
['Unsupported format.', _('This book format is not supported.')],
|
||||
];
|
||||
const { library } = useLibraryStore.getState();
|
||||
for (const file of files) {
|
||||
try {
|
||||
const book = await appService?.importBook(file, libraryBooks);
|
||||
setLibrary(libraryBooks);
|
||||
const book = await appService?.importBook(file, library);
|
||||
setLibrary([...library]);
|
||||
if (user && book && !book.uploadedAt && settings.autoUpload) {
|
||||
console.log('Uploading book:', book.title);
|
||||
handleBookUpload(book);
|
||||
@@ -386,13 +396,19 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
console.error('Failed to import book:', filename, error);
|
||||
}
|
||||
}
|
||||
appService?.saveLibraryBooks(libraryBooks);
|
||||
appService?.saveLibraryBooks(library);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const selectFilesTauri = async () => {
|
||||
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const exts = appService?.isMobileApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
|
||||
if (appService?.isIOSApp) {
|
||||
return files.filter((file) => {
|
||||
const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown';
|
||||
return SUPPORTED_FILE_EXTS.includes(fileExt);
|
||||
});
|
||||
}
|
||||
// Cannot filter out files on Android since some content providers may not return the file name
|
||||
return files;
|
||||
};
|
||||
@@ -515,36 +531,37 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
let files;
|
||||
|
||||
if (isTauriAppPlatform()) {
|
||||
if (appService?.isIOSApp) {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
} else {
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
}
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
} else {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
}
|
||||
importBooks(files);
|
||||
};
|
||||
|
||||
const handleToggleSelectMode = () => {
|
||||
if (!isSelectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
setIsSelectMode((pre) => !pre);
|
||||
};
|
||||
|
||||
const handleSetSelectMode = (selectMode: boolean) => {
|
||||
if (selectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
setIsSelectMode(selectMode);
|
||||
setIsSelectAll(false);
|
||||
setIsSelectNone(false);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setIsSelectAll(true);
|
||||
setIsSelectNone(false);
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
setIsSelectNone(true);
|
||||
setIsSelectAll(false);
|
||||
};
|
||||
|
||||
const handleShowDetailsBook = (book: Book) => {
|
||||
setShowDetailsBook(book);
|
||||
};
|
||||
|
||||
if (!appService) {
|
||||
if (!appService || !insets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -567,11 +584,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
<div className='fixed top-0 z-40 w-full'>
|
||||
<div className='top-0 z-40 w-full'>
|
||||
<LibraryHeader
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
onImportBooks={handleImportBooks}
|
||||
onToggleSelectMode={handleToggleSelectMode}
|
||||
onToggleSelectMode={() => handleSetSelectMode(!isSelectMode)}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeselectAll={handleDeselectAll}
|
||||
/>
|
||||
</div>
|
||||
{loading && (
|
||||
@@ -585,20 +605,21 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'scroll-container drop-zone flex-grow overflow-y-auto',
|
||||
appService?.hasSafeAreaInset && 'pt-[52px]',
|
||||
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
|
||||
isDragging && 'drag-over',
|
||||
)}
|
||||
style={{
|
||||
marginTop: appService?.hasSafeAreaInset
|
||||
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
|
||||
: '48px',
|
||||
paddingTop: '0px',
|
||||
paddingRight: `${insets.right}px`,
|
||||
paddingBottom: `${insets.bottom}px`,
|
||||
paddingLeft: `${insets.left}px`,
|
||||
}}
|
||||
>
|
||||
<DropIndicator />
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
isSelectNone={isSelectNone}
|
||||
handleImportBooks={handleImportBooks}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
|
||||
@@ -4,21 +4,22 @@ import React, { useEffect } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
import { getGridTemplate, getInsetEdges } from '@/utils/grid';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FooterBar from './FooterBar';
|
||||
import PageInfoView from './PageInfo';
|
||||
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';
|
||||
import DoubleBorder from './DoubleBorder';
|
||||
import TTSControl from './tts/TTSControl';
|
||||
|
||||
interface BooksGridProps {
|
||||
bookKeys: string[];
|
||||
@@ -28,10 +29,14 @@ interface BooksGridProps {
|
||||
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings, hoveredBookKey } = useReaderStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { getGridInsets, setGridInsets, hoveredBookKey } = useReaderStore();
|
||||
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
|
||||
const screenInsets = useSafeAreaInsets();
|
||||
const aspectRatio = window.innerWidth / window.innerHeight;
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, aspectRatio);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
@@ -41,6 +46,23 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!screenInsets) return;
|
||||
bookKeys.forEach((bookKey, index) => {
|
||||
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
|
||||
const gridInsets = {
|
||||
top: top ? screenInsets.top : 0,
|
||||
right: right ? screenInsets.right : 0,
|
||||
bottom: bottom ? screenInsets.bottom : 0,
|
||||
left: left ? screenInsets.left : 0,
|
||||
};
|
||||
setGridInsets(bookKey, gridInsets);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKeys, screenInsets]);
|
||||
|
||||
if (!screenInsets) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('relative grid h-full flex-grow')}
|
||||
@@ -54,13 +76,20 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const config = getConfig(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const gridInsets = getGridInsets(bookKey);
|
||||
const { book, bookDoc } = bookData || {};
|
||||
if (!book || !config || !bookDoc || !viewSettings) return null;
|
||||
if (!book || !config || !bookDoc || !viewSettings || !gridInsets) return null;
|
||||
|
||||
const { section, pageinfo, sectionLabel } = progress || {};
|
||||
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
|
||||
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
|
||||
const horizontalGapPercent = viewSettings.gapPercent;
|
||||
const verticalMarginPixels = viewSettings.marginPx;
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
const contentInsets = {
|
||||
top: gridInsets.top + viewInsets.top,
|
||||
right: gridInsets.right + viewInsets.right,
|
||||
bottom: gridInsets.bottom + viewInsets.bottom,
|
||||
left: gridInsets.left + viewInsets.left,
|
||||
};
|
||||
const scrolled = viewSettings.scrolled;
|
||||
const showBarsOnScroll = viewSettings.showBarsOnScroll;
|
||||
const showHeader = viewSettings.showHeader && (scrolled ? showBarsOnScroll : true);
|
||||
@@ -72,10 +101,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
key={bookKey}
|
||||
className={clsx(
|
||||
'relative h-full w-full overflow-hidden',
|
||||
index === 0 &&
|
||||
!viewSettings?.scrolled &&
|
||||
appService?.hasSafeAreaInset &&
|
||||
'pt-[env(safe-area-inset-top)]',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
@@ -87,16 +112,22 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
isHoveredAnim={bookKeys.length > 2}
|
||||
onCloseBook={onCloseBook}
|
||||
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
<FoliateViewer
|
||||
bookKey={bookKey}
|
||||
bookDoc={bookDoc}
|
||||
config={config}
|
||||
contentInsets={contentInsets}
|
||||
/>
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
|
||||
{viewSettings.vertical && viewSettings.scrolled && (
|
||||
<>
|
||||
{(showFooter || viewSettings.doubleBorder) && (
|
||||
<div
|
||||
className='bg-base-100 absolute left-0 top-0 h-full'
|
||||
style={{
|
||||
width: `calc(${horizontalGapPercent}%)`,
|
||||
height: `calc(100% - ${verticalMarginPixels}px)`,
|
||||
width: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px)`,
|
||||
height: `calc(100%)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -104,8 +135,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<div
|
||||
className='bg-base-100 absolute right-0 top-0 h-full'
|
||||
style={{
|
||||
width: `calc(${horizontalGapPercent}%)`,
|
||||
height: `calc(100% - ${verticalMarginPixels}px)`,
|
||||
width: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px)`,
|
||||
height: `calc(100%)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -117,7 +148,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
showFooter={showFooter}
|
||||
borderColor={viewSettings.borderColor}
|
||||
horizontalGap={horizontalGapPercent}
|
||||
verticalMargin={verticalMarginPixels}
|
||||
contentInsets={contentInsets}
|
||||
/>
|
||||
)}
|
||||
{showHeader && (
|
||||
@@ -127,7 +158,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
isScrolled={viewSettings.scrolled}
|
||||
isVertical={viewSettings.vertical}
|
||||
horizontalGap={horizontalGapPercent}
|
||||
verticalMargin={verticalMarginPixels}
|
||||
contentInsets={contentInsets}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
)}
|
||||
<HintInfo
|
||||
@@ -136,18 +168,19 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
isScrolled={viewSettings.scrolled}
|
||||
isVertical={viewSettings.vertical}
|
||||
horizontalGap={horizontalGapPercent}
|
||||
verticalMargin={verticalMarginPixels}
|
||||
contentInsets={contentInsets}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
{showFooter && (
|
||||
<PageInfoView
|
||||
<ProgressInfoView
|
||||
bookKey={bookKey}
|
||||
bookFormat={book.format}
|
||||
section={section}
|
||||
pageinfo={pageinfo}
|
||||
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
|
||||
isScrolled={viewSettings.scrolled}
|
||||
isVertical={viewSettings.vertical}
|
||||
timeinfo={timeinfo}
|
||||
horizontalGap={horizontalGapPercent}
|
||||
verticalMargin={verticalMarginPixels}
|
||||
contentInsets={contentInsets}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
)}
|
||||
<Annotator bookKey={bookKey} />
|
||||
@@ -158,12 +191,12 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
section={section}
|
||||
pageinfo={pageinfo}
|
||||
isHoveredAnim={false}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<TTSControl />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { Insets } from '@/types/misc';
|
||||
|
||||
interface DoubleBorderProps {
|
||||
borderColor: string;
|
||||
horizontalGap: number;
|
||||
verticalMargin: number;
|
||||
showHeader: boolean;
|
||||
showFooter: boolean;
|
||||
contentInsets: Insets;
|
||||
}
|
||||
|
||||
const paddingPx = 10;
|
||||
|
||||
const DoubleBorder: React.FC<DoubleBorderProps> = ({
|
||||
borderColor,
|
||||
horizontalGap,
|
||||
verticalMargin,
|
||||
showHeader,
|
||||
showFooter,
|
||||
contentInsets,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{/* outter frame */}
|
||||
<div
|
||||
className={'borderframe pointer-events-none absolute'}
|
||||
style={{
|
||||
border: `4px solid ${borderColor}`,
|
||||
height: `calc(100% - ${verticalMargin * 2}px + ${paddingPx * 2}px)`,
|
||||
top: `calc(${verticalMargin}px - ${paddingPx}px)`,
|
||||
left: `calc(${horizontalGap}% - ${showFooter ? 32 : 0}px - ${paddingPx}px)`,
|
||||
right: `calc(${horizontalGap}% - ${showHeader ? 32 : 0}px - ${paddingPx}px)`,
|
||||
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px + ${paddingPx * 2}px)`,
|
||||
top: `calc(${contentInsets.top}px - ${paddingPx}px)`,
|
||||
left: `calc(${contentInsets.left}px - ${paddingPx}px)`,
|
||||
right: `calc(${contentInsets.right}px - ${paddingPx}px)`,
|
||||
}}
|
||||
></div>
|
||||
{/* inner frame */}
|
||||
@@ -33,10 +34,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
|
||||
className={'borderframe pointer-events-none absolute'}
|
||||
style={{
|
||||
border: `1px solid ${borderColor}`,
|
||||
height: `calc(100% - ${verticalMargin * 2}px)`,
|
||||
top: `${verticalMargin}px`,
|
||||
left: showFooter ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
|
||||
right: showHeader ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
|
||||
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
|
||||
top: `${contentInsets.top}px`,
|
||||
left: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px`,
|
||||
right: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px`,
|
||||
}}
|
||||
/>
|
||||
{/* footer */}
|
||||
@@ -48,9 +49,9 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
|
||||
borderBottom: `1px solid ${borderColor}`,
|
||||
borderLeft: `1px solid ${borderColor}`,
|
||||
width: '32px',
|
||||
height: `calc(100% - ${verticalMargin * 2}px)`,
|
||||
top: `${verticalMargin}px`,
|
||||
left: `calc(${horizontalGap}% - 32px)`,
|
||||
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
|
||||
top: `${contentInsets.top}px`,
|
||||
left: `calc(${contentInsets.left}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -63,13 +64,13 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
|
||||
borderBottom: `1px solid ${borderColor}`,
|
||||
borderRight: `1px solid ${borderColor}`,
|
||||
width: '32px',
|
||||
height: `calc(100% - ${verticalMargin * 2}px)`,
|
||||
top: `${verticalMargin}px`,
|
||||
left: `calc(100% - ${horizontalGap}%)`,
|
||||
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
|
||||
top: `${contentInsets.top}px`,
|
||||
left: `calc(100% - ${contentInsets.right}px - 32px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,16 +2,19 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { BookDoc, getDirection } from '@/libs/document';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { FoliateView, wrappedFoliateView } from '@/types/view';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
import { useMouseEvent, useTouchEvent } from '../hooks/useIframeEvents';
|
||||
import { usePagination } from '../hooks/usePagination';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { useProgressSync } from '../hooks/useProgressSync';
|
||||
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
|
||||
import { getStyles, mountAdditionalFonts, transformStylesheet } from '@/utils/style';
|
||||
import { getStyles, transformStylesheet } from '@/utils/style';
|
||||
import { mountAdditionalFonts } from '@/utils/font';
|
||||
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
|
||||
import { useUICSS } from '@/hooks/useUICSS';
|
||||
import {
|
||||
@@ -26,24 +29,39 @@ import {
|
||||
} from '../utils/iframeEventHandlers';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { isCJKLang } from '@/utils/lang';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
eval(script: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
const FoliateViewer: React.FC<{
|
||||
bookKey: string;
|
||||
bookDoc: BookDoc;
|
||||
config: BookConfig;
|
||||
}> = ({ bookKey, bookDoc, config }) => {
|
||||
const { appService } = useEnv();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<FoliateView | null>(null);
|
||||
const isViewCreated = useRef(false);
|
||||
contentInsets: Insets;
|
||||
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
|
||||
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { appService } = useEnv();
|
||||
const { themeCode, isDarkMode } = useThemeStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
const viewRef = useRef<FoliateView | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isViewCreated = useRef(false);
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setToastMessage(''), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
@@ -52,10 +70,19 @@ const FoliateViewer: React.FC<{
|
||||
useUICSS(bookKey);
|
||||
useProgressSync(bookKey);
|
||||
useProgressAutoSave(bookKey);
|
||||
useTextTranslation(bookKey, viewRef.current);
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range);
|
||||
setProgress(
|
||||
bookKey,
|
||||
detail.cfi,
|
||||
detail.tocItem,
|
||||
detail.section,
|
||||
detail.location,
|
||||
detail.time,
|
||||
detail.range,
|
||||
);
|
||||
};
|
||||
|
||||
const getDocTransformHandler = ({ width, height }: { width: number; height: number }) => {
|
||||
@@ -65,7 +92,7 @@ const FoliateViewer: React.FC<{
|
||||
.then((data) => {
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
if (viewSettings && detail.type === 'text/css')
|
||||
return transformStylesheet(viewSettings, width, height, data);
|
||||
return transformStylesheet(width, height, data);
|
||||
if (viewSettings && detail.type === 'application/xhtml+xml') {
|
||||
const ctx = {
|
||||
bookKey,
|
||||
@@ -90,6 +117,7 @@ const FoliateViewer: React.FC<{
|
||||
if (detail.doc) {
|
||||
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
viewSettings.vertical =
|
||||
writingDir?.vertical || viewSettings.writingMode.includes('vertical') || false;
|
||||
viewSettings.rtl =
|
||||
@@ -99,7 +127,17 @@ const FoliateViewer: React.FC<{
|
||||
false;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
|
||||
mountAdditionalFonts(detail.doc);
|
||||
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
|
||||
|
||||
// Inline scripts in tauri platforms are not executed by default
|
||||
if (viewSettings.allowScript && isTauriAppPlatform()) {
|
||||
evalInlineScripts(detail.doc);
|
||||
}
|
||||
|
||||
// only call on load if we have highlighting turned on.
|
||||
if (viewSettings.codeHighlighting) {
|
||||
manageSyntaxHighlighting(detail.doc, viewSettings);
|
||||
}
|
||||
|
||||
if (!detail.doc.isEventListenersAdded) {
|
||||
// listened events in iframes are posted to the main window
|
||||
@@ -118,6 +156,22 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const evalInlineScripts = (doc: Document) => {
|
||||
if (doc.defaultView && doc.defaultView.frameElement) {
|
||||
const iframe = doc.defaultView.frameElement as HTMLIFrameElement;
|
||||
const scripts = doc.querySelectorAll('script:not([src])');
|
||||
scripts.forEach((script, index) => {
|
||||
const scriptContent = script.textContent || script.innerHTML;
|
||||
try {
|
||||
console.warn('Evaluating inline scripts in iframe');
|
||||
iframe.contentWindow?.eval(scriptContent);
|
||||
} catch (error) {
|
||||
console.error(`Error executing iframe script ${index + 1}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const docRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
|
||||
@@ -145,14 +199,6 @@ const FoliateViewer: React.FC<{
|
||||
onRendererRelocate: docRelocateHandler,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current && viewRef.current.renderer) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [themeCode, isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isViewCreated.current) return;
|
||||
isViewCreated.current = true;
|
||||
@@ -165,10 +211,6 @@ const FoliateViewer: React.FC<{
|
||||
document.body.append(view);
|
||||
containerRef.current?.appendChild(view);
|
||||
|
||||
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||
const width = containerRect?.width || window.innerWidth;
|
||||
const height = containerRect?.height || window.innerHeight;
|
||||
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const writingMode = viewSettings.writingMode;
|
||||
if (writingMode) {
|
||||
@@ -188,15 +230,19 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const { book } = view;
|
||||
|
||||
book.transformTarget?.addEventListener('load', (event: Event) => {
|
||||
const { detail } = event as CustomEvent;
|
||||
if (detail.isScript) {
|
||||
detail.allowScript = viewSettings.allowScript ?? false;
|
||||
}
|
||||
});
|
||||
const viewWidth = appService?.isMobile ? screen.width : window.innerWidth;
|
||||
const viewHeight = appService?.isMobile ? screen.height : window.innerHeight;
|
||||
const width = viewWidth - insets.left - insets.right;
|
||||
const height = viewHeight - insets.top - insets.bottom;
|
||||
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
|
||||
view.renderer.setStyles?.(getStyles(viewSettings));
|
||||
|
||||
const isScrolled = viewSettings.scrolled!;
|
||||
const showHeader = viewSettings.showHeader!;
|
||||
const showFooter = viewSettings.showFooter!;
|
||||
const isCompact = !showHeader && !showFooter;
|
||||
const marginPx = isCompact ? viewSettings.compactMarginPx : viewSettings.marginPx;
|
||||
const gapPercent = isCompact ? viewSettings.compactGapPercent : viewSettings.gapPercent;
|
||||
const animated = viewSettings.animated!;
|
||||
const maxColumnCount = viewSettings.maxColumnCount!;
|
||||
const maxInlineSize = getMaxInlineSize(viewSettings);
|
||||
@@ -210,12 +256,10 @@ const FoliateViewer: React.FC<{
|
||||
} else {
|
||||
view.renderer.removeAttribute('animated');
|
||||
}
|
||||
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
|
||||
view.renderer.setAttribute('margin', `${marginPx}px`);
|
||||
view.renderer.setAttribute('gap', `${gapPercent}%`);
|
||||
view.renderer.setAttribute('max-column-count', maxColumnCount);
|
||||
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
|
||||
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
|
||||
applyMarginAndGap();
|
||||
|
||||
const lastLocation = config.location;
|
||||
if (lastLocation) {
|
||||
@@ -229,15 +273,63 @@ const FoliateViewer: React.FC<{
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const applyMarginAndGap = () => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
|
||||
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
|
||||
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
|
||||
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
|
||||
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
|
||||
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
|
||||
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
|
||||
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
|
||||
const topMargin = (showTopHeader ? insets.top : viewInsets.top) + moreTopInset;
|
||||
const rightMargin = insets.right + moreRightInset;
|
||||
const bottomMargin = (showBottomFooter ? insets.bottom : viewInsets.bottom) + moreBottomInset;
|
||||
const leftMargin = insets.left + moreLeftInset;
|
||||
|
||||
viewRef.current?.renderer.setAttribute('margin-top', `${topMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-right', `${rightMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-bottom', `${bottomMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-left', `${leftMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('gap', `${viewSettings.gapPercent}%`);
|
||||
if (viewSettings.scrolled) {
|
||||
viewRef.current?.renderer.setAttribute('flow', 'scrolled');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current && viewRef.current.renderer) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [themeCode, isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current && viewRef.current.renderer && viewSettings) {
|
||||
applyMarginAndGap();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
insets.top,
|
||||
insets.right,
|
||||
insets.bottom,
|
||||
insets.left,
|
||||
viewSettings?.doubleBorder,
|
||||
viewSettings?.showHeader,
|
||||
viewSettings?.showFooter,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='foliate-viewer h-[100%] w-[100%]'
|
||||
{...mouseHandlers}
|
||||
{...touchHandlers}
|
||||
/>
|
||||
</>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='foliate-viewer h-[100%] w-[100%]'
|
||||
{...mouseHandlers}
|
||||
{...touchHandlers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ 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';
|
||||
import Slider from '@/components/Slider';
|
||||
import TTSControl from './tts/TTSControl';
|
||||
|
||||
interface FooterBarProps {
|
||||
bookKey: string;
|
||||
@@ -30,6 +32,7 @@ interface FooterBarProps {
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
isHoveredAnim: boolean;
|
||||
gridInsets: Insets;
|
||||
}
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({
|
||||
@@ -38,11 +41,12 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
section,
|
||||
pageinfo,
|
||||
isHoveredAnim,
|
||||
gridInsets,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { hoveredBookKey, setHoveredBookKey, getView, getProgress, getViewSettings } =
|
||||
useReaderStore();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const [actionTab, setActionTab] = React.useState('');
|
||||
const sliderHeight = useResponsiveSize(28);
|
||||
@@ -53,6 +57,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
const view = getView(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
|
||||
const handleProgressChange = (value: number) => {
|
||||
view?.goToFraction(value / 100.0);
|
||||
@@ -63,9 +68,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
};
|
||||
|
||||
const handleMarginChange = (value: number) => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const marginPx = Math.round((value / 100) * 88);
|
||||
const gapPercent = Math.round((value / 100) * 10);
|
||||
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx, false, false);
|
||||
viewSettings.marginTopPx = marginPx;
|
||||
viewSettings.marginBottomPx = marginPx / 2;
|
||||
viewSettings.marginLeftPx = marginPx / 2;
|
||||
viewSettings.marginRightPx = marginPx / 2;
|
||||
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent, false, false);
|
||||
view?.renderer.setAttribute('margin', `${marginPx}px`);
|
||||
view?.renderer.setAttribute('gap', `${gapPercent}%`);
|
||||
@@ -107,12 +116,11 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
};
|
||||
|
||||
const handleSpeakText = async () => {
|
||||
if (!view || !progress) return;
|
||||
const { range } = progress;
|
||||
if (eventDispatcher.dispatchSync('tts-is-speaking')) {
|
||||
if (!view || !progress || !viewState) return;
|
||||
if (viewState.ttsEnabled) {
|
||||
eventDispatcher.dispatch('tts-stop', { bookKey });
|
||||
} else {
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range });
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,6 +155,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
};
|
||||
|
||||
const isVisible = hoveredBookKey === bookKey;
|
||||
const ttsEnabled = viewState?.ttsEnabled;
|
||||
const progressInfo = bookFormat === 'PDF' ? section : pageinfo;
|
||||
const progressValid = !!progressInfo;
|
||||
const progressFraction =
|
||||
@@ -154,6 +163,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
? (progressInfo!.current + 1) / progressInfo!.total || 0
|
||||
: 0;
|
||||
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -193,9 +204,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset
|
||||
? 'calc(env(safe-area-inset-bottom) + 64px)'
|
||||
: '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
@@ -249,9 +258,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset
|
||||
? 'calc(env(safe-area-inset-bottom) + 64px)'
|
||||
: '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
@@ -268,7 +275,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
initialValue={getMarginProgressValue(
|
||||
viewSettings?.marginPx ?? 44,
|
||||
viewSettings?.marginTopPx ?? 44,
|
||||
viewSettings?.gapPercent ?? 5,
|
||||
)}
|
||||
bubbleElement={<TbBoxMargin size={marginIconSize} />}
|
||||
@@ -291,8 +298,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom)+16px)]',
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom + 16}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<TOCIcon size={tocIconSize} className='' />}
|
||||
@@ -312,10 +321,18 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
}
|
||||
onClick={() => handleSetActionTab('font')}
|
||||
/>
|
||||
<Button icon={<TTSIcon className='' />} onClick={() => handleSetActionTab('tts')} />
|
||||
<Button
|
||||
icon={<TTSIcon className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={() => handleSetActionTab('tts')}
|
||||
/>
|
||||
</div>
|
||||
{/* Desktop footer bar */}
|
||||
<div className='hidden w-full items-center gap-x-4 px-4 sm:flex'>
|
||||
{/* Desktop / Pad footer bar */}
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom / 2}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
@@ -351,7 +368,11 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
handleProgressChange(parseInt((e.target as HTMLInputElement).value, 10))
|
||||
}
|
||||
/>
|
||||
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
|
||||
<Button
|
||||
icon={<FaHeadphones className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={handleSpeakText}
|
||||
tooltip={_('Speak')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
@@ -364,6 +385,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TTSControl bookKey={bookKey} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,15 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { getFootnoteStyles, getStyles, getThemeCode, mountAdditionalFonts } from '@/utils/style';
|
||||
import { getFootnoteStyles, getStyles, getThemeCode } from '@/utils/style';
|
||||
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
|
||||
import { FootnoteHandler } from 'foliate-js/footnotes.js';
|
||||
import { mountAdditionalFonts } from '@/utils/font';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { FootnoteHandler } from 'foliate-js/footnotes.js';
|
||||
import { isCJKLang } from '@/utils/lang';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
interface FootnotePopupProps {
|
||||
@@ -26,6 +29,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const [popupPosition, setPopupPosition] = useState<Position | null>();
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
@@ -55,13 +59,17 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
});
|
||||
view.addEventListener('load', (e: CustomEvent) => {
|
||||
const { doc } = e.detail;
|
||||
mountAdditionalFonts(doc);
|
||||
const bookData = getBookData(bookKey)!;
|
||||
mountAdditionalFonts(doc, isCJKLang(bookData.book?.primaryLanguage));
|
||||
});
|
||||
footnoteViewRef.current = view;
|
||||
footnoteRef.current?.replaceChildren(view);
|
||||
const { renderer } = view;
|
||||
renderer.setAttribute('flow', 'scrolled');
|
||||
renderer.setAttribute('margin', '0px');
|
||||
renderer.setAttribute('margin-top', '0px');
|
||||
renderer.setAttribute('margin-right', '0px');
|
||||
renderer.setAttribute('margin-bottom', '0px');
|
||||
renderer.setAttribute('margin-left', '0px');
|
||||
renderer.setAttribute('gap', '0%');
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const themeCode = getThemeCode();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user