forked from akai/readest
Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| def157cacb | |||
| 7c464b9a8d | |||
| 90f3512ae2 | |||
| 12d93ba749 | |||
| 554d786105 | |||
| eb71c59524 | |||
| 93e81ee05b | |||
| b83972e366 | |||
| 967536aa23 | |||
| 4523437e34 | |||
| e49dd4accb | |||
| b7c1f5703c | |||
| 5373b67aae | |||
| ddc9673260 | |||
| 16b0821eab | |||
| 93876a73cc | |||
| 048867e595 | |||
| 602be376a1 | |||
| c52ae43cd9 | |||
| 348d743e82 | |||
| c7e495c379 | |||
| e368076cac | |||
| 382e0ca3c6 | |||
| d96fb32bcf | |||
| 4779e2638f | |||
| d094ba9916 | |||
| e4ba0c302e | |||
| 0150177a9d | |||
| 74e32d47b0 | |||
| 2dd7cf54e6 | |||
| a5cbfe97db | |||
| da49526f4c | |||
| 2e46bc1627 | |||
| 7e542e8694 | |||
| 2a624ef57a | |||
| 60ae0c9ac3 | |||
| 110b4b37cd | |||
| f0edbf14a9 | |||
| 20669a9cb3 | |||
| 3756bec66c | |||
| 3e1d03dc17 | |||
| 4aebbf679f | |||
| 0a562d90ac | |||
| e91a39e8b6 | |||
| 2006baac4b | |||
| 0d4f61f08a | |||
| 0b5f0ede3b | |||
| 99a146525f | |||
| 6080f9e0f3 | |||
| 6b3d41a4b8 | |||
| e403c009c2 | |||
| 14bf2bcf8f | |||
| 7f66af71c8 | |||
| cbab201138 | |||
| 0f47d6d393 | |||
| 38101cb76d | |||
| 8f0982ae33 | |||
| 1c015f5973 | |||
| 4c1af671bb | |||
| b4cda00a91 | |||
| c326ad402d | |||
| e03636e0a1 | |||
| 9669f1272d | |||
| 4094fd86ff | |||
| 27df89e261 | |||
| ddcee6b3df | |||
| 5e04f6ae03 | |||
| 6299ea09b7 | |||
| 4f6f45fe8a | |||
| 6b290f09f5 | |||
| c9e08f2c01 | |||
| 8167f27156 | |||
| 77c7176526 | |||
| a8d657414d | |||
| ed368e1205 | |||
| 7de1bdf824 | |||
| 141c502c76 | |||
| b1ea729a54 | |||
| d3eff19ab9 | |||
| 59f0cede39 | |||
| 946d17882b | |||
| f9adf789d7 | |||
| efb8f70d01 | |||
| e235cb98d2 | |||
| 4275508ccd | |||
| a424ae8b15 | |||
| 9303ec8c5f | |||
| 98faed320a | |||
| 5bbdc70420 | |||
| de21182240 | |||
| 4ae35992a1 | |||
| b91d7b7a61 | |||
| 7c0d9bf375 | |||
| ea8ab91ed3 | |||
| efd415ece2 | |||
| ec86d68b80 | |||
| 172ab382bc | |||
| ad551a04a1 | |||
| 4dc943d23e | |||
| ee93885c04 | |||
| 14c032aaff | |||
| 2f619ca745 | |||
| 8979827cf5 | |||
| ae16eeffa7 | |||
| 8e96ef3e5d | |||
| 38e24da968 | |||
| 7371efe1d1 | |||
| 3cb37bca5d | |||
| b9fa204eda | |||
| cf66665096 | |||
| 759779a98d | |||
| 68409db8a0 | |||
| 46f0e8e7f6 | |||
| 6260168caa | |||
| a8430a7be1 | |||
| 7bfaa38746 | |||
| c5b2feda48 | |||
| 59f875f590 | |||
| 8efad90932 | |||
| 5c23642ac0 | |||
| ee6500cecb | |||
| f004d7b3ea | |||
| f7d53508a6 | |||
| 53e7f2526c | |||
| 8f0e2d1642 | |||
| c807f0ccc8 | |||
| 6ba15319a8 | |||
| 3ea3a50baa | |||
| ccd467ebcf | |||
| fe25c3e995 | |||
| d4af2d3ef5 | |||
| 6ff1edb67c | |||
| 3d2847e427 | |||
| ff4d8af054 | |||
| 612302f42f | |||
| a26607a154 | |||
| 939c7049ed | |||
| 0ead07e704 |
@@ -7,13 +7,17 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Does your feature request involve difficulty for you to complete a task? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I think it takes too many steps to [...]
|
||||
|
||||
> A clear and concise description of what the problem is. Ex. I think it takes too many steps to [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you'd like to happen.
|
||||
|
||||
> A clear and concise description of what you'd like to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
> A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any additional context or screenshots about the feature request here.
|
||||
|
||||
> Add any additional context or screenshots about the feature request here.
|
||||
|
||||
@@ -11,22 +11,16 @@ assignees: ''
|
||||
A clear and concise description of what the current behavior is.
|
||||
Please also add **screenshots** of the existing application.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
|
||||
[screenshot]
|
||||
```
|
||||
> **Example:**
|
||||
> In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
|
||||
> [screenshot]
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what the expected behavior is.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
The blank square should be transparent (invisible)
|
||||
```
|
||||
> **Example:**
|
||||
> The blank square should be transparent (invisible)
|
||||
|
||||
## Technical inputs
|
||||
|
||||
@@ -34,11 +28,8 @@ Operating System:
|
||||
|
||||
Readest Version:
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
Operating System: macOS 14.3.1
|
||||
Readest Version: 0.9.0
|
||||
We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
|
||||
Probably fixable with CSS
|
||||
```
|
||||
> **Example:**
|
||||
> Operating System: Android 14 (WebView 135.0)
|
||||
> Readest Version: 0.9.0
|
||||
> We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
|
||||
> Probably fixable with CSS
|
||||
|
||||
@@ -67,6 +67,11 @@ jobs:
|
||||
release: linux
|
||||
arch: aarch64
|
||||
rust_target: aarch64-unknown-linux-gnu
|
||||
- os: ubuntu-22.04-arm
|
||||
release: linux
|
||||
arch: armhf
|
||||
rust_target: arm-unknown-linux-gnueabihf
|
||||
args: '--target arm-unknown-linux-gnueabihf --bundles deb'
|
||||
- os: macos-latest
|
||||
release: macos
|
||||
arch: aarch64
|
||||
@@ -76,7 +81,7 @@ jobs:
|
||||
release: windows
|
||||
arch: x86_64
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
args: '--target x86_64-pc-windows-msvc'
|
||||
args: '--target x86_64-pc-windows-msvc --bundles nsis'
|
||||
- os: windows-latest
|
||||
release: windows
|
||||
arch: aarch64
|
||||
@@ -130,14 +135,25 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||
key: ${{ matrix.config.os }}-${{ matrix.config.release }}-${{ matrix.config.arch }}-cargo
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: contains(matrix.config.os, 'ubuntu') && matrix.config.release != 'android'
|
||||
if: contains(matrix.config.os, 'ubuntu') && matrix.config.release != 'android' && matrix.config.arch != 'armhf'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
|
||||
- name: install dependencies (ubuntu only - armhf specific)
|
||||
if: contains(matrix.config.os, 'ubuntu') && matrix.config.arch == 'armhf'
|
||||
run: |
|
||||
sudo dpkg --add-architecture armhf
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev:armhf libgtk-3-dev:armhf libwebkit2gtk-4.1-dev:armhf libappindicator3-dev:armhf librsvg2-dev:armhf gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
|
||||
echo 'PKG_CONFIG_ALLOW_CROSS=1' >> $GITHUB_ENV
|
||||
echo 'PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig' >> $GITHUB_ENV
|
||||
echo 'PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf' >> $GITHUB_ENV
|
||||
echo 'CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc' >> $GITHUB_ENV
|
||||
|
||||
- name: create .env.local file for Next.js
|
||||
run: |
|
||||
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
|
||||
@@ -152,6 +168,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.0.11902837
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
cd apps/readest-app/
|
||||
rm -rf src-tauri/gen/android
|
||||
@@ -179,6 +197,37 @@ jobs:
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $universial_apk --clobber
|
||||
echo "Uploading $arm64_apk to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $arm64_apk --clobber
|
||||
echo "Uploading signatures to GitHub release"
|
||||
pnpm tauri signer sign $universial_apk
|
||||
pnpm tauri signer sign $arm64_apk
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $universial_apk.sig --clobber
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $arm64_apk.sig --clobber
|
||||
|
||||
- name: download and update latest.json for Android release
|
||||
if: matrix.config.release == 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cd apps/readest-app/
|
||||
curl -sL https://github.com/readest/readest/releases/latest/download/latest.json -o latest.json
|
||||
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
universial_apk_url="https://github.com/readest/readest/releases/download/${{ needs.get-release.outputs.release_tag }}/Readest_${version}_universal.apk"
|
||||
arm64_apk_url="https://github.com/readest/readest/releases/download/${{ needs.get-release.outputs.release_tag }}/Readest_${version}_arm64.apk"
|
||||
|
||||
universial_sig=$(cat Readest_${version}_universal.apk.sig)
|
||||
arm64_sig=$(cat Readest_${version}_arm64.apk.sig)
|
||||
|
||||
jq --arg url "$universial_apk_url" \
|
||||
--arg sig "$universial_sig" \
|
||||
'.platforms["android-universal"] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json
|
||||
|
||||
jq --arg url "$arm64_apk_url" \
|
||||
--arg sig "$arm64_sig" \
|
||||
'.platforms["android-arm64"] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json
|
||||
|
||||
echo "Uploading updated latest.json to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
if: matrix.config.release != 'android'
|
||||
@@ -199,6 +248,13 @@ jobs:
|
||||
releaseBody: ${{ needs.get-release.outputs.release_note }}
|
||||
args: ${{ matrix.config.args || '' }}
|
||||
|
||||
- name: upload release notes to GitHub release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Uploading release notes to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} apps/readest-app/release-notes.json --clobber
|
||||
|
||||
- name: upload portable binaries (Windows only)
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
env:
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
url = https://github.com/readest/foliate-js.git
|
||||
[submodule "packages/tauri"]
|
||||
path = packages/tauri
|
||||
url = https://github.com/tauri-apps/tauri.git
|
||||
url = https://github.com/readest/tauri.git
|
||||
|
||||
Generated
+525
-440
File diff suppressed because it is too large
Load Diff
@@ -48,12 +48,13 @@
|
||||
| **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. | ✅ |
|
||||
|
||||
## Planned Features
|
||||
|
||||
@@ -62,7 +63,6 @@
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | 🛠 |
|
||||
| **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. | 🔄 |
|
||||
@@ -78,10 +78,10 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -252,9 +252,11 @@ The following libraries and frameworks are used in this software:
|
||||
- [zip.js](https://github.com/gildas-lormeau/zip.js), which is licensed under the BSD-3-Clause license.
|
||||
- [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](https://github.com/facebook/react), 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.
|
||||
- [tauri](https://github.com/tauri-apps/tauri), which is MIT licensed.
|
||||
|
||||
The following fonts are utilized in this software, either bundled within the application or provided through web fonts:
|
||||
|
||||
@@ -32,7 +32,7 @@ yarn-error.log*
|
||||
/private_keys
|
||||
|
||||
# plists
|
||||
*.plist
|
||||
Entitlements*.plist
|
||||
|
||||
# local confs
|
||||
tauri.*.conf.json
|
||||
|
||||
@@ -17,6 +17,7 @@ module.exports = {
|
||||
'el',
|
||||
'ko',
|
||||
'uk',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
|
||||
@@ -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.31",
|
||||
"version": "0.9.53",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -30,8 +30,10 @@
|
||||
"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",
|
||||
"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": {
|
||||
@@ -39,22 +41,23 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.735.0",
|
||||
"@ducanh2912/next-pwa": "^10.2.9",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.1.0",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.47.7",
|
||||
"@tauri-apps/api": "2.4.1",
|
||||
"@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.3.1",
|
||||
"@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.0",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -66,26 +69,29 @@
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^15.0.12",
|
||||
"next": "15.2.4",
|
||||
"posthog-js": "^1.205.0",
|
||||
"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",
|
||||
"semver": "^7.7.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/cloudflare": "^0.5.12",
|
||||
"@tauri-apps/cli": "2.4.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.5.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/semver": "^7.7.0",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cpx2": "^8.0.0",
|
||||
@@ -102,6 +108,6 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"wrangler": "^4.4.0"
|
||||
"wrangler": "^4.19.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "الكتاب",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "حذف",
|
||||
"Delete Highlight": "حذف التمييز",
|
||||
"Dictionary": "القاموس",
|
||||
"Disable Click-to-Flip": "تعطيل التقليب بالنقر",
|
||||
"Download Readest": "تحميل ريديست",
|
||||
"Edit": "تحرير",
|
||||
"Enter your custom CSS here...": "أدخل الـ CSS المخصص هنا...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "الإعدادات العامة",
|
||||
"Go Back": "العودة",
|
||||
"Go Forward": "التقدم",
|
||||
"Go Left": "الذهاب لليسار",
|
||||
"Go Right": "الذهاب لليمين",
|
||||
"Grass": "عشبي",
|
||||
"Gray": "رمادي",
|
||||
"Gruvbox": "چروفبوكس",
|
||||
"Highlight": "تمييز",
|
||||
"Horizontal Direction": "الاتجاه الأفقي",
|
||||
"Hyphenation": "الواصلة",
|
||||
"Identifier:": "المعرف:",
|
||||
"Import Books": "استيراد الكتب",
|
||||
"Language:": "اللغة:",
|
||||
"Layout": "التخطيط",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "مطابقة الكلمات الكاملة",
|
||||
"Maximum Number of Columns": "العدد الأقصى للأعمدة",
|
||||
"Minimum Font Size": "الحد الأدنى لحجم الخط",
|
||||
"Misc": "أخرى",
|
||||
"Monospace Font": "خط أحادي المسافة (Monospace)",
|
||||
"More Info": "مزيد من المعلومات",
|
||||
"Nord": "نورد",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "غير معروف",
|
||||
"Untitled": "بدون عنوان",
|
||||
"Updated:": "تم التحديث:",
|
||||
"User avatar": "صورة المستخدم",
|
||||
"Version {{version}}": "الإصدار {{version}}",
|
||||
"Vertical Direction": "الاتجاه العمودي",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "مرحبًا بكم في مكتبتكم. يمكنكم استيراد كتبكم هنا وقراءتها في أي وقت.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "تطبيق على جميع الكتب",
|
||||
"Apply to This Book": "تطبيق على هذا الكتاب",
|
||||
"Unable to fetch the translation. Try again later.": "تعذر جلب الترجمة. حاول مرة أخرى لاحقًا.",
|
||||
"Update Now!": "قم بالتحديث الآن!",
|
||||
"Update": "تحديث",
|
||||
"Check Update": "التحقق من التحديث",
|
||||
"Already the latest version": "الإصدار الحالي هو الإصدار الأحدث بالفعل",
|
||||
"Book Details": "تفاصيل الكتاب",
|
||||
@@ -195,7 +186,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": "أجهزة مزامنة سحابية غير محدودة",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "تم النسخ إلى الحافظة",
|
||||
"Export Annotations": "تصدير التعليقات",
|
||||
"Auto Import on File Open": "استيراد تلقائي عند فتح الملف",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "تبديل منطقة التقليب بالنقر",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "لم يتم التعرف على فصول.",
|
||||
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
|
||||
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم.",
|
||||
@@ -273,5 +263,98 @@
|
||||
"{{value}} hour": "{{value}} ساعة",
|
||||
"{{value}} hours": "{{value}} ساعات",
|
||||
"CJK Font": "خط CJK",
|
||||
"Clear Search": "مسح البحث"
|
||||
"Clear Search": "مسح البحث",
|
||||
"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?": "هل ترغب في تنزيل وتثبيت الآن؟",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
|
||||
"Download finished": "اكتمل التنزيل",
|
||||
"DOWNLOAD & INSTALL": "تنزيل وتثبيت",
|
||||
"Changelog:": "سجل التغييرات:",
|
||||
"Software Update": "تحديث البرنامج",
|
||||
"Title": "العنوان",
|
||||
"Date Read": "تاريخ القراءة",
|
||||
"Date Added": "تاريخ الإضافة",
|
||||
"Format": "التنسيق",
|
||||
"Ascending": "تصاعدي",
|
||||
"Descending": "تنازلي",
|
||||
"Sort by...": "ترتيب حسب...",
|
||||
"Added:": "تاريخ الإضافة:",
|
||||
"Format:": "التنسيق:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "الوصف:",
|
||||
"No description available": "لا يوجد وصف متاح",
|
||||
"List": "قائمة",
|
||||
"Grid": "شبكة",
|
||||
"(from 'As You Like It', Act II)": "(من 'كما تحب'، الفصل الثاني)",
|
||||
"Link Color": "لون الرابط",
|
||||
"Volume Keys for Page Flip": "مفاتيح الصوت لتقليب الصفحات",
|
||||
"Clicks for Page Flip": "نقرات لتقليب الصفحات",
|
||||
"Swap Clicks Area": "تبديل منطقة النقرات",
|
||||
"Screen": "الشاشة",
|
||||
"Orientation": "الاتجاه",
|
||||
"Portrait": "العمودي",
|
||||
"Landscape": "الأفقي",
|
||||
"Open Last Book on Start": "فتح آخر كتاب عند البدء",
|
||||
"Checking for updates...": "جارٍ التحقق من التحديثات...",
|
||||
"Error checking for updates": "خطأ في التحقق من التحديثات",
|
||||
"Details": "التفاصيل",
|
||||
"File Size:": "حجم الملف:",
|
||||
"Auto Detect": "الكشف التلقائي",
|
||||
"Next Section": "القسم التالي",
|
||||
"Previous Section": "القسم السابق",
|
||||
"Next 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": "ديب إل",
|
||||
"Google Translate": "ترجمة جوجل",
|
||||
"Azure Translator": "مترجم أزور",
|
||||
"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": "أحرف الترجمة"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Löschen",
|
||||
"Delete Highlight": "Hervorhebung löschen",
|
||||
"Dictionary": "Wörterbuch",
|
||||
"Disable Click-to-Flip": "Klick zum Blättern deaktivieren",
|
||||
"Download Readest": "Readest herunterladen",
|
||||
"Edit": "Bearbeiten",
|
||||
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Globale Einstellungen",
|
||||
"Go Back": "Zurück",
|
||||
"Go Forward": "Vorwärts",
|
||||
"Go Left": "Nach links",
|
||||
"Go Right": "Nach rechts",
|
||||
"Grass": "Grasgrün",
|
||||
"Gray": "Grau",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Hervorheben",
|
||||
"Horizontal Direction": "Horizontale Richtung",
|
||||
"Hyphenation": "Silbentrennung",
|
||||
"Identifier:": "Kennung:",
|
||||
"Import Books": "Bücher importieren",
|
||||
"Language:": "Sprache:",
|
||||
"Layout": "Layout",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Unbekannt",
|
||||
"Untitled": "Ohne Titel",
|
||||
"Updated:": "Aktualisiert:",
|
||||
"User avatar": "Benutzer-Avatar",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Vertikale Richtung",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Jetzt aktualisieren!",
|
||||
"Update": "Aktualisieren",
|
||||
"Check Update": "Aktualisierung prüfen",
|
||||
"Already the latest version": "Bereits die neueste Version",
|
||||
"Book Details": "Buchdetails",
|
||||
@@ -138,7 +129,7 @@
|
||||
"Book downloaded: {{title}}": "Buch heruntergeladen: {{title}}",
|
||||
"Failed to download book: {{title}}": "Fehler beim Herunterladen des Buches: {{title}}",
|
||||
"Upload Book": "Buch hochladen",
|
||||
"Auto Upload Books to Cloud": "Bücher automatisch in die Cloud hochladen",
|
||||
"Auto Upload Books to Cloud": "Bücher automatisch hochladen",
|
||||
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% des Cloud-Speichers verwendet.",
|
||||
"Storage": "Speicher",
|
||||
"Book deleted: {{title}}": "Buch gelöscht: {{title}}",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "In die Zwischenablage kopiert",
|
||||
"Export Annotations": "Anmerkungen exportieren",
|
||||
"Auto Import on File Open": "Automatischer Import beim Öffnen einer Datei",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Klick-zu-Blättern-Bereich tauschen",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Keine Kapitel erkannt.",
|
||||
"Failed to parse the EPUB file.": "Fehler beim Parsen der EPUB-Datei.",
|
||||
"This book format is not supported.": "Dieses Buchformat wird nicht unterstützt.",
|
||||
@@ -273,5 +263,90 @@
|
||||
"{{value}} hour": "{{value}} Stunde",
|
||||
"{{value}} hours": "{{value}} Stunden",
|
||||
"CJK Font": "CJK-Schriftart",
|
||||
"Clear Search": "Suche löschen"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Download abgeschlossen",
|
||||
"DOWNLOAD & INSTALL": "HERUNTERLADEN & INSTALLIEREN",
|
||||
"Changelog:": "Änderungsprotokoll:",
|
||||
"Software Update": "Software-Update",
|
||||
"Title": "Titel",
|
||||
"Date Read": "Datum gelesen",
|
||||
"Date Added": "Datum hinzugefügt",
|
||||
"Format": "Format",
|
||||
"Ascending": "Aufsteigend",
|
||||
"Descending": "Absteigend",
|
||||
"Sort by...": "Sortieren nach...",
|
||||
"Added:": "Hinzugefügt:",
|
||||
"Format:": "Format:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Beschreibung:",
|
||||
"No description available": "Keine Beschreibung verfügbar",
|
||||
"List": "Liste",
|
||||
"Grid": "Raster",
|
||||
"(from 'As You Like It', Act II)": "(aus 'Wie es euch gefällt', Akt II)",
|
||||
"Link Color": "Linkfarbe",
|
||||
"Volume Keys for Page Flip": "Volumen-Tasten für Seitenwechsel",
|
||||
"Clicks for Page Flip": "Tippen zum Blättern",
|
||||
"Swap Clicks Area": "Tippbereiche tauschen",
|
||||
"Screen": "Bildschirm",
|
||||
"Orientation": "Orientierung",
|
||||
"Portrait": "Hochformat",
|
||||
"Landscape": "Querformat",
|
||||
"Open Last Book on Start": "Letztes Buch beim Start öffnen",
|
||||
"Checking for updates...": "Auf Updates prüfen...",
|
||||
"Error checking for updates": "Fehler beim Überprüfen auf Updates",
|
||||
"Details": "Details",
|
||||
"File Size:": "Dateigröße:",
|
||||
"Auto Detect": "Automatisch erkennen",
|
||||
"Next Section": "Nächster Abschnitt",
|
||||
"Previous Section": "Vorheriger Abschnitt",
|
||||
"Next Page": "Nächste 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"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Κινούμενα σχέδια",
|
||||
"Annotate": "Σχολιασμός",
|
||||
"Apply": "Εφαρμογή",
|
||||
"Are you sure to delete the selected books?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα βιβλία;",
|
||||
"Auto Mode": "Αυτόματη λειτουργία",
|
||||
"Behavior": "Συμπεριφορά",
|
||||
"Book": "Βιβλίο",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Διαγραφή",
|
||||
"Delete Highlight": "Διαγραφή επισήμανσης",
|
||||
"Dictionary": "Λεξικό",
|
||||
"Disable Click-to-Flip": "Απενεργοποίηση κλικ για γύρισμα σελίδας",
|
||||
"Download Readest": "Λήψη Readest",
|
||||
"Edit": "Επεξεργασία",
|
||||
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Γενικές ρυθμίσεις",
|
||||
"Go Back": "Πίσω",
|
||||
"Go Forward": "Μπροστά",
|
||||
"Go Left": "Αριστερά",
|
||||
"Go Right": "Δεξιά",
|
||||
"Grass": "Γρασίδι",
|
||||
"Gray": "Γκρι",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Επισήμανση",
|
||||
"Horizontal Direction": "Οριζόντια κατεύθυνση",
|
||||
"Hyphenation": "Συλλαβισμός",
|
||||
"Identifier:": "Αναγνωριστικό:",
|
||||
"Import Books": "Εισαγωγή βιβλίων",
|
||||
"Language:": "Γλώσσα:",
|
||||
"Layout": "Διάταξη",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "Ταίριασμα ολόκληρων λέξεων",
|
||||
"Maximum Number of Columns": "Μέγιστος αριθμός στηλών",
|
||||
"Minimum Font Size": "Ελάχιστο μέγεθος γραμματοσειράς",
|
||||
"Misc": "Διάφορα",
|
||||
"Monospace Font": "Γραμματοσειρά σταθερού πλάτους",
|
||||
"More Info": "Περισσότερες πληροφορίες",
|
||||
"Nord": "Nord",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Άγνωστο",
|
||||
"Untitled": "Χωρίς τίτλο",
|
||||
"Updated:": "Ενημερώθηκε:",
|
||||
"User avatar": "Εικόνα χρήστη",
|
||||
"Version {{version}}": "Έκδοση {{version}}",
|
||||
"Vertical Direction": "Κάθετη κατεύθυνση",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Καλώς ήρθατε στη βιβλιοθήκη σας. Μπορείτε να εισάγετε τα βιβλία σας εδώ και να τα διαβάσετε οποιαδήποτε στιγμή.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "Εφαρμογή σε όλα τα βιβλία",
|
||||
"Apply to This Book": "Εφαρμογή σε αυτό το βιβλίο",
|
||||
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
|
||||
"Update Now!": "Ενημέρωση τώρα!",
|
||||
"Update": "Ενημέρωση",
|
||||
"Check Update": "Έλεγχος ενημέρωσης",
|
||||
"Already the latest version": "Ήδη η τελευταία έκδοση",
|
||||
"Book Details": "Λεπτομέρειες βιβλίου",
|
||||
@@ -138,12 +129,12 @@
|
||||
"Book downloaded: {{title}}": "Το βιβλίο με τίτλο {{title}} κατέβηκε",
|
||||
"Failed to download book: {{title}}": "Αποτυχία λήψης βιβλίου: {{title}}",
|
||||
"Upload Book": "Ανέβασμα βιβλίου",
|
||||
"Auto Upload Books to Cloud": "Αυτόματο ανέβασμα βιβλίων στο cloud",
|
||||
"Auto Upload Books to Cloud": "Αυτόματη αποθήκευση στο cloud",
|
||||
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% της αποθήκευσης στο cloud χρησιμοποιήθηκε.",
|
||||
"Storage": "Αποθήκευση",
|
||||
"Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε",
|
||||
"Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}",
|
||||
"Check Updates on Start": "Έλεγχος ενημερώσεων κατά την εκκίνηση",
|
||||
"Check Updates on Start": "Αυτόματος έλεγχος ενημερώσεων",
|
||||
"Insufficient storage quota": "Ανεπαρκές όριο αποθήκευσης",
|
||||
"Font Weight": "Βάρος γραμματοσειράς",
|
||||
"Line Spacing": "Διάκενο γραμμών",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Αντιγράφηκε στο πρόχειρο",
|
||||
"Export Annotations": "Εξαγωγή σχολιασμών",
|
||||
"Auto Import on File Open": "Αυτόματη εισαγωγή κατά το άνοιγμα αρχείου",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Ανταλλαγή περιοχής κλικ για γύρισμα σελίδας",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Δεν εντοπίστηκαν κεφάλαια.",
|
||||
"Failed to parse the EPUB file.": "Αποτυχία ανάλυσης του αρχείου EPUB.",
|
||||
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται.",
|
||||
@@ -273,5 +263,90 @@
|
||||
"{{value}} hour": "{{value}} ώρα",
|
||||
"{{value}} hours": "{{value}} ώρες",
|
||||
"CJK Font": "Γραμματοσειρά CJK",
|
||||
"Clear Search": "Καθαρισμός αναζήτησης"
|
||||
"Clear Search": "Καθαρισμός αναζήτησης",
|
||||
"Header & Footer": "Κεφαλίδα & Υποσέλιδο",
|
||||
"Apply also in Scrolled Mode": "Εφαρμογή και σε λειτουργία κύλισης",
|
||||
"A new version of Readest is Available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Η έκδοση Readest {{newVersion}} είναι διαθέσιμη (έκδοση που είναι εγκατεστημένη {{currentVersion}}).",
|
||||
"Download and install now?": "Θέλετε να την κατεβάσετε και να την εγκαταστήσετε τώρα;",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
|
||||
"Download finished": "Η λήψη ολοκληρώθηκε",
|
||||
"DOWNLOAD & INSTALL": "ΛΗΨΗ & ΕΓΚΑΤΑΣΤΑΣΗ",
|
||||
"Changelog:": "Αλλαγές:",
|
||||
"Software Update": "Ενημέρωση λογισμικού",
|
||||
"Title": "Τίτλος",
|
||||
"Date Read": "Ημερομηνία ανάγνωσης",
|
||||
"Date Added": "Ημερομηνία προσθήκης",
|
||||
"Format": "Μορφή",
|
||||
"Ascending": "Αύξουσα",
|
||||
"Descending": "Φθίνουσα",
|
||||
"Sort by...": "Ταξινόμηση κατά...",
|
||||
"Added:": "Προστέθηκε:",
|
||||
"Format:": "Μορφή:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Περιγραφή:",
|
||||
"No description available": "Δεν είναι διαθέσιμη καμία περιγραφή",
|
||||
"List": "Λίστα",
|
||||
"Grid": "Πλέγμα",
|
||||
"(from 'As You Like It', Act II)": "('Όπως σας αρέσει', Πράξη II)",
|
||||
"Link Color": "Χρώμα συνδέσμου",
|
||||
"Volume Keys for Page Flip": "Ανατροπή με πλήκτρα έντασης",
|
||||
"Clicks for Page Flip": "Κλικ για ανατροπή σελίδας",
|
||||
"Swap Clicks Area": "Ανταλλαγή περιοχής κλικ",
|
||||
"Screen": "Οθόνη",
|
||||
"Orientation": "Προσανατολισμός",
|
||||
"Portrait": "Πορτραίτο",
|
||||
"Landscape": "Τοπίο",
|
||||
"Open Last Book on Start": "Άνοιγμα τελευταίου βιβλίου",
|
||||
"Checking for updates...": "Έλεγχος για ενημερώσεις...",
|
||||
"Error checking for updates": "Σφάλμα κατά τον έλεγχο για ενημερώσεις",
|
||||
"Details": "Πληρ.",
|
||||
"File Size:": "Μέγεθος αρχείου:",
|
||||
"Auto Detect": "Αυτόματη ανίχνευση",
|
||||
"Next Section": "Επόμενη ενότητα",
|
||||
"Previous Section": "Προηγούμενη ενότητα",
|
||||
"Next 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": "Χαρακτήρες μετάφρασης"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"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..."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Eliminar",
|
||||
"Delete Highlight": "Eliminar resaltado",
|
||||
"Dictionary": "Diccionario",
|
||||
"Disable Click-to-Flip": "Desactivar clic para voltear",
|
||||
"Download Readest": "Descargar Readest",
|
||||
"Edit": "Editar",
|
||||
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Configuración global",
|
||||
"Go Back": "Volver",
|
||||
"Go Forward": "Avanzar",
|
||||
"Go Left": "Ir a la izquierda",
|
||||
"Go Right": "Ir a la derecha",
|
||||
"Grass": "Hierba",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Resaltar",
|
||||
"Horizontal Direction": "Dirección horizontal",
|
||||
"Hyphenation": "Guionización",
|
||||
"Identifier:": "Identificador:",
|
||||
"Import Books": "Importar libros",
|
||||
"Language:": "Idioma:",
|
||||
"Layout": "Diseño",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Desconocido",
|
||||
"Untitled": "Sin título",
|
||||
"Updated:": "Actualizado:",
|
||||
"User avatar": "Avatar del usuario",
|
||||
"Version {{version}}": "Versión {{version}}",
|
||||
"Vertical Direction": "Dirección vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "¡Actualizar ahora!",
|
||||
"Update": "Actualizar",
|
||||
"Check Update": "Comprobar actualización",
|
||||
"Already the latest version": "Ya es la última versión",
|
||||
"Book Details": "Detalles del libro",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Copiado al portapapeles",
|
||||
"Export Annotations": "Exportar anotaciones",
|
||||
"Auto Import on File Open": "Importación automática al abrir archivo",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Intercambiar área de clic para voltear",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "No se detectaron capítulos.",
|
||||
"Failed to parse the EPUB file.": "Error al analizar el archivo EPUB.",
|
||||
"This book format is not supported.": "Este formato de libro no es compatible.",
|
||||
@@ -273,5 +263,92 @@
|
||||
"{{value}} hour": "{{value}} hora",
|
||||
"{{value}} hours": "{{value}} horas",
|
||||
"CJK Font": "Fuente CJK",
|
||||
"Clear Search": "Limpiar búsqueda"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Descarga finalizada",
|
||||
"DOWNLOAD & INSTALL": "DESCARGAR E INSTALAR",
|
||||
"Changelog:": "Notas de la versión:",
|
||||
"Software Update": "Actualización de software",
|
||||
"Title": "Título",
|
||||
"Date Read": "Fecha de lectura",
|
||||
"Date Added": "Fecha añadida",
|
||||
"Format": "Formato",
|
||||
"Ascending": "Ascendente",
|
||||
"Descending": "Descendente",
|
||||
"Sort by...": "Ordenar por...",
|
||||
"Added:": "Agregado:",
|
||||
"Format:": "Formato:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Descripción:",
|
||||
"No description available": "No hay descripción disponible",
|
||||
"List": "Lista",
|
||||
"Grid": "Cuadrícula",
|
||||
"(from 'As You Like It', Act II)": "(de 'Como gustéis', Acto II)",
|
||||
"Link Color": "Color del enlace",
|
||||
"Volume Keys for Page Flip": "Botones de volumen para pasar página",
|
||||
"Clicks for Page Flip": "Toques para pasar página",
|
||||
"Swap Clicks Area": "Invertir zonas de toque",
|
||||
"Screen": "Pantalla",
|
||||
"Orientation": "Orientación",
|
||||
"Portrait": "Vertical",
|
||||
"Landscape": "Horizontal",
|
||||
"Open Last Book on Start": "Abrir último libro al iniciar",
|
||||
"Checking for updates...": "Comprobando actualizaciones...",
|
||||
"Error checking for updates": "Error al comprobar actualizaciones",
|
||||
"Details": "Detalles",
|
||||
"File Size:": "Peso del archivo:",
|
||||
"Auto Detect": "Detección automática",
|
||||
"Next Section": "Siguiente sección",
|
||||
"Previous Section": "Sección anterior",
|
||||
"Next Page": "Página siguiente",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Supprimer",
|
||||
"Delete Highlight": "Supprimer le surlignage",
|
||||
"Dictionary": "Dictionnaire",
|
||||
"Disable Click-to-Flip": "Désactiver le clic pour tourner",
|
||||
"Download Readest": "Télécharger Readest",
|
||||
"Edit": "Modifier",
|
||||
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Paramètres globaux",
|
||||
"Go Back": "Retour",
|
||||
"Go Forward": "Avancer",
|
||||
"Go Left": "Aller à gauche",
|
||||
"Go Right": "Aller à droite",
|
||||
"Grass": "Herbe",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Surligner",
|
||||
"Horizontal Direction": "Direction horizontale",
|
||||
"Hyphenation": "Césure",
|
||||
"Identifier:": "Identifiant :",
|
||||
"Import Books": "Importer des livres",
|
||||
"Language:": "Langue :",
|
||||
"Layout": "Mise en page",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Inconnu",
|
||||
"Untitled": "Sans titre",
|
||||
"Updated:": "Mis à jour :",
|
||||
"User avatar": "Avatar de l'utilisateur",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Direction verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Mettre à jour maintenant !",
|
||||
"Update": "Mettre à jour",
|
||||
"Check Update": "Vérifier la mise à jour",
|
||||
"Already the latest version": "Déjà la dernière version",
|
||||
"Book Details": "Détails du livre",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Copié dans le presse-papiers",
|
||||
"Export Annotations": "Exporter les annotations",
|
||||
"Auto Import on File Open": "Importation automatique à l'ouverture du fichier",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Échanger la zone de clic pour tourner",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Aucun chapitre détecté.",
|
||||
"Failed to parse the EPUB file.": "Échec de l'analyse du fichier EPUB.",
|
||||
"This book format is not supported.": "Ce format de livre n'est pas pris en charge.",
|
||||
@@ -273,5 +263,92 @@
|
||||
"{{value}} hour": "{{value}} heure",
|
||||
"{{value}} hours": "{{value}} heures",
|
||||
"CJK Font": "Police CJK",
|
||||
"Clear Search": "Effacer la recherche"
|
||||
"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",
|
||||
"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}}",
|
||||
"Download finished": "Téléchargement terminé",
|
||||
"DOWNLOAD & INSTALL": "TÉLÉCHARGER ET INSTALLER",
|
||||
"Changelog:": "Journal des modifications :",
|
||||
"Software Update": "Mise à jour logicielle",
|
||||
"Title": "Titre",
|
||||
"Date Read": "Date de lecture",
|
||||
"Date Added": "Date ajoutée",
|
||||
"Format": "Format",
|
||||
"Ascending": "Ascendant",
|
||||
"Descending": "Descendant",
|
||||
"Sort by...": "Trier par...",
|
||||
"Added:": "Ajouté :",
|
||||
"Format:": "Format :",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Description :",
|
||||
"No description available": "Aucune description disponible",
|
||||
"List": "Liste",
|
||||
"Grid": "Grille",
|
||||
"(from 'As You Like It', Act II)": "(de 'Comme il vous plaira', Acte II)",
|
||||
"Link Color": "Couleur du lien",
|
||||
"Volume Keys for Page Flip": "Boutons volume pour tourner la page",
|
||||
"Clicks for Page Flip": "Taps pour tourner la page",
|
||||
"Swap Clicks Area": "Inverser zones de tap",
|
||||
"Screen": "Écran",
|
||||
"Orientation": "Orientation",
|
||||
"Portrait": "Portrait",
|
||||
"Landscape": "Paysage",
|
||||
"Open Last Book on Start": "Ouvrir le dernier livre au démarrage",
|
||||
"Checking for updates...": "Vérification des mises à jour...",
|
||||
"Error checking for updates": "Erreur lors de la vérification des mises à jour",
|
||||
"Details": "Détails",
|
||||
"File Size:": "Taille du fichier :",
|
||||
"Auto Detect": "Détection automatique",
|
||||
"Next Section": "Section suivante",
|
||||
"Previous Section": "Section précédente",
|
||||
"Next Page": "Page suivante",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "एनीमेशन",
|
||||
"Annotate": "टिप्पणी",
|
||||
"Apply": "लागू करें",
|
||||
"Are you sure to delete the selected books?": "क्या आप चयनित पुस्तकों को हटाना चाहते हैं?",
|
||||
"Auto Mode": "स्वचालित मोड",
|
||||
"Behavior": "व्यवहार",
|
||||
"Book": "पुस्तक",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "हटाएं",
|
||||
"Delete Highlight": "हाइलाइट हटाएं",
|
||||
"Dictionary": "शब्दकोश",
|
||||
"Disable Click-to-Flip": "क्लिक-टू-फ्लिप अक्षम करें",
|
||||
"Download Readest": "Readest डाउनलोड करें",
|
||||
"Edit": "संपादित करें",
|
||||
"Enter your custom CSS here...": "अपना कस्टम CSS यहां दर्ज करें...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "वैश्विक सेटिंग्स",
|
||||
"Go Back": "वापस जाएं",
|
||||
"Go Forward": "आगे जाएं",
|
||||
"Go Left": "बाएं जाएं",
|
||||
"Go Right": "दाएं जाएं",
|
||||
"Grass": "घास",
|
||||
"Gray": "ग्रे",
|
||||
"Gruvbox": "ग्रूवबॉक्स",
|
||||
"Highlight": "हाइलाइट",
|
||||
"Horizontal Direction": "क्षैतिज दिशा",
|
||||
"Hyphenation": "हाइफ़नेशन",
|
||||
"Identifier:": "पहचानकर्ता:",
|
||||
"Import Books": "पुस्तकें आयात करें",
|
||||
"Language:": "भाषा:",
|
||||
"Layout": "लेआउट",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "पूरे शब्द मैच करें",
|
||||
"Maximum Number of Columns": "कॉलम की अधिकतम संख्या",
|
||||
"Minimum Font Size": "न्यूनतम फ़ॉन्ट साइज़",
|
||||
"Misc": "विविध",
|
||||
"Monospace Font": "मोनोस्पेस फ़ॉन्ट",
|
||||
"More Info": "अधिक जानकारी",
|
||||
"Nord": "नॉर्ड",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "अज्ञात",
|
||||
"Untitled": "शीर्षकहीन",
|
||||
"Updated:": "अपडेट किया गया:",
|
||||
"User avatar": "उपयोगकर्ता अवतार",
|
||||
"Version {{version}}": "संस्करण {{version}}",
|
||||
"Vertical Direction": "ऊर्ध्वाधर दिशा",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "आपकी लाइब्रेरी में आपका स्वागत है। आप यहां अपनी पुस्तकें आयात कर सकते हैं और कभी भी पढ़ सकते हैं।",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "सभी पुस्तकों पर लागू करें",
|
||||
"Apply to This Book": "इस पुस्तक पर लागू करें",
|
||||
"Unable to fetch the translation. Try again later.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
|
||||
"Update Now!": "अभी अपडेट करें!",
|
||||
"Update": "अपडेट करें",
|
||||
"Check Update": "अपडेट जांचें",
|
||||
"Already the latest version": "पहले से ही नवीनतम संस्करण",
|
||||
"Book Details": "पुस्तक विवरण",
|
||||
@@ -195,7 +186,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": "असीमित क्लाउड सिंक उपकरण",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "क्लिपबोर्ड पर कॉपी किया गया",
|
||||
"Export Annotations": "टिप्पणियाँ निर्यात करें",
|
||||
"Auto Import on File Open": "फ़ाइल खोलने पर स्वचालित आयात",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "क्लिक-टू-फ्लिप क्षेत्र स्वैप करें",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "कोई अध्याय नहीं मिला।",
|
||||
"Failed to parse the EPUB file.": "EPUB फ़ाइल को पार्स करने में विफल।",
|
||||
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।",
|
||||
@@ -273,5 +263,90 @@
|
||||
"{{value}} hour": "{{value}} घंटा",
|
||||
"{{value}} hours": "{{value}} घंटे",
|
||||
"CJK Font": "CJK फ़ॉन्ट",
|
||||
"Clear Search": "खोज साफ करें"
|
||||
"Clear Search": "खोज साफ करें",
|
||||
"Header & Footer": "हेडर और फ़ुटर",
|
||||
"Apply also in Scrolled Mode": "स्क्रॉल मोड में भी लागू करें",
|
||||
"A new version of Readest is Available!": "Readest का एक नया संस्करण उपलब्ध है!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} उपलब्ध है (स्थापित संस्करण {{currentVersion}})।",
|
||||
"Download and install now?": "अब डाउनलोड और इंस्टॉल करें?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
|
||||
"Download finished": "डाउनलोड पूरा हुआ",
|
||||
"DOWNLOAD & INSTALL": "डाउनलोड और इंस्टॉल करें",
|
||||
"Changelog:": "चेंजलॉग:",
|
||||
"Software Update": "सॉफ़्टवेयर अपडेट",
|
||||
"Title": "शीर्षक",
|
||||
"Date Read": "पढ़ने की तारीख",
|
||||
"Date Added": "जोड़ा गया दिनांक",
|
||||
"Format": "फॉर्मेट",
|
||||
"Ascending": "आरोही",
|
||||
"Descending": "अवरोही",
|
||||
"Sort by...": "द्वारा क्रमबद्ध करें...",
|
||||
"Added:": "जोड़ा गया:",
|
||||
"Format:": "फॉर्मेट:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "विवरण:",
|
||||
"No description available": "कोई विवरण उपलब्ध नहीं है",
|
||||
"List": "सूची",
|
||||
"Grid": "ग्रिड",
|
||||
"(from 'As You Like It', Act II)": "('जैसा आप इसे पसंद करते हैं', अधिनियम II)",
|
||||
"Link Color": "लिंक रंग",
|
||||
"Volume Keys for Page Flip": "पृष्ठ फ़्लिप के लिए वॉल्यूम कुंजियाँ",
|
||||
"Clicks for Page Flip": "पृष्ठ फ़्लिप के लिए क्लिक करें",
|
||||
"Swap Clicks Area": "क्लिक क्षेत्र स्वैप करें",
|
||||
"Screen": "स्क्रीन",
|
||||
"Orientation": "अवयव",
|
||||
"Portrait": "पोर्ट्रेट",
|
||||
"Landscape": "लैंडस्केप",
|
||||
"Open Last Book on Start": "शुरू में अंतिम पुस्तक खोलें",
|
||||
"Checking for updates...": "अपडेट की जांच कर रहा है...",
|
||||
"Error checking for updates": "अपडेट की जांच करते समय त्रुटि",
|
||||
"Details": "विवरण",
|
||||
"File Size:": "फ़ाइल आकार:",
|
||||
"Auto Detect": "स्वचालित पहचान",
|
||||
"Next Section": "अगला अनुभाग",
|
||||
"Previous Section": "पिछला अनुभाग",
|
||||
"Next 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": "अनुवाद वर्ण"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Hapus",
|
||||
"Delete Highlight": "Hapus Sorotan",
|
||||
"Dictionary": "Kamus",
|
||||
"Disable Click-to-Flip": "Nonaktifkan Klik untuk Membalik",
|
||||
"Download Readest": "Unduh Readest",
|
||||
"Edit": "Edit",
|
||||
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Pengaturan Global",
|
||||
"Go Back": "Kembali",
|
||||
"Go Forward": "Maju",
|
||||
"Go Left": "Ke Kiri",
|
||||
"Go Right": "Ke Kanan",
|
||||
"Grass": "Rumput",
|
||||
"Gray": "Abu-abu",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Sorot",
|
||||
"Horizontal Direction": "Arah Horizontal",
|
||||
"Hyphenation": "Pemenggalan Kata",
|
||||
"Identifier:": "Pengenal:",
|
||||
"Import Books": "Impor Buku",
|
||||
"Language:": "Bahasa:",
|
||||
"Layout": "Tata Letak",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Tidak Diketahui",
|
||||
"Untitled": "Tanpa Judul",
|
||||
"Updated:": "Diperbarui:",
|
||||
"User avatar": "Avatar Pengguna",
|
||||
"Version {{version}}": "Versi {{version}}",
|
||||
"Vertical Direction": "Arah Vertikal",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Perbarui Sekarang!",
|
||||
"Update": "Perbarui",
|
||||
"Check Update": "Periksa pembaruan",
|
||||
"Already the latest version": "Sudah versi terbaru",
|
||||
"Book Details": "Detail Buku",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Disalin ke papan klip",
|
||||
"Export Annotations": "Ekspor Anotasi",
|
||||
"Auto Import on File Open": "Impor Otomatis saat File Dibuka",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Tukar Area Klik untuk Membalik",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Tidak ada bab yang terdeteksi.",
|
||||
"Failed to parse the EPUB file.": "Gagal mengurai file EPUB.",
|
||||
"This book format is not supported.": "Format buku ini tidak didukung.",
|
||||
@@ -273,5 +263,88 @@
|
||||
"{{value}} hour": "{{value}} jam",
|
||||
"{{value}} hours": "{{value}} jam",
|
||||
"CJK Font": "Font CJK",
|
||||
"Clear Search": "Hapus Pencarian"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Unduhan selesai",
|
||||
"DOWNLOAD & INSTALL": "UNDUH & INSTAL",
|
||||
"Changelog:": "Catatan Perubahan:",
|
||||
"Software Update": "Pembaruan Perangkat Lunak",
|
||||
"Title": "Judul",
|
||||
"Date Read": "Tanggal Dibaca",
|
||||
"Date Added": "Tanggal Ditambahkan",
|
||||
"Format": "Format",
|
||||
"Ascending": "Naik",
|
||||
"Descending": "Turun",
|
||||
"Sort by...": "Urutkan berdasarkan...",
|
||||
"Added:": "Ditambahkan:",
|
||||
"Format:": "Format:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Deskripsi:",
|
||||
"No description available": "Tidak ada deskripsi yang tersedia",
|
||||
"List": "Daftar",
|
||||
"Grid": "Jala",
|
||||
"(from 'As You Like It', Act II)": "(dari 'Seperti yang Anda Inginkan', Bab II)",
|
||||
"Link Color": "Warna Tautan",
|
||||
"Volume Keys for Page Flip": "Volume Keys untuk Pembalikan Halaman",
|
||||
"Clicks for Page Flip": "Klik untuk Pembalikan Halaman",
|
||||
"Swap Clicks Area": "Area Klik Tukar",
|
||||
"Screen": "Screen",
|
||||
"Orientation": "Orientasi",
|
||||
"Portrait": "Potret",
|
||||
"Landscape": "Lanskap",
|
||||
"Open Last Book on Start": "Buka Buku Terakhir saat Memulai",
|
||||
"Checking for updates...": "Memeriksa pembaruan...",
|
||||
"Error checking for updates": "Kesalahan saat memeriksa pembaruan",
|
||||
"Details": "Detail",
|
||||
"File Size:": "Ukuran File:",
|
||||
"Auto Detect": "Deteksi Otomatis",
|
||||
"Next Section": "Bagian Selanjutnya",
|
||||
"Previous Section": "Bagian Sebelumnya",
|
||||
"Next Page": "Halaman Berikutnya",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Elimina",
|
||||
"Delete Highlight": "Elimina evidenziazione",
|
||||
"Dictionary": "Dizionario",
|
||||
"Disable Click-to-Flip": "Disattiva click per voltare pagina",
|
||||
"Download Readest": "Scarica Readest",
|
||||
"Edit": "Modifica",
|
||||
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Impostazioni globali",
|
||||
"Go Back": "Indietro",
|
||||
"Go Forward": "Avanti",
|
||||
"Go Left": "Vai a sinistra",
|
||||
"Go Right": "Vai a destra",
|
||||
"Grass": "Erba",
|
||||
"Gray": "Grigio",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Evidenzia",
|
||||
"Horizontal Direction": "Direzione orizzontale",
|
||||
"Hyphenation": "Sillabazione",
|
||||
"Identifier:": "Identificatore:",
|
||||
"Import Books": "Importa libri",
|
||||
"Language:": "Lingua:",
|
||||
"Layout": "Layout",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Sconosciuto",
|
||||
"Untitled": "Senza titolo",
|
||||
"Updated:": "Aggiornato:",
|
||||
"User avatar": "Avatar utente",
|
||||
"Version {{version}}": "Versione {{version}}",
|
||||
"Vertical Direction": "Direzione verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Aggiorna ora!",
|
||||
"Update": "Aggiorna",
|
||||
"Check Update": "Controlla aggiornamento",
|
||||
"Already the latest version": "Già l'ultima versione",
|
||||
"Book Details": "Dettagli libro",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Copiato negli appunti",
|
||||
"Export Annotations": "Esporta annotazioni",
|
||||
"Auto Import on File Open": "Importazione automatica all'apertura del file",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Scambia area click per voltare pagina",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Nessun capitolo rilevato.",
|
||||
"Failed to parse the EPUB file.": "Impossibile analizzare il file EPUB.",
|
||||
"This book format is not supported.": "Questo formato di libro non è supportato.",
|
||||
@@ -273,5 +263,92 @@
|
||||
"{{value}} hour": "{{value}} ora",
|
||||
"{{value}} hours": "{{value}} ore",
|
||||
"CJK Font": "Font CJK",
|
||||
"Clear Search": "Cancella ricerca"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Download completato",
|
||||
"DOWNLOAD & INSTALL": "SCARICA E INSTALLA",
|
||||
"Changelog:": "Note di rilascio:",
|
||||
"Software Update": "Aggiornamento software",
|
||||
"Title": "Titolo",
|
||||
"Date Read": "Data di lettura",
|
||||
"Date Added": "Data aggiunta",
|
||||
"Format": "Formato",
|
||||
"Ascending": "Crescente",
|
||||
"Descending": "Decrescente",
|
||||
"Sort by...": "Ordina per...",
|
||||
"Added:": "Aggiunto:",
|
||||
"Format:": "Formato:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Descrizione:",
|
||||
"No description available": "Nessuna descrizione disponibile",
|
||||
"List": "Elenco",
|
||||
"Grid": "Griglia",
|
||||
"(from 'As You Like It', Act II)": "(da 'Come vi piace', Atto II)",
|
||||
"Link Color": "Colore link",
|
||||
"Volume Keys for Page Flip": "Tasti volume per girare pagina",
|
||||
"Clicks for Page Flip": "Clic per girare pagina",
|
||||
"Swap Clicks Area": "Inverti area dei clic",
|
||||
"Screen": "Schermo",
|
||||
"Orientation": "Orientamento",
|
||||
"Portrait": "Ritratto",
|
||||
"Landscape": "Paesaggio",
|
||||
"Open Last Book on Start": "Apri ultimo libro all'avvio",
|
||||
"Checking for updates...": "Controllo aggiornamenti...",
|
||||
"Error checking for updates": "Errore durante il controllo degli aggiornamenti",
|
||||
"Details": "Dettagli",
|
||||
"File Size:": "Dimensione file:",
|
||||
"Auto Detect": "Rilevamento automatico",
|
||||
"Next Section": "Sezione successiva",
|
||||
"Previous Section": "Sezione precedente",
|
||||
"Next Page": "Pagina successiva",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "アニメーション",
|
||||
"Annotate": "注釈",
|
||||
"Apply": "適用",
|
||||
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
|
||||
"Auto Mode": "自動モード",
|
||||
"Behavior": "動作",
|
||||
"Book": "書籍",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "削除",
|
||||
"Delete Highlight": "ハイライトを削除",
|
||||
"Dictionary": "辞書",
|
||||
"Disable Click-to-Flip": "クリックめくりを無効化",
|
||||
"Download Readest": "Readestをダウンロード",
|
||||
"Edit": "編集",
|
||||
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "全体設定",
|
||||
"Go Back": "戻る",
|
||||
"Go Forward": "進む",
|
||||
"Go Left": "左へ",
|
||||
"Go Right": "右へ",
|
||||
"Grass": "グリーン",
|
||||
"Gray": "グレー",
|
||||
"Gruvbox": "グルーブボックス",
|
||||
"Highlight": "ハイライト",
|
||||
"Horizontal Direction": "横書",
|
||||
"Hyphenation": "ハイフネーション",
|
||||
"Identifier:": "識別子:",
|
||||
"Import Books": "書籍をインポート",
|
||||
"Language:": "言語:",
|
||||
"Layout": "レイアウト",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "単語全体を一致",
|
||||
"Maximum Number of Columns": "最大列数",
|
||||
"Minimum Font Size": "最小フォントサイズ",
|
||||
"Misc": "その他",
|
||||
"Monospace Font": "等幅フォント",
|
||||
"More Info": "詳細情報",
|
||||
"Nord": "ノルド",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "不明",
|
||||
"Untitled": "無題",
|
||||
"Updated:": "更新日:",
|
||||
"User avatar": "ユーザーアバター",
|
||||
"Version {{version}}": "バージョン {{version}}",
|
||||
"Vertical Direction": "縦書",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "すべての書籍に適用",
|
||||
"Apply to This Book": "この書籍に適用",
|
||||
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。",
|
||||
"Update Now!": "今すぐ更新!",
|
||||
"Update": "更新",
|
||||
"Check Update": "更新を確認",
|
||||
"Already the latest version": "すでに最新バージョン",
|
||||
"Book Details": "書籍の詳細",
|
||||
@@ -195,7 +186,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": "無制限のクラウド同期デバイス",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "クリップボードにコピー",
|
||||
"Export Annotations": "注釈をエクスポート",
|
||||
"Auto Import on File Open": "ファイルを開くと自動インポート",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "クリックめくりエリアを入れ替え",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "章が検出されません。",
|
||||
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました。",
|
||||
"This book format is not supported.": "この書籍形式はサポートされていません。",
|
||||
@@ -273,5 +263,88 @@
|
||||
"{{value}} hour": "{{value}}時間",
|
||||
"{{value}} hours": "{{value}}時間",
|
||||
"CJK Font": "CJKフォント",
|
||||
"Clear Search": "検索をクリア"
|
||||
"Clear Search": "検索をクリア",
|
||||
"Header & Footer": "ヘッダーとフッター",
|
||||
"Apply also in Scrolled Mode": "スクロールモードでも適用",
|
||||
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}が利用可能です(インストールされたバージョン{{currentVersion}})。",
|
||||
"Download and install now?": "今すぐダウンロードしてインストールしますか?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
|
||||
"Download finished": "ダウンロード完了",
|
||||
"DOWNLOAD & INSTALL": "更新",
|
||||
"Changelog:": "変更ログ:",
|
||||
"Software Update": "ソフトウェアの更新",
|
||||
"Title": "タイトル",
|
||||
"Date Read": "読書日",
|
||||
"Date Added": "追加日",
|
||||
"Format": "形式",
|
||||
"Ascending": "昇順",
|
||||
"Descending": "降順",
|
||||
"Sort by...": "並べ替え...",
|
||||
"Added:": "追加日:",
|
||||
"Format:": "形式:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "説明:",
|
||||
"No description available": "説明はありません",
|
||||
"List": "リスト",
|
||||
"Grid": "グリッド",
|
||||
"(from 'As You Like It', Act II)": "(「お気に召すまま」第2幕)",
|
||||
"Link Color": "リンクカラー",
|
||||
"Volume Keys for Page Flip": "音量キーでページめくり",
|
||||
"Clicks for Page Flip": "タップでページめくり",
|
||||
"Swap Clicks Area": "タップ領域を入れ替え",
|
||||
"Screen": "画面",
|
||||
"Orientation": "向き",
|
||||
"Portrait": "縦",
|
||||
"Landscape": "横",
|
||||
"Open Last Book on Start": "開始時に最後の書籍を開く",
|
||||
"Checking for updates...": "更新を確認中...",
|
||||
"Error checking for updates": "更新の確認中にエラーが発生しました",
|
||||
"Details": "詳細",
|
||||
"File Size:": "ファイルサイズ:",
|
||||
"Auto Detect": "自動検出",
|
||||
"Next Section": "次のセクション",
|
||||
"Previous Section": "前のセクション",
|
||||
"Next 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": "翻訳文字数"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "애니메이션",
|
||||
"Annotate": "주석",
|
||||
"Apply": "적용",
|
||||
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Auto Mode": "자동 모드",
|
||||
"Behavior": "동작",
|
||||
"Book": "책",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "삭제",
|
||||
"Delete Highlight": "하이라이트 삭제",
|
||||
"Dictionary": "사전",
|
||||
"Disable Click-to-Flip": "클릭 넘기기 비활성화",
|
||||
"Download Readest": "Readest 다운로드",
|
||||
"Edit": "편집",
|
||||
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "전역 설정",
|
||||
"Go Back": "뒤로",
|
||||
"Go Forward": "앞으로",
|
||||
"Go Left": "왼쪽으로",
|
||||
"Go Right": "오른쪽으로",
|
||||
"Grass": "잔디색",
|
||||
"Gray": "회색",
|
||||
"Gruvbox": "그러브박스",
|
||||
"Highlight": "하이라이트",
|
||||
"Horizontal Direction": "수평 방향",
|
||||
"Hyphenation": "하이픈 넣기",
|
||||
"Identifier:": "식별자:",
|
||||
"Import Books": "책 가져오기",
|
||||
"Language:": "언어:",
|
||||
"Layout": "레이아웃",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "전체 단어 일치",
|
||||
"Maximum Number of Columns": "최대 열 수",
|
||||
"Minimum Font Size": "최소 글꼴 크기",
|
||||
"Misc": "기타",
|
||||
"Monospace Font": "고정폭 글꼴",
|
||||
"More Info": "추가 정보",
|
||||
"Nord": "노드",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "알 수 없음",
|
||||
"Untitled": "제목 없음",
|
||||
"Updated:": "업데이트일:",
|
||||
"User avatar": "사용자 아바타",
|
||||
"Version {{version}}": "버전 {{version}}",
|
||||
"Vertical Direction": "수직 방향",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "모든 책에 적용",
|
||||
"Apply to This Book": "이 책에 적용",
|
||||
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
|
||||
"Update Now!": "지금 업데이트!",
|
||||
"Update": "업데이트",
|
||||
"Check Update": "업데이트 확인",
|
||||
"Already the latest version": "이미 최신 버전",
|
||||
"Book Details": "책 세부 정보",
|
||||
@@ -195,7 +186,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": "무제한 클라우드 동기화 기기",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "클립보드에 복사",
|
||||
"Export Annotations": "주석 내보내기",
|
||||
"Auto Import on File Open": "파일 열 때 자동 가져오기",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "클릭-뒤집기 영역 교환",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "장 발견되지 않음.",
|
||||
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
|
||||
"This book format is not supported.": "이 책 형식은 지원되지 않습니다.",
|
||||
@@ -273,5 +263,88 @@
|
||||
"{{value}} hour": "{{value}} 시간",
|
||||
"{{value}} hours": "{{value}} 시간",
|
||||
"CJK Font": "CJK 글꼴",
|
||||
"Clear Search": "검색 지우기"
|
||||
"Clear Search": "검색 지우기",
|
||||
"Header & Footer": "헤더 및 푸터",
|
||||
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
|
||||
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}가 사용 가능 (설치된 버전 {{currentVersion}}).",
|
||||
"Download and install now?": "지금 다운로드하고 설치하시겠습니까?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
|
||||
"Download finished": "다운로드 완료",
|
||||
"DOWNLOAD & INSTALL": "다운로드 및 설치",
|
||||
"Changelog:": "변경 로그:",
|
||||
"Software Update": "소프트웨어 업데이트",
|
||||
"Title": "제목",
|
||||
"Date Read": "읽은 날짜",
|
||||
"Date Added": "추가된 날짜",
|
||||
"Format": "형식",
|
||||
"Ascending": "오름차순",
|
||||
"Descending": "내림차순",
|
||||
"Sort by...": "정렬 기준...",
|
||||
"Added:": "추가됨:",
|
||||
"Format:": "형식:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "설명:",
|
||||
"No description available": "설명 없음",
|
||||
"List": "목록",
|
||||
"Grid": "격자",
|
||||
"(from 'As You Like It', Act II)": "(당신이 좋아하는 대로, 2막)",
|
||||
"Link Color": "링크 색상",
|
||||
"Volume Keys for Page Flip": "페이지 넘김을 위한 볼륨 키",
|
||||
"Clicks for Page Flip": "페이지 넘김을 위한 클릭",
|
||||
"Swap Clicks Area": "클릭 영역 교환",
|
||||
"Screen": "화면",
|
||||
"Orientation": "방향",
|
||||
"Portrait": "세로",
|
||||
"Landscape": "가로",
|
||||
"Open Last Book on Start": "시작 시 마지막 책 열기",
|
||||
"Checking for updates...": "업데이트 확인 중...",
|
||||
"Error checking for updates": "업데이트 확인 중 오류 발생",
|
||||
"Details": "세부정보",
|
||||
"File Size:": "파일 크기:",
|
||||
"Auto Detect": "자동 감지",
|
||||
"Next Section": "다음 섹션",
|
||||
"Previous Section": "이전 섹션",
|
||||
"Next 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": "번역 문자"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"Email address": "E-mailadres",
|
||||
"Your Password": "Uw wachtwoord",
|
||||
"Your email address": "Uw e-mailadres",
|
||||
"Your password": "Uw wachtwoord",
|
||||
"Sign in": "Inloggen",
|
||||
"Signing in...": "Bezig met inloggen...",
|
||||
"Sign in with {{provider}}": "Inloggen met {{provider}}",
|
||||
"Already have an account? Sign in": "Heeft u al een account? Log in",
|
||||
"Create a Password": "Maak een wachtwoord aan",
|
||||
"Sign up": "Registreren",
|
||||
"Signing up...": "Bezig met registreren...",
|
||||
"Check your email for the confirmation link": "Controleer uw e-mail voor de bevestigingslink",
|
||||
"Signing in ...": "Bezig met inloggen ...",
|
||||
"Send a magic link email": "Stuur een magic link e-mail",
|
||||
"Check your email for the magic link": "Controleer uw e-mail voor de magic link",
|
||||
"Send reset password instructions": "Stuur instructies voor wachtwoordherstel",
|
||||
"Sending reset instructions ...": "Bezig met versturen van herstelinstructies ...",
|
||||
"Forgot your password?": "Wachtwoord vergeten?",
|
||||
"Check your email for the password reset link": "Controleer uw e-mail voor de link om uw wachtwoord te herstellen",
|
||||
"Phone number": "Telefoonnummer",
|
||||
"Your phone number": "Uw telefoonnummer",
|
||||
"Token": "Token",
|
||||
"Your OTP token": "Uw OTP-token",
|
||||
"Verify token": "Token verifiëren",
|
||||
"Sign in with Google": "Inloggen met Google",
|
||||
"Sign in with Apple": "Inloggen met Apple",
|
||||
"Sign in with GitHub": "Inloggen met GitHub",
|
||||
"New Password": "Nieuw wachtwoord",
|
||||
"Your new password": "Uw nieuwe wachtwoord",
|
||||
"Update password": "Wachtwoord bijwerken",
|
||||
"Updating password ...": "Wachtwoord bijwerken ...",
|
||||
"Your password has been updated": "Uw wachtwoord is bijgewerkt",
|
||||
"Open": "Openen",
|
||||
"Group": "Groep",
|
||||
"Details": "Details",
|
||||
"Delete": "Verwijderen",
|
||||
"Cancel": "Annuleren",
|
||||
"Confirm Deletion": "Verwijdering bevestigen",
|
||||
"Deselect Book": "Boek deselecteren",
|
||||
"Select Book": "Boek selecteren",
|
||||
"Show Book Details": "Boekdetails weergeven",
|
||||
"Download Book": "Boek downloaden",
|
||||
"Upload Book": "Boek uploaden",
|
||||
"Deselect Group": "Groep deselecteren",
|
||||
"Select Group": "Groep selecteren",
|
||||
"Untitled Group": "Groep zonder titel",
|
||||
"Group Books": "Boeken groeperen",
|
||||
"Remove From Group": "Uit groep verwijderen",
|
||||
"Create New Group": "Nieuwe groep maken",
|
||||
"Save": "Opslaan",
|
||||
"Confirm": "Bevestigen",
|
||||
"From Local File": "Van lokaal bestand",
|
||||
"Go Back": "Terug",
|
||||
"Search Books...": "Boeken zoeken...",
|
||||
"Clear Search": "Zoekopdracht wissen",
|
||||
"Import Books": "Boeken importeren",
|
||||
"Select Multiple Books": "Meerdere boeken selecteren",
|
||||
"Select Books": "Boeken selecteren",
|
||||
"Storage": "Opslag",
|
||||
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% van cloudopslag gebruikt.",
|
||||
"Logged in as {{userDisplayName}}": "Ingelogd als {{userDisplayName}}",
|
||||
"Logged in": "Ingelogd",
|
||||
"Account": "Account",
|
||||
"Sign In": "Inloggen",
|
||||
"Auto Upload Books to Cloud": "Boeken automatisch uploaden naar cloud",
|
||||
"Auto Import on File Open": "Automatisch importeren bij bestand openen",
|
||||
"Open Last Book on Start": "Open laatste boek bij opstarten",
|
||||
"Check Updates on Start": "Controleer updates bij opstarten",
|
||||
"Fullscreen": "Volledig scherm",
|
||||
"Always on Top": "Altijd bovenaan",
|
||||
"Keep Screen Awake": "Scherm wakker houden",
|
||||
"Reload Page": "Pagina herladen",
|
||||
"Download Readest": "Readest downloaden",
|
||||
"About Readest": "Over Readest",
|
||||
"List": "Lijst",
|
||||
"Grid": "Raster",
|
||||
"Title": "Titel",
|
||||
"Author": "Auteur",
|
||||
"Format": "Formaat",
|
||||
"Date Read": "Datum gelezen",
|
||||
"Date Added": "Datum toegevoegd",
|
||||
"Ascending": "Oplopend",
|
||||
"Descending": "Aflopend",
|
||||
"Sort by...": "Sorteren op...",
|
||||
"No supported files found. Supported formats: {{formats}}": "Geen ondersteunde bestanden gevonden. Ondersteunde formaten: {{formats}}",
|
||||
"No chapters detected.": "Geen hoofdstukken gedetecteerd.",
|
||||
"Failed to parse the EPUB file.": "EPUB-bestand kon niet worden verwerkt.",
|
||||
"This book format is not supported.": "Dit boekformaat wordt niet ondersteund.",
|
||||
"Failed to import book(s): {{filenames}}": "Importeren van boek(en) mislukt: {{filenames}}",
|
||||
"Book uploaded: {{title}}": "Boek geüpload: {{title}}",
|
||||
"Insufficient storage quota": "Onvoldoende opslagruimte",
|
||||
"Failed to upload book: {{title}}": "Uploaden van boek mislukt: {{title}}",
|
||||
"Book downloaded: {{title}}": "Boek gedownload: {{title}}",
|
||||
"Failed to download book: {{title}}": "Downloaden van boek mislukt: {{title}}",
|
||||
"Book deleted: {{title}}": "Boek verwijderd: {{title}}",
|
||||
"Failed to delete book: {{title}}": "Verwijderen van boek mislukt: {{title}}",
|
||||
"Your Library": "Uw bibliotheek",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Welkom bij uw bibliotheek. U kunt hier uw boeken importeren en ze op elk moment lezen.",
|
||||
"Copied to notebook": "Gekopieerd naar notitieblok",
|
||||
"No annotations to export": "Geen annotaties om te exporteren",
|
||||
"Exported from Readest": "Geëxporteerd uit Readest",
|
||||
"Highlights & Annotations": "Markeringen & annotaties",
|
||||
"Untitled": "Zonder titel",
|
||||
"Note": "Notitie",
|
||||
"Copied to clipboard": "Gekopieerd naar klembord",
|
||||
"Copy": "Kopiëren",
|
||||
"Delete Highlight": "Markering verwijderen",
|
||||
"Highlight": "Markeren",
|
||||
"Annotate": "Annoteren",
|
||||
"Search": "Zoeken",
|
||||
"Dictionary": "Woordenboek",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Translate": "Vertalen",
|
||||
"Speak": "Voorlezen",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Kan de vertaling niet ophalen. Log eerst in en probeer het opnieuw.",
|
||||
"Unable to fetch the translation. Try again later.": "Kan de vertaling niet ophalen. Probeer het later opnieuw.",
|
||||
"Original Text": "Originele tekst",
|
||||
"Auto Detect": "Automatisch detecteren",
|
||||
"(detected)": "(gedetecteerd)",
|
||||
"Translated Text": "Vertaalde tekst",
|
||||
"Loading...": "Laden...",
|
||||
"Bookmark": "Bladwijzer",
|
||||
"Go Forward": "Vooruit",
|
||||
"Small": "Klein",
|
||||
"Large": "Groot",
|
||||
"Notebook": "Notitieblok",
|
||||
"Excerpts": "Fragmenten",
|
||||
"Notes": "Notities",
|
||||
"Add your notes here...": "Voeg hier uw notities toe...",
|
||||
"Theme Mode": "Thema modus",
|
||||
"Auto Mode": "Automatische modus",
|
||||
"Light Mode": "Lichte modus",
|
||||
"Dark Mode": "Donkere modus",
|
||||
"Theme Color": "Themakleur",
|
||||
"Custom": "Aangepast",
|
||||
"Apply to All Books": "Toepassen op alle boeken",
|
||||
"Apply to This Book": "Toepassen op dit boek",
|
||||
"Global Settings": "Algemene instellingen",
|
||||
"System Fonts": "Systeemlettertypen",
|
||||
"Serif Font": "Serif lettertype",
|
||||
"Sans-Serif Font": "Sans-serif lettertype",
|
||||
"Font Size": "Lettergrootte",
|
||||
"Default Font Size": "Standaard lettergrootte",
|
||||
"Minimum Font Size": "Minimale lettergrootte",
|
||||
"Font Weight": "Letterdikte",
|
||||
"Font Family": "Lettertypefamilie",
|
||||
"Default Font": "Standaard lettertype",
|
||||
"CJK Font": "CJK lettertype",
|
||||
"Override Book Font": "Boek lettertype overschrijven",
|
||||
"Font Face": "Lettertype",
|
||||
"Monospace Font": "Monospace lettertype",
|
||||
"Scrolled Mode": "Scroll-modus",
|
||||
"Writing Mode": "Schrijfmodus",
|
||||
"Default": "Standaard",
|
||||
"Horizontal Direction": "Horizontale richting",
|
||||
"Vertical Direction": "Verticale richting",
|
||||
"RTL Direction": "RTL-richting",
|
||||
"Border Frame": "Randkader",
|
||||
"Double Border": "Dubbele rand",
|
||||
"Border Color": "Randkleur",
|
||||
"Paragraph": "Alinea",
|
||||
"Paragraph Margin": "Alinea marge",
|
||||
"Line Spacing": "Regelafstand",
|
||||
"Word Spacing": "Woordafstand",
|
||||
"Letter Spacing": "Letterafstand",
|
||||
"Text Indent": "Tekstinspringing",
|
||||
"Full Justification": "Volledig uitvullen",
|
||||
"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",
|
||||
"Header & Footer": "Koptekst & voettekst",
|
||||
"Show Header": "Koptekst weergeven",
|
||||
"Show Footer": "Voettekst weergeven",
|
||||
"Apply also in Scrolled Mode": "Ook toepassen in scroll-modus",
|
||||
"Auto": "Automatisch",
|
||||
"Language": "Taal",
|
||||
"Animation": "Animatie",
|
||||
"Paging Animation": "Paginering animatie",
|
||||
"Screen": "Scherm",
|
||||
"Orientation": "Oriëntatie",
|
||||
"Portrait": "Portret",
|
||||
"Landscape": "Landschap",
|
||||
"Behavior": "Gedrag",
|
||||
"Continuous Scroll": "Doorlopend scrollen",
|
||||
"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",
|
||||
"(from 'As You Like It', Act II)": "(uit 'As You Like It', Akte II)",
|
||||
"Custom Theme": "Aangepast thema",
|
||||
"Theme Name": "Themanaam",
|
||||
"Text Color": "Tekstkleur",
|
||||
"Background Color": "Achtergrondkleur",
|
||||
"Link Color": "Linkkleur",
|
||||
"Preview": "Voorbeeld",
|
||||
"Font & Layout": "Lettertype & opmaak",
|
||||
"Book Cover": "Boekomslag",
|
||||
"More Info": "Meer informatie",
|
||||
"Parallel Read": "Parallel lezen",
|
||||
"Export Annotations": "Annotaties exporteren",
|
||||
"Edit": "Bewerken",
|
||||
"Search...": "Zoeken...",
|
||||
"Book": "Boek",
|
||||
"Chapter": "Hoofdstuk",
|
||||
"Match Case": "Hoofdlettergevoelig",
|
||||
"Match Whole Words": "Hele woorden",
|
||||
"Match Diacritics": "Diakritische tekens",
|
||||
"TOC": "Inhoudsopgave",
|
||||
"Sidebar": "Zijbalk",
|
||||
"TTS not supported for PDF": "TTS niet ondersteund voor PDF",
|
||||
"TTS not supported in this device": "TTS niet ondersteund op dit apparaat",
|
||||
"No Timeout": "Geen time-out",
|
||||
"{{value}} minute": "{{value}} minuut",
|
||||
"{{value}} minutes": "{{value}} minuten",
|
||||
"{{value}} hour": "{{value}} uur",
|
||||
"{{value}} hours": "{{value}} uren",
|
||||
"Slow": "Langzaam",
|
||||
"Fast": "Snel",
|
||||
"Reading Progress Synced": "Leesvoortgang gesynchroniseerd",
|
||||
"Failed to delete user. Please try again later.": "Gebruiker verwijderen mislukt. Probeer het later opnieuw.",
|
||||
"Free Tier": "Gratis niveau",
|
||||
"Community Support": "Community-ondersteuning",
|
||||
"DeepL Free Access": "DeepL gratis toegang",
|
||||
"Essential Text-to-Speech Voices": "Essentiële tekst-naar-spraak stemmen",
|
||||
"Unlimited Offline/Online Reading": "Onbeperkt offline/online lezen",
|
||||
"Unlimited Cloud Sync Devices": "Onbeperkt aantal cloud-synchronisatie apparaten",
|
||||
"500 MB Cloud Sync Space": "500 MB cloud-synchronisatieruimte",
|
||||
"Plus Tier": "Plus niveau",
|
||||
"Includes All Free Tier Benefits": "Inclusief alle voordelen van het gratis niveau",
|
||||
"Priority Support": "Prioriteitsondersteuning",
|
||||
"AI Summaries": "AI-samenvattingen",
|
||||
"AI Translations": "AI-vertalingen",
|
||||
"DeepL Pro Access": "DeepL Pro toegang",
|
||||
"Expanded Text-to-Speech Voices": "Uitgebreide tekst-naar-spraak stemmen",
|
||||
"2000 MB Cloud Sync Space": "2000 MB cloud-synchronisatieruimte",
|
||||
"Pro Tier": "Pro niveau",
|
||||
"Includes All Plus Tier Benefits": "Inclusief alle voordelen van het Plus niveau",
|
||||
"Unlimited AI Summaries": "Onbeperkte AI-samenvattingen",
|
||||
"Unlimited AI Translations": "Onbeperkte AI-vertalingen",
|
||||
"Unlimited DeepL Pro Access": "Onbeperkte DeepL Pro toegang",
|
||||
"Advanced AI Tools": "Geavanceerde AI-tools",
|
||||
"Early Feature Access": "Vroege toegang tot functies",
|
||||
"10 GB Cloud Sync Space": "10 GB cloud-synchronisatieruimte",
|
||||
"Loading profile...": "Profiel laden...",
|
||||
"Features": "Functies",
|
||||
"Sign Out": "Uitloggen",
|
||||
"Delete Account": "Account verwijderen",
|
||||
"Delete Your Account?": "Uw account verwijderen?",
|
||||
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Deze actie kan niet ongedaan worden gemaakt. Al uw gegevens in de cloud worden permanent verwijderd.",
|
||||
"Delete Permanently": "Permanent verwijderen",
|
||||
"Version {{version}}": "Versie {{version}}",
|
||||
"Check Update": "Update controleren",
|
||||
"Already the latest version": "Reeds de nieuwste versie",
|
||||
"Checking for updates...": "Controleren op updates...",
|
||||
"Error checking for updates": "Fout bij controleren op updates",
|
||||
"Book Details": "Boekdetails",
|
||||
"Unknown": "Onbekend",
|
||||
"Publisher:": "Uitgever:",
|
||||
"Published:": "Gepubliceerd:",
|
||||
"Updated:": "Bijgewerkt:",
|
||||
"Added:": "Toegevoegd:",
|
||||
"Language:": "Taal:",
|
||||
"Subjects:": "Onderwerpen:",
|
||||
"Format:": "Formaat:",
|
||||
"File Size:": "Bestandsgrootte:",
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Download voltooid",
|
||||
"DOWNLOAD & INSTALL": "DOWNLOADEN & INSTALLEREN",
|
||||
"Changelog:": "Wijzigingslogboek:",
|
||||
"Software Update": "Software-update",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai GB Scherm",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Gray": "Grijs",
|
||||
"Sepia": "Sepia",
|
||||
"Grass": "Gras",
|
||||
"Cherry": "Kers",
|
||||
"Sky": "Hemel",
|
||||
"Solarized": "Gesolariseerd",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Nord": "Noord",
|
||||
"Contrast": "Contrast",
|
||||
"Sunset": "Zonsondergang",
|
||||
"Reveal in Finder": "Tonen in Finder",
|
||||
"Reveal in File Explorer": "Tonen in Verkenner",
|
||||
"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",
|
||||
"Next Section": "Volgende sectie",
|
||||
"Previous Section": "Vorige sectie",
|
||||
"Next Page": "Volgende 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"
|
||||
}
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Usuń",
|
||||
"Delete Highlight": "Usuń zaznaczenie",
|
||||
"Dictionary": "Słownik",
|
||||
"Disable Click-to-Flip": "Wyłącz przewracanie kliknięciem",
|
||||
"Download Readest": "Pobierz Readest",
|
||||
"Edit": "Edytuj",
|
||||
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Ustawienia globalne",
|
||||
"Go Back": "Wstecz",
|
||||
"Go Forward": "Dalej",
|
||||
"Go Left": "W lewo",
|
||||
"Go Right": "W prawo",
|
||||
"Grass": "Trawiasty",
|
||||
"Gray": "Szary",
|
||||
"Gruvbox": "Ciepły pomarańczowy",
|
||||
"Highlight": "Zaznacz",
|
||||
"Horizontal Direction": "Kierunek poziomy",
|
||||
"Hyphenation": "Dzielenie wyrazów",
|
||||
"Identifier:": "Identyfikator:",
|
||||
"Import Books": "Importuj książki",
|
||||
"Language:": "Język:",
|
||||
"Layout": "Układ",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Nieznane",
|
||||
"Untitled": "Bez tytułu",
|
||||
"Updated:": "Zaktualizowano:",
|
||||
"User avatar": "Awatar użytkownika",
|
||||
"Version {{version}}": "Wersja {{version}}",
|
||||
"Vertical Direction": "Kierunek pionowy",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Witaj w swojej bibliotece. Możesz tutaj zaimportować swoje książki i czytać je w dowolnym momencie.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Zaktualizuj teraz!",
|
||||
"Update": "Zaktualizuj",
|
||||
"Check Update": "Sprawdź aktualizację",
|
||||
"Already the latest version": "Już najnowsza wersja",
|
||||
"Book Details": "Szczegóły książki",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Skopiowano do schowka",
|
||||
"Export Annotations": "Eksportuj adnotacje",
|
||||
"Auto Import on File Open": "Automatyczne importowanie przy otwieraniu pliku",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Zamień obszar kliknięcia na przewracanie",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Nie wykryto rozdziałów.",
|
||||
"Failed to parse the EPUB file.": "Nie udało się przeanalizować pliku EPUB.",
|
||||
"This book format is not supported.": "Ten format książki nie jest obsługiwany.",
|
||||
@@ -273,5 +263,94 @@
|
||||
"{{value}} hour": "{{value}} godzina",
|
||||
"{{value}} hours": "{{value}} godziny",
|
||||
"CJK Font": "Czcionka CJK",
|
||||
"Clear Search": "Wyczyść wyszukiwanie"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Pobieranie zakończone",
|
||||
"DOWNLOAD & INSTALL": "POBIERZ I ZAINSTALUJ",
|
||||
"Changelog:": "Dziennik zmian:",
|
||||
"Software Update": "Aktualizacja oprogramowania",
|
||||
"Title": "Tytuł",
|
||||
"Date Read": "Data przeczytania",
|
||||
"Date Added": "Data dodania",
|
||||
"Format": "Format",
|
||||
"Ascending": "Rosnąco",
|
||||
"Descending": "Malejąco",
|
||||
"Sort by...": "Sortuj według...",
|
||||
"Added:": "Data dodania:",
|
||||
"Format:": "Format:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Opis:",
|
||||
"No description available": "Brak opisu",
|
||||
"List": "Lista",
|
||||
"Grid": "Siatka",
|
||||
"(from 'As You Like It', Act II)": "(z „Jak wam się podoba”, Akt II)",
|
||||
"Link Color": "Kolor linku",
|
||||
"Volume Keys for Page Flip": "Klucze głośności do przewracania stron",
|
||||
"Clicks for Page Flip": "Kliknięcia do przewracania stron",
|
||||
"Swap Clicks Area": "Zamień obszar kliknięcia",
|
||||
"Screen": "Ekran",
|
||||
"Orientation": "Orientacja",
|
||||
"Portrait": "Portret",
|
||||
"Landscape": "Krajobraz",
|
||||
"Open Last Book on Start": "Otwórz ostatnią książkę przy starcie",
|
||||
"Checking for updates...": "Sprawdzanie aktualizacji...",
|
||||
"Error checking for updates": "Błąd podczas sprawdzania aktualizacji",
|
||||
"Details": "Szczegóły",
|
||||
"File Size:": "Rozmiar pliku:",
|
||||
"Auto Detect": "Automatyczne wykrywanie",
|
||||
"Next Section": "Następna sekcja",
|
||||
"Previous Section": "Poprzednia sekcja",
|
||||
"Next Page": "Następna 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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Excluir",
|
||||
"Delete Highlight": "Excluir Destaque",
|
||||
"Dictionary": "Dicionário",
|
||||
"Disable Click-to-Flip": "Desativar Clique para Virar",
|
||||
"Download Readest": "Baixar Readest",
|
||||
"Edit": "Editar",
|
||||
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Configurações Globais",
|
||||
"Go Back": "Voltar",
|
||||
"Go Forward": "Avançar",
|
||||
"Go Left": "Ir para Esquerda",
|
||||
"Go Right": "Ir para Direita",
|
||||
"Grass": "Grama",
|
||||
"Gray": "Cinza",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Destaque",
|
||||
"Horizontal Direction": "Direção Horizontal",
|
||||
"Hyphenation": "Hifenização",
|
||||
"Identifier:": "Identificador:",
|
||||
"Import Books": "Importar Livros",
|
||||
"Language:": "Idioma:",
|
||||
"Layout": "Layout",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Desconhecido",
|
||||
"Untitled": "Sem Título",
|
||||
"Updated:": "Atualizado:",
|
||||
"User avatar": "Avatar do usuário",
|
||||
"Version {{version}}": "Versão {{version}}",
|
||||
"Vertical Direction": "Direção Vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Atualizar Agora!",
|
||||
"Update": "Atualizar",
|
||||
"Check Update": "Verificar atualização",
|
||||
"Already the latest version": "Já é a versão mais recente",
|
||||
"Book Details": "Detalhes do Livro",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Copiado para a área de transferência",
|
||||
"Export Annotations": "Exportar Anotações",
|
||||
"Auto Import on File Open": "Importação Automática ao Abrir Arquivo",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Trocar Área de Clique para Virar",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Nenhum capítulo detectado.",
|
||||
"Failed to parse the EPUB file.": "Falha ao analisar o arquivo EPUB.",
|
||||
"This book format is not supported.": "Este formato de livro não é suportado.",
|
||||
@@ -273,5 +263,92 @@
|
||||
"{{value}} hour": "{{value}} hora",
|
||||
"{{value}} hours": "{{value}} horas",
|
||||
"CJK Font": "Fonte CJK",
|
||||
"Clear Search": "Limpar Pesquisa"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Download concluído",
|
||||
"DOWNLOAD & INSTALL": "BAIXAR E INSTALAR",
|
||||
"Changelog:": "Notas de versão:",
|
||||
"Software Update": "Atualização de Software",
|
||||
"Title": "Título",
|
||||
"Date Read": "Data de leitura",
|
||||
"Date Added": "Data de adição",
|
||||
"Format": "Formato",
|
||||
"Ascending": "Crescente",
|
||||
"Descending": "Decrescente",
|
||||
"Sort by...": "Ordenar por...",
|
||||
"Added:": "Adicionado:",
|
||||
"Format:": "Formato:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Descrição:",
|
||||
"No description available": "Nenhuma descrição disponível",
|
||||
"List": "Lista",
|
||||
"Grid": "Grade",
|
||||
"(from 'As You Like It', Act II)": "(de 'Como Gostais', Ato II)",
|
||||
"Link Color": "Cor do Link",
|
||||
"Volume Keys for Page Flip": "Teclas de Volume para Virar Página",
|
||||
"Clicks for Page Flip": "Cliques para Virar Página",
|
||||
"Swap Clicks Area": "Trocar Área de Cliques",
|
||||
"Screen": "Tela",
|
||||
"Orientation": "Orientação",
|
||||
"Portrait": "Retrato",
|
||||
"Landscape": "Paisagem",
|
||||
"Open Last Book on Start": "Abrir o Último Livro ao Iniciar",
|
||||
"Checking for updates...": "Verificando atualizações...",
|
||||
"Error checking for updates": "Erro ao verificar atualizações",
|
||||
"Details": "Detalhes",
|
||||
"File Size:": "Tamanho do arquivo:",
|
||||
"Auto Detect": "Detectar Automaticamente",
|
||||
"Next Section": "Próxima seção",
|
||||
"Previous Section": "Seção anterior",
|
||||
"Next Page": "Próxima página",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Анимация",
|
||||
"Annotate": "Аннотация",
|
||||
"Apply": "Применить",
|
||||
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
|
||||
"Auto Mode": "Автоматический режим",
|
||||
"Behavior": "Поведение",
|
||||
"Book": "Книга",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Удалить",
|
||||
"Delete Highlight": "Удалить выделение",
|
||||
"Dictionary": "Словарь",
|
||||
"Disable Click-to-Flip": "Отключить переворот по клику",
|
||||
"Download Readest": "Скачать Readest",
|
||||
"Edit": "Редактировать",
|
||||
"Enter your custom CSS here...": "Введите ваш пользовательский CSS здесь...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Общие настройки",
|
||||
"Go Back": "Назад",
|
||||
"Go Forward": "Вперёд",
|
||||
"Go Left": "Влево",
|
||||
"Go Right": "Вправо",
|
||||
"Grass": "Трава",
|
||||
"Gray": "Серый",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Выделить",
|
||||
"Horizontal Direction": "Горизонтальное направление",
|
||||
"Hyphenation": "Перенос слов",
|
||||
"Identifier:": "Идентификатор:",
|
||||
"Import Books": "Импорт книг",
|
||||
"Language:": "Язык:",
|
||||
"Layout": "Макет",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "Только целые слова",
|
||||
"Maximum Number of Columns": "Максимальное количество колонок",
|
||||
"Minimum Font Size": "Минимальный размер шрифта",
|
||||
"Misc": "Разное",
|
||||
"Monospace Font": "Моноширинный шрифт",
|
||||
"More Info": "Подробнее",
|
||||
"Nord": "Nord",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Неизвестно",
|
||||
"Untitled": "Без названия",
|
||||
"Updated:": "Обновлено:",
|
||||
"User avatar": "Аватар пользователя",
|
||||
"Version {{version}}": "Версия {{version}}",
|
||||
"Vertical Direction": "Вертикальное направление",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "Применить ко всем книгам",
|
||||
"Apply to This Book": "Применить к этой книге",
|
||||
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже.",
|
||||
"Update Now!": "Обновить сейчас!",
|
||||
"Update": "Обновить",
|
||||
"Check Update": "Проверить обновление",
|
||||
"Already the latest version": "Уже последняя версия",
|
||||
"Book Details": "Детали книги",
|
||||
@@ -195,7 +186,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": "Неограниченное количество устройств для облачной синхронизации",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Скопировано в буфер обмена",
|
||||
"Export Annotations": "Экспортировать аннотации",
|
||||
"Auto Import on File Open": "Автоматический импорт при открытии файла",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Переключить область клика для переворота",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Не обнаружено глав.",
|
||||
"Failed to parse the EPUB file.": "Не удалось разобрать файл EPUB.",
|
||||
"This book format is not supported.": "Этот формат книги не поддерживается.",
|
||||
@@ -273,5 +263,94 @@
|
||||
"{{value}} hour": "{{value}} час",
|
||||
"{{value}} hours": "{{value}} часа",
|
||||
"CJK Font": "CJK Шрифт",
|
||||
"Clear Search": "Очистить поиск"
|
||||
"Clear Search": "Очистить поиск",
|
||||
"Header & Footer": "Заголовок и нижний колонтитул",
|
||||
"Apply also in Scrolled Mode": "Применить также в прокручиваемом режиме",
|
||||
"A new version of Readest is Available!": "Доступна новая версия Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступен (установленная версия {{currentVersion}}).",
|
||||
"Download and install now?": "Скачать и установить сейчас?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
|
||||
"Download finished": "Загрузка завершена",
|
||||
"DOWNLOAD & INSTALL": "СКАЧАТЬ И УСТАНОВИТЬ",
|
||||
"Changelog:": "Изменения:",
|
||||
"Software Update": "Обновление программного обеспечения",
|
||||
"Title": "Название",
|
||||
"Date Read": "Дата прочтения",
|
||||
"Date Added": "Дата добавления",
|
||||
"Format": "Формат",
|
||||
"Ascending": "По возрастанию",
|
||||
"Descending": "По убыванию",
|
||||
"Sort by...": "Сортировать по...",
|
||||
"Added:": "Добавлено:",
|
||||
"Format:": "Формат:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Описание:",
|
||||
"No description available": "Нет доступного описания",
|
||||
"List": "Список",
|
||||
"Grid": "Сетка",
|
||||
"(from 'As You Like It', Act II)": "(из 'Как вам это понравится', Акт II)",
|
||||
"Link Color": "Цвет ссылки",
|
||||
"Volume Keys for Page Flip": "Клавиши громкости для перелистывания страниц",
|
||||
"Clicks for Page Flip": "Клики для перелистывания страниц",
|
||||
"Swap Clicks Area": "Переключить область кликов",
|
||||
"Screen": "Экран",
|
||||
"Orientation": "Ориентация",
|
||||
"Portrait": "Портрет",
|
||||
"Landscape": "Ландшафт",
|
||||
"Open Last Book on Start": "Открыть последнюю книгу при запуске",
|
||||
"Checking for updates...": "Проверка обновлений...",
|
||||
"Error checking for updates": "Ошибка при проверке обновлений",
|
||||
"Details": "Инфо",
|
||||
"File Size:": "Размер файла:",
|
||||
"Auto Detect": "Автоопределение",
|
||||
"Next Section": "Следующий раздел",
|
||||
"Previous Section": "Предыдущий раздел",
|
||||
"Next 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": "Символы перевода"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Sil",
|
||||
"Delete Highlight": "Vurguyu Sil",
|
||||
"Dictionary": "Sözlük",
|
||||
"Disable Click-to-Flip": "Tıkla-Çevir'i Devre Dışı Bırak",
|
||||
"Download Readest": "Readest'i İndir",
|
||||
"Edit": "Düzenle",
|
||||
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Genel Ayarlar",
|
||||
"Go Back": "Geri Git",
|
||||
"Go Forward": "İleri Git",
|
||||
"Go Left": "Sola Git",
|
||||
"Go Right": "Sağa Git",
|
||||
"Grass": "Çimen",
|
||||
"Gray": "Gri",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Vurgula",
|
||||
"Horizontal Direction": "Yatay Yön",
|
||||
"Hyphenation": "Heceleme",
|
||||
"Identifier:": "Tanımlayıcı:",
|
||||
"Import Books": "Kitapları İçe Aktar",
|
||||
"Language:": "Dil:",
|
||||
"Layout": "Düzen",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Untitled": "Başlıksız",
|
||||
"Updated:": "Güncellendi:",
|
||||
"User avatar": "Kullanıcı avatarı",
|
||||
"Version {{version}}": "Sürüm {{version}}",
|
||||
"Vertical Direction": "Dikey Yön",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Şimdi Güncelle!",
|
||||
"Update": "Güncelle",
|
||||
"Check Update": "Güncellemeyi kontrol et",
|
||||
"Already the latest version": "Zaten en son sürüm",
|
||||
"Book Details": "Kitap Detayları",
|
||||
@@ -195,7 +186,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ı",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Panoya kopyalandı",
|
||||
"Export Annotations": "Notları Dışa Aktar",
|
||||
"Auto Import on File Open": "Dosya Açıldığında Otomatik İçe Aktar",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Tıkla-Çevir Alanını Değiştir",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Bölüm bulunamadı.",
|
||||
"Failed to parse the EPUB file.": "EPUB dosyası ayrıştırılamadı.",
|
||||
"This book format is not supported.": "Bu kitap formatı desteklenmiyor.",
|
||||
@@ -273,5 +263,90 @@
|
||||
"{{value}} hour": "{{value}} saat",
|
||||
"{{value}} hours": "{{value}} saat",
|
||||
"CJK Font": "CJK Yazı Tipi",
|
||||
"Clear Search": "Aramayı Temizle"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "İndirme tamamlandı",
|
||||
"DOWNLOAD & INSTALL": "İNDİR & YÜKLE",
|
||||
"Changelog:": "Değişiklik Günlüğü:",
|
||||
"Software Update": "Yazılım Güncellemesi",
|
||||
"Title": "Başlık",
|
||||
"Date Read": "Okunma Tarihi",
|
||||
"Date Added": "Eklenme Tarihi",
|
||||
"Format": "Biçim",
|
||||
"Ascending": "Artan",
|
||||
"Descending": "Azalan",
|
||||
"Sort by...": "Şuna göre sırala...",
|
||||
"Added:": "Eklenme:",
|
||||
"Format:": "Biçim:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Tanım:",
|
||||
"No description available": "Tanım mevcut değil",
|
||||
"List": "Liste",
|
||||
"Grid": "Izgara",
|
||||
"(from 'As You Like It', Act II)": "(As You Like It, 2. Perde)",
|
||||
"Link Color": "Bağlantı Rengi",
|
||||
"Volume Keys for Page Flip": "Sayfa Çevirme için Ses Seviyesi Tuşları",
|
||||
"Clicks for Page Flip": "Sayfa Çevirme için Tıklamalar",
|
||||
"Swap Clicks Area": "Tıklama Alanını Değiştir",
|
||||
"Screen": "Ekran",
|
||||
"Orientation": "Yönlendirme",
|
||||
"Portrait": "Portre",
|
||||
"Landscape": "Yatay",
|
||||
"Open Last Book on Start": "Açılışta Son Kitabı Aç",
|
||||
"Checking for updates...": "Yenilikler kontrol ediliyor...",
|
||||
"Error checking for updates": "Güncellemeleri kontrol ederken hata oluştu",
|
||||
"Details": "Detaylar",
|
||||
"File Size:": "Dosya Boyutu:",
|
||||
"Auto Detect": "Algıla",
|
||||
"Next Section": "Sonraki Bölüm",
|
||||
"Previous Section": "Önceki Bölüm",
|
||||
"Next Page": "Sonraki 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"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "Анімація",
|
||||
"Annotate": "Анотація",
|
||||
"Apply": "Застосувати",
|
||||
"Are you sure to delete the selected books?": "Ви впевнені, що хочете видалити вибрані книги?",
|
||||
"Auto Mode": "Автоматичний режим",
|
||||
"Behavior": "Поведінка",
|
||||
"Book": "Книга",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Видалити",
|
||||
"Delete Highlight": "Видалити виділення",
|
||||
"Dictionary": "Словник",
|
||||
"Disable Click-to-Flip": "Вимкнути гортання кліком",
|
||||
"Download Readest": "Завантажити Readest",
|
||||
"Edit": "Редагувати",
|
||||
"Enter your custom CSS here...": "Введіть ваш користувацький CSS тут...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Глобальні налаштування",
|
||||
"Go Back": "Назад",
|
||||
"Go Forward": "Вперед",
|
||||
"Go Left": "Вліво",
|
||||
"Go Right": "Вправо",
|
||||
"Grass": "Трав'яний",
|
||||
"Gray": "Сірий",
|
||||
"Gruvbox": "Теплий помаранчевий",
|
||||
"Highlight": "Виділення",
|
||||
"Horizontal Direction": "Горизонтальний напрямок",
|
||||
"Hyphenation": "Перенесення слів",
|
||||
"Identifier:": "Ідентифікатор:",
|
||||
"Import Books": "Імпортувати книги",
|
||||
"Language:": "Мова:",
|
||||
"Layout": "Макет",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "Шукати цілі слова",
|
||||
"Maximum Number of Columns": "Максимальна кількість колонок",
|
||||
"Minimum Font Size": "Мінімальний розмір шрифту",
|
||||
"Misc": "Різне",
|
||||
"Monospace Font": "Моноширинний шрифт",
|
||||
"More Info": "Більше інформації",
|
||||
"Nord": "Північний",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Невідомо",
|
||||
"Untitled": "Без назви",
|
||||
"Updated:": "Оновлено:",
|
||||
"User avatar": "Аватар користувача",
|
||||
"Version {{version}}": "Версія {{version}}",
|
||||
"Vertical Direction": "Вертикальний напрямок",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Ласкаво просимо до вашої бібліотеки. Ви можете імпортувати сюди свої книги та читати їх будь-коли.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "Застосувати до всіх книг",
|
||||
"Apply to This Book": "Застосувати до цієї книги",
|
||||
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
|
||||
"Update Now!": "Оновити зараз!",
|
||||
"Update": "Оновити",
|
||||
"Check Update": "Перевірити оновлення",
|
||||
"Already the latest version": "Вже остання версія",
|
||||
"Book Details": "Деталі книги",
|
||||
@@ -195,7 +186,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": "Необмежена кількість пристроїв для хмарної синхронізації",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Скопійовано в буфер обміну",
|
||||
"Export Annotations": "Експортувати анотації",
|
||||
"Auto Import on File Open": "Автоматичний імпорт при відкритті файлу",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Поміняти місцями область кліку для перевороту",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Не виявлено жодного розділу.",
|
||||
"Failed to parse the EPUB file.": "Не вдалося розібрати файл EPUB.",
|
||||
"This book format is not supported.": "Цей формат книги не підтримується.",
|
||||
@@ -273,5 +263,94 @@
|
||||
"{{value}} hour": "{{value}} година",
|
||||
"{{value}} hours": "{{value}} години",
|
||||
"CJK Font": "CJK шрифт",
|
||||
"Clear Search": "Очистити пошук"
|
||||
"Clear Search": "Очистити пошук",
|
||||
"Header & Footer": "Заголовок і нижній колонтитул",
|
||||
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
|
||||
"A new version of Readest is Available!": "Доступна нова версія Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
|
||||
"Download and install now?": "Завантажити та встановити зараз?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
|
||||
"Download finished": "Завантаження завершено",
|
||||
"DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ І ВСТАНОВИТИ",
|
||||
"Changelog:": "Журнал змін:",
|
||||
"Software Update": "Оновлення програмного забезпечення",
|
||||
"Title": "Назва",
|
||||
"Date Read": "Дата прочитання",
|
||||
"Date Added": "Дата додавання",
|
||||
"Format": "Формат",
|
||||
"Ascending": "За зростанням",
|
||||
"Descending": "За спаданням",
|
||||
"Sort by...": "Сортувати за...",
|
||||
"Added:": "Додано:",
|
||||
"Format:": "Формат:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Опис:",
|
||||
"No description available": "Опис недоступний",
|
||||
"List": "Список",
|
||||
"Grid": "Сітка",
|
||||
"(from 'As You Like It', Act II)": "(з 'Як вам це подобається', Акт II)",
|
||||
"Link Color": "Колір посилання",
|
||||
"Volume Keys for Page Flip": "Клавіші гучності для перевороту сторінки",
|
||||
"Clicks for Page Flip": "Клацання для перевороту сторінки",
|
||||
"Swap Clicks Area": "Змінити область клацання",
|
||||
"Screen": "Екран",
|
||||
"Orientation": "Орієнтація",
|
||||
"Portrait": "Портрет",
|
||||
"Landscape": "Ландшафт",
|
||||
"Open Last Book on Start": "Відкрити останню книгу при запуску",
|
||||
"Checking for updates...": "Перевірка наявності оновлень...",
|
||||
"Error checking for updates": "Помилка перевірки наявності оновлень",
|
||||
"Details": "Деталі",
|
||||
"File Size:": "Розмір файлу:",
|
||||
"Auto Detect": "Авто виявлення",
|
||||
"Next Section": "Наступний розділ",
|
||||
"Previous Section": "Попередній розділ",
|
||||
"Next 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": "Символи перекладу"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "Xóa",
|
||||
"Delete Highlight": "Xóa văn bản được đánh dấu",
|
||||
"Dictionary": "Từ điển",
|
||||
"Disable Click-to-Flip": "Tắt tính năng nhấp để lật trang",
|
||||
"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...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "Cài đặt chung",
|
||||
"Go Back": "Quay lại",
|
||||
"Go Forward": "Tiến tới",
|
||||
"Go Left": "Sang trái",
|
||||
"Go Right": "Sang phải",
|
||||
"Grass": "Xanh cỏ",
|
||||
"Gray": "Xám",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Đánh dấu",
|
||||
"Horizontal Direction": "Hướng ngang",
|
||||
"Hyphenation": "Gạch nối từ",
|
||||
"Identifier:": "Định danh:",
|
||||
"Import Books": "Nhập sách",
|
||||
"Language:": "Ngôn ngữ:",
|
||||
"Layout": "Bố cục",
|
||||
@@ -64,7 +59,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",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "Không xác định",
|
||||
"Untitled": "Không có tiêu đề",
|
||||
"Updated:": "Cập nhật:",
|
||||
"User avatar": "Ảnh đại diện",
|
||||
"Version {{version}}": "Phiên bản {{version}}",
|
||||
"Vertical Direction": "Hướng dọc",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
|
||||
@@ -126,8 +119,6 @@
|
||||
"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.",
|
||||
"Update Now!": "Cập nhật ngay!",
|
||||
"Update": "Cập nhật",
|
||||
"Check Update": "Kiểm tra cập nhật",
|
||||
"Already the latest version": "Đã là phiên bản mới nhất",
|
||||
"Book Details": "Chi tiết sách",
|
||||
@@ -195,7 +186,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",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "Đã sao chép vào bảng ghi",
|
||||
"Export Annotations": "Xuất chú thích",
|
||||
"Auto Import on File Open": "Tự động nhập khi mở tệp",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai",
|
||||
"Swap Click-to-Flip Area": "Hoán đổi khu vực nhấp để lật trang",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Không phát hiện thấy chương nào.",
|
||||
"Failed to parse the EPUB file.": "Không thể phân tích tệp EPUB.",
|
||||
"This book format is not supported.": "Định dạng sách này không được hỗ trợ.",
|
||||
@@ -273,5 +263,88 @@
|
||||
"{{value}} hour": "{{value}} giờ",
|
||||
"{{value}} hours": "{{value}} giờ",
|
||||
"CJK Font": "Phông chữ CJK",
|
||||
"Clear Search": "Xóa tìm kiếm"
|
||||
"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!",
|
||||
"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}}",
|
||||
"Download finished": "Tải xuống hoàn tất",
|
||||
"DOWNLOAD & INSTALL": "TẢI XUỐNG & CÀI ĐẶT",
|
||||
"Changelog:": "Thay đổi:",
|
||||
"Software Update": "Cập nhật phần mềm",
|
||||
"Title": "Tiêu đề",
|
||||
"Date Read": "Ngày đã đọc",
|
||||
"Date Added": "Ngày thêm",
|
||||
"Format": "Định dạng",
|
||||
"Ascending": "Tăng dần",
|
||||
"Descending": "Giảm dần",
|
||||
"Sort by...": "Sắp xếp theo...",
|
||||
"Added:": "Đã thêm:",
|
||||
"Format:": "Định dạng:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Miêu tả:",
|
||||
"No description available": "Không có mô tả nào",
|
||||
"List": "Danh sách",
|
||||
"Grid": "Lưới",
|
||||
"(from 'As You Like It', Act II)": "(từ 'Như bạn thích', Hành động II)",
|
||||
"Link Color": "Màu liên kết",
|
||||
"Volume Keys for Page Flip": "Phím âm lượng để lật trang",
|
||||
"Clicks for Page Flip": "Nhấp để lật trang",
|
||||
"Swap Clicks Area": "Đổi khu vực nhấp",
|
||||
"Screen": "Màn hình",
|
||||
"Orientation": "Định hướng",
|
||||
"Portrait": "Chân dung",
|
||||
"Landscape": "Phong cảnh",
|
||||
"Open Last Book on Start": "Mở sách cuối cùng khi khởi động",
|
||||
"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",
|
||||
"File Size:": "Kích thước tệp:",
|
||||
"Auto Detect": "Phát hiện tự động",
|
||||
"Next Section": "Phần tiếp theo",
|
||||
"Previous Section": "Phần trước",
|
||||
"Next Page": "Trang tiếp theo",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "动画",
|
||||
"Annotate": "笔记",
|
||||
"Apply": "应用",
|
||||
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
|
||||
"Auto Mode": "自动主题",
|
||||
"Behavior": "行为",
|
||||
"Book": "书籍",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "删除",
|
||||
"Delete Highlight": "删除划线",
|
||||
"Dictionary": "词典",
|
||||
"Disable Click-to-Flip": "禁用点击翻页",
|
||||
"Download Readest": "下载 Readest",
|
||||
"Edit": "编辑",
|
||||
"Enter your custom CSS here...": "在此处输入您的自定义 CSS...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "全局设置",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前进",
|
||||
"Go Left": "向左",
|
||||
"Go Right": "向右",
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"Highlight": "划线",
|
||||
"Horizontal Direction": "横排",
|
||||
"Hyphenation": "断字",
|
||||
"Identifier:": "识别码",
|
||||
"Import Books": "导入书籍",
|
||||
"Language:": "语言",
|
||||
"Layout": "布局",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "匹配整个单词",
|
||||
"Maximum Number of Columns": "分栏数",
|
||||
"Minimum Font Size": "最小字号",
|
||||
"Misc": "杂项",
|
||||
"Monospace Font": "等宽字体",
|
||||
"More Info": "更多信息",
|
||||
"Nord": "极光",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "未知",
|
||||
"Untitled": "无标题",
|
||||
"Updated:": "更新日期",
|
||||
"User avatar": "用户头像",
|
||||
"Version {{version}}": "版本 {{version}}",
|
||||
"Vertical Direction": "竖排",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "书库空空如也,您可以导入您的书籍并随时阅读。",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "应用于所有书籍",
|
||||
"Apply to This Book": "应用于此书籍",
|
||||
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。",
|
||||
"Update Now!": "立即更新!",
|
||||
"Update": "更新",
|
||||
"Check Update": "检查更新",
|
||||
"Already the latest version": "已经是最新版本",
|
||||
"Book Details": "书籍详情",
|
||||
@@ -195,7 +186,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": "无限云同步设备个数",
|
||||
@@ -260,7 +250,7 @@
|
||||
"Export Annotations": "导出笔记",
|
||||
"Auto Import on File Open": "打开文件时自动导入",
|
||||
"LXGW WenKai GB Screen": "霞鹜文楷",
|
||||
"Swap Click-to-Flip Area": "交换点击翻页区域",
|
||||
"LXGW WenKai TC": "霞鶩文楷",
|
||||
"No chapters detected.": "未检测到章节。",
|
||||
"Failed to parse the EPUB file.": "无法解析 EPUB 文件。",
|
||||
"This book format is not supported.": "不支持此书籍格式。",
|
||||
@@ -273,5 +263,88 @@
|
||||
"{{value}} hour": "{{value}}小时",
|
||||
"{{value}} hours": "{{value}}小时",
|
||||
"CJK Font": "中文字体",
|
||||
"Clear Search": "清除搜索"
|
||||
"Clear Search": "清除搜索",
|
||||
"Header & Footer": "页眉页脚",
|
||||
"Apply also in Scrolled Mode": "应用到滚动模式",
|
||||
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安装版本 {{currentVersion}})。",
|
||||
"Download and install now?": "立即下载并安装?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
|
||||
"Download finished": "下载完成",
|
||||
"DOWNLOAD & INSTALL": "下载并安装",
|
||||
"Changelog:": "更新日志:",
|
||||
"Software Update": "软件更新",
|
||||
"Title": "书名",
|
||||
"Date Read": "阅读日期",
|
||||
"Date Added": "添加日期",
|
||||
"Format": "格式",
|
||||
"Ascending": "升序",
|
||||
"Descending": "降序",
|
||||
"Sort by...": "排序方式...",
|
||||
"Added:": "添加日期",
|
||||
"Format:": "格式",
|
||||
"GuanKiapTsingKhai-T": "原俠正楷-T",
|
||||
"Description:": "简介",
|
||||
"No description available": "暂无简介",
|
||||
"List": "列表",
|
||||
"Grid": "网格",
|
||||
"(from 'As You Like It', Act II)": "出自《皆大欢喜》,第二幕",
|
||||
"Link Color": "链接颜色",
|
||||
"Volume Keys for Page Flip": "音量键翻页",
|
||||
"Clicks for Page Flip": "点击翻页",
|
||||
"Swap Clicks Area": "交换点击区域",
|
||||
"Screen": "屏幕",
|
||||
"Orientation": "屏幕方向",
|
||||
"Portrait": "竖屏",
|
||||
"Landscape": "横屏",
|
||||
"Open Last Book on Start": "启动时打开上次阅读的书籍",
|
||||
"Checking for updates...": "检查更新中...",
|
||||
"Error checking for updates": "检查更新时出错",
|
||||
"Details": "详情",
|
||||
"File Size:": "文件大小",
|
||||
"Auto Detect": "自动检测",
|
||||
"Next Section": "下一章",
|
||||
"Previous Section": "上一章",
|
||||
"Next 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": "翻译字数"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"Animation": "動畫",
|
||||
"Annotate": "筆記",
|
||||
"Apply": "應用",
|
||||
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
|
||||
"Auto Mode": "自動主題",
|
||||
"Behavior": "行為",
|
||||
"Book": "書籍",
|
||||
@@ -27,7 +26,6 @@
|
||||
"Delete": "刪除",
|
||||
"Delete Highlight": "刪除劃線",
|
||||
"Dictionary": "詞典",
|
||||
"Disable Click-to-Flip": "禁用點擊翻頁",
|
||||
"Download Readest": "下載 Readest",
|
||||
"Edit": "編輯",
|
||||
"Enter your custom CSS here...": "在此處輸入您的自定義 CSS...",
|
||||
@@ -43,15 +41,12 @@
|
||||
"Global Settings": "全局設置",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前進",
|
||||
"Go Left": "向左",
|
||||
"Go Right": "向右",
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"Highlight": "劃線",
|
||||
"Horizontal Direction": "橫排",
|
||||
"Hyphenation": "斷字",
|
||||
"Identifier:": "識別碼",
|
||||
"Import Books": "導入書籍",
|
||||
"Language:": "語言",
|
||||
"Layout": "版面",
|
||||
@@ -64,7 +59,6 @@
|
||||
"Match Whole Words": "匹配整個單詞",
|
||||
"Maximum Number of Columns": "分欄數",
|
||||
"Minimum Font Size": "最小字號",
|
||||
"Misc": "雜項",
|
||||
"Monospace Font": "等寬字體",
|
||||
"More Info": "更多信息",
|
||||
"Nord": "極光",
|
||||
@@ -112,7 +106,6 @@
|
||||
"Unknown": "未知",
|
||||
"Untitled": "無標題",
|
||||
"Updated:": "更新日期",
|
||||
"User avatar": "用戶頭像",
|
||||
"Version {{version}}": "版本 {{version}}",
|
||||
"Vertical Direction": "豎排",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
|
||||
@@ -126,8 +119,6 @@
|
||||
"Apply to All Books": "應用於所有書籍",
|
||||
"Apply to This Book": "應用於此書籍",
|
||||
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。",
|
||||
"Update Now!": "立即更新!",
|
||||
"Update": "更新",
|
||||
"Check Update": "檢查更新",
|
||||
"Already the latest version": "已經是最新版本",
|
||||
"Book Details": "書籍詳情",
|
||||
@@ -195,7 +186,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": "無限雲端同步裝置",
|
||||
@@ -259,8 +249,8 @@
|
||||
"Copied to clipboard": "已複製到剪貼板",
|
||||
"Export Annotations": "匯出筆記",
|
||||
"Auto Import on File Open": "打開文件時自動導入",
|
||||
"LXGW WenKai GB Screen": "霞鶩文楷",
|
||||
"Swap Click-to-Flip Area": "交換點擊翻頁區域",
|
||||
"LXGW WenKai GB Screen": "霞鶩文楷 SC",
|
||||
"LXGW WenKai TC": "霞鶩文楷 TC",
|
||||
"No chapters detected.": "未檢測到章節。",
|
||||
"Failed to parse the EPUB file.": "無法解析 EPUB 文件。",
|
||||
"This book format is not supported.": "不支援此書籍格式。",
|
||||
@@ -273,5 +263,88 @@
|
||||
"{{value}} hour": "{{value}}小時",
|
||||
"{{value}} hours": "{{value}}小時",
|
||||
"CJK Font": "中文字體",
|
||||
"Clear Search": "清除搜索"
|
||||
"Clear Search": "清除搜索",
|
||||
"Header & Footer": "頁眉頁腳",
|
||||
"Apply also in Scrolled Mode": "滾動模式同樣適用",
|
||||
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安裝版本 {{currentVersion}})。",
|
||||
"Download and install now?": "立即下載並安裝?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
|
||||
"Download finished": "下載完成",
|
||||
"DOWNLOAD & INSTALL": "下載並安裝",
|
||||
"Changelog:": "更新日誌:",
|
||||
"Software Update": "軟體更新",
|
||||
"Title": "書名",
|
||||
"Date Read": "閱讀日期",
|
||||
"Date Added": "添加日期",
|
||||
"Format": "格式",
|
||||
"Ascending": "升序",
|
||||
"Descending": "降序",
|
||||
"Sort by...": "排序方式...",
|
||||
"Added:": "添加日期",
|
||||
"Format:": "格式",
|
||||
"GuanKiapTsingKhai-T": "原俠正楷-T",
|
||||
"Description:": "簡介",
|
||||
"No description available": "暫無簡介",
|
||||
"List": "列表",
|
||||
"Grid": "網格",
|
||||
"(from 'As You Like It', Act II)": "(出自《皆大歡喜》,第二幕)",
|
||||
"Link Color": "連結顏色",
|
||||
"Volume Keys for Page Flip": "音量鍵翻頁",
|
||||
"Clicks for Page Flip": "點擊翻頁",
|
||||
"Swap Clicks Area": "交換點擊區域",
|
||||
"Screen": "螢幕",
|
||||
"Orientation": "螢幕方向",
|
||||
"Portrait": "豎屏",
|
||||
"Landscape": "橫屏",
|
||||
"Open Last Book on Start": "啟動時打開上次閱讀的書籍",
|
||||
"Checking for updates...": "檢查更新中...",
|
||||
"Error checking for updates": "檢查更新時出錯",
|
||||
"Details": "詳情",
|
||||
"File Size:": "檔案大小",
|
||||
"Auto Detect": "自動檢測",
|
||||
"Next Section": "下一章",
|
||||
"Previous Section": "上一章",
|
||||
"Next 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": "翻譯字數"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,172 @@
|
||||
{
|
||||
"releases": {
|
||||
"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": [
|
||||
"Enhanced compatibility with Gutenberg and Feedbooks ebooks for better styling",
|
||||
"Reduced sensitivity of trackpad and mouse during page flips",
|
||||
"Cloud backup status is now visible for each book on mobile and desktop",
|
||||
"New option to control scroll overlap when paginating in scroll mode",
|
||||
"Added Dutch language translations",
|
||||
"Added more font weight options for built-in fonts",
|
||||
"Added previous and next section buttons in the footer bar for easier navigation"
|
||||
]
|
||||
},
|
||||
"0.9.41": {
|
||||
"date": "2025-05-13",
|
||||
"notes": [
|
||||
"Improved UI and layout on mobile devices",
|
||||
"Avatar images are now cached for offline use",
|
||||
"Book file size is now displayed in the book details view",
|
||||
"Update status is now correctly shown in the About window",
|
||||
"TTS control positioning improved for notebook and RTL modes",
|
||||
"TXT files are segmented by paragraph count if no chapter titles are found",
|
||||
"Android: App now respects system auto-rotation settings",
|
||||
"Linux: Updater is disabled on Linux when not using AppImage",
|
||||
"Linux: Added support for Linux ARMHF builds",
|
||||
"CSS: Resolved layout and color issues caused by hardcoded CSS"
|
||||
]
|
||||
},
|
||||
"0.9.40": {
|
||||
"date": "2025-05-08",
|
||||
"notes": [
|
||||
"Prevented oversized images from causing layout issues",
|
||||
"Resolved issue with transient navigation bar on Android 9",
|
||||
"Action bar now closes properly when the footbar is dismissed",
|
||||
"Smoother experience when expanding Table of Contents sections",
|
||||
"Reverted unintended style overrides"
|
||||
]
|
||||
},
|
||||
"0.9.39": {
|
||||
"date": "2025-05-07",
|
||||
"notes": [
|
||||
"Enhanced Navigation: Added location information to the table of contents",
|
||||
"Enhanced Navigation: Larger touch targets for table of contents expansion icons",
|
||||
"Screen Orientation Lock: You can now lock your device's screen orientation when reading",
|
||||
"Quick Access to Recent Books: Option to automatically open your last read book when starting the app",
|
||||
"Better Language Support: Improved handling of CJK characters in search",
|
||||
"Enhanced Text-to-Speech: More accurate language detection for TTS",
|
||||
"Enhanced Text-to-Speech: Text-to-Speech now maintains your position in scroll mode",
|
||||
"Enhanced Annotations: Better handling of annotations with ruby elements in Japanese books",
|
||||
"Smoother Scrolling: We've made continuous scrolling more reliable for a seamless reading experience",
|
||||
"Compact View: Reading view is more compact when header and footer are hidden",
|
||||
"TXT Parsing: More robust handling of TXT book format"
|
||||
]
|
||||
},
|
||||
"0.9.38": {
|
||||
"date": "2025-04-29",
|
||||
"notes": [
|
||||
"Page Turning with Volume Keys: You can now turn pages using your device’s volume buttons",
|
||||
"Expanded English Voices: More English voices are available across all en locales",
|
||||
"Theme Editor: Added a primary color picker for easier customization",
|
||||
"System Fonts: Retrieve system fonts on iOS and Android, with improved font weight and style detection",
|
||||
"Popup Footnotes: Popup footnotes now inherit the book's font for consistent appearance",
|
||||
"Better Text Selection: Preserves text selection anchors when spanning paginated content",
|
||||
"Android Compatibility: Navigation bar auto-hides correctly on Android 10 and below",
|
||||
"Popup Dialogs: Back key now properly dismisses popup dialogs on Android"
|
||||
]
|
||||
},
|
||||
"0.9.37": {
|
||||
"date": "2025-04-25",
|
||||
"notes": [
|
||||
"Full-Screen Toggle: You can now press F11 to quickly toggle full-screen mode",
|
||||
"List View: A new list layout is available for your bookshelf",
|
||||
"Book Details: Book descriptions from metadata are now shown in the details view",
|
||||
"Enhanced Android behavior: better navigation bar handling and status bar dismissal when resuming",
|
||||
"TTS now defaults to the book’s metadata language when needed",
|
||||
"Synced custom themes reliably across sessions"
|
||||
]
|
||||
},
|
||||
"0.9.36": {
|
||||
"date": "2025-04-20",
|
||||
"notes": [
|
||||
"📥 New download and upload buttons in the book detail dialog for easier file management",
|
||||
"📂 You can now open files directly in Readest from your file manager on Android and iOS",
|
||||
"🛠️ Fixed an issue where files with quotation marks in the filename could not be opened",
|
||||
"📖 Improved the immersive reading experience on the reader page for Android and iOS",
|
||||
"🌐 Added GuanKiapTsingKhai-T to the list of available CJK fonts"
|
||||
]
|
||||
},
|
||||
"0.9.35": {
|
||||
"date": "2025-04-15",
|
||||
"notes": [
|
||||
"🛠️ Fixed an issue where books without cover images can not be synced across devices",
|
||||
"📱 Added in-app update support for Android",
|
||||
"📚 You can now sort books by title or author in your library"
|
||||
]
|
||||
},
|
||||
"0.9.33": {
|
||||
"date": "2025-04-13",
|
||||
"notes": [
|
||||
"New: Redesigned update dialog with a progress bar during downloads for a smoother update experience.",
|
||||
"New: You can now set header and footer visibility separately for paginated and scroll modes.",
|
||||
"Fixed layout issues in the bottom configuration panel when using RTL (right-to-left) languages.",
|
||||
"Fixed TXT file import issues on Android by improving support for content provider file paths.",
|
||||
"Fixed transformed punctuations when highlighting text with vertical layout.",
|
||||
"Improved login flow on macOS by handling OAuth with the native ASWebAuthenticationSession."
|
||||
]
|
||||
},
|
||||
"0.9.32": {
|
||||
"date": "2025-04-09",
|
||||
"notes": [
|
||||
"No more resetting of font size and color when you apply custom book styles",
|
||||
"The underline and squiggly highlight decorations have been adjusted to sit neatly between text lines",
|
||||
"With more font options available, the font menu is no longer forced in-line",
|
||||
"LXGW WenKai TC is now part of our CJK font list"
|
||||
]
|
||||
},
|
||||
"0.9.31": {
|
||||
"date": "2025-04-08",
|
||||
"notes": [
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Updating bundleVersion in tauri.appstore.conf.json..."
|
||||
|
||||
CONFIG_FILE="src-tauri/tauri.appstore.conf.json"
|
||||
CURRENT_DATE=$(date "+%Y%m%d.%H%M%S")
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Error: Config file $CONFIG_FILE not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_FILE=$(mktemp)
|
||||
cat "$CONFIG_FILE" | jq --arg version "$CURRENT_DATE" '.bundle.macOS.bundleVersion = $version' > "$TMP_FILE"
|
||||
mv "$TMP_FILE" "$CONFIG_FILE"
|
||||
echo "Updated bundleVersion to $CURRENT_DATE"
|
||||
|
||||
echo "Building macOS universal app for App Store..."
|
||||
pnpm run build-macos-universial-appstore
|
||||
|
||||
BUNDLE_DIR=../../target/universal-apple-darwin/release/bundle/macos
|
||||
|
||||
@@ -31,9 +31,9 @@ reqwest = { version = "0.12", default-features = false, features = [
|
||||
"stream",
|
||||
] }
|
||||
# FIXME: remove the devtools feature in production
|
||||
tauri = { version = "2.4.1", features = [ "protocol-asset", "devtools"] }
|
||||
tauri = { version = "2.5.1", features = [ "protocol-asset", "devtools"] }
|
||||
tauri-build = "2.1.1"
|
||||
tauri-plugin-log = "2.3.1"
|
||||
tauri-plugin-log = "2.4.0"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
@@ -48,13 +48,18 @@ tauri-plugin-haptics = "2.2.4"
|
||||
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
rand = "0.8"
|
||||
cocoa = "0.25"
|
||||
objc = "0.2.7"
|
||||
rand = "0.8"
|
||||
objc-foundation = "0.1.1"
|
||||
objc_id = "0.1.1"
|
||||
block = "0.1.6"
|
||||
objc2 = "0.6"
|
||||
objc2-authentication-services = "0.3"
|
||||
objc2-foundation = { version = "0.3", features = ["NSError", "NSArray"] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-updater = "2.7.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
font-enumeration = "0.9.0"
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<false/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIHomeIndicatorAutoHidden</key>
|
||||
<true/>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>EPUB Document</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.idpf.epub-container</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>PDF Document</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.adobe.pdf</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>FB2 Document</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.readest.fb2</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>CBZ Archive</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.readest.cbz</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>MOBI Document</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.mobipocket.mobi</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>AZW Document</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.amazon.azw</string>
|
||||
<string>com.amazon.azw3</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Text File</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.plain-text</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.idpf.epub-container</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>EPUB Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.content</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>epub</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/epub+zip</string>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.readest.fb2</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>FB2 Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.xml</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>fb2</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.readest.cbz</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>CBZ Archive</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.archive</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>cbz</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/x-cbz</string>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.mobipocket.mobi</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>MOBI Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>mobi</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/x-mobipocket-ebook</string>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.amazon.azw</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>AZW Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>azw</string>
|
||||
<string>azw3</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/vnd.amazon.ebook</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "updater"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"fs:default",
|
||||
@@ -39,14 +39,37 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-cache-read",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$CACHEDIR/**/*"
|
||||
},
|
||||
{
|
||||
"path": "/private/var/mobile/Containers/Data/Application/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*.deepl.com"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/readest/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.cloudflarestorage.com"
|
||||
},
|
||||
{
|
||||
"url": "https://edge.microsoft.com"
|
||||
},
|
||||
{
|
||||
"url": "https://*.microsofttranslator.com"
|
||||
},
|
||||
{
|
||||
"url": "https://translate.googleapis.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -57,6 +80,7 @@
|
||||
"core:window:allow-center",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-maximized",
|
||||
@@ -64,6 +88,8 @@
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:webview:allow-create-webview-window",
|
||||
"core:path:allow-resolve-directory",
|
||||
"log:default",
|
||||
"shell:default",
|
||||
"process:default",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "desktop-capability",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "updater"],
|
||||
"platforms": ["linux", "macOS", "windows"],
|
||||
"permissions": [
|
||||
"updater:default",
|
||||
"cli:default"
|
||||
]
|
||||
"permissions": ["updater:default", "cli:default"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
@@ -19,12 +20,43 @@
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="oauth">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="readest" android:host="auth-callback" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
<data android:mimeType="application/fb2" />
|
||||
<data android:mimeType="application/x-fb2" />
|
||||
<data android:mimeType="application/vnd.amazon.mobi8-ebook" />
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
<data android:mimeType="application/x-cbz" />
|
||||
<data android:mimeType="application/x-mobipocket-ebook" />
|
||||
<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" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="oauth">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="readest" android:host="auth-callback" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
+71
-41
@@ -1,51 +1,81 @@
|
||||
package com.bilingify.readest
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import app.tauri.plugin.JSObject
|
||||
import com.readest.native_bridge.NativeBridgePlugin
|
||||
import android.view.KeyEvent
|
||||
import android.webkit.WebView
|
||||
import android.util.Log
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import com.readest.native_bridge.KeyDownInterceptor
|
||||
|
||||
class MainActivity : TauriActivity(), KeyDownInterceptor {
|
||||
private lateinit var wv: WebView
|
||||
private var interceptVolumeKeysEnabled = false
|
||||
private var interceptBackKeyEnabled = false
|
||||
|
||||
override fun onWebViewCreate(webView: WebView) {
|
||||
wv = webView
|
||||
}
|
||||
|
||||
private val keyEventMap = mapOf(
|
||||
KeyEvent.KEYCODE_BACK to "Back",
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN to "VolumeDown",
|
||||
KeyEvent.KEYCODE_VOLUME_UP to "VolumeUp"
|
||||
)
|
||||
|
||||
override fun interceptVolumeKeys(enabled: Boolean) {
|
||||
Log.d("MainActivity", "Intercept volume keys: $enabled")
|
||||
interceptVolumeKeysEnabled = enabled
|
||||
}
|
||||
|
||||
override fun interceptBackKey(enabled: Boolean) {
|
||||
Log.d("MainActivity", "Intercept back key: $enabled")
|
||||
interceptBackKeyEnabled = enabled
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val keyName = keyEventMap[keyCode]
|
||||
if (keyName != null) {
|
||||
wv.evaluateJavascript(
|
||||
"""
|
||||
try {
|
||||
window.onNativeKeyDown("$keyName", $keyCode)
|
||||
} catch (err) {
|
||||
false
|
||||
}
|
||||
""".trimIndent()
|
||||
) { result ->
|
||||
run {
|
||||
if (result.equals("true", ignoreCase = true)) {
|
||||
Log.d("Key Event", "Key event $keyName intercepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
return when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (interceptVolumeKeysEnabled) {
|
||||
true
|
||||
} else {
|
||||
super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_BACK -> {
|
||||
if (interceptBackKeyEnabled) {
|
||||
true
|
||||
} else {
|
||||
super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
else -> super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
hideSystemUI()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "readest" && uri.host == "auth-callback") {
|
||||
val result = JSObject().apply {
|
||||
put("redirectUrl", uri.toString())
|
||||
}
|
||||
|
||||
NativeBridgePlugin.pendingInvoke?.resolve(result)
|
||||
NativeBridgePlugin.pendingInvoke = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.decorView.windowInsetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:fitsSystemWindows">false</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 87 KiB |
@@ -17,3 +17,6 @@ schemars = "0.8"
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.4", features = ["build"] }
|
||||
schemars = "0.8"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
font-enumeration = "0.9.0"
|
||||
|
||||
+292
@@ -4,6 +4,21 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.KeyEvent
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowInsetsController
|
||||
import android.graphics.Color
|
||||
import android.webkit.WebView
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.fonts.SystemFonts
|
||||
import android.graphics.fonts.Font
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
@@ -11,6 +26,7 @@ import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import org.json.JSONArray
|
||||
import java.io.*
|
||||
|
||||
@InvokeArg
|
||||
@@ -24,6 +40,33 @@ class CopyURIRequestArgs {
|
||||
var dst: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class InstallPackageRequestArgs {
|
||||
var path: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class SetSystemUIVisibilityRequestArgs {
|
||||
var visible: Boolean? = false
|
||||
var darkMode: Boolean? = false
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class InterceptKeysRequestArgs {
|
||||
var volumeKeys: Boolean? = null
|
||||
var backKey: Boolean? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class LockScreenOrientationRequestArgs {
|
||||
var orientation: String? = null
|
||||
}
|
||||
|
||||
interface KeyDownInterceptor {
|
||||
fun interceptVolumeKeys(enabled: Boolean)
|
||||
fun interceptBackKey(enabled: Boolean)
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
private val implementation = NativeBridge()
|
||||
@@ -34,6 +77,40 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
var pendingInvoke: Invoke? = null
|
||||
}
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
super.load(webView)
|
||||
handleIntent(activity.intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
val uri = intent?.data ?: return
|
||||
Log.e("NativeBridgePlugin", "Received intent: $uri")
|
||||
when {
|
||||
uri.scheme == "readest" && uri.host == "auth-callback" -> {
|
||||
val result = JSObject().apply {
|
||||
put("redirectUrl", uri.toString())
|
||||
}
|
||||
pendingInvoke?.resolve(result)
|
||||
pendingInvoke = null
|
||||
}
|
||||
|
||||
intent.action == Intent.ACTION_VIEW -> {
|
||||
try {
|
||||
activity.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e("NativeBridgePlugin", "Failed to take persistable URI permission: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun auth_with_custom_tab(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(AuthRequestArgs::class.java)
|
||||
@@ -74,4 +151,219 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun install_package(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(InstallPackageRequestArgs::class.java)
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val file = File(args.path ?: "")
|
||||
if (file.exists()) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val apkUri = FileProvider.getUriForFile(activity, "${activity.packageName}.fileprovider", file)
|
||||
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val packageManager = activity.packageManager
|
||||
val resolveInfos = packageManager.queryIntentActivities(intent, 0)
|
||||
for (resolveInfo in resolveInfos) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
activity.grantUriPermission(packageName, apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
ret.put("success", true)
|
||||
} else {
|
||||
ret.put("success", false)
|
||||
ret.put("error", "File does not exist")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ret.put("success", false)
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_system_ui_visibility(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetSystemUIVisibilityRequestArgs::class.java)
|
||||
val visible = args.visible ?: false
|
||||
var isDarkMode = args.darkMode ?: false
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val window = activity.window
|
||||
val decorView = window.decorView
|
||||
if (!visible) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
val controller = window.insetsController
|
||||
if (controller != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
val compatController = WindowCompat.getInsetsController(window, decorView)
|
||||
compatController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
if (isDarkMode) {
|
||||
controller.setSystemBarsAppearance(
|
||||
0,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||
)
|
||||
} else {
|
||||
controller.setSystemBarsAppearance(
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||
)
|
||||
}
|
||||
if (visible) {
|
||||
controller.show(WindowInsets.Type.statusBars())
|
||||
controller.hide(WindowInsets.Type.navigationBars())
|
||||
} else {
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
}
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val compatController = WindowCompat.getInsetsController(window, decorView)
|
||||
compatController?.let {
|
||||
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
if (!isDarkMode) {
|
||||
it.isAppearanceLightStatusBars = true
|
||||
} else {
|
||||
it.isAppearanceLightStatusBars = false
|
||||
}
|
||||
if (visible) {
|
||||
it.show(WindowInsetsCompat.Type.statusBars())
|
||||
it.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
} else {
|
||||
it.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
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
|
||||
ret.put("success", true)
|
||||
} catch (e: Exception) {
|
||||
ret.put("success", false)
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_status_bar_height(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val resourceId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
val height = if (resourceId > 0) {
|
||||
activity.resources.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
ret.put("height", height)
|
||||
} catch (e: Exception) {
|
||||
ret.put("height", -1)
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_sys_fonts_list(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val fontList = mutableListOf<String>()
|
||||
val fontFileList = mutableListOf<String>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val systemFonts = SystemFonts.getAvailableFonts()
|
||||
for (font in systemFonts) {
|
||||
val file = font.getFile()?: continue
|
||||
if (file.isFile && (file.name.endsWith(".ttf", true) || file.name.endsWith(".otf", true))) {
|
||||
fontFileList.add(file.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val fontDirs = listOf("/system/fonts", "/system/font", "/data/fonts")
|
||||
for (dirPath in fontDirs) {
|
||||
val dir = File(dirPath)
|
||||
if (dir.exists() && dir.isDirectory) {
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isFile && (file.name.endsWith(".ttf", true) || file.name.endsWith(".otf", true))) {
|
||||
fontFileList.add(file.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (fileFileName in fontFileList) {
|
||||
var fontName = fileFileName
|
||||
.replace(Regex("\\.(ttf|otf)$", RegexOption.IGNORE_CASE), "")
|
||||
.trim()
|
||||
fontList.add(fontName)
|
||||
}
|
||||
var fontDict = JSObject()
|
||||
for (fontName in fontList) {
|
||||
fontDict.put(fontName, fontName)
|
||||
}
|
||||
ret.put("fonts", fontDict)
|
||||
} catch (e: Exception) {
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun intercept_keys(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(InterceptKeysRequestArgs::class.java)
|
||||
if (activity is KeyDownInterceptor) {
|
||||
when (args.backKey) {
|
||||
true -> (activity as KeyDownInterceptor).interceptBackKey(true)
|
||||
false -> (activity as KeyDownInterceptor).interceptBackKey(false)
|
||||
else -> {}
|
||||
}
|
||||
when (args.volumeKeys) {
|
||||
true -> (activity as KeyDownInterceptor).interceptVolumeKeys(true)
|
||||
false -> (activity as KeyDownInterceptor).interceptVolumeKeys(false)
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
Log.e("NativeBridgePlugin", "Activity does not implement KeyDownInterceptor")
|
||||
}
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun lock_screen_orientation(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(LockScreenOrientationRequestArgs::class.java)
|
||||
val orientation = args.orientation ?: "auto"
|
||||
when (orientation) {
|
||||
"portrait" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
"landscape" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
"auto" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
else -> {
|
||||
invoke.reject("Invalid orientation mode")
|
||||
return
|
||||
}
|
||||
}
|
||||
invoke.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ const COMMANDS: &[&str] = &[
|
||||
"auth_with_custom_tab",
|
||||
"copy_uri_to_path",
|
||||
"use_background_audio",
|
||||
"install_package",
|
||||
"set_system_ui_visibility",
|
||||
"get_status_bar_height",
|
||||
"get_sys_fonts_list",
|
||||
"intercept_keys",
|
||||
"lock_screen_orientation",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -4,29 +4,29 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-native-bridge",
|
||||
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-bridge",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-native-bridge"]),
|
||||
],
|
||||
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-bridge",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
name: "tauri-plugin-native-bridge",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v14),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-native-bridge",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-native-bridge"])
|
||||
],
|
||||
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-bridge",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
|
||||
+360
-8
@@ -1,10 +1,30 @@
|
||||
import AuthenticationServices
|
||||
import AVFoundation
|
||||
import AuthenticationServices
|
||||
import CoreText
|
||||
import MediaPlayer
|
||||
import ObjectiveC
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
import os
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NativeBridge")
|
||||
|
||||
func getLocalizedDisplayName(familyName: String) -> String? {
|
||||
let fontDescriptor = CTFontDescriptorCreateWithAttributes(
|
||||
[
|
||||
kCTFontFamilyNameAttribute: familyName
|
||||
] as CFDictionary)
|
||||
|
||||
let font = CTFontCreateWithFontDescriptor(fontDescriptor, 0.0, nil)
|
||||
|
||||
var actualLanguage: Unmanaged<CFString>?
|
||||
if let localizedName = CTFontCopyLocalizedName(font, kCTFontFamilyNameKey, &actualLanguage) {
|
||||
return localizedName as String
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
class SafariAuthRequestArgs: Decodable {
|
||||
let authUrl: String
|
||||
@@ -14,8 +34,207 @@ class UseBackgroundAudioRequestArgs: Decodable {
|
||||
let enabled: Bool
|
||||
}
|
||||
|
||||
class SetSystemUIVisibilityRequestArgs: Decodable {
|
||||
let visible: Bool
|
||||
let darkMode: Bool
|
||||
}
|
||||
|
||||
class InterceptKeysRequestArgs: Decodable {
|
||||
let backKey: Bool?
|
||||
let volumeKeys: Bool?
|
||||
}
|
||||
|
||||
class LockScreenOrientationRequestArgs: Decodable {
|
||||
let orientation: String?
|
||||
}
|
||||
|
||||
class VolumeKeyHandler: NSObject {
|
||||
private var audioSession: AVAudioSession?
|
||||
private var originalVolume: Float = 0.0
|
||||
private var referenceVolume: Float = 0.5
|
||||
private var previousVolume: Float = 0.5
|
||||
private var volumeView: MPVolumeView?
|
||||
private(set) var isIntercepting = false
|
||||
private var webView: WKWebView?
|
||||
private var volumeSlider: UISlider?
|
||||
|
||||
func startInterception(webView: WKWebView) {
|
||||
if isIntercepting {
|
||||
stopInterception()
|
||||
}
|
||||
|
||||
logger.log("Starting volume key interception")
|
||||
self.webView = webView
|
||||
isIntercepting = true
|
||||
|
||||
audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession?.setCategory(.playback, mode: .default, options: [.mixWithOthers])
|
||||
try audioSession?.setActive(true)
|
||||
} catch {
|
||||
logger.error("Failed to activate audio session: \(error)")
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.originalVolume = self.audioSession?.outputVolume ?? 0.1
|
||||
if self.originalVolume > 0.9 {
|
||||
self.referenceVolume = 0.9
|
||||
} else if self.originalVolume < 0.1 {
|
||||
self.referenceVolume = 0.1
|
||||
} else {
|
||||
self.referenceVolume = self.originalVolume
|
||||
}
|
||||
logger.log("Reference volume set to \(self.referenceVolume)")
|
||||
self.previousVolume = self.referenceVolume
|
||||
self.setSessionVolume(self.referenceVolume)
|
||||
self.setupHiddenVolumeView()
|
||||
self.audioSession?.addObserver(
|
||||
self, forKeyPath: "outputVolume", options: [.new], context: nil)
|
||||
}
|
||||
|
||||
audioSession?.addObserver(self, forKeyPath: "outputVolume", options: [.new], context: nil)
|
||||
}
|
||||
|
||||
func stopInterception() {
|
||||
if !isIntercepting {
|
||||
return
|
||||
}
|
||||
|
||||
logger.log("Stopping volume key interception")
|
||||
isIntercepting = false
|
||||
audioSession?.removeObserver(self, forKeyPath: "outputVolume")
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.setSessionVolume(self?.originalVolume ?? 0.1)
|
||||
self?.volumeView?.removeFromSuperview()
|
||||
self?.volumeView = nil
|
||||
self?.volumeSlider = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setSessionVolume(_ volume: Float) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.volumeSlider?.value = volume
|
||||
}
|
||||
}
|
||||
|
||||
private func setupHiddenVolumeView() {
|
||||
assert(Thread.isMainThread, "setupHiddenVolumeView must be called on main thread")
|
||||
let frame = CGRect(x: -1000, y: -1000, width: 1, height: 1)
|
||||
volumeView = MPVolumeView(frame: frame)
|
||||
volumeSlider = volumeView?.subviews.first(where: { $0 is UISlider }) as? UISlider
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
window.addSubview(volumeView!)
|
||||
}
|
||||
}
|
||||
|
||||
override func observeValue(
|
||||
forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?,
|
||||
context: UnsafeMutableRawPointer?
|
||||
) {
|
||||
if keyPath == "outputVolume", let audioSession = self.audioSession, isIntercepting {
|
||||
let currentVolume = audioSession.outputVolume
|
||||
if currentVolume > self.previousVolume {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.webView?.evaluateJavaScript(
|
||||
"window.onNativeKeyDown('VolumeUp');", completionHandler: nil)
|
||||
}
|
||||
} else if currentVolume < self.previousVolume {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.webView?.evaluateJavaScript(
|
||||
"window.onNativeKeyDown('VolumeDown');", completionHandler: nil)
|
||||
}
|
||||
}
|
||||
self.previousVolume = currentVolume
|
||||
self.setSessionVolume(self.referenceVolume)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NativeBridgePlugin: Plugin {
|
||||
private var webView: WKWebView?
|
||||
private var authSession: ASWebAuthenticationSession?
|
||||
private var isOrientationLocked = false
|
||||
private var currentOrientationMask: UIInterfaceOrientationMask = .all
|
||||
private var orientationObserver: NSObjectProtocol?
|
||||
|
||||
@objc public override func load(webview: WKWebView) {
|
||||
self.webView = webview
|
||||
logger.log("NativeBridgePlugin loaded")
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appDidBecomeActive),
|
||||
name: UIApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appDidEnterBackground),
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc func appDidBecomeActive() {
|
||||
if volumeKeyHandler != nil {
|
||||
activateVolumeKeyInterception()
|
||||
}
|
||||
|
||||
if orientationObserver != nil {
|
||||
self.setupOrientationObserver()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func appDidEnterBackground() {
|
||||
if let handler = volumeKeyHandler, handler.isIntercepting {
|
||||
handler.stopInterception()
|
||||
}
|
||||
}
|
||||
|
||||
func activateVolumeKeyInterception() {
|
||||
if volumeKeyHandler == nil {
|
||||
volumeKeyHandler = VolumeKeyHandler()
|
||||
}
|
||||
|
||||
if let webView = self.webView {
|
||||
volumeKeyHandler?.stopInterception()
|
||||
volumeKeyHandler?.startInterception(webView: webView)
|
||||
} else {
|
||||
logger.warning("Cannot activate volume key interception: webView is nil")
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private struct AssociatedKeys {
|
||||
static var volumeKeyHandler = "volumeKeyHandler"
|
||||
static var interceptingVolumeKeys = "interceptingVolumeKeys"
|
||||
}
|
||||
|
||||
private var volumeKeyHandler: VolumeKeyHandler? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.volumeKeyHandler) as? VolumeKeyHandler
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(
|
||||
self, &AssociatedKeys.volumeKeyHandler, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var interceptingVolumeKeys: Bool {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.interceptingVolumeKeys) as? Bool
|
||||
?? false
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(
|
||||
self, &AssociatedKeys.interceptingVolumeKeys, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func use_background_audio(_ invoke: Invoke) {
|
||||
do {
|
||||
@@ -25,15 +244,15 @@ class NativeBridgePlugin: Plugin {
|
||||
if enabled {
|
||||
try session.setCategory(.playback, mode: .default, options: [.mixWithOthers])
|
||||
try session.setActive(true)
|
||||
print("AVAudioSession activated")
|
||||
logger.log("AVAudioSession activated")
|
||||
} else {
|
||||
try session.setActive(false)
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
print("AVAudioSession deactivated")
|
||||
logger.log("AVAudioSession deactivated")
|
||||
}
|
||||
invoke.resolve()
|
||||
} catch {
|
||||
print("Failed to set up audio session:", error)
|
||||
logger.error("Failed to set up audio session: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +265,12 @@ class NativeBridgePlugin: Plugin {
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.error("Auth session error: \(error.localizedDescription)")
|
||||
logger.error("Auth session error: \(error.localizedDescription)")
|
||||
invoke.reject(error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
if let callbackURL = callbackURL {
|
||||
Logger.info("Auth session callback URL: \(callbackURL.absoluteString)")
|
||||
strongSelf.authSession?.cancel()
|
||||
strongSelf.authSession = nil
|
||||
invoke.resolve(["redirectUrl": callbackURL.absoluteString])
|
||||
@@ -64,7 +282,141 @@ class NativeBridgePlugin: Plugin {
|
||||
}
|
||||
|
||||
let started = authSession?.start() ?? false
|
||||
Logger.info("Auth session start result: \(started)")
|
||||
logger.log("Auth session start result: \(started)")
|
||||
}
|
||||
|
||||
@objc public func set_system_ui_visibility(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(SetSystemUIVisibilityRequestArgs.self)
|
||||
let visible = args.visible
|
||||
let darkMode = args.darkMode
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.isIdleTimerDisabled = !visible
|
||||
UIApplication.shared.setStatusBarHidden(!visible, with: .none)
|
||||
|
||||
let windows = UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap { $0.windows }
|
||||
|
||||
let keyWindow = windows.first(where: { $0.isKeyWindow }) ?? windows.first
|
||||
if let keyWindow = keyWindow {
|
||||
keyWindow.overrideUserInterfaceStyle = darkMode ? .dark : .light
|
||||
keyWindow.layoutIfNeeded()
|
||||
} else {
|
||||
logger.error("No key window found")
|
||||
}
|
||||
}
|
||||
invoke.resolve(["success": true])
|
||||
}
|
||||
|
||||
@objc public func get_sys_fonts_list(_ invoke: Invoke) throws {
|
||||
var fontDict: [String: String] = [:]
|
||||
|
||||
for family in UIFont.familyNames.sorted() {
|
||||
if let localized = getLocalizedDisplayName(familyName: family) {
|
||||
fontDict[family] = localized
|
||||
} else {
|
||||
let fontNames = UIFont.fontNames(forFamilyName: family)
|
||||
if fontNames.isEmpty {
|
||||
fontDict[family] = family
|
||||
} else {
|
||||
for fontName in fontNames {
|
||||
fontDict[fontName] = family
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invoke.resolve(["fonts": fontDict])
|
||||
}
|
||||
|
||||
@objc public func intercept_keys(_ invoke: Invoke) {
|
||||
do {
|
||||
let args = try invoke.parseArgs(InterceptKeysRequestArgs.self)
|
||||
|
||||
if let volumeKeys = args.volumeKeys {
|
||||
if volumeKeys {
|
||||
self.activateVolumeKeyInterception()
|
||||
} else {
|
||||
self.volumeKeyHandler?.stopInterception()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.volumeKeyHandler = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
invoke.resolve()
|
||||
} catch {
|
||||
invoke.reject(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func lock_screen_orientation(_ invoke: Invoke) throws {
|
||||
guard let args = try? invoke.parseArgs(LockScreenOrientationRequestArgs.self) else {
|
||||
return invoke.reject("Invalid arguments")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
let orientation = args.orientation ?? "auto"
|
||||
switch orientation.lowercased() {
|
||||
case "portrait":
|
||||
self.isOrientationLocked = true
|
||||
self.currentOrientationMask = .portrait
|
||||
self.forceOrientation(.portrait)
|
||||
self.setupOrientationObserver()
|
||||
case "landscape":
|
||||
self.isOrientationLocked = true
|
||||
self.currentOrientationMask = .landscape
|
||||
self.forceOrientation(.landscapeRight)
|
||||
self.setupOrientationObserver()
|
||||
case "auto":
|
||||
self.isOrientationLocked = false
|
||||
self.currentOrientationMask = .all
|
||||
default:
|
||||
invoke.reject("Invalid orientation mode")
|
||||
return
|
||||
}
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private func forceOrientation(_ orientation: UIInterfaceOrientation) {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,4 +430,4 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding {
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return UIApplication.shared.windows.first ?? UIWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-status-bar-height"
|
||||
description = "Enables the get_status_bar_height command without any pre-configured scope."
|
||||
commands.allow = ["get_status_bar_height"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-status-bar-height"
|
||||
description = "Denies the get_status_bar_height command without any pre-configured scope."
|
||||
commands.deny = ["get_status_bar_height"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-sys-fonts-list"
|
||||
description = "Enables the get_sys_fonts_list command without any pre-configured scope."
|
||||
commands.allow = ["get_sys_fonts_list"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-sys-fonts-list"
|
||||
description = "Denies the get_sys_fonts_list command without any pre-configured scope."
|
||||
commands.deny = ["get_sys_fonts_list"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-install-package"
|
||||
description = "Enables the install_package command without any pre-configured scope."
|
||||
commands.allow = ["install_package"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-install-package"
|
||||
description = "Denies the install_package command without any pre-configured scope."
|
||||
commands.deny = ["install_package"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-intercept-keys"
|
||||
description = "Enables the intercept_keys command without any pre-configured scope."
|
||||
commands.allow = ["intercept_keys"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-intercept-keys"
|
||||
description = "Denies the intercept_keys command without any pre-configured scope."
|
||||
commands.deny = ["intercept_keys"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-lock-screen-orientation"
|
||||
description = "Enables the lock_screen_orientation command without any pre-configured scope."
|
||||
commands.allow = ["lock_screen_orientation"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-lock-screen-orientation"
|
||||
description = "Denies the lock_screen_orientation command without any pre-configured scope."
|
||||
commands.deny = ["lock_screen_orientation"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-system-ui-visibility"
|
||||
description = "Enables the set_system_ui_visibility command without any pre-configured scope."
|
||||
commands.allow = ["set_system_ui_visibility"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-system-ui-visibility"
|
||||
description = "Denies the set_system_ui_visibility command without any pre-configured scope."
|
||||
commands.deny = ["set_system_ui_visibility"]
|
||||
+162
@@ -8,6 +8,12 @@ Default permissions for the plugin
|
||||
- `allow-auth-with-custom-tab`
|
||||
- `allow-copy-uri-to-path`
|
||||
- `allow-use-background-audio`
|
||||
- `allow-install-package`
|
||||
- `allow-set-system-ui-visibility`
|
||||
- `allow-get-status-bar-height`
|
||||
- `allow-get-sys-fonts-list`
|
||||
- `allow-intercept-keys`
|
||||
- `allow-lock-screen-orientation`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -99,6 +105,162 @@ Denies the copy_uri_to_path command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-status-bar-height`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_status_bar_height command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-status-bar-height`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_status_bar_height command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-sys-fonts-list`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_sys_fonts_list command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-sys-fonts-list`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_sys_fonts_list command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-install-package`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the install_package command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-install-package`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the install_package command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-intercept-keys`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the intercept_keys command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-intercept-keys`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the intercept_keys command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-lock-screen-orientation`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the lock_screen_orientation command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-lock-screen-orientation`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the lock_screen_orientation command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-set-system-ui-visibility`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_system_ui_visibility command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-set-system-ui-visibility`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_system_ui_visibility command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-use-background-audio`
|
||||
|
||||
</td>
|
||||
|
||||
+12
-1
@@ -1,3 +1,14 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = ["allow-auth-with-safari", "allow-auth-with-custom-tab", "allow-copy-uri-to-path", "allow-use-background-audio"]
|
||||
permissions = [
|
||||
"allow-auth-with-safari",
|
||||
"allow-auth-with-custom-tab",
|
||||
"allow-copy-uri-to-path",
|
||||
"allow-use-background-audio",
|
||||
"allow-install-package",
|
||||
"allow-set-system-ui-visibility",
|
||||
"allow-get-status-bar-height",
|
||||
"allow-get-sys-fonts-list",
|
||||
"allow-intercept-keys",
|
||||
"allow-lock-screen-orientation",
|
||||
]
|
||||
|
||||
+74
-2
@@ -330,6 +330,78 @@
|
||||
"const": "deny-copy-uri-to-path",
|
||||
"markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_status_bar_height command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-status-bar-height",
|
||||
"markdownDescription": "Enables the get_status_bar_height command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_status_bar_height command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-status-bar-height",
|
||||
"markdownDescription": "Denies the get_status_bar_height command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_sys_fonts_list command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-sys-fonts-list",
|
||||
"markdownDescription": "Enables the get_sys_fonts_list command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_sys_fonts_list command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-sys-fonts-list",
|
||||
"markdownDescription": "Denies the get_sys_fonts_list command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the install_package command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-install-package",
|
||||
"markdownDescription": "Enables the install_package command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the install_package command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-install-package",
|
||||
"markdownDescription": "Denies the install_package command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the intercept_keys command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-intercept-keys",
|
||||
"markdownDescription": "Enables the intercept_keys command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the intercept_keys command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-intercept-keys",
|
||||
"markdownDescription": "Denies the intercept_keys command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the lock_screen_orientation command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-lock-screen-orientation",
|
||||
"markdownDescription": "Enables the lock_screen_orientation command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the lock_screen_orientation command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-lock-screen-orientation",
|
||||
"markdownDescription": "Denies the lock_screen_orientation command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-system-ui-visibility",
|
||||
"markdownDescription": "Enables the set_system_ui_visibility command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_system_ui_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-system-ui-visibility",
|
||||
"markdownDescription": "Denies the set_system_ui_visibility command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the use_background_audio command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -343,10 +415,10 @@
|
||||
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`",
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,3 +35,49 @@ pub(crate) async fn use_background_audio<R: Runtime>(
|
||||
) -> Result<()> {
|
||||
app.native_bridge().use_background_audio(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn install_package<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: InstallPackageRequest,
|
||||
) -> Result<InstallPackageResponse> {
|
||||
app.native_bridge().install_package(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn set_system_ui_visibility<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: SetSystemUIVisibilityRequest,
|
||||
) -> Result<SetSystemUIVisibilityResponse> {
|
||||
app.native_bridge().set_system_ui_visibility(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_status_bar_height<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetStatusBarHeightResponse> {
|
||||
app.native_bridge().get_status_bar_height()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_sys_fonts_list<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetSysFontsListResponse> {
|
||||
app.native_bridge().get_sys_fonts_list()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn intercept_keys<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: InterceptKeysRequest,
|
||||
) -> Result<()> {
|
||||
app.native_bridge().intercept_keys(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn lock_screen_orientation<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: LockScreenOrientationRequest,
|
||||
) -> Result<()> {
|
||||
app.native_bridge().lock_screen_orientation(payload)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashMap;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
@@ -29,4 +30,47 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn use_background_audio(&self, _payload: UseBackgroundAudioRequest) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn install_package(
|
||||
&self,
|
||||
_payload: InstallPackageRequest,
|
||||
) -> crate::Result<InstallPackageResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn set_system_ui_visibility(
|
||||
&self,
|
||||
_payload: SetSystemUIVisibilityRequest,
|
||||
) -> crate::Result<SetSystemUIVisibilityResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn get_status_bar_height(&self) -> crate::Result<GetStatusBarHeightResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn get_sys_fonts_list(&self) -> crate::Result<GetSysFontsListResponse> {
|
||||
let font_collection = font_enumeration::Collection::new().unwrap();
|
||||
let mut fonts = HashMap::new();
|
||||
for font in font_collection.all() {
|
||||
if cfg!(target_os = "windows") {
|
||||
// FIXME: temporarily disable font name with style for windows
|
||||
fonts.insert(font.family_name.clone(), font.family_name.clone());
|
||||
} else {
|
||||
fonts.insert(font.font_name.clone(), font.family_name.clone());
|
||||
}
|
||||
}
|
||||
Ok(GetSysFontsListResponse { fonts, error: None })
|
||||
}
|
||||
|
||||
pub fn intercept_keys(&self, _payload: InterceptKeysRequest) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn lock_screen_orientation(
|
||||
&self,
|
||||
_payload: LockScreenOrientationRequest,
|
||||
) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::auth_with_custom_tab,
|
||||
commands::copy_uri_to_path,
|
||||
commands::use_background_audio,
|
||||
commands::install_package,
|
||||
commands::set_system_ui_visibility,
|
||||
commands::get_status_bar_height,
|
||||
commands::get_sys_fonts_list,
|
||||
commands::intercept_keys,
|
||||
commands::lock_screen_orientation,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -55,3 +55,60 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn install_package(
|
||||
&self,
|
||||
payload: InstallPackageRequest,
|
||||
) -> crate::Result<InstallPackageResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("install_package", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn set_system_ui_visibility(
|
||||
&self,
|
||||
payload: SetSystemUIVisibilityRequest,
|
||||
) -> crate::Result<SetSystemUIVisibilityResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_system_ui_visibility", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_status_bar_height(&self) -> crate::Result<GetStatusBarHeightResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_status_bar_height", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_sys_fonts_list(&self) -> crate::Result<GetSysFontsListResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_sys_fonts_list", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn intercept_keys(&self, payload: InterceptKeysRequest) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("intercept_keys", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn lock_screen_orientation(
|
||||
&self,
|
||||
payload: LockScreenOrientationRequest,
|
||||
) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("lock_screen_orientation", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -31,3 +32,57 @@ pub struct CopyURIResponse {
|
||||
pub struct UseBackgroundAudioRequest {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstallPackageRequest {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstallPackageResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetSystemUIVisibilityRequest {
|
||||
pub visible: bool,
|
||||
pub dark_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetSystemUIVisibilityResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetStatusBarHeightResponse {
|
||||
pub height: u32,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSysFontsListResponse {
|
||||
pub fonts: HashMap<String, String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InterceptKeysRequest {
|
||||
pub volume_keys: Option<bool>,
|
||||
pub back_key: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LockScreenOrientationRequest {
|
||||
pub orientation: String,
|
||||
}
|
||||
|
||||
@@ -6,13 +6,6 @@ extern crate cocoa;
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod traffic_light;
|
||||
#[cfg(target_os = "macos")]
|
||||
use traffic_light::set_traffic_lights;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
|
||||
@@ -23,6 +16,8 @@ use tauri::{AppHandle, Listener, Manager, Url};
|
||||
#[cfg(desktop)]
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod transfer_file;
|
||||
use tauri::{command, Emitter, WebviewUrl, WebviewWindowBuilder, Window};
|
||||
use tauri_plugin_oauth::start;
|
||||
@@ -76,7 +71,10 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
let files = files
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let file = f.to_string_lossy().replace("\\", "\\\\");
|
||||
let file = f
|
||||
.to_string_lossy()
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"");
|
||||
format!("\"{file}\"",)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -107,17 +105,6 @@ async fn start_server(window: Window) -> Result<u16, String> {
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[command]
|
||||
async fn list_fonts() -> Result<Vec<String>, String> {
|
||||
let font_collection = font_enumeration::Collection::new().unwrap();
|
||||
let mut fonts = Vec::new();
|
||||
for font in font_collection.all() {
|
||||
fonts.push(font.family_name.clone());
|
||||
}
|
||||
Ok(fonts)
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Payload {
|
||||
@@ -135,9 +122,11 @@ pub fn run() {
|
||||
download_file,
|
||||
upload_file,
|
||||
#[cfg(target_os = "macos")]
|
||||
set_traffic_lights,
|
||||
#[cfg(desktop)]
|
||||
list_fonts
|
||||
macos::safari_auth::auth_with_safari,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::apple_auth::start_apple_sign_in,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::traffic_light::set_traffic_lights,
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
@@ -170,7 +159,10 @@ pub fn run() {
|
||||
let builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.plugin(traffic_light::init());
|
||||
let builder = builder.plugin(macos::traffic_light::init());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.plugin(macos::safari_auth::init());
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
let builder = builder.plugin(tauri_plugin_sign_in_with_apple::init());
|
||||
@@ -199,36 +191,70 @@ pub fn run() {
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
app.listen("window-ready", move |_| {
|
||||
app_handle.get_webview_window("main").unwrap()
|
||||
.eval("window.__READEST_CLI_ACCESS = true; window.__READEST_UPDATER_ACCESS = true;")
|
||||
let webview = app_handle.get_webview_window("main").unwrap();
|
||||
webview
|
||||
.eval("window.__READEST_CLI_ACCESS = true;")
|
||||
.expect("Failed to set cli access config");
|
||||
|
||||
set_rounded_window(&app_handle, true);
|
||||
#[cfg(target_os = "windows")]
|
||||
if tauri_plugin_os::version() <= tauri_plugin_os::Version::from_string("10.0.19045") {
|
||||
if tauri_plugin_os::version()
|
||||
<= tauri_plugin_os::Version::from_string("10.0.19045")
|
||||
{
|
||||
set_rounded_window(&app_handle, false);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let is_appimage = std::env::var("APPIMAGE").is_ok()
|
||||
|| std::env::current_exe()
|
||||
.map(|path| path.to_string_lossy().contains("/tmp/.mount_"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let script =
|
||||
format!("window.__READEST_UPDATER_DISABLED = {};", !is_appimage);
|
||||
webview
|
||||
.eval(&script)
|
||||
.expect("Failed to set updater disabled config");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
}
|
||||
|
||||
app.handle().plugin(
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
fn has_xdg_mime() -> bool {
|
||||
std::process::Command::new("which")
|
||||
.arg("xdg-mime")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
if has_xdg_mime() {
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
} else {
|
||||
println!("xdg-mime not found; skipping deep link setup on Linux.");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
) {
|
||||
eprintln!("Failed to initialize tauri_plugin_log: {}", e);
|
||||
};
|
||||
|
||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let win_builder = win_builder
|
||||
.inner_size(800.0, 600.0)
|
||||
.resizable(true);
|
||||
let win_builder = win_builder.inner_size(800.0, 600.0).resizable(true);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win_builder = win_builder
|
||||
@@ -245,7 +271,9 @@ pub fn run() {
|
||||
.title("Readest");
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
if tauri_plugin_os::version() <= tauri_plugin_os::Version::from_string("10.0.19045") {
|
||||
if tauri_plugin_os::version()
|
||||
<= tauri_plugin_os::Version::from_string("10.0.19045")
|
||||
{
|
||||
win_builder = win_builder.shadow(false);
|
||||
} else {
|
||||
win_builder = win_builder.shadow(true);
|
||||
@@ -262,7 +290,7 @@ pub fn run() {
|
||||
// win.open_devtools();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
menu::setup_macos_menu(app.handle())?;
|
||||
macos::menu::setup_macos_menu(app.handle())?;
|
||||
|
||||
app.handle().emit("window-ready", ()).unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2::{
|
||||
define_class, msg_send, AllocAnyThread, DefinedClass, MainThreadMarker, MainThreadOnly,
|
||||
};
|
||||
use objc2_authentication_services::{
|
||||
ASAuthorization, ASAuthorizationAppleIDCredential, ASAuthorizationAppleIDProvider,
|
||||
ASAuthorizationController, ASAuthorizationControllerDelegate, ASAuthorizationRequest,
|
||||
ASAuthorizationScopeEmail, ASAuthorizationScopeFullName,
|
||||
};
|
||||
use objc2_foundation::{NSArray, NSError, NSObject, NSObjectProtocol, NSString};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use tauri::{command, AppHandle, Emitter};
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppleIDAuthorizationRequest {
|
||||
pub scope: Vec<String>,
|
||||
pub nonce: Option<String>,
|
||||
pub state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppleIDAuthorizationResponse {
|
||||
pub user_identifier: Option<String>,
|
||||
pub given_name: Option<String>,
|
||||
pub family_name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub authorization_code: Option<String>,
|
||||
pub identity_token: Option<String>,
|
||||
pub state: Option<String>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static APPLE_SIGN_IN_DELEGATE: RefCell<Option<Retained<ASAuthorizationControllerDelegateImpl>>> = RefCell::new(None);
|
||||
static AUTHORIZATION_CONTROLLER: RefCell<Option<Retained<ASAuthorizationController>>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ivars {
|
||||
app: AppHandle,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[name = "ASAuthorizationControllerDelegateImpl"]
|
||||
#[ivars = Ivars]
|
||||
pub struct ASAuthorizationControllerDelegateImpl;
|
||||
|
||||
unsafe impl NSObjectProtocol for ASAuthorizationControllerDelegateImpl {}
|
||||
|
||||
unsafe impl ASAuthorizationControllerDelegate for ASAuthorizationControllerDelegateImpl {
|
||||
#[unsafe(method(authorizationController:didCompleteWithAuthorization:))]
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn authorizationController_didCompleteWithAuthorization(
|
||||
&self,
|
||||
_controller: &ASAuthorizationController,
|
||||
authorization: &ASAuthorization,
|
||||
) {
|
||||
if let Ok(credential) = authorization
|
||||
.credential()
|
||||
.downcast::<ASAuthorizationAppleIDCredential>()
|
||||
{
|
||||
let user_identifier = {
|
||||
let user = credential.user();
|
||||
if user.len() > 0 {
|
||||
Some(user.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let given_name = credential
|
||||
.fullName()
|
||||
.and_then(|name| name.givenName())
|
||||
.map(|nsstr| nsstr.to_string());
|
||||
let family_name = credential
|
||||
.fullName()
|
||||
.and_then(|name| name.familyName())
|
||||
.map(|nsstr| nsstr.to_string());
|
||||
let email = credential.email().map(|nsstr| nsstr.to_string());
|
||||
|
||||
let authorization_code = credential
|
||||
.authorizationCode()
|
||||
.and_then(|code_data| String::from_utf8(code_data.to_vec()).ok());
|
||||
|
||||
let identity_token = credential
|
||||
.identityToken()
|
||||
.and_then(|token_data| String::from_utf8(token_data.to_vec()).ok());
|
||||
|
||||
let state = credential.state().map(|nsstr| nsstr.to_string());
|
||||
|
||||
let resp = AppleIDAuthorizationResponse {
|
||||
user_identifier,
|
||||
given_name,
|
||||
family_name,
|
||||
email,
|
||||
authorization_code,
|
||||
identity_token,
|
||||
state,
|
||||
};
|
||||
let _ = self.ivars().app.emit("apple-sign-in-complete", json!(resp));
|
||||
log::info!("Apple authorization complete");
|
||||
} else {
|
||||
let _ = self
|
||||
.ivars()
|
||||
.app
|
||||
.emit("apple-sign-in-error", "Invalid credential type");
|
||||
log::error!("Invalid credential type received");
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(method(authorizationController:didCompleteWithError:))]
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn authorizationController_didCompleteWithError(
|
||||
&self,
|
||||
_controller: &ASAuthorizationController,
|
||||
error: &NSError,
|
||||
) {
|
||||
let code = error.code();
|
||||
let description = error.localizedDescription().to_string();
|
||||
let _ = self.ivars().app.emit(
|
||||
"apple-sign-in-error",
|
||||
json!({
|
||||
"code": code,
|
||||
"description": description,
|
||||
}),
|
||||
);
|
||||
|
||||
log::error!(
|
||||
"Authorization error: code={}, description={}",
|
||||
code,
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl ASAuthorizationControllerDelegateImpl {
|
||||
fn new(app: AppHandle) -> Retained<Self> {
|
||||
let mtm = MainThreadMarker::new().expect("must run on main thread");
|
||||
let this = Self::alloc(mtm).set_ivars(Ivars { app });
|
||||
unsafe { msg_send![super(this), init] }
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn start_apple_sign_in(app: AppHandle, payload: AppleIDAuthorizationRequest) {
|
||||
unsafe {
|
||||
let provider = ASAuthorizationAppleIDProvider::new();
|
||||
let request = provider.createRequest();
|
||||
|
||||
if let Some(ref nonce) = payload.nonce {
|
||||
let ns_nonce = NSString::from_str(nonce);
|
||||
request.setNonce(Some(&ns_nonce));
|
||||
}
|
||||
|
||||
if let Some(ref state) = payload.state {
|
||||
let ns_state = NSString::from_str(state);
|
||||
request.setState(Some(&ns_state));
|
||||
}
|
||||
|
||||
let mut scopes = Vec::new();
|
||||
for scope in payload.scope.iter() {
|
||||
match scope.as_str() {
|
||||
"email" => scopes.push(ASAuthorizationScopeEmail),
|
||||
"fullName" => scopes.push(ASAuthorizationScopeFullName),
|
||||
_ => {
|
||||
log::warn!("[Apple Sign-In] Unsupported scope: {}", scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !scopes.is_empty() {
|
||||
request.setRequestedScopes(Some(&*NSArray::from_slice(&scopes)));
|
||||
}
|
||||
|
||||
let auth_request = &request as &ASAuthorizationRequest;
|
||||
let controller = ASAuthorizationController::initWithAuthorizationRequests(
|
||||
ASAuthorizationController::alloc(),
|
||||
&*NSArray::from_slice(&[auth_request]),
|
||||
);
|
||||
|
||||
let delegate = ASAuthorizationControllerDelegateImpl::new(app.clone());
|
||||
APPLE_SIGN_IN_DELEGATE.with(|cell| {
|
||||
*cell.borrow_mut() = Some(delegate.clone());
|
||||
});
|
||||
|
||||
AUTHORIZATION_CONTROLLER.with(|cell| {
|
||||
*cell.borrow_mut() = Some(controller.clone());
|
||||
});
|
||||
|
||||
controller.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
||||
|
||||
log::info!("Starting Apple Sign-In authorization request");
|
||||
controller.performRequests();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod apple_auth;
|
||||
pub mod menu;
|
||||
pub mod safari_auth;
|
||||
pub mod traffic_light;
|
||||
@@ -0,0 +1,255 @@
|
||||
use std::ffi::CStr;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use block::ConcreteBlock;
|
||||
use objc::declare::ClassDecl;
|
||||
use objc::runtime::{Class, Object, Sel, BOOL, YES};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use objc_foundation::{INSString, NSString};
|
||||
use objc_id::Id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use tauri::{
|
||||
command,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Emitter, Manager, Runtime, State, Window,
|
||||
};
|
||||
|
||||
pub struct ThreadSafeObjcPointer(*mut Object, PhantomData<*mut Object>);
|
||||
|
||||
unsafe impl Send for ThreadSafeObjcPointer {}
|
||||
unsafe impl Sync for ThreadSafeObjcPointer {}
|
||||
|
||||
impl ThreadSafeObjcPointer {
|
||||
pub fn new(ptr: *mut Object) -> Self {
|
||||
Self(ptr, PhantomData)
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *mut Object {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.0.is_null()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthSession {
|
||||
auth_session: Option<ThreadSafeObjcPointer>,
|
||||
}
|
||||
|
||||
impl Default for AuthSession {
|
||||
fn default() -> Self {
|
||||
Self { auth_session: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AuthSession {
|
||||
fn drop(&mut self) {
|
||||
if let Some(session) = &self.auth_session {
|
||||
if !session.is_null() {
|
||||
unsafe {
|
||||
let _: () = msg_send![session.as_ptr(), cancel];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SafariAuthRequestArgs {
|
||||
auth_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthResult {
|
||||
redirect_url: String,
|
||||
}
|
||||
|
||||
fn setup_presentation_context_provider() -> &'static Class {
|
||||
let class_name = "TauriPresentationContextProvider";
|
||||
let mut decl = match Class::get(class_name) {
|
||||
Some(class) => return class,
|
||||
None => ClassDecl::new(class_name, class!(NSObject)).unwrap(),
|
||||
};
|
||||
|
||||
extern "C" fn presentation_anchor_for_web_auth_session(
|
||||
this: &Object,
|
||||
_sel: Sel,
|
||||
_session: *mut Object,
|
||||
) -> *mut Object {
|
||||
unsafe {
|
||||
let window: *mut Object = *this.get_ivar("window");
|
||||
window
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
decl.add_ivar::<*mut Object>("window");
|
||||
|
||||
let sel = sel!(presentationAnchorForWebAuthenticationSession:);
|
||||
decl.add_method(
|
||||
sel,
|
||||
presentation_anchor_for_web_auth_session
|
||||
as extern "C" fn(&Object, Sel, *mut Object) -> *mut Object,
|
||||
);
|
||||
}
|
||||
|
||||
decl.register()
|
||||
}
|
||||
|
||||
unsafe fn create_provider(window: *mut Object) -> *mut Object {
|
||||
let class = setup_presentation_context_provider();
|
||||
let provider: *mut Object = msg_send![class, alloc];
|
||||
let provider: *mut Object = msg_send![provider, init];
|
||||
|
||||
(*provider).set_ivar("window", window);
|
||||
|
||||
provider
|
||||
}
|
||||
|
||||
unsafe fn nsstring_to_string(ns_string: *mut Object) -> String {
|
||||
if ns_string.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let utf8_ptr: *const i8 = msg_send![ns_string, UTF8String];
|
||||
if utf8_ptr.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
CStr::from_ptr(utf8_ptr).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn auth_with_safari<R: Runtime>(
|
||||
window: Window<R>,
|
||||
state: State<'_, Arc<Mutex<AuthSession>>>,
|
||||
payload: SafariAuthRequestArgs,
|
||||
) -> Result<AuthResult, String> {
|
||||
let auth_url_str = NSString::from_str(&payload.auth_url);
|
||||
let url_class = Class::get("NSURL").unwrap();
|
||||
let auth_url: Id<Object> = unsafe {
|
||||
let url: *mut Object = msg_send![url_class, URLWithString:auth_url_str];
|
||||
if url.is_null() {
|
||||
return Err("Failed to create URL".to_string());
|
||||
}
|
||||
Id::from_ptr(url)
|
||||
};
|
||||
|
||||
let auth_session_class = match Class::get("ASWebAuthenticationSession") {
|
||||
Some(class) => class,
|
||||
None => {
|
||||
return Err(
|
||||
"ASWebAuthenticationSession class not found. This requires macOS 10.15+"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let window_clone = window.clone();
|
||||
let state_clone = Arc::clone(&*state);
|
||||
let completion_block =
|
||||
ConcreteBlock::new(move |callback_url: *mut Object, error: *mut Object| {
|
||||
let mut auth_session = match state_clone.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if !error.is_null() {
|
||||
unsafe {
|
||||
let description: *mut Object = msg_send![error, localizedDescription];
|
||||
let error_description = nsstring_to_string(description);
|
||||
log::error!("Auth session error: {}", error_description);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if !callback_url.is_null() {
|
||||
let url_string_value = unsafe {
|
||||
let abs_str: *mut Object = msg_send![callback_url, absoluteString];
|
||||
nsstring_to_string(abs_str)
|
||||
};
|
||||
|
||||
log::info!("Auth session callback URL: {}", url_string_value);
|
||||
|
||||
if let Some(session) = &auth_session.auth_session {
|
||||
unsafe {
|
||||
let _: () = msg_send![session.as_ptr(), cancel];
|
||||
}
|
||||
}
|
||||
|
||||
auth_session.auth_session = None;
|
||||
|
||||
let result = AuthResult {
|
||||
redirect_url: url_string_value,
|
||||
};
|
||||
|
||||
let _ = window_clone.emit("safari-auth-complete", result);
|
||||
}
|
||||
});
|
||||
|
||||
let completion_block = completion_block.copy();
|
||||
|
||||
let callback_scheme = NSString::from_str("readest");
|
||||
let auth_session_ptr: *mut Object = unsafe {
|
||||
let alloc: *mut Object = msg_send![auth_session_class, alloc];
|
||||
if alloc.is_null() {
|
||||
return Err("Failed to allocate ASWebAuthenticationSession".to_string());
|
||||
}
|
||||
|
||||
let init: *mut Object = msg_send![
|
||||
alloc,
|
||||
initWithURL:auth_url
|
||||
callbackURLScheme:callback_scheme
|
||||
completionHandler:&*completion_block
|
||||
];
|
||||
|
||||
if init.is_null() {
|
||||
return Err("Failed to initialize ASWebAuthenticationSession".to_string());
|
||||
}
|
||||
init
|
||||
};
|
||||
|
||||
unsafe {
|
||||
match window.ns_window() {
|
||||
Ok(ns_window) => {
|
||||
let ns_window = ns_window as *mut Object;
|
||||
let provider = create_provider(ns_window);
|
||||
|
||||
let _: () = msg_send![auth_session_ptr, setPresentationContextProvider:provider];
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to get NSWindow for presentation context: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let started: BOOL = unsafe { msg_send![auth_session_ptr, start] };
|
||||
|
||||
log::info!("Auth session start result: {}", started == YES);
|
||||
|
||||
if started != YES {
|
||||
return Err("Failed to start authentication session".to_string());
|
||||
}
|
||||
|
||||
let mut auth_session_guard = state.lock().unwrap();
|
||||
auth_session_guard.auth_session = Some(ThreadSafeObjcPointer::new(auth_session_ptr));
|
||||
|
||||
Ok(AuthResult {
|
||||
redirect_url: "pending".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("safari_auth")
|
||||
.setup(|app, _| {
|
||||
app.manage(Arc::new(Mutex::new(AuthSession::default())));
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![auth_with_safari])
|
||||
.build()
|
||||
}
|
||||
+16
-17
@@ -30,7 +30,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[command]
|
||||
pub fn set_traffic_lights(window: Window, visible: bool, x: f64, y: f64) {
|
||||
unsafe {
|
||||
@@ -47,7 +46,6 @@ pub fn set_traffic_lights(window: Window, visible: bool, x: f64, y: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, visible: bool, x: f64, y: f64) {
|
||||
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
|
||||
use cocoa::foundation::NSRect;
|
||||
@@ -83,13 +81,11 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, visible: bool,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug)]
|
||||
struct WindowState<R: Runtime> {
|
||||
window: Window<R>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
use cocoa::appkit::NSWindow;
|
||||
use cocoa::base::{id, BOOL};
|
||||
@@ -148,13 +144,14 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
.expect("NS window should exist on state to handle resize")
|
||||
as id;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
||||
TRAFFIC_LIGHTS_VISIBLE,
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
if state.window.label() == "main" {
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
||||
TRAFFIC_LIGHTS_VISIBLE,
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
@@ -281,12 +278,14 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
.expect("Failed to emit event");
|
||||
|
||||
let id = state.window.ns_window().expect("Failed to emit event") as id;
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
||||
TRAFFIC_LIGHTS_VISIBLE,
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
if state.window.label() == "main" {
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
||||
TRAFFIC_LIGHTS_VISIBLE,
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
@@ -15,17 +15,22 @@
|
||||
"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",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://fonts.gstatic.com https://db.onlinewebfonts.com https://cdn.jsdelivr.net",
|
||||
"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",
|
||||
"frame-src": "'self' blob: asset: http://asset.localhost",
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com"
|
||||
},
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": {
|
||||
"allow": ["$RESOURCE/**", "$APPDATA/**/*", "$TEMP/**/*"],
|
||||
"allow": [
|
||||
"$RESOURCE/**",
|
||||
"$APPDATA/**/*",
|
||||
"$TEMP/**/*",
|
||||
"/private/var/mobile/Containers/Data/Application/**/*"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
@@ -60,7 +65,8 @@
|
||||
"minSdkVersion": 26
|
||||
},
|
||||
"iOS": {
|
||||
"developmentTeam": "J5W48D69VR"
|
||||
"developmentTeam": "J5W48D69VR",
|
||||
"minimumSystemVersion": "14.0"
|
||||
},
|
||||
"fileAssociations": [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
export default function AuthErrorPage() {
|
||||
const router = useRouter();
|
||||
useTheme();
|
||||
useTheme({ systemUIVisible: false });
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IoArrowBack } from 'react-icons/io5';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -43,6 +44,7 @@ interface ProviderLoginProp {
|
||||
|
||||
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
|
||||
const DEEPLINK_CALLBACK = 'readest://auth-callback';
|
||||
const USE_APPLE_SIGN_IN = process.env['NEXT_PUBLIC_USE_APPLE_SIGN_IN'] === 'true';
|
||||
|
||||
const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
|
||||
return (
|
||||
@@ -73,14 +75,19 @@ export default function AuthPage() {
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useTheme({ systemUIVisible: false });
|
||||
|
||||
const getTauriRedirectTo = (isOAuth: boolean) => {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
|
||||
if (appService?.isMobile) {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN) {
|
||||
if (appService?.isMobileApp) {
|
||||
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
|
||||
}
|
||||
return DEEPLINK_CALLBACK;
|
||||
}
|
||||
return `http://localhost:${port}`; // only for development env on Desktop
|
||||
// For development env on Desktop, use a custom OAuth callback server
|
||||
// it's possible to register a custom URL scheme for the app
|
||||
// but this is not supported by macOS, so we use a local server instead
|
||||
return `http://localhost:${port}`;
|
||||
};
|
||||
|
||||
const getWebRedirectTo = () => {
|
||||
@@ -97,15 +104,19 @@ export default function AuthPage() {
|
||||
const request = {
|
||||
scope: ['fullName', 'email'] as Scope[],
|
||||
};
|
||||
const appleAuthResponse = await getAppleIdAuth(request);
|
||||
if (appleAuthResponse.identityToken) {
|
||||
const { error } = await supabase.auth.signInWithIdToken({
|
||||
provider: 'apple',
|
||||
token: appleAuthResponse.identityToken,
|
||||
});
|
||||
if (error) {
|
||||
console.error('Authentication error:', error);
|
||||
if (appService?.isIOSApp || USE_APPLE_SIGN_IN) {
|
||||
const appleAuthResponse = await getAppleIdAuth(request);
|
||||
if (appleAuthResponse.identityToken) {
|
||||
const { error } = await supabase.auth.signInWithIdToken({
|
||||
provider: 'apple',
|
||||
token: appleAuthResponse.identityToken,
|
||||
});
|
||||
if (error) {
|
||||
console.error('Authentication error:', error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('Sign in with Apple on this platform is not supported yet');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,7 +139,7 @@ export default function AuthPage() {
|
||||
}
|
||||
// Open the OAuth URL in a ASWebAuthenticationSession on iOS to comply with Apple's guidelines
|
||||
// for other platforms, open the OAuth URL in the default browser
|
||||
if (appService?.isIOSApp) {
|
||||
if (appService?.isIOSApp || appService?.isMacOSApp) {
|
||||
const res = await authWithSafari({ authUrl: data.url });
|
||||
if (res) {
|
||||
handleOAuthUrl(res.redirectUrl);
|
||||
@@ -161,7 +172,7 @@ export default function AuthPage() {
|
||||
|
||||
const startTauriOAuth = async () => {
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN) {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
currentWindow.listen('single-instance', ({ event, payload }) => {
|
||||
@@ -345,7 +356,7 @@ export default function AuthPage() {
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='apple'
|
||||
handleSignIn={appService?.isIOSApp ? tauriSignInApple : tauriSignIn}
|
||||
handleSignIn={appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn}
|
||||
Icon={FaApple}
|
||||
label={_('Sign in with Apple')}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
export type Scope = 'fullName' | 'email';
|
||||
export interface AppleIDAuthorizationRequest {
|
||||
@@ -23,12 +25,46 @@ export interface AppleIDAuthorizationResponse {
|
||||
export async function getAppleIdAuth(
|
||||
request: AppleIDAuthorizationRequest,
|
||||
): Promise<AppleIDAuthorizationResponse> {
|
||||
const result = await invoke<AppleIDAuthorizationResponse>(
|
||||
'plugin:sign-in-with-apple|get_apple_id_credential',
|
||||
{
|
||||
payload: request,
|
||||
},
|
||||
);
|
||||
const OS_TYPE = osType();
|
||||
if (OS_TYPE === 'ios') {
|
||||
const result = await invoke<AppleIDAuthorizationResponse>(
|
||||
'plugin:sign-in-with-apple|get_apple_id_credential',
|
||||
{
|
||||
payload: request,
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
return result;
|
||||
} else if (OS_TYPE === 'macos') {
|
||||
return new Promise<AppleIDAuthorizationResponse>(async (resolve, reject) => {
|
||||
const unlistenComplete = await listen<AppleIDAuthorizationResponse>(
|
||||
'apple-sign-in-complete',
|
||||
({ payload }) => {
|
||||
cleanup();
|
||||
resolve(payload);
|
||||
},
|
||||
);
|
||||
|
||||
const unlistenError = await listen<string>('apple-sign-in-error', ({ payload }) => {
|
||||
cleanup();
|
||||
reject(
|
||||
typeof payload === 'string' ? new Error(payload) : new Error('Apple sign‑in failed'),
|
||||
);
|
||||
});
|
||||
|
||||
function cleanup() {
|
||||
unlistenComplete();
|
||||
unlistenError();
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('start_apple_sign_in', { payload: request });
|
||||
} catch (err) {
|
||||
cleanup();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('Unsupported platform');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
export interface AuthRequest {
|
||||
authUrl: string;
|
||||
@@ -9,11 +11,33 @@ export interface AuthResponse {
|
||||
}
|
||||
|
||||
export async function authWithSafari(request: AuthRequest): Promise<AuthResponse> {
|
||||
const result = await invoke<AuthResponse>('plugin:native-bridge|auth_with_safari', {
|
||||
payload: request,
|
||||
});
|
||||
const OS_TYPE = osType();
|
||||
if (OS_TYPE === 'ios') {
|
||||
const result = await invoke<AuthResponse>('plugin:native-bridge|auth_with_safari', {
|
||||
payload: request,
|
||||
});
|
||||
return result;
|
||||
} else if (OS_TYPE === 'macos') {
|
||||
return new Promise<AuthResponse>(async (resolve, reject) => {
|
||||
const unlistenComplete = await listen<AuthResponse>('safari-auth-complete', ({ payload }) => {
|
||||
cleanup();
|
||||
resolve(payload);
|
||||
});
|
||||
|
||||
return result;
|
||||
function cleanup() {
|
||||
unlistenComplete();
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke<AuthResponse>('auth_with_safari', { payload: request });
|
||||
} catch (err) {
|
||||
cleanup();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('Unsupported OS type');
|
||||
}
|
||||
}
|
||||
|
||||
export async function authWithCustomTab(request: AuthRequest): Promise<AuthResponse> {
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import Providers from '@/components/Providers';
|
||||
|
||||
import '../styles/globals.css';
|
||||
import '../styles/fonts.css';
|
||||
|
||||
const url = 'https://web.readest.com/';
|
||||
const title = 'Readest — Where You Read, Digest and Get Insight';
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import clsx from 'clsx';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import { CiCircleMore } from 'react-icons/ci';
|
||||
import { LiaCloudUploadAltSolid, LiaCloudDownloadAltSolid } from 'react-icons/lia';
|
||||
import {
|
||||
LiaCloudUploadAltSolid,
|
||||
LiaCloudDownloadAltSolid,
|
||||
LiaInfoCircleSolid,
|
||||
} from 'react-icons/lia';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
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 { navigateToLogin } from '@/utils/nav';
|
||||
import { formatAuthors } from '@/utils/book';
|
||||
import ReadingProgress from './ReadingProgress';
|
||||
import BookCover from './BookCover';
|
||||
import BookCover from '@/components/BookCover';
|
||||
|
||||
interface BookItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
book: Book;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
@@ -20,6 +29,7 @@ interface BookItemProps {
|
||||
}
|
||||
|
||||
const BookItem: React.FC<BookItemProps> = ({
|
||||
mode,
|
||||
book,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
@@ -28,8 +38,10 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
handleBookDownload,
|
||||
showBookDetailsModal,
|
||||
}) => {
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { appService } = useEnv();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
const stopEvent = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -39,12 +51,19 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'book-item flex h-full flex-col',
|
||||
'book-item flex',
|
||||
mode === 'grid' && 'h-full flex-col',
|
||||
mode === 'list' && 'h-28 flex-row gap-4 overflow-hidden',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
>
|
||||
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md'>
|
||||
<BookCover book={book} />
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
|
||||
mode === 'list' && 'min-w-20',
|
||||
)}
|
||||
>
|
||||
<BookCover mode={mode} book={book} />
|
||||
{selectedBooks.includes(book.hash) && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
@@ -58,21 +77,53 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={clsx('flex w-full flex-col p-0 pt-2')}>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-[0.6em] text-xs font-semibold'>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full flex-col p-0',
|
||||
mode === 'grid' && 'pt-2',
|
||||
mode === 'list' && 'py-2',
|
||||
)}
|
||||
>
|
||||
<div className={clsx('min-w-0 flex-1', mode === 'list' && 'flex flex-col gap-2')}>
|
||||
<h4
|
||||
className={clsx(
|
||||
'overflow-hidden text-ellipsis font-semibold',
|
||||
mode === 'grid' && 'block whitespace-nowrap text-[0.6em] text-xs',
|
||||
mode === 'list' && 'line-clamp-2 text-base',
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
</h4>
|
||||
{mode === 'list' && (
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatAuthors(book.author, book.primaryLanguage) || ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
|
||||
>
|
||||
{book.progress && <ReadingProgress book={book} />}
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<div className='flex items-center justify-center gap-x-2'>
|
||||
{!appService?.isMobile && (
|
||||
<button
|
||||
className='show-detail-button -m-2 p-2 sm:opacity-0 sm:group-hover:opacity-100'
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => showBookDetailsModal(book)}
|
||||
>
|
||||
<div className='pt-[1px]'>
|
||||
<LiaInfoCircleSolid size={iconSize15} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{transferProgress !== null ? (
|
||||
transferProgress === 100 ? null : (
|
||||
<div
|
||||
className='radial-progress opacity-0 group-hover:opacity-100'
|
||||
className='radial-progress'
|
||||
style={
|
||||
{
|
||||
'--value': transferProgress,
|
||||
@@ -84,38 +135,33 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
></div>
|
||||
)
|
||||
) : (
|
||||
<button
|
||||
className='show-detail-button opacity-0 group-hover:opacity-100'
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => {
|
||||
if (!book.uploadedAt) {
|
||||
handleBookUpload(book);
|
||||
} else if (!book.downloadedAt) {
|
||||
handleBookDownload(book);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!book.uploadedAt && <LiaCloudUploadAltSolid size={iconSize15} />}
|
||||
{book.uploadedAt && !book.downloadedAt && (
|
||||
<LiaCloudDownloadAltSolid size={iconSize15} />
|
||||
)}
|
||||
</button>
|
||||
(!book.uploadedAt || (book.uploadedAt && !book.downloadedAt)) && (
|
||||
<button
|
||||
className='show-cloud-button -m-2 p-2'
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => {
|
||||
if (!user) {
|
||||
navigateToLogin(router);
|
||||
return;
|
||||
}
|
||||
if (!book.uploadedAt) {
|
||||
handleBookUpload(book);
|
||||
} else if (!book.downloadedAt) {
|
||||
handleBookDownload(book);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!book.uploadedAt && <LiaCloudUploadAltSolid size={iconSize15} />}
|
||||
{book.uploadedAt && !book.downloadedAt && (
|
||||
<LiaCloudDownloadAltSolid size={iconSize15} />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button
|
||||
className='show-detail-button opacity-0 group-hover:opacity-100'
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => showBookDetailsModal(book)}
|
||||
>
|
||||
<CiCircleMore size={iconSize15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,13 @@ import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel } from 'react-icons/md';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
|
||||
@@ -15,12 +17,15 @@ import { isMd5 } from '@/utils/md5';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
|
||||
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>;
|
||||
@@ -33,6 +38,8 @@ interface BookshelfProps {
|
||||
const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
libraryBooks,
|
||||
isSelectMode,
|
||||
isSelectAll,
|
||||
isSelectNone,
|
||||
handleImportBooks,
|
||||
handleBookUpload,
|
||||
handleBookDownload,
|
||||
@@ -45,6 +52,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
|
||||
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
|
||||
@@ -53,19 +61,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const [queryTerm, setQueryTerm] = useState<string | null>(null);
|
||||
const [navBooksGroup, setNavBooksGroup] = useState<BooksGroup | null>(null);
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
const [viewMode, setViewMode] = useState(searchParams?.get('view') || settings.libraryViewMode);
|
||||
const [sortBy, setSortBy] = useState(searchParams?.get('sort') || settings.librarySortBy);
|
||||
const [sortOrder, setSortOrder] = useState(
|
||||
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
|
||||
);
|
||||
const isImportingBook = useRef(false);
|
||||
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const allBookshelfItems = generateBookshelfItems(libraryBooks);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectMode) {
|
||||
setShowSelectModeActions(true);
|
||||
} else {
|
||||
setSelectedBooks([]);
|
||||
setShowSelectModeActions(false);
|
||||
}
|
||||
}, [isSelectMode]);
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const allBookshelfItems =
|
||||
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
|
||||
|
||||
useEffect(() => {
|
||||
if (isImportingBook.current) return;
|
||||
@@ -86,26 +91,67 @@ 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 params = new URLSearchParams(searchParams?.toString());
|
||||
if (query) {
|
||||
params.set('q', query);
|
||||
setQueryTerm(query);
|
||||
} else {
|
||||
params.delete('q');
|
||||
setQueryTerm(null);
|
||||
}
|
||||
if (sort) {
|
||||
params.set('sort', sort);
|
||||
setSortBy(sort);
|
||||
} else {
|
||||
params.delete('sort');
|
||||
}
|
||||
if (order) {
|
||||
params.set('order', order);
|
||||
setSortOrder(order);
|
||||
} else {
|
||||
params.delete('order');
|
||||
}
|
||||
if (view) {
|
||||
params.set('view', view);
|
||||
setViewMode(view);
|
||||
} else {
|
||||
params.delete('view');
|
||||
}
|
||||
if (sort === 'updated' && order === 'desc' && view === 'grid') {
|
||||
params.delete('sort');
|
||||
params.delete('order');
|
||||
params.delete('view');
|
||||
}
|
||||
if (group) {
|
||||
const booksGroup = allBookshelfItems.find(
|
||||
(item) => 'name' in item && item.id === group,
|
||||
) as BooksGroup;
|
||||
if (booksGroup) {
|
||||
setNavBooksGroup(booksGroup);
|
||||
params.set('group', group);
|
||||
} else {
|
||||
navigateToLibrary(router, query ? `q=${query}` : undefined);
|
||||
params.delete('group');
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}
|
||||
} else {
|
||||
setNavBooksGroup(null);
|
||||
navigateToLibrary(router, query ? `q=${query}` : undefined);
|
||||
params.delete('group');
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, libraryBooks, showGroupingModal]);
|
||||
@@ -121,14 +167,29 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
navigateToReader(router, selectedBooks);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
const openBookDetails = () => {
|
||||
const book = libraryBooks.find((book) => book.hash === selectedBooks[0]);
|
||||
if (book) {
|
||||
handleShowDetailsBook(book);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -144,7 +205,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');
|
||||
@@ -152,23 +212,83 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const authors = formatAuthors(item.author);
|
||||
return searchTerm.test(title) || searchTerm.test(authors);
|
||||
};
|
||||
const filteredBookshelfItems = currentBookshelfItems.filter((item) => {
|
||||
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
|
||||
else if (queryTerm) return bookFilter(item, queryTerm);
|
||||
return true;
|
||||
});
|
||||
const bookSorter = (a: Book, b: Book) => {
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
switch (sortBy) {
|
||||
case 'title':
|
||||
const aTitle = formatTitle(a.title);
|
||||
const bTitle = formatTitle(b.title);
|
||||
return aTitle.localeCompare(bTitle, uiLanguage || navigator.language);
|
||||
case 'author':
|
||||
const aAuthors = formatAuthors(a.author);
|
||||
const bAuthors = formatAuthors(b.author);
|
||||
return aAuthors.localeCompare(bAuthors, uiLanguage || navigator.language);
|
||||
case 'updated':
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||
case 'created':
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
case 'format':
|
||||
return a.format.localeCompare(b.format, uiLanguage || navigator.language);
|
||||
default:
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||
}
|
||||
};
|
||||
const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1;
|
||||
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
|
||||
const filteredBookshelfItems = currentBookshelfItems
|
||||
.filter((item) => {
|
||||
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
|
||||
else if (queryTerm) return bookFilter(item, queryTerm);
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
if (sortBy === 'updated') {
|
||||
return (
|
||||
(new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()) * sortOrderMultiplier
|
||||
);
|
||||
} else if ('name' in a || 'name' in b) {
|
||||
const aName = 'name' in a ? a.name : formatTitle(a.title);
|
||||
const bName = 'name' in b ? b.name : formatTitle(b.title);
|
||||
return aName.localeCompare(bName, uiLanguage || navigator.language) * sortOrderMultiplier;
|
||||
} else if (!('name' in a || 'name' in b)) {
|
||||
return bookSorter(a, b) * sortOrderMultiplier;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
className={clsx(
|
||||
'transform-wrapper grid flex-1 gap-x-4 sm:gap-x-0',
|
||||
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
|
||||
'transform-wrapper',
|
||||
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
|
||||
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
|
||||
viewMode === 'list' && 'flex flex-col',
|
||||
)}
|
||||
>
|
||||
{filteredBookshelfItems.map((item, index) => (
|
||||
<BookshelfItem
|
||||
key={`library-item-${index}`}
|
||||
mode={viewMode as LibraryViewModeType}
|
||||
item={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
@@ -184,7 +304,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
|
||||
@@ -207,14 +327,14 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center shadow-lg',
|
||||
'text-base-content bg-base-300 text-sm',
|
||||
'bg-gray-600 text-xs text-white',
|
||||
'mx-auto w-fit space-x-6 rounded-lg p-4',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={openSelectedBooks}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center',
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(!selectedBooks.length || !selectedBooks.every((id) => isMd5(id))) &&
|
||||
'btn-disabled opacity-50',
|
||||
)}
|
||||
@@ -225,17 +345,28 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
<button
|
||||
onClick={groupSelectedBooks}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center',
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
!selectedBooks.length && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<LuFolderPlus />
|
||||
<div>{_('Group')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={openBookDetails}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(selectedBooks.length !== 1 || !selectedBooks.every((id) => isMd5(id))) &&
|
||||
'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdInfoOutline />
|
||||
<div>{_('Details')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteSelectedBooks}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center',
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
!selectedBooks.length && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
@@ -244,7 +375,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSetSelectMode(false)}
|
||||
className={clsx('flex flex-col items-center justify-center')}
|
||||
className={clsx('flex flex-col items-center justify-center gap-1')}
|
||||
>
|
||||
<MdOutlineCancel />
|
||||
<div>{_('Cancel')}</div>
|
||||
@@ -253,7 +384,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
)}
|
||||
</div>
|
||||
{showGroupingModal && (
|
||||
<div>
|
||||
<ModalPortal>
|
||||
<GroupingModal
|
||||
libraryBooks={libraryBooks}
|
||||
selectedBooks={selectedBooks}
|
||||
@@ -266,7 +397,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleSetSelectMode(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{showDeleteAlert && (
|
||||
<div
|
||||
@@ -277,7 +408,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,6 +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 { 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';
|
||||
@@ -18,7 +19,7 @@ import GroupItem from './GroupItem';
|
||||
|
||||
export type BookshelfItem = Book | BooksGroup;
|
||||
|
||||
export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
|
||||
if (book.deletedAt) return acc;
|
||||
book.groupId = book.groupId || BOOK_UNGROUPED_ID;
|
||||
@@ -47,6 +48,10 @@ export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] =>
|
||||
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
export const generateListItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
return books.filter((book) => !book.deletedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
export const generateGroupsList = (items: Book[]): BookGroupType[] => {
|
||||
return items
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
@@ -66,6 +71,7 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
|
||||
};
|
||||
|
||||
interface BookshelfItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
item: BookshelfItem;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
@@ -80,6 +86,7 @@ interface BookshelfItemProps {
|
||||
}
|
||||
|
||||
const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
mode,
|
||||
item,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
@@ -107,6 +114,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 {
|
||||
@@ -248,30 +258,35 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'sm:hover:bg-base-300/50 group flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
pressing ? 'scale-95' : 'scale-100',
|
||||
)}
|
||||
style={{
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
book={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
transferProgress={transferProgress}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
|
||||
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'group',
|
||||
mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
|
||||
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
|
||||
)}
|
||||
style={{
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
mode={mode}
|
||||
book={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
transferProgress={transferProgress}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BooksGroup } from '@/types/book';
|
||||
import BookCover from './BookCover';
|
||||
import BookCover from '@/components/BookCover';
|
||||
|
||||
interface GroupItemProps {
|
||||
group: BooksGroup;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
@@ -8,6 +10,7 @@ interface ImportMenuProps {
|
||||
|
||||
const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBooks }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
|
||||
const handleImportBooks = () => {
|
||||
onImportBooks();
|
||||
@@ -17,7 +20,10 @@ const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBook
|
||||
return (
|
||||
<ul
|
||||
tabIndex={-1}
|
||||
className='dropdown-content dropdown-center bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
|
||||
className={clsx(
|
||||
'dropdown-content bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow',
|
||||
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
|
||||
)}
|
||||
>
|
||||
<MenuItem label={_('From Local File')} onClick={handleImportBooks} />
|
||||
</ul>
|
||||
|
||||
@@ -4,36 +4,48 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { FaSearch } from 'react-icons/fa';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { PiSelectionAllDuotone } from 'react-icons/pi';
|
||||
import { PiDotsThreeCircle } from 'react-icons/pi';
|
||||
import { MdOutlineMenu, MdArrowBackIosNew } from 'react-icons/md';
|
||||
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 { 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 useShortcuts from '@/hooks/useShortcuts';
|
||||
import SortMenu from './SortMenu';
|
||||
|
||||
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 { currentBookshelf } = useLibraryStore();
|
||||
const {
|
||||
isTrafficLightVisible,
|
||||
initializeTrafficLightStore,
|
||||
@@ -44,7 +56,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
const [searchQuery, setSearchQuery] = useState(searchParams?.get('q') ?? '');
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const iconSize18 = useResponsiveSize(18);
|
||||
const iconSize20 = useResponsiveSize(20);
|
||||
|
||||
useShortcuts({
|
||||
@@ -52,8 +64,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
});
|
||||
|
||||
// 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);
|
||||
@@ -61,14 +73,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(() => {
|
||||
@@ -85,15 +97,23 @@ 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,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px] sm:pr-6',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
'titlebar bg-base-200 z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px] sm:pr-6',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
|
||||
)}
|
||||
style={{
|
||||
marginTop: appService?.hasSafeAreaInset
|
||||
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
|
||||
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
|
||||
@@ -116,7 +136,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(
|
||||
@@ -132,9 +158,9 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
type='button'
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
throttledUpdateQueryParam('');
|
||||
debouncedUpdateQueryParam('');
|
||||
}}
|
||||
className='text-gray-400 hover:text-gray-600'
|
||||
className='pe-1 text-gray-400 hover:text-gray-600'
|
||||
aria-label={_('Clear Search')}
|
||||
>
|
||||
<IoMdCloseCircle className='h-4 w-4' />
|
||||
@@ -142,7 +168,10 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
)}
|
||||
<span className='bg-base-content/50 mx-2 h-4 w-[0.5px]'></span>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
|
||||
className={clsx(
|
||||
'exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center',
|
||||
appService?.isMobile ? 'dropdown-end' : 'dropdown-center',
|
||||
)}
|
||||
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center'
|
||||
toggleButton={
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Import Books')}>
|
||||
@@ -152,40 +181,68 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
>
|
||||
<ImportMenu onImportBooks={onImportBooks} />
|
||||
</Dropdown>
|
||||
<button
|
||||
onClick={onToggleSelectMode}
|
||||
aria-label={_('Select Multiple Books')}
|
||||
className='h-6'
|
||||
>
|
||||
<div
|
||||
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
|
||||
data-tip={_('Select Books')}
|
||||
{appService?.isMobile ? null : (
|
||||
<button
|
||||
onClick={onToggleSelectMode}
|
||||
aria-label={_('Select Multiple Books')}
|
||||
className='h-6'
|
||||
>
|
||||
<PiSelectionAllDuotone
|
||||
role='button'
|
||||
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
|
||||
data-tip={_('Select Books')}
|
||||
>
|
||||
<PiSelectionAllDuotone
|
||||
role='button'
|
||||
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={iconSize16} />}
|
||||
{isSelectMode ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-full items-center',
|
||||
'w-max-[72px] w-min-[72px] sm:w-max-[80px] sm:w-min-[80px]',
|
||||
)}
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={windowButtonVisible}
|
||||
showMaximize={windowButtonVisible}
|
||||
showClose={windowButtonVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<span className='font-sans text-base font-normal sm:text-sm'>
|
||||
{isSelectAll ? _('Deselect') : _('Select All')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
|
||||
>
|
||||
<SortMenu />
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={iconSize18} />}
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={windowButtonVisible}
|
||||
showMaximize={windowButtonVisible}
|
||||
showClose={windowButtonVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,47 +1,58 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PiUserCircle } from 'react-icons/pi';
|
||||
import { PiUserCircleCheck } from 'react-icons/pi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { hasUpdater, isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
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 { getStoragePlanData } from '@/utils/access';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
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';
|
||||
|
||||
interface BookMenuProps {
|
||||
interface SettingsMenuProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
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 [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);
|
||||
@@ -57,6 +68,11 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const cycleThemeMode = () => {
|
||||
const nextMode = themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
|
||||
setThemeMode(nextMode);
|
||||
};
|
||||
|
||||
const handleReloadPage = () => {
|
||||
window.location.reload();
|
||||
setIsDropdownOpen?.(false);
|
||||
@@ -108,23 +124,25 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsScreenWakeLock(settings.screenWakeLock);
|
||||
};
|
||||
|
||||
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 toggleOpenLastBooks = () => {
|
||||
settings.openLastBooks = !settings.openLastBooks;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsOpenLastBooks(settings.openLastBooks);
|
||||
};
|
||||
|
||||
const toggleTelemetry = () => {
|
||||
settings.telemetryEnabled = !settings.telemetryEnabled;
|
||||
if (settings.telemetryEnabled) {
|
||||
optInTelemetry();
|
||||
} else {
|
||||
optOutTelemetry();
|
||||
}
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsTelemetryEnabled(settings.telemetryEnabled);
|
||||
};
|
||||
|
||||
const isWebApp = isWebAppPlatform();
|
||||
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
|
||||
const userFullName = user?.user_metadata?.['full_name'];
|
||||
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
|
||||
@@ -132,7 +150,10 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-72 shadow-2xl'
|
||||
className={clsx(
|
||||
'settings-menu dropdown-content no-triangle border-base-100',
|
||||
'z-20 mt-2 max-w-[90vw] shadow-2xl',
|
||||
)}
|
||||
>
|
||||
{user ? (
|
||||
<MenuItem
|
||||
@@ -142,45 +163,45 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
: _('Logged in')
|
||||
}
|
||||
labelClass='!max-w-40'
|
||||
icon={
|
||||
Icon={
|
||||
avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={_('User avatar')}
|
||||
className='h-5 w-5 rounded-full'
|
||||
referrerPolicy='no-referrer'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<UserAvatar url={avatarUrl} size={iconSize} DefaultIcon={PiUserCircleCheck} />
|
||||
) : (
|
||||
<PiUserCircleCheck />
|
||||
PiUserCircleCheck
|
||||
)
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
<Quota quotas={quotas} className='h-10 pl-4 pr-2' />
|
||||
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
|
||||
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem label={_('Sign In')} icon={<PiUserCircle />} onClick={handleUserLogin}></MenuItem>
|
||||
<MenuItem label={_('Sign In')} Icon={PiUserCircle} onClick={handleUserLogin}></MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Auto Upload Books to Cloud')}
|
||||
icon={isAutoUpload ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAutoUpload ? MdCheck : undefined}
|
||||
onClick={toggleAutoUploadBooks}
|
||||
/>
|
||||
{isTauriAppPlatform() && !appService?.isMobile && (
|
||||
<MenuItem
|
||||
label={_('Auto Import on File Open')}
|
||||
icon={isAutoImportBooksOnOpen ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAutoImportBooksOnOpen ? MdCheck : undefined}
|
||||
onClick={toggleAutoImportBooksOnOpen}
|
||||
/>
|
||||
)}
|
||||
{hasUpdater() && (
|
||||
{isTauriAppPlatform() && (
|
||||
<MenuItem
|
||||
label={_('Open Last Book on Start')}
|
||||
Icon={isOpenLastBooks ? MdCheck : undefined}
|
||||
onClick={toggleOpenLastBooks}
|
||||
/>
|
||||
)}
|
||||
{appService?.hasUpdater && (
|
||||
<MenuItem
|
||||
label={_('Check Updates on Start')}
|
||||
icon={isAutoCheckUpdates ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAutoCheckUpdates ? MdCheck : undefined}
|
||||
onClick={toggleAutoCheckUpdates}
|
||||
/>
|
||||
)}
|
||||
@@ -189,19 +210,36 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
{appService?.hasWindow && (
|
||||
<MenuItem
|
||||
label={_('Always on Top')}
|
||||
icon={isAlwaysOnTop ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAlwaysOnTop ? MdCheck : undefined}
|
||||
onClick={toggleAlwaysOnTop}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Keep Screen Awake')}
|
||||
icon={isScreenWakeLock ? <MdCheck className='text-base-content' /> : undefined}
|
||||
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' />
|
||||
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
interface SortMenuProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
|
||||
const viewMode = settings.libraryViewMode;
|
||||
const sortBy = settings.librarySortBy;
|
||||
const isAscending = settings.librarySortAscending;
|
||||
|
||||
const viewOptions = [
|
||||
{ label: _('List'), value: 'list' },
|
||||
{ label: _('Grid'), value: 'grid' },
|
||||
];
|
||||
|
||||
const sortByOptions = [
|
||||
{ label: _('Title'), value: 'title' },
|
||||
{ label: _('Author'), value: 'author' },
|
||||
{ label: _('Format'), value: 'format' },
|
||||
{ label: _('Date Read'), value: 'updated' },
|
||||
{ label: _('Date Added'), value: 'created' },
|
||||
];
|
||||
|
||||
const sortingOptions = [
|
||||
{ label: _('Ascending'), value: true },
|
||||
{ label: _('Descending'), value: false },
|
||||
];
|
||||
|
||||
const handleSetViewMode = (value: LibraryViewModeType) => {
|
||||
settings.libraryViewMode = value;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsDropdownOpen?.(false);
|
||||
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('view', value);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleSetSortBy = (value: LibrarySortByType) => {
|
||||
settings.librarySortBy = value;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsDropdownOpen?.(false);
|
||||
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('sort', value);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleSetSortAscending = (value: boolean) => {
|
||||
settings.librarySortAscending = value;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsDropdownOpen?.(false);
|
||||
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('order', value ? 'asc' : 'desc');
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
|
||||
>
|
||||
{viewOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={viewMode === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleSetViewMode(option.value as LibraryViewModeType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem
|
||||
label={_('Sort by...')}
|
||||
buttonClass='h-8'
|
||||
labelClass='text-sm sm:text-xs'
|
||||
disabled
|
||||
/>
|
||||
{sortByOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={sortBy === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
{sortingOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value.toString()}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={isAscending === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleSetSortAscending(option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortMenu;
|
||||
@@ -98,11 +98,12 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
|
||||
if (newBook.uploadedAt && !newBook.deletedAt) {
|
||||
try {
|
||||
await appService?.downloadBook(newBook, true);
|
||||
} catch {
|
||||
console.error('Failed to download book:', newBook);
|
||||
} finally {
|
||||
newBook.coverImageUrl = await appService?.generateCoverImageUrl(newBook);
|
||||
updatedLibrary.push(newBook);
|
||||
setLibrary(updatedLibrary);
|
||||
} catch {
|
||||
console.error('Failed to download book:', newBook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ interface DemoBooks {
|
||||
|
||||
export const useDemoBooks = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const userLang = getUserLang() as keyof typeof libraries;
|
||||
const [books, setBooks] = useState<Book[]>([]);
|
||||
const isLoading = useRef(false);
|
||||
|
||||
@@ -27,6 +26,7 @@ export const useDemoBooks = () => {
|
||||
if (isLoading.current) return;
|
||||
isLoading.current = true;
|
||||
|
||||
const userLang = getUserLang() as keyof typeof libraries;
|
||||
const fetchDemoBooks = async () => {
|
||||
try {
|
||||
const appService = await envConfig.getAppService();
|
||||
|
||||
@@ -12,8 +12,8 @@ import { getFilename, listFormater } from '@/utils/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { ProgressPayload } from '@/utils/transfer';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { isTauriAppPlatform, hasUpdater } from '@/services/environment';
|
||||
import { parseOpenWithFiles } from '@/helpers/openWith';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
@@ -21,7 +21,6 @@ import { getCurrentWebview } from '@tauri-apps/api/webview';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
@@ -29,11 +28,19 @@ 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 { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriQuitApp } from '@/utils/window';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { mountAdditionalFonts } from '@/utils/font';
|
||||
import {
|
||||
tauriHandleSetAlwaysOnTop,
|
||||
tauriHandleToggleFullScreen,
|
||||
tauriQuitApp,
|
||||
} from '@/utils/window';
|
||||
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from './components/LibraryHeader';
|
||||
@@ -56,20 +63,25 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
updateBook,
|
||||
setLibrary,
|
||||
checkOpenWithBooks,
|
||||
checkLastOpenBooks,
|
||||
setCheckOpenWithBooks,
|
||||
setCheckLastOpenBooks,
|
||||
} = useLibraryStore();
|
||||
const _ = useTranslation();
|
||||
useTheme();
|
||||
const { updateAppTheme } = useThemeStore();
|
||||
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
|
||||
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;
|
||||
}>({});
|
||||
const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState<string[] | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const demoBooks = useDemoBooks();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -86,6 +98,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useShortcuts({
|
||||
onToggleFullscreen: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriHandleToggleFullScreen();
|
||||
}
|
||||
},
|
||||
onQuitApp: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriQuitApp();
|
||||
@@ -94,13 +111,12 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-200');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
mountAdditionalFonts(document);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const doCheckAppUpdates = async () => {
|
||||
if (hasUpdater() && settings.autoCheckUpdates) {
|
||||
if (appService?.hasUpdater && settings.autoCheckUpdates) {
|
||||
await checkForAppUpdates(_);
|
||||
}
|
||||
};
|
||||
@@ -111,6 +127,12 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (appService?.isMobileApp) {
|
||||
lockScreenOrientation({ orientation: 'auto' });
|
||||
}
|
||||
}, [appService]);
|
||||
|
||||
const handleDropedFiles = async (files: File[] | string[]) => {
|
||||
if (files.length === 0) return;
|
||||
const supportedFiles = files.filter((file) => {
|
||||
@@ -196,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();
|
||||
@@ -203,7 +232,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
for (const file of openWithFiles) {
|
||||
console.log('Open with book:', file);
|
||||
try {
|
||||
const temp = !settings.autoImportBooksOnOpen;
|
||||
const temp = appService.isMobile ? false : !settings.autoImportBooksOnOpen;
|
||||
const book = await appService.importBook(file, libraryBooks, true, true, false, temp);
|
||||
if (book) {
|
||||
bookIds.push(book.hash);
|
||||
@@ -217,15 +246,46 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
|
||||
console.log('Opening books:', bookIds);
|
||||
if (bookIds.length > 0) {
|
||||
setTimeout(() => {
|
||||
navigateToReader(router, bookIds);
|
||||
}, 0);
|
||||
setPendingNavigationBookIds(bookIds);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOpenLastBooks = async (
|
||||
appService: AppService,
|
||||
lastBookIds: string[],
|
||||
libraryBooks: Book[],
|
||||
) => {
|
||||
if (lastBookIds.length === 0) return false;
|
||||
const bookIds: string[] = [];
|
||||
for (const bookId of lastBookIds) {
|
||||
const book = libraryBooks.find((b) => b.hash === bookId);
|
||||
if (book && (await appService.isBookAvailable(book))) {
|
||||
bookIds.push(book.hash);
|
||||
}
|
||||
}
|
||||
console.log('Opening last books:', bookIds);
|
||||
if (bookIds.length > 0) {
|
||||
setPendingNavigationBookIds(bookIds);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingNavigationBookIds) {
|
||||
const bookIds = pendingNavigationBookIds;
|
||||
setPendingNavigationBookIds(null);
|
||||
if (bookIds.length > 0) {
|
||||
navigateToReader(router, bookIds);
|
||||
}
|
||||
}
|
||||
}, [pendingNavigationBookIds, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
@@ -250,34 +310,35 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
const settings = await appService.loadSettings();
|
||||
setSettings(settings);
|
||||
|
||||
const libraryBooks = await appService.loadLibraryBooks();
|
||||
if (checkOpenWithBooks && isTauriAppPlatform()) {
|
||||
await handleOpenWithBooks(appService, libraryBooks);
|
||||
} else {
|
||||
setCheckOpenWithBooks(false);
|
||||
setLibrary(libraryBooks);
|
||||
}
|
||||
// Reuse the library from the store when we return from the reader
|
||||
const library = libraryBooks.length > 0 ? libraryBooks : await appService.loadLibraryBooks();
|
||||
setCheckOpenWithBooks(checkOpenWithBooks && (await handleOpenWithBooks(appService, library)));
|
||||
setCheckLastOpenBooks(
|
||||
checkLastOpenBooks &&
|
||||
settings.openLastBooks &&
|
||||
(await handleOpenLastBooks(appService, settings.lastOpenBooks, library)),
|
||||
);
|
||||
|
||||
setLibrary(library);
|
||||
setLibraryLoaded(true);
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleOpenWithBooks = async (appService: AppService, libraryBooks: Book[]) => {
|
||||
const handleOpenWithBooks = async (appService: AppService, library: Book[]) => {
|
||||
const openWithFiles = (await parseOpenWithFiles()) || [];
|
||||
|
||||
if (openWithFiles.length > 0) {
|
||||
await processOpenWithFiles(appService, openWithFiles, libraryBooks);
|
||||
} else {
|
||||
setCheckOpenWithBooks(false);
|
||||
setLibrary(libraryBooks);
|
||||
return await processOpenWithFiles(appService, openWithFiles, library);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
initLogin();
|
||||
initLibrary();
|
||||
return () => {
|
||||
setCheckOpenWithBooks(false);
|
||||
setCheckLastOpenBooks(false);
|
||||
isInitiating.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -308,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);
|
||||
@@ -334,7 +396,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
console.error('Failed to import book:', filename, error);
|
||||
}
|
||||
}
|
||||
appService?.saveLibraryBooks(libraryBooks);
|
||||
appService?.saveLibraryBooks(library);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -474,18 +536,23 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
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) => {
|
||||
@@ -496,7 +563,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
return null;
|
||||
}
|
||||
|
||||
if (checkOpenWithBooks) {
|
||||
if (checkOpenWithBooks || checkLastOpenBooks) {
|
||||
return (
|
||||
loading && (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
@@ -518,8 +585,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
<div className='fixed top-0 z-40 w-full'>
|
||||
<LibraryHeader
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
onImportBooks={handleImportBooks}
|
||||
onToggleSelectMode={handleToggleSelectMode}
|
||||
onToggleSelectMode={() => handleSetSelectMode(!isSelectMode)}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeselectAll={handleDeselectAll}
|
||||
/>
|
||||
</div>
|
||||
{loading && (
|
||||
@@ -532,16 +602,23 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'scroll-container drop-zone mt-[48px] flex-grow overflow-y-auto px-4 sm:px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(52px+env(safe-area-inset-top))]',
|
||||
'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',
|
||||
}}
|
||||
>
|
||||
<DropIndicator />
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
isSelectNone={isSelectNone}
|
||||
handleImportBooks={handleImportBooks}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
@@ -574,9 +651,13 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
isOpen={!!showDetailsBook}
|
||||
book={showDetailsBook}
|
||||
onClose={() => setShowDetailsBook(null)}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
handleBookDelete={handleBookDelete}
|
||||
/>
|
||||
)}
|
||||
<AboutWindow />
|
||||
{appService?.isAndroidApp && <UpdaterWindow />}
|
||||
<Toast />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { redirectToLibrary } from '@/utils/nav';
|
||||
'use client';
|
||||
|
||||
const HomePage = () => {
|
||||
redirectToLibrary();
|
||||
};
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default HomePage;
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace('/library');
|
||||
}, [router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@ 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[];
|
||||
@@ -27,7 +28,7 @@ interface BooksGridProps {
|
||||
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { getProgress, getViewState, getViewSettings, hoveredBookKey } = useReaderStore();
|
||||
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
@@ -42,10 +43,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'grid h-full flex-grow',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
className={clsx('relative grid h-full flex-grow')}
|
||||
style={{
|
||||
gridTemplateColumns: gridTemplate.columns,
|
||||
gridTemplateRows: gridTemplate.rows,
|
||||
@@ -59,10 +57,14 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { book, bookDoc } = bookData || {};
|
||||
if (!book || !config || !bookDoc || !viewSettings) 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 scrolled = viewSettings.scrolled;
|
||||
const showBarsOnScroll = viewSettings.showBarsOnScroll;
|
||||
const showHeader = viewSettings.showHeader && (scrolled ? showBarsOnScroll : true);
|
||||
const showFooter = viewSettings.showFooter && (scrolled ? showBarsOnScroll : true);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -70,10 +72,14 @@ 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',
|
||||
)}
|
||||
>
|
||||
{isBookmarked && <Ribbon width={`${horizontalGapPercent}%`} />}
|
||||
{isBookmarked && !hoveredBookKey && <Ribbon width={`${horizontalGapPercent}%`} />}
|
||||
<HeaderBar
|
||||
bookKey={bookKey}
|
||||
bookTitle={book.title}
|
||||
@@ -85,32 +91,36 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
|
||||
{viewSettings.vertical && viewSettings.scrolled && (
|
||||
<>
|
||||
<div
|
||||
className='bg-base-100 absolute left-0 top-0 h-full'
|
||||
style={{
|
||||
width: `calc(${horizontalGapPercent}%)`,
|
||||
height: `calc(100% - ${verticalMarginPixels}px)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='bg-base-100 absolute right-0 top-0 h-full'
|
||||
style={{
|
||||
width: `calc(${horizontalGapPercent}%)`,
|
||||
height: `calc(100% - ${verticalMarginPixels}px)`,
|
||||
}}
|
||||
/>
|
||||
{(showFooter || viewSettings.doubleBorder) && (
|
||||
<div
|
||||
className='bg-base-100 absolute left-0 top-0 h-full'
|
||||
style={{
|
||||
width: `calc(${horizontalGapPercent}%)`,
|
||||
height: `calc(100% - ${verticalMarginPixels}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(showHeader || viewSettings.doubleBorder) && (
|
||||
<div
|
||||
className='bg-base-100 absolute right-0 top-0 h-full'
|
||||
style={{
|
||||
width: `calc(${horizontalGapPercent}%)`,
|
||||
height: `calc(100% - ${verticalMarginPixels}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{viewSettings.vertical && viewSettings.doubleBorder && (
|
||||
<DoubleBorder
|
||||
showHeader={viewSettings.showHeader}
|
||||
showFooter={viewSettings.showFooter}
|
||||
showHeader={showHeader}
|
||||
showFooter={showFooter}
|
||||
borderColor={viewSettings.borderColor}
|
||||
horizontalGap={horizontalGapPercent}
|
||||
verticalMargin={verticalMarginPixels}
|
||||
/>
|
||||
)}
|
||||
{viewSettings.showHeader && (
|
||||
{showHeader && (
|
||||
<SectionInfo
|
||||
section={sectionLabel}
|
||||
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
|
||||
@@ -123,18 +133,18 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<HintInfo
|
||||
bookKey={bookKey}
|
||||
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
|
||||
isScrolled={viewSettings.scrolled}
|
||||
isVertical={viewSettings.vertical}
|
||||
horizontalGap={horizontalGapPercent}
|
||||
verticalMargin={verticalMarginPixels}
|
||||
/>
|
||||
{viewSettings.showFooter && (
|
||||
<PageInfoView
|
||||
{showFooter && (
|
||||
<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}
|
||||
/>
|
||||
@@ -152,6 +162,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<TTSControl />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,14 +2,18 @@ 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 { 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 { useClickEvent, useTouchEvent } from '../hooks/useIframeEvents';
|
||||
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 {
|
||||
@@ -23,21 +27,26 @@ import {
|
||||
handleTouchEnd,
|
||||
} from '../utils/iframeEventHandlers';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { isCJKLang } from '@/utils/lang';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
|
||||
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);
|
||||
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { themeCode, isDarkMode } = useThemeStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
useEffect(() => {
|
||||
@@ -45,35 +54,48 @@ const FoliateViewer: React.FC<{
|
||||
return () => clearTimeout(timer);
|
||||
}, [toastMessage]);
|
||||
|
||||
useUICSS(bookKey, viewSettings);
|
||||
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 docTransformHandler = (event: Event) => {
|
||||
const { detail } = event as CustomEvent;
|
||||
detail.data = Promise.resolve(detail.data)
|
||||
.then((data) => {
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
if (detail.type === 'text/css') return transformStylesheet(data);
|
||||
if (viewSettings && detail.type === 'application/xhtml+xml') {
|
||||
const ctx = {
|
||||
bookKey,
|
||||
viewSettings,
|
||||
content: data,
|
||||
};
|
||||
return Promise.resolve(transformContent(ctx));
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(new Error(`Failed to load ${detail.name}`, { cause: e }));
|
||||
return '';
|
||||
});
|
||||
const getDocTransformHandler = ({ width, height }: { width: number; height: number }) => {
|
||||
return (event: Event) => {
|
||||
const { detail } = event as CustomEvent;
|
||||
detail.data = Promise.resolve(detail.data)
|
||||
.then((data) => {
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
if (viewSettings && detail.type === 'text/css')
|
||||
return transformStylesheet(viewSettings, width, height, data);
|
||||
if (viewSettings && detail.type === 'application/xhtml+xml') {
|
||||
const ctx = {
|
||||
bookKey,
|
||||
viewSettings,
|
||||
content: data,
|
||||
transformers: ['punctuation', 'footnote'],
|
||||
};
|
||||
return Promise.resolve(transformContent(ctx));
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(new Error(`Failed to load ${detail.name}`, { cause: e }));
|
||||
return '';
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const docLoadHandler = (event: Event) => {
|
||||
@@ -82,14 +104,22 @@ 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 = writingDir?.rtl || viewSettings.writingMode.includes('rl') || false;
|
||||
viewSettings.rtl =
|
||||
writingDir?.rtl ||
|
||||
getDirFromUILanguage() === 'rtl' ||
|
||||
viewSettings.writingMode.includes('rl') ||
|
||||
false;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
|
||||
mountAdditionalFonts(detail.doc);
|
||||
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
|
||||
|
||||
if (!detail.doc.isEventListenersAdded) {
|
||||
// listened events in iframes are posted to the main window
|
||||
// and then used by useMouseEvent and useTouchEvent
|
||||
// and more gesture events can be detected in the iframeEventHandlers
|
||||
detail.doc.isEventListenersAdded = true;
|
||||
detail.doc.addEventListener('keydown', handleKeydown.bind(null, bookKey));
|
||||
detail.doc.addEventListener('mousedown', handleMousedown.bind(null, bookKey));
|
||||
@@ -107,18 +137,6 @@ const FoliateViewer: React.FC<{
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
|
||||
|
||||
if (detail.reason === 'scroll') {
|
||||
const renderer = viewRef.current?.renderer;
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
if (renderer && viewSettings.continuousScroll) {
|
||||
if (renderer.start <= 0) {
|
||||
viewRef.current?.prev(1);
|
||||
// sometimes viewSize has subpixel value that the end never reaches
|
||||
} else if (renderer.end + 1 >= renderer.viewSize) {
|
||||
viewRef.current?.next(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
const parallelViews = getParallels(bookKey);
|
||||
if (parallelViews && parallelViews.size > 0) {
|
||||
parallelViews.forEach((key) => {
|
||||
@@ -132,8 +150,9 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
useTouchEvent(bookKey, viewRef);
|
||||
const { handleTurnPage } = useClickEvent(bookKey, viewRef, containerRef);
|
||||
const { handlePageFlip, handleContinuousScroll } = usePagination(bookKey, viewRef, containerRef);
|
||||
const mouseHandlers = useMouseEvent(bookKey, handlePageFlip, handleContinuousScroll);
|
||||
const touchHandlers = useTouchEvent(bookKey, handleContinuousScroll);
|
||||
|
||||
useFoliateEvents(viewRef.current, {
|
||||
onLoad: docLoadHandler,
|
||||
@@ -161,6 +180,11 @@ 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) {
|
||||
const settingsDir = getBookDirFromWritingMode(writingMode);
|
||||
@@ -179,16 +203,23 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const { book } = view;
|
||||
|
||||
book.transformTarget?.addEventListener('data', docTransformHandler);
|
||||
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
|
||||
view.renderer.setStyles?.(getStyles(viewSettings));
|
||||
|
||||
const isScrolled = viewSettings.scrolled!;
|
||||
const marginPx = viewSettings.marginPx!;
|
||||
const gapPercent = viewSettings.gapPercent!;
|
||||
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);
|
||||
const maxBlockSize = viewSettings.maxBlockSize!;
|
||||
const screenOrientation = viewSettings.screenOrientation!;
|
||||
if (appService?.isMobileApp) {
|
||||
await lockScreenOrientation({ orientation: screenOrientation });
|
||||
}
|
||||
if (animated) {
|
||||
view.renderer.setAttribute('animated', '');
|
||||
} else {
|
||||
@@ -216,9 +247,10 @@ const FoliateViewer: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='foliate-viewer h-[100%] w-[100%]'
|
||||
onClick={(event) => handleTurnPage(event)}
|
||||
ref={containerRef}
|
||||
className='foliate-viewer h-[100%] w-[100%]'
|
||||
{...mouseHandlers}
|
||||
{...touchHandlers}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
import { IoIosList as TOCIcon } from 'react-icons/io';
|
||||
import { PiNotePencil as NoteIcon } from 'react-icons/pi';
|
||||
@@ -17,6 +18,7 @@ import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { viewPagination } from '../hooks/usePagination';
|
||||
import { saveViewSettings } from '../utils/viewSettingsHelper';
|
||||
import { PageInfo } from '@/types/book';
|
||||
import Button from '@/components/Button';
|
||||
@@ -39,8 +41,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
}) => {
|
||||
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);
|
||||
@@ -51,6 +53,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);
|
||||
@@ -76,12 +79,24 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
saveViewSettings(envConfig, bookKey, 'lineHeight', value / 10);
|
||||
};
|
||||
|
||||
const handleGoPrev = () => {
|
||||
view?.goLeft();
|
||||
const handleGoPrevPage = () => {
|
||||
viewPagination(view, viewSettings, 'left');
|
||||
};
|
||||
|
||||
const handleGoNext = () => {
|
||||
view?.goRight();
|
||||
const handleGoNextPage = () => {
|
||||
viewPagination(view, viewSettings, 'right');
|
||||
};
|
||||
|
||||
const handleGoPrevSection = () => {
|
||||
if (view?.renderer.prevSection) {
|
||||
view?.renderer.prevSection();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoNextSection = () => {
|
||||
if (view?.renderer.nextSection) {
|
||||
view?.renderer.nextSection();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
@@ -94,16 +109,14 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
|
||||
const handleSpeakText = async () => {
|
||||
if (!view || !progress) return;
|
||||
const { range } = progress;
|
||||
if (eventDispatcher.dispatchSync('tts-is-speaking')) {
|
||||
eventDispatcher.dispatch('tts-stop', { bookKey });
|
||||
} else {
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range });
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetActionTab = (tab: string) => {
|
||||
console.log('handleSetActionTab', tab);
|
||||
setActionTab(actionTab === tab ? '' : tab);
|
||||
if (tab === 'tts') {
|
||||
setHoveredBookKey('');
|
||||
@@ -123,16 +136,24 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredBookKey !== bookKey) {
|
||||
setActionTab('');
|
||||
}
|
||||
}, [hoveredBookKey, bookKey]);
|
||||
|
||||
const getMarginProgressValue = (marginPx: number, gapPercent: number) => {
|
||||
return (marginPx / 88 + gapPercent / 10) * 50;
|
||||
};
|
||||
|
||||
const isVisible = hoveredBookKey === bookKey;
|
||||
const ttsEnabled = viewState?.ttsEnabled;
|
||||
const progressInfo = bookFormat === 'PDF' ? section : pageinfo;
|
||||
const progressValid = !!progressInfo;
|
||||
const progressFraction = progressValid
|
||||
? ((progressInfo!.next ?? progressInfo!.current) + 1) / progressInfo!.total
|
||||
: 0;
|
||||
const progressFraction =
|
||||
progressValid && progressInfo?.total > 0
|
||||
? (progressInfo!.current + 1) / progressInfo!.total || 0
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -149,7 +170,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
className={clsx(
|
||||
'footer-bar shadow-xs absolute bottom-0 z-50 flex w-full flex-col',
|
||||
'sm:h-[52px] sm:justify-center',
|
||||
'sm:bg-base-100 border-base-300/50 border sm:border-none',
|
||||
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
|
||||
'transition-[opacity,transform] duration-300',
|
||||
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
|
||||
@@ -167,7 +188,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
{/* Mobile footer bar */}
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-200 absolute bottom-16 flex w-full items-center gap-x-2 px-4 transition-all sm:hidden',
|
||||
'bg-base-200 absolute bottom-16 flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
|
||||
actionTab === 'progress'
|
||||
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
@@ -178,34 +199,48 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: '64px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightWideLine /> : <RiArrowLeftWideLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNext : handleGoPrev}
|
||||
tooltip={viewSettings?.rtl ? _('Go Right') : _('Go Left')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
tooltip={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
tooltip={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<Slider
|
||||
heightPx={sliderHeight}
|
||||
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
|
||||
initialValue={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftWideLine /> : <RiArrowRightWideLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrev : handleGoNext}
|
||||
tooltip={viewSettings?.rtl ? _('Go Left') : _('Go Right')}
|
||||
/>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Slider
|
||||
heightPx={sliderHeight}
|
||||
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
|
||||
initialValue={progressValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
tooltip={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
|
||||
tooltip={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
tooltip={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
tooltip={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -278,14 +313,22 @@ 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'>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightWideLine /> : <RiArrowLeftWideLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNext : handleGoPrev}
|
||||
tooltip={viewSettings?.rtl ? _('Go Right') : _('Go Left')}
|
||||
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
|
||||
tooltip={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
|
||||
tooltip={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
|
||||
@@ -312,11 +355,20 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
handleProgressChange(parseInt((e.target as HTMLInputElement).value, 10))
|
||||
}
|
||||
/>
|
||||
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftWideLine /> : <RiArrowRightWideLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrev : handleGoNext}
|
||||
tooltip={viewSettings?.rtl ? _('Go Left') : _('Go Right')}
|
||||
icon={<FaHeadphones className={ttsEnabled ? 'text-blue-500' : ''} />}
|
||||
onClick={handleSpeakText}
|
||||
tooltip={_('Speak')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
|
||||
/>
|
||||
<Button
|
||||
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
|
||||
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
|
||||
tooltip={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 } 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)!;
|
||||
@@ -53,6 +57,11 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
getView(bookKey)?.goTo(popupLinkDetail.href);
|
||||
});
|
||||
});
|
||||
view.addEventListener('load', (e: CustomEvent) => {
|
||||
const { doc } = e.detail;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
mountAdditionalFonts(doc, isCJKLang(bookData.book?.primaryLanguage));
|
||||
});
|
||||
footnoteViewRef.current = view;
|
||||
footnoteRef.current?.replaceChildren(view);
|
||||
const { renderer } = view;
|
||||
@@ -131,6 +140,11 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
setGridRect(rect);
|
||||
setTrianglePosition(triangPos);
|
||||
|
||||
const { a: anchor } = detail as { a: HTMLAnchorElement };
|
||||
const footnoteClasses = ['duokan-footnote', 'footnote-link', 'footnote'];
|
||||
if (footnoteClasses.some((cls) => anchor.classList.contains(cls))) {
|
||||
detail['follow'] = true;
|
||||
}
|
||||
footnoteHandler.handle(bookDoc, event)?.catch((err) => {
|
||||
console.warn(err);
|
||||
const detail = (event as CustomEvent).detail;
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
@@ -13,6 +14,7 @@ import SidebarToggler from './SidebarToggler';
|
||||
import BookmarkToggler from './BookmarkToggler';
|
||||
import NotebookToggler from './NotebookToggler';
|
||||
import SettingsToggler from './SettingsToggler';
|
||||
import TranslationToggler from './TranslationToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
|
||||
interface HeaderBarProps {
|
||||
@@ -42,7 +44,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
cleanupTrafficLightListeners,
|
||||
} = useTrafficLightStore();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { hoveredBookKey, setHoveredBookKey, bookKeys } = useReaderStore();
|
||||
const { bookKeys, hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
@@ -70,56 +73,89 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSideBarVisible]);
|
||||
|
||||
const isVisible = hoveredBookKey === bookKey || isDropdownOpen;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
`header-bar absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
|
||||
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
|
||||
`shadow-xs bg-base-100 transition-opacity duration-300`,
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey || isDropdownOpen ? `visible` : `opacity-0`,
|
||||
isDropdownOpen && 'header-bar-pinned',
|
||||
'bg-base-100 absolute top-0 w-full',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='sidebar-bookmark-toggler bg-base-100 z-20 flex h-full items-center gap-x-4'>
|
||||
<div className='hidden sm:flex'>
|
||||
<SidebarToggler bookKey={bookKey} />
|
||||
<div
|
||||
className={clsx('absolute top-0 z-10 hidden h-11 w-full sm:flex')}
|
||||
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
|
||||
isVisible ? 'visible' : 'hidden',
|
||||
)}
|
||||
style={{
|
||||
height: systemUIVisible ? `max(env(safe-area-inset-top), ${statusBarHeight}px)` : '',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
|
||||
`shadow-xs transition-[opacity,margin-top] duration-300`,
|
||||
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
isVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
|
||||
isDropdownOpen && 'header-bar-pinned',
|
||||
)}
|
||||
style={{
|
||||
marginTop: systemUIVisible
|
||||
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
|
||||
: 'env(safe-area-inset-top)',
|
||||
}}
|
||||
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
>
|
||||
<div className='sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4'>
|
||||
<div className='hidden sm:flex'>
|
||||
<SidebarToggler bookKey={bookKey} />
|
||||
</div>
|
||||
<BookmarkToggler bookKey={bookKey} />
|
||||
<TranslationToggler bookKey={bookKey} />
|
||||
</div>
|
||||
<BookmarkToggler bookKey={bookKey} />
|
||||
</div>
|
||||
|
||||
<div className='header-title z-15 pointer-events-none absolute inset-0 hidden items-center justify-center sm:flex'>
|
||||
<h2 className='line-clamp-1 max-w-[50%] text-center text-xs font-semibold'>{bookTitle}</h2>
|
||||
</div>
|
||||
<div className='header-title z-15 bg-base-100 pointer-events-none absolute inset-0 hidden items-center justify-center sm:flex'>
|
||||
<h2 className='line-clamp-1 max-w-[50%] text-center text-xs font-semibold'>
|
||||
{bookTitle}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
|
||||
<SettingsToggler />
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
|
||||
onToggle={handleToggleDropdown}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
|
||||
</Dropdown>
|
||||
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
|
||||
<SettingsToggler />
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
|
||||
onToggle={handleToggleDropdown}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
|
||||
</Dropdown>
|
||||
|
||||
<WindowButtons
|
||||
className='window-buttons flex h-full items-center'
|
||||
headerRef={headerRef}
|
||||
showMinimize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
showMaximize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
onClose={() => onCloseBook(bookKey)}
|
||||
/>
|
||||
<WindowButtons
|
||||
className='window-buttons flex h-full items-center'
|
||||
headerRef={headerRef}
|
||||
showMinimize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
showMaximize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
onClose={() => {
|
||||
setHoveredBookKey(null);
|
||||
onCloseBook(bookKey);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { eventDispatcher } from '@/utils/event';
|
||||
interface SectionInfoProps {
|
||||
bookKey: string;
|
||||
showDoubleBorder: boolean;
|
||||
isScrolled: boolean;
|
||||
isVertical: boolean;
|
||||
horizontalGap: number;
|
||||
verticalMargin: number;
|
||||
@@ -13,6 +14,7 @@ interface SectionInfoProps {
|
||||
const HintInfo: React.FC<SectionInfoProps> = ({
|
||||
bookKey,
|
||||
showDoubleBorder,
|
||||
isScrolled,
|
||||
isVertical,
|
||||
horizontalGap,
|
||||
verticalMargin,
|
||||
@@ -45,26 +47,42 @@ const HintInfo: React.FC<SectionInfoProps> = ({
|
||||
}, [hintMessage]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'hintinfo absolute flex items-center justify-end overflow-hidden',
|
||||
hintMessage ? 'bg-base-100' : 'bg-transparent',
|
||||
isVertical ? 'writing-vertical-rl max-h-[50%]' : 'top-0 h-[44px] max-w-[50%]',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
bottom: `${verticalMargin * 1.5}px`,
|
||||
left: `calc(100% - ${horizontalGap}%)`,
|
||||
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
|
||||
}
|
||||
: { insetInlineEnd: `${horizontalGap}%` }
|
||||
}
|
||||
>
|
||||
<h2 className={clsx('text-neutral-content text-center font-sans text-xs font-light')}>
|
||||
{hintMessage || ''}
|
||||
</h2>
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
|
||||
hintMessage ? 'bg-base-100' : 'bg-transparent',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'hintinfo absolute flex items-center justify-end overflow-hidden ps-2',
|
||||
'mt-[env(safe-area-inset-top)]',
|
||||
hintMessage ? 'bg-base-100' : 'bg-transparent',
|
||||
isVertical ? 'writing-vertical-rl' : 'top-0 h-[44px]',
|
||||
isScrolled
|
||||
? isVertical
|
||||
? 'h-full'
|
||||
: 'w-full'
|
||||
: isVertical
|
||||
? 'max-h-[50%]'
|
||||
: 'max-w-[50%]',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
bottom: `${verticalMargin * 1.5}px`,
|
||||
left: `calc(100% - ${horizontalGap}%)`,
|
||||
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
|
||||
}
|
||||
: { insetInlineEnd: `${horizontalGap}%` }
|
||||
}
|
||||
>
|
||||
<h2 className={clsx('text-neutral-content text-center font-sans text-xs font-light')}>
|
||||
{hintMessage || ''}
|
||||
</h2>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { LuNotebookPen } from 'react-icons/lu';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -13,11 +15,16 @@ interface NotebookTogglerProps {
|
||||
|
||||
const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
|
||||
const { isNotebookVisible, toggleNotebook } = useNotebookStore();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
if (appService?.isMobile) {
|
||||
setHoveredBookKey('');
|
||||
}
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleNotebook();
|
||||
} else {
|
||||
|
||||
+28
-17
@@ -1,32 +1,37 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { PageInfo } from '@/types/book';
|
||||
import { PageInfo, TimeInfo } from '@/types/book';
|
||||
|
||||
interface PageInfoProps {
|
||||
bookKey: string;
|
||||
bookFormat: string;
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
showDoubleBorder: boolean;
|
||||
isScrolled: boolean;
|
||||
isVertical: boolean;
|
||||
timeinfo?: TimeInfo;
|
||||
horizontalGap: number;
|
||||
verticalMargin: number;
|
||||
}
|
||||
|
||||
const PageInfoView: React.FC<PageInfoProps> = ({
|
||||
const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
bookKey,
|
||||
bookFormat,
|
||||
section,
|
||||
pageinfo,
|
||||
showDoubleBorder,
|
||||
isScrolled,
|
||||
isVertical,
|
||||
timeinfo,
|
||||
horizontalGap,
|
||||
verticalMargin,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
const isScrolled = viewSettings.scrolled;
|
||||
const isVertical = viewSettings.vertical;
|
||||
const pageInfo = ['PDF', 'CBZ'].includes(bookFormat)
|
||||
? section
|
||||
? isVertical
|
||||
@@ -35,15 +40,19 @@ const PageInfoView: React.FC<PageInfoProps> = ({
|
||||
: ''
|
||||
: pageinfo
|
||||
? _(isVertical ? '{{currentPage}} · {{totalPage}}' : 'Loc. {{currentPage}} / {{totalPage}}', {
|
||||
currentPage: (pageinfo.next ?? pageinfo.current) + 1,
|
||||
currentPage: pageinfo.current + 1,
|
||||
totalPage: pageinfo.total,
|
||||
})
|
||||
: '';
|
||||
const timeInfo = timeinfo
|
||||
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'pageinfo absolute bottom-0 flex items-center justify-end',
|
||||
'pageinfo absolute bottom-0 flex items-center justify-between',
|
||||
'text-neutral-content font-sans text-xs font-extralight',
|
||||
isVertical ? 'writing-vertical-rl' : 'h-12 w-full',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
@@ -53,19 +62,21 @@ const PageInfoView: React.FC<PageInfoProps> = ({
|
||||
bottom: `${verticalMargin * 1.5}px`,
|
||||
left: showDoubleBorder ? `calc(${horizontalGap}% - 32px)` : 0,
|
||||
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
|
||||
height: `calc(100% - ${verticalMargin * 2}px)`,
|
||||
height: `calc(100% - ${verticalMargin * 3}px)`,
|
||||
}
|
||||
: {
|
||||
insetInlineEnd: `${horizontalGap}%`,
|
||||
paddingBottom: appService?.hasSafeAreaInset ? 'env(safe-area-inset-bottom)' : 0,
|
||||
paddingInlineStart: `${horizontalGap}%`,
|
||||
paddingInlineEnd: `${horizontalGap}%`,
|
||||
paddingBottom: appService?.hasSafeAreaInset
|
||||
? 'calc(env(safe-area-inset-bottom)*0.67)'
|
||||
: 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h2 className='text-neutral-content text-right font-sans text-xs font-extralight'>
|
||||
{pageInfo}
|
||||
</h2>
|
||||
{viewSettings.showRemainingTime && <span className='text-start'>{timeInfo}</span>}
|
||||
{viewSettings.showPageNumber && <span className='ms-auto text-end'>{pageInfo}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageInfoView;
|
||||
export default ProgressInfoView;
|
||||
@@ -2,32 +2,90 @@
|
||||
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense, useRef } from 'react';
|
||||
import { useEffect, Suspense, useRef, useState } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { interceptWindowOpen } from '@/utils/open';
|
||||
import { mountAdditionalFonts } from '@/utils/font';
|
||||
import { setSystemUIVisibility } from '@/utils/bridge';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import ReaderContent from './ReaderContent';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const { getVisibleLibrary, setLibrary } = useLibraryStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
const { updateAppTheme } = useThemeStore();
|
||||
useTheme();
|
||||
useTheme({ systemUIVisible: false, appThemeColor: 'base-100' });
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-100');
|
||||
mountAdditionalFonts(document);
|
||||
interceptWindowOpen();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (appService?.isMobileApp && !document.hidden) {
|
||||
dismissSystemUI();
|
||||
setSystemUIVisibility({ visible: false, darkMode: isDarkMode });
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService, isDarkMode]);
|
||||
|
||||
const handleKeyDown = (event: CustomEvent) => {
|
||||
if (event.detail.keyName === 'Back') {
|
||||
setSideBarVisible(false);
|
||||
setNotebookVisible(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isAndroidApp) return;
|
||||
if (isSideBarVisible || isNotebookVisible) {
|
||||
acquireBackKeyInterception();
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
if (!isSideBarVisible && !isNotebookVisible) {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSideBarVisible, isNotebookVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
const initLibrary = async () => {
|
||||
@@ -35,14 +93,27 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const settings = await appService.loadSettings();
|
||||
setSettings(settings);
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
setLibraryLoaded(true);
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isMobileApp) return;
|
||||
const systemUIVisible = !!hoveredBookKey;
|
||||
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
|
||||
if (systemUIVisible) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
dismissSystemUI();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hoveredBookKey]);
|
||||
|
||||
return (
|
||||
getVisibleLibrary().length > 0 &&
|
||||
libraryLoaded &&
|
||||
settings.globalReadSettings && (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -53,6 +124,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
<Suspense>
|
||||
<ReaderContent ids={ids} settings={settings} />
|
||||
<AboutWindow />
|
||||
{appService?.isAndroidApp && <UpdaterWindow />}
|
||||
<Toast />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { Book } from '@/types/book';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { parseOpenWithFiles } from '@/helpers/openWith';
|
||||
import { tauriHandleClose, tauriHandleOnCloseWindow } from '@/utils/window';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
@@ -27,7 +27,6 @@ import Spinner from '@/components/Spinner';
|
||||
import SideBar from './sidebar/SideBar';
|
||||
import Notebook from './notebook/Notebook';
|
||||
import BooksGrid from './BooksGrid';
|
||||
import TTSControl from './tts/TTSControl';
|
||||
|
||||
const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ ids, settings }) => {
|
||||
const router = useRouter();
|
||||
@@ -77,6 +76,12 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (bookKeys && bookKeys.length > 0) {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
settings.lastOpenBooks = bookKeys.map((key) => key.split('-')[0]!);
|
||||
saveSettings(envConfig, settings);
|
||||
}
|
||||
|
||||
if (isTauriAppPlatform()) tauriHandleOnCloseWindow(handleCloseBooks);
|
||||
window.addEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.on('quit-app', handleCloseBooks);
|
||||
@@ -135,7 +140,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
dismissBook(bookKey);
|
||||
if (bookKeys.filter((key) => key !== bookKey).length == 0) {
|
||||
const openWithFiles = (await parseOpenWithFiles()) || [];
|
||||
if (openWithFiles.length > 0) {
|
||||
if (openWithFiles.length > 0 && !appService?.isMobile) {
|
||||
tauriHandleClose();
|
||||
} else {
|
||||
saveSettingsAndGoToLibrary();
|
||||
@@ -160,7 +165,6 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
<div className={clsx('flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
|
||||
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<TTSControl />
|
||||
<Notebook />
|
||||
{showDetailsBook && (
|
||||
<BookDetailModal
|
||||
|
||||
@@ -5,22 +5,24 @@ interface RibbonProps {
|
||||
width: string;
|
||||
}
|
||||
|
||||
const Ribbon: React.FC<RibbonProps> = ({ width }) => {
|
||||
const Ribbon: React.FC<RibbonProps> = ({}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('absolute inset-0 z-10 flex h-11 justify-center')}
|
||||
style={{ width: width }}
|
||||
className={clsx(
|
||||
'fixed inset-0 z-10 flex w-8 justify-center sm:w-6',
|
||||
'h-[calc(env(safe-area-inset-top)+44px)]',
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
stroke='currentColor'
|
||||
fill='currentColor'
|
||||
strokeWidth='0'
|
||||
version='1'
|
||||
viewBox='0 0 20 45'
|
||||
enableBackground='new 0 0 20 45'
|
||||
width='100%'
|
||||
height='100%'
|
||||
preserveAspectRatio='none'
|
||||
viewBox='0 0 100 100'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
shapeRendering='geometricPrecision'
|
||||
imageRendering='optimizeQuality'
|
||||
>
|
||||
<path fill='#F44336' d='M 20 45 L 10 35 L 0 45 L 0 0 L 20 0'></path>
|
||||
<polygon fill='#F44336' points='100 100, 50 78, 0 100, 0 0, 100 0' />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,32 +19,41 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
verticalMargin,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'sectioninfo absolute flex items-center overflow-hidden',
|
||||
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
top: `${verticalMargin * 1.5}px`,
|
||||
left: `calc(100% - ${horizontalGap}%)`,
|
||||
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
|
||||
height: `calc(100% - ${verticalMargin * 2}px)`,
|
||||
}
|
||||
: { insetInlineStart: `${horizontalGap}%`, width: `calc(100% - ${horizontalGap * 2}%)` }
|
||||
}
|
||||
>
|
||||
<h2
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'text-neutral-content text-center font-sans text-xs font-light',
|
||||
isVertical ? '' : 'line-clamp-1',
|
||||
'absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'sectioninfo absolute flex items-center overflow-hidden',
|
||||
'mt-[env(safe-area-inset-top)]',
|
||||
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
top: `${verticalMargin * 1.5}px`,
|
||||
left: `calc(100% - ${horizontalGap}%)`,
|
||||
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
|
||||
height: `calc(100% - ${verticalMargin * 2}px)`,
|
||||
}
|
||||
: { insetInlineStart: `${horizontalGap}%`, width: `calc(100% - ${horizontalGap * 2}%)` }
|
||||
}
|
||||
>
|
||||
{section || ''}
|
||||
</h2>
|
||||
</div>
|
||||
<h2
|
||||
className={clsx(
|
||||
'text-neutral-content text-center font-sans text-xs font-light',
|
||||
isVertical ? '' : 'line-clamp-1',
|
||||
)}
|
||||
>
|
||||
{section || ''}
|
||||
</h2>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { RiFontSize } from 'react-icons/ri';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
const SettingsToggler = () => {
|
||||
const _ = useTranslation();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const handleToggleSettings = () => {
|
||||
setHoveredBookKey('');
|
||||
setFontLayoutSettingsDialogOpen(!isFontLayoutSettingsDialogOpen);
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { RiTranslateAi } from 'react-icons/ri';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { saveViewSettings } from '../utils/viewSettingsHelper';
|
||||
import { isSameLang } from '@/utils/lang';
|
||||
import Button from '@/components/Button';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getViewSettings, setViewSettings, setHoveredBookKey } = useReaderStore();
|
||||
|
||||
const bookData = getBookData(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
|
||||
|
||||
const primaryLanguage = bookData?.book?.primaryLanguage;
|
||||
const targetLanguage = viewSettings.translateTargetLang;
|
||||
|
||||
useEffect(() => {
|
||||
if (translationEnabled === viewSettings.translationEnabled) return;
|
||||
if (appService?.isMobile) {
|
||||
setHoveredBookKey('');
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
|
||||
viewSettings.translationEnabled = translationEnabled;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translationEnabled]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={
|
||||
<RiTranslateAi className={translationEnabled ? 'text-blue-500' : 'text-base-content'} />
|
||||
}
|
||||
disabled={
|
||||
!bookData ||
|
||||
bookData.book?.format === 'PDF' ||
|
||||
isSameLang(primaryLanguage, targetLanguage) ||
|
||||
(!targetLanguage && isSameLang(primaryLanguage, getLocale()))
|
||||
}
|
||||
onClick={() => setTranslationEnabled(!translationEnabled)}
|
||||
tooltip={translationEnabled ? _('Disable Translation') : _('Enable Translation')}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TranslationToggler;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user