Compare commits

...

52 Commits

Author SHA1 Message Date
Huang Xin 4fde9955b2 release: version 0.9.61 (#1479) 2025-06-26 11:27:02 +02:00
Huang Xin 3875c8f239 feat: add an option to show remaining pages in chapter (#1478) 2025-06-26 11:19:30 +02:00
Huang Xin 7bd1b03652 fix: get correct filename when importing txt files on iOS (#1477) 2025-06-26 10:21:55 +02:00
Huang Xin 73878c14e3 fix: EPUB covers occasionally fail to display in library view, closes #1029 (#1476) 2025-06-26 10:03:35 +02:00
Huang Xin bc2c3a2c9a feat: add options to reset settings in the config dialog, closes #1111 (#1475) 2025-06-26 09:59:34 +02:00
Huang Xin 460d37c656 chore: update assetlinks and deeplink for Android (#1474) 2025-06-25 14:47:50 +02:00
Huang Xin fff9e3221e chore: use default production supabase and posthog if not customized, closes #1454 (#1473) 2025-06-25 13:46:48 +02:00
Huang Xin f77fa17cbc bump: update tauri to latest dev branch, closes #1468 (#1471) 2025-06-25 05:36:45 +02:00
Huang Xin d4565f7d60 feat: add an option in the view menu to crop or fit book covers of the library page (#1469) 2025-06-25 03:48:08 +02:00
Huang Xin e8b1976f4f doc: adjust the priority of the readest koplugin for Koreader (#1467) 2025-06-24 08:34:31 +02:00
Huang Xin 6b4bb64885 feat: custom css for the reader UI (#1466) 2025-06-24 08:33:49 +02:00
Huang Xin fc0c42f7db android: fix transparent icon background on Android, closes #1462 (#1464) 2025-06-24 06:54:49 +02:00
Huang Xin 41e5f27d4d layout: view menu now has a maximum width, closes #1459 (#1463)
* refactor: auth callback page

* layout: view menu now has a maximum width, closes #1459
2025-06-24 05:44:03 +02:00
Huang Xin a042b4c739 fix: longer timeout to save changes before reloading page (#1461) 2025-06-24 05:01:46 +02:00
Huang Xin a1c1a9f983 chore: bump opennextjs and wrangler to latest versions (#1460) 2025-06-24 03:22:12 +02:00
Huang Xin 423ae77160 doc: add the Support section in README (#1458) 2025-06-23 12:46:08 +02:00
Huang Xin 05c61a4b1a doc: add donation badge and link with crypto payment (#1457) 2025-06-23 11:37:13 +02:00
Huang Xin ce8f06b7fb chore: add sponsor readest link 2025-06-23 05:03:21 +02:00
Huang Xin f815844577 release: version 0.9.60 (#1452) 2025-06-22 11:24:47 +02:00
Huang Xin 7fe4d38c51 fix: allow inline scripts on Tauri platforms (#1451) 2025-06-22 11:22:39 +02:00
Huang Xin 43edcb4e9e android: dismiss the splash screen and change icon background color in task list (#1450)
* layout: various layout fix on iPad

* android: dismiss the splash screen and change icon background color in task list, closes #1424
2025-06-22 09:10:03 +02:00
Huang Xin e259b29afa fix: disable translation when the primary language is not defined, closes #1443 (#1448) 2025-06-22 07:10:22 +02:00
Huang Xin cd5f57a004 layout: ignore system top/bottom safe insets when there is no header or footer, closes #1442 (#1447) 2025-06-22 06:32:23 +02:00
Huang Xin 8c0cdb0e22 layout: tweaks on layout for iPad (#1446) 2025-06-22 05:55:40 +02:00
初五 c0df9f1cb6 doc: correct spelling 'Themeing' to 'Theming' in README (#1444) 2025-06-22 08:19:21 +08:00
Huang Xin cb1ddaeedb fix: smoother orientation change in iOS, closes #1434 (#1441) 2025-06-21 17:11:45 +02:00
Huang Xin 4447dd7b9a css: fix unintended font size adjust in scrolled mode on iOS, closes #1438 (#1440) 2025-06-21 13:25:46 +02:00
Huang Xin dcb7602837 fix: importing books on iOS at first launch is more reliable now (#1439) 2025-06-21 13:17:24 +02:00
Huang Xin 60e48742fe chore: allow download url for installers (#1437) 2025-06-21 11:13:59 +02:00
Huang Xin dd43569778 chore: download releases using cloudflare cdn (#1436) 2025-06-21 10:36:51 +02:00
Huang Xin 8433dd6950 chore: upload release artifacts to a R2 release bucket (#1435) 2025-06-21 16:24:49 +08:00
Huang Xin 379802f580 release: ready for Google Play release, closes #1001 (#1433) 2025-06-20 16:48:53 +02:00
Huang Xin 2dcb5d7c1e sync: add network timeout of sync to 5 sec (#1432)
* sync: add network timeout of sync to 5 sec

* css: minor tweaks on headings and list
2025-06-20 16:45:10 +02:00
Huang Xin 98870d9f3e release: version 0.9.59 (#1429) 2025-06-19 17:10:14 +02:00
Huang Xin 31aeb2289c settings: more reasonable constraints for top and bottom margins (#1428) 2025-06-19 17:01:20 +02:00
Huang Xin 0f66ecbba1 layout: remove unnecessary inset for header on landscape screen in iOS (#1425) 2025-06-19 12:01:42 +02:00
Huang Xin f0d3c9dd4e fix: transform vw/vh to pixels correctly, closes #1420 (#1421)
* i18n: update translations

* fix: transform vw/vh to pixels correctly, closes #1420
2025-06-19 10:53:58 +02:00
Huang Xin 92aae340d9 settings: unset deprecated margin settings (#1419) 2025-06-18 18:56:41 +02:00
Huang Xin 3cf40039ee release: version 0.9.58 (#1418) 2025-06-18 18:23:09 +02:00
Huang Xin 7ed4937d9d settings: add an option to always show status bar (#1417) 2025-06-18 18:15:55 +02:00
Huang Xin b3593c0365 feat: support split-screen mode on iPad, closes #971 (#1416) 2025-06-18 15:54:46 +02:00
Huang Xin 6235202066 layout: fix issue where sidebars couldn’t be dragged to resize on iPad (#1415) 2025-06-18 15:43:51 +02:00
Huang Xin b3d1085ddf fix: now it's possible to have multiple columns in portrait mode, closes #1369 (#1413) 2025-06-18 14:25:32 +02:00
Huang Xin b2a4ddae60 feat: show annotation create time and various other UI fixes (#1412) 2025-06-18 13:17:18 +02:00
Huang Xin 42f50af27b tts: each book view now has its own tts controller (#1411) 2025-06-18 11:37:02 +02:00
Huang Xin 07f74d8858 settings: individually adjust top, bottom, left, and right margins on the reader page (#1410) 2025-06-18 10:55:24 +02:00
Huang Xin 9477789db1 layout: apply responsive safe area insets to the reader page, closes #1067 and closes #1212 (#1408)
This also fixes layout glitches when showing status bar on devices without display cutouts
2025-06-17 03:36:23 +02:00
Huang Xin 8a7d0e1b0d fix: annotation tools now works when TTS is enabled, closes #1403 (#1406) 2025-06-16 08:53:55 +02:00
Huang Xin 261ce95ed1 feat: add keyboard shortcut t to toggle TTS (#1405) 2025-06-16 08:50:10 +02:00
Huang Xin c5a3b44cbf chore: add NEXT_PUBLIC_API_BASE_URL in example env file (#1404) 2025-06-16 06:02:50 +02:00
Huang Xin ed68d25b15 fix: restore full view settings when reopening book (#1400) 2025-06-15 16:07:51 +02:00
Huang Xin f7f8872a13 tts: translation also when TTS is played in the background, closes #1383 (#1399) 2025-06-15 09:07:22 +02:00
126 changed files with 3343 additions and 1967 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: ['readest']
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+5
View File
@@ -177,6 +177,11 @@ jobs:
pnpm tauri icon ../../data/icons/readest-book.png
git checkout .
MANIFEST="src-tauri/gen/android/app/src/main/AndroidManifest.xml"
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST" || \
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
pushd src-tauri/gen/android
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
+60
View File
@@ -0,0 +1,60 @@
name: Upload Release Assets to R2
on:
workflow_call:
inputs:
tag:
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: 'Release tag name (e.g., v1.2.3)'
required: true
type: string
jobs:
upload-to-r2:
runs-on: ubuntu-latest
env:
RELEASE_R2_BUCKET: readest-releases
RELEASE_R2_ACCOUNT_ID: ${{ secrets.RELEASE_R2_ACCOUNT_ID }}
RELEASE_R2_ACCESS_KEY_ID: ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
RELEASE_R2_SECRET_ACCESS_KEY: ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
steps:
- name: Download release assets
run: |
gh release download "${{ inputs.tag }}" --repo readest/readest --dir ./release-assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rclone
run: curl https://rclone.org/install.sh | sudo bash
- name: Configure rclone
run: |
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf <<EOF
[r2]
type = s3
provider = Cloudflare
access_key_id = $RELEASE_R2_ACCESS_KEY_ID
secret_access_key = $RELEASE_R2_SECRET_ACCESS_KEY
endpoint = https://${RELEASE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com
EOF
- name: Modify latest.json download URLs
run: |
GITHUB_BASE_URL="https://github.com/readest/readest/releases/download"
READEST_BASE_URL="https://download.readest.com/releases"
sed -i "s#${GITHUB_BASE_URL}#${READEST_BASE_URL}#g" ./release-assets/latest.json
- name: Upload to R2
run: |
mkdir releases
mv ./release-assets/latest.json releases
mv ./release-assets/release-notes.json releases
rclone copy ./release-assets r2:${RELEASE_R2_BUCKET}/releases/${{ inputs.tag }}/
rclone copy ./releases r2:${RELEASE_R2_BUCKET}/releases/
Generated
+580 -670
View File
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -15,6 +15,8 @@
[![][badge-discord]][link-discord]
[![AGPL Licence][badge-license]](LICENSE)
[![Latest release][badge-release]][link-gh-releases]
[![Donate][badge-donate]][link-donate]
<br>
[![Last commit][badge-last-commit]][link-gh-commits]
[![Commits][badge-commit-activity]][link-gh-pulse]
@@ -27,6 +29,7 @@
<a href="#downloads">Downloads</a> •
<a href="#getting-started">Getting Started</a> •
<a href="#troubleshooting">Troubleshooting</a> •
<a href="#support">Support</a> •
<a href="#license">License</a>
</p>
@@ -64,8 +67,8 @@
| **Feature** | **Description** | **Priority** |
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
@@ -87,7 +90,7 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
![Wikipedia](./data/screenshots/wikipedia_vertical.png)
![Themeing Dark Mode](./data/screenshots/theming_dark_mode.png)
![Theming Dark Mode](./data/screenshots/theming_dark_mode.png)
---
@@ -243,6 +246,20 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
</p>
</a>
## Support
If Readest has been useful to you, consider supporting its development. Your contribution helps us squash bugs faster, improve performance, and keep building great features.
### How to Donate
1. **GitHub Sponsors**
Back the project directly on GitHub:
👉 [https://github.com/sponsors/readest](https://github.com/sponsors/readest)
2. **Crypto Donations**
Prefer crypto? You can donate here:
👉 [https://donate.readest.com/](https://donate.readest.com/)
## License
Readest is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file for details.
@@ -277,6 +294,8 @@ The following fonts are utilized in this software, either bundled within the app
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
[badge-donate]: https://donate.readest.com/badge.svg
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
[link-web-readest]: https://web.readest.com
+5 -2
View File
@@ -2,5 +2,8 @@ PDFJS_BUILD_PATH=../../packages/foliate-js/node_modules/pdfjs-dist/legacy/build
PDFJS_FONTS_PATH=../../packages/foliate-js/node_modules/pdfjs-dist
PDFJS_STYLE_PATH=../../packages/foliate-js/vendor/pdfjs
NEXT_PUBLIC_DEV_SUPABASE_URL=https://gxkhxxxeapexynpouyjz.supabase.co
NEXT_PUBLIC_DEV_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd4a2h4eHhlYXBleHlucG91eWp6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ0MzAwNTksImV4cCI6MjA1MDAwNjA1OX0.jhinkQsimQoidsg_U59YD5ROw4PmMJQNKuyXbr4TiQA
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64="aHR0cHM6Ly91cy5pLnBvc3Rob2cuY29t"
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThpTmRSNTNsR1A3VFM3VGh4S08="
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
+2
View File
@@ -6,6 +6,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824
NEXT_PUBLIC_API_BASE_URL=https://your-api-base-url.com
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS
+19 -17
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.57",
"version": "0.9.61",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -30,6 +30,7 @@
"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",
"release-google-play": "dotenv -e .env.google-play.local -- bash scripts/release-google-play.sh",
"config-wrangler": "sed -i \"s/\\${TRANSLATIONS_KV_ID}/$TRANSLATIONS_KV_ID/g\" wrangler.toml",
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
@@ -41,28 +42,29 @@
"@aws-sdk/s3-request-presigner": "^3.735.0",
"@ducanh2912/next-pwa": "^10.2.9",
"@fabianlars/tauri-plugin-oauth": "2",
"@opennextjs/cloudflare": "^1.1.0",
"@opennextjs/cloudflare": "^1.3.1",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.49.10",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-haptics": "^2.2.4",
"@tauri-apps/plugin-http": "^2.4.3",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.1",
"@tauri-apps/api": "2.6.0",
"@tauri-apps/plugin-cli": "^2.3.0",
"@tauri-apps/plugin-deep-link": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-haptics": "^2.2.5",
"@tauri-apps/plugin-http": "^2.4.4",
"@tauri-apps/plugin-log": "^2.5.1",
"@tauri-apps/plugin-opener": "^2.3.1",
"@tauri-apps/plugin-os": "^2.2.2",
"@tauri-apps/plugin-process": "^2.2.2",
"@tauri-apps/plugin-shell": "~2.2.2",
"@tauri-apps/plugin-updater": "^2.8.1",
"@zip.js/zip.js": "^2.7.53",
"aws4fetch": "^1.0.20",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"cssbeautify": "^0.3.1",
"dayjs": "^1.11.13",
"foliate-js": "workspace:*",
"highlight.js": "^11.11.1",
"i18next": "^24.2.0",
@@ -86,7 +88,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "2.5.0",
"@tauri-apps/cli": "2.6.0",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -111,6 +113,6 @@
"raw-loader": "^4.0.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"wrangler": "^4.19.1"
"wrangler": "^4.21.0"
}
}
@@ -1,11 +1,15 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.bilingify.readest",
"sha256_cert_fingerprints": [
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82"
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82",
"E0:E7:60:55:80:8D:3A:DE:A0:D1:CF:7C:20:85:40:A3:DD:4B:E6:4D:17:5C:0F:DE:26:57:7D:9C:5B:29:5F:51"
]
}
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "تأكيد الحذف",
"Copied to notebook": "تم النسخ إلى المفكرة",
"Copy": "نسخ",
"Custom CSS": "CSS مخصص",
"Dark Mode": "الوضع الداكن",
"Default": "الافتراضي",
"Default Font": "الخط الافتراضي",
@@ -28,7 +27,6 @@
"Dictionary": "القاموس",
"Download Readest": "تحميل ريديست",
"Edit": "تحرير",
"Enter your custom CSS here...": "أدخل الـ CSS المخصص هنا...",
"Excerpts": "مقتطفات",
"Failed to import book(s): {{filenames}}": "فشل استيراد الكتاب/الكتب: {{filenames}}",
"Fast": "سريع",
@@ -114,8 +112,6 @@
"Your Library": "المكتبة الخاصة بك",
"TTS not supported for PDF": "القراءة الصوتية غير مدعومة لملفات PDF",
"Override Book Font": "تجاوز خط الكتاب",
"Vertical Margins (px)": "هوامش عمودية (بكسل)",
"Horizontal Margins (%)": "هوامش أفقية (%)",
"Apply to All Books": "تطبيق على جميع الكتب",
"Apply to This Book": "تطبيق على هذا الكتاب",
"Unable to fetch the translation. Try again later.": "تعذر جلب الترجمة. حاول مرة أخرى لاحقًا.",
@@ -266,7 +262,7 @@
"Clear Search": "مسح البحث",
"Header & Footer": "الترويسة والتذييل",
"Apply also in Scrolled Mode": "تطبيق أيضًا في وضع التمرير",
"A new version of Readest is Available!": "يتوفر إصدار جديد من Readest!",
"A new version of Readest is available!": "يتوفر إصدار جديد من Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "إصدار {{newVersion}} من ريديست متاح للتنزيل (الإصدار المثبت حاليًا {{currentVersion}}).",
"Download and install now?": "هل ترغب في تنزيله وتثبيته الآن؟",
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
@@ -371,5 +367,28 @@
"Contact Support": "اتصل بالدعم الفني",
"Code Highlighting": "تمييز الكود",
"Enable Highlighting": "تمكين التمييز",
"Code Language": "لغة الكود"
"Code Language": "لغة الكود",
"Top Margin (px)": "الهامش العلوي (بكسل)",
"Bottom Margin (px)": "الهامش السفلي (بكسل)",
"Right Margin (px)": "الهامش الأيمن (بكسل)",
"Left Margin (px)": "الهامش الأيسر (بكسل)",
"Column Gap (%)": "تباعد الأعمدة (%)",
"Always Show Status Bar": "دائمًا إظهار شريط الحالة",
"Translation Not Available": "الترجمة غير متاحة",
"Custom Content CSS": "CSS مخصص للمحتوى",
"Enter CSS for book content styling...": "أدخل CSS لتنسيق محتوى الكتاب...",
"Custom Reader UI CSS": "CSS مخصص لواجهة القارئ",
"Enter CSS for reader interface styling...": "أدخل CSS لتنسيق واجهة القارئ...",
"Crop": "قص",
"Book Covers": "أغلفة الكتب",
"Fit": "ملاءمة",
"Reset {{settings}}": "إعادة تعيين {{settings}}",
"Reset Settings": "إعادة تعيين الإعدادات",
"{{count}} pages left in chapter_zero": "لم يتبق أي صفحات في هذا الفصل",
"{{count}} pages left in chapter_one": "تبقّت صفحة واحدة في هذا الفصل",
"{{count}} pages left in chapter_two": "تبقّت صفحتان في هذا الفصل",
"{{count}} pages left in chapter_few": "تبقّت {{count}} صفحات في هذا الفصل",
"{{count}} pages left in chapter_many": "تبقّت {{count}} صفحة في هذا الفصل",
"{{count}} pages left in chapter_other": "تبقّى {{count}} صفحة في هذا الفصل",
"Show Remaining Pages": "عرض الصفحات المتبقية"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Löschen bestätigen",
"Copied to notebook": "In Notizbuch kopiert",
"Copy": "Kopieren",
"Custom CSS": "Benutzerdefiniertes CSS",
"Dark Mode": "Dunkelmodus",
"Default": "Standard",
"Default Font": "Standardschriftart",
@@ -28,7 +27,6 @@
"Dictionary": "Wörterbuch",
"Download Readest": "Readest herunterladen",
"Edit": "Bearbeiten",
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
"Excerpts": "Auszüge",
"Failed to import book(s): {{filenames}}": "Fehler beim Importieren von Buch/Büchern: {{filenames}}",
"Fast": "Schnell",
@@ -114,8 +112,6 @@
"Your Library": "Ihre Bibliothek",
"TTS not supported for PDF": "TTS wird für PDF nicht unterstützt",
"Override Book Font": "Buch-Schriftart überschreiben",
"Vertical Margins (px)": "Vertikale Ränder (px)",
"Horizontal Margins (%)": "Horizontale Ränder (%)",
"Apply to All Books": "Auf alle Bücher anwenden",
"Apply to This Book": "Auf dieses Buch anwenden",
"Unable to fetch the translation. Try again later.": "Übersetzung konnte nicht abgerufen werden. Versuchen Sie es später erneut.",
@@ -266,7 +262,7 @@
"Clear Search": "Suche löschen",
"Header & Footer": "Kopf- & Fußzeile",
"Apply also in Scrolled Mode": "Auch im Scroll-Modus anwenden",
"A new version of Readest is Available!": "Eine neue Version von Readest ist verfügbar!",
"A new version of Readest is available!": "Eine neue Version von Readest ist verfügbar!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} ist verfügbar (installierte Version {{currentVersion}}).",
"Download and install now?": "Jetzt herunterladen und installieren?",
"Downloading {{downloaded}} of {{contentLength}}": "Herunterladen {{downloaded}} von {{contentLength}}",
@@ -359,5 +355,24 @@
"Contact Support": "Support kontaktieren",
"Code Highlighting": "Code-Hervorhebung",
"Enable Highlighting": "Code-Hervorhebung aktivieren",
"Code Language": "Programmiersprache"
"Code Language": "Programmiersprache",
"Top Margin (px)": "Oberer Rand (px)",
"Bottom Margin (px)": "Unterer Rand (px)",
"Right Margin (px)": "Rechter Rand (px)",
"Left Margin (px)": "Linker Rand (px)",
"Column Gap (%)": "Spaltenabstand (%)",
"Always Show Status Bar": "Statusleiste immer anzeigen",
"Translation Not Available": "Übersetzung nicht verfügbar",
"Custom Content CSS": "Benutzerdefiniertes Inhalts-CSS",
"Enter CSS for book content styling...": "CSS für die Buchinhaltsgestaltung eingeben...",
"Custom Reader UI CSS": "Benutzerdefiniertes CSS für die Leseroberfläche",
"Enter CSS for reader interface styling...": "CSS für die Leseroberfläche eingeben...",
"Crop": "Zuschneiden",
"Book Covers": "Buchcover",
"Fit": "Anpassen",
"Reset {{settings}}": "{{settings}} zurücksetzen",
"Reset Settings": "Einstellungen zurücksetzen",
"{{count}} pages left in chapter_one": "{{count}} Seite verbleibend im Kapitel",
"{{count}} pages left in chapter_other": "{{count}} Seiten verbleibend im Kapitel",
"Show Remaining Pages": "Verbleibende Seiten anzeigen"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Επιβεβαίωση διαγραφής",
"Copied to notebook": "Αντιγράφηκε στο σημειωματάριο",
"Copy": "Αντιγραφή",
"Custom CSS": "Προσαρμοσμένο CSS",
"Dark Mode": "Σκοτεινή λειτουργία",
"Default": "Προεπιλογή",
"Default Font": "Προεπιλεγμένη γραμματοσειρά",
@@ -28,7 +27,6 @@
"Dictionary": "Λεξικό",
"Download Readest": "Λήψη Readest",
"Edit": "Επεξεργασία",
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
"Excerpts": "Αποσπάσματα",
"Failed to import book(s): {{filenames}}": "Αποτυχία εισαγωγής βιβλίου(ων): {{filenames}}",
"Fast": "Γρήγορα",
@@ -114,8 +112,6 @@
"Your Library": "Η βιβλιοθήκη σας",
"TTS not supported for PDF": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται για αρχεία PDF",
"Override Book Font": "Παράκαμψη γραμματοσειράς βιβλίου",
"Vertical Margins (px)": "Κάθετα περιθώρια (px)",
"Horizontal Margins (%)": "Οριζόντια περιθώρια (%)",
"Apply to All Books": "Εφαρμογή σε όλα τα βιβλία",
"Apply to This Book": "Εφαρμογή σε αυτό το βιβλίο",
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
@@ -266,7 +262,7 @@
"Clear Search": "Καθαρισμός αναζήτησης",
"Header & Footer": "Κεφαλίδα & Υποσέλιδο",
"Apply also in Scrolled Mode": "Εφαρμογή και σε λειτουργία κύλισης",
"A new version of Readest is Available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
"A new version of Readest is available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Η έκδοση Readest {{newVersion}} είναι διαθέσιμη (έκδοση που είναι εγκατεστημένη {{currentVersion}}).",
"Download and install now?": "Θέλετε να την κατεβάσετε και να την εγκαταστήσετε τώρα;",
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
@@ -359,5 +355,24 @@
"Contact Support": "Επικοινωνήστε με την υποστήριξη",
"Code Highlighting": "Επισήμανση κώδικα",
"Enable Highlighting": "Ενεργοποίηση επισήμανσης",
"Code Language": "Γλώσσα κώδικα"
"Code Language": "Γλώσσα κώδικα",
"Top Margin (px)": "Πάνω περιθώριο (px)",
"Bottom Margin (px)": "Κάτω περιθώριο (px)",
"Right Margin (px)": "Δεξί περιθώριο (px)",
"Left Margin (px)": "Αριστερό περιθώριο (px)",
"Column Gap (%)": "Απόσταση στηλών (%)",
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης",
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη",
"Custom Content CSS": "Προσαρμοσμένο CSS περιεχομένου",
"Enter CSS for book content styling...": "Εισαγάγετε CSS για στυλ περιεχομένου βιβλίου...",
"Custom Reader UI CSS": "Προσαρμοσμένο CSS διεπαφής αναγνώστη",
"Enter CSS for reader interface styling...": "Εισαγάγετε CSS για στυλ διεπαφής αναγνώστη...",
"Crop": "Περικοπή",
"Book Covers": "Εξώφυλλα βιβλίων",
"Fit": "Προσαρμογή",
"Reset {{settings}}": "Επαναφορά {{settings}}",
"Reset Settings": "Επαναφορά ρυθμίσεων",
"{{count}} pages left in chapter_one": "Μένει {{count}} σελίδα",
"{{count}} pages left in chapter_other": "Μένουν {{count}} σελίδες",
"Show Remaining Pages": "Εμφάνιση υπολοίπων"
}
@@ -4,5 +4,7 @@
"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..."
"Search in {{count}} Book(s)..._other": "Search in {{count}} books...",
"{{count}} pages left in chapter_one": "{{count}} page left in chapter",
"{{count}} pages left in chapter_other": "{{count}} pages left in chapter"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmar eliminación",
"Copied to notebook": "Copiado al cuaderno",
"Copy": "Copiar",
"Custom CSS": "CSS personalizado",
"Dark Mode": "Modo oscuro",
"Default": "Predeterminado",
"Default Font": "Fuente predeterminada",
@@ -28,7 +27,6 @@
"Dictionary": "Diccionario",
"Download Readest": "Descargar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
"Excerpts": "Extractos",
"Failed to import book(s): {{filenames}}": "Error al importar libro(s): {{filenames}}",
"Fast": "Rápido",
@@ -114,8 +112,6 @@
"Your Library": "Tu biblioteca",
"TTS not supported for PDF": "TTS no es compatible con PDF",
"Override Book Font": "Sobrescribir fuente del libro",
"Vertical Margins (px)": "Márgenes verticales (px)",
"Horizontal Margins (%)": "Márgenes horizontales (%)",
"Apply to All Books": "Aplicar a todos los libros",
"Apply to This Book": "Aplicar a este libro",
"Unable to fetch the translation. Try again later.": "No se puede obtener la traducción. Inténtalo de nuevo más tarde.",
@@ -266,7 +262,7 @@
"Clear Search": "Limpiar búsqueda",
"Header & Footer": "Encabezado y pie de página",
"Apply also in Scrolled Mode": "Aplicar también en modo desplazamiento",
"A new version of Readest is Available!": "¡Una nueva versión de Readest está disponible!",
"A new version of Readest is available!": "¡Una nueva versión de Readest está disponible!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponible (versión instalada {{currentVersion}}).",
"Download and install now?": "Descargar e instalar ahora?",
"Downloading {{downloaded}} of {{contentLength}}": "Descargando {{downloaded}} de {{contentLength}}",
@@ -362,5 +358,25 @@
"Contact Support": "Contactar con soporte",
"Code Highlighting": "Resaltado de código",
"Enable Highlighting": "Activar resaltado",
"Code Language": "Lenguaje de código"
"Code Language": "Lenguaje de código",
"Top Margin (px)": "Margen superior (px)",
"Bottom Margin (px)": "Margen inferior (px)",
"Right Margin (px)": "Margen derecho (px)",
"Left Margin (px)": "Margen izquierdo (px)",
"Column Gap (%)": "Espacio entre columnas (%)",
"Always Show Status Bar": "Mostrar siempre la barra de estado",
"Translation Not Available": "Traducción no disponible",
"Custom Content CSS": "CSS del contenido",
"Enter CSS for book content styling...": "Introduce CSS para el contenido del libro...",
"Custom Reader UI CSS": "CSS de la interfaz",
"Enter CSS for reader interface styling...": "Introduce CSS para la interfaz de lectura...",
"Crop": "Recortar",
"Book Covers": "Portadas de libros",
"Fit": "Encajar",
"Reset {{settings}}": "Restablecer {{settings}}",
"Reset Settings": "Restablecer configuración",
"{{count}} pages left in chapter_one": "{{count}} página restante en el capítulo",
"{{count}} pages left in chapter_many": "{{count}} páginas restantes en el capítulo",
"{{count}} pages left in chapter_other": "{{count}} páginas restantes en el capítulo",
"Show Remaining Pages": "Mostrar resto"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmer la suppression",
"Copied to notebook": "Copié dans le carnet de notes",
"Copy": "Copier",
"Custom CSS": "CSS personnalisé",
"Dark Mode": "Mode sombre",
"Default": "Par défaut",
"Default Font": "Police par défaut",
@@ -28,7 +27,6 @@
"Dictionary": "Dictionnaire",
"Download Readest": "Télécharger Readest",
"Edit": "Modifier",
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
"Excerpts": "Extraits",
"Failed to import book(s): {{filenames}}": "Échec de l'importation du/des livre(s) : {{filenames}}",
"Fast": "Rapide",
@@ -114,8 +112,6 @@
"Your Library": "Votre bibliothèque",
"TTS not supported for PDF": "La synthèse vocale n'est pas prise en charge pour les fichiers PDF",
"Override Book Font": "Remplacer la police du livre",
"Vertical Margins (px)": "Marges verticales (px)",
"Horizontal Margins (%)": "Marges horizontales (%)",
"Apply to All Books": "Appliquer à tous les livres",
"Apply to This Book": "Appliquer à ce livre",
"Unable to fetch the translation. Try again later.": "Impossible de récupérer la traduction. Réessayez plus tard.",
@@ -266,7 +262,7 @@
"Clear Search": "Effacer la recherche",
"Header & Footer": "En-tête et pied de page",
"Apply also in Scrolled Mode": "Appliquer également en mode défilement",
"A new version of Readest is Available!": "Avis de mise à jour",
"A new version of Readest is available!": "Avis de mise à jour",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} est disponible (version installée {{currentVersion}}).",
"Download and install now?": "Télécharger et installer maintenant ?",
"Downloading {{downloaded}} of {{contentLength}}": "Téléchargement {{downloaded}} sur {{contentLength}}",
@@ -362,5 +358,25 @@
"Contact Support": "Contacter le support",
"Code Highlighting": "Coloration syntaxique",
"Enable Highlighting": "Activer la coloration syntaxique",
"Code Language": "Langage de programmation"
"Code Language": "Langage de programmation",
"Top Margin (px)": "Marge supérieure (px)",
"Bottom Margin (px)": "Marge inférieure (px)",
"Right Margin (px)": "Marge droite (px)",
"Left Margin (px)": "Marge gauche (px)",
"Column Gap (%)": "Espacement des colonnes (%)",
"Always Show Status Bar": "Afficher toujours la barre d'état",
"Translation Not Available": "Traduction non disponible",
"Custom Content CSS": "CSS du contenu",
"Enter CSS for book content styling...": "Saisissez le CSS pour le contenu du livre...",
"Custom Reader UI CSS": "CSS de linterface",
"Enter CSS for reader interface styling...": "Saisissez le CSS pour linterface de lecture...",
"Crop": "Rogner",
"Book Covers": "Couvertures de livres",
"Fit": "Adapter",
"Reset {{settings}}": "Réinitialiser {{settings}}",
"Reset Settings": "Réinitialiser les paramètres",
"{{count}} pages left in chapter_one": "{{count}} page restant dans le chapitre",
"{{count}} pages left in chapter_many": "{{count}} pages restantes dans le chapitre",
"{{count}} pages left in chapter_other": "{{count}} pages restantes dans le chapitre",
"Show Remaining Pages": "Voir restantes"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "हटाने की पुष्टि करें",
"Copied to notebook": "नोटबुक में कॉपी किया गया",
"Copy": "कॉपी",
"Custom CSS": "कस्टम CSS",
"Dark Mode": "डार्क मोड",
"Default": "डिफ़ॉल्ट",
"Default Font": "डिफ़ॉल्ट फ़ॉन्ट",
@@ -28,7 +27,6 @@
"Dictionary": "शब्दकोश",
"Download Readest": "Readest डाउनलोड करें",
"Edit": "संपादित करें",
"Enter your custom CSS here...": "अपना कस्टम CSS यहां दर्ज करें...",
"Excerpts": "अंश",
"Failed to import book(s): {{filenames}}": "पुस्तक(ें) आयात करने में विफल: {{filenames}}",
"Fast": "तेज़",
@@ -114,8 +112,6 @@
"Your Library": "आपकी लाइब्रेरी",
"TTS not supported for PDF": "PDF के लिए TTS समर्थित नहीं है",
"Override Book Font": "पुस्तक फ़ॉन्ट ओवरराइड करें",
"Vertical Margins (px)": "ऊर्ध्वाधर मार्जिन (px)",
"Horizontal Margins (%)": "क्षैतिज मार्जिन (%)",
"Apply to All Books": "सभी पुस्तकों पर लागू करें",
"Apply to This Book": "इस पुस्तक पर लागू करें",
"Unable to fetch the translation. Try again later.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
@@ -266,7 +262,7 @@
"Clear Search": "खोज साफ करें",
"Header & Footer": "हेडर और फ़ुटर",
"Apply also in Scrolled Mode": "स्क्रॉल मोड में भी लागू करें",
"A new version of Readest is Available!": "Readest का एक नया संस्करण उपलब्ध है!",
"A new version of Readest is available!": "Readest का एक नया संस्करण उपलब्ध है!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} उपलब्ध है (स्थापित संस्करण {{currentVersion}})।",
"Download and install now?": "अब डाउनलोड और इंस्टॉल करें?",
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
@@ -359,5 +355,24 @@
"Contact Support": "सहायता से संपर्क करें",
"Code Highlighting": "कोड हाइलाइटिंग",
"Enable Highlighting": "हाइलाइटिंग सक्षम करें",
"Code Language": "कोड भाषा"
"Code Language": "कोड भाषा",
"Top Margin (px)": "ऊपरी हाशिया (px)",
"Bottom Margin (px)": "निचला हाशिया (px)",
"Right Margin (px)": "दायां हाशिया (px)",
"Left Margin (px)": "बायां हाशिया (px)",
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं",
"Translation Not Available": "अनुवाद उपलब्ध नहीं है",
"Custom Content CSS": "सामग्री का CSS",
"Enter CSS for book content styling...": "पुस्तक सामग्री के लिए CSS दर्ज करें...",
"Custom Reader UI CSS": "इंटरफ़ेस का CSS",
"Enter CSS for reader interface styling...": "रीडर इंटरफ़ेस के लिए CSS दर्ज करें...",
"Crop": "काटें",
"Book Covers": "पुस्तक कवर",
"Fit": "फिट",
"Reset {{settings}}": "रीसेट {{settings}}",
"Reset Settings": "सेटिंग्स रीसेट करें",
"{{count}} pages left in chapter_one": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
"{{count}} pages left in chapter_other": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
"Show Remaining Pages": "शेष पृष्ठ दिखाएँ"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Konfirmasi Penghapusan",
"Copied to notebook": "Disalin ke buku catatan",
"Copy": "Salin",
"Custom CSS": "CSS Kustom",
"Dark Mode": "Mode Gelap",
"Default": "Default",
"Default Font": "Font Default",
@@ -28,7 +27,6 @@
"Dictionary": "Kamus",
"Download Readest": "Unduh Readest",
"Edit": "Edit",
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
"Excerpts": "Kutipan",
"Failed to import book(s): {{filenames}}": "Gagal mengimpor buku: {{filenames}}",
"Fast": "Cepat",
@@ -114,8 +112,6 @@
"Your Library": "Perpustakaan Anda",
"TTS not supported for PDF": "TTS tidak didukung untuk PDF",
"Override Book Font": "Ganti Font Buku",
"Vertical Margins (px)": "Margin Vertikal (px)",
"Horizontal Margins (%)": "Margin Horizontal (%)",
"Apply to All Books": "Terapkan ke semua buku",
"Apply to This Book": "Terapkan ke buku ini",
"Unable to fetch the translation. Try again later.": "Tidak dapat mengambil terjemahan. Coba lagi nanti.",
@@ -266,7 +262,7 @@
"Clear Search": "Hapus Pencarian",
"Header & Footer": "Header & Footer",
"Apply also in Scrolled Mode": "Terapkan juga di Mode Gulir",
"A new version of Readest is Available!": "Versi baru Readest Tersedia!",
"A new version of Readest is available!": "Versi baru Readest Tersedia!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} tersedia (versi terinstal {{currentVersion}}).",
"Download and install now?": "Unduh dan instal sekarang?",
"Downloading {{downloaded}} of {{contentLength}}": "Mengunduh {{downloaded}} dari {{contentLength}}",
@@ -356,5 +352,23 @@
"Contact Support": "Hubungi Dukungan",
"Code Highlighting": "Penyorotan Kode",
"Enable Highlighting": "Aktifkan Penyorotan",
"Code Language": "Bahasa Kode"
"Code Language": "Bahasa Kode",
"Top Margin (px)": "Margin Atas (px)",
"Bottom Margin (px)": "Margin Bawah (px)",
"Right Margin (px)": "Margin Kanan (px)",
"Left Margin (px)": "Margin Kiri (px)",
"Column Gap (%)": "Jarak Antar Kolom (%)",
"Always Show Status Bar": "Selalu Tampilkan Bilah Status",
"Translation Not Available": "Terjemahan Tidak Tersedia",
"Custom Content CSS": "CSS konten",
"Enter CSS for book content styling...": "Masukkan CSS untuk gaya konten buku...",
"Custom Reader UI CSS": "CSS antarmuka",
"Enter CSS for reader interface styling...": "Masukkan CSS untuk gaya antarmuka pembaca...",
"Crop": "Potong",
"Book Covers": "Sampul Buku",
"Fit": "Pas",
"Reset {{settings}}": "Atur Ulang {{settings}}",
"Reset Settings": "Atur Ulang Pengaturan",
"{{count}} pages left in chapter_other": "{{count}} halaman tersisa di bab ini",
"Show Remaining Pages": "Tampilkan halaman tersisa"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Conferma eliminazione",
"Copied to notebook": "Copiato nel quaderno",
"Copy": "Copia",
"Custom CSS": "CSS personalizzato",
"Dark Mode": "Modalità scura",
"Default": "Predefinito",
"Default Font": "Font predefinito",
@@ -28,7 +27,6 @@
"Dictionary": "Dizionario",
"Download Readest": "Scarica Readest",
"Edit": "Modifica",
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
"Excerpts": "Estratti",
"Failed to import book(s): {{filenames}}": "Impossibile importare il/i libro/i: {{filenames}}",
"Fast": "Veloce",
@@ -114,8 +112,6 @@
"Your Library": "La tua biblioteca",
"TTS not supported for PDF": "TTS non supportato per PDF",
"Override Book Font": "Sovrascrivi font libro",
"Vertical Margins (px)": "Margini verticali (px)",
"Horizontal Margins (%)": "Margini orizzontali (%)",
"Apply to All Books": "Applica a tutti i libri",
"Apply to This Book": "Applica a questo libro",
"Unable to fetch the translation. Try again later.": "Impossibile recuperare la traduzione. Riprova più tardi.",
@@ -266,7 +262,7 @@
"Clear Search": "Cancella ricerca",
"Header & Footer": "Intestazione e piè di pagina",
"Apply also in Scrolled Mode": "Applica anche in modalità scorrimento",
"A new version of Readest is Available!": "È disponibile una nuova versione di Readest!",
"A new version of Readest is available!": "È disponibile una nuova versione di Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} è disponibile (versione installata {{currentVersion}}).",
"Download and install now?": "Scarica e installa ora?",
"Downloading {{downloaded}} of {{contentLength}}": "Scaricando {{downloaded}} di {{contentLength}}",
@@ -362,5 +358,25 @@
"Contact Support": "Contatta il supporto",
"Code Highlighting": "Evidenziazione codice",
"Enable Highlighting": "Abilita evidenziazione",
"Code Language": "Tipo di codice"
"Code Language": "Tipo di codice",
"Top Margin (px)": "Margine Superiore (px)",
"Bottom Margin (px)": "Margine Inferiore (px)",
"Right Margin (px)": "Margine Destro (px)",
"Left Margin (px)": "Margine Sinistro (px)",
"Column Gap (%)": "Spazio tra Colonne (%)",
"Always Show Status Bar": "Mostra sempre la barra di stato",
"Translation Not Available": "Traduzione non disponibile",
"Custom Content CSS": "CSS contenuto",
"Enter CSS for book content styling...": "Inserisci il CSS per il contenuto del libro...",
"Custom Reader UI CSS": "CSS interfaccia",
"Enter CSS for reader interface styling...": "Inserisci il CSS per l'interfaccia di lettura...",
"Crop": "Ritaglia",
"Book Covers": "Copertine libri",
"Fit": "Adatta",
"Reset {{settings}}": "Reimposta {{settings}}",
"Reset Settings": "Reimposta impostazioni",
"{{count}} pages left in chapter_one": "{{count}} pagina rimasta nel capitolo",
"{{count}} pages left in chapter_many": "{{count}} pagine rimaste nel capitolo",
"{{count}} pages left in chapter_other": "{{count}} pagine rimaste nel capitolo",
"Show Remaining Pages": "Mostra pagine rimanenti"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "削除の確認",
"Copied to notebook": "ノートブックにコピー",
"Copy": "コピー",
"Custom CSS": "カスタムCSS",
"Dark Mode": "ダークモード",
"Default": "デフォルト",
"Default Font": "デフォルトフォント",
@@ -28,7 +27,6 @@
"Dictionary": "辞書",
"Download Readest": "Readestをダウンロード",
"Edit": "編集",
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
"Excerpts": "抜粋",
"Failed to import book(s): {{filenames}}": "書籍のインポートに失敗しました:{{filenames}}",
"Fast": "高速",
@@ -114,8 +112,6 @@
"Your Library": "ライブラリー",
"TTS not supported for PDF": "PDFではTTSがサポートされていません",
"Override Book Font": "書籍フォントを上書き",
"Vertical Margins (px)": "縦マージン(px",
"Horizontal Margins (%)": "横マージン(%",
"Apply to All Books": "すべての書籍に適用",
"Apply to This Book": "この書籍に適用",
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。",
@@ -266,7 +262,7 @@
"Clear Search": "検索をクリア",
"Header & Footer": "ヘッダーとフッター",
"Apply also in Scrolled Mode": "スクロールモードでも適用",
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
"A new version of Readest is available!": "新しいバージョンのReadestが利用可能です!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}が利用可能です(インストールされたバージョン{{currentVersion}})。",
"Download and install now?": "今すぐダウンロードしてインストールしますか?",
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
@@ -356,5 +352,23 @@
"Contact Support": "サポートに連絡",
"Code Highlighting": "コードハイライト",
"Enable Highlighting": "ハイライトを有効にする",
"Code Language": "コード言語"
"Code Language": "コード言語",
"Top Margin (px)": "上マージン (px)",
"Bottom Margin (px)": "下マージン (px)",
"Right Margin (px)": "右マージン (px)",
"Left Margin (px)": "左マージン (px)",
"Column Gap (%)": "列間の間隔 (%)",
"Always Show Status Bar": "ステータスバーを常に表示",
"Translation Not Available": "翻訳は利用できません",
"Custom Content CSS": "コンテンツCSS",
"Enter CSS for book content styling...": "本のコンテンツ用CSSを入力...",
"Custom Reader UI CSS": "リーダーUIのCSS",
"Enter CSS for reader interface styling...": "リーダーインターフェース用CSSを入力...",
"Crop": "トリミング",
"Book Covers": "書籍の表紙",
"Fit": "フィット",
"Reset {{settings}}": "{{settings}}をリセット",
"Reset Settings": "設定をリセット",
"{{count}} pages left in chapter_other": "{{count}} ページ残り",
"Show Remaining Pages": "残りを表示"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "삭제 확인",
"Copied to notebook": "노트북에 복사됨",
"Copy": "복사",
"Custom CSS": "사용자 정의 CSS",
"Dark Mode": "다크 모드",
"Default": "기본값",
"Default Font": "기본 글꼴",
@@ -28,7 +27,6 @@
"Dictionary": "사전",
"Download Readest": "Readest 다운로드",
"Edit": "편집",
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
"Excerpts": "발췌",
"Failed to import book(s): {{filenames}}": "책 가져오기 실패: {{filenames}}",
"Fast": "빠름",
@@ -114,8 +112,6 @@
"Your Library": "내 서재",
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음",
"Override Book Font": "책 글꼴 덮어쓰기",
"Vertical Margins (px)": "수직 여백 (px)",
"Horizontal Margins (%)": "수평 여백 (%)",
"Apply to All Books": "모든 책에 적용",
"Apply to This Book": "이 책에 적용",
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
@@ -266,7 +262,7 @@
"Clear Search": "검색 지우기",
"Header & Footer": "헤더 및 푸터",
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
"A new version of Readest is available!": "Readest의 새 버전이 있습니다!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}가 사용 가능 (설치된 버전 {{currentVersion}}).",
"Download and install now?": "지금 다운로드하고 설치하시겠습니까?",
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
@@ -356,5 +352,23 @@
"Contact Support": "지원팀에 문의",
"Code Highlighting": "코드 하이라이팅",
"Enable Highlighting": "하이라이팅 활성화",
"Code Language": "코드 언어"
"Code Language": "코드 언어",
"Top Margin (px)": "위쪽 여백 (px)",
"Bottom Margin (px)": "아래쪽 여백 (px)",
"Right Margin (px)": "오른쪽 여백 (px)",
"Left Margin (px)": "왼쪽 여백 (px)",
"Column Gap (%)": "열 간격 (%)",
"Always Show Status Bar": "상태 표시줄 항상 표시",
"Translation Not Available": "번역을 사용할 수 없음",
"Custom Content CSS": "콘텐츠 CSS",
"Enter CSS for book content styling...": "도서 콘텐츠 스타일을 위한 CSS 입력...",
"Custom Reader UI CSS": "리더 UI CSS",
"Enter CSS for reader interface styling...": "리더 인터페이스 스타일용 CSS 입력...",
"Crop": "자르기",
"Book Covers": "책 표지",
"Fit": "맞춤",
"Reset {{settings}}": "{{settings}} 재설정",
"Reset Settings": "설정 재설정",
"{{count}} pages left in chapter_other": "남은 페이지 {{count}}장",
"Show Remaining Pages": "남은 페이지 보기"
}
@@ -169,8 +169,6 @@
"Hyphenation": "Woordafbreking",
"Override Book Layout": "Boekopmaak overschrijven",
"Page": "Pagina",
"Vertical Margins (px)": "Verticale marges (px)",
"Horizontal Margins (%)": "Horizontale marges (%)",
"Maximum Number of Columns": "Maximum aantal kolommen",
"Maximum Column Height": "Maximale kolomhoogte",
"Maximum Column Width": "Maximale kolombreedte",
@@ -191,8 +189,6 @@
"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",
@@ -277,7 +273,7 @@
"Description:": "Beschrijving:",
"No description available": "Geen beschrijving beschikbaar",
"Drop to Import Books": "Sleep om boeken te importeren",
"A new version of Readest is Available!": "Een nieuwe versie van Readest is beschikbaar!",
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} is beschikbaar (geïnstalleerde versie {{currentVersion}}).",
"Download and install now?": "Nu downloaden en installeren?",
"Downloading {{downloaded}} of {{contentLength}}": "Downloaden {{downloaded}} van {{contentLength}}",
@@ -359,5 +355,24 @@
"Contact Support": "Neem contact op met ondersteuning",
"Code Highlighting": "Code markering",
"Enable Highlighting": "Markering inschakelen",
"Code Language": "Code taal"
"Code Language": "Code taal",
"Top Margin (px)": "Bovenmarge (px)",
"Bottom Margin (px)": "Ondermarge (px)",
"Right Margin (px)": "Rechter marge (px)",
"Left Margin (px)": "Linker marge (px)",
"Column Gap (%)": "Kolomafstand (%)",
"Always Show Status Bar": "Statusbalk altijd weergeven",
"Translation Not Available": "Vertaling niet beschikbaar",
"Custom Content CSS": "Aangepaste inhoud-CSS",
"Enter CSS for book content styling...": "Voer CSS in voor de opmaak van de boekinhoud...",
"Custom Reader UI CSS": "Aangepaste interface-CSS",
"Enter CSS for reader interface styling...": "Voer CSS in voor de opmaak van de leesinterface...",
"Crop": "Bijsnijden",
"Book Covers": "Boekomslagen",
"Fit": "Passend maken",
"Reset {{settings}}": "Reset {{settings}}",
"Reset Settings": "Instellingen resetten",
"{{count}} pages left in chapter_one": "Nog {{count}} pagina in hoofdstuk",
"{{count}} pages left in chapter_other": "Nog {{count}} paginas in hoofdstuk",
"Show Remaining Pages": "Toon resterende paginas"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Potwierdź usunięcie",
"Copied to notebook": "Skopiowano do notatnika",
"Copy": "Kopiuj",
"Custom CSS": "Własny CSS",
"Dark Mode": "Tryb ciemny",
"Default": "Domyślne",
"Default Font": "Domyślna czcionka",
@@ -28,7 +27,6 @@
"Dictionary": "Słownik",
"Download Readest": "Pobierz Readest",
"Edit": "Edytuj",
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
"Excerpts": "Fragmenty",
"Failed to import book(s): {{filenames}}": "Nie udało się zaimportować książek: {{filenames}}",
"Fast": "Szybko",
@@ -114,8 +112,6 @@
"Your Library": "Twoja biblioteka",
"TTS not supported for PDF": "TTS nie jest obsługiwane dla plików PDF",
"Override Book Font": "Nadpisz czcionkę książki",
"Vertical Margins (px)": "Marginesy pionowe (px)",
"Horizontal Margins (%)": "Marginesy poziome (%)",
"Apply to All Books": "Zastosuj do wszystkich książek",
"Apply to This Book": "Zastosuj do tej książki",
"Unable to fetch the translation. Try again later.": "Nie udało się pobrać tłumaczenia. Spróbuj ponownie później.",
@@ -266,7 +262,7 @@
"Clear Search": "Wyczyść wyszukiwanie",
"Header & Footer": "Nagłówek i stopka",
"Apply also in Scrolled Mode": "Zastosuj również w trybie przewijania",
"A new version of Readest is Available!": "Nowa wersja Readest jest dostępna!",
"A new version of Readest is available!": "Nowa wersja Readest jest dostępna!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} jest dostępny (zainstalowana wersja {{currentVersion}}).",
"Download and install now?": "Pobierz i zainstaluj teraz?",
"Downloading {{downloaded}} of {{contentLength}}": "Pobieranie {{downloaded}} z {{contentLength}}",
@@ -365,5 +361,26 @@
"Contact Support": "Skontaktuj się z pomocą techniczną",
"Code Highlighting": "Podświetlanie kodu",
"Enable Highlighting": "Zezwól na podświetlanie",
"Code Language": "Język kodu"
"Code Language": "Język kodu",
"Top Margin (px)": "Górny margines (px)",
"Bottom Margin (px)": "Dolny margines (px)",
"Right Margin (px)": "Prawy margines (px)",
"Left Margin (px)": "Lewy margines (px)",
"Column Gap (%)": "Odstęp między kolumnami (%)",
"Always Show Status Bar": "Zawsze pokazuj pasek stanu",
"Translation Not Available": "Brak dostępnego tłumaczenia",
"Custom Content CSS": "CSS treści",
"Enter CSS for book content styling...": "Wprowadź CSS dla stylu treści książki...",
"Custom Reader UI CSS": "CSS interfejsu",
"Enter CSS for reader interface styling...": "Wprowadź CSS dla stylu interfejsu czytnika...",
"Crop": "Przytnij",
"Book Covers": "Okładki książek",
"Fit": "Pasuje",
"Reset {{settings}}": "Zresetuj {{settings}}",
"Reset Settings": "Zresetuj ustawienia",
"{{count}} pages left in chapter_one": "Pozostała {{count}} strona w rozdziale",
"{{count}} pages left in chapter_few": "Pozostały {{count}} strony w rozdziale",
"{{count}} pages left in chapter_many": "Pozostało {{count}} stron w rozdziale",
"{{count}} pages left in chapter_other": "Pozostało {{count}} stron w rozdziale",
"Show Remaining Pages": "Pokaż pozostałe strony"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmar Exclusão",
"Copied to notebook": "Copiado para o caderno",
"Copy": "Copiar",
"Custom CSS": "CSS Personalizado",
"Dark Mode": "Modo Escuro",
"Default": "Padrão",
"Default Font": "Fonte Padrão",
@@ -28,7 +27,6 @@
"Dictionary": "Dicionário",
"Download Readest": "Baixar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
"Excerpts": "Trechos",
"Failed to import book(s): {{filenames}}": "Falha ao importar livro(s): {{filenames}}",
"Fast": "Rápido",
@@ -114,8 +112,6 @@
"Your Library": "Sua Biblioteca",
"TTS not supported for PDF": "TTS não suportado para PDF",
"Override Book Font": "Sobrescrever Fonte do Livro",
"Vertical Margins (px)": "Margens Verticais (px)",
"Horizontal Margins (%)": "Margens Horizontais (%)",
"Apply to All Books": "Aplicar a todos os livros",
"Apply to This Book": "Aplicar a este livro",
"Unable to fetch the translation. Try again later.": "Não foi possível buscar a tradução. Tente novamente mais tarde.",
@@ -266,7 +262,7 @@
"Clear Search": "Limpar Pesquisa",
"Header & Footer": "Cabeçalho e Rodapé",
"Apply also in Scrolled Mode": "Aplicar também no Modo Rolado",
"A new version of Readest is Available!": "Uma nova versão do Readest está disponível!",
"A new version of Readest is available!": "Uma nova versão do Readest está disponível!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponível (versão instalada {{currentVersion}}).",
"Download and install now?": "Baixar e instalar agora?",
"Downloading {{downloaded}} of {{contentLength}}": "Baixando {{downloaded}} de {{contentLength}}",
@@ -362,5 +358,25 @@
"Contact Support": "Contato com Suporte",
"Code Highlighting": "Realce de Código",
"Enable Highlighting": "Ativar Realce",
"Code Language": "Idioma do Código"
"Code Language": "Idioma do Código",
"Top Margin (px)": "Margem superior (px)",
"Bottom Margin (px)": "Margem inferior (px)",
"Right Margin (px)": "Margem direita (px)",
"Left Margin (px)": "Margem esquerda (px)",
"Column Gap (%)": "Espaço entre colunas (%)",
"Always Show Status Bar": "Exibir sempre a barra de status",
"Translation Not Available": "Tradução não disponível",
"Custom Content CSS": "CSS do conteúdo",
"Enter CSS for book content styling...": "Insira o CSS para o estilo do conteúdo do livro...",
"Custom Reader UI CSS": "CSS da interface",
"Enter CSS for reader interface styling...": "Insira o CSS para o estilo da interface de leitura...",
"Crop": "Cortar",
"Book Covers": "Capas de Livros",
"Fit": "Encaixar",
"Reset {{settings}}": "Redefinir {{settings}}",
"Reset Settings": "Redefinir Configurações",
"{{count}} pages left in chapter_one": "Falta {{count}} página neste capítulo",
"{{count}} pages left in chapter_many": "Faltam {{count}} páginas neste capítulo",
"{{count}} pages left in chapter_other": "Faltam {{count}} páginas neste capítulo",
"Show Remaining Pages": "Mostrar páginas restantes"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Подтвердить удаление",
"Copied to notebook": "Скопировано в блокнот",
"Copy": "Копировать",
"Custom CSS": "Пользовательский CSS",
"Dark Mode": "Тёмная тема",
"Default": "По умолчанию",
"Default Font": "Шрифт по умолчанию",
@@ -28,7 +27,6 @@
"Dictionary": "Словарь",
"Download Readest": "Скачать Readest",
"Edit": "Редактировать",
"Enter your custom CSS here...": "Введите ваш пользовательский CSS здесь...",
"Excerpts": "Отрывки",
"Failed to import book(s): {{filenames}}": "Не удалось импортировать книгу(и): {{filenames}}",
"Fast": "Быстро",
@@ -114,8 +112,6 @@
"Your Library": "Ваша библиотека",
"TTS not supported for PDF": "TTS не поддерживается для PDF",
"Override Book Font": "Переопределить шрифт книги",
"Vertical Margins (px)": "Вертикальные поля (px)",
"Horizontal Margins (%)": "Горизонтальные поля (%)",
"Apply to All Books": "Применить ко всем книгам",
"Apply to This Book": "Применить к этой книге",
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже.",
@@ -266,7 +262,7 @@
"Clear Search": "Очистить поиск",
"Header & Footer": "Заголовок и нижний колонтитул",
"Apply also in Scrolled Mode": "Применить также в прокручиваемом режиме",
"A new version of Readest is Available!": "Доступна новая версия Readest!",
"A new version of Readest is available!": "Доступна новая версия Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступен (установленная версия {{currentVersion}}).",
"Download and install now?": "Скачать и установить сейчас?",
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
@@ -365,5 +361,26 @@
"Contact Support": "Связаться с поддержкой",
"Code Highlighting": "Подсветка кода",
"Enable Highlighting": "Включить подсветку",
"Code Language": "Язык кода"
"Code Language": "Язык кода",
"Top Margin (px)": "Верхний отступ (пикс)",
"Bottom Margin (px)": "Нижний отступ (пикс)",
"Right Margin (px)": "Правый отступ (пикс)",
"Left Margin (px)": "Левый отступ (пикс)",
"Column Gap (%)": "Промежуток между колонками (%)",
"Always Show Status Bar": "Всегда показывать панель состояния",
"Translation Not Available": "Перевод недоступен",
"Custom Content CSS": "CSS содержимого",
"Enter CSS for book content styling...": "Введите CSS для оформления содержимого книги...",
"Custom Reader UI CSS": "CSS интерфейса",
"Enter CSS for reader interface styling...": "Введите CSS для оформления интерфейса ридера...",
"Crop": "Обрезать",
"Book Covers": "Обложки книг",
"Fit": "Подогнать",
"Reset {{settings}}": "Сбросить {{settings}}",
"Reset Settings": "Сбросить настройки",
"{{count}} pages left in chapter_one": "Осталась {{count}} страница в главе",
"{{count}} pages left in chapter_few": "Осталось {{count}} страницы в главе",
"{{count}} pages left in chapter_many": "Осталось {{count}} страниц в главе",
"{{count}} pages left in chapter_other": "Осталось {{count}} страниц в главе",
"Show Remaining Pages": "Показать оставшиеся страницы"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Silmeyi Onayla",
"Copied to notebook": "Not defterine kopyalandı",
"Copy": "Kopyala",
"Custom CSS": "Özel CSS",
"Dark Mode": "Karanlık Mod",
"Default": "Varsayılan",
"Default Font": "Varsayılan Yazı Tipi",
@@ -28,7 +27,6 @@
"Dictionary": "Sözlük",
"Download Readest": "Readest'i İndir",
"Edit": "Düzenle",
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
"Excerpts": "Alıntılar",
"Failed to import book(s): {{filenames}}": "Kitap(lar) içe aktarılamadı: {{filenames}}",
"Fast": "Hızlı",
@@ -114,8 +112,6 @@
"Your Library": "Kütüphaneniz",
"TTS not supported for PDF": "PDF için TTS desteklenmiyor",
"Override Book Font": "Kitap Yazı Tipini Geçersiz Kıl",
"Vertical Margins (px)": "Dikey Kenar Boşlukları (px)",
"Horizontal Margins (%)": "Yatay Kenar Boşlukları (%)",
"Apply to All Books": "Tüm kitaplara uygula",
"Apply to This Book": "Bu kitaba uygula",
"Unable to fetch the translation. Try again later.": "Çeviri alınamıyor. Daha sonra tekrar deneyin.",
@@ -266,7 +262,7 @@
"Clear Search": "Aramayı Temizle",
"Header & Footer": "Başlık ve Altbilgi",
"Apply also in Scrolled Mode": "Kaydırılmış Modda da Uygula",
"A new version of Readest is Available!": "Yeni bir Readest sürümü mevcut!",
"A new version of Readest is available!": "Yeni bir Readest sürümü mevcut!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} mevcut (yüklü sürüm {{currentVersion}}).",
"Download and install now?": "Şimdi indirip yükleyelim mi?",
"Downloading {{downloaded}} of {{contentLength}}": "İndiriliyor {{downloaded}}/{{contentLength}}",
@@ -359,5 +355,24 @@
"Contact Support": "Destekle İletişime Geç",
"Code Highlighting": "Code Vurgulama",
"Enable Highlighting": "Vurgulamayı Etkinleştir",
"Code Language": "Code Dili"
"Code Language": "Code Dili",
"Top Margin (px)": "Üst Kenar Boşluğu (px)",
"Bottom Margin (px)": "Alt Kenar Boşluğu (px)",
"Right Margin (px)": "Sağ Kenar Boşluğu (px)",
"Left Margin (px)": "Sol Kenar Boşluğu (px)",
"Column Gap (%)": "Sütun Aralığı (%)",
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster",
"Translation Not Available": "Çeviri Mevcut Değil",
"Custom Content CSS": "Özel İçerik CSS'si",
"Enter CSS for book content styling...": "Kitap içeriği stili için CSS girin...",
"Custom Reader UI CSS": "Özel Okuyucu Arayüzü CSS'si",
"Enter CSS for reader interface styling...": "Okuyucu arayüzü stili için CSS girin...",
"Crop": "Kes",
"Book Covers": "Kitap Kapağı",
"Fit": "Uygun",
"Reset {{settings}}": "{{settings}}'i Sıfırla",
"Reset Settings": "Ayarları Sıfırla",
"{{count}} pages left in chapter_one": "Bu bölümde {{count}} sayfa kaldı",
"{{count}} pages left in chapter_other": "Bu bölümde {{count}} sayfa kaldı",
"Show Remaining Pages": "Kalan sayfaları göster"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Підтвердити видалення",
"Copied to notebook": "Скопійовано до нотатника",
"Copy": "Копіювати",
"Custom CSS": "Користувацький CSS",
"Dark Mode": "Темний режим",
"Default": "За замовчуванням",
"Default Font": "Шрифт за замовчуванням",
@@ -28,7 +27,6 @@
"Dictionary": "Словник",
"Download Readest": "Завантажити Readest",
"Edit": "Редагувати",
"Enter your custom CSS here...": "Введіть ваш користувацький CSS тут...",
"Excerpts": "Уривки",
"Failed to import book(s): {{filenames}}": "Не вдалося імпортувати книгу(и): {{filenames}}",
"Fast": "Швидко",
@@ -114,8 +112,6 @@
"Your Library": "Ваша бібліотека",
"TTS not supported for PDF": "TTS не підтримується для PDF",
"Override Book Font": "Перевизначити шрифт книги",
"Vertical Margins (px)": "Вертикальні відступи (px)",
"Horizontal Margins (%)": "Горизонтальні відступи (%)",
"Apply to All Books": "Застосувати до всіх книг",
"Apply to This Book": "Застосувати до цієї книги",
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
@@ -266,7 +262,7 @@
"Clear Search": "Очистити пошук",
"Header & Footer": "Заголовок і нижній колонтитул",
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
"A new version of Readest is Available!": "Доступна нова версія Readest!",
"A new version of Readest is available!": "Доступна нова версія Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
"Download and install now?": "Завантажити та встановити зараз?",
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
@@ -365,5 +361,26 @@
"Contact Support": "Зв'язатися з підтримкою",
"Code Highlighting": "Підсвічування коду",
"Enable Highlighting": "Увімкнути підсвічування",
"Code Language": "Мова коду"
"Code Language": "Мова коду",
"Top Margin (px)": "Верхній відступ (px)",
"Bottom Margin (px)": "Нижній відступ (px)",
"Right Margin (px)": "Правий відступ (px)",
"Left Margin (px)": "Лівий відступ (px)",
"Column Gap (%)": "Проміжок між стовпцями (%)",
"Always Show Status Bar": "Завжди показувати панель стану",
"Translation Not Available": "Переклад недоступний",
"Custom Content CSS": "CSS вмісту",
"Enter CSS for book content styling...": "Введіть CSS для стилізації вмісту книги...",
"Custom Reader UI CSS": "CSS інтерфейсу",
"Enter CSS for reader interface styling...": "Введіть CSS для стилізації інтерфейсу рідера...",
"Crop": "Обрізати",
"Book Covers": "Обкладинки книг",
"Fit": "Вписати",
"Reset {{settings}}": "Скинути {{settings}}",
"Reset Settings": "Скинути налаштування",
"{{count}} pages left in chapter_one": "Залишилася {{count}} сторінка у розділі",
"{{count}} pages left in chapter_few": "Залишилось {{count}} сторінки у розділі",
"{{count}} pages left in chapter_many": "Залишилось {{count}} сторінок у розділі",
"{{count}} pages left in chapter_other": "Залишилось {{count}} сторінок у розділі",
"Show Remaining Pages": "Показати залишок сторінок"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Xác nhận xóa",
"Copied to notebook": "Đã sao chép vào sổ ghi chú",
"Copy": "Sao chép",
"Custom CSS": "CSS tùy chỉnh",
"Dark Mode": "Chế độ tối",
"Default": "Mặc định",
"Default Font": "Phông chữ mặc định",
@@ -28,7 +27,6 @@
"Dictionary": "Từ điển",
"Download Readest": "Tải Readest",
"Edit": "Chỉnh sửa",
"Enter your custom CSS here...": "Nhập CSS tùy chỉnh của bạn vào đây...",
"Excerpts": "Trích dẫn",
"Failed to import book(s): {{filenames}}": "Không thể nhập sách: {{filenames}}",
"Fast": "Nhanh",
@@ -114,8 +112,6 @@
"Your Library": "Thư viện của bạn",
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF",
"Override Book Font": "Ghi đè phông chữ sách",
"Vertical Margins (px)": "Lề dọc (px)",
"Horizontal Margins (%)": "Lề ngang (%)",
"Apply to All Books": "Áp dụng cho tất cả sách",
"Apply to This Book": "Áp dụng cho sách này",
"Unable to fetch the translation. Try again later.": "Không thể tải dịch vụ. Vui lòng thử lại sau.",
@@ -266,7 +262,7 @@
"Clear Search": "Xóa tìm kiếm",
"Header & Footer": "Đầu trang & Chân trang",
"Apply also in Scrolled Mode": "Áp dụng cũng trong chế độ cuộn",
"A new version of Readest is Available!": "Đã có phiên bản mới của Readest!",
"A new version of Readest is available!": "Đã có phiên bản mới của Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} đã có sẵn (phiên bản đã cài đặt {{currentVersion}}).",
"Download and install now?": "Tải xuống và cài đặt ngay bây giờ?",
"Downloading {{downloaded}} of {{contentLength}}": "Đang tải {{downloaded}} trong {{contentLength}}",
@@ -356,5 +352,23 @@
"Contact Support": "Liên hệ hỗ trợ",
"Code Highlighting": "Đánh dấu mã",
"Enable Highlighting": "Cho phép đánh dấu",
"Code Language": "Ngôn ngữ mã"
"Code Language": "Ngôn ngữ mã",
"Top Margin (px)": "Lề trên (px)",
"Bottom Margin (px)": "Lề dưới (px)",
"Right Margin (px)": "Lề phải (px)",
"Left Margin (px)": "Lề trái (px)",
"Column Gap (%)": "Khoảng cách cột (%)",
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái",
"Translation Not Available": "Không có bản dịch",
"Custom Content CSS": "CSS nội dung",
"Enter CSS for book content styling...": "Nhập CSS cho phần nội dung sách...",
"Custom Reader UI CSS": "CSS giao diện đọc",
"Enter CSS for reader interface styling...": "Nhập CSS cho giao diện trình đọc...",
"Crop": "Cắt",
"Book Covers": "Bìa sách",
"Fit": "Vừa vặn",
"Reset {{settings}}": "Đặt lại {{settings}}",
"Reset Settings": "Đặt lại cài đặt",
"{{count}} pages left in chapter_other": "Còn {{count}} trang trong chương",
"Show Remaining Pages": "Hiển thị trang còn lại"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "确认删除",
"Copied to notebook": "已复制到笔记本",
"Copy": "复制",
"Custom CSS": "自定义 CSS",
"Dark Mode": "深色主题",
"Default": "默认",
"Default Font": "默认字体",
@@ -28,7 +27,6 @@
"Dictionary": "词典",
"Download Readest": "下载 Readest",
"Edit": "编辑",
"Enter your custom CSS here...": "在此处输入您的自定义 CSS...",
"Excerpts": "摘录",
"Failed to import book(s): {{filenames}}": "导入书籍失败:{{filenames}}",
"Fast": "快",
@@ -114,8 +112,6 @@
"Your Library": "书库",
"TTS not supported for PDF": "PDF 文档暂不支持 TTS",
"Override Book Font": "覆盖书籍字体",
"Vertical Margins (px)": "垂直边距(px",
"Horizontal Margins (%)": "水平边距(%",
"Apply to All Books": "应用于所有书籍",
"Apply to This Book": "应用于此书籍",
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。",
@@ -266,7 +262,7 @@
"Clear Search": "清除搜索",
"Header & Footer": "页眉页脚",
"Apply also in Scrolled Mode": "应用到滚动模式",
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安装版本 {{currentVersion}})。",
"Download and install now?": "立即下载并安装?",
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
@@ -356,5 +352,23 @@
"Contact Support": "联系客服",
"Code Highlighting": "代码高亮",
"Enable Highlighting": "启用高亮",
"Code Language": "代码语言"
"Code Language": "代码语言",
"Top Margin (px)": "上边距",
"Bottom Margin (px)": "下边距",
"Right Margin (px)": "右边距",
"Left Margin (px)": "左边距",
"Column Gap (%)": "列间距",
"Always Show Status Bar": "始终显示状态栏",
"Translation Not Available": "翻译不可用",
"Custom Content CSS": "自定义内容 CSS",
"Enter CSS for book content styling...": "输入书籍内容样式的CSS…",
"Custom Reader UI CSS": "自定义界面 CSS",
"Enter CSS for reader interface styling...": "输入阅读器界面样式的CSS…",
"Crop": "裁剪",
"Book Covers": "书籍封面",
"Fit": "适应",
"Reset {{settings}}": "重置{{settings}}",
"Reset Settings": "重置设置",
"{{count}} pages left in chapter_other": "本章剩余 {{count}} 页",
"Show Remaining Pages": "显示剩余页数"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "確認刪除",
"Copied to notebook": "已複製到筆記本",
"Copy": "複製",
"Custom CSS": "自定義 CSS",
"Dark Mode": "深色主題",
"Default": "預設",
"Default Font": "預設字體",
@@ -28,7 +27,6 @@
"Dictionary": "詞典",
"Download Readest": "下載 Readest",
"Edit": "編輯",
"Enter your custom CSS here...": "在此處輸入您的自定義 CSS...",
"Excerpts": "摘錄",
"Failed to import book(s): {{filenames}}": "導入書籍失敗:{{filenames}}",
"Fast": "快",
@@ -114,8 +112,6 @@
"Your Library": "書庫",
"TTS not supported for PDF": "PDF 文檔暫不支持 TTS",
"Override Book Font": "覆蓋書籍字體",
"Vertical Margins (px)": "垂直邊距(px",
"Horizontal Margins (%)": "水平邊距(%",
"Apply to All Books": "應用於所有書籍",
"Apply to This Book": "應用於此書籍",
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。",
@@ -266,7 +262,7 @@
"Clear Search": "清除搜索",
"Header & Footer": "頁眉頁腳",
"Apply also in Scrolled Mode": "滾動模式同樣適用",
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安裝版本 {{currentVersion}})。",
"Download and install now?": "立即下載並安裝?",
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
@@ -356,5 +352,23 @@
"Contact Support": "聯繫支援",
"Code Highlighting": "程式碼高亮",
"Enable Highlighting": "啟用高亮",
"Code Language": "程式語言"
"Code Language": "程式語言",
"Top Margin (px)": "上邊距",
"Bottom Margin (px)": "下邊距",
"Right Margin (px)": "右邊距",
"Left Margin (px)": "左邊距",
"Column Gap (%)": "欄間距",
"Always Show Status Bar": "始終顯示狀態欄",
"Translation Not Available": "翻譯不可用",
"Custom Content CSS": "自訂內容 CSS",
"Enter CSS for book content styling...": "輸入書籍內容樣式的CSS…",
"Custom Reader UI CSS": "自訂介面 CSS",
"Enter CSS for reader interface styling...": "輸入閱讀器介面樣式的CSS…",
"Crop": "裁剪",
"Book Covers": "書籍封面",
"Fit": "適應",
"Reset {{settings}}": "重置{{settings}}",
"Reset Settings": "重置設置",
"{{count}} pages left in chapter_other": "本章剩餘 {{count}} 頁",
"Show Remaining Pages": "顯示剩餘頁數"
}
+50
View File
@@ -1,5 +1,55 @@
{
"releases": {
"0.9.61": {
"date": "2025-06-26",
"notes": [
"Layout: Fixed view menu width on smaller screens",
"Layout: Added option to crop or fit book covers in library view",
"Reader: You can now apply custom CSS to the reading interface (UI)",
"Reader: Added an option to show remaining pages in the current chapter",
"Settings: Added options to reset settings from the config dialog",
"Import: Fixed encoded filenames when importing TXT files on iOS",
"Linux: Fixed missing app icon on some Linux distributions",
"GitHub: Added donation link to support Readest development"
]
},
"0.9.60": {
"date": "2025-06-22",
"notes": [
"Layout has been improved for iPad devices",
"Screen orientation changes are now smoother on iOS",
"Importing books on iOS during the first launch is now more reliable",
"Font size no longer changes unexpectedly in scrolled mode on iOS",
"Top and bottom safe insets are now ignored when no header or footer is present",
"Interactive books now load correctly on the Desktop platform",
"Translation is now disabled when the primary language is undefined",
"Android: the task list background color has been updated with current theme color"
]
},
"0.9.59": {
"date": "2025-06-19",
"notes": [
"Layout: Image size now adjusts more accurately by converting screen units to pixels properly",
"Layout: Improved header layout on iOS landscape mode — extra spacing has been removed",
"Settings: Top and bottom margin settings are now more intuitive and better constrained"
]
},
"0.9.58": {
"date": "2025-06-18",
"notes": [
"TTS: Translation now works even when TTS is playing in the background",
"TTS: Press T on your keyboard to quickly toggle TTS on or off",
"TTS: Each open book now has its own independent TTS controller",
"Annotations: Annotation tools now function properly while TTS is active",
"Settings: Margins for top, bottom, left, and right can now be adjusted individually",
"Settings: New option to always show the status bar while reading",
"Layout: Reader page now adapts better to device safe areas for a more comfortable layout",
"UI: Added creation timestamps to annotations and made various interface improvements",
"iPad: Multiple columns are now supported in portrait mode",
"iPad: Fixed issue where sidebars couldn't be resized by dragging",
"iPad: Added support for split-screen mode"
]
},
"0.9.57": {
"date": "2025-06-14",
"notes": ["TTS: Fixed an issue to detect custom TTS engines on some Android devices"]
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -e
VERSION=$(jq -r '.version' package.json)
if [[ -z "$VERSION" ]]; then
echo "❌ Failed to extract version from package.json"
exit 1
fi
echo "📦 Found version: $VERSION"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then
echo "❌ Invalid version format: $VERSION"
exit 1
fi
# Convert x.y.z => x * 10000 + y * 1000 + z
VERSION_CODE=$((10#$MAJOR * 10000 + 10#$MINOR * 1000 + 10#$PATCH))
echo "🔢 Computed versionCode: $VERSION_CODE"
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
if [[ ! -f "$PROPERTIES_FILE" ]]; then
echo "❌ File not found: $PROPERTIES_FILE"
exit 1
fi
tmpfile=$(mktemp)
sed "s/^tauri\.android\.versionName=.*/tauri.android.versionName=$VERSION/" "$PROPERTIES_FILE" | \
sed "s/^tauri\.android\.versionCode=.*/tauri.android.versionCode=$VERSION_CODE/" > "$tmpfile"
mv "$tmpfile" "$PROPERTIES_FILE"
echo "✅ Updated $PROPERTIES_FILE"
echo "🚀 Running: pnpm tauri android build"
pnpm tauri android build
+1 -1
View File
@@ -32,7 +32,7 @@ reqwest = { version = "0.12", default-features = false, features = [
] }
# FIXME: remove the devtools feature in production
tauri = { version = "2.5.1", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.1.1"
tauri-build = "2.3.0"
tauri-plugin-log = "2.4.0"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
+2 -2
View File
@@ -11,7 +11,7 @@
<key>UIHomeIndicatorAutoHidden</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
<false/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
@@ -198,4 +198,4 @@
</dict>
</array>
</dict>
</plist>
</plist>
@@ -54,11 +54,14 @@
"identifier": "http:default",
"allow": [
{
"url": "https://*.deepl.com"
"url": "https://*.readest.com"
},
{
"url": "https://github.com/readest/*"
},
{
"url": "https://*.deepl.com"
},
{
"url": "https://*.cloudflarestorage.com"
},
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@@ -10,10 +9,11 @@
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:largeHeap="true"
android:label="@string/app_name"
android:theme="@style/Theme.readest"
android:hardwareAccelerated="true"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
@@ -42,15 +42,15 @@
<data android:mimeType="application/zip" />
<data android:mimeType="text/plain" />
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.pdf" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.fb2.zip" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.azw" />
<data android:pathPattern=".*\\.cbz" />
<data android:pathPattern=".*\\.zip" />
<data android:pathPattern=".*\\.txt" />
<data android:pathPattern="/.*\\.epub" />
<data android:pathPattern="/.*\\.pdf" />
<data android:pathPattern="/.*\\.fb2" />
<data android:pathPattern="/.*\\.fb2.zip" />
<data android:pathPattern="/.*\\.mobi" />
<data android:pathPattern="/.*\\.azw" />
<data android:pathPattern="/.*\\.cbz" />
<data android:pathPattern="/.*\\.zip" />
<data android:pathPattern="/.*\\.txt" />
</intent-filter>
<intent-filter android:label="oauth">
@@ -70,10 +70,9 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="web.readest.com" />
</intent-filter>
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity>
@@ -1,10 +1,14 @@
package com.bilingify.readest
import android.os.Build
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.view.KeyEvent
import android.webkit.WebView
import android.util.Log
import android.graphics.Color
import android.app.ActivityManager
import android.content.res.Configuration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@@ -77,5 +81,15 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setTaskDescription(
ActivityManager.TaskDescription(
getString(R.string.app_name),
null,
Color.TRANSPARENT
)
)
}
}
}
@@ -2,6 +2,9 @@
<!-- Base application theme. -->
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowSplashScreenBackground">@android:color/transparent</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="31">@null</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
@@ -154,9 +154,8 @@ class VolumeKeyHandler: NSObject {
class NativeBridgePlugin: Plugin {
private var webView: WKWebView?
private var authSession: ASWebAuthenticationSession?
private var isOrientationLocked = false
private var currentOrientationMask: UIInterfaceOrientationMask = .all
private var orientationObserver: NSObjectProtocol?
private var originalDelegate: UIApplicationDelegate?
@objc public override func load(webview: WKWebView) {
self.webView = webview
@@ -175,16 +174,19 @@ class NativeBridgePlugin: Plugin {
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
if let app = UIApplication.value(forKey: "sharedApplication") as? UIApplication {
self.originalDelegate = app.delegate
app.delegate = self
} else {
Logger.error("NativeBridgePlugin: Failed to get shared application")
}
}
@objc func appDidBecomeActive() {
if volumeKeyHandler != nil {
activateVolumeKeyInterception()
}
if orientationObserver != nil {
self.setupOrientationObserver()
}
}
@objc func appDidEnterBackground() {
@@ -360,18 +362,11 @@ class NativeBridgePlugin: Plugin {
let orientation = args.orientation ?? "auto"
switch orientation.lowercased() {
case "portrait":
self.isOrientationLocked = true
self.currentOrientationMask = .portrait
self.forceOrientation(.portrait)
self.setupOrientationObserver()
self.changeOrientation(.portrait)
case "landscape":
self.isOrientationLocked = true
self.currentOrientationMask = .landscape
self.forceOrientation(.landscapeRight)
self.setupOrientationObserver()
self.changeOrientation(.landscape)
case "auto":
self.isOrientationLocked = false
self.currentOrientationMask = .all
self.changeOrientation(.all)
default:
invoke.reject("Invalid orientation mode")
return
@@ -381,40 +376,49 @@ class NativeBridgePlugin: Plugin {
}
}
private func forceOrientation(_ orientation: UIInterfaceOrientation) {
private func changeOrientation(_ orientationMask: UIInterfaceOrientationMask) {
self.currentOrientationMask = orientationMask
if #available(iOS 16.0, *) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
let orientationMask: UIInterfaceOrientationMask =
orientation.isPortrait ? .portrait : .landscape
for window in windowScene.windows {
if let rootVC = window.rootViewController {
rootVC.setNeedsUpdateOfSupportedInterfaceOrientations()
}
}
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
print("Orientation update error: \(error.localizedDescription)")
if orientationMask == .all {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .all)) { error in
logger.error("Orientation update error: \(error.localizedDescription)")
DispatchQueue.main.async {
UIViewController.attemptRotationToDeviceOrientation()
}
}
} else {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
logger.error("Orientation update error: \(error.localizedDescription)")
}
}
}
} else {
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
}
}
private func setupOrientationObserver() {
orientationObserver = NotificationCenter.default.addObserver(
forName: UIDevice.orientationDidChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
guard let self = self, self.isOrientationLocked else { return }
if self.currentOrientationMask == .portrait {
self.forceOrientation(.portrait)
} else if self.currentOrientationMask == .landscape {
self.forceOrientation(.landscapeRight)
if orientationMask == .all {
UIViewController.attemptRotationToDeviceOrientation()
} else {
let specificOrientation: UIInterfaceOrientation
if orientationMask.contains(.portrait) {
specificOrientation = .portrait
} else if orientationMask.contains(.landscape) {
let currentOrientation = UIDevice.current.orientation
if currentOrientation == .landscapeLeft {
specificOrientation = .landscapeRight
} else if currentOrientation == .landscapeRight {
specificOrientation = .landscapeLeft
} else {
specificOrientation = .landscapeRight
}
} else {
specificOrientation = .portrait
}
UIDevice.current.setValue(specificOrientation.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
}
}
}
@@ -431,3 +435,66 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding {
return UIApplication.shared.windows.first ?? UIWindow()
}
}
extension NativeBridgePlugin: UIApplicationDelegate {
public func application(
_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
return self.currentOrientationMask
}
/*
Proxy all application delegate methods to the original delegate:
sel!(application:didFinishLaunchingWithOptions:),
sel!(application:openURL:options:),
sel!(application:continue:restorationHandler:),
sel!(applicationDidBecomeActive:),
sel!(applicationWillResignActive:),
sel!(applicationWillEnterForeground:),
sel!(applicationDidEnterBackground:),
sel!(applicationWillTerminate:),
*/
public func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
self.originalDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions)
?? false
}
public func application(
_ application: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
self.originalDelegate?.application?(application, open: url, options: options) ?? false
}
public func application(
_ application: UIApplication, continue continueUserActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
self.originalDelegate?.application?(
application, continue: continueUserActivity, restorationHandler: restorationHandler) ?? false
}
public func applicationDidBecomeActive(_ application: UIApplication) {
self.originalDelegate?.applicationDidBecomeActive?(application)
}
public func applicationWillResignActive(_ application: UIApplication) {
self.originalDelegate?.applicationWillResignActive?(application)
}
public func applicationWillEnterForeground(_ application: UIApplication) {
self.originalDelegate?.applicationWillEnterForeground?(application)
}
public func applicationDidEnterBackground(_ application: UIApplication) {
self.originalDelegate?.applicationDidEnterBackground?(application)
}
public func applicationWillTerminate(_ application: UIApplication) {
self.originalDelegate?.applicationWillTerminate?(application)
}
}
+4 -1
View File
@@ -151,7 +151,10 @@
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
"endpoints": [
"https://download.readest.com/releases/latest.json",
"https://github.com/readest/readest/releases/latest/download/latest.json"
]
}
}
}
+25 -25
View File
@@ -1,42 +1,42 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { handleAuthCallback } from '@/helpers/auth';
import { useAuth } from '@/context/AuthContext';
import { handleAuthCallback } from '@/helpers/auth';
export default function AuthCallback() {
const router = useRouter();
const { login } = useAuth();
if (typeof window === 'undefined') {
return null;
}
const hash = window.location.hash || '';
const params = new URLSearchParams(hash.slice(1));
useEffect(() => {
const hash = window.location.hash || '';
const params = new URLSearchParams(hash.slice(1));
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
const next = params.get('next') ?? '/';
const error = params.get('error');
const errorDescription = params.get('error_description');
const errorCode = params.get('error_code');
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
const next = params.get('next') ?? '/';
const error = params.get('error');
const errorDescription = params.get('error_description');
const errorCode = params.get('error_code');
handleAuthCallback({
accessToken,
refreshToken,
type,
next,
error,
errorCode,
errorDescription,
login,
navigate: router.push,
});
handleAuthCallback({
accessToken,
refreshToken,
type,
next,
error,
errorCode,
errorDescription,
login,
navigate: router.push,
});
}, [login, router]);
return (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<span className='loading loading-infinity loading-xl w-20'></span>
<span className='loading loading-infinity loading-xl w-20' />
</div>
);
}
+2 -3
View File
@@ -18,14 +18,13 @@ import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { isTauriAppPlatform } from '@/services/environment';
import { getBaseUrl, isTauriAppPlatform } from '@/services/environment';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
import { openUrl } from '@tauri-apps/plugin-opener';
import { handleAuthCallback } from '@/helpers/auth';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
import { READEST_WEB_BASE_URL } from '@/services/constants';
import WindowButtons from '@/components/WindowButtons';
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
@@ -42,7 +41,7 @@ interface ProviderLoginProp {
label: string;
}
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
const WEB_AUTH_CALLBACK = `${getBaseUrl()}/auth/callback`;
const DEEPLINK_CALLBACK = 'readest://auth-callback';
const USE_APPLE_SIGN_IN = process.env['NEXT_PUBLIC_USE_APPLE_SIGN_IN'] === 'true';
+5 -5
View File
@@ -19,7 +19,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
useEffect(() => {
setBrowserInfo(parseWebViewVersion(appService));
posthog.captureException(error);
}, [error]);
}, [appService, error]);
const handleGoHome = () => {
window.location.href = '/library';
@@ -36,8 +36,8 @@ export default function Error({ error, reset }: ErrorPageProps) {
return (
<div className='hero bg-base-200 min-h-screen'>
<div className='hero-content text-center'>
<div className='max-w-2xl'>
<div className='mb-8'>
<div className='w-full max-w-2xl p-1'>
<div className='mb-8 mt-6'>
<div className='text-error animate-pulse text-8xl'></div>
</div>
@@ -55,8 +55,8 @@ export default function Error({ error, reset }: ErrorPageProps) {
<p className='break-words font-mono text-sm'>{error.message}</p>
{browserInfo && <p className='mt-2 font-mono text-sm'>Browser: {browserInfo}</p>}
{error.stack && (
<p className='mt-2 break-words font-mono text-sm'>
{error.stack.split('\n').slice(0, 5).join('\n')}
<p className='mt-2 whitespace-pre-wrap break-all font-mono text-sm'>
{error.stack.split('\n').slice(0, 3).join('\n')}
</p>
)}
{error.digest && <p className='mt-2 text-xs opacity-70'>Error ID: {error.digest}</p>}
@@ -11,15 +11,16 @@ import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { navigateToLogin } from '@/utils/nav';
import { formatAuthors } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
import BookCover from '@/components/BookCover';
interface BookItemProps {
mode: LibraryViewModeType;
book: Book;
mode: LibraryViewModeType;
coverFit: LibraryCoverFitType;
isSelectMode: boolean;
selectedBooks: string[];
transferProgress: number | null;
@@ -29,8 +30,9 @@ interface BookItemProps {
}
const BookItem: React.FC<BookItemProps> = ({
mode,
book,
mode,
coverFit,
isSelectMode,
selectedBooks,
transferProgress,
@@ -59,11 +61,11 @@ const BookItem: React.FC<BookItemProps> = ({
>
<div
className={clsx(
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
'relative flex aspect-[28/41] items-center justify-center',
mode === 'list' && 'min-w-20',
)}
>
<BookCover mode={mode} book={book} />
<BookCover mode={mode} book={book} coverFit={coverFit} className='drop-shadow-md' />
{selectedBooks.includes(book.hash) && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
@@ -102,6 +104,10 @@ const BookItem: React.FC<BookItemProps> = ({
</div>
<div
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
style={{
height: `${iconSize15}px`,
minHeight: `${iconSize15}px`,
}}
>
{book.progress && <ReadingProgress book={book} />}
<div className='flex items-center justify-center gap-x-2'>
@@ -6,7 +6,7 @@ import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-ico
import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
@@ -66,6 +66,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [sortOrder, setSortOrder] = useState(
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
);
const [coverFit, setCoverFit] = useState(searchParams?.get('cover') || settings.libraryCoverFit);
const isImportingBook = useRef(false);
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
@@ -106,6 +107,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const view = searchParams?.get('view') || settings.libraryViewMode;
const sort = searchParams?.get('sort') || settings.librarySortBy;
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
const cover = searchParams?.get('cover') || settings.libraryCoverFit;
const params = new URLSearchParams(searchParams?.toString());
if (query) {
params.set('q', query);
@@ -132,6 +134,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
} else {
params.delete('view');
}
setCoverFit(cover);
if (cover === 'crop') {
params.delete('cover');
}
if (sort === 'updated' && order === 'desc' && view === 'grid') {
params.delete('sort');
params.delete('order');
@@ -287,9 +293,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
>
{filteredBookshelfItems.map((item, index) => (
<BookshelfItem
key={`library-item-${index}`}
mode={viewMode as LibraryViewModeType}
key={`library-item-${'hash' in item ? item.hash : item.id}-${index}`}
item={item}
mode={viewMode as LibraryViewModeType}
coverFit={coverFit as LibraryCoverFitType}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
setLoading={setLoading}
@@ -10,7 +10,7 @@ import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { getOSPlatform } from '@/utils/misc';
import { getLocalBookFilename } from '@/utils/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { Book, BookGroupType, BooksGroup } from '@/types/book';
@@ -73,6 +73,7 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
interface BookshelfItemProps {
mode: LibraryViewModeType;
item: BookshelfItem;
coverFit: LibraryCoverFitType;
isSelectMode: boolean;
selectedBooks: string[];
transferProgress: number | null;
@@ -88,6 +89,7 @@ interface BookshelfItemProps {
const BookshelfItem: React.FC<BookshelfItemProps> = ({
mode,
item,
coverFit,
isSelectMode,
selectedBooks,
transferProgress,
@@ -276,6 +278,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
<BookItem
mode={mode}
book={item}
coverFit={coverFit}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
transferProgress={transferProgress}
@@ -13,6 +13,7 @@ import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { navigateToLibrary } from '@/utils/nav';
import { debounce } from '@/utils/debounce';
@@ -21,7 +22,7 @@ import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
import ImportMenu from './ImportMenu';
import SortMenu from './SortMenu';
import ViewMenu from './ViewMenu';
interface LibraryHeaderProps {
isSelectMode: boolean;
@@ -44,7 +45,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const router = useRouter();
const searchParams = useSearchParams();
const { appService } = useEnv();
const { statusBarHeight } = useThemeStore();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const { currentBookshelf } = useLibraryStore();
const {
isTrafficLightVisible,
@@ -58,6 +59,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const headerRef = useRef<HTMLDivElement>(null);
const iconSize18 = useResponsiveSize(18);
const iconSize20 = useResponsiveSize(20);
const insets = useSafeAreaInsets();
useShortcuts({
onToggleSelectMode,
@@ -102,6 +104,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
0,
);
if (!insets) return null;
return (
<div
ref={headerRef}
@@ -111,8 +115,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
)}
style={{
marginTop: appService?.hasSafeAreaInset
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: '',
? `max(${insets.top}px, ${systemUIVisible ? statusBarHeight : 0}px)`
: '0px',
}}
>
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
@@ -224,7 +228,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<SortMenu />
<ViewMenu />
</Dropdown>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
@@ -38,6 +38,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
const [isAlwaysShowStatusBar, setIsAlwaysShowStatusBar] = useState(settings.alwaysShowStatusBar);
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
const [isOpenLastBooks, setIsOpenLastBooks] = useState(settings.openLastBooks);
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
@@ -92,6 +93,14 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const toggleAlwaysShowStatusBar = () => {
settings.alwaysShowStatusBar = !settings.alwaysShowStatusBar;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAlwaysShowStatusBar(settings.alwaysShowStatusBar);
setIsDropdownOpen?.(false);
};
const toggleAutoUploadBooks = () => {
settings.autoUpload = !settings.autoUpload;
setSettings(settings);
@@ -214,6 +223,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
onClick={toggleAlwaysOnTop}
/>
)}
{appService?.isMobileApp && (
<MenuItem
label={_('Always Show Status Bar')}
Icon={isAlwaysShowStatusBar ? MdCheck : undefined}
onClick={toggleAlwaysShowStatusBar}
/>
)}
<MenuItem
label={_('Keep Screen Awake')}
Icon={isScreenWakeLock ? MdCheck : undefined}
@@ -4,15 +4,15 @@ import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
interface SortMenuProps {
interface ViewMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
@@ -22,12 +22,18 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const viewMode = settings.libraryViewMode;
const sortBy = settings.librarySortBy;
const isAscending = settings.librarySortAscending;
const coverFit = settings.libraryCoverFit;
const viewOptions = [
{ label: _('List'), value: 'list' },
{ label: _('Grid'), value: 'grid' },
];
const coverFitOptions = [
{ label: _('Crop'), value: 'crop' },
{ label: _('Fit'), value: 'fit' },
];
const sortByOptions = [
{ label: _('Title'), value: 'title' },
{ label: _('Author'), value: 'author' },
@@ -52,6 +58,17 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
navigateToLibrary(router, `${params.toString()}`);
};
const handleToggleCropCovers = (value: LibraryCoverFitType) => {
settings.libraryCoverFit = value;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
params.set('cover', value);
navigateToLibrary(router, `${params.toString()}`);
};
const handleSetSortBy = (value: LibrarySortByType) => {
settings.librarySortBy = value;
setSettings(settings);
@@ -89,6 +106,22 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Book Covers')}
buttonClass='h-8'
labelClass='text-sm sm:text-xs'
disabled
/>
{coverFitOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={coverFit === option.value ? MdCheck : undefined}
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Sort by...')}
buttonClass='h-8'
@@ -118,4 +151,4 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
);
};
export default SortMenu;
export default ViewMenu;
+16 -15
View File
@@ -28,7 +28,7 @@ import { usePullToRefresh } from '@/hooks/usePullToRefresh';
import { useTheme } from '@/hooks/useTheme';
import { useDemoBooks } from './hooks/useDemoBooks';
import { useBooksSync } from './hooks/useBooksSync';
import { useThemeStore } from '@/store/themeStore';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { lockScreenOrientation } from '@/utils/bridge';
@@ -69,8 +69,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
} = useLibraryStore();
const _ = useTranslation();
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
const insets = useSafeAreaInsets();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { statusBarHeight } = useThemeStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
const [libraryLoaded, setLibraryLoaded] = useState(false);
@@ -401,8 +401,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
};
const selectFilesTauri = async () => {
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
const exts = appService?.isMobileApp ? [] : SUPPORTED_FILE_EXTS;
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
if (appService?.isIOSApp) {
return files.filter((file) => {
const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown';
return SUPPORTED_FILE_EXTS.includes(fileExt);
});
}
// Cannot filter out files on Android since some content providers may not return the file name
return files;
};
@@ -525,11 +531,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
let files;
if (isTauriAppPlatform()) {
if (appService?.isIOSApp) {
files = (await selectFilesWeb()) as [File];
} else {
files = (await selectFilesTauri()) as [string];
}
files = (await selectFilesTauri()) as [string];
} else {
files = (await selectFilesWeb()) as [File];
}
@@ -559,7 +561,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
setShowDetailsBook(book);
};
if (!appService) {
if (!appService || !insets) {
return null;
}
@@ -582,7 +584,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<div className='fixed top-0 z-40 w-full'>
<div className='top-0 z-40 w-full'>
<LibraryHeader
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
@@ -603,14 +605,13 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
ref={containerRef}
className={clsx(
'scroll-container drop-zone flex-grow overflow-y-auto',
appService?.hasSafeAreaInset && 'pt-[52px]',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
isDragging && 'drag-over',
)}
style={{
marginTop: appService?.hasSafeAreaInset
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: '48px',
paddingTop: '0px',
paddingRight: `${insets.right}px`,
paddingBottom: `${insets.bottom}px`,
paddingLeft: `${insets.left}px`,
}}
>
<DropIndicator />
@@ -4,10 +4,12 @@ import React, { useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { getGridTemplate, getInsetEdges } from '@/utils/grid';
import { getViewInsets } from '@/utils/insets';
import FoliateViewer from './FoliateViewer';
import getGridTemplate from '@/utils/grid';
import SectionInfo from './SectionInfo';
import HeaderBar from './HeaderBar';
import FooterBar from './FooterBar';
@@ -18,7 +20,6 @@ import Annotator from './annotator/Annotator';
import FootnotePopup from './FootnotePopup';
import HintInfo from './HintInfo';
import DoubleBorder from './DoubleBorder';
import TTSControl from './tts/TTSControl';
interface BooksGridProps {
bookKeys: string[];
@@ -28,10 +29,14 @@ interface BooksGridProps {
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings, hoveredBookKey } = useReaderStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { getGridInsets, setGridInsets, hoveredBookKey } = useReaderStore();
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
const screenInsets = useSafeAreaInsets();
const aspectRatio = window.innerWidth / window.innerHeight;
const gridTemplate = getGridTemplate(bookKeys.length, aspectRatio);
useEffect(() => {
if (!sideBarBookKey) return;
@@ -41,6 +46,23 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarBookKey]);
useEffect(() => {
if (!screenInsets) return;
bookKeys.forEach((bookKey, index) => {
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
const gridInsets = {
top: top ? screenInsets.top : 0,
right: right ? screenInsets.right : 0,
bottom: bottom ? screenInsets.bottom : 0,
left: left ? screenInsets.left : 0,
};
setGridInsets(bookKey, gridInsets);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bookKeys, screenInsets]);
if (!screenInsets) return null;
return (
<div
className={clsx('relative grid h-full flex-grow')}
@@ -54,13 +76,20 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const gridInsets = getGridInsets(bookKey);
const { book, bookDoc } = bookData || {};
if (!book || !config || !bookDoc || !viewSettings) return null;
if (!book || !config || !bookDoc || !viewSettings || !gridInsets) return null;
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
const horizontalGapPercent = viewSettings.gapPercent;
const verticalMarginPixels = viewSettings.marginPx;
const viewInsets = getViewInsets(viewSettings);
const contentInsets = {
top: gridInsets.top + viewInsets.top,
right: gridInsets.right + viewInsets.right,
bottom: gridInsets.bottom + viewInsets.bottom,
left: gridInsets.left + viewInsets.left,
};
const scrolled = viewSettings.scrolled;
const showBarsOnScroll = viewSettings.showBarsOnScroll;
const showHeader = viewSettings.showHeader && (scrolled ? showBarsOnScroll : true);
@@ -72,10 +101,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
key={bookKey}
className={clsx(
'relative h-full w-full overflow-hidden',
index === 0 &&
!viewSettings?.scrolled &&
appService?.hasSafeAreaInset &&
'pt-[env(safe-area-inset-top)]',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
)}
>
@@ -87,16 +112,22 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isHoveredAnim={bookKeys.length > 2}
onCloseBook={onCloseBook}
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
gridInsets={gridInsets}
/>
<FoliateViewer
bookKey={bookKey}
bookDoc={bookDoc}
config={config}
contentInsets={contentInsets}
/>
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
{viewSettings.vertical && viewSettings.scrolled && (
<>
{(showFooter || viewSettings.doubleBorder) && (
<div
className='bg-base-100 absolute left-0 top-0 h-full'
style={{
width: `calc(${horizontalGapPercent}%)`,
height: `calc(100% - ${verticalMarginPixels}px)`,
width: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px)`,
height: `calc(100%)`,
}}
/>
)}
@@ -104,8 +135,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<div
className='bg-base-100 absolute right-0 top-0 h-full'
style={{
width: `calc(${horizontalGapPercent}%)`,
height: `calc(100% - ${verticalMarginPixels}px)`,
width: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px)`,
height: `calc(100%)`,
}}
/>
)}
@@ -117,7 +148,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
showFooter={showFooter}
borderColor={viewSettings.borderColor}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
/>
)}
{showHeader && (
@@ -127,7 +158,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isScrolled={viewSettings.scrolled}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
gridInsets={gridInsets}
/>
)}
<HintInfo
@@ -136,7 +168,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isScrolled={viewSettings.scrolled}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
gridInsets={gridInsets}
/>
{showFooter && (
<ProgressInfoView
@@ -146,7 +179,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
pageinfo={pageinfo}
timeinfo={timeinfo}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
contentInsets={contentInsets}
gridInsets={gridInsets}
/>
)}
<Annotator bookKey={bookKey} />
@@ -157,12 +191,12 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
section={section}
pageinfo={pageinfo}
isHoveredAnim={false}
gridInsets={gridInsets}
/>
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
</div>
);
})}
<TTSControl />
</div>
);
};
@@ -1,31 +1,32 @@
import { Insets } from '@/types/misc';
interface DoubleBorderProps {
borderColor: string;
horizontalGap: number;
verticalMargin: number;
showHeader: boolean;
showFooter: boolean;
contentInsets: Insets;
}
const paddingPx = 10;
const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderColor,
horizontalGap,
verticalMargin,
showHeader,
showFooter,
contentInsets,
}) => {
return (
<>
<div>
{/* outter frame */}
<div
className={'borderframe pointer-events-none absolute'}
style={{
border: `4px solid ${borderColor}`,
height: `calc(100% - ${verticalMargin * 2}px + ${paddingPx * 2}px)`,
top: `calc(${verticalMargin}px - ${paddingPx}px)`,
left: `calc(${horizontalGap}% - ${showFooter ? 32 : 0}px - ${paddingPx}px)`,
right: `calc(${horizontalGap}% - ${showHeader ? 32 : 0}px - ${paddingPx}px)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px + ${paddingPx * 2}px)`,
top: `calc(${contentInsets.top}px - ${paddingPx}px)`,
left: `calc(${contentInsets.left}px - ${paddingPx}px)`,
right: `calc(${contentInsets.right}px - ${paddingPx}px)`,
}}
></div>
{/* inner frame */}
@@ -33,10 +34,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
className={'borderframe pointer-events-none absolute'}
style={{
border: `1px solid ${borderColor}`,
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: showFooter ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
right: showHeader ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px`,
right: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px`,
}}
/>
{/* footer */}
@@ -48,9 +49,9 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderBottom: `1px solid ${borderColor}`,
borderLeft: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(${horizontalGap}% - 32px)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(${contentInsets.left}px)`,
}}
/>
)}
@@ -63,13 +64,13 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderBottom: `1px solid ${borderColor}`,
borderRight: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(100% - ${horizontalGap}%)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(100% - ${contentInsets.right}px - 32px)`,
}}
/>
)}
</>
</div>
);
};
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { BookDoc, getDirection } from '@/libs/document';
import { BookConfig } from '@/types/book';
import { FoliateView, wrappedFoliateView } from '@/types/view';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
@@ -29,27 +30,38 @@ import {
import { getMaxInlineSize } from '@/utils/config';
import { getDirFromUILanguage } from '@/utils/rtl';
import { isCJKLang } from '@/utils/lang';
import { isTauriAppPlatform } from '@/services/environment';
import { transformContent } from '@/services/transformService';
import { lockScreenOrientation } from '@/utils/bridge';
import { useTextTranslation } from '../hooks/useTextTranslation';
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
import { getViewInsets } from '@/utils/insets';
declare global {
interface Window {
eval(script: string): void;
}
}
const FoliateViewer: React.FC<{
bookKey: string;
bookDoc: BookDoc;
config: BookConfig;
}> = ({ bookKey, bookDoc, config }) => {
const { appService } = useEnv();
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<FoliateView | null>(null);
const isViewCreated = useRef(false);
contentInsets: Insets;
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
const { getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { getBookData } = useBookDataStore();
const { appService } = useEnv();
const { themeCode, isDarkMode } = useThemeStore();
const viewSettings = getViewSettings(bookKey);
const viewRef = useRef<FoliateView | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isViewCreated = useRef(false);
const [toastMessage, setToastMessage] = useState('');
useEffect(() => {
const timer = setTimeout(() => setToastMessage(''), 2000);
return () => clearTimeout(timer);
@@ -80,7 +92,7 @@ const FoliateViewer: React.FC<{
.then((data) => {
const viewSettings = getViewSettings(bookKey);
if (viewSettings && detail.type === 'text/css')
return transformStylesheet(viewSettings, width, height, data);
return transformStylesheet(width, height, data);
if (viewSettings && detail.type === 'application/xhtml+xml') {
const ctx = {
bookKey,
@@ -117,6 +129,11 @@ const FoliateViewer: React.FC<{
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
// Inline scripts in tauri platforms are not executed by default
if (viewSettings.allowScript && isTauriAppPlatform()) {
evalInlineScripts(detail.doc);
}
// only call on load if we have highlighting turned on.
if (viewSettings.codeHighlighting) {
manageSyntaxHighlighting(detail.doc, viewSettings);
@@ -139,6 +156,22 @@ const FoliateViewer: React.FC<{
}
};
const evalInlineScripts = (doc: Document) => {
if (doc.defaultView && doc.defaultView.frameElement) {
const iframe = doc.defaultView.frameElement as HTMLIFrameElement;
const scripts = doc.querySelectorAll('script:not([src])');
scripts.forEach((script, index) => {
const scriptContent = script.textContent || script.innerHTML;
try {
console.warn('Evaluating inline scripts in iframe');
iframe.contentWindow?.eval(scriptContent);
} catch (error) {
console.error(`Error executing iframe script ${index + 1}:`, error);
}
});
}
};
const docRelocateHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
@@ -166,14 +199,6 @@ const FoliateViewer: React.FC<{
onRendererRelocate: docRelocateHandler,
});
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode]);
useEffect(() => {
if (isViewCreated.current) return;
isViewCreated.current = true;
@@ -186,10 +211,6 @@ const FoliateViewer: React.FC<{
document.body.append(view);
containerRef.current?.appendChild(view);
const containerRect = containerRef.current?.getBoundingClientRect();
const width = containerRect?.width || window.innerWidth;
const height = containerRect?.height || window.innerHeight;
const viewSettings = getViewSettings(bookKey)!;
const writingMode = viewSettings.writingMode;
if (writingMode) {
@@ -215,15 +236,13 @@ const FoliateViewer: React.FC<{
detail.allowScript = viewSettings.allowScript ?? false;
}
});
const viewWidth = appService?.isMobile ? screen.width : window.innerWidth;
const viewHeight = appService?.isMobile ? screen.height : window.innerHeight;
const width = viewWidth - insets.left - insets.right;
const height = viewHeight - insets.top - insets.bottom;
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
view.renderer.setStyles?.(getStyles(viewSettings));
const isScrolled = viewSettings.scrolled!;
const showHeader = viewSettings.showHeader!;
const showFooter = viewSettings.showFooter!;
const isCompact = !showHeader && !showFooter;
const marginPx = isCompact ? viewSettings.compactMarginPx : viewSettings.marginPx;
const gapPercent = isCompact ? viewSettings.compactGapPercent : viewSettings.gapPercent;
const animated = viewSettings.animated!;
const maxColumnCount = viewSettings.maxColumnCount!;
const maxInlineSize = getMaxInlineSize(viewSettings);
@@ -237,12 +256,10 @@ const FoliateViewer: React.FC<{
} else {
view.renderer.removeAttribute('animated');
}
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
view.renderer.setAttribute('margin', `${marginPx}px`);
view.renderer.setAttribute('gap', `${gapPercent}%`);
view.renderer.setAttribute('max-column-count', maxColumnCount);
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
applyMarginAndGap();
const lastLocation = config.location;
if (lastLocation) {
@@ -256,15 +273,63 @@ const FoliateViewer: React.FC<{
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const applyMarginAndGap = () => {
const viewSettings = getViewSettings(bookKey)!;
const viewInsets = getViewInsets(viewSettings);
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
const topMargin = (showTopHeader ? insets.top : viewInsets.top) + moreTopInset;
const rightMargin = insets.right + moreRightInset;
const bottomMargin = (showBottomFooter ? insets.bottom : viewInsets.bottom) + moreBottomInset;
const leftMargin = insets.left + moreLeftInset;
viewRef.current?.renderer.setAttribute('margin-top', `${topMargin}px`);
viewRef.current?.renderer.setAttribute('margin-right', `${rightMargin}px`);
viewRef.current?.renderer.setAttribute('margin-bottom', `${bottomMargin}px`);
viewRef.current?.renderer.setAttribute('margin-left', `${leftMargin}px`);
viewRef.current?.renderer.setAttribute('gap', `${viewSettings.gapPercent}%`);
if (viewSettings.scrolled) {
viewRef.current?.renderer.setAttribute('flow', 'scrolled');
}
};
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode]);
useEffect(() => {
if (viewRef.current && viewRef.current.renderer && viewSettings) {
applyMarginAndGap();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
insets.top,
insets.right,
insets.bottom,
insets.left,
viewSettings?.doubleBorder,
viewSettings?.showHeader,
viewSettings?.showFooter,
]);
return (
<>
<div
ref={containerRef}
className='foliate-viewer h-[100%] w-[100%]'
{...mouseHandlers}
{...touchHandlers}
/>
</>
<div
ref={containerRef}
className='foliate-viewer h-[100%] w-[100%]'
{...mouseHandlers}
{...touchHandlers}
/>
);
};
@@ -21,8 +21,10 @@ import { eventDispatcher } from '@/utils/event';
import { viewPagination } from '../hooks/usePagination';
import { saveViewSettings } from '../utils/viewSettingsHelper';
import { PageInfo } from '@/types/book';
import { Insets } from '@/types/misc';
import Button from '@/components/Button';
import Slider from '@/components/Slider';
import TTSControl from './tts/TTSControl';
interface FooterBarProps {
bookKey: string;
@@ -30,6 +32,7 @@ interface FooterBarProps {
section?: PageInfo;
pageinfo?: PageInfo;
isHoveredAnim: boolean;
gridInsets: Insets;
}
const FooterBar: React.FC<FooterBarProps> = ({
@@ -38,6 +41,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
section,
pageinfo,
isHoveredAnim,
gridInsets,
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
@@ -64,9 +68,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
};
const handleMarginChange = (value: number) => {
const viewSettings = getViewSettings(bookKey)!;
const marginPx = Math.round((value / 100) * 88);
const gapPercent = Math.round((value / 100) * 10);
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx, false, false);
viewSettings.marginTopPx = marginPx;
viewSettings.marginBottomPx = marginPx / 2;
viewSettings.marginLeftPx = marginPx / 2;
viewSettings.marginRightPx = marginPx / 2;
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent, false, false);
view?.renderer.setAttribute('margin', `${marginPx}px`);
view?.renderer.setAttribute('gap', `${gapPercent}%`);
@@ -155,6 +163,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
? (progressInfo!.current + 1) / progressInfo!.total || 0
: 0;
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
return (
<>
<div
@@ -194,9 +204,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom) + 64px)'
: '64px',
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
}}
>
<div className='flex w-full items-center justify-between gap-x-6'>
@@ -250,9 +258,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom) + 64px)'
: '64px',
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
}}
>
<Slider
@@ -269,7 +275,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
<div className='flex w-full items-center justify-between gap-x-6'>
<Slider
initialValue={getMarginProgressValue(
viewSettings?.marginPx ?? 44,
viewSettings?.marginTopPx ?? 44,
viewSettings?.gapPercent ?? 5,
)}
bubbleElement={<TbBoxMargin size={marginIconSize} />}
@@ -292,8 +298,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
<div
className={clsx(
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom)+16px)]',
)}
style={{
paddingBottom: isMobile ? `${gridInsets.bottom + 16}px` : '0px',
}}
>
<Button
icon={<TOCIcon size={tocIconSize} className='' />}
@@ -318,8 +326,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
onClick={() => handleSetActionTab('tts')}
/>
</div>
{/* Desktop footer bar */}
<div className='hidden w-full items-center gap-x-4 px-4 sm:flex'>
{/* Desktop / Pad footer bar */}
<div
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
style={{
bottom: isMobile ? `${gridInsets.bottom / 2}px` : '0px',
}}
>
<Button
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
@@ -372,6 +385,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
/>
</div>
</div>
<TTSControl bookKey={bookKey} />
</>
);
};
@@ -66,7 +66,10 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
footnoteRef.current?.replaceChildren(view);
const { renderer } = view;
renderer.setAttribute('flow', 'scrolled');
renderer.setAttribute('margin', '0px');
renderer.setAttribute('margin-top', '0px');
renderer.setAttribute('margin-right', '0px');
renderer.setAttribute('margin-bottom', '0px');
renderer.setAttribute('margin-left', '0px');
renderer.setAttribute('gap', '0%');
const viewSettings = getViewSettings(bookKey)!;
const themeCode = getThemeCode();
@@ -2,6 +2,7 @@ import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
@@ -22,6 +23,7 @@ interface HeaderBarProps {
bookTitle: string;
isTopLeft: boolean;
isHoveredAnim: boolean;
gridInsets: Insets;
onCloseBook: (bookKey: string) => void;
onSetSettingsDialogOpen: (open: boolean) => void;
}
@@ -31,6 +33,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
bookTitle,
isTopLeft,
isHoveredAnim,
gridInsets,
onCloseBook,
onSetSettingsDialogOpen,
}) => {
@@ -77,10 +80,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
return (
<div
className={clsx(
'bg-base-100 absolute top-0 w-full',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
)}
className={clsx('bg-base-100 absolute top-0 w-full')}
style={{
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
}}
>
<div
className={clsx('absolute top-0 z-10 hidden h-11 w-full sm:flex')}
@@ -89,11 +92,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
/>
<div
className={clsx(
'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'bg-base-100 absolute left-0 right-0 top-0 z-10',
isVisible ? 'visible' : 'hidden',
)}
style={{
height: systemUIVisible ? `max(env(safe-area-inset-top), ${statusBarHeight}px)` : '',
height: systemUIVisible ? `${Math.max(gridInsets.top, statusBarHeight)}px` : '0px',
}}
/>
<div
@@ -110,12 +113,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
)}
style={{
marginTop: systemUIVisible
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: 'env(safe-area-inset-top)',
? `${Math.max(gridInsets.top, statusBarHeight)}px`
: `${gridInsets.top}px`,
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4'>
<div className='bg-base-100 sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4 pe-2'>
<div className='hidden sm:flex'>
<SidebarToggler bookKey={bookKey} />
</div>
@@ -129,7 +132,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
</h2>
</div>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4 ps-2'>
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
@@ -1,5 +1,8 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { eventDispatcher } from '@/utils/event';
interface SectionInfoProps {
@@ -8,7 +11,8 @@ interface SectionInfoProps {
isScrolled: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
contentInsets: Insets;
gridInsets: Insets;
}
const HintInfo: React.FC<SectionInfoProps> = ({
@@ -17,8 +21,16 @@ const HintInfo: React.FC<SectionInfoProps> = ({
isScrolled,
isVertical,
horizontalGap,
verticalMargin,
contentInsets,
gridInsets,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
const hintTimeout = useRef(2000);
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -50,14 +62,16 @@ const HintInfo: React.FC<SectionInfoProps> = ({
<>
<div
className={clsx(
'absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'absolute left-0 right-0 top-0 z-10',
hintMessage ? 'bg-base-100' : 'bg-transparent',
)}
style={{
height: `${topInset}px`,
}}
/>
<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
@@ -71,11 +85,16 @@ const HintInfo: React.FC<SectionInfoProps> = ({
style={
isVertical
? {
bottom: `${verticalMargin * 1.5}px`,
left: `calc(100% - ${horizontalGap}%)`,
bottom: `${contentInsets.bottom * 1.5}px`,
right: showDoubleBorder
? `calc(${contentInsets.right}px)`
: `calc(${Math.max(0, contentInsets.right - 32)}px)`,
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
}
: { insetInlineEnd: `${horizontalGap}%` }
: {
top: `${topInset}px`,
insetInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
}
}
>
<h2 className={clsx('text-neutral-content text-center font-sans text-xs font-light')}>
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import React from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -12,7 +13,8 @@ interface PageInfoProps {
pageinfo?: PageInfo;
timeinfo?: TimeInfo;
horizontalGap: number;
verticalMargin: number;
contentInsets: Insets;
gridInsets: Insets;
}
const ProgressInfoView: React.FC<PageInfoProps> = ({
@@ -22,11 +24,13 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
pageinfo,
timeinfo,
horizontalGap,
verticalMargin,
contentInsets,
gridInsets,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getViewSettings } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
@@ -44,36 +48,43 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
totalPage: pageinfo.total,
})
: '';
const timeInfo = timeinfo
const timeLeft = timeinfo
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
: '';
const { page = 0, pages = 0 } = view?.renderer || {};
const pageLeft =
pages - 1 > page ? _('{{count}} pages left in chapter', { count: pages - 1 - page }) : '';
return (
<div
className={clsx(
'pageinfo absolute bottom-0 flex items-center justify-between',
'progressinfo 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',
isVertical ? 'writing-vertical-rl' : 'h-[52px] w-full',
isScrolled && !isVertical && 'bg-base-100',
)}
style={
isVertical
? {
bottom: `${verticalMargin * 1.5}px`,
left: showDoubleBorder ? `calc(${horizontalGap}% - 32px)` : 0,
bottom: `${contentInsets.bottom * 1.5}px`,
left: showDoubleBorder
? `calc(${contentInsets.left}px)`
: `calc(${Math.max(0, contentInsets.left - 32)}px)`,
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 3}px)`,
height: `calc(100% - ${((contentInsets.top + contentInsets.bottom) / 2) * 3}px)`,
}
: {
paddingInlineStart: `${horizontalGap}%`,
paddingInlineEnd: `${horizontalGap}%`,
paddingBottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom)*0.67)'
: 0,
paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
paddingInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.67}px` : 0,
}
}
>
{viewSettings.showRemainingTime && <span className='text-start'>{timeInfo}</span>}
{viewSettings.showRemainingTime ? (
<span className='text-start'>{timeLeft}</span>
) : viewSettings.showRemainingPages ? (
<span className='text-start'>{pageLeft}</span>
) : null}
{viewSettings.showPageNumber && <span className='ms-auto text-end'>{pageInfo}</span>}
</div>
);
@@ -17,10 +17,13 @@ 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 { isTauriAppPlatform } from '@/services/environment';
import { getSysFontsList, setSystemUIVisibility } from '@/utils/bridge';
import { AboutWindow } from '@/components/AboutWindow';
import { UpdaterWindow } from '@/components/UpdaterWindow';
import { Toast } from '@/components/Toast';
import { getLocale } from '@/utils/misc';
import { initDayjs } from '@/utils/time';
import ReaderContent from './ReaderContent';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
@@ -30,33 +33,23 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { settings, setSettings } = useSettingsStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [libraryLoaded, setLibraryLoaded] = useState(false);
const isInitiating = useRef(false);
useTheme({ systemUIVisible: false, appThemeColor: 'base-100' });
useTheme({ systemUIVisible: settings.alwaysShowStatusBar, appThemeColor: 'base-100' });
useScreenWakeLock(settings.screenWakeLock);
useEffect(() => {
mountAdditionalFonts(document);
interceptWindowOpen();
if (isTauriAppPlatform()) {
setTimeout(getSysFontsList, 3000);
}
initDayjs(getLocale());
}, []);
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);
@@ -102,9 +95,10 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
useEffect(() => {
if (!appService?.isMobileApp) return;
const systemUIVisible = !!hoveredBookKey;
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
if (systemUIVisible) {
const systemUIVisible = !!hoveredBookKey || settings.alwaysShowStatusBar;
const visible = systemUIVisible && !systemUIAlwaysHidden;
setSystemUIVisibility({ visible, darkMode: isDarkMode });
if (visible) {
showSystemUI();
} else {
dismissSystemUI();
@@ -36,7 +36,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
const { saveSettings } = useSettingsStore();
const { getConfig, getBookData, saveConfig } = useBookDataStore();
const { getView, setBookKeys } = useReaderStore();
const { getView, setBookKeys, getViewSettings } = useReaderStore();
const { initViewState, getViewState, clearViewState } = useReaderStore();
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
const isInitiating = useRef(false);
@@ -150,7 +150,8 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
if (!bookKeys || bookKeys.length === 0) return null;
const bookData = getBookData(bookKeys[0]!);
if (!bookData || !bookData.book || !bookData.bookDoc) {
const viewSettings = getViewSettings(bookKeys[0]!);
if (!bookData || !bookData.book || !bookData.bookDoc || !viewSettings) {
setTimeout(() => setLoading(true), 300);
return (
loading && (
@@ -1,5 +1,8 @@
import clsx from 'clsx';
import React from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
interface SectionInfoProps {
section?: string;
@@ -7,7 +10,8 @@ interface SectionInfoProps {
isScrolled: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
contentInsets: Insets;
gridInsets: Insets;
}
const SectionInfo: React.FC<SectionInfoProps> = ({
@@ -16,32 +20,48 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
isScrolled,
isVertical,
horizontalGap,
verticalMargin,
contentInsets,
gridInsets,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
return (
<>
<div
className={clsx(
'absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'absolute left-0 right-0 top-0 z-10',
isScrolled && !isVertical && 'bg-base-100',
)}
style={{
height: `${topInset}px`,
}}
/>
<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}%)`,
top: `${contentInsets.top * 1.5}px`,
right: showDoubleBorder
? `calc(${contentInsets.right}px)`
: `calc(${Math.max(0, contentInsets.right - 32)}px)`,
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 2}px)`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
}
: {
top: `${topInset}px`,
insetInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
width: `calc(100% - ${contentInsets.left + contentInsets.right}px)`,
}
: { insetInlineStart: `${horizontalGap}%`, width: `calc(100% - ${horizontalGap * 2}%)` }
}
>
<h2
@@ -6,9 +6,8 @@ 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 { isTranslationAvailable } from '@/services/translators/utils';
import Button from '@/components/Button';
import { getLocale } from '@/utils/misc';
const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
const _ = useTranslation();
@@ -19,9 +18,9 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
const bookData = getBookData(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
const primaryLanguage = bookData?.book?.primaryLanguage;
const targetLanguage = viewSettings.translateTargetLang;
const [translationAvailable, setTranslationAvailable] = useState(
isTranslationAvailable(bookData?.book, viewSettings.translateTargetLang),
);
useEffect(() => {
if (translationEnabled === viewSettings.translationEnabled) return;
@@ -34,19 +33,27 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [translationEnabled]);
useEffect(() => {
setTranslationEnabled(viewSettings.translationEnabled);
setTranslationAvailable(
isTranslationAvailable(bookData?.book, viewSettings.translateTargetLang),
);
}, [bookData, viewSettings.translationEnabled, viewSettings.translateTargetLang]);
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()))
}
disabled={!translationAvailable}
onClick={() => setTranslationEnabled(!translationEnabled)}
tooltip={translationEnabled ? _('Disable Translation') : _('Enable Translation')}
tooltip={
translationAvailable
? translationEnabled
? _('Disable Translation')
: _('Enable Translation')
: _('Translation Not Available')
}
tooltipDirection='bottom'
></Button>
);
@@ -107,7 +107,14 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
return (
<div
tabIndex={0}
className='view-menu dropdown-content bgcolor-base-200 dropdown-right no-triangle border-base-200 z-20 mt-1 border shadow-2xl'
className={clsx(
'view-menu dropdown-content dropdown-right no-triangle z-20 mt-1 border',
'bgcolor-base-200 border-base-200 shadow-2xl',
)}
style={{
maxWidth: `${window.innerWidth - 40}px`,
marginRight: window.innerWidth < 640 ? '-36px' : '0px',
}}
>
<div className={clsx('flex items-center justify-between rounded-md')}>
<button
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { FiSearch } from 'react-icons/fi';
import { FiCopy } from 'react-icons/fi';
import { PiHighlighterFill } from 'react-icons/pi';
@@ -26,6 +26,7 @@ import { useTextSelector } from '../../hooks/useTextSelector';
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { findTocItemBS } from '@/utils/toc';
import { throttle } from '@/utils/throttle';
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
import AnnotationPopup from './AnnotationPopup';
import WiktionaryPopup from './WiktionaryPopup';
@@ -78,13 +79,17 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const annotPopupHeight = useResponsiveSize(44);
const androidSelectionHandlerHeight = 0;
const handleDismissPopup = () => {
setSelection(null);
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleDismissPopup = useCallback(
throttle(() => {
setSelection(null);
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
}, 500),
[],
);
const handleDismissPopupAndSelection = () => {
handleDismissPopup();
@@ -69,16 +69,18 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
className={clsx(
'w-4 p-0 text-center leading-none',
style === 'highlight' &&
(selectedStyle === 'highlight' ? `bg-${selectedColor}-400` : `bg-gray-300`),
(selectedStyle === 'highlight'
? `bg-${selectedColor}-300 pt-[2px]`
: `bg-gray-300 pt-[2px]`),
(style === 'underline' || style === 'squiggly') &&
'text-gray-300 underline decoration-2',
style === 'underline' &&
(selectedStyle === 'underline'
? `decoration-${selectedColor}-400`
? `decoration-${selectedColor}-300`
: `decoration-gray-300`),
style === 'squiggly' &&
(selectedStyle === 'squiggly'
? `decoration-wavy decoration-${selectedColor}-400`
? `decoration-wavy decoration-${selectedColor}-300`
: `decoration-gray-300 decoration-wavy`),
)}
>
@@ -100,10 +102,10 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
key={color}
onClick={() => handleSelectColor(color)}
style={{ width: size16, height: size16 }}
className={clsx(`rounded-full p-0`, selectedColor !== color && `bg-${color}-400`)}
className={clsx(`rounded-full p-0`, selectedColor !== color && `bg-${color}-300`)}
>
{selectedColor === color && (
<FaCheckCircle size={size16} className={clsx(`fill-${color}-400`)} />
<FaCheckCircle size={size16} className={clsx(`fill-${color}-300`)} />
)}
</button>
))}
@@ -7,20 +7,14 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useTranslator } from '@/hooks/useTranslator';
import { TRANSLATED_LANGS } from '@/services/constants';
import { UseTranslatorOptions, getTranslators } from '@/services/translators';
import { localeToLang } from '@/utils/lang';
import Select from '@/components/Select';
const notSupportedLangs = ['hi', 'vi'];
const notSupportedLangs = [''];
const generateTranslatorLangs = () => {
const langs = { ...TRANSLATED_LANGS };
const result: Record<string, string> = {};
for (const [code, name] of Object.entries(langs)) {
if (notSupportedLangs.includes(code)) continue;
const newCode = localeToLang(code).toUpperCase();
result[newCode] = name;
}
return result;
return Object.fromEntries(
Object.entries(TRANSLATED_LANGS).filter(([code]) => !notSupportedLangs.includes(code)),
);
};
const TRANSLATOR_LANGS = generateTranslatorLangs();
@@ -205,7 +199,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
</div>
<div className='absolute bottom-0 flex h-8 w-full items-center justify-between bg-gray-600 px-4'>
{provider && !loading && (
<div className='text-xs opacity-60'>
<div className='line-clamp-1 text-xs opacity-60'>
{error
? ''
: _('Translated by {{provider}}.', {
@@ -16,10 +16,11 @@ const NotebookHeader: React.FC<{
}> = ({ isPinned, isSearchBarVisible, handleClose, handleTogglePin, handleToggleSearchBar }) => {
const _ = useTranslation();
const iconSize14 = useResponsiveSize(14);
const iconSize18 = useResponsiveSize(18);
return (
<div className='notebook-header relative flex h-11 items-center px-3' dir='ltr'>
<div className='absolute inset-0 z-[-1] flex items-center justify-center space-x-2'>
<LuNotebookPen />
<LuNotebookPen size={iconSize18} />
<div className='notebook-title hidden text-sm font-medium sm:flex'>{_('Notebook')}</div>
</div>
<div className='flex w-full items-center gap-x-4'>
@@ -44,7 +45,7 @@ const NotebookHeader: React.FC<{
onClick={handleToggleSearchBar}
className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0', isSearchBarVisible && 'bg-base-300')}
>
<FiSearch />
<FiSearch size={iconSize18} />
</button>
</div>
</div>
@@ -216,8 +216,9 @@ const Notebook: React.FC = ({}) => {
}
`}</style>
<div
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
className='drag-bar absolute left-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3'
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
/>
<div className='flex-shrink-0'>
<NotebookHeader
@@ -18,12 +18,14 @@ import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { CODE_LANGUAGES, CodeLanguage, manageSyntaxHighlighting } from '@/utils/highlightjs';
import { SettingsPanelPanelProp } from './SettingsDialog';
import Select from '@/components/Select';
import ThemeEditor from './ThemeEditor';
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor, saveCustomTheme } =
useThemeStore();
@@ -44,6 +46,24 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [codeHighlighting, setcodeHighlighting] = useState(viewSettings.codeHighlighting!);
const [codeLanguage, setCodeLanguage] = useState(viewSettings.codeLanguage!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
overrideColor: setOverrideColor,
invertImgColorInDark: setInvertImgColorInDark,
codeHighlighting: setcodeHighlighting,
codeLanguage: setCodeLanguage,
});
setThemeColor('default');
setThemeMode('auto');
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (invertImgColorInDark === viewSettings.invertImgColorInDark) return;
saveViewSettings(envConfig, bookKey, 'invertImgColorInDark', invertImgColorInDark);
@@ -7,9 +7,12 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import NumberInput from './NumberInput';
const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings } = useReaderStore();
@@ -27,6 +30,26 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [animated, setAnimated] = useState(viewSettings.animated!);
const [allowScript, setAllowScript] = useState(viewSettings.allowScript!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
scrolled: setScrolledMode,
continuousScroll: setIsContinuousScroll,
scrollingOverlap: setScrollingOverlap,
volumeKeysToFlip: setVolumeKeysToFlip,
disableClick: setIsDisableClick,
swapClickArea: setSwapClickArea,
animated: setAnimated,
allowScript: setAllowScript,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isScrolledMode === viewSettings.scrolled) return;
saveViewSettings(envConfig, bookKey, 'scrolled', isScrolledMode);
@@ -85,7 +108,7 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (viewSettings.allowScript === allowScript) return;
saveViewSettings(envConfig, bookKey, 'allowScript', allowScript, true, false);
setTimeout(() => window.location.reload(), 100);
setTimeout(() => window.location.reload(), RELOAD_BEFREE_SAVED_TIMEOUT_MS);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allowScript]);
@@ -3,20 +3,30 @@ import React from 'react';
import { MdCheck } from 'react-icons/md';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { SettingsPanelType } from './SettingsDialog';
import MenuItem from '@/components/MenuItem';
interface DialogMenuProps {
toggleDropdown?: () => void;
activePanel: SettingsPanelType;
setIsDropdownOpen?: (open: boolean) => void;
onReset: () => void;
resetLabel?: string;
}
const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
const DialogMenu: React.FC<DialogMenuProps> = ({ setIsDropdownOpen, onReset, resetLabel }) => {
const _ = useTranslation();
const iconSize = useDefaultIconSize();
const iconSize = useResponsiveSize(16);
const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useSettingsStore();
const handleToggleGlobal = () => {
setFontLayoutSettingsGlobal(!isFontLayoutSettingsGlobal);
toggleDropdown?.();
setIsDropdownOpen?.(false);
};
const handleResetToDefaults = () => {
onReset();
setIsDropdownOpen?.(false);
};
return (
@@ -27,24 +37,18 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
'text-base sm:text-sm',
)}
>
<button
className='hover:bg-base-200 text-base-content flex w-full items-center justify-between rounded-md p-1'
<MenuItem
label={_('Global Settings')}
tooltip={isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')}
buttonClass='lg:tooltip'
Icon={
isFontLayoutSettingsGlobal ? (
<MdCheck size={iconSize} className='text-base-content' />
) : null
}
onClick={handleToggleGlobal}
>
<div className='flex items-center'>
<span style={{ minWidth: `${iconSize}px` }}>
{isFontLayoutSettingsGlobal && <MdCheck className='text-base-content' />}
</span>
<div
className='lg:tooltip'
data-tip={
isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')
}
>
<span className='ml-2 whitespace-nowrap'>{_('Global Settings')}</span>
</div>
</div>
</button>
/>
<MenuItem label={resetLabel || _('Reset Settings')} onClick={handleResetToDefaults} />
</div>
);
};
@@ -24,6 +24,8 @@ import { getOSPlatform, isCJKEnv } from '@/utils/misc';
import { getSysFontsList } from '@/utils/bridge';
import { isTauriAppPlatform } from '@/services/environment';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
import FontDropdown from './FontDropDown';
@@ -79,7 +81,7 @@ const FontFace = ({
);
};
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getView, getViewSettings } = useReaderStore();
@@ -119,10 +121,10 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
break;
}
const [sysFonts, setSysFonts] = useState<string[]>(defaultSysFonts);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultCJKFont, setDefaultCJKFont] = useState(viewSettings.defaultCJKFont!);
const [serifFont, setSerifFont] = useState(viewSettings.serifFont!);
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
@@ -132,6 +134,27 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
return genCJKFontsList(sysFonts);
});
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
defaultFont: setDefaultFont,
defaultFontSize: setDefaultFontSize,
minimumFontSize: setMinFontSize,
overrideFont: setOverrideFont,
defaultCJKFont: setDefaultCJKFont,
serifFont: setSerifFont,
sansSerifFont: setSansSerifFont,
monospaceFont: setMonospaceFont,
fontWeight: setFontWeight,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setCJKFonts((prev) => {
const newFonts = genCJKFontsList(sysFonts);
@@ -8,19 +8,39 @@ import { useTranslation } from '@/hooks/useTranslation';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { getTranslators } from '@/services/translators';
import { TRANSLATED_LANGS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { initDayjs } from '@/utils/time';
import Select from '@/components/Select';
const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { token } = useAuth();
const { envConfig } = useEnv();
const { getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [uiLanguage, setUILanguage] = useState(viewSettings.uiLanguage!);
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
uiLanguage: setUILanguage,
translationEnabled: setTranslationEnabled,
translationProvider: setTranslationProvider,
translateTargetLang: setTranslateTargetLang,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getCurrentUILangOption = () => {
const uiLanguage = viewSettings.uiLanguage;
return {
@@ -42,8 +62,7 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const handleSelectUILang = (event: React.ChangeEvent<HTMLSelectElement>) => {
const option = event.target.value;
saveViewSettings(envConfig, bookKey, 'uiLanguage', option, false, false);
i18n.changeLanguage(option ? option : navigator.language);
setUILanguage(option);
};
const getTranslationProviderOptions = () => {
@@ -94,6 +113,15 @@ const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setViewSettings(bookKey, { ...viewSettings });
};
useEffect(() => {
if (uiLanguage === viewSettings.uiLanguage) return;
saveViewSettings(envConfig, bookKey, 'uiLanguage', uiLanguage, false, false);
const locale = uiLanguage ? uiLanguage : navigator.language;
i18n.changeLanguage(locale);
initDayjs(locale);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uiLanguage]);
useEffect(() => {
if (translationEnabled === viewSettings.translationEnabled) return;
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
@@ -1,28 +1,33 @@
import React, { useEffect, useState } from 'react';
import { MdOutlineAutoMode } from 'react-icons/md';
import { MdOutlineAutoMode, MdOutlineScreenRotation } from 'react-icons/md';
import { MdOutlineTextRotationNone, MdTextRotateVertical } from 'react-icons/md';
import { IoPhoneLandscapeOutline, IoPhonePortraitOutline } from 'react-icons/io5';
import { TbTextDirectionRtl } from 'react-icons/tb';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { isCJKEnv } from '@/utils/misc';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { lockScreenOrientation } from '@/utils/bridge';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import { MIGHT_BE_RTL_LANGS } from '@/services/constants';
import { MIGHT_BE_RTL_LANGS, RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
const { getBookData } = useBookDataStore();
const view = getView(bookKey);
const bookData = getBookData(bookKey)!;
const viewSettings = getViewSettings(bookKey)!;
const gridInsets = getGridInsets(bookKey)!;
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
@@ -31,10 +36,23 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [textIndent, setTextIndent] = useState(viewSettings.textIndent!);
const [fullJustification, setFullJustification] = useState(viewSettings.fullJustification!);
const [hyphenation, setHyphenation] = useState(viewSettings.hyphenation!);
const [marginPx, setMarginPx] = useState(viewSettings.marginPx!);
const [marginTopPx, setMarginTopPx] = useState(
viewSettings.marginPx || viewSettings.marginTopPx!,
);
const [marginBottomPx, setMarginBottomPx] = useState(viewSettings.marginBottomPx!);
const [marginLeftPx, setMarginLeftPx] = useState(viewSettings.marginLeftPx!);
const [marginRightPx, setMarginRightPx] = useState(viewSettings.marginRightPx!);
const [compactMarginTopPx, setCompactMarginTopPx] = useState(
viewSettings.compactMarginPx || viewSettings.compactMarginTopPx!,
);
const [compactMarginBottomPx, setCompactMarginBottomPx] = useState(
viewSettings.compactMarginBottomPx!,
);
const [gapPercent, setGapPercent] = useState(viewSettings.gapPercent!);
const [compactMarginPx, setCompactMarginPx] = useState(viewSettings.compactMarginPx!);
const [compactGapPercent, setCompactGapPercent] = useState(viewSettings.compactGapPercent!);
const [compactMarginLeftPx, setCompactMarginLeftPx] = useState(viewSettings.compactMarginLeftPx!);
const [compactMarginRightPx, setCompactMarginRightPx] = useState(
viewSettings.compactMarginRightPx!,
);
const [maxColumnCount, setMaxColumnCount] = useState(viewSettings.maxColumnCount!);
const [maxInlineSize, setMaxInlineSize] = useState(viewSettings.maxInlineSize!);
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!);
@@ -46,7 +64,49 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [showFooter, setShowFooter] = useState(viewSettings.showFooter!);
const [showBarsOnScroll, setShowBarsOnScroll] = useState(viewSettings.showBarsOnScroll!);
const [showRemainingTime, setShowRemainingTime] = useState(viewSettings.showRemainingTime!);
const [showRemainingPages, setShowRemainingPages] = useState(viewSettings.showRemainingPages!);
const [showPageNumber, setShowPageNumber] = useState(viewSettings.showPageNumber!);
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
paragraphMargin: setParagraphMargin,
lineHeight: setLineHeight,
wordSpacing: setWordSpacing,
letterSpacing: setLetterSpacing,
textIndent: setTextIndent,
fullJustification: setFullJustification,
hyphenation: setHyphenation,
marginTopPx: setMarginTopPx,
marginBottomPx: setMarginBottomPx,
marginLeftPx: setMarginLeftPx,
marginRightPx: setMarginRightPx,
compactMarginTopPx: setCompactMarginTopPx,
compactMarginBottomPx: setCompactMarginBottomPx,
compactMarginLeftPx: setCompactMarginLeftPx,
compactMarginRightPx: setCompactMarginRightPx,
gapPercent: setGapPercent,
maxColumnCount: setMaxColumnCount,
maxInlineSize: setMaxInlineSize,
maxBlockSize: setMaxBlockSize,
overrideLayout: setOverrideLayout,
doubleBorder: setDoubleBorder,
borderColor: setBorderColor,
showHeader: setShowHeader,
showFooter: setShowFooter,
showBarsOnScroll: setShowBarsOnScroll,
showRemainingTime: setShowRemainingTime,
showRemainingPages: setShowRemainingPages,
showPageNumber: setShowPageNumber,
});
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'paragraphMargin', paragraphMargin);
@@ -84,18 +144,90 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, [hyphenation]);
useEffect(() => {
if (marginPx === viewSettings.marginPx) return;
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx, false, false);
view?.renderer.setAttribute('margin', `${marginPx}px`);
if (marginTopPx === viewSettings.marginTopPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginTopPx', marginTopPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginPx]);
}, [marginTopPx]);
useEffect(() => {
if (compactMarginPx === viewSettings.compactMarginPx) return;
saveViewSettings(envConfig, bookKey, 'compactMarginPx', compactMarginPx, false, false);
view?.renderer.setAttribute('margin', `${compactMarginPx}px`);
if (marginBottomPx === viewSettings.marginBottomPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginBottomPx', marginBottomPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginPx]);
}, [marginBottomPx]);
useEffect(() => {
if (marginRightPx === viewSettings.marginRightPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginRightPx', marginRightPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginRightPx]);
useEffect(() => {
if (marginLeftPx === viewSettings.marginLeftPx) return;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginLeftPx', marginLeftPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginLeftPx]);
useEffect(() => {
if (compactMarginTopPx === viewSettings.compactMarginTopPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'compactMarginTopPx', compactMarginTopPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginTopPx]);
useEffect(() => {
if (compactMarginBottomPx === viewSettings.compactMarginBottomPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(
envConfig,
bookKey,
'compactMarginBottomPx',
compactMarginBottomPx,
false,
false,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginBottomPx]);
useEffect(() => {
if (compactMarginRightPx === viewSettings.compactMarginRightPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(
envConfig,
bookKey,
'compactMarginRightPx',
compactMarginRightPx,
false,
false,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginRightPx]);
useEffect(() => {
if (compactMarginLeftPx === viewSettings.compactMarginLeftPx) return;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'compactMarginLeftPx', compactMarginLeftPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginLeftPx]);
useEffect(() => {
if (gapPercent === viewSettings.gapPercent) return;
@@ -107,16 +239,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gapPercent]);
useEffect(() => {
if (compactGapPercent === viewSettings.compactGapPercent) return;
saveViewSettings(envConfig, bookKey, 'compactGapPercent', compactGapPercent, false, false);
view?.renderer.setAttribute('gap', `${compactGapPercent}%`);
if (viewSettings.scrolled) {
view?.renderer.setAttribute('flow', 'scrolled');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactGapPercent]);
useEffect(() => {
if (maxColumnCount === viewSettings.maxColumnCount) return;
saveViewSettings(envConfig, bookKey, 'maxColumnCount', maxColumnCount, false, false);
@@ -158,7 +280,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
(['horizontal-rl', 'vertical-rl'].includes(writingMode) ||
['horizontal-rl', 'vertical-rl'].includes(prevWritingMode))
) {
setTimeout(() => window.location.reload(), 100);
setTimeout(() => window.location.reload(), RELOAD_BEFREE_SAVED_TIMEOUT_MS);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [writingMode]);
@@ -171,14 +293,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (doubleBorder === viewSettings.doubleBorder) return;
if (doubleBorder && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setGapPercent(viewSettings.gapPercent);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'doubleBorder', doubleBorder, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [doubleBorder]);
@@ -198,64 +312,53 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showRemainingTime]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showRemainingPages', showRemainingPages, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showRemainingPages]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showPageNumber', showPageNumber, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showPageNumber]);
const applyMarginAndGap = () => {
const isCompact = !showHeader && !showFooter;
const marginPx = isCompact ? viewSettings.compactMarginPx : viewSettings.marginPx;
const gapPercent = isCompact ? viewSettings.compactGapPercent : viewSettings.gapPercent;
view?.renderer.setAttribute('margin', `${marginPx}px`);
view?.renderer.setAttribute('gap', `${gapPercent}%`);
if (viewSettings.scrolled) {
view?.renderer.setAttribute('flow', 'scrolled');
}
};
useEffect(() => {
if (showHeader === viewSettings.showHeader) return;
if (showHeader && !viewSettings.vertical) {
viewSettings.marginPx = Math.max(viewSettings.marginPx, 44);
setMarginPx(viewSettings.marginPx);
setViewSettings(bookKey, viewSettings);
} else if (showHeader && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setGapPercent(viewSettings.gapPercent);
const minMarginTop = Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4);
viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, minMarginTop);
setMarginTopPx(viewSettings.marginTopPx);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'showHeader', showHeader, false, false);
applyMarginAndGap();
// Margin and gap settings will be applied in FoliateViewer
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showHeader]);
useEffect(() => {
if (showFooter === viewSettings.showFooter) return;
if (showFooter && !viewSettings.vertical) {
viewSettings.marginPx = Math.max(viewSettings.marginPx, 44);
setMarginPx(viewSettings.marginPx);
setViewSettings(bookKey, viewSettings);
} else if (showFooter && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setGapPercent(viewSettings.gapPercent);
const minMarginBottom = Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4);
viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, minMarginBottom);
setMarginBottomPx(viewSettings.marginBottomPx);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'showFooter', showFooter, false, false);
applyMarginAndGap();
// Margin and gap settings will be applied in FoliateViewer
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showFooter]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'screenOrientation', screenOrientation, false, false);
if (appService?.isMobileApp) {
lockScreenOrientation({ orientation: screenOrientation });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenOrientation]);
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
const mightBeRTLBook = MIGHT_BE_RTL_LANGS.includes(langCode) || isCJKEnv();
const isVertical = viewSettings.vertical || writingMode.includes('vertical');
return (
<div className='my-4 w-full space-y-6'>
@@ -418,22 +521,50 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='card bg-base-100 border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<NumberInput
label={_('Vertical Margins (px)')}
value={showFooter || showHeader ? marginPx : compactMarginPx}
onChange={showFooter || showHeader ? setMarginPx : setCompactMarginPx}
min={!viewSettings.vertical && (showFooter || showHeader) ? 44 : 0}
label={_('Top Margin (px)')}
value={showHeader && !isVertical ? marginTopPx : compactMarginTopPx}
onChange={showHeader && !isVertical ? setMarginTopPx : setCompactMarginTopPx}
min={
showHeader && !isVertical
? Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4)
: 0
}
max={88}
step={4}
/>
<NumberInput
label={_('Horizontal Margins (%)')}
value={showFooter || showHeader ? gapPercent : compactGapPercent}
onChange={showFooter || showHeader ? setGapPercent : setCompactGapPercent}
label={_('Bottom Margin (px)')}
value={showFooter && !isVertical ? marginBottomPx : compactMarginBottomPx}
onChange={showFooter && !isVertical ? setMarginBottomPx : setCompactMarginBottomPx}
min={
viewSettings.vertical && (showFooter || showHeader)
? Math.ceil(4800 / window.innerWidth)
showFooter && !isVertical
? Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4)
: 0
}
max={88}
step={4}
/>
<NumberInput
label={_('Left Margin (px)')}
value={showFooter && isVertical ? marginLeftPx : compactMarginLeftPx}
onChange={showFooter && isVertical ? setMarginLeftPx : setCompactMarginLeftPx}
min={0}
max={88}
step={4}
/>
<NumberInput
label={_('Right Margin (px)')}
value={showHeader && isVertical ? marginRightPx : compactMarginRightPx}
onChange={showHeader && isVertical ? setMarginRightPx : setCompactMarginRightPx}
min={0}
max={88}
step={4}
/>
<NumberInput
label={_('Column Gap (%)')}
value={gapPercent}
onChange={setGapPercent}
min={0}
max={30}
/>
<NumberInput
@@ -494,7 +625,31 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
className='toggle'
checked={showRemainingTime}
disabled={!showFooter}
onChange={() => setShowRemainingTime(!showRemainingTime)}
onChange={() => {
if (!showRemainingTime) {
setShowRemainingTime(true);
setShowRemainingPages(false);
} else {
setShowRemainingTime(false);
}
}}
/>
</div>
<div className='config-item'>
<span className=''>{_('Show Remaining Pages')}</span>
<input
type='checkbox'
className='toggle'
checked={showRemainingPages}
disabled={!showFooter}
onChange={() => {
if (!showRemainingPages) {
setShowRemainingPages(true);
setShowRemainingTime(false);
} else {
setShowRemainingPages(false);
}
}}
/>
</div>
<div className='config-item'>
@@ -519,6 +674,47 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</div>
</div>
</div>
{appService?.isMobileApp && (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Orientation')}</span>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('auto')}
>
<MdOutlineScreenRotation />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Portrait')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'portrait' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('portrait')}
>
<IoPhonePortraitOutline />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Landscape')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'landscape' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('landscape')}
>
<IoPhoneLandscapeOutline />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
@@ -1,70 +1,116 @@
import clsx from 'clsx';
import cssbeautify from 'cssbeautify';
import React, { useEffect, useRef, useState } from 'react';
import { IoPhoneLandscapeOutline, IoPhonePortraitOutline } from 'react-icons/io5';
import { MdOutlineScreenRotation } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResetViewSettings } from '../../hooks/useResetSettings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { getStyles } from '@/utils/style';
import { lockScreenOrientation } from '@/utils/bridge';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import cssbeautify from 'cssbeautify';
import cssValidate from '@/utils/css';
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
type CSSType = 'book' | 'reader';
const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!);
const [draftContentStylesheet, setDraftContentStylesheet] = useState(viewSettings.userStylesheet);
const [draftContentStylesheetSaved, setDraftContentStylesheetSaved] = useState(true);
const [contentError, setContentError] = useState<string | null>(null);
const [draftUIStylesheet, setDraftUIStylesheet] = useState(viewSettings.userUIStylesheet);
const [draftUIStylesheetSaved, setDraftUIStylesheetSaved] = useState(true);
const [uiError, setUIError] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [inputFocusInAndroid, setInputFocusInAndroid] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const contentTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const uiTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const handleUserStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const cssInput = e.target.value;
setDraftStylesheet(cssInput);
setDraftStylesheetSaved(false);
const resetToDefaults = useResetViewSettings();
const handleReset = () => {
resetToDefaults({
userStylesheet: setDraftContentStylesheet,
userUIStylesheet: setDraftUIStylesheet,
});
applyStyles('book', true);
applyStyles('reader', true);
};
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const validateCSS = (cssInput: string): { isValid: boolean; error?: string } => {
if (!cssInput.trim()) return { isValid: true };
try {
const { isValid, error } = cssValidate(cssInput);
if (cssInput && !isValid) {
throw new Error(error || 'Invalid CSS');
if (!isValid) {
return { isValid: false, error: error || 'Invalid CSS' };
}
setError(null);
return { isValid: true };
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Invalid CSS: Please check your input.');
return { isValid: false, error: err.message };
}
console.log('CSS Error:', err);
return { isValid: false, error: 'Invalid CSS: Please check your input.' };
}
};
const applyStyles = () => {
const formattedCSS = cssbeautify(draftStylesheet, {
const handleStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>, type: CSSType) => {
const cssInput = e.target.value;
if (type === 'book') {
setDraftContentStylesheet(cssInput);
setDraftContentStylesheetSaved(false);
const { isValid, error } = validateCSS(cssInput);
setContentError(isValid ? null : error || 'Invalid CSS');
} else {
setDraftUIStylesheet(cssInput);
setDraftUIStylesheetSaved(false);
const { isValid, error } = validateCSS(cssInput);
setUIError(isValid ? null : error || 'Invalid CSS');
}
};
const applyStyles = (type: CSSType, clear = false) => {
const cssInput = type === 'book' ? draftContentStylesheet : draftUIStylesheet;
const formattedCSS = cssbeautify(clear ? '' : cssInput, {
indent: ' ',
openbrace: 'end-of-line',
autosemicolon: true,
});
setDraftStylesheet(formattedCSS);
setDraftStylesheetSaved(true);
viewSettings.userStylesheet = formattedCSS;
setViewSettings(bookKey, { ...viewSettings });
if (type === 'book') {
setDraftContentStylesheet(formattedCSS);
setDraftContentStylesheetSaved(true);
viewSettings.userStylesheet = formattedCSS;
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userStylesheet = formattedCSS;
setSettings(settings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userStylesheet = formattedCSS;
setSettings(settings);
}
} else {
setDraftUIStylesheet(formattedCSS);
setDraftUIStylesheetSaved(true);
viewSettings.userUIStylesheet = formattedCSS;
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userUIStylesheet = formattedCSS;
setSettings(settings);
}
}
setViewSettings(bookKey, { ...viewSettings });
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings));
};
@@ -73,7 +119,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
e.nativeEvent.stopImmediatePropagation();
};
const handleInputFocus = () => {
const handleInputFocus = (textareaRef: React.RefObject<HTMLTextAreaElement>) => {
if (appService?.isAndroidApp) {
setInputFocusInAndroid(true);
}
@@ -93,13 +139,53 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
};
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'screenOrientation', screenOrientation, false, false);
if (appService?.isMobileApp) {
lockScreenOrientation({ orientation: screenOrientation });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenOrientation]);
const renderCSSEditor = (
type: CSSType,
title: string,
placeholder: string,
value: string,
error: string | null,
saved: boolean,
textareaRef: React.RefObject<HTMLTextAreaElement>,
) => (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_(title)}</h2>
<div
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
>
<div className='relative p-1'>
<textarea
ref={textareaRef}
className={clsx(
'textarea textarea-ghost h-48 w-full border-0 p-3 text-base !outline-none sm:text-sm',
'placeholder:text-base-content/70',
)}
placeholder={_(placeholder)}
spellCheck='false'
value={value}
onFocus={() => handleInputFocus(textareaRef)}
onBlur={handleInputBlur}
onInput={handleInput}
onKeyDown={handleInput}
onKeyUp={handleInput}
onChange={(e) => handleStylesheetChange(e, type)}
/>
<button
className={clsx(
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
saved ? 'hidden' : '',
error ? 'btn-disabled' : '',
)}
onClick={() => applyStyles(type)}
disabled={!!error}
>
{_('Apply')}
</button>
</div>
</div>
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
</div>
);
return (
<div
@@ -108,84 +194,25 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
inputFocusInAndroid && 'h-[50%] overflow-y-auto pb-[200px]',
)}
>
{appService?.isMobileApp && (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Orientation')}</span>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('auto')}
>
<MdOutlineScreenRotation />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Portrait')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'portrait' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('portrait')}
>
<IoPhonePortraitOutline />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Landscape')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${screenOrientation === 'landscape' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setScreenOrientation('landscape')}
>
<IoPhoneLandscapeOutline />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{renderCSSEditor(
'book',
_('Custom Content CSS'),
_('Enter CSS for book content styling...'),
draftContentStylesheet,
contentError,
draftContentStylesheetSaved,
contentTextareaRef,
)}
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Custom CSS')}</h2>
<div
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
>
<div className='relative p-1'>
<textarea
ref={textareaRef}
className={clsx(
'textarea textarea-ghost h-48 w-full border-0 p-3 text-base !outline-none sm:text-sm',
'placeholder:text-base-content/70',
)}
placeholder={_('Enter your custom CSS here...')}
spellCheck='false'
value={draftStylesheet}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onInput={handleInput}
onKeyDown={handleInput}
onKeyUp={handleInput}
onChange={handleUserStylesheetChange}
/>
<button
className={clsx(
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
draftStylesheetSaved ? 'hidden' : '',
error ? 'btn-disabled' : '',
)}
onClick={applyStyles}
disabled={!!error}
>
{_('Apply')}
</button>
</div>
</div>
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
</div>
{renderCSSEditor(
'reader',
_('Custom Reader UI CSS'),
_('Enter CSS for reader interface styling...'),
draftUIStylesheet,
uiError,
draftUIStylesheetSaved,
uiTextareaRef,
)}
</div>
);
};
@@ -67,7 +67,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
return (
<div className={clsx('config-item', className)}>
<span className='text-base-content'>{label}</span>
<span className='text-base-content line-clamp-2'>{label}</span>
<div className='text-base-content flex items-center gap-2'>
<input
type='text'
@@ -75,7 +75,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
value={localValue}
onChange={handleChange}
onBlur={handleOnBlur}
className='input input-ghost settings-content text-base-content w-20 max-w-xs rounded border-0 bg-transparent px-3 py-1 text-right !outline-none'
className='input input-ghost settings-content text-base-content w-16 max-w-xs rounded border-0 bg-transparent py-1 pe-3 ps-1 text-right !outline-none'
onFocus={(e) => e.target.select()}
/>
<button
@@ -22,7 +22,11 @@ import ControlPanel from './ControlPanel';
import LangPanel from './LangPanel';
import MiscPanel from './MiscPanel';
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
export type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
export type SettingsPanelPanelProp = {
bookKey: string;
onRegisterReset: (resetFn: () => void) => void;
};
type TabConfig = {
tab: SettingsPanelType;
@@ -84,6 +88,28 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
localStorage.setItem('lastConfigPanel', tab);
};
const [resetFunctions, setResetFunctions] = useState<
Record<SettingsPanelType, (() => void) | null>
>({
Font: null,
Layout: null,
Color: null,
Control: null,
Language: null,
Custom: null,
});
const registerResetFunction = (panel: SettingsPanelType, resetFn: () => void) => {
setResetFunctions((prev) => ({ ...prev, [panel]: resetFn }));
};
const handleResetCurrentPanel = () => {
const resetFn = resetFunctions[activePanel];
if (resetFn) {
resetFn();
}
};
const handleClose = () => {
setFontLayoutSettingsDialogOpen(false);
};
@@ -130,6 +156,8 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
};
}, []);
const currentPanel = tabConfig.find((tab) => tab.tab === activePanel);
return (
<Dialog
isOpen={true}
@@ -140,7 +168,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
header={
<div className='flex w-full flex-col items-center'>
<div className='tab-title flex pb-2 text-base font-semibold sm:hidden'>
{tabConfig.find((tab) => tab.tab === activePanel)?.label || ''}
{currentPanel?.label || ''}
</div>
<div className='flex w-full flex-row items-center justify-between'>
<button
@@ -184,7 +212,15 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 flex items-center justify-center'
toggleButton={<PiDotsThreeVerticalBold />}
>
<DialogMenu />
<DialogMenu
activePanel={activePanel}
onReset={handleResetCurrentPanel}
resetLabel={
currentPanel
? _('Reset {{settings}}', { settings: currentPanel.label })
: undefined
}
/>
</Dropdown>
<button
onClick={handleClose}
@@ -209,12 +245,39 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
</div>
}
>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Control' && <ControlPanel bookKey={bookKey} />}
{activePanel === 'Language' && <LangPanel bookKey={bookKey} />}
{activePanel === 'Custom' && <MiscPanel bookKey={bookKey} />}
{activePanel === 'Font' && (
<FontPanel bookKey={bookKey} onRegisterReset={(fn) => registerResetFunction('Font', fn)} />
)}
{activePanel === 'Layout' && (
<LayoutPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Layout', fn)}
/>
)}
{activePanel === 'Color' && (
<ColorPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Color', fn)}
/>
)}
{activePanel === 'Control' && (
<ControlPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Control', fn)}
/>
)}
{activePanel === 'Language' && (
<LangPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Language', fn)}
/>
)}
{activePanel === 'Custom' && (
<MiscPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Custom', fn)}
/>
)}
</Dialog>
);
};
@@ -1,4 +1,5 @@
import clsx from 'clsx';
import dayjs from 'dayjs';
import React from 'react';
import { marked } from 'marked';
@@ -103,7 +104,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<div className={clsx('content font-size-sm line-clamp-3', item.note && 'mt-2')}>
<span
className={clsx(
'inline',
'inline leading-normal',
item.note && 'content font-size-xs text-gray-500',
(item.style === 'underline' || item.style === 'squiggly') &&
'underline decoration-2',
@@ -130,47 +131,54 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
}
onClick={(e) => e.stopPropagation()}
>
<div className='flex justify-end space-x-3 p-2' dir='ltr'>
{item.note && (
<button
<div className='flex cursor-default items-center justify-between'>
<div className='flex items-center'>
<span className='text-sm text-gray-500 sm:text-xs'>
{dayjs(item.createdAt).fromNow()}
</span>
</div>
<div className='flex items-center justify-end space-x-3 p-2' dir='ltr'>
{item.note && (
<div
className={clsx(
'content settings-content cursor-pointer',
'flex items-end p-0 hover:bg-transparent',
)}
onClick={editNote.bind(null, item)}
>
<div
className={clsx(
'align-bottom text-blue-500',
'transition duration-300 ease-in-out',
'content font-size-sm',
'opacity-0 group-hover:opacity-100',
'hover:text-blue-600',
)}
>
{_('Edit')}
</div>
</div>
)}
<div
className={clsx(
'btn btn-ghost content settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
'content settings-content cursor-pointer',
'flex items-end p-0 hover:bg-transparent',
)}
onClick={editNote.bind(null, item)}
onClick={deleteNote.bind(null, item)}
>
<div
className={clsx(
'align-bottom text-blue-500',
'align-bottom text-red-500',
'transition duration-300 ease-in-out',
'content font-size-sm',
'opacity-0 group-hover:opacity-100',
'hover:text-blue-600',
'hover:text-red-600',
)}
>
{_('Edit')}
{_('Delete')}
</div>
</button>
)}
<button
className={clsx(
'btn btn-ghost content settings-content hover:bg-transparent',
'flex h-4 min-h-4 items-end p-0',
)}
onClick={deleteNote.bind(null, item)}
>
<div
className={clsx(
'align-bottom text-red-500',
'transition duration-300 ease-in-out',
'content font-size-sm',
'opacity-0 group-hover:opacity-100',
'hover:text-red-600',
)}
>
{_('Delete')}
</div>
</button>
</div>
</div>
</div>
</li>
@@ -177,6 +177,7 @@ const SideBar: React.FC<{
className={clsx(
'sidebar-container bg-base-200 z-20 flex min-w-60 select-none flex-col',
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
'transition-[padding-top] duration-300',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
!isSideBarPinned && 'shadow-2xl',
@@ -248,8 +249,9 @@ const SideBar: React.FC<{
<SidebarContent bookDoc={bookDoc} sideBarBookKey={sideBarBookKey!} />
)}
<div
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
className='drag-bar absolute right-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3'
onMouseDown={handleHorizontalDragStart}
onTouchStart={handleHorizontalDragStart}
></div>
</div>
{!isSideBarPinned && (
@@ -20,13 +20,16 @@ const POPUP_WIDTH = 282;
const POPUP_HEIGHT = 160;
const POPUP_PADDING = 10;
const TTSControl = () => {
interface TTSControlProps {
bookKey: string;
}
const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getProgress, getViewSettings } = useReaderStore();
const { setViewSettings, setTTSEnabled } = useReaderStore();
const [bookKey, setBookKey] = useState<string>('');
const [ttsLang, setTtsLang] = useState<string>('en');
const [isPlaying, setIsPlaying] = useState(false);
const [isPaused, setIsPaused] = useState(false);
@@ -152,7 +155,9 @@ const TTSControl = () => {
}, [ttsController, bookKey]);
const handleTTSSpeak = async (event: CustomEvent) => {
const { bookKey, range } = event.detail;
const { bookKey: ttsBookKey, range } = event.detail;
if (bookKey !== ttsBookKey) return;
const view = getView(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
@@ -182,7 +187,6 @@ const TTSControl = () => {
}
const primaryLang = bookData.book.primaryLanguage;
setBookKey(bookKey);
if (ttsControllerRef.current) {
ttsControllerRef.current.stop();
@@ -230,8 +234,8 @@ const TTSControl = () => {
};
const handleTTSStop = async (event: CustomEvent) => {
const { bookKey } = event.detail;
if (ttsControllerRef.current) {
const { bookKey: ttsBookKey } = event.detail;
if (ttsControllerRef.current && bookKey === ttsBookKey) {
handleStop(bookKey);
}
};
@@ -274,7 +278,7 @@ const TTSControl = () => {
const handleStop = async (bookKey: string) => {
const ttsController = ttsControllerRef.current;
if (ttsController) {
await ttsController.stop();
await ttsController.shutdown();
ttsControllerRef.current = null;
setTtsController(null);
getView(bookKey)?.deselect();
@@ -1,8 +1,6 @@
import { useReaderStore } from '@/store/readerStore';
import { useNotebookStore } from '@/store/notebookStore';
import { isTauriAppPlatform } from '@/services/environment';
import useShortcuts from '@/hooks/useShortcuts';
import useBooksManager from './useBooksManager';
import { useSidebarStore } from '@/store/sidebarStore';
import { useSettingsStore } from '@/store/settingsStore';
import { getStyles } from '@/utils/style';
@@ -10,6 +8,8 @@ import { tauriHandleToggleFullScreen, tauriQuitApp } from '@/utils/window';
import { eventDispatcher } from '@/utils/event';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
import { viewPagination } from './usePagination';
import useShortcuts from '@/hooks/useShortcuts';
import useBooksManager from './useBooksManager';
interface UseBookShortcutsProps {
sideBarBookKey: string | null;
@@ -17,7 +17,7 @@ interface UseBookShortcutsProps {
}
const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) => {
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { getView, getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { toggleSideBar, setSideBarBookKey } = useSidebarStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { toggleNotebook } = useNotebookStore();
@@ -136,6 +136,13 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
const toggleTTS = () => {
if (!sideBarBookKey) return;
const bookKey = sideBarBookKey;
const viewState = getViewState(bookKey);
eventDispatcher.dispatch(viewState?.ttsEnabled ? 'tts-stop' : 'tts-speak', { bookKey });
};
useShortcuts(
{
onSwitchSideBar: switchSideBar,
@@ -144,8 +151,9 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
onToggleScrollMode: toggleScrollMode,
onOpenFontLayoutSettings: () => setFontLayoutSettingsDialogOpen(true),
onToggleSearchBar: showSearchBar,
onReloadPage: reloadPage,
onToggleFullscreen: toggleFullscreen,
onToggleTTS: toggleTTS,
onReloadPage: reloadPage,
onQuitApp: quitApp,
onGoLeft: goLeft,
onGoRight: goRight,
@@ -0,0 +1,28 @@
import { Dispatch, SetStateAction } from 'react';
import { useEnv } from '@/context/EnvContext';
import { ViewSettings } from '@/types/book';
type SetterKey = keyof ViewSettings;
type SetterValue = SetStateAction<string> & SetStateAction<number> & SetStateAction<boolean>;
type StateSetters = Partial<{
[Key in SetterKey]: Dispatch<SetterValue>;
}>;
export const useResetViewSettings = () => {
const { appService } = useEnv();
const resetToDefaults = (setters: StateSetters) => {
if (!appService) return;
const defaultSettings = appService.getDefaultViewSettings();
Object.entries(setters).forEach(([settingKey, setter]) => {
const freshValue = defaultSettings[settingKey as SetterKey];
if (freshValue !== undefined) {
setter(freshValue as SetterValue);
}
});
};
return resetToDefaults;
};
@@ -14,7 +14,7 @@ export const useTextSelector = (
) => {
const { appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getViewState, getViewSettings } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const bookData = getBookData(bookKey)!;
const primaryLang = bookData.book?.primaryLanguage || 'en';
@@ -94,9 +94,6 @@ export const useTextSelector = (
const handlePointerup = (doc: Document, index: number) => {
// Available on iOS and Desktop, fired at touchend or mouseup
// Note that on Android, pointerup event is fired after an additional touch event
const viewState = getViewState(bookKey);
if (viewState?.ttsEnabled) return;
const sel = doc.getSelection() as Selection;
if (isValidSelection(sel)) {
if (osPlatform === 'ios') {
@@ -116,6 +113,7 @@ export const useTextSelector = (
// Prevent the container from scrolling when text is selected in paginated mode
// FIXME: this is a workaround for issue #873
// TODO: support text selection across pages
handleDismissPopup();
const viewSettings = getViewSettings(bookKey);
if (
appService?.isAndroidApp &&
@@ -1,15 +1,17 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { FoliateView } from '@/types/view';
import { UseTranslatorOptions } from '@/services/translators';
import { useReaderStore } from '@/store/readerStore';
import { useTranslator } from '@/hooks/useTranslator';
import { walkTextNodes } from '@/utils/walk';
import { localeToLang } from '@/utils/lang';
import { debounce } from '@/utils/debounce';
import { getLocale } from '@/utils/misc';
export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElement | null) {
const { getViewSettings } = useReaderStore();
const { getViewSettings, getViewState, getProgress } = useReaderStore();
const viewSettings = getViewSettings(bookKey);
const viewState = getViewState(bookKey);
const progress = getProgress(bookKey);
const enabled = useRef(viewSettings?.translationEnabled);
const [provider, setProvider] = useState(viewSettings?.translationProvider);
@@ -17,7 +19,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
const { translate } = useTranslator({
provider,
targetLang: localeToLang(targetLang || getLocale()),
targetLang: targetLang || getLocale(),
} as UseTranslatorOptions);
const translateRef = useRef(translate);
@@ -118,7 +120,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
const translateElement = async (el: HTMLElement) => {
if (!enabled.current) return;
const text = el.textContent?.trim();
const text = el.textContent?.replaceAll('\n', '').trim();
if (!text) return;
if (el.querySelector('.translation-target')) {
@@ -134,7 +136,6 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
wrapper.className = `translation-target ${!enabled.current ? 'hidden' : ''}`;
wrapper.setAttribute('translation-element-mark', '1');
wrapper.setAttribute('lang', targetLang || getLocale());
wrapper.appendChild(document.createElement('br'));
const blockWrapper = document.createElement('font');
blockWrapper.className = 'translation-target translation-block-wrapper';
@@ -156,6 +157,61 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
}
};
const findNodeIndicesInRange = (range: Range, nodes: HTMLElement[]) => {
const startContainer = range.startContainer;
const endContainer = range.endContainer;
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]!;
if (node === startContainer || node.contains(startContainer)) {
if (startIndex === -1) startIndex = i;
}
if (node === endContainer || node.contains(endContainer)) {
endIndex = i;
}
}
if (startIndex !== -1 && endIndex === -1) {
endIndex = startIndex;
}
return { startIndex, endIndex };
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const translateInRange = useCallback(
debounce((range: Range) => {
const nodes = allTextNodes.current;
if (nodes.length === 0) {
console.warn('No text nodes available for translation.');
return;
}
const { startIndex, endIndex } = findNodeIndicesInRange(range, nodes);
if (startIndex === -1) {
console.log('Range not found in text nodes');
return;
}
const beforeStart = Math.max(0, startIndex - 2);
const afterEnd = Math.min(nodes.length - 1, endIndex + 2);
for (let i = beforeStart; i <= afterEnd; i++) {
const node = nodes[i];
if (node) {
translateElement(node);
}
}
}, 500),
[],
);
useEffect(() => {
if (viewState?.ttsEnabled && progress && document.hidden) {
const { range } = progress;
translateInRange(range);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewState?.ttsEnabled, progress]);
useEffect(() => {
if (!viewSettings) return;
+21 -6
View File
@@ -1,23 +1,38 @@
import clsx from 'clsx';
import Image from 'next/image';
import { Book } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { formatAuthors, formatTitle } from '@/utils/book';
interface BookCoverProps {
book: Book;
isPreview?: boolean;
mode?: LibraryViewModeType;
coverFit?: LibraryCoverFitType;
className?: string;
imageClassName?: string;
isPreview?: boolean;
}
const BookCover: React.FC<BookCoverProps> = ({ mode = 'grid', book, isPreview }) => {
const BookCover: React.FC<BookCoverProps> = ({
book,
mode = 'grid',
coverFit = 'crop',
className,
imageClassName,
isPreview,
}) => {
return (
<div className={clsx('relative flex h-full w-full')}>
<div className={clsx('relative flex h-full w-full', className)}>
<Image
src={book.coverImageUrl!}
alt={book.title}
fill={true}
className='object-cover'
className={clsx(
coverFit === 'crop'
? 'object-cover'
: `object-contain ${mode === 'grid' ? 'object-bottom' : ''}`,
imageClassName,
)}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
@@ -27,7 +42,7 @@ const BookCover: React.FC<BookCoverProps> = ({ mode = 'grid', book, isPreview })
className={clsx(
'invisible absolute inset-0 rounded-none p-2',
'text-neutral-content text-center font-serif font-medium',
isPreview && 'bg-base-200/50',
isPreview ? 'bg-base-200/50' : 'bg-base-100',
)}
>
<div className='flex h-1/2 items-center justify-center'>
+4 -6
View File
@@ -47,7 +47,7 @@ const Dialog: React.FC<DialogProps> = ({
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const dialogRef = useRef<HTMLDialogElement>(null);
const iconSize22 = useResponsiveSize(22);
const isMobile = window.innerWidth < 640;
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
const handleKeyDown = (event: KeyboardEvent | CustomEvent) => {
if (event instanceof CustomEvent) {
@@ -63,7 +63,7 @@ const Dialog: React.FC<DialogProps> = ({
useEffect(() => {
if (!isOpen) return;
setIsFullHeightInMobile(!snapHeight);
setIsFullHeightInMobile(!snapHeight && isMobile);
window.addEventListener('keydown', handleKeyDown);
if (appService?.isAndroidApp) {
acquireBackKeyInterception();
@@ -177,9 +177,7 @@ const Dialog: React.FC<DialogProps> = ({
appService?.hasSafeAreaInset && isFullHeightInMobile
? `max(env(safe-area-inset-top), ${systemUIVisible ? statusBarHeight : 0}px)`
: '0px',
...(appService?.isMobile
? { height: snapHeight ? `${snapHeight * 100}%` : '100%', bottom: 0 }
: {}),
...(isMobile ? { height: snapHeight ? `${snapHeight * 100}%` : '100%', bottom: 0 } : {}),
}}
>
<div
@@ -192,7 +190,7 @@ const Dialog: React.FC<DialogProps> = ({
>
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
</div>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-2 sm:px-4'>
{header ? (
header
) : (
+1 -1
View File
@@ -36,7 +36,7 @@ const Dropdown: React.FC<DropdownProps> = ({
: children;
return (
<div className='dropdown-container'>
<div className='dropdown-container flex'>
{isOpen && (
<div className='fixed inset-0 bg-transparent' onClick={() => setIsDropdownOpen(false)} />
)}
@@ -6,6 +6,7 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface MenuItemProps {
label: string;
description?: string;
tooltip?: string;
buttonClass?: string;
labelClass?: string;
shortcut?: string;
@@ -19,6 +20,7 @@ interface MenuItemProps {
const MenuItem: React.FC<MenuItemProps> = ({
label,
description,
tooltip,
buttonClass,
labelClass,
shortcut,
@@ -36,6 +38,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
disabled && 'btn-disabled text-gray-400',
buttonClass,
)}
data-tip={tooltip ? tooltip : ''}
onClick={onClick}
disabled={disabled}
>
@@ -274,7 +274,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
<div className='text-base-content flex-grow text-sm'>
<h2 className='mb-4 text-center font-bold sm:text-start'>
{_('A new version of Readest is Available!')}
{_('A new version of Readest is available!')}
</h2>
<p className='mb-2'>
{_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
+11 -7
View File
@@ -1,4 +1,5 @@
'use client';
import posthog from 'posthog-js';
import { ReactNode } from 'react';
import { PostHogProvider } from 'posthog-js/react';
@@ -9,13 +10,16 @@ const shouldDisablePostHog = () => {
return localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === 'true';
};
if (
typeof window !== 'undefined' &&
process.env['NODE_ENV'] === 'production' &&
process.env['NEXT_PUBLIC_POSTHOG_KEY']
) {
posthog.init(process.env['NEXT_PUBLIC_POSTHOG_KEY'], {
api_host: process.env['NEXT_PUBLIC_POSTHOG_HOST'],
const posthogUrl =
process.env['NEXT_PUBLIC_POSTHOG_HOST'] ||
atob(process.env['NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64']!);
const posthogKey =
process.env['NEXT_PUBLIC_POSTHOG_KEY'] ||
atob(process.env['NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64']!);
if (typeof window !== 'undefined' && process.env['NODE_ENV'] === 'production' && posthogKey) {
posthog.init(posthogKey, {
api_host: posthogUrl,
person_profiles: 'always',
});
if (shouldDisablePostHog()) {
+6 -27
View File
@@ -1,36 +1,11 @@
export interface ShortcutConfig {
onSwitchSideBar: string[];
onToggleSideBar: string[];
onToggleNotebook: string[];
onToggleSearchBar: string[];
onToggleScrollMode: string[];
onToggleSelectMode: string[];
onOpenFontLayoutSettings: string[];
onReloadPage: string[];
onToggleFullscreen: string[];
onQuitApp: string[];
onGoLeft: string[];
onGoRight: string[];
onGoNext: string[];
onGoPrev: string[];
onGoHalfPageDown: string[];
onGoHalfPageUp: string[];
onGoBack: string[];
onGoForward: string[];
onZoomIn: string[];
onZoomOut: string[];
onResetZoom: string[];
onSaveNote: string[];
onCloseNote: string[];
}
const DEFAULT_SHORTCUTS: ShortcutConfig = {
const DEFAULT_SHORTCUTS = {
onSwitchSideBar: ['ctrl+Tab', 'opt+Tab', 'alt+Tab'],
onToggleSideBar: ['s'],
onToggleNotebook: ['n'],
onToggleSearchBar: ['ctrl+f', 'cmd+f'],
onToggleScrollMode: ['shift+j'],
onToggleSelectMode: ['shift+s'],
onToggleTTS: ['t'],
onOpenFontLayoutSettings: ['shift+f'],
onReloadPage: ['shift+r'],
onToggleFullscreen: ['F11'],
@@ -50,6 +25,10 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
onCloseNote: ['Escape'],
};
export type ShortcutConfig = {
[K in keyof typeof DEFAULT_SHORTCUTS]: string[];
};
// Load shortcuts from localStorage or fallback to defaults
export const loadShortcuts = (): ShortcutConfig => {
if (typeof localStorage === 'undefined') return DEFAULT_SHORTCUTS;
@@ -0,0 +1,35 @@
import { useState, useEffect, useCallback } from 'react';
export const useSafeAreaInsets = () => {
const [updated, setUpdated] = useState(false);
const [insets, setInsets] = useState({
top: 0,
right: 0,
bottom: 0,
left: 0,
});
const updateSafeAreaInsets = useCallback(() => {
const rootStyles = getComputedStyle(document.documentElement);
const hasCustomProperties = rootStyles.getPropertyValue('--safe-area-inset-top');
if (hasCustomProperties) {
setInsets({
top: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-top')) || 0,
right: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-right')) || 0,
bottom: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-bottom')) || 0,
left: parseFloat(rootStyles.getPropertyValue('--safe-area-inset-left')) || 0,
});
}
setUpdated(true);
}, []);
useEffect(() => {
updateSafeAreaInsets();
window.addEventListener('resize', updateSafeAreaInsets);
return () => {
window.removeEventListener('resize', updateSafeAreaInsets);
};
}, [updateSafeAreaInsets]);
return updated ? insets : null;
};
+53 -10
View File
@@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { applyCustomTheme, Palette } from '@/styles/themes';
import { getStatusBarHeight, setSystemUIVisibility } from '@/utils/bridge';
import { getOSPlatform } from '@/utils/misc';
type UseThemeProps = {
systemUIVisible?: boolean;
@@ -23,18 +24,12 @@ export const useTheme = ({
dismissSystemUI,
updateAppTheme,
setStatusBarHeight,
systemUIAlwaysHidden,
setSystemUIAlwaysHidden,
} = useThemeStore();
useEffect(() => {
updateAppTheme(appThemeColor);
if (appService?.isMobileApp) {
if (systemUIVisible) {
showSystemUI();
} else {
dismissSystemUI();
}
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
}
if (appService?.isAndroidApp) {
getStatusBarHeight().then((res) => {
if (res.height && res.height > 0) {
@@ -43,7 +38,55 @@ export const useTheme = ({
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService, isDarkMode]);
}, []);
const handleSystemUIVisibility = useCallback(() => {
if (!appService?.isMobileApp) return;
const visible = systemUIVisible && !systemUIAlwaysHidden;
if (visible) {
showSystemUI();
} else {
dismissSystemUI();
}
setSystemUIVisibility({ visible, darkMode: isDarkMode });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService, isDarkMode, systemUIVisible]);
useEffect(() => {
if (appService?.isMobileApp) {
handleSystemUIVisibility();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSystemUIVisibility]);
useEffect(() => {
if (!appService?.isMobileApp) return;
handleSystemUIVisibility();
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
handleSystemUIVisibility();
}
};
const handleOrientationChange = () => {
if (appService?.isIOSApp && getOSPlatform() === 'ios') {
// FIXME: This is a workaround for iPhone apps where the system UI is not visible in landscape mode
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
// in the ViewController. Note that screen.orientation.type is not abailable in iOS before 16.4.
const systemUIAlwaysHidden = screen.orientation?.type.includes('landscape');
setSystemUIAlwaysHidden(systemUIAlwaysHidden);
handleSystemUIVisibility();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
screen.orientation?.addEventListener('change', handleOrientationChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
screen.orientation?.removeEventListener('change', handleOrientationChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSystemUIVisibility]);
useEffect(() => {
const customThemes = settings.globalReadSettings?.customThemes ?? [];
+1 -1
View File
@@ -14,7 +14,7 @@ export const useUICSS = (bookKey: string) => {
styleElement.remove();
}
const rawCSS = viewSettings.userStylesheet || '';
const rawCSS = viewSettings.userUIStylesheet || '';
const newStyleEl = document.createElement('style');
newStyleEl.textContent = rawCSS.replace('foliate-view', `#foliate-view-${bookKey}`);
document.head.appendChild(newStyleEl);
+20 -11
View File
@@ -1,6 +1,7 @@
import { Book, BookConfig, BookNote, BookDataRecord } from '@/types/book';
import { getAPIBaseUrl } from '@/services/environment';
import { getAccessToken } from '@/utils/access';
import { fetchWithTimeout } from '@/utils/fetch';
const SYNC_API_ENDPOINT = getAPIBaseUrl() + '/sync';
@@ -33,11 +34,15 @@ export class SyncClient {
if (!token) throw new Error('Not authenticated');
const url = `${SYNC_API_ENDPOINT}?since=${encodeURIComponent(since)}&type=${type ?? ''}&book=${book ?? ''}`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
const res = await fetchWithTimeout(
url,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
});
5000,
);
if (!res.ok) {
const error = await res.json();
@@ -55,14 +60,18 @@ export class SyncClient {
const token = await getAccessToken();
if (!token) throw new Error('Not authenticated');
const res = await fetch(SYNC_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
const res = await fetchWithTimeout(
SYNC_API_ENDPOINT,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
},
body: JSON.stringify(payload),
});
5000,
);
if (!res.ok) {
const error = await res.json();
+17 -21
View File
@@ -2,7 +2,7 @@ import { AppPlatform, AppService, OsPlatform } from '@/types/system';
import { SystemSettings } from '@/types/settings';
import { FileSystem, BaseDir } from '@/types/system';
import { Book, BookConfig, BookContent, BookFormat } from '@/types/book';
import { Book, BookConfig, BookContent, BookFormat, ViewSettings } from '@/types/book';
import {
getDir,
getLocalBookFilename,
@@ -74,6 +74,20 @@ export abstract class BaseAppService implements AppService {
abstract selectDirectory(): Promise<string>;
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
getDefaultViewSettings(): ViewSettings {
return {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
};
}
async loadSettings(): Promise<SystemSettings> {
let settings: SystemSettings;
const { fp, base } = this.resolvePath('settings.json', 'Settings');
@@ -90,15 +104,7 @@ export abstract class BaseAppService implements AppService {
settings = { ...DEFAULT_SYSTEM_SETTINGS, ...settings };
settings.globalReadSettings = { ...DEFAULT_READSETTINGS, ...settings.globalReadSettings };
settings.globalViewSettings = {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
...this.getDefaultViewSettings(),
...settings.globalViewSettings,
};
} catch {
@@ -110,17 +116,7 @@ export abstract class BaseAppService implements AppService {
...DEFAULT_READSETTINGS,
...(this.isMobile ? DEFAULT_MOBILE_READSETTINGS : {}),
},
globalViewSettings: {
...DEFAULT_BOOK_LAYOUT,
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
},
globalViewSettings: this.getDefaultViewSettings(),
} as SystemSettings;
await this.fs.createDir('', 'Books', true);
+18 -6
View File
@@ -37,6 +37,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
keepLogin: false,
autoUpload: true,
alwaysOnTop: false,
alwaysShowStatusBar: false,
autoCheckUpdates: true,
screenWakeLock: false,
openLastBooks: false,
@@ -46,6 +47,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
libraryViewMode: 'grid',
librarySortBy: 'updated',
librarySortAscending: false,
libraryCoverFit: 'crop',
lastSyncedAtBooks: 0,
lastSyncedAtConfigs: 0,
@@ -87,10 +89,15 @@ export const DEFAULT_BOOK_FONT: BookFont = {
};
export const DEFAULT_BOOK_LAYOUT: BookLayout = {
marginPx: 44,
marginTopPx: 44,
marginBottomPx: 44,
marginLeftPx: 16,
marginRightPx: 16,
compactMarginTopPx: 16,
compactMarginBottomPx: 16,
compactMarginLeftPx: 16,
compactMarginRightPx: 16,
gapPercent: 5,
compactMarginPx: 0,
compactGapPercent: 5,
scrolled: false,
disableClick: false,
swapClickArea: false,
@@ -124,12 +131,14 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
codeHighlighting: false,
codeLanguage: 'auto-detect',
userStylesheet: '',
userUIStylesheet: '',
};
export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
fullJustification: false,
animated: true,
defaultFont: 'Sans-serif',
marginBottomPx: 16,
};
export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
@@ -149,6 +158,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = {
showFooter: true,
showBarsOnScroll: false,
showRemainingTime: false,
showRemainingPages: false,
showPageNumber: true,
};
@@ -516,17 +526,19 @@ export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web'
export const READEST_WEB_BASE_URL = 'https://web.readest.com';
export const GITHUB_LATEST_DOWNLOAD = 'https://github.com/readest/readest/releases/latest/download';
const LATEST_DOWNLOAD_BASE_URL = 'https://download.readest.com/releases';
export const READEST_UPDATER_FILE = `${GITHUB_LATEST_DOWNLOAD}/latest.json`;
export const READEST_UPDATER_FILE = `${LATEST_DOWNLOAD_BASE_URL}/latest.json`;
export const READEST_CHANGELOG_FILE = `${GITHUB_LATEST_DOWNLOAD}/release-notes.json`;
export const READEST_CHANGELOG_FILE = `${LATEST_DOWNLOAD_BASE_URL}/release-notes.json`;
export const SYNC_PROGRESS_INTERVAL_SEC = 3;
export const SYNC_NOTES_INTERVAL_SEC = 5;
export const SYNC_BOOKS_INTERVAL_SEC = 5;
export const CHECK_UPDATE_INTERVAL_SEC = 24 * 60 * 60;
export const RELOAD_BEFREE_SAVED_TIMEOUT_MS = 300;
export const MAX_ZOOM_LEVEL = 500;
export const MIN_ZOOM_LEVEL = 50;
export const ZOOM_STEP = 10;
+2 -3
View File
@@ -11,14 +11,13 @@ export const isTauriAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM']
export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web';
export const hasCli = () => window.__READEST_CLI_ACCESS === true;
export const isPWA = () => window.matchMedia('(display-mode: standalone)').matches;
export const getBaseUrl = () => process.env['NEXT_PUBLIC_API_BASE_URL'] ?? READEST_WEB_BASE_URL;
// Dev API only in development mode and web platform
// with command `pnpm dev-web`
// for production build or tauri app use the production Web API
export const getAPIBaseUrl = () =>
process.env['NODE_ENV'] === 'development' && isWebAppPlatform()
? '/api'
: `${process.env['NEXT_PUBLIC_API_BASE_URL'] ?? READEST_WEB_BASE_URL}/api`;
process.env['NODE_ENV'] === 'development' && isWebAppPlatform() ? '/api' : `${getBaseUrl()}/api`;
export interface EnvConfigType {
getAppService: () => Promise<AppService>;
@@ -2,7 +2,7 @@ import { stubTranslation as _ } from '@/utils/misc';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { isTauriAppPlatform } from '@/services/environment';
import { TranslationProvider } from '../types';
import { langToDefaultLocale } from '@/utils/lang';
import { normalizeToFullLang } from '@/utils/lang';
interface TokenCache {
token: string;
@@ -53,8 +53,8 @@ export const azureProvider: TranslationProvider = {
if (!text.length) return [];
const results: string[] = [];
const msSourceLang = sourceLang ? langToDefaultLocale(sourceLang.toLowerCase()) : '';
const msTargetLang = langToDefaultLocale(targetLang.toLowerCase());
const msSourceLang = sourceLang ? normalizeToFullLang(sourceLang) : '';
const msTargetLang = normalizeToFullLang(targetLang);
const translationPromises = text.map(async (line, index) => {
if (!line?.trim().length) {
@@ -67,7 +67,7 @@ export const azureProvider: TranslationProvider = {
to: msTargetLang,
'api-version': '3.0',
});
if (msSourceLang && msSourceLang !== 'auto') {
if (msSourceLang && msSourceLang.toLowerCase() !== 'auto') {
params.append('from', msSourceLang);
}
@@ -3,6 +3,7 @@ import { stubTranslation as _ } from '@/utils/misc';
import { ErrorCodes, TranslationProvider } from '../types';
import { UserPlan } from '@/types/user';
import { getUserPlan } from '@/utils/access';
import { normalizeToShortLang } from '@/utils/lang';
import { DEFAULT_DAILY_TRANSLATION_QUOTA } from '@/services/constants';
import { saveDailyUsage } from '../utils';
@@ -38,8 +39,8 @@ export const deeplProvider: TranslationProvider = {
const body = JSON.stringify({
text: text,
source_lang: sourceLang.toUpperCase(),
target_lang: targetLang.toUpperCase(),
source_lang: normalizeToShortLang(sourceLang).toUpperCase(),
target_lang: normalizeToShortLang(targetLang).toUpperCase(),
use_cache: useCache,
});
@@ -1,6 +1,7 @@
import { stubTranslation as _ } from '@/utils/misc';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { isTauriAppPlatform } from '@/services/environment';
import { normalizeToShortLang } from '@/utils/lang';
import { TranslationProvider } from '../types';
export const googleProvider: TranslationProvider = {
@@ -20,8 +21,8 @@ export const googleProvider: TranslationProvider = {
const url = new URL('https://translate.googleapis.com/translate_a/single');
url.searchParams.append('client', 'gtx');
url.searchParams.append('dt', 't');
url.searchParams.append('sl', sourceLang.toLowerCase() || 'auto');
url.searchParams.append('tl', targetLang.toLowerCase());
url.searchParams.append('sl', normalizeToShortLang(sourceLang).toLowerCase() || 'auto');
url.searchParams.append('tl', normalizeToShortLang(targetLang).toLowerCase());
url.searchParams.append('q', line);
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;

Some files were not shown because too many files have changed in this diff Show More