forked from akai/readest
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f5c96fcc2 | |||
| 475bab24c5 | |||
| 369d894557 | |||
| 41657d4c1d | |||
| ac03d7f817 | |||
| 399f374dfa | |||
| fb4853a9d4 | |||
| c678d8fa44 | |||
| 4d5297fdb5 | |||
| 41bcf05127 | |||
| 533324e558 | |||
| c8bfb64ad3 | |||
| ee0d7d7ad0 | |||
| fc2cad3980 | |||
| 07116bdae8 | |||
| a7ba5db830 | |||
| 7fcaab8b9f | |||
| a853bc52d2 | |||
| e34978672e | |||
| 751bf3836c | |||
| 87069c7308 | |||
| ca2a62a921 | |||
| f53f5b7594 | |||
| 443809f5af | |||
| c009edca61 | |||
| 58c907c7cc | |||
| 5ca173818b | |||
| f64b5606f1 | |||
| 010568eeb1 | |||
| d24f737428 | |||
| 1635958ae3 | |||
| c8f0c09426 | |||
| 60a9eb5b57 | |||
| 46fdea3522 | |||
| ba82203629 | |||
| 8dbcf25331 | |||
| 7e9ddcd7cc | |||
| 211fc2cd22 | |||
| 3578d7eba8 | |||
| d220adbfdf | |||
| 752f4449b4 | |||
| 66f369b269 |
@@ -0,0 +1,3 @@
|
||||
[*.{ts,tsx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build Web Application on Pull Request
|
||||
name: PR checks
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
@@ -6,6 +6,28 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
rust_lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: "-C target-cpu=skylake"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
- name: Install minimal stable with clippy and rustfmt
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev
|
||||
- name: Format
|
||||
working-directory: apps/readest-app/src-tauri
|
||||
run: cargo fmt --check
|
||||
- name: Clippy Check
|
||||
working-directory: apps/readest-app/src-tauri
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
build_web_app:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -104,14 +104,12 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('apps/readest-app/src-tauri/Cargo.lock') }}
|
||||
workspaces: apps/readest-app/src-tauri -> target
|
||||
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
- name: create .env.local file for Next.js
|
||||
run: |
|
||||
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_DEEPL_API_KEY=${{ secrets.NEXT_PUBLIC_DEEPL_API_KEY }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
|
||||
@@ -161,7 +159,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exe_file="apps/readest-app/src-tauri/target/${{ matrix.config.rust_target }}/release/readest.exe"
|
||||
exe_file="target/${{ matrix.config.rust_target }}/release/readest.exe"
|
||||
# Browsers on Windows won't download zip files that contain exe files
|
||||
# so upload the exe files instead. This is totally stupid.
|
||||
# powershell.exe -Command "Compress-Archive -Path $exe_file -DestinationPath $bin_file -Force"
|
||||
@@ -182,10 +180,18 @@ jobs:
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
release_id: ${{ needs.get-release.outputs.release_id }}
|
||||
release_tag: ${{ needs.get-release.outputs.release_tag }}
|
||||
release_note: ${{ needs.get-release.outputs.release_note }}
|
||||
with:
|
||||
script: |
|
||||
const body = `## Release Highlight\n${process.env.release_note}`;
|
||||
const { data } = await github.rest.repos.generateReleaseNotes({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: process.env.release_tag,
|
||||
})
|
||||
const notes = process.env.release_note.split(/(?:\d\.\s)/).filter(Boolean);
|
||||
const formattedNotes = notes.map(note => `* ${note.trim()}`).join("\n");
|
||||
const body = `## Release Highlight\n${formattedNotes}\n\n${data.body}`;
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
|
||||
@@ -35,3 +35,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Rust build
|
||||
target
|
||||
|
||||
+5
-4
@@ -77,10 +77,11 @@ Now you're all setup and can start implementing your changes.
|
||||
|
||||
This project is a monorepo. The code for the `readest-app` is in the `app/readest-app` directory. Here are some useful scripts for developing the frontend only without compiling Tauri:
|
||||
|
||||
| Command | Description |
|
||||
| ---------------- | -------------------------------------------------- |
|
||||
| `pnpm dev-web` | Starts the development server for the web app only |
|
||||
| `pnpm build-web` | Builds the web app |
|
||||
| Command | Description |
|
||||
| ------------------------------------------ | -------------------------------------------------- |
|
||||
| `pnpm dev-web` | Starts the development server for the web app only |
|
||||
| `TAURI_DEV_HOST=192.168.1.22 pnpm dev-web` | Start the dev server on a LAN address |
|
||||
| `pnpm build-web` | Builds the web app |
|
||||
|
||||
Recommended Visual Studio Code plugins for development:
|
||||
|
||||
|
||||
+363
-434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["apps/readest-app/src-tauri"]
|
||||
resolver = "2"
|
||||
@@ -164,7 +164,18 @@ pnpm tauri ios dev
|
||||
pnpm tauri build
|
||||
```
|
||||
|
||||
### 6. More information
|
||||
### 6. Setup dev environment with Nix
|
||||
|
||||
If you have Nix installed, you can leverage flake to enter a development shell
|
||||
with all the necessary dependencies:
|
||||
|
||||
```bash
|
||||
nix develop ./ops # enter a dev shell for the web app
|
||||
nix develop ./ops#ios # enter a dev shell for the ios app
|
||||
nix develop ./ops#android # enter a dev shell for the android app
|
||||
```
|
||||
|
||||
### 7. More information
|
||||
|
||||
Please check the [wiki][link-gh-wiki] of this project for more information on development.
|
||||
|
||||
@@ -182,12 +193,20 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
|
||||
|
||||
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.
|
||||
|
||||
The following JavaScript libraries are bundled in this software:
|
||||
The following libraries and software are used in this software:
|
||||
|
||||
- [foliate-js](https://github.com/johnfactotum/foliate-js), which is MIT licensed.
|
||||
- [zip.js](https://github.com/gildas-lormeau/zip.js), which is licensed under the BSD-3-Clause license.
|
||||
- [fflate](https://github.com/101arrowz/fflate), which is MIT licensed.
|
||||
- [PDF.js](https://github.com/mozilla/pdf.js), which is licensed under Apache License 2.0.
|
||||
- [next.js](https://github.com/vercel/next.js), which is MIT licensed.
|
||||
- [react](https://github.com/facebook/react), which is MIT licensed.
|
||||
- [react-icons](https://github.com/react-icons/react-icons), which has various open-source licenses.
|
||||
- [tauri](https://github.com/tauri-apps/tauri), which is MIT licensed.
|
||||
|
||||
The following fonts are utilized in this software, either bundled within the application or provided through web fonts:
|
||||
|
||||
[Bitter](https://fonts.google.com/?query=Bitter), [Fira Code](https://fonts.google.com/?query=Fira+Code), [Literata](https://fonts.google.com/?query=Literata), [Merriweather](https://fonts.google.com/?query=Merriweather), [Noto Sans](https://fonts.google.com/?query=Noto+Sans), [Roboto](https://fonts.google.com/?query=Roboto), [LXGW WenKai](https://github.com/lxgw/LxgwWenKai), [MiSans](https://hyperos.mi.com/font/en/), [Source Han](https://github.com/adobe-fonts/source-han-sans/), [WenQuanYi Micro Hei](http://wenq.org/wqy2/)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
NEXT_PUBLIC_POSTHOG_KEY=YOUR_POSTHOG_KEY
|
||||
NEXT_PUBLIC_POSTHOG_HOST=YOUR_POSTHOG_HOST
|
||||
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
|
||||
DEEPL_PRO_API_KEY=YOUR_DEEPL_PRO_API_KEY
|
||||
DEEPL_FREE_API_KEY=YOUR_DEEPL_FREE_API_KEY
|
||||
@@ -49,3 +49,7 @@ src-tauri/gen
|
||||
|
||||
# vendor
|
||||
/public/vendor
|
||||
|
||||
# Auto Generated PWA files
|
||||
/public/sw.js
|
||||
/public/workbox-*.js
|
||||
|
||||
@@ -8,7 +8,25 @@ module.exports = {
|
||||
list: ['_'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
lngs: ['de', 'ja', 'es', 'fr', 'it', 'ko', 'pt', 'ru', 'tr', 'id', 'vi', 'zh-CN', 'zh-TW'],
|
||||
lngs: [
|
||||
'de',
|
||||
'ja',
|
||||
'es',
|
||||
'fr',
|
||||
'it',
|
||||
'el',
|
||||
'ko',
|
||||
'uk',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'tr',
|
||||
'hi',
|
||||
'id',
|
||||
'vi',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
ns: ['translation'],
|
||||
defaultNs: 'translation',
|
||||
defaultValue: '__STRING_NOT_TRANSLATED__',
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import withPWA from 'next-pwa';
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const internalHost = process.env.TAURI_DEV_HOST || 'localhost';
|
||||
|
||||
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Ensure Next.js uses SSG instead of SSR
|
||||
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
|
||||
output: process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web' ? undefined : 'export',
|
||||
output: appPlatform === 'web' ? undefined : 'export',
|
||||
// Note: This feature is required to use the Next.js Image component in SSG mode.
|
||||
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
|
||||
images: {
|
||||
@@ -19,14 +23,11 @@ const nextConfig = {
|
||||
// Configure assetPrefix or else the server won't properly resolve your assets.
|
||||
assetPrefix: isProd ? '' : `http://${internalHost}:3000`,
|
||||
reactStrictMode: true,
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/deepl/:path*',
|
||||
destination: 'https://api-free.deepl.com/v2/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
export default withPWA({
|
||||
dest: 'public',
|
||||
disable: appPlatform !== 'web',
|
||||
register: true,
|
||||
skipWaiting: true,
|
||||
})(nextConfig);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -60,6 +60,7 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-responsive": "^10.0.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
@@ -79,6 +80,7 @@
|
||||
"eslint-config-next": "15.0.3",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"next-pwa": "^5.6.0",
|
||||
"node-env-run": "^4.0.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-cli": "^11.0.0",
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Schriftschnitt",
|
||||
"Font Family": "Schriftfamilie",
|
||||
"Font Size": "Schriftgröße",
|
||||
"From Local File": "Aus lokaler Datei",
|
||||
"Full Justification": "Blocksatz",
|
||||
"Gaps (%)": "Abstände (%)",
|
||||
"Global Settings": "Globale Einstellungen",
|
||||
"Go Back": "Zurück",
|
||||
"Go Forward": "Vorwärts",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Angemeldet",
|
||||
"Logged in as {{userDisplayName}}": "Angemeldet als {{userDisplayName}}",
|
||||
"Margins (px)": "Ränder (px)",
|
||||
"Match Case": "Groß-/Kleinschreibung beachten",
|
||||
"Match Diacritics": "Akzente beachten",
|
||||
"Match Whole Words": "Ganze Wörter",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Öffnen",
|
||||
"Open Book": "Buch öffnen",
|
||||
"Original Text": "Originaltext",
|
||||
"Override Publisher Font": "Verleger-Schriftart überschreiben",
|
||||
"Page": "Seite",
|
||||
"Paging Animation": "Blätter-Animation",
|
||||
"Paragraph": "Absatz",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Schreibmodus",
|
||||
"Your Library": "Ihre Bibliothek"
|
||||
"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.",
|
||||
"Update Now!": "Jetzt aktualisieren!",
|
||||
"Update": "Aktualisieren",
|
||||
"Check update": "Aktualisierung prüfen",
|
||||
"Already the latest version": "Bereits die neueste Version",
|
||||
"Book Details": "Buchdetails",
|
||||
"Import books": "Bücher importieren",
|
||||
"From local file": "Aus lokaler Datei"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"(detected)": "(εντοπίστηκε)",
|
||||
"About Readest": "Σχετικά με το Readest",
|
||||
"Add your notes here...": "Προσθέστε τις σημειώσεις σας εδώ...",
|
||||
"Animation": "Κινούμενα σχέδια",
|
||||
"Annotate": "Σχολιασμός",
|
||||
"Apply": "Εφαρμογή",
|
||||
"Are you sure to delete the selected books?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα βιβλία;",
|
||||
"Auto Mode": "Αυτόματη λειτουργία",
|
||||
"Behavior": "Συμπεριφορά",
|
||||
"Book": "Βιβλίο",
|
||||
"Book Cover": "Εξώφυλλο βιβλίου",
|
||||
"Bookmark": "Σελιδοδείκτης",
|
||||
"Cancel": "Ακύρωση",
|
||||
"Chapter": "Κεφάλαιο",
|
||||
"Cherry": "Κερασί",
|
||||
"Color": "Χρώμα",
|
||||
"Confirm": "Επιβεβαίωση",
|
||||
"Confirm Deletion": "Επιβεβαίωση διαγραφής",
|
||||
"Copied to notebook": "Αντιγράφηκε στο σημειωματάριο",
|
||||
"Copy": "Αντιγραφή",
|
||||
"Custom CSS": "Προσαρμοσμένο CSS",
|
||||
"Dark Mode": "Σκοτεινή λειτουργία",
|
||||
"Default": "Προεπιλογή",
|
||||
"Default Font": "Προεπιλεγμένη γραμματοσειρά",
|
||||
"Default Font Size": "Προεπιλεγμένο μέγεθος γραμματοσειράς",
|
||||
"Delete": "Διαγραφή",
|
||||
"Delete Highlight": "Διαγραφή επισήμανσης",
|
||||
"Dictionary": "Λεξικό",
|
||||
"Disable Click-to-Flip": "Απενεργοποίηση κλικ για γύρισμα σελίδας",
|
||||
"Download Readest": "Λήψη Readest",
|
||||
"Edit": "Επεξεργασία",
|
||||
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
|
||||
"Excerpts": "Αποσπάσματα",
|
||||
"Failed to import book(s): {{filenames}}": "Αποτυχία εισαγωγής βιβλίου(ων): {{filenames}}",
|
||||
"Fast": "Γρήγορα",
|
||||
"Font": "Γραμματοσειρά",
|
||||
"Font & Layout": "Γραμματοσειρά & Διάταξη",
|
||||
"Font Face": "Τύπος γραμματοσειράς",
|
||||
"Font Family": "Οικογένεια γραμματοσειράς",
|
||||
"Font Size": "Μέγεθος γραμματοσειράς",
|
||||
"Full Justification": "Πλήρης στοίχιση",
|
||||
"Global Settings": "Γενικές ρυθμίσεις",
|
||||
"Go Back": "Πίσω",
|
||||
"Go Forward": "Μπροστά",
|
||||
"Go Left": "Αριστερά",
|
||||
"Go Right": "Δεξιά",
|
||||
"Grass": "Γρασίδι",
|
||||
"Gray": "Γκρι",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Επισήμανση",
|
||||
"Horizontal Direction": "Οριζόντια κατεύθυνση",
|
||||
"Hyphenation": "Συλλαβισμός",
|
||||
"Identifier:": "Αναγνωριστικό:",
|
||||
"Import Books": "Εισαγωγή βιβλίων",
|
||||
"Invert Colors in Dark Mode": "Αντιστροφή χρωμάτων στη σκοτεινή λειτουργία",
|
||||
"Language:": "Γλώσσα:",
|
||||
"Layout": "Διάταξη",
|
||||
"Light Mode": "Φωτεινή λειτουργία",
|
||||
"Line Height": "Ύψος γραμμής",
|
||||
"Loading...": "Φόρτωση...",
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Θέση {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Συνδεδεμένος",
|
||||
"Logged in as {{userDisplayName}}": "Συνδεδεμένος ως {{userDisplayName}}",
|
||||
"Match Case": "Ταίριασμα πεζών-κεφαλαίων",
|
||||
"Match Diacritics": "Ταίριασμα διακριτικών σημείων",
|
||||
"Match Whole Words": "Ταίριασμα ολόκληρων λέξεων",
|
||||
"Maximum Block Size": "Μέγιστο μέγεθος μπλοκ",
|
||||
"Maximum Inline Size": "Μέγιστο μέγεθος γραμμής",
|
||||
"Maximum Number of Columns": "Μέγιστος αριθμός στηλών",
|
||||
"Minimum Font Size": "Ελάχιστο μέγεθος γραμματοσειράς",
|
||||
"Misc": "Διάφορα",
|
||||
"Monospace Font": "Γραμματοσειρά σταθερού πλάτους",
|
||||
"More Info": "Περισσότερες πληροφορίες",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Σημειωματάριο",
|
||||
"Notes": "Σημειώσεις",
|
||||
"Open": "Άνοιγμα",
|
||||
"Open Book": "Άνοιγμα βιβλίου",
|
||||
"Original Text": "Αρχικό κείμενο",
|
||||
"Page": "Σελίδα",
|
||||
"Paging Animation": "Κινούμενα σχέδια σελιδοποίησης",
|
||||
"Paragraph": "Παράγραφος",
|
||||
"Parallel Read": "Παράλληλη ανάγνωση",
|
||||
"Published:": "Δημοσιεύτηκε:",
|
||||
"Publisher:": "Εκδότης:",
|
||||
"Reading progress synced": "Ο συγχρονισμός προόδου ανάγνωσης ολοκληρώθηκε",
|
||||
"Reload Page": "Επαναφόρτωση σελίδας",
|
||||
"Reveal in File Explorer": "Εμφάνιση στον εξερευνητή αρχείων",
|
||||
"Reveal in Finder": "Εμφάνιση στον Finder",
|
||||
"Reveal in Folder": "Εμφάνιση στο φάκελο",
|
||||
"Sans-Serif Font": "Γραμματοσειρά Sans-Serif",
|
||||
"Save": "Αποθήκευση",
|
||||
"Scrolled Mode": "Λειτουργία κύλισης",
|
||||
"Search": "Αναζήτηση",
|
||||
"Search books...": "Αναζήτηση βιβλίων...",
|
||||
"Search...": "Αναζήτηση...",
|
||||
"Select Book": "Επιλογή βιβλίου",
|
||||
"Select books": "Επιλογή βιβλίων",
|
||||
"Select multiple books": "Επιλογή πολλαπλών βιβλίων",
|
||||
"Sepia": "Σέπια",
|
||||
"Serif Font": "Γραμματοσειρά Serif",
|
||||
"Show Book Details": "Εμφάνιση λεπτομερειών βιβλίου",
|
||||
"Sidebar": "Πλευρική γραμμή",
|
||||
"Sign In": "Σύνδεση",
|
||||
"Sign Out": "Αποσύνδεση",
|
||||
"Sky": "Ουρανός",
|
||||
"Slow": "Αργά",
|
||||
"Solarized": "Solarized",
|
||||
"Speak": "Ομιλία",
|
||||
"Subjects:": "Θέματα:",
|
||||
"System Fonts": "Γραμματοσειρές συστήματος",
|
||||
"Table of Contents": "Πίνακας περιεχομένων",
|
||||
"TTS not supported in this device": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται σε αυτή τη συσκευή",
|
||||
"Theme Color": "Χρώμα θέματος",
|
||||
"Theme Mode": "Λειτουργία θέματος",
|
||||
"Translate": "Μετάφραση",
|
||||
"Translated Text": "Μεταφρασμένο κείμενο",
|
||||
"Unknown": "Άγνωστο",
|
||||
"Untitled": "Χωρίς τίτλο",
|
||||
"Updated:": "Ενημερώθηκε:",
|
||||
"User avatar": "Εικόνα χρήστη",
|
||||
"Version {{version}}": "Έκδοση {{version}}",
|
||||
"Vertical Direction": "Κάθετη κατεύθυνση",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Καλώς ήρθατε στη βιβλιοθήκη σας. Μπορείτε να εισάγετε τα βιβλία σας εδώ και να τα διαβάσετε οποιαδήποτε στιγμή.",
|
||||
"Wikipedia": "Βικιπαίδεια",
|
||||
"Writing Mode": "Λειτουργία γραφής",
|
||||
"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.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
|
||||
"Update Now!": "Ενημέρωση τώρα!",
|
||||
"Update": "Ενημέρωση",
|
||||
"Check update": "Έλεγχος ενημέρωσης",
|
||||
"Already the latest version": "Ήδη η τελευταία έκδοση",
|
||||
"Book Details": "Λεπτομέρειες βιβλίου",
|
||||
"Import books": "Εισαγωγή βιβλίων",
|
||||
"From local file": "Από τοπικό αρχείο"
|
||||
}
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Tipo de fuente",
|
||||
"Font Family": "Familia de fuente",
|
||||
"Font Size": "Tamaño de fuente",
|
||||
"From Local File": "Desde archivo local",
|
||||
"Full Justification": "Justificación completa",
|
||||
"Gaps (%)": "Espacios (%)",
|
||||
"Global Settings": "Configuración global",
|
||||
"Go Back": "Volver",
|
||||
"Go Forward": "Avanzar",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Sesión iniciada",
|
||||
"Logged in as {{userDisplayName}}": "Sesión iniciada como {{userDisplayName}}",
|
||||
"Margins (px)": "Márgenes (px)",
|
||||
"Match Case": "Coincidir mayúsculas y minúsculas",
|
||||
"Match Diacritics": "Coincidir diacríticos",
|
||||
"Match Whole Words": "Coincidir palabras completas",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Abrir",
|
||||
"Open Book": "Abrir libro",
|
||||
"Original Text": "Texto original",
|
||||
"Override Publisher Font": "Sobrescribir fuente del editor",
|
||||
"Page": "Página",
|
||||
"Paging Animation": "Animación de paginación",
|
||||
"Paragraph": "Párrafo",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Modo de escritura",
|
||||
"Your Library": "Tu biblioteca"
|
||||
}
|
||||
"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.",
|
||||
"Update Now!": "¡Actualizar ahora!",
|
||||
"Update": "Actualizar",
|
||||
"Check update": "Comprobar actualización",
|
||||
"Already the latest version": "Ya es la última versión",
|
||||
"Book Details": "Detalles del libro",
|
||||
"Import books": "Importar libros",
|
||||
"From local file": "Desde archivo local"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Style de police",
|
||||
"Font Family": "Famille de police",
|
||||
"Font Size": "Taille de police",
|
||||
"From Local File": "Depuis un fichier local",
|
||||
"Full Justification": "Justification complète",
|
||||
"Gaps (%)": "Espaces (%)",
|
||||
"Global Settings": "Paramètres globaux",
|
||||
"Go Back": "Retour",
|
||||
"Go Forward": "Avancer",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Connecté",
|
||||
"Logged in as {{userDisplayName}}": "Connecté en tant que {{userDisplayName}}",
|
||||
"Margins (px)": "Marges (px)",
|
||||
"Match Case": "Respecter la casse",
|
||||
"Match Diacritics": "Respecter les accents",
|
||||
"Match Whole Words": "Mots entiers",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Ouvrir",
|
||||
"Open Book": "Ouvrir le livre",
|
||||
"Original Text": "Texte original",
|
||||
"Override Publisher Font": "Remplacer la police de l'éditeur",
|
||||
"Page": "Page",
|
||||
"Paging Animation": "Animation de page",
|
||||
"Paragraph": "Paragraphe",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
|
||||
"Wikipedia": "Wikipédia",
|
||||
"Writing Mode": "Mode écriture",
|
||||
"Your Library": "Votre bibliothèque"
|
||||
"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.",
|
||||
"Update Now!": "Mettre à jour maintenant !",
|
||||
"Update": "Mettre à jour",
|
||||
"Check update": "Vérifier la mise à jour",
|
||||
"Already the latest version": "Déjà la dernière version",
|
||||
"Book Details": "Détails du livre",
|
||||
"Import books": "Importer des livres",
|
||||
"From local file": "Depuis un fichier local"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"(detected)": "(पता लगाया)",
|
||||
"About Readest": "Readest के बारे में",
|
||||
"Add your notes here...": "अपनी टिप्पणियां यहां जोड़ें...",
|
||||
"Animation": "एनीमेशन",
|
||||
"Annotate": "टिप्पणी",
|
||||
"Apply": "लागू करें",
|
||||
"Are you sure to delete the selected books?": "क्या आप चयनित पुस्तकों को हटाना चाहते हैं?",
|
||||
"Auto Mode": "स्वचालित मोड",
|
||||
"Behavior": "व्यवहार",
|
||||
"Book": "पुस्तक",
|
||||
"Book Cover": "पुस्तक का कवर",
|
||||
"Bookmark": "बुकमार्क",
|
||||
"Cancel": "रद्द करें",
|
||||
"Chapter": "अध्याय",
|
||||
"Cherry": "चेरी",
|
||||
"Color": "रंग",
|
||||
"Confirm": "पुष्टि करें",
|
||||
"Confirm Deletion": "हटाने की पुष्टि करें",
|
||||
"Copied to notebook": "नोटबुक में कॉपी किया गया",
|
||||
"Copy": "कॉपी",
|
||||
"Custom CSS": "कस्टम CSS",
|
||||
"Dark Mode": "डार्क मोड",
|
||||
"Default": "डिफ़ॉल्ट",
|
||||
"Default Font": "डिफ़ॉल्ट फ़ॉन्ट",
|
||||
"Default Font Size": "डिफ़ॉल्ट फ़ॉन्ट साइज़",
|
||||
"Delete": "हटाएं",
|
||||
"Delete Highlight": "हाइलाइट हटाएं",
|
||||
"Dictionary": "शब्दकोश",
|
||||
"Disable Click-to-Flip": "क्लिक-टू-फ्लिप अक्षम करें",
|
||||
"Download Readest": "Readest डाउनलोड करें",
|
||||
"Edit": "संपादित करें",
|
||||
"Enter your custom CSS here...": "अपना कस्टम CSS यहां दर्ज करें...",
|
||||
"Excerpts": "अंश",
|
||||
"Failed to import book(s): {{filenames}}": "पुस्तक(ें) आयात करने में विफल: {{filenames}}",
|
||||
"Fast": "तेज़",
|
||||
"Font": "फ़ॉन्ट",
|
||||
"Font & Layout": "फ़ॉन्ट और लेआउट",
|
||||
"Font Face": "फ़ॉन्ट फेस",
|
||||
"Font Family": "फ़ॉन्ट परिवार",
|
||||
"Font Size": "फ़ॉन्ट साइज़",
|
||||
"Full Justification": "पूर्ण जस्टिफिकेशन",
|
||||
"Global Settings": "वैश्विक सेटिंग्स",
|
||||
"Go Back": "वापस जाएं",
|
||||
"Go Forward": "आगे जाएं",
|
||||
"Go Left": "बाएं जाएं",
|
||||
"Go Right": "दाएं जाएं",
|
||||
"Grass": "घास",
|
||||
"Gray": "ग्रे",
|
||||
"Gruvbox": "ग्रूवबॉक्स",
|
||||
"Highlight": "हाइलाइट",
|
||||
"Horizontal Direction": "क्षैतिज दिशा",
|
||||
"Hyphenation": "हाइफ़नेशन",
|
||||
"Identifier:": "पहचानकर्ता:",
|
||||
"Import Books": "पुस्तकें आयात करें",
|
||||
"Invert Colors in Dark Mode": "डार्क मोड में रंग उलटें",
|
||||
"Language:": "भाषा:",
|
||||
"Layout": "लेआउट",
|
||||
"Light Mode": "लाइट मोड",
|
||||
"Line Height": "लाइन हाइट",
|
||||
"Loading...": "लोड हो रहा है...",
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "स्थान {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "लॉग इन किया",
|
||||
"Logged in as {{userDisplayName}}": "{{userDisplayName}} के रूप में लॉग इन किया",
|
||||
"Match Case": "केस मैच करें",
|
||||
"Match Diacritics": "डायाक्रिटिक्स मैच करें",
|
||||
"Match Whole Words": "पूरे शब्द मैच करें",
|
||||
"Maximum Block Size": "अधिकतम ब्लॉक साइज़",
|
||||
"Maximum Inline Size": "अधिकतम इनलाइन साइज़",
|
||||
"Maximum Number of Columns": "कॉलम की अधिकतम संख्या",
|
||||
"Minimum Font Size": "न्यूनतम फ़ॉन्ट साइज़",
|
||||
"Misc": "विविध",
|
||||
"Monospace Font": "मोनोस्पेस फ़ॉन्ट",
|
||||
"More Info": "अधिक जानकारी",
|
||||
"Nord": "नॉर्ड",
|
||||
"Notebook": "नोटबुक",
|
||||
"Notes": "नोट्स",
|
||||
"Open": "खोलें",
|
||||
"Open Book": "पुस्तक खोलें",
|
||||
"Original Text": "मूल पाठ",
|
||||
"Page": "पृष्ठ",
|
||||
"Paging Animation": "पेजिंग एनीमेशन",
|
||||
"Paragraph": "पैराग्राफ",
|
||||
"Parallel Read": "समानांतर पढ़ें",
|
||||
"Published:": "प्रकाशित:",
|
||||
"Publisher:": "प्रकाशक:",
|
||||
"Reading progress synced": "पढ़ने की प्रगति सिंक की गई",
|
||||
"Reload Page": "पृष्ठ रीलोड करें",
|
||||
"Reveal in File Explorer": "फ़ाइल एक्सप्लोरर में दिखाएं",
|
||||
"Reveal in Finder": "फाइंडर में दिखाएं",
|
||||
"Reveal in Folder": "फ़ोल्डर में दिखाएं",
|
||||
"Sans-Serif Font": "सैन्स-सेरिफ फ़ॉन्ट",
|
||||
"Save": "सहेजें",
|
||||
"Scrolled Mode": "स्क्रॉल मोड",
|
||||
"Search": "खोजें",
|
||||
"Search books...": "पुस्तकें खोजें...",
|
||||
"Search...": "खोजें...",
|
||||
"Select Book": "पुस्तक चुनें",
|
||||
"Select books": "पुस्तकें चुनें",
|
||||
"Select multiple books": "कई पुस्तकें चुनें",
|
||||
"Sepia": "सेपिया",
|
||||
"Serif Font": "सेरिफ फ़ॉन्ट",
|
||||
"Show Book Details": "पुस्तक विवरण दिखाएं",
|
||||
"Sidebar": "साइडबार",
|
||||
"Sign In": "साइन इन करें",
|
||||
"Sign Out": "साइन आउट करें",
|
||||
"Sky": "आकाश",
|
||||
"Slow": "धीमा",
|
||||
"Solarized": "सोलराइज्ड",
|
||||
"Speak": "बोलें",
|
||||
"Subjects:": "विषय:",
|
||||
"System Fonts": "सिस्टम फ़ॉन्ट्स",
|
||||
"Table of Contents": "विषय-सूची",
|
||||
"TTS not supported in this device": "यह डिवाइस TTS का समर्थन नहीं करता",
|
||||
"Theme Color": "थीम रंग",
|
||||
"Theme Mode": "थीम मोड",
|
||||
"Translate": "अनुवाद करें",
|
||||
"Translated Text": "अनुवादित पाठ",
|
||||
"Unknown": "अज्ञात",
|
||||
"Untitled": "शीर्षकहीन",
|
||||
"Updated:": "अपडेट किया गया:",
|
||||
"User avatar": "उपयोगकर्ता अवतार",
|
||||
"Version {{version}}": "संस्करण {{version}}",
|
||||
"Vertical Direction": "ऊर्ध्वाधर दिशा",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "आपकी लाइब्रेरी में आपका स्वागत है। आप यहां अपनी पुस्तकें आयात कर सकते हैं और कभी भी पढ़ सकते हैं।",
|
||||
"Wikipedia": "विकिपीडिया",
|
||||
"Writing Mode": "लेखन मोड",
|
||||
"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.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
|
||||
"Update Now!": "अभी अपडेट करें!",
|
||||
"Update": "अपडेट करें",
|
||||
"Check update": "अपडेट जांचें",
|
||||
"Already the latest version": "पहले से ही नवीनतम संस्करण",
|
||||
"Book Details": "पुस्तक विवरण",
|
||||
"Import books": "पुस्तकें आयात करें",
|
||||
"From local file": "स्थानीय फ़ाइल से"
|
||||
}
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Jenis Font",
|
||||
"Font Family": "Keluarga Font",
|
||||
"Font Size": "Ukuran Font",
|
||||
"From Local File": "Dari File Lokal",
|
||||
"Full Justification": "Rata Penuh",
|
||||
"Gaps (%)": "Jarak (%)",
|
||||
"Global Settings": "Pengaturan Global",
|
||||
"Go Back": "Kembali",
|
||||
"Go Forward": "Maju",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Lok. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Sudah Masuk",
|
||||
"Logged in as {{userDisplayName}}": "Masuk sebagai {{userDisplayName}}",
|
||||
"Margins (px)": "Margin (px)",
|
||||
"Match Case": "Cocokkan Huruf Besar/Kecil",
|
||||
"Match Diacritics": "Cocokkan Diakritik",
|
||||
"Match Whole Words": "Cocokkan Kata Utuh",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Buka",
|
||||
"Open Book": "Buka Buku",
|
||||
"Original Text": "Teks Asli",
|
||||
"Override Publisher Font": "Ganti Font Penerbit",
|
||||
"Page": "Halaman",
|
||||
"Paging Animation": "Animasi Halaman",
|
||||
"Paragraph": "Paragraf",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Mode Menulis",
|
||||
"Your Library": "Perpustakaan Anda"
|
||||
"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.",
|
||||
"Update Now!": "Perbarui Sekarang!",
|
||||
"Update": "Perbarui",
|
||||
"Check update": "Periksa pembaruan",
|
||||
"Already the latest version": "Sudah versi terbaru",
|
||||
"Book Details": "Detail Buku",
|
||||
"Import books": "Impor buku",
|
||||
"From local file": "Dari file lokal"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Tipo di carattere",
|
||||
"Font Family": "Famiglia di caratteri",
|
||||
"Font Size": "Dimensione carattere",
|
||||
"From Local File": "Da file locale",
|
||||
"Full Justification": "Giustificazione completa",
|
||||
"Gaps (%)": "Spaziature (%)",
|
||||
"Global Settings": "Impostazioni globali",
|
||||
"Go Back": "Indietro",
|
||||
"Go Forward": "Avanti",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Accesso effettuato",
|
||||
"Logged in as {{userDisplayName}}": "Accesso effettuato come {{userDisplayName}}",
|
||||
"Margins (px)": "Margini (px)",
|
||||
"Match Case": "Maiuscole/minuscole",
|
||||
"Match Diacritics": "Corrispondenza diacritici",
|
||||
"Match Whole Words": "Parole intere",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Apri",
|
||||
"Open Book": "Apri libro",
|
||||
"Original Text": "Testo originale",
|
||||
"Override Publisher Font": "Sostituisci font editore",
|
||||
"Page": "Pagina",
|
||||
"Paging Animation": "Animazione cambio pagina",
|
||||
"Paragraph": "Paragrafo",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Modalità scrittura",
|
||||
"Your Library": "La tua biblioteca"
|
||||
"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.",
|
||||
"Update Now!": "Aggiorna ora!",
|
||||
"Update": "Aggiorna",
|
||||
"Check update": "Controlla aggiornamento",
|
||||
"Already the latest version": "Già l'ultima versione",
|
||||
"Book Details": "Dettagli libro",
|
||||
"Import books": "Importa libri",
|
||||
"From local file": "Da file locale"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "書体",
|
||||
"Font Family": "フォントファミリー",
|
||||
"Font Size": "フォントサイズ",
|
||||
"From Local File": "ローカルファイルから",
|
||||
"Full Justification": "両端揃え",
|
||||
"Gaps (%)": "間隔 (%)",
|
||||
"Global Settings": "全体設定",
|
||||
"Go Back": "戻る",
|
||||
"Go Forward": "進む",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "ログイン済み",
|
||||
"Logged in as {{userDisplayName}}": "{{userDisplayName}}としてログイン中",
|
||||
"Margins (px)": "余白 (px)",
|
||||
"Match Case": "大文字・小文字を区別",
|
||||
"Match Diacritics": "アクセント記号を区別",
|
||||
"Match Whole Words": "単語全体を一致",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "開く",
|
||||
"Open Book": "書籍を開く",
|
||||
"Original Text": "原文",
|
||||
"Override Publisher Font": "出版社フォントを上書き",
|
||||
"Page": "ページ",
|
||||
"Paging Animation": "ページめくりアニメーション",
|
||||
"Paragraph": "段落",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
|
||||
"Wikipedia": "ウィキペディア",
|
||||
"Writing Mode": "書き込みモード",
|
||||
"Your Library": "ライブラリー"
|
||||
"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.": "翻訳を取得できません。後で再試行してください。",
|
||||
"Update Now!": "今すぐ更新!",
|
||||
"Update": "更新",
|
||||
"Check update": "更新を確認",
|
||||
"Already the latest version": "すでに最新バージョン",
|
||||
"Book Details": "書籍の詳細",
|
||||
"Import books": "書籍をインポート",
|
||||
"From local file": "ローカルファイルから"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "글꼴 스타일",
|
||||
"Font Family": "글꼴 패밀리",
|
||||
"Font Size": "글꼴 크기",
|
||||
"From Local File": "로컬 파일에서",
|
||||
"Full Justification": "양쪽 정렬",
|
||||
"Gaps (%)": "간격 (%)",
|
||||
"Global Settings": "전역 설정",
|
||||
"Go Back": "뒤로",
|
||||
"Go Forward": "앞으로",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "위치 {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "로그인됨",
|
||||
"Logged in as {{userDisplayName}}": "{{userDisplayName}}(으)로 로그인됨",
|
||||
"Margins (px)": "여백 (px)",
|
||||
"Match Case": "대소문자 구분",
|
||||
"Match Diacritics": "발음 구별 부호 구분",
|
||||
"Match Whole Words": "전체 단어 일치",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "열기",
|
||||
"Open Book": "책 열기",
|
||||
"Original Text": "원본 텍스트",
|
||||
"Override Publisher Font": "출판사 글꼴 재정의",
|
||||
"Page": "페이지",
|
||||
"Paging Animation": "페이지 넘김 애니메이션",
|
||||
"Paragraph": "단락",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
|
||||
"Wikipedia": "위키피디아",
|
||||
"Writing Mode": "글쓰기 모드",
|
||||
"Your Library": "내 서재"
|
||||
"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.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
|
||||
"Update Now!": "지금 업데이트!",
|
||||
"Update": "업데이트",
|
||||
"Check update": "업데이트 확인",
|
||||
"Already the latest version": "이미 최신 버전",
|
||||
"Book Details": "책 세부 정보",
|
||||
"Import books": "책 가져오기",
|
||||
"From local file": "로컬 파일에서"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"(detected)": "(wykryto)",
|
||||
"About Readest": "O Readest",
|
||||
"Add your notes here...": "Dodaj swoje notatki tutaj...",
|
||||
"Animation": "Animacja",
|
||||
"Annotate": "Adnotacja",
|
||||
"Apply": "Zastosuj",
|
||||
"Are you sure to delete the selected books?": "Czy na pewno chcesz usunąć wybrane książki?",
|
||||
"Auto Mode": "Tryb automatyczny",
|
||||
"Behavior": "Zachowanie",
|
||||
"Book": "Książka",
|
||||
"Book Cover": "Okładka książki",
|
||||
"Bookmark": "Zakładka",
|
||||
"Cancel": "Anuluj",
|
||||
"Chapter": "Rozdział",
|
||||
"Cherry": "Wiśniowy",
|
||||
"Color": "Kolor",
|
||||
"Confirm": "Potwierdź",
|
||||
"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",
|
||||
"Default Font Size": "Domyślny rozmiar czcionki",
|
||||
"Delete": "Usuń",
|
||||
"Delete Highlight": "Usuń zaznaczenie",
|
||||
"Dictionary": "Słownik",
|
||||
"Disable Click-to-Flip": "Wyłącz przewracanie kliknięciem",
|
||||
"Download Readest": "Pobierz Readest",
|
||||
"Edit": "Edytuj",
|
||||
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
|
||||
"Excerpts": "Fragmenty",
|
||||
"Failed to import book(s): {{filenames}}": "Nie udało się zaimportować książek: {{filenames}}",
|
||||
"Fast": "Szybko",
|
||||
"Font": "Czcionka",
|
||||
"Font & Layout": "Czcionka i układ",
|
||||
"Font Face": "Krój czcionki",
|
||||
"Font Family": "Rodzina czcionek",
|
||||
"Font Size": "Rozmiar czcionki",
|
||||
"Full Justification": "Pełne justowanie",
|
||||
"Global Settings": "Ustawienia globalne",
|
||||
"Go Back": "Wstecz",
|
||||
"Go Forward": "Dalej",
|
||||
"Go Left": "W lewo",
|
||||
"Go Right": "W prawo",
|
||||
"Grass": "Trawiasty",
|
||||
"Gray": "Szary",
|
||||
"Gruvbox": "Ciepły pomarańczowy",
|
||||
"Highlight": "Zaznacz",
|
||||
"Horizontal Direction": "Kierunek poziomy",
|
||||
"Hyphenation": "Dzielenie wyrazów",
|
||||
"Identifier:": "Identyfikator:",
|
||||
"Import Books": "Importuj książki",
|
||||
"Invert Colors in Dark Mode": "Odwróć kolory w trybie ciemnym",
|
||||
"Language:": "Język:",
|
||||
"Layout": "Układ",
|
||||
"Light Mode": "Tryb jasny",
|
||||
"Line Height": "Wysokość linii",
|
||||
"Loading...": "Ładowanie...",
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Poz. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Zalogowano",
|
||||
"Logged in as {{userDisplayName}}": "Zalogowano jako {{userDisplayName}}",
|
||||
"Match Case": "Uwzględnij wielkość liter",
|
||||
"Match Diacritics": "Uwzględnij znaki diakrytyczne",
|
||||
"Match Whole Words": "Uwzględnij całe słowa",
|
||||
"Maximum Block Size": "Maksymalny rozmiar bloku",
|
||||
"Maximum Inline Size": "Maksymalny rozmiar linii",
|
||||
"Maximum Number of Columns": "Maksymalna liczba kolumn",
|
||||
"Minimum Font Size": "Minimalny rozmiar czcionki",
|
||||
"Misc": "Różne",
|
||||
"Monospace Font": "Czcionka o stałej szerokości",
|
||||
"More Info": "Więcej informacji",
|
||||
"Nord": "Północny",
|
||||
"Notebook": "Notatnik",
|
||||
"Notes": "Notatki",
|
||||
"Open": "Otwórz",
|
||||
"Open Book": "Otwórz książkę",
|
||||
"Original Text": "Tekst oryginalny",
|
||||
"Page": "Strona",
|
||||
"Paging Animation": "Animacja przewracania stron",
|
||||
"Paragraph": "Akapit",
|
||||
"Parallel Read": "Czytanie równoległe",
|
||||
"Published:": "Opublikowano:",
|
||||
"Publisher:": "Wydawca:",
|
||||
"Reading progress synced": "Zsynchronizowano postęp czytania",
|
||||
"Reload Page": "Odśwież stronę",
|
||||
"Reveal in File Explorer": "Pokaż w Eksploratorze plików",
|
||||
"Reveal in Finder": "Pokaż w Finderze",
|
||||
"Reveal in Folder": "Pokaż w folderze",
|
||||
"Sans-Serif Font": "Czcionka bezszeryfowa",
|
||||
"Save": "Zapisz",
|
||||
"Scrolled Mode": "Tryb przewijania",
|
||||
"Search": "Szukaj",
|
||||
"Search books...": "Szukaj książek...",
|
||||
"Search...": "Szukaj...",
|
||||
"Select Book": "Wybierz książkę",
|
||||
"Select books": "Wybierz książki",
|
||||
"Select multiple books": "Wybierz wiele książek",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Czcionka szeryfowa",
|
||||
"Show Book Details": "Pokaż szczegóły książki",
|
||||
"Sidebar": "Panel boczny",
|
||||
"Sign In": "Zaloguj się",
|
||||
"Sign Out": "Wyloguj się",
|
||||
"Sky": "Niebieski",
|
||||
"Slow": "Wolno",
|
||||
"Solarized": "Słoneczny",
|
||||
"Speak": "Czytaj",
|
||||
"Subjects:": "Tematy:",
|
||||
"System Fonts": "Czcionki systemowe",
|
||||
"Table of Contents": "Spis treści",
|
||||
"TTS not supported in this device": "TTS nie jest obsługiwane na tym urządzeniu",
|
||||
"Theme Color": "Kolor motywu",
|
||||
"Theme Mode": "Tryb motywu",
|
||||
"Translate": "Tłumacz",
|
||||
"Translated Text": "Przetłumaczony tekst",
|
||||
"Unknown": "Nieznane",
|
||||
"Untitled": "Bez tytułu",
|
||||
"Updated:": "Zaktualizowano:",
|
||||
"User avatar": "Awatar użytkownika",
|
||||
"Version {{version}}": "Wersja {{version}}",
|
||||
"Vertical Direction": "Kierunek pionowy",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Witaj w swojej bibliotece. Możesz tutaj zaimportować swoje książki i czytać je w dowolnym momencie.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Tryb pisania",
|
||||
"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.",
|
||||
"Update Now!": "Zaktualizuj teraz!",
|
||||
"Update": "Zaktualizuj",
|
||||
"Check update": "Sprawdź aktualizację",
|
||||
"Already the latest version": "Już najnowsza wersja",
|
||||
"Book Details": "Szczegóły książki",
|
||||
"Import books": "Importuj książki",
|
||||
"From local file": "Z pliku lokalnego"
|
||||
}
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Estilo da Fonte",
|
||||
"Font Family": "Família da Fonte",
|
||||
"Font Size": "Tamanho da Fonte",
|
||||
"From Local File": "Do Arquivo Local",
|
||||
"Full Justification": "Justificação Completa",
|
||||
"Gaps (%)": "Espaçamentos (%)",
|
||||
"Global Settings": "Configurações Globais",
|
||||
"Go Back": "Voltar",
|
||||
"Go Forward": "Avançar",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Loc. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Conectado",
|
||||
"Logged in as {{userDisplayName}}": "Conectado como {{userDisplayName}}",
|
||||
"Margins (px)": "Margens (px)",
|
||||
"Match Case": "Diferenciar Maiúsculas e Minúsculas",
|
||||
"Match Diacritics": "Corresponder Acentos",
|
||||
"Match Whole Words": "Corresponder Palavras Inteiras",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Abrir",
|
||||
"Open Book": "Abrir Livro",
|
||||
"Original Text": "Texto Original",
|
||||
"Override Publisher Font": "Substituir Fonte do Editor",
|
||||
"Page": "Página",
|
||||
"Paging Animation": "Animação de Página",
|
||||
"Paragraph": "Parágrafo",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
|
||||
"Wikipedia": "Wikipédia",
|
||||
"Writing Mode": "Modo de Escrita",
|
||||
"Your Library": "Sua Biblioteca"
|
||||
"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.",
|
||||
"Update Now!": "Atualizar Agora!",
|
||||
"Update": "Atualizar",
|
||||
"Check update": "Verificar atualização",
|
||||
"Already the latest version": "Já é a versão mais recente",
|
||||
"Book Details": "Detalhes do Livro",
|
||||
"Import books": "Importar livros",
|
||||
"From local file": "Do arquivo local"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Начертание шрифта",
|
||||
"Font Family": "Семейство шрифтов",
|
||||
"Font Size": "Размер шрифта",
|
||||
"From Local File": "Из локального файла",
|
||||
"Full Justification": "Полное выравнивание",
|
||||
"Gaps (%)": "Промежутки (%)",
|
||||
"Global Settings": "Общие настройки",
|
||||
"Go Back": "Назад",
|
||||
"Go Forward": "Вперёд",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Поз. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Выполнен вход",
|
||||
"Logged in as {{userDisplayName}}": "Вход выполнен как {{userDisplayName}}",
|
||||
"Margins (px)": "Отступы (px)",
|
||||
"Match Case": "Учитывать регистр",
|
||||
"Match Diacritics": "Учитывать диакритические знаки",
|
||||
"Match Whole Words": "Только целые слова",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Открыть",
|
||||
"Open Book": "Открыть книгу",
|
||||
"Original Text": "Оригинальный текст",
|
||||
"Override Publisher Font": "Переопределить шрифт издателя",
|
||||
"Page": "Страница",
|
||||
"Paging Animation": "Анимация перелистывания",
|
||||
"Paragraph": "Абзац",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
|
||||
"Wikipedia": "Википедия",
|
||||
"Writing Mode": "Режим записи",
|
||||
"Your Library": "Ваша библиотека"
|
||||
"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.": "Не удалось получить перевод. Попробуйте позже.",
|
||||
"Update Now!": "Обновить сейчас!",
|
||||
"Update": "Обновить",
|
||||
"Check update": "Проверить обновление",
|
||||
"Already the latest version": "Уже последняя версия",
|
||||
"Book Details": "Детали книги",
|
||||
"Import books": "Импортировать книги",
|
||||
"From local file": "Из локального файла"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Yazı Tipi Yüzü",
|
||||
"Font Family": "Yazı Tipi Ailesi",
|
||||
"Font Size": "Yazı Boyutu",
|
||||
"From Local File": "Yerel Dosyadan",
|
||||
"Full Justification": "Tam Hizalama",
|
||||
"Gaps (%)": "Boşluklar (%)",
|
||||
"Global Settings": "Genel Ayarlar",
|
||||
"Go Back": "Geri Git",
|
||||
"Go Forward": "İleri Git",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Konum {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Giriş Yapıldı",
|
||||
"Logged in as {{userDisplayName}}": "{{userDisplayName}} olarak giriş yapıldı",
|
||||
"Margins (px)": "Kenar Boşlukları (px)",
|
||||
"Match Case": "Büyük/Küçük Harf Eşleştir",
|
||||
"Match Diacritics": "Aksan İşaretlerini Eşleştir",
|
||||
"Match Whole Words": "Tam Kelimeleri Eşleştir",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Aç",
|
||||
"Open Book": "Kitabı Aç",
|
||||
"Original Text": "Orijinal Metin",
|
||||
"Override Publisher Font": "Yayıncı Yazı Tipini Geçersiz Kıl",
|
||||
"Page": "Sayfa",
|
||||
"Paging Animation": "Sayfa Çevirme Animasyonu",
|
||||
"Paragraph": "Paragraf",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
|
||||
"Wikipedia": "Vikipedi",
|
||||
"Writing Mode": "Yazma Modu",
|
||||
"Your Library": "Kütüphaneniz"
|
||||
"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.",
|
||||
"Update Now!": "Şimdi Güncelle!",
|
||||
"Update": "Güncelle",
|
||||
"Check update": "Güncellemeyi kontrol et",
|
||||
"Already the latest version": "Zaten en son sürüm",
|
||||
"Book Details": "Kitap Detayları",
|
||||
"Import books": "Kitapları içe aktar",
|
||||
"From local file": "Yerel dosyadan"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"(detected)": "(виявлено)",
|
||||
"About Readest": "Про Readest",
|
||||
"Add your notes here...": "Додайте свої нотатки тут...",
|
||||
"Animation": "Анімація",
|
||||
"Annotate": "Анотація",
|
||||
"Apply": "Застосувати",
|
||||
"Are you sure to delete the selected books?": "Ви впевнені, що хочете видалити вибрані книги?",
|
||||
"Auto Mode": "Автоматичний режим",
|
||||
"Behavior": "Поведінка",
|
||||
"Book": "Книга",
|
||||
"Book Cover": "Обкладинка книги",
|
||||
"Bookmark": "Закладка",
|
||||
"Cancel": "Скасувати",
|
||||
"Chapter": "Розділ",
|
||||
"Cherry": "Вишневий",
|
||||
"Color": "Колір",
|
||||
"Confirm": "Підтвердити",
|
||||
"Confirm Deletion": "Підтвердити видалення",
|
||||
"Copied to notebook": "Скопійовано до нотатника",
|
||||
"Copy": "Копіювати",
|
||||
"Custom CSS": "Користувацький CSS",
|
||||
"Dark Mode": "Темний режим",
|
||||
"Default": "За замовчуванням",
|
||||
"Default Font": "Шрифт за замовчуванням",
|
||||
"Default Font Size": "Розмір шрифту за замовчуванням",
|
||||
"Delete": "Видалити",
|
||||
"Delete Highlight": "Видалити виділення",
|
||||
"Dictionary": "Словник",
|
||||
"Disable Click-to-Flip": "Вимкнути гортання кліком",
|
||||
"Download Readest": "Завантажити Readest",
|
||||
"Edit": "Редагувати",
|
||||
"Enter your custom CSS here...": "Введіть ваш користувацький CSS тут...",
|
||||
"Excerpts": "Уривки",
|
||||
"Failed to import book(s): {{filenames}}": "Не вдалося імпортувати книгу(и): {{filenames}}",
|
||||
"Fast": "Швидко",
|
||||
"Font": "Шрифт",
|
||||
"Font & Layout": "Шрифт та макет",
|
||||
"Font Face": "Накреслення шрифту",
|
||||
"Font Family": "Сімейство шрифтів",
|
||||
"Font Size": "Розмір шрифту",
|
||||
"Full Justification": "Повне вирівнювання",
|
||||
"Global Settings": "Глобальні налаштування",
|
||||
"Go Back": "Назад",
|
||||
"Go Forward": "Вперед",
|
||||
"Go Left": "Вліво",
|
||||
"Go Right": "Вправо",
|
||||
"Grass": "Трав'яний",
|
||||
"Gray": "Сірий",
|
||||
"Gruvbox": "Теплий помаранчевий",
|
||||
"Highlight": "Виділення",
|
||||
"Horizontal Direction": "Горизонтальний напрямок",
|
||||
"Hyphenation": "Перенесення слів",
|
||||
"Identifier:": "Ідентифікатор:",
|
||||
"Import Books": "Імпортувати книги",
|
||||
"Invert Colors in Dark Mode": "Інвертувати кольори в темному режимі",
|
||||
"Language:": "Мова:",
|
||||
"Layout": "Макет",
|
||||
"Light Mode": "Світлий режим",
|
||||
"Line Height": "Висота рядка",
|
||||
"Loading...": "Завантаження...",
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Поз. {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Увійшли",
|
||||
"Logged in as {{userDisplayName}}": "Увійшли як {{userDisplayName}}",
|
||||
"Match Case": "Враховувати регістр",
|
||||
"Match Diacritics": "Враховувати діакритичні знаки",
|
||||
"Match Whole Words": "Шукати цілі слова",
|
||||
"Maximum Block Size": "Максимальний розмір блоку",
|
||||
"Maximum Inline Size": "Максимальний розмір рядка",
|
||||
"Maximum Number of Columns": "Максимальна кількість колонок",
|
||||
"Minimum Font Size": "Мінімальний розмір шрифту",
|
||||
"Misc": "Різне",
|
||||
"Monospace Font": "Моноширинний шрифт",
|
||||
"More Info": "Більше інформації",
|
||||
"Nord": "Північний",
|
||||
"Notebook": "Нотатник",
|
||||
"Notes": "Нотатки",
|
||||
"Open": "Відкрити",
|
||||
"Open Book": "Відкрити книгу",
|
||||
"Original Text": "Оригінальний текст",
|
||||
"Page": "Сторінка",
|
||||
"Paging Animation": "Анімація гортання",
|
||||
"Paragraph": "Абзац",
|
||||
"Parallel Read": "Паралельне читання",
|
||||
"Published:": "Опубліковано:",
|
||||
"Publisher:": "Видавець:",
|
||||
"Reading progress synced": "Прогрес читання синхронізовано",
|
||||
"Reload Page": "Перезавантажити сторінку",
|
||||
"Reveal in File Explorer": "Показати у провіднику",
|
||||
"Reveal in Finder": "Показати у Finder",
|
||||
"Reveal in Folder": "Показати в папці",
|
||||
"Sans-Serif Font": "Шрифт без засічок",
|
||||
"Save": "Зберегти",
|
||||
"Scrolled Mode": "Режим прокрутки",
|
||||
"Search": "Пошук",
|
||||
"Search books...": "Шукати книги...",
|
||||
"Search...": "Пошук...",
|
||||
"Select Book": "Вибрати книгу",
|
||||
"Select books": "Вибрати книги",
|
||||
"Select multiple books": "Вибрати кілька книг",
|
||||
"Sepia": "Сепія",
|
||||
"Serif Font": "Шрифт із засічками",
|
||||
"Show Book Details": "Показати деталі книги",
|
||||
"Sidebar": "Бічна панель",
|
||||
"Sign In": "Увійти",
|
||||
"Sign Out": "Вийти",
|
||||
"Sky": "Небесний",
|
||||
"Slow": "Повільно",
|
||||
"Solarized": "Сонячний",
|
||||
"Speak": "Озвучити",
|
||||
"Subjects:": "Теми:",
|
||||
"System Fonts": "Системні шрифти",
|
||||
"Table of Contents": "Зміст",
|
||||
"TTS not supported in this device": "TTS не підтримується на цьому пристрої",
|
||||
"Theme Color": "Колір теми",
|
||||
"Theme Mode": "Режим теми",
|
||||
"Translate": "Перекласти",
|
||||
"Translated Text": "Перекладений текст",
|
||||
"Unknown": "Невідомо",
|
||||
"Untitled": "Без назви",
|
||||
"Updated:": "Оновлено:",
|
||||
"User avatar": "Аватар користувача",
|
||||
"Version {{version}}": "Версія {{version}}",
|
||||
"Vertical Direction": "Вертикальний напрямок",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Ласкаво просимо до вашої бібліотеки. Ви можете імпортувати сюди свої книги та читати їх будь-коли.",
|
||||
"Wikipedia": "Вікіпедія",
|
||||
"Writing Mode": "Режим письма",
|
||||
"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.": "Не вдалося отримати переклад. Спробуйте пізніше.",
|
||||
"Update Now!": "Оновити зараз!",
|
||||
"Update": "Оновити",
|
||||
"Check update": "Перевірити оновлення",
|
||||
"Already the latest version": "Вже остання версія",
|
||||
"Book Details": "Деталі книги",
|
||||
"Import books": "Імпортувати книги",
|
||||
"From local file": "З локального файлу"
|
||||
}
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "Kiểu chữ",
|
||||
"Font Family": "Họ phông chữ",
|
||||
"Font Size": "Cỡ chữ",
|
||||
"From Local File": "Từ tệp cục bộ",
|
||||
"Full Justification": "Căn đều hai bên",
|
||||
"Gaps (%)": "Khoảng cách (%)",
|
||||
"Global Settings": "Cài đặt chung",
|
||||
"Go Back": "Quay lại",
|
||||
"Go Forward": "Tiến tới",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "Vị trí {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "Đã đăng nhập",
|
||||
"Logged in as {{userDisplayName}}": "Đăng nhập với tên {{userDisplayName}}",
|
||||
"Margins (px)": "Lề (px)",
|
||||
"Match Case": "Phân biệt chữ hoa/thường",
|
||||
"Match Diacritics": "Phân biệt dấu",
|
||||
"Match Whole Words": "Khớp toàn bộ từ",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "Mở",
|
||||
"Open Book": "Mở sách",
|
||||
"Original Text": "Văn bản gốc",
|
||||
"Override Publisher Font": "Ghi đè phông chữ của nhà xuất bản",
|
||||
"Page": "Trang",
|
||||
"Paging Animation": "Hiệu ứng lật trang",
|
||||
"Paragraph": "Đoạn văn",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Chế độ viết",
|
||||
"Your Library": "Thư viện của bạn"
|
||||
"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.",
|
||||
"Update Now!": "Cập nhật ngay!",
|
||||
"Update": "Cập nhật",
|
||||
"Check update": "Kiểm tra cập nhật",
|
||||
"Already the latest version": "Đã là phiên bản mới nhất",
|
||||
"Book Details": "Chi tiết sách",
|
||||
"Import books": "Nhập sách",
|
||||
"From local file": "Từ tệp địa phương"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "字型",
|
||||
"Font Family": "字族",
|
||||
"Font Size": "字号",
|
||||
"From Local File": "从本地文件导入",
|
||||
"Full Justification": "两端对齐",
|
||||
"Gaps (%)": "间距 (%)",
|
||||
"Global Settings": "全局设置",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前进",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "已登录",
|
||||
"Logged in as {{userDisplayName}}": "{{userDisplayName}} 已登录",
|
||||
"Margins (px)": "页边距 (px)",
|
||||
"Match Case": "匹配大小写",
|
||||
"Match Diacritics": "匹配重音符号",
|
||||
"Match Whole Words": "匹配整个单词",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "打开",
|
||||
"Open Book": "打开书籍",
|
||||
"Original Text": "原文",
|
||||
"Override Publisher Font": "覆盖内置字体",
|
||||
"Page": "页面",
|
||||
"Paging Animation": "翻页动画",
|
||||
"Paragraph": "段落",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "书库空空如也,您可以导入您的书籍并随时阅读。",
|
||||
"Wikipedia": "维基百科",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "书库"
|
||||
"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.": "无法获取翻译,请稍后重试。",
|
||||
"Update Now!": "立即更新!",
|
||||
"Update": "更新",
|
||||
"Check update": "检查更新",
|
||||
"Already the latest version": "已经是最新版本",
|
||||
"Book Details": "书籍详情",
|
||||
"Import books": "导入书籍",
|
||||
"From local file": "从本地文件导入"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"Font Face": "字型",
|
||||
"Font Family": "字族",
|
||||
"Font Size": "字號",
|
||||
"From Local File": "從本地檔案導入",
|
||||
"Full Justification": "兩端對齊",
|
||||
"Gaps (%)": "間距 (%)",
|
||||
"Global Settings": "全局設置",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前進",
|
||||
@@ -64,7 +62,6 @@
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
|
||||
"Logged in": "已登入",
|
||||
"Logged in as {{userDisplayName}}": "{{userDisplayName}} 已登入",
|
||||
"Margins (px)": "頁邊距 (px)",
|
||||
"Match Case": "匹配大小寫",
|
||||
"Match Diacritics": "匹配重音符號",
|
||||
"Match Whole Words": "匹配整個單詞",
|
||||
@@ -81,7 +78,6 @@
|
||||
"Open": "打開",
|
||||
"Open Book": "打開書籍",
|
||||
"Original Text": "原文",
|
||||
"Override Publisher Font": "覆蓋內置字體",
|
||||
"Page": "頁面",
|
||||
"Paging Animation": "翻頁動畫",
|
||||
"Paragraph": "段落",
|
||||
@@ -129,5 +125,19 @@
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
|
||||
"Wikipedia": "維基百科",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "書庫"
|
||||
"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.": "無法獲取翻譯,請稍後再試。",
|
||||
"Update Now!": "立即更新!",
|
||||
"Update": "更新",
|
||||
"Check update": "檢查更新",
|
||||
"Already the latest version": "已經是最新版本",
|
||||
"Book Details": "書籍詳情",
|
||||
"Import books": "導入書籍",
|
||||
"From local file": "從本地文件導入"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Readest",
|
||||
"short_name": "Readest",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"description": "Readest is an open-source eBook reader supporting EPUB, PDF, and sync across devices.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"splash_pages": null
|
||||
}
|
||||
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.7": {
|
||||
"date": "2025-01-23",
|
||||
"notes": [
|
||||
"Support PWA for the web version",
|
||||
"Swipe up to toggle header/footer",
|
||||
"Various fixes and enhancements on updater, header bar and tts"
|
||||
]
|
||||
},
|
||||
"0.9.6": {
|
||||
"date": "2025-01-18",
|
||||
"notes": ["Various enhancements for mobile platforms", "Fix DeepL translation API"]
|
||||
},
|
||||
"0.9.5": {
|
||||
"date": "2025-01-15",
|
||||
"notes": [
|
||||
"Various TTS related UX enhancements",
|
||||
"Add more system fonts as custom fonts",
|
||||
"Show reading progress in bookshelf"
|
||||
]
|
||||
},
|
||||
"0.9.3": {
|
||||
"date": "2025-01-11",
|
||||
"notes": [
|
||||
|
||||
@@ -18,7 +18,6 @@ use std::path::PathBuf;
|
||||
use tauri::{command, Window};
|
||||
use tauri::{AppHandle, Emitter, Manager, Url};
|
||||
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri_plugin_dialog;
|
||||
use tauri_plugin_fs::FsExt;
|
||||
use tauri_plugin_oauth::start;
|
||||
|
||||
@@ -30,12 +29,12 @@ fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
let fs_scope = app.fs_scope();
|
||||
let asset_protocol_scope = app.asset_protocol_scope();
|
||||
for file in &files {
|
||||
if let Err(e) = fs_scope.allow_file(&file) {
|
||||
if let Err(e) = fs_scope.allow_file(file) {
|
||||
eprintln!("Failed to allow file in fs_scope: {}", e);
|
||||
} else {
|
||||
println!("Allowed file in fs_scope: {:?}", file);
|
||||
}
|
||||
if let Err(e) = asset_protocol_scope.allow_file(&file) {
|
||||
if let Err(e) = asset_protocol_scope.allow_file(file) {
|
||||
eprintln!("Failed to allow file in asset_protocol_scope: {}", e);
|
||||
} else {
|
||||
println!("Allowed file in asset_protocol_scope: {:?}", file);
|
||||
@@ -197,9 +196,9 @@ pub fn run() {
|
||||
// win.open_devtools();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
menu::setup_macos_menu(&app.handle())?;
|
||||
menu::setup_macos_menu(app.handle())?;
|
||||
|
||||
app.handle().emit("window-ready", {}).unwrap();
|
||||
app.handle().emit("window-ready", ()).unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -216,7 +215,7 @@ pub fn run() {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let app_handler_clone = app_handle.clone();
|
||||
allow_file_in_scopes(&app_handle, files.clone());
|
||||
allow_file_in_scopes(app_handle, files.clone());
|
||||
app_handle.listen("window-ready", move |_| {
|
||||
println!("Window is ready, proceeding to handle files.");
|
||||
set_window_open_with_files(&app_handler_clone, files.clone());
|
||||
|
||||
@@ -25,7 +25,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
setup_traffic_light_positioner(window);
|
||||
}
|
||||
});
|
||||
return;
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -118,7 +117,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
}
|
||||
extern "C" fn on_window_did_resize<R: Runtime>(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
with_window_state(&*this, |state: &mut WindowState<R>| {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
let id = state
|
||||
.window
|
||||
.ns_window()
|
||||
@@ -216,7 +215,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(&*this, |state: &mut WindowState<R>| {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("did-enter-fullscreen", ())
|
||||
@@ -233,7 +232,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(&*this, |state: &mut WindowState<R>| {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("will-enter-fullscreen", ())
|
||||
@@ -250,7 +249,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(&*this, |state: &mut WindowState<R>| {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("did-exit-fullscreen", ())
|
||||
@@ -274,7 +273,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(&*this, |state: &mut WindowState<R>| {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("will-exit-fullscreen", ())
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://fonts.gstatic.com https://db.onlinewebfonts.com",
|
||||
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
|
||||
"connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net",
|
||||
"font-src": "'self' data: asset: http://asset.localhost tauri: https://fonts.gstatic.com https://db.onlinewebfonts.com https://cdn.jsdelivr.net",
|
||||
"frame-src": "'self' blob: asset: http://asset.localhost",
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com"
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Ic
|
||||
'bg-base-100 border-gray-300 shadow-sm transition hover:bg-gray-50',
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<Icon />
|
||||
<span className='text-neutral-content/70 px-2 text-sm'>{label}</span>
|
||||
</button>
|
||||
);
|
||||
@@ -194,7 +194,7 @@ export default function AuthPage() {
|
||||
onClick={handleGoBack}
|
||||
className='btn btn-ghost fixed left-3 top-11 h-8 min-h-8 w-8 p-0'
|
||||
>
|
||||
<IoArrowBack size={20} className='text-base-content' />
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
|
||||
<ProviderLogin
|
||||
@@ -238,7 +238,7 @@ export default function AuthPage() {
|
||||
onClick={handleGoBack}
|
||||
className='btn btn-ghost fixed left-10 top-6 h-8 min-h-8 w-8 p-0'
|
||||
>
|
||||
<IoArrowBack size={20} className='text-base-content' />
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
|
||||
@@ -13,6 +13,32 @@ const description =
|
||||
'Perfect for deep reading, analysis, and understanding. Explore now!';
|
||||
const previewImage = 'https://cdn.readest.com/images/open_graph_preview_read_now.png';
|
||||
|
||||
export const metadata = {
|
||||
title,
|
||||
description,
|
||||
generator: 'Next.js',
|
||||
manifest: '/manifest.json',
|
||||
keywords: ['epub', 'pdf', 'ebook', 'reader', 'readest', 'pwa'],
|
||||
authors: [
|
||||
{
|
||||
name: 'readest',
|
||||
url: 'https://github.com/readest/readest',
|
||||
},
|
||||
],
|
||||
icons: [
|
||||
{ rel: 'apple-touch-icon', url: '/apple-touch-icon.png' },
|
||||
{ rel: 'icon', url: '/icon.png' },
|
||||
],
|
||||
};
|
||||
|
||||
export const viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
themeColor: 'white',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
@@ -23,6 +49,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='description' content={description} />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:type' content='website' />
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getFilename } from '@/utils/book';
|
||||
@@ -25,6 +26,7 @@ import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import BookDetailModal from '@/components/BookDetailModal';
|
||||
import ReadingProgress from './ReadingProgress';
|
||||
|
||||
type BookshelfItem = Book | BooksGroup;
|
||||
|
||||
@@ -72,6 +74,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
const isImportingBook = useRef(false);
|
||||
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
const showBookDetailsModal = (book: Book) => {
|
||||
setShowDetailsBook(book);
|
||||
@@ -231,28 +234,58 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
{isSelectMode && (
|
||||
<div className='absolute bottom-1 right-1'>
|
||||
{selectedBooks.includes(item.hash) ? (
|
||||
<MdCheckCircle size={20} className='fill-blue-500' />
|
||||
<MdCheckCircle className='fill-blue-500' />
|
||||
) : (
|
||||
<MdCheckCircleOutline
|
||||
size={20}
|
||||
className='fill-gray-300 drop-shadow-sm'
|
||||
/>
|
||||
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row items-center justify-between p-0 pt-2'>
|
||||
<h4 className='card-title line-clamp-1 text-[0.6em] text-xs font-semibold'>
|
||||
{(item as Book).title}
|
||||
</h4>
|
||||
{isWebAppPlatform() && (
|
||||
<div
|
||||
className='show-detail-button self-start opacity-0 group-hover:opacity-100'
|
||||
role='button'
|
||||
onClick={showBookDetailsModal.bind(null, item as Book)}
|
||||
>
|
||||
<CiCircleMore size={15} />
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full p-0 pt-2',
|
||||
isWebAppPlatform() ? 'flex-col' : 'flex-row justify-between',
|
||||
)}
|
||||
>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-[0.6em] text-xs font-semibold'>
|
||||
{(item as Book).title}
|
||||
</h4>
|
||||
</div>
|
||||
{item.progress && (
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<ReadingProgress book={item as Book} />
|
||||
{isWebAppPlatform() && (
|
||||
<button
|
||||
type='button'
|
||||
className='show-detail-button opacity-0 group-hover:opacity-100'
|
||||
onClick={showBookDetailsModal.bind(null, item as Book)}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
showBookDetailsModal(item as Book);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CiCircleMore size={iconSize15} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!item.progress && isWebAppPlatform() && (
|
||||
<div className={'flex items-center justify-end'}>
|
||||
<button
|
||||
type='button'
|
||||
className='show-detail-button opacity-0 group-hover:opacity-100'
|
||||
onClick={showBookDetailsModal.bind(null, item as Book)}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
showBookDetailsModal(item as Book);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CiCircleMore size={iconSize15} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -270,6 +303,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
className='h-48 w-full object-cover'
|
||||
/>
|
||||
</figure>
|
||||
<div className='card-body p-4'>
|
||||
<h3 className='card-title line-clamp-2 text-sm'>{book.title}</h3>
|
||||
<p className='text-neutral-content line-clamp-1 text-xs'>{book.author}</p>
|
||||
<ReadingProgress book={book} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MdOutlineMenu } from 'react-icons/md';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
@@ -27,6 +28,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
const { appService } = useEnv();
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -48,8 +50,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-2',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center justify-between space-x-6'>
|
||||
<div className='exclude-title-bar-mousedown sm:w relative flex w-full items-center pl-4'>
|
||||
<div className='flex items-center justify-between space-x-12'>
|
||||
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
|
||||
<span className='absolute left-8 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
</span>
|
||||
@@ -66,7 +68,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
|
||||
<span className='mx-2 h-5 w-[1px] bg-gray-400'></span>
|
||||
<div className='dropdown dropdown-bottom flex h-5 cursor-pointer justify-center'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip='Add books'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Import books')}>
|
||||
<PiPlus tabIndex={-1} className='h-5 w-5' />
|
||||
</div>
|
||||
<ul
|
||||
@@ -75,7 +77,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
>
|
||||
<li>
|
||||
<button className='text-base-content' onClick={onImportBooks}>
|
||||
{_('From Local File')}
|
||||
{_('From local file')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -97,20 +99,22 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-full items-center'>
|
||||
<div className='flex h-full items-center gap-x-4'>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end mr-2'
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={16} />}
|
||||
toggleButton={<MdOutlineMenu size={iconSize16} />}
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
/>
|
||||
{!isTrafficLightVisible && appService?.appPlatform !== 'web' && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={true}
|
||||
showMaximize={true}
|
||||
showClose={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type React from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import type { Book } from '@/types/book';
|
||||
|
||||
interface ReadingProgressProps {
|
||||
book: Book;
|
||||
}
|
||||
|
||||
const getProgressPercentage = (book: Book) => {
|
||||
if (!book.progress || !book.progress[1]) {
|
||||
return null;
|
||||
}
|
||||
if (book.progress && book.progress[1] === 1) {
|
||||
return 100;
|
||||
}
|
||||
return Math.round((book.progress[0] / book.progress[1]) * 100);
|
||||
};
|
||||
|
||||
const ReadingProgress: React.FC<ReadingProgressProps> = memo(
|
||||
({ book }) => {
|
||||
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
|
||||
|
||||
if (progressPercentage === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='text-neutral-content/70 flex justify-between text-xs'>
|
||||
<span>{progressPercentage}%</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.book.hash === nextProps.book.hash &&
|
||||
prevProps.book.updatedAt === nextProps.book.updatedAt
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ReadingProgress.displayName = 'ReadingProgress';
|
||||
|
||||
export default ReadingProgress;
|
||||
@@ -75,7 +75,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
height={20}
|
||||
/>
|
||||
) : (
|
||||
<PiUserCircleCheck size={20} />
|
||||
<PiUserCircleCheck />
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -84,11 +84,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
</ul>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
label={_('Sign In')}
|
||||
icon={<PiUserCircle size={20} />}
|
||||
onClick={handleUserLogin}
|
||||
></MenuItem>
|
||||
<MenuItem label={_('Sign In')} icon={<PiUserCircle />} onClick={handleUserLogin}></MenuItem>
|
||||
)}
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useState, useRef, useEffect, Suspense } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -38,8 +39,8 @@ const LibraryPage = () => {
|
||||
checkOpenWithBooks,
|
||||
clearOpenWithBooks,
|
||||
} = useLibraryStore();
|
||||
useTheme();
|
||||
const _ = useTranslation();
|
||||
const { updateAppTheme } = useTheme();
|
||||
const { setSettings, saveSettings } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
@@ -48,12 +49,14 @@ const LibraryPage = () => {
|
||||
const demoBooks = useDemoBooks();
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-200');
|
||||
const doAppUpdates = async () => {
|
||||
if (hasUpdater()) {
|
||||
await checkForAppUpdates();
|
||||
await checkForAppUpdates(_);
|
||||
}
|
||||
};
|
||||
doAppUpdates();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const processOpenWithFiles = React.useCallback(
|
||||
@@ -233,7 +236,12 @@ const LibraryPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='library-page rounded-window bg-base-200/50 text-base-content flex h-full min-h-screen select-none flex-col overflow-hidden'>
|
||||
<div
|
||||
className={clsx(
|
||||
'library-page bg-base-200 text-base-content flex h-dvh select-none flex-col overflow-hidden',
|
||||
isTauriAppPlatform() && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
<div className='fixed top-0 z-40 w-full'>
|
||||
<LibraryHeader
|
||||
isSelectMode={isSelectMode}
|
||||
|
||||
@@ -97,9 +97,9 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
isBookmarked ? (
|
||||
<MdOutlineBookmark size={20} className='text-base-content' />
|
||||
<MdOutlineBookmark className='text-base-content' />
|
||||
) : (
|
||||
<MdOutlineBookmarkAdd size={20} className='text-base-content' />
|
||||
<MdOutlineBookmarkAdd className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={toggleBookmark}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SectionInfo from './SectionInfo';
|
||||
@@ -22,9 +24,18 @@ interface BooksGridProps {
|
||||
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
const bookData = getBookData(sideBarBookKey);
|
||||
if (!bookData || !bookData.book) return;
|
||||
document.title = bookData.book.title;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className='grid h-full flex-grow'
|
||||
@@ -49,7 +60,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<div
|
||||
id={`gridcell-${bookKey}`}
|
||||
key={bookKey}
|
||||
className='rounded-window relative h-full w-full overflow-hidden'
|
||||
className={`${isTauriAppPlatform() ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
|
||||
>
|
||||
{isBookmarked && <Ribbon width={marginGap} />}
|
||||
<HeaderBar
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BookConfig } from '@/types/book';
|
||||
import { FoliateView, wrappedFoliateView } from '@/types/view';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
import { useClickEvent } from '../hooks/useClickEvent';
|
||||
import { useClickEvent, useTouchEvent } from '../hooks/useClickEvent';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { useProgressSync } from '../hooks/useProgressSync';
|
||||
import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar';
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
handleMouseup,
|
||||
handleClick,
|
||||
handleWheel,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
} from '../utils/iframeEventHandlers';
|
||||
|
||||
const FoliateViewer: React.FC<{
|
||||
@@ -42,7 +45,6 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
// console.log('relocate:', detail);
|
||||
setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range);
|
||||
};
|
||||
|
||||
@@ -68,6 +70,9 @@ const FoliateViewer: React.FC<{
|
||||
detail.doc.addEventListener('mouseup', handleMouseup.bind(null, bookKey));
|
||||
detail.doc.addEventListener('click', handleClick.bind(null, bookKey));
|
||||
detail.doc.addEventListener('wheel', handleWheel.bind(null, bookKey));
|
||||
detail.doc.addEventListener('touchstart', handleTouchStart.bind(null, bookKey));
|
||||
detail.doc.addEventListener('touchmove', handleTouchMove.bind(null, bookKey));
|
||||
detail.doc.addEventListener('touchend', handleTouchEnd.bind(null, bookKey));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -88,6 +93,7 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
useTouchEvent(bookKey, viewRef);
|
||||
const { handleTurnPage } = useClickEvent(bookKey, viewRef, containerRef);
|
||||
useFoliateEvents(viewRef.current, {
|
||||
onLoad: docLoadHandler,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isPWA, isTauriAppPlatform } from '@/services/environment';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface FooterBarProps {
|
||||
@@ -47,7 +48,11 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
const handleSpeakText = async () => {
|
||||
if (!view || !progress) return;
|
||||
const { range } = progress;
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range });
|
||||
if (eventDispatcher.dispatchSync('tts-is-speaking')) {
|
||||
eventDispatcher.dispatch('tts-stop', { bookKey });
|
||||
} else {
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range });
|
||||
}
|
||||
};
|
||||
|
||||
const pageinfoValid = pageinfo && pageinfo.total > 0 && pageinfo.current >= 0;
|
||||
@@ -55,28 +60,28 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center px-4',
|
||||
'shadow-xs bg-base-100 rounded-window-bottom-right transition-opacity duration-300',
|
||||
!isSideBarVisible && 'rounded-window-bottom-left',
|
||||
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
|
||||
'shadow-xs bg-base-100 transition-opacity duration-300',
|
||||
isPWA() ? 'pb-[env(safe-area-inset-bottom)]' : '',
|
||||
isTauriAppPlatform() && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-bottom-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
|
||||
)}
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='hidden sm:flex'>
|
||||
<Button icon={<RiArrowLeftWideLine />} onClick={handleGoPrev} tooltip={_('Go Left')} />
|
||||
</div>
|
||||
<Button
|
||||
icon={<RiArrowLeftWideLine size={20} />}
|
||||
onClick={handleGoPrev}
|
||||
tooltip={_('Go Left')}
|
||||
/>
|
||||
<Button
|
||||
icon={<RiArrowGoBackLine size={20} />}
|
||||
icon={<RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
tooltip={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={<RiArrowGoForwardLine size={20} />}
|
||||
icon={<RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
tooltip={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
@@ -92,12 +97,10 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
value={pageinfoValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
<Button icon={<FaHeadphones size={20} />} onClick={handleSpeakText} tooltip={_('Speak')} />
|
||||
<Button
|
||||
icon={<RiArrowRightWideLine size={20} />}
|
||||
onClick={handleGoNext}
|
||||
tooltip={_('Go Right')}
|
||||
/>
|
||||
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
|
||||
<div className='hidden sm:flex'>
|
||||
<Button icon={<RiArrowRightWideLine />} onClick={handleGoNext} tooltip={_('Go Right')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,15 @@ import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SidebarToggler from './SidebarToggler';
|
||||
import BookmarkToggler from './BookmarkToggler';
|
||||
import NotebookToggler from './NotebookToggler';
|
||||
import SettingsToggler from './SettingsToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
|
||||
interface HeaderBarProps {
|
||||
@@ -36,6 +39,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { hoveredBookKey, setHoveredBookKey, bookKeys } = useReaderStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
const handleToggleDropdown = (isOpen: boolean) => {
|
||||
setIsDropdownOpen(isOpen);
|
||||
@@ -48,8 +52,9 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
className={clsx(
|
||||
`header-bar absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
|
||||
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
|
||||
`shadow-xs bg-base-100 rounded-window-top-right transition-opacity duration-300`,
|
||||
!isSideBarVisible && 'rounded-window-top-left',
|
||||
`shadow-xs bg-base-100 transition-opacity duration-300`,
|
||||
isTauriAppPlatform() && 'rounded-window-top-right',
|
||||
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-top-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey || isDropdownOpen ? `visible` : `opacity-0`,
|
||||
isDropdownOpen && 'header-bar-pinned',
|
||||
@@ -57,21 +62,22 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='sidebar-bookmark-toggler flex h-full items-center'>
|
||||
<div className='sidebar-bookmark-toggler bg-base-100 z-20 flex h-full items-center space-x-4'>
|
||||
<SidebarToggler bookKey={bookKey} />
|
||||
<BookmarkToggler bookKey={bookKey} />
|
||||
</div>
|
||||
|
||||
<div className='header-title flex flex-1 items-center justify-center'>
|
||||
<h2 className='line-clamp-1 max-w-[80%] text-center text-xs font-semibold'>{bookTitle}</h2>
|
||||
<div className='header-title z-15 pointer-events-none absolute inset-0 hidden items-center justify-center sm:flex'>
|
||||
<h2 className='line-clamp-1 max-w-[50%] text-center text-xs font-semibold'>{bookTitle}</h2>
|
||||
</div>
|
||||
|
||||
<div className='flex h-full items-center space-x-2'>
|
||||
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
|
||||
<SettingsToggler />
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
|
||||
onToggle={handleToggleDropdown}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
|
||||
@@ -80,8 +86,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<WindowButtons
|
||||
className='window-buttons flex h-full items-center'
|
||||
headerRef={headerRef}
|
||||
showMinimize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
|
||||
showMaximize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
|
||||
showMinimize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
showMaximize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
onClose={() => onCloseBook(bookKey)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { TbLayoutSidebarRight, TbLayoutSidebarRightFilled } from 'react-icons/tb';
|
||||
import { LuNotebookPen } from 'react-icons/lu';
|
||||
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface NotebookTogglerProps {
|
||||
@@ -14,6 +15,8 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
|
||||
const { isNotebookVisible, toggleNotebook } = useNotebookStore();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleNotebook();
|
||||
@@ -26,9 +29,9 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
sideBarBookKey == bookKey && isNotebookVisible ? (
|
||||
<TbLayoutSidebarRightFilled size={20} className='text-base-content' />
|
||||
<LuNotebookPen size={iconSize16} className='text-base-content' />
|
||||
) : (
|
||||
<TbLayoutSidebarRight size={20} className='text-base-content' />
|
||||
<LuNotebookPen size={iconSize16} className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense, useRef } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import ReaderContent from './ReaderContent';
|
||||
@@ -16,7 +19,10 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { library, setLibrary } = useLibraryStore();
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
const { updateAppTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-100');
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
const initLibrary = async () => {
|
||||
@@ -33,7 +39,12 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
return (
|
||||
library.length > 0 &&
|
||||
settings.globalReadSettings && (
|
||||
<div className='reader-page rounded-window bg-base-100 text-base-content min-h-screen select-none'>
|
||||
<div
|
||||
className={clsx(
|
||||
`reader-page bg-base-100 text-base-content select-none`,
|
||||
isTauriAppPlatform() && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
<Suspense>
|
||||
<ReaderContent ids={ids} settings={settings} />
|
||||
<AboutWindow />
|
||||
|
||||
@@ -145,7 +145,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
setTimeout(() => setLoading(true), 300);
|
||||
return (
|
||||
loading && (
|
||||
<div className={'hero hero-content min-h-screen'}>
|
||||
<div className={'hero hero-content h-dvh'}>
|
||||
<Spinner loading={true} />
|
||||
</div>
|
||||
)
|
||||
@@ -153,7 +153,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-screen'>
|
||||
<div className='flex h-dvh'>
|
||||
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<TTSControl />
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { RiFontSize } from 'react-icons/ri';
|
||||
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
const SettingsToggler = () => {
|
||||
const _ = useTranslation();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const handleToggleSettings = () => {
|
||||
setFontLayoutSettingsDialogOpen(!isFontLayoutSettingsDialogOpen);
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
icon={<RiFontSize className='text-base-content' />}
|
||||
onClick={handleToggleSettings}
|
||||
tooltip={_('Font & Layout')}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsToggler;
|
||||
@@ -24,9 +24,9 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
sideBarBookKey === bookKey && isSideBarVisible ? (
|
||||
<TbLayoutSidebarFilled size={20} className='text-base-content' />
|
||||
<TbLayoutSidebarFilled className='text-base-content' />
|
||||
) : (
|
||||
<TbLayoutSidebar size={20} className='text-base-content' />
|
||||
<TbLayoutSidebar className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
|
||||
@@ -92,15 +92,15 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomLevel <= MIN_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomOut size={20} />
|
||||
<MdZoomOut />
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
|
||||
'hover:bg-base-300 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
|
||||
)}
|
||||
onClick={resetZoom}
|
||||
>
|
||||
@@ -109,11 +109,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
'hover:bg-base-300 text-base-content rounded-full p-2',
|
||||
zoomLevel >= MAX_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomIn size={20} />
|
||||
<MdZoomIn />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +124,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<MenuItem
|
||||
label={_('Scrolled Mode')}
|
||||
shortcut='Shift+J'
|
||||
icon={isScrolledMode ? <MdCheck size={20} /> : undefined}
|
||||
icon={isScrolledMode ? <MdCheck /> : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
/>
|
||||
|
||||
@@ -138,20 +138,12 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
? _('Light Mode')
|
||||
: _('Auto Mode')
|
||||
}
|
||||
icon={
|
||||
themeMode === 'dark' ? (
|
||||
<BiMoon size={20} />
|
||||
) : themeMode === 'light' ? (
|
||||
<BiSun size={20} />
|
||||
) : (
|
||||
<TbSunMoon size={20} />
|
||||
)
|
||||
}
|
||||
icon={themeMode === 'dark' ? <BiMoon /> : themeMode === 'light' ? <BiSun /> : <TbSunMoon />}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<MenuItem
|
||||
label={_('Invert Colors in Dark Mode')}
|
||||
icon={isInvertedColors ? <MdCheck size={20} className='text-base-content' /> : undefined}
|
||||
icon={isInvertedColors ? <MdCheck className='text-base-content' /> : undefined}
|
||||
onClick={toggleInvertedColors}
|
||||
disabled={!isDarkMode}
|
||||
/>
|
||||
|
||||
@@ -305,6 +305,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (!selection || !selection.text) return;
|
||||
const { sectionHref: href } = progress;
|
||||
selection.href = href;
|
||||
setShowAnnotPopup(false);
|
||||
setHighlightOptionsVisible(false);
|
||||
setNotebookVisible(true);
|
||||
setNotebookNewAnnotation(selection);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
const LANGUAGES = {
|
||||
AUTO: 'Auto Detect',
|
||||
@@ -23,6 +24,8 @@ const LANGUAGES = {
|
||||
'ZH-HANT': 'Chinese (Traditional)',
|
||||
};
|
||||
|
||||
const DEEPL_API_ENDPOINT = getAPIBaseUrl() + '/deepl/translate';
|
||||
|
||||
interface DeepLPopupProps {
|
||||
text: string;
|
||||
position: Position;
|
||||
@@ -39,6 +42,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
popupHeight,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { token } = useAuth();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const [sourceLang, setSourceLang] = useState('AUTO');
|
||||
const [targetLang, setTargetLang] = useState(settings.globalReadSettings.translateTargetLang);
|
||||
@@ -63,23 +67,12 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
setError(null);
|
||||
setTranslation(null);
|
||||
|
||||
if (!process.env['NEXT_PUBLIC_DEEPL_API_KEY']) {
|
||||
console.error('DeepL API key not found. Set NEXT_PUBLIC_DEEPL_API_KEY in .env.local');
|
||||
}
|
||||
|
||||
const { fetch, url } = isWebAppPlatform()
|
||||
? { fetch: window.fetch, url: '/api/deepl/translate' }
|
||||
: {
|
||||
fetch: (await import('@tauri-apps/plugin-http')).fetch,
|
||||
url: 'https://api-free.deepl.com/v2/translate',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await fetch(DEEPL_API_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `DeepL-Auth-Key ${process.env['NEXT_PUBLIC_DEEPL_API_KEY']}`,
|
||||
Authorization: `Bearer ${token ?? ''}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: [text],
|
||||
@@ -106,14 +99,15 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
setTranslation(translatedText);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Unable to fetch the translation. Try again later.');
|
||||
setError(_('Unable to fetch the translation. Try again later.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTranslation();
|
||||
}, [text, sourceLang, targetLang]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [text, token, sourceLang, targetLang]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { FaCheckCircle } from 'react-icons/fa';
|
||||
import { HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const styles = ['highlight', 'underline', 'squiggly'] as HighlightStyle[];
|
||||
const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
|
||||
@@ -25,6 +25,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
const globalReadSettings = settings.globalReadSettings;
|
||||
const [selectedStyle, setSelectedStyle] = React.useState<HighlightStyle>(_selectedStyle);
|
||||
const [selectedColor, setSelectedColor] = React.useState<HighlightColor>(_selectedColor);
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
const handleSelectStyle = (style: HighlightStyle) => {
|
||||
globalReadSettings.highlightStyle = style;
|
||||
@@ -84,7 +85,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
)}
|
||||
>
|
||||
{selectedColor === color && (
|
||||
<FaCheckCircle size={16} className={clsx(`fill-${color}-400`)} />
|
||||
<FaCheckCircle size={iconSize16} className={clsx(`fill-${color}-400`)} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -15,14 +15,14 @@ const PopupButton: React.FC<PopupButtonProps> = ({ showTooltip, tooltipText, Ico
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className='tooltip tooltip-bottom'
|
||||
className='lg:tooltip lg:tooltip-bottom'
|
||||
data-tip={!buttonClicked && showTooltip ? tooltipText : null}
|
||||
>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className='my-2 flex h-8 min-h-8 w-8 items-center justify-center p-0'
|
||||
>
|
||||
<Icon size={20} />
|
||||
<Icon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { LuNotebookPen } from 'react-icons/lu';
|
||||
import { MdArrowBackIosNew, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const NotebookHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
handleClose: () => void;
|
||||
handleTogglePin: () => void;
|
||||
}> = ({ isPinned, handleTogglePin }) => {
|
||||
}> = ({ isPinned, handleClose, handleTogglePin }) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize14 = useResponsiveSize(14);
|
||||
return (
|
||||
<div className='notebook-header relative flex h-11 items-center px-3'>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<div className='absolute inset-0 flex items-center justify-center space-x-2'>
|
||||
<LuNotebookPen />
|
||||
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center space-x-3'>
|
||||
<div className='z-10 flex items-center gap-x-4'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
className={clsx(
|
||||
'btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
|
||||
isPinned ? 'bg-base-300' : 'bg-base-300/65',
|
||||
)}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
{isPinned ? <MdPushPin size={iconSize14} /> : <MdOutlinePushPin size={iconSize14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
|
||||
>
|
||||
<MdArrowBackIosNew />
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
<FiSearch />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,20 +7,24 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
|
||||
const MIN_NOTEBOOK_WIDTH = 0.15;
|
||||
const MAX_NOTEBOOK_WIDTH = 0.45;
|
||||
|
||||
const Notebook: React.FC = ({}) => {
|
||||
const _ = useTranslation();
|
||||
const { updateAppTheme } = useTheme();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
@@ -31,10 +35,32 @@ const Notebook: React.FC = ({}) => {
|
||||
const { setNotebookWidth, setNotebookVisible, toggleNotebookPin } = useNotebookStore();
|
||||
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
|
||||
|
||||
const onNavigateEvent = async () => {
|
||||
const pinButton = document.querySelector('.sidebar-pin-btn');
|
||||
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
|
||||
if (isPinButtonHidden) {
|
||||
setNotebookVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isNotebookVisible) {
|
||||
updateAppTheme('base-200');
|
||||
} else {
|
||||
updateAppTheme('base-100');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNotebookVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
setNotebookWidth(settings.globalReadSettings.notebookWidth);
|
||||
setNotebookPin(settings.globalReadSettings.isNotebookPinned);
|
||||
setNotebookVisible(settings.globalReadSettings.isNotebookPinned);
|
||||
|
||||
eventDispatcher.on('navigate', onNavigateEvent);
|
||||
return () => {
|
||||
eventDispatcher.off('navigate', onNavigateEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -126,7 +152,7 @@ const Notebook: React.FC = ({}) => {
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 h-full min-w-60 select-none',
|
||||
'rounded-window-top-right rounded-window-bottom-right',
|
||||
isTauriAppPlatform() && 'rounded-window-top-right rounded-window-bottom-right',
|
||||
!isNotebookPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
@@ -135,11 +161,23 @@ const Notebook: React.FC = ({}) => {
|
||||
position: isNotebookPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
@media (max-width: 640px) {
|
||||
.notebook-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<NotebookHeader isPinned={isNotebookPinned} handleTogglePin={handleTogglePin} />
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
handleClose={() => setNotebookVisible(false)}
|
||||
handleTogglePin={handleTogglePin}
|
||||
/>
|
||||
<div className='max-h-[calc(100vh-44px)] overflow-y-auto px-3'>
|
||||
<div>{excerptNotes.length > 0 && <p className='pt-1 text-sm'>{_('Excerpts')}</p>}</div>
|
||||
<ul className=''>
|
||||
|
||||
@@ -5,9 +5,10 @@ import { TbSunMoon } from 'react-icons/tb';
|
||||
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { themes } from '@/styles/themes';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -15,6 +16,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
useTheme();
|
||||
const { getViews, getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const iconSize24 = useResponsiveSize(24);
|
||||
|
||||
useEffect(() => {
|
||||
getViews().forEach((view) => {
|
||||
@@ -28,30 +30,30 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>{_('Theme Mode')}</h2>
|
||||
<div className='flex gap-2'>
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Auto Mode')}>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto Mode')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('auto')}
|
||||
>
|
||||
<TbSunMoon size={20} />
|
||||
<TbSunMoon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Light Mode')}>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Light Mode')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('light')}
|
||||
>
|
||||
<MdOutlineLightMode size={20} />
|
||||
<MdOutlineLightMode />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Dark Mode')}>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Dark Mode')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('dark')}
|
||||
>
|
||||
<MdOutlineDarkMode size={20} />
|
||||
<MdOutlineDarkMode />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,9 +82,9 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className='hidden'
|
||||
/>
|
||||
{themeColor === name ? (
|
||||
<MdRadioButtonChecked size={24} />
|
||||
<MdRadioButtonChecked size={iconSize24} />
|
||||
) : (
|
||||
<MdRadioButtonUnchecked size={24} />
|
||||
<MdRadioButtonUnchecked size={iconSize24} />
|
||||
)}
|
||||
<span>{_(label)}</span>
|
||||
</label>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
interface DialogMenuProps {
|
||||
toggleDropdown?: () => void;
|
||||
@@ -9,6 +10,7 @@ interface DialogMenuProps {
|
||||
|
||||
const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize = useDefaultIconSize();
|
||||
const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useSettingsStore();
|
||||
|
||||
const handleToggleGlobal = () => {
|
||||
@@ -26,10 +28,15 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
onClick={handleToggleGlobal}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isFontLayoutSettingsGlobal && <MdCheck size={20} className='text-base-content' />}
|
||||
<span style={{ minWidth: `${iconSize}px` }}>
|
||||
{isFontLayoutSettingsGlobal && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<div className='tooltip' data-tip='Uncheck for current book settings'>
|
||||
<div
|
||||
className='lg:tooltip'
|
||||
data-tip={
|
||||
isFontLayoutSettingsGlobal ? _('Apply to all books') : _('Apply to this book')
|
||||
}
|
||||
>
|
||||
<span className='ml-2'>{_('Global Settings')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { FiChevronUp, FiChevronLeft } from 'react-icons/fi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface DropdownProps {
|
||||
family?: string;
|
||||
@@ -22,6 +23,8 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
onGetFontFamily,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const defaultIconSize = useDefaultIconSize();
|
||||
return (
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
@@ -29,7 +32,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
|
||||
>
|
||||
<span style={{ fontFamily: onGetFontFamily(selected, family ?? '') }}>{selected}</span>
|
||||
<FiChevronUp className='h-4 w-4' />
|
||||
<FiChevronUp size={iconSize16} />
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
@@ -38,8 +41,8 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
{options.map((option) => (
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selected === option && <MdCheck size={20} className='text-base-content' />}
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
{selected === option && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>{option}</span>
|
||||
</div>
|
||||
@@ -48,8 +51,8 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
{moreOptions && moreOptions.length > 0 && (
|
||||
<li className='dropdown dropdown-left dropdown-top'>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
<FiChevronLeft className='h-4 w-4' />
|
||||
<span style={{ minWidth: `${iconSize16}px` }}>
|
||||
<FiChevronLeft size={iconSize16} />
|
||||
</span>
|
||||
<span>{_('System Fonts')}</span>
|
||||
</div>
|
||||
@@ -57,14 +60,14 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] overflow-y-scroll shadow',
|
||||
'!mr-5 mb-[-46px] max-h-80 w-[292px]',
|
||||
'!mr-5 mb-[-46px] inline max-h-80 w-[220px] overflow-y-scroll',
|
||||
)}
|
||||
>
|
||||
{moreOptions.map((option) => (
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
<div className='flex items-center px-2'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selected === option && <MdCheck size={20} className='text-base-content' />}
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
{selected === option && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
|
||||
{option}
|
||||
|
||||
@@ -218,7 +218,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className=''>{_('Override Publisher Font')}</span>
|
||||
<span className=''>{_('Override Book Font')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
|
||||
@@ -148,30 +148,30 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>{_('Writing Mode')}</h2>
|
||||
<div className='flex gap-2'>
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Default')}>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Default')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('auto')}
|
||||
>
|
||||
<MdOutlineAutoMode size={20} />
|
||||
<MdOutlineAutoMode />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Horizontal Direction')}>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Horizontal Direction')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('horizontal-tb')}
|
||||
>
|
||||
<MdOutlineTextRotationNone size={20} />
|
||||
<MdOutlineTextRotationNone />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Vertical Direction')}>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Vertical Direction')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('vertical-rl')}
|
||||
>
|
||||
<MdOutlineTextRotationDown size={20} />
|
||||
<MdOutlineTextRotationDown />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +220,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label={_('Margins (px)')}
|
||||
label={_('Vertical Margins (px)')}
|
||||
value={marginPx}
|
||||
onChange={setMarginPx}
|
||||
min={0}
|
||||
@@ -228,7 +228,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
step={4}
|
||||
/>
|
||||
<NumberInput
|
||||
label={_('Gaps (%)')}
|
||||
label={_('Horizontal Margins (%)')}
|
||||
value={gapPercent}
|
||||
onChange={setGapPercent}
|
||||
min={0}
|
||||
|
||||
@@ -135,7 +135,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
>
|
||||
<div className='relative p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 text-base !outline-none sm:text-sm'
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={draftStylesheet}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -7,95 +8,123 @@ import { RiDashboardLine } from 'react-icons/ri';
|
||||
import { VscSymbolColor } from 'react-icons/vsc';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
import { IoAccessibilityOutline } from 'react-icons/io5';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
import FontPanel from './FontPanel';
|
||||
import LayoutPanel from './LayoutPanel';
|
||||
import ColorPanel from './ColorPanel';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import DialogMenu from './DialogMenu';
|
||||
import MiscPanel from './MiscPanel';
|
||||
|
||||
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Misc';
|
||||
|
||||
type TabConfig = {
|
||||
tab: SettingsPanelType;
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setFontLayoutSettingsDialogOpen(false);
|
||||
}
|
||||
const tabConfig = [
|
||||
{
|
||||
tab: 'Font',
|
||||
icon: RiFontSize,
|
||||
label: _('Font'),
|
||||
},
|
||||
{
|
||||
tab: 'Layout',
|
||||
icon: RiDashboardLine,
|
||||
label: _('Layout'),
|
||||
},
|
||||
{
|
||||
tab: 'Color',
|
||||
icon: VscSymbolColor,
|
||||
label: _('Color'),
|
||||
},
|
||||
{
|
||||
tab: 'Misc',
|
||||
icon: IoAccessibilityOutline,
|
||||
label: _('Misc'),
|
||||
},
|
||||
] as TabConfig[];
|
||||
|
||||
const handleClose = () => {
|
||||
setFontLayoutSettingsDialogOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dialog className='modal modal-open min-w-90 w-full !bg-[rgba(0,0,0,0.2)]'>
|
||||
<div className='modal-box settings-content flex h-[65%] w-1/2 min-w-[540px] max-w-full flex-col p-0'>
|
||||
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
|
||||
<div className='dialog-tabs flex h-10 max-w-[80%] flex-grow items-center justify-around'>
|
||||
<>
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
onClose={handleClose}
|
||||
className='modal-open'
|
||||
boxClassName='sm:w-1/2 sm:min-w-[480px]'
|
||||
header={
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Font' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Font')}
|
||||
tabIndex={-1}
|
||||
onClick={handleClose}
|
||||
className={
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<RiFontSize size={20} className='mr-0' />
|
||||
{_('Font')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Layout')}
|
||||
>
|
||||
<RiDashboardLine size={20} className='mr-0' />
|
||||
{_('Layout')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Color' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Color')}
|
||||
>
|
||||
<VscSymbolColor size={20} className='mr-0' />
|
||||
{_('Color')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Misc' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Misc')}
|
||||
>
|
||||
<IoAccessibilityOutline size={20} className='mr-0' />
|
||||
{_('Misc')}
|
||||
<MdArrowBackIosNew />
|
||||
</button>
|
||||
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center justify-around pl-4'>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={clsx(
|
||||
'btn btn-ghost text-base-content h-8 min-h-8',
|
||||
activePanel === tab ? 'btn-active' : '',
|
||||
)}
|
||||
onClick={() => setActivePanel(tab)}
|
||||
>
|
||||
<Icon className='mr-0' />
|
||||
{window.innerWidth >= 500 ? label : ''}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end gap-x-2'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold />}
|
||||
>
|
||||
<DialogMenu />
|
||||
</Dropdown>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex'}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1em'
|
||||
height='1em'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
>
|
||||
<DialogMenu />
|
||||
</Dropdown>
|
||||
<WindowButtons
|
||||
className='window-buttons absolute right-4 !ml-2 flex h-full items-center'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={() => setFontLayoutSettingsDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='text-base-content mt-2 flex-grow overflow-y-auto px-[10%]'>
|
||||
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
}
|
||||
>
|
||||
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import { MdInfoOutline } from 'react-icons/md';
|
||||
import { Book } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const BookCard = ({ book }: { book: Book }) => {
|
||||
const { coverImageUrl, title, author } = book;
|
||||
const _ = useTranslation();
|
||||
const iconSize18 = useResponsiveSize(18);
|
||||
|
||||
const showBookDetails = () => {
|
||||
eventDispatcher.dispatchSync('show-book-details', book);
|
||||
@@ -20,7 +22,7 @@ const BookCard = ({ book }: { book: Book }) => {
|
||||
alt={_('Book Cover')}
|
||||
width={56}
|
||||
height={80}
|
||||
className='mr-4 aspect-auto max-h-20 w-[15%] max-w-14 rounded-sm object-cover shadow-md'
|
||||
className='mr-4 aspect-auto max-h-16 w-[15%] max-w-12 rounded-sm object-cover shadow-md'
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
@@ -33,7 +35,7 @@ const BookCard = ({ book }: { book: Book }) => {
|
||||
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
|
||||
aria-label={_('More Info')}
|
||||
>
|
||||
<MdInfoOutline size={18} className='fill-base-content' onClick={showBookDetails} />
|
||||
<MdInfoOutline size={iconSize18} className='fill-base-content' onClick={showBookDetails} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -10,10 +11,11 @@ import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import useBooksManager from '../../hooks/useBooksManager';
|
||||
|
||||
interface BookMenuProps {
|
||||
menuClassName?: string;
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const { library } = useLibraryStore();
|
||||
const { openParallelView } = useBooksManager();
|
||||
@@ -39,7 +41,10 @@ const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='book-menu dropdown-content dropdown-center border-base-100 z-20 mt-3 w-60 shadow-2xl'
|
||||
className={clsx(
|
||||
'book-menu dropdown-content border-base-100 z-20 w-60 shadow-2xl',
|
||||
menuClassName,
|
||||
)}
|
||||
>
|
||||
<MenuItem label={_('Parallel Read')} noIcon>
|
||||
<ul className='max-h-60 overflow-y-auto'>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import useScrollToItem from '../../hooks/useScrollToItem';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
@@ -29,6 +30,8 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
eventDispatcher.dispatch('navigate', { bookKey, cfi });
|
||||
|
||||
getView(bookKey)?.goTo(cfi);
|
||||
if (note) {
|
||||
setNotebookVisible(true);
|
||||
@@ -122,12 +125,16 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
)}
|
||||
onClick={editNote.bind(null, item)}
|
||||
>
|
||||
<div className={clsx(
|
||||
'align-bottom text-blue-400',
|
||||
'transition duration-300 ease-in-out',
|
||||
'group-hover:opacity-100 opacity-0',
|
||||
'hover:text-blue-600',
|
||||
)}>{_('Edit')}</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'align-bottom text-blue-400',
|
||||
'transition duration-300 ease-in-out',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'hover:text-blue-600',
|
||||
)}
|
||||
>
|
||||
{_('Edit')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -137,12 +144,16 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
)}
|
||||
onClick={deleteNote.bind(null, item)}
|
||||
>
|
||||
<div className={clsx(
|
||||
'align-bottom text-red-400',
|
||||
'transition duration-300 ease-in-out',
|
||||
'group-hover:opacity-100 opacity-0',
|
||||
'hover:text-red-600',
|
||||
)}>{_('Delete')}</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'align-bottom text-red-400',
|
||||
'transition duration-300 ease-in-out',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'hover:text-red-600',
|
||||
)}
|
||||
>
|
||||
{_('Delete')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ const SidebarContent: React.FC<{
|
||||
const handleTabChange = (tab: string) => {
|
||||
setFade(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setFade(false)
|
||||
setFade(false);
|
||||
setTargetTab(tab);
|
||||
setConfig(sideBarBookKey!, config);
|
||||
clearTimeout(timeout);
|
||||
@@ -68,15 +68,15 @@ const SidebarContent: React.FC<{
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-content flex min-h-0 flex-grow flex-col',
|
||||
'font-sans text-sm font-normal shadow-inner',
|
||||
'sidebar-content flex min-h-0 flex-grow flex-col shadow-inner',
|
||||
'font-sans text-base font-normal sm:text-sm',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={clsx(
|
||||
'scroll-container overflow-y-auto transition-opacity duration-300 ease-in-out',
|
||||
{ 'opacity-0': fade, 'opacity-100': !fade }
|
||||
{ 'opacity-0': fade, 'opacity-100': !fade },
|
||||
)}
|
||||
>
|
||||
{targetTab === 'toc' && bookDoc.toc && (
|
||||
|
||||
@@ -3,8 +3,10 @@ import React from 'react';
|
||||
import { GiBookshelf } from 'react-icons/gi';
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import BookMenu from './BookMenu';
|
||||
|
||||
@@ -12,10 +14,14 @@ const SidebarHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
isSearchBarVisible: boolean;
|
||||
onGoToLibrary: () => void;
|
||||
onClose: () => void;
|
||||
onTogglePin: () => void;
|
||||
onToggleSearchBar: () => void;
|
||||
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onTogglePin, onToggleSearchBar }) => {
|
||||
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
const iconSize14 = useResponsiveSize(14);
|
||||
const iconSize18 = useResponsiveSize(18);
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -23,12 +29,18 @@ const SidebarHeader: React.FC<{
|
||||
isTrafficLightVisible ? 'pl-20' : 'pl-1.5',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center gap-x-8'>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
|
||||
>
|
||||
<MdArrowBackIosNew size={iconSize22} />
|
||||
</button>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
<GiBookshelf size={20} className='fill-base-content' />
|
||||
<GiBookshelf className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex size-[70%] min-w-24 max-w-32 items-center justify-between'>
|
||||
<div className='flex min-w-24 max-w-32 items-center justify-between sm:size-[70%]'>
|
||||
<button
|
||||
onClick={onToggleSearchBar}
|
||||
className={clsx(
|
||||
@@ -36,21 +48,28 @@ const SidebarHeader: React.FC<{
|
||||
isSearchBarVisible ? 'bg-base-300' : '',
|
||||
)}
|
||||
>
|
||||
<FiSearch size={18} className='text-base-content' />
|
||||
<FiSearch size={iconSize18} className='text-base-content' />
|
||||
</button>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
className={clsx(
|
||||
window.innerWidth < 640 && 'dropdown-end',
|
||||
'dropdown-bottom flex justify-center',
|
||||
)}
|
||||
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3'}
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={20} className='fill-base-content' />}
|
||||
toggleButton={<MdOutlineMenu className='fill-base-content' />}
|
||||
>
|
||||
<BookMenu />
|
||||
</Dropdown>
|
||||
<div className='right-0 flex h-8 w-8 items-center justify-center'>
|
||||
<div className='right-0 hidden h-8 w-8 items-center justify-center sm:flex'>
|
||||
<button
|
||||
onClick={onTogglePin}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
className={clsx(
|
||||
'sidebar-pin-btn btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
|
||||
isPinned ? 'bg-base-300' : 'bg-base-300/65',
|
||||
)}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
{isPinned ? <MdPushPin size={iconSize14} /> : <MdOutlinePushPin size={iconSize14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SearchOptions from './SearchOptions';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const MINIMUM_SEARCH_TERM_LENGTH_DEFAULT = 2;
|
||||
const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1;
|
||||
@@ -43,6 +44,9 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
const isSearchPending = useRef(false);
|
||||
const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const iconSize12 = useResponsiveSize(12);
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearchTermChange(searchTerm);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -153,7 +157,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
<div className='relative p-2'>
|
||||
<div className='bg-base-100 flex h-8 items-center rounded-lg'>
|
||||
<div className='pl-3'>
|
||||
<FaSearch className='text-gray-500' />
|
||||
<FaSearch size={iconSize16} className='text-gray-500' />
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -166,11 +170,11 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
|
||||
/>
|
||||
|
||||
<div className='flex h-8 w-8 items-center rounded-r-lg bg-gray-300'>
|
||||
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg'
|
||||
toggleButton={<FaChevronDown size={12} className='text-gray-500' />}
|
||||
toggleButton={<FaChevronDown size={iconSize12} className='text-gray-500' />}
|
||||
>
|
||||
<SearchOptions
|
||||
searchConfig={searchConfig}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { BookSearchConfig } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface SearchOptionsProps {
|
||||
searchConfig: BookSearchConfig;
|
||||
@@ -9,100 +10,68 @@ interface SearchOptionsProps {
|
||||
setIsDropdownOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
interface OptionProps {
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const Option: React.FC<OptionProps> = ({ label, isActive, onClick }) => (
|
||||
<button
|
||||
className='hover:bg-base-300 flex w-full items-center justify-between rounded-md p-2'
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: `${useDefaultIconSize()}px` }}>
|
||||
{isActive && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{label}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
searchConfig,
|
||||
onSearchConfigChanged,
|
||||
setIsDropdownOpen,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const handleSetScope = () => {
|
||||
onSearchConfigChanged({
|
||||
...searchConfig,
|
||||
scope: searchConfig.scope === 'book' ? 'section' : 'book',
|
||||
});
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handleSetMatchCase = () => {
|
||||
onSearchConfigChanged({ ...searchConfig, matchCase: !searchConfig.matchCase });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handleSetMatchWholeWords = () => {
|
||||
onSearchConfigChanged({ ...searchConfig, matchWholeWords: !searchConfig.matchWholeWords });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handleSetMatchDiacritics = () => {
|
||||
onSearchConfigChanged({ ...searchConfig, matchDiacritics: !searchConfig.matchDiacritics });
|
||||
const updateConfig = (key: keyof BookSearchConfig, value: boolean | string) => {
|
||||
onSearchConfigChanged({ ...searchConfig, [key]: value });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='book-menu dropdown-content dropdown-center z-20 mt-3 w-56 border bg-white shadow-2xl'
|
||||
className='book-menu dropdown-content dropdown-center border-base-200 z-20 mt-3 w-56 border shadow-2xl'
|
||||
>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetScope}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.scope === 'book' && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Book')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetScope}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.scope === 'section' && (
|
||||
<MdCheck size={20} className='text-base-content' />
|
||||
)}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Chapter')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<hr className='my-1' />
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetMatchCase}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchCase && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Match Case')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetMatchWholeWords}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchWholeWords && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Match Whole Words')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleSetMatchDiacritics}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchDiacritics && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{_('Match Diacritics')}</span>
|
||||
</div>
|
||||
</button>
|
||||
<Option
|
||||
label={_('Book')}
|
||||
isActive={searchConfig.scope === 'book'}
|
||||
onClick={() => updateConfig('scope', 'book')}
|
||||
/>
|
||||
<Option
|
||||
label={_('Chapter')}
|
||||
isActive={searchConfig.scope === 'section'}
|
||||
onClick={() => updateConfig('scope', 'section')}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
<Option
|
||||
label={_('Match Case')}
|
||||
isActive={searchConfig.matchCase}
|
||||
onClick={() => updateConfig('matchCase', !searchConfig.matchCase)}
|
||||
/>
|
||||
<Option
|
||||
label={_('Match Whole Words')}
|
||||
isActive={searchConfig.matchWholeWords}
|
||||
onClick={() => updateConfig('matchWholeWords', !searchConfig.matchWholeWords)}
|
||||
/>
|
||||
<Option
|
||||
label={_('Match Diacritics')}
|
||||
isActive={searchConfig.matchDiacritics}
|
||||
onClick={() => updateConfig('matchDiacritics', !searchConfig.matchDiacritics)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { BookSearchResult } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
import BookCard from './BookCard';
|
||||
@@ -22,6 +24,7 @@ const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
const SideBar: React.FC<{
|
||||
onGoToLibrary: () => void;
|
||||
}> = ({ onGoToLibrary }) => {
|
||||
const { updateAppTheme } = useTheme();
|
||||
const { settings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
@@ -38,7 +41,7 @@ const SideBar: React.FC<{
|
||||
handleSideBarTogglePin,
|
||||
} = useSidebar(
|
||||
settings.globalReadSettings.sideBarWidth,
|
||||
settings.globalReadSettings.isSideBarPinned,
|
||||
window.innerWidth >= 640 ? settings.globalReadSettings.isSideBarPinned : false,
|
||||
);
|
||||
|
||||
const onSearchEvent = async (event: CustomEvent) => {
|
||||
@@ -48,10 +51,29 @@ const SideBar: React.FC<{
|
||||
setSearchTerm(term);
|
||||
};
|
||||
|
||||
const onNavigateEvent = async () => {
|
||||
const pinButton = document.querySelector('.sidebar-pin-btn');
|
||||
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
|
||||
if (isPinButtonHidden) {
|
||||
setSideBarVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSideBarVisible) {
|
||||
updateAppTheme('base-200');
|
||||
} else {
|
||||
updateAppTheme('base-100');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSideBarVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('search', onSearchEvent);
|
||||
eventDispatcher.on('navigate', onNavigateEvent);
|
||||
return () => {
|
||||
eventDispatcher.off('search', onSearchEvent);
|
||||
eventDispatcher.off('navigate', onNavigateEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -79,6 +101,7 @@ const SideBar: React.FC<{
|
||||
useShortcuts({ onToggleSearchBar: handleToggleSearchBar }, [sideBarBookKey]);
|
||||
|
||||
const handleSearchResultClick = (cfi: string) => {
|
||||
onNavigateEvent();
|
||||
getView(sideBarBookKey)?.goTo(cfi);
|
||||
};
|
||||
|
||||
@@ -94,8 +117,8 @@ const SideBar: React.FC<{
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 flex h-full min-w-60 flex-col',
|
||||
'rounded-window-top-left rounded-window-bottom-left select-none',
|
||||
'sidebar-container bg-base-200 z-20 flex h-full min-w-60 select-none flex-col',
|
||||
isTauriAppPlatform() && 'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isSideBarPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
@@ -104,11 +127,20 @@ const SideBar: React.FC<{
|
||||
position: isSideBarPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
@media (max-width: 640px) {
|
||||
.sidebar-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className='flex-shrink-0'>
|
||||
<SidebarHeader
|
||||
isPinned={isSideBarPinned}
|
||||
isSearchBarVisible={isSearchBarVisible}
|
||||
onGoToLibrary={onGoToLibrary}
|
||||
onClose={() => setSideBarVisible(false)}
|
||||
onTogglePin={handleSideBarTogglePin}
|
||||
onToggleSearchBar={handleToggleSearchBar}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { TOCItem } from '@/libs/document';
|
||||
@@ -5,7 +6,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { findParentPath } from '@/utils/toc';
|
||||
import { getContentMd5 } from '@/utils/misc';
|
||||
import clsx from 'clsx';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
const createExpanderIcon = (isExpanded: boolean) => {
|
||||
return (
|
||||
@@ -43,6 +44,7 @@ const TOCItemView: React.FC<{
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
eventDispatcher.dispatch('navigate', { bookKey, href: item.href });
|
||||
if (item.href) {
|
||||
getView(bookKey)?.goTo(item.href);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { MdToc, MdEditNote, MdBookmarkBorder } from 'react-icons/md';
|
||||
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { isPWA } from '@/services/environment';
|
||||
|
||||
const TabNavigation: React.FC<{
|
||||
activeTab: string;
|
||||
@@ -13,42 +14,46 @@ const TabNavigation: React.FC<{
|
||||
const tabs = ['toc', 'annotations', 'bookmarks'];
|
||||
|
||||
return (
|
||||
<div className="bottom-tab border-base-300/50 flex w-full border-t relative">
|
||||
<div
|
||||
className={clsx(
|
||||
'bottom-tab border-base-300/50 relative flex w-full border-t',
|
||||
isPWA() ? 'bottom-[calc(env(safe-area-inset-bottom)_/_2)]' : '',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute bottom-0 left-0 h-full w-1/3 bg-base-300 -z-10',
|
||||
'transition-transform duration-300 transform',
|
||||
'bg-base-300 absolute bottom-1.5 left-1 -z-10 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
|
||||
'transform transition-transform duration-300',
|
||||
activeTab === 'toc' && 'translate-x-0',
|
||||
activeTab === 'annotations' && 'translate-x-full',
|
||||
activeTab === 'bookmarks' && 'translate-x-[200%]',
|
||||
activeTab === 'annotations' && 'translate-x-[calc(100%+8px)]',
|
||||
activeTab === 'bookmarks' && 'translate-x-[calc(200%+16px)]',
|
||||
)}
|
||||
/>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab}
|
||||
className='tooltip z-50 tooltip-top m-1.5 flex-1 rounded-md p-2 cursor-pointer'
|
||||
data-tip={tab === 'toc' ? _('Table of Contents') : tab === 'annotations' ? _('Annotate') : _('Bookmark')}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'',
|
||||
)}
|
||||
onClick={() => onTabChange(tab)}
|
||||
>
|
||||
{
|
||||
tab === 'toc' ? (
|
||||
<MdToc size={20} className='mx-auto' />
|
||||
) : tab === 'annotations' ? (
|
||||
<MdEditNote size={20} className='mx-auto' />
|
||||
) : (
|
||||
<MdBookmarkBorder size={20} className='mx-auto' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
key={tab}
|
||||
className='lg:tooltip lg:tooltip-top z-50 m-1.5 flex-1 cursor-pointer rounded-md p-2'
|
||||
data-tip={
|
||||
tab === 'toc'
|
||||
? _('Table of Contents')
|
||||
: tab === 'annotations'
|
||||
? _('Annotate')
|
||||
: _('Bookmark')
|
||||
}
|
||||
>
|
||||
<div className={clsx('')} onClick={() => onTabChange(tab)}>
|
||||
{tab === 'toc' ? (
|
||||
<MdToc className='mx-auto' />
|
||||
) : tab === 'annotations' ? (
|
||||
<MdEditNote className='mx-auto' />
|
||||
) : (
|
||||
<MdBookmarkBorder className='mx-auto' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigation;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { TTSController } from '@/services/tts/TTSController';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { TTSController, SILENCE_DATA } from '@/services/tts';
|
||||
import { getPopupPosition, Position } from '@/utils/sel';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { parseSSMLLang } from '@/utils/ssml';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { throttle } from '@/utils/ui';
|
||||
import { isPWA } from '@/services/environment';
|
||||
import Popup from '@/components/Popup';
|
||||
import TTSPanel from './TTSPanel';
|
||||
import TTSIcon from './TTSIcon';
|
||||
@@ -15,6 +21,7 @@ const POPUP_PADDING = 10;
|
||||
|
||||
const TTSControl = () => {
|
||||
const _ = useTranslation();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const [bookKey, setBookKey] = useState<string>('');
|
||||
const [ttsLang, setTtsLang] = useState<string>('en');
|
||||
@@ -25,9 +32,23 @@ const TTSControl = () => {
|
||||
const [panelPosition, setPanelPosition] = useState<Position>();
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
|
||||
const popupWidth = useResponsiveSize(POPUP_WIDTH);
|
||||
const popupHeight = useResponsiveSize(POPUP_HEIGHT);
|
||||
const popupPadding = useResponsiveSize(POPUP_PADDING);
|
||||
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const ttsControllerRef = useRef<TTSController | null>(null);
|
||||
|
||||
// this enables WebAudio to play even when the mute toggle switch is ON
|
||||
const unblockAudio = () => {
|
||||
const audio = document.createElement('audio');
|
||||
audio.setAttribute('x-webkit-airplay', 'deny');
|
||||
audio.preload = 'auto';
|
||||
audio.loop = true;
|
||||
audio.src = SILENCE_DATA;
|
||||
audio.play();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (ttsControllerRef.current) {
|
||||
@@ -40,9 +61,11 @@ const TTSControl = () => {
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('tts-speak', handleTTSSpeak);
|
||||
eventDispatcher.on('tts-stop', handleTTSStop);
|
||||
eventDispatcher.onSync('tts-is-speaking', handleQueryIsSpeaking);
|
||||
return () => {
|
||||
eventDispatcher.off('tts-speak', handleTTSSpeak);
|
||||
eventDispatcher.off('tts-stop', handleTTSStop);
|
||||
eventDispatcher.offSync('tts-is-speaking', handleQueryIsSpeaking);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -51,7 +74,15 @@ const TTSControl = () => {
|
||||
const { bookKey, range } = event.detail;
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
if (!view || !viewSettings) return;
|
||||
const bookData = getBookData(bookKey);
|
||||
if (!view || !viewSettings || !bookData) return;
|
||||
if (bookData.book?.format === 'PDF') {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('TTS not supported for PDF'),
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setBookKey(bookKey);
|
||||
|
||||
@@ -62,6 +93,9 @@ const TTSControl = () => {
|
||||
setShowIndicator(true);
|
||||
|
||||
try {
|
||||
if (getOSPlatform() === 'ios') {
|
||||
unblockAudio();
|
||||
}
|
||||
const ttsController = new TTSController(view);
|
||||
await ttsController.init();
|
||||
await ttsController.initViewTTS();
|
||||
@@ -86,21 +120,27 @@ const TTSControl = () => {
|
||||
};
|
||||
|
||||
const handleTTSStop = async (event: CustomEvent) => {
|
||||
const { bookKey } = event.detail;
|
||||
if (bookKey === bookKey) {
|
||||
const { bookKey: stopBookKey } = event.detail;
|
||||
if (bookKey === stopBookKey) {
|
||||
handleStop();
|
||||
}
|
||||
};
|
||||
|
||||
const handleQueryIsSpeaking = () => {
|
||||
return !!ttsControllerRef.current;
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (!ttsController) return;
|
||||
|
||||
if (isPlaying) {
|
||||
await ttsController.pause();
|
||||
setIsPlaying(false);
|
||||
setIsPaused(true);
|
||||
await ttsController.pause();
|
||||
} else if (isPaused) {
|
||||
setIsPlaying(true);
|
||||
setIsPaused(false);
|
||||
// start for forward/backward/setvoice-paused
|
||||
// set rate don't pause the tts
|
||||
if (ttsController.state === 'paused') {
|
||||
@@ -108,8 +148,6 @@ const TTSControl = () => {
|
||||
} else {
|
||||
await ttsController.start();
|
||||
}
|
||||
setIsPlaying(true);
|
||||
setIsPaused(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -132,6 +170,7 @@ const TTSControl = () => {
|
||||
if (ttsController) {
|
||||
await ttsController.stop();
|
||||
ttsControllerRef.current = null;
|
||||
getView(bookKey)?.deselect();
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
setShowIndicator(false);
|
||||
@@ -139,31 +178,39 @@ const TTSControl = () => {
|
||||
};
|
||||
|
||||
// rate range: 0.5 - 3, 1.0 is normal speed
|
||||
const handleSetRate = async (rate: number) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
if (ttsController.state === 'playing') {
|
||||
await ttsController.pause();
|
||||
await ttsController.setRate(rate);
|
||||
await ttsController.start();
|
||||
} else {
|
||||
await ttsController.setRate(rate);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleSetRate = useCallback(
|
||||
throttle(async (rate: number) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
if (ttsController.state === 'playing') {
|
||||
await ttsController.stop();
|
||||
await ttsController.setRate(rate);
|
||||
await ttsController.start();
|
||||
} else {
|
||||
await ttsController.setRate(rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, 3000),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSetVoice = async (voice: string) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
if (ttsController.state === 'playing') {
|
||||
await ttsController.pause();
|
||||
await ttsController.setVoice(voice);
|
||||
await ttsController.start();
|
||||
} else {
|
||||
await ttsController.setVoice(voice);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleSetVoice = useCallback(
|
||||
throttle(async (voice: string) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
if (ttsController.state === 'playing') {
|
||||
await ttsController.stop();
|
||||
await ttsController.setVoice(voice);
|
||||
await ttsController.start();
|
||||
} else {
|
||||
await ttsController.setVoice(voice);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, 3000),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleGetVoices = async (lang: string) => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
@@ -194,9 +241,9 @@ const TTSControl = () => {
|
||||
const popupPos = getPopupPosition(
|
||||
trianglePos,
|
||||
windowRect,
|
||||
POPUP_WIDTH,
|
||||
POPUP_HEIGHT,
|
||||
POPUP_PADDING,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
popupPadding,
|
||||
);
|
||||
|
||||
setPanelPosition(popupPos);
|
||||
@@ -223,14 +270,20 @@ const TTSControl = () => {
|
||||
/>
|
||||
)}
|
||||
{showIndicator && (
|
||||
<div ref={iconRef} className='absolute bottom-12 right-6 h-12 w-12'>
|
||||
<div
|
||||
ref={iconRef}
|
||||
className={clsx(
|
||||
'absolute right-6 h-12 w-12',
|
||||
isPWA() ? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]' : 'bottom-12',
|
||||
)}
|
||||
>
|
||||
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
|
||||
</div>
|
||||
)}
|
||||
{showPanel && panelPosition && trianglePosition && (
|
||||
<Popup
|
||||
width={POPUP_WIDTH}
|
||||
height={POPUP_HEIGHT}
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
position={panelPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
className='bg-base-200 absolute flex shadow-lg'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdStop } from
|
||||
import { RiVoiceAiFill } from 'react-icons/ri';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { TTSVoice } from '@/services/tts';
|
||||
import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
type TTSPanelProps = {
|
||||
bookKey: string;
|
||||
@@ -42,6 +43,10 @@ const TTSPanel = ({
|
||||
const [rate, setRate] = useState(viewSettings?.ttsRate ?? 1.0);
|
||||
const [selectedVoice, setSelectedVoice] = useState(viewSettings?.ttsVoice ?? '');
|
||||
|
||||
const defaultIconSize = useDefaultIconSize();
|
||||
const iconSize32 = useResponsiveSize(32);
|
||||
const iconSize48 = useResponsiveSize(48);
|
||||
|
||||
const handleSetRate = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
let newRate = parseFloat(e.target.value);
|
||||
newRate = Math.max(0.2, Math.min(3.0, newRate));
|
||||
@@ -108,30 +113,30 @@ const TTSPanel = ({
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<button onClick={onBackward} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdFastRewind size={32} />
|
||||
<MdFastRewind size={iconSize32} />
|
||||
</button>
|
||||
<button onClick={onTogglePlay} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
{isPlaying ? (
|
||||
<MdPauseCircle size={48} className='fill-primary' />
|
||||
<MdPauseCircle size={iconSize48} className='fill-primary' />
|
||||
) : (
|
||||
<MdPlayCircle size={48} className='fill-primary' />
|
||||
<MdPlayCircle size={iconSize48} className='fill-primary' />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onForward} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdFastForward size={32} />
|
||||
<MdFastForward size={iconSize32} />
|
||||
</button>
|
||||
<button onClick={onStop} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdStop size={32} />
|
||||
<MdStop size={iconSize32} />
|
||||
</button>
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button tabIndex={0} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<RiVoiceAiFill size={32} />
|
||||
<RiVoiceAiFill size={iconSize32} />
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute right-0 z-[1] shadow',
|
||||
'mt-4 max-h-96 w-[250px] overflow-y-scroll',
|
||||
'dropdown-content bgcolor-base-200 no-triangle menu menu-vertical rounded-box absolute right-0 z-[1] shadow',
|
||||
'mt-4 inline max-h-96 w-[250px] overflow-y-scroll',
|
||||
)}
|
||||
>
|
||||
{voices.map((voice, index) => (
|
||||
@@ -140,12 +145,10 @@ const TTSPanel = ({
|
||||
onClick={() => !voice.disabled && handleSelectVoice(voice.id)}
|
||||
>
|
||||
<div className='flex items-center px-2'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selectedVoice === voice.id && (
|
||||
<MdCheck size={20} className='text-base-content' />
|
||||
)}
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
{selectedVoice === voice.id && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span className={clsx('text-sm', voice.disabled && 'text-gray-400')}>
|
||||
<span className={clsx('text-base sm:text-sm', voice.disabled && 'text-gray-400')}>
|
||||
{voice.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -29,16 +29,9 @@ export const useClickEvent = (
|
||||
if (!consumed) {
|
||||
const centerStartX = rect.left + rect.width * 0.375;
|
||||
const centerEndX = rect.left + rect.width * 0.625;
|
||||
const centerStartY = rect.top + rect.height * 0.375;
|
||||
const centerEndY = rect.top + rect.height * 0.625;
|
||||
if (
|
||||
screenX >= centerStartX &&
|
||||
screenX <= centerEndX &&
|
||||
screenY >= centerStartY &&
|
||||
screenY <= centerEndY
|
||||
) {
|
||||
if (screenX >= centerStartX && screenX <= centerEndX) {
|
||||
// toggle visibility of the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? '' : bookKey);
|
||||
setHoveredBookKey(hoveredBookKey ? null : bookKey);
|
||||
} else if (screenX >= rect.left + rect.width / 2) {
|
||||
viewRef.current?.goRight();
|
||||
} else if (screenX < rect.left + rect.width / 2) {
|
||||
@@ -53,6 +46,12 @@ export const useClickEvent = (
|
||||
} else if (deltaY < 0) {
|
||||
viewRef.current?.prev(1);
|
||||
}
|
||||
} else if (msg.data.type === 'iframe-mouseup') {
|
||||
if (msg.data.button === 3) {
|
||||
viewRef.current?.history.back();
|
||||
} else if (msg.data.button === 4) {
|
||||
viewRef.current?.history.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -80,3 +79,92 @@ export const useClickEvent = (
|
||||
handleTurnPage,
|
||||
};
|
||||
};
|
||||
|
||||
interface IframeTouch {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
}
|
||||
|
||||
interface IframeTouchEvent {
|
||||
targetTouches: IframeTouch[];
|
||||
}
|
||||
|
||||
export const useTouchEvent = (
|
||||
bookKey: string,
|
||||
viewRef: React.MutableRefObject<FoliateView | null>,
|
||||
) => {
|
||||
const { hoveredBookKey, setHoveredBookKey, getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
let touchStart: IframeTouch | null = null;
|
||||
let touchEnd: IframeTouch | null = null;
|
||||
|
||||
const onTouchStart = (e: IframeTouchEvent) => {
|
||||
touchEnd = null;
|
||||
const touch = e.targetTouches[0];
|
||||
if (!touch) return;
|
||||
touchStart = touch;
|
||||
};
|
||||
|
||||
const onTouchMove = (e: IframeTouchEvent) => {
|
||||
if (!touchStart) return;
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
touchEnd = touch;
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchEnd = (e: IframeTouchEvent) => {
|
||||
if (!touchStart) return;
|
||||
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
touchEnd = touch;
|
||||
}
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
if (touchEnd) {
|
||||
const deltaY = touchEnd.screenY - touchStart.screenY;
|
||||
const deltaX = touchEnd.screenX - touchStart.screenX;
|
||||
// also check for deltaX to prevent swipe page turn from triggering the toggle
|
||||
if (
|
||||
deltaY < -10 &&
|
||||
Math.abs(deltaY) > Math.abs(deltaX) &&
|
||||
Math.abs(deltaX) < windowWidth * 0.3
|
||||
) {
|
||||
// swipe up to toggle the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? null : bookKey);
|
||||
}
|
||||
}
|
||||
|
||||
touchStart = null;
|
||||
touchEnd = null;
|
||||
};
|
||||
|
||||
const handleTouch = (msg: MessageEvent) => {
|
||||
if (msg.data && msg.data.bookKey === bookKey) {
|
||||
if (msg.data.type === 'iframe-touchstart') {
|
||||
onTouchStart(msg.data);
|
||||
} else if (msg.data.type === 'iframe-touchmove') {
|
||||
onTouchMove(msg.data);
|
||||
} else if (msg.data.type === 'iframe-touchend') {
|
||||
onTouchEnd(msg.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// swipe touch is not compatible with scrolled mode, so only enable it in page mode
|
||||
if (!viewSettings!.scrolled) {
|
||||
window.addEventListener('message', handleTouch);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleTouch);
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hoveredBookKey, viewRef]);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const useProgressSync = (bookKey: string) => {
|
||||
const compressedConfig = JSON.parse(
|
||||
serializeConfig(newConfig, settings.globalViewSettings, DEFAULT_BOOK_SEARCH_CONFIG),
|
||||
);
|
||||
delete compressedConfig.booknotes;
|
||||
syncConfigs([compressedConfig], bookHash, 'push');
|
||||
};
|
||||
const pullConfig = (bookKey: string) => {
|
||||
@@ -40,18 +41,20 @@ export const useProgressSync = (bookKey: string) => {
|
||||
if (!configSynced.current) {
|
||||
pullConfig(bookKey);
|
||||
} else {
|
||||
pushConfig(bookKey, config);
|
||||
if (config && config.progress && config.progress[0] > 0) {
|
||||
pushConfig(bookKey, config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
pullConfig(bookKey);
|
||||
return () => {
|
||||
if (configSynced.current) {
|
||||
pushConfig(bookKey, config);
|
||||
}
|
||||
};
|
||||
// return () => {
|
||||
// if (configSynced.current) {
|
||||
// pushConfig(bookKey, config);
|
||||
// }
|
||||
// };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -4,17 +4,20 @@ import { useEffect } from 'react';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { hasUpdater } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Reader from './components/Reader';
|
||||
|
||||
export default function Page() {
|
||||
const _ = useTranslation();
|
||||
useTheme();
|
||||
useEffect(() => {
|
||||
const doAppUpdates = async () => {
|
||||
if (hasUpdater()) {
|
||||
await checkForAppUpdates();
|
||||
await checkForAppUpdates(_);
|
||||
}
|
||||
};
|
||||
doAppUpdates();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <Reader />;
|
||||
|
||||
@@ -30,6 +30,7 @@ export const handleMousedown = (bookKey: string, event: MouseEvent) => {
|
||||
{
|
||||
type: 'iframe-mousedown',
|
||||
bookKey,
|
||||
button: event.button,
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
clientX: event.clientX,
|
||||
@@ -42,10 +43,15 @@ export const handleMousedown = (bookKey: string, event: MouseEvent) => {
|
||||
};
|
||||
|
||||
export const handleMouseup = (bookKey: string, event: MouseEvent) => {
|
||||
// we will handle mouse back and forward buttons ourselves
|
||||
if ([3, 4].includes(event.button)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'iframe-mouseup',
|
||||
bookKey,
|
||||
button: event.button,
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
clientX: event.clientX,
|
||||
@@ -130,3 +136,36 @@ export const handleClick = (bookKey: string, event: MouseEvent) => {
|
||||
}
|
||||
}, doubleClickThreshold);
|
||||
};
|
||||
|
||||
const handleTouchEv = (bookKey: string, event: TouchEvent, type: string) => {
|
||||
const touch = event.targetTouches[0];
|
||||
const touches = [];
|
||||
if (touch) {
|
||||
touches.push({
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
screenX: touch.screenX,
|
||||
screenY: touch.screenY,
|
||||
});
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
type: type,
|
||||
bookKey,
|
||||
targetTouches: touches,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
|
||||
export const handleTouchStart = (bookKey: string, event: TouchEvent) => {
|
||||
handleTouchEv(bookKey, event, 'iframe-touchstart');
|
||||
};
|
||||
|
||||
export const handleTouchMove = (bookKey: string, event: TouchEvent) => {
|
||||
handleTouchEv(bookKey, event, 'iframe-touchmove');
|
||||
};
|
||||
|
||||
export const handleTouchEnd = (bookKey: string, event: TouchEvent) => {
|
||||
handleTouchEv(bookKey, event, 'iframe-touchend');
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import packageJson from '../../package.json';
|
||||
import WindowButtons from './WindowButtons';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { hasUpdater } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
export const setAboutDialogVisible = (visible: boolean) => {
|
||||
const dialog = document.getElementById('about_window');
|
||||
@@ -15,61 +17,77 @@ export const setAboutDialogVisible = (visible: boolean) => {
|
||||
|
||||
export const AboutWindow = () => {
|
||||
const _ = useTranslation();
|
||||
const [isUpdated, setIsUpdated] = React.useState(false);
|
||||
|
||||
const handleCheckUpdate = async () => {
|
||||
const update = await checkForAppUpdates(_);
|
||||
if (!update) {
|
||||
setIsUpdated(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog id='about_window' className='modal'>
|
||||
<form method='dialog' className='modal-box w-96 max-w-lg p-4'>
|
||||
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center p-2'>
|
||||
<WindowButtons
|
||||
className='window-buttons absolute right-0 flex h-full items-center'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={() => setAboutDialogVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col items-center px-8'>
|
||||
<div className='mb-4'>
|
||||
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
|
||||
<>
|
||||
<Dialog
|
||||
id='about_window'
|
||||
isOpen={false}
|
||||
title={_('About Readest')}
|
||||
onClose={() => setAboutDialogVisible(false)}
|
||||
boxClassName='sm:!w-96'
|
||||
>
|
||||
<div className='about-content flex h-full flex-col items-center justify-center'>
|
||||
<div className='flex flex-col items-center px-8'>
|
||||
<div className='mb-4'>
|
||||
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
|
||||
</div>
|
||||
<h2 className='text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{_('Version {{version}}', { version: packageJson.version })}
|
||||
</p>
|
||||
{hasUpdater() && !isUpdated && (
|
||||
<span className='badge badge-primary mt-2 cursor-pointer' onClick={handleCheckUpdate}>
|
||||
{_('Check update')}
|
||||
</span>
|
||||
)}
|
||||
{isUpdated && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
|
||||
)}
|
||||
</div>
|
||||
<h2 className='text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-sm'>Bilingify LLC</p>
|
||||
<span className='badge badge-primary mt-2'>
|
||||
{_('Version {{version}}', { version: packageJson.version })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='divider'></div>
|
||||
<div className='divider py-12 sm:py-2'></div>
|
||||
|
||||
<div className='flex flex-col items-center px-4 text-center'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
This software is licensed under the{' '}
|
||||
<a
|
||||
href='https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GNU Affero General Public License v3.0
|
||||
</a>
|
||||
. You are free to use, modify, and distribute this software under the terms of the AGPL
|
||||
v3 license. Please see the license for more details.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<a
|
||||
href='https://github.com/readest/readest'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className='flex flex-col items-center px-4 text-center'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
This software is licensed under the{' '}
|
||||
<a
|
||||
href='https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GNU Affero General Public License v3.0
|
||||
</a>
|
||||
. You are free to use, modify, and distribute this software under the terms of the
|
||||
AGPL v3 license. Please see the license for more details.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<a
|
||||
href='https://github.com/readest/readest'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const Alert: React.FC<{
|
||||
<div
|
||||
role='alert'
|
||||
className={clsx(
|
||||
'alert fixed bottom-4 left-1/2 flex -translate-x-1/2 transform items-center justify-between',
|
||||
'alert fixed bottom-4 left-1/2 z-[100] flex -translate-x-1/2 transform items-center justify-between',
|
||||
'rounded-lg bg-gray-100 p-4 shadow-2xl',
|
||||
'w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] lg:max-w-[40vw] xl:max-w-[40vw]',
|
||||
)}
|
||||
|
||||
@@ -3,13 +3,15 @@ import React, { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { formatDate, formatSubject } from '@/utils/book';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { formatDate, formatLanguage, formatPublisher, formatSubject } from '@/utils/book';
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from './Spinner';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
interface BookDetailModalProps {
|
||||
book: Book;
|
||||
@@ -20,24 +22,11 @@ interface BookDetailModalProps {
|
||||
const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
const _ = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
|
||||
const [bookMeta, setBookMeta] = useState<BookDoc['metadata'] | null>(null);
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
const { deleteBook } = useLibraryStore();
|
||||
|
||||
useEffect(() => {
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
||||
@@ -57,7 +46,15 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
const handleDelete = () => {
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
deleteBook(envConfig, book);
|
||||
handleClose();
|
||||
setShowDeleteAlert(false);
|
||||
};
|
||||
|
||||
if (!bookMeta)
|
||||
return (
|
||||
@@ -69,97 +66,132 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex w-full select-text items-center justify-center'>
|
||||
<div className='min-w-[480px] max-w-md'>
|
||||
<div className='fixed inset-0 bg-gray-800 bg-opacity-70' onClick={handleClose} />
|
||||
<>
|
||||
<Dialog
|
||||
title={_('Book Details')}
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
boxClassName='sm:min-w-[480px]'
|
||||
contentClassName='!p-6'
|
||||
>
|
||||
<div className='z-50 flex w-full select-text items-center justify-center'>
|
||||
<div className='relative w-full rounded-lg'>
|
||||
<div className='mb-10 flex h-40 items-start'>
|
||||
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
|
||||
<Image
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
fill={true}
|
||||
className='w-10 object-cover'
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
|
||||
'invisible',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'invisible absolute inset-0 flex items-center justify-center p-1',
|
||||
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-base-200 relative z-50 w-full rounded-lg p-6 shadow-xl'>
|
||||
<div className='absolute right-4 top-4 flex space-x-2'>
|
||||
<WindowButtons
|
||||
className='window-buttons flex'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mb-6 flex h-40 items-start'>
|
||||
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
|
||||
<Image
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
fill={true}
|
||||
className='w-10 object-cover'
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'invisible absolute inset-0 flex items-center justify-center p-1',
|
||||
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
|
||||
<div className='title-author flex h-40 flex-col justify-between'>
|
||||
<div>
|
||||
<p className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
|
||||
{book.title || _('Untitled')}
|
||||
</p>
|
||||
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
|
||||
</div>
|
||||
{window.innerWidth >= 400 && (
|
||||
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
|
||||
<button
|
||||
className='btn rounded-xl bg-red-600 px-4 text-white hover:bg-red-700'
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{_('Delete')}
|
||||
</button>
|
||||
<button className='btn btn-disabled bg-primary/25 hover:bg-primary/85 rounded-xl px-4 text-white'>
|
||||
{_('More Info')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='title-author flex h-40 max-w-[60%] flex-col justify-between pr-4'>
|
||||
<div>
|
||||
<h2 className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
|
||||
{book.title || _('Untitled')}
|
||||
</h2>
|
||||
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
|
||||
{window.innerWidth < 400 && (
|
||||
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
|
||||
<button
|
||||
className='btn rounded bg-red-600 text-white hover:bg-red-700'
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{_('Delete')}
|
||||
</button>
|
||||
<button className='btn btn-disabled bg-primary/25 hover:bg-primary/85 rounded px-4 text-white'>
|
||||
{_('More Info')}
|
||||
</button>
|
||||
</div>
|
||||
<button className='btn-disabled bg-primary/25 hover:bg-primary/85 w-36 rounded px-4 py-2 text-white'>
|
||||
{_('More Info')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='text-base-content mb-4'>
|
||||
<div className='mb-4 grid grid-cols-3 gap-4'>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Publisher:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{bookMeta.publisher || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Published:')}</span>
|
||||
<p className='text-neutral-content max-w-28 text-ellipsis text-sm'>
|
||||
{formatDate(bookMeta.published) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Updated:')}</span>
|
||||
<p className='text-neutral-content text-sm'>{formatDate(book.lastUpdated) || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-base-content my-4'>
|
||||
<div className='mb-4 grid grid-cols-2 gap-4 sm:grid-cols-3'>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Publisher:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatPublisher(bookMeta.publisher || '') || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Published:')}</span>
|
||||
<p className='text-neutral-content max-w-28 text-ellipsis text-sm'>
|
||||
{formatDate(bookMeta.published) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Updated:')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{formatDate(book.lastUpdated) || ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Language:')}</span>
|
||||
<p className='text-neutral-content text-sm'>{bookMeta.language || _('Unknown')}</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Identifier:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{bookMeta.identifier || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Subjects:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatSubject(bookMeta.subject) || _('Unknown')}
|
||||
</p>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Language:')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{formatLanguage(bookMeta.language) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Identifier:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{bookMeta.identifier || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Subjects:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatSubject(bookMeta.subject) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
{showDeleteAlert && (
|
||||
<Alert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete the selected books?')}
|
||||
onClickCancel={() => {
|
||||
setShowDeleteAlert(false);
|
||||
}}
|
||||
onClickConfirm={confirmDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,14 +20,18 @@ const Button: React.FC<ButtonProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('tooltip z-50', tooltip && `tooltip-${tooltipDirection}`, {
|
||||
'tooltip-hidden': !tooltip,
|
||||
})}
|
||||
className={clsx(
|
||||
'lg:tooltip z-50 h-8 min-h-8 w-8',
|
||||
tooltip && `lg:tooltip-${tooltipDirection}`,
|
||||
{
|
||||
'tooltip-hidden': !tooltip,
|
||||
},
|
||||
)}
|
||||
data-tip={tooltip}
|
||||
>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost mx-2 h-8 min-h-8 w-8 p-0',
|
||||
'btn btn-ghost h-8 min-h-8 w-8 p-0',
|
||||
disabled && 'btn-disabled !bg-transparent',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface DialogProps {
|
||||
id?: string;
|
||||
isOpen: boolean;
|
||||
children: ReactNode;
|
||||
header?: ReactNode;
|
||||
title?: string;
|
||||
className?: string;
|
||||
boxClassName?: string;
|
||||
contentClassName?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Dialog: React.FC<DialogProps> = ({
|
||||
id,
|
||||
isOpen,
|
||||
children,
|
||||
header,
|
||||
title,
|
||||
className,
|
||||
boxClassName,
|
||||
contentClassName,
|
||||
onClose,
|
||||
}) => {
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
id={id ?? 'dialog'}
|
||||
open={isOpen}
|
||||
className={clsx(
|
||||
'modal sm:min-w-90 z-50 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:rounded-2xl',
|
||||
'sm:h-[60%] sm:w-[65%] sm:max-w-[600px]',
|
||||
boxClassName,
|
||||
)}
|
||||
>
|
||||
<div className='dialog-header bg-base-100 sticky top-2 z-10 flex items-center justify-between px-4'>
|
||||
{header ? (
|
||||
header
|
||||
) : (
|
||||
<div className='flex h-11 w-full items-center justify-between'>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
onClick={onClose}
|
||||
className={
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew size={iconSize22} />
|
||||
</button>
|
||||
<div className='z-15 pointer-events-none absolute inset-0 flex h-11 items-center justify-center'>
|
||||
<span className='line-clamp-1 text-center font-bold'>{title ?? ''}</span>
|
||||
</div>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
onClick={onClose}
|
||||
className={
|
||||
'bg-base-300/65 btn btn-ghost btn-circle ml-auto hidden h-6 min-h-6 w-6 focus:outline-none sm:flex'
|
||||
}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1em'
|
||||
height='1em'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'text-base-content my-2 flex-grow overflow-y-auto px-[10%]',
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
@@ -3,14 +3,16 @@ import React, { useState, isValidElement, ReactElement } from 'react';
|
||||
|
||||
interface DropdownProps {
|
||||
className?: string;
|
||||
menuClassName?: string;
|
||||
buttonClassName?: string;
|
||||
toggleButton: React.ReactNode;
|
||||
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void }>;
|
||||
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void; menuClassName?: string }>;
|
||||
onToggle?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
className,
|
||||
menuClassName,
|
||||
buttonClassName,
|
||||
toggleButton,
|
||||
children,
|
||||
@@ -30,7 +32,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
};
|
||||
|
||||
const childrenWithToggle = isValidElement(children)
|
||||
? React.cloneElement(children, { setIsDropdownOpen })
|
||||
? React.cloneElement(children, { setIsDropdownOpen, menuClassName })
|
||||
: children;
|
||||
|
||||
return (
|
||||
@@ -42,7 +44,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onClick={toggleDropdown}
|
||||
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-200')}
|
||||
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-300/50')}
|
||||
>
|
||||
{toggleButton}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface MenuItemProps {
|
||||
label: string;
|
||||
@@ -22,20 +23,21 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
const iconSize = useDefaultIconSize();
|
||||
const menuButton = (
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-neutral text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
|
||||
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
|
||||
disabled && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
{!noIcon && <span style={{ minWidth: '20px' }}>{icon}</span>}
|
||||
{!noIcon && <span style={{ minWidth: `${iconSize}px` }}>{icon}</span>}
|
||||
<span className={clsx('ml-2 max-w-32 truncate', labelClass)}>{label}</span>
|
||||
</div>
|
||||
{shortcut && <span className='text-neutral-content text-sm'>{shortcut}</span>}
|
||||
{shortcut && <span className='text-neutral-content hidden text-sm sm:flex'>{shortcut}</span>}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -44,7 +46,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
<ul className='menu rounded-box m-0 p-0'>
|
||||
<li>
|
||||
<details>
|
||||
<summary className='hover:bg-neutral p-0 pr-3'>{menuButton}</summary>
|
||||
<summary className='hover:bg-base-300 p-0 pr-3'>{menuButton}</summary>
|
||||
{children}
|
||||
</details>
|
||||
</li>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { EnvProvider } from '@/context/EnvContext';
|
||||
import { CSPostHogProvider } from '@/context/PHContext';
|
||||
import { SyncProvider } from '@/context/SyncContext';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => (
|
||||
<CSPostHogProvider>
|
||||
<EnvProvider>
|
||||
<AuthProvider>
|
||||
<SyncProvider>{children}</SyncProvider>
|
||||
</AuthProvider>
|
||||
</EnvProvider>
|
||||
</CSPostHogProvider>
|
||||
);
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const iconSize = useDefaultIconSize();
|
||||
return (
|
||||
<CSPostHogProvider>
|
||||
<EnvProvider>
|
||||
<AuthProvider>
|
||||
<IconContext.Provider value={{ size: `${iconSize}px` }}>
|
||||
<SyncProvider>{children}</SyncProvider>
|
||||
</IconContext.Provider>
|
||||
</AuthProvider>
|
||||
</EnvProvider>
|
||||
</CSPostHogProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Providers;
|
||||
|
||||
@@ -2,10 +2,11 @@ import { check } from '@tauri-apps/plugin-updater';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { CHECK_UPDATE_INTERVAL_SEC } from '@/services/constants';
|
||||
import { TranslationFunc } from '@/hooks/useTranslation';
|
||||
|
||||
const LAST_CHECK_KEY = 'lastAppUpdateCheck';
|
||||
|
||||
export const checkForAppUpdates = async () => {
|
||||
export const checkForAppUpdates = async (_: TranslationFunc) => {
|
||||
const lastCheck = localStorage.getItem(LAST_CHECK_KEY);
|
||||
const now = Date.now();
|
||||
if (lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000) return;
|
||||
@@ -20,10 +21,10 @@ export const checkForAppUpdates = async () => {
|
||||
Release notes: ${update.body}
|
||||
`,
|
||||
{
|
||||
title: 'Update Now!',
|
||||
title: _('Update Now!'),
|
||||
kind: 'info',
|
||||
okLabel: 'Update',
|
||||
cancelLabel: 'Cancel',
|
||||
okLabel: _('Update'),
|
||||
cancelLabel: _('Cancel'),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,6 +32,7 @@ export const checkForAppUpdates = async () => {
|
||||
console.log(`found update ${update.version} from ${update.date} with notes ${update.body}`);
|
||||
let downloaded = 0;
|
||||
let contentLength = 0;
|
||||
let lastLogged = 0;
|
||||
await update.downloadAndInstall((event) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
@@ -39,7 +41,10 @@ export const checkForAppUpdates = async () => {
|
||||
break;
|
||||
case 'Progress':
|
||||
downloaded += event.data.chunkLength;
|
||||
console.log(`downloaded ${downloaded} from ${contentLength}`);
|
||||
if (downloaded - lastLogged >= 1 * 1024 * 1024) {
|
||||
console.log(`downloaded ${downloaded} bytes from ${contentLength}`);
|
||||
lastLogged = downloaded;
|
||||
}
|
||||
break;
|
||||
case 'Finished':
|
||||
console.log('download finished');
|
||||
@@ -50,4 +55,5 @@ export const checkForAppUpdates = async () => {
|
||||
await relaunch();
|
||||
}
|
||||
}
|
||||
return update;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
// use desktop size as base size
|
||||
export const useResponsiveSize = (baseSize: number) => {
|
||||
const isPhone = useMediaQuery({ maxWidth: 480 });
|
||||
const isTablet = useMediaQuery({ minWidth: 481, maxWidth: 1024 });
|
||||
if (isPhone) return baseSize * 1.25;
|
||||
if (isTablet) return baseSize * 1.15;
|
||||
return baseSize;
|
||||
};
|
||||
|
||||
export const useDefaultIconSize = () => {
|
||||
return useResponsiveSize(20);
|
||||
};
|
||||
@@ -30,6 +30,8 @@ const computeMaxTimestamp = (records: BookDataRecord[]): number => {
|
||||
return maxTime;
|
||||
};
|
||||
|
||||
const SEVEN_DAYS_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export function useSync(bookKey?: string) {
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
@@ -40,14 +42,19 @@ export function useSync(bookKey?: string) {
|
||||
const [syncingNotes, setSyncingNotes] = useState(false);
|
||||
|
||||
const [syncError, setSyncError] = useState<string | null>(null);
|
||||
|
||||
const lastSyncedBooksAt = settings.lastSyncedAtBooks ?? 0;
|
||||
const lastSyncedConfigsAt = config?.lastSyncedAtConfig ?? settings.lastSyncedAtConfigs ?? 0;
|
||||
const lastSyncedNotesAt = config?.lastSyncedAtNotes ?? settings.lastSyncedAtNotes ?? 0;
|
||||
|
||||
const [lastSyncedAtBooks, setLastSyncedAtBooks] = useState<number>(
|
||||
settings.lastSyncedAtBooks ?? 0,
|
||||
lastSyncedBooksAt > 0 ? lastSyncedBooksAt - SEVEN_DAYS_IN_MS : 0,
|
||||
);
|
||||
const [lastSyncedAtConfigs, setLastSyncedAtConfigs] = useState<number>(
|
||||
config?.lastSyncedAtConfig ?? settings.lastSyncedAtConfigs ?? 0,
|
||||
lastSyncedConfigsAt > 0 ? lastSyncedConfigsAt - SEVEN_DAYS_IN_MS : 0,
|
||||
);
|
||||
const [lastSyncedAtNotes, setLastSyncedAtNotes] = useState<number>(
|
||||
config?.lastSyncedAtNotes ?? settings.lastSyncedAtNotes ?? 0,
|
||||
lastSyncedNotesAt > 0 ? lastSyncedNotesAt - SEVEN_DAYS_IN_MS : 0,
|
||||
);
|
||||
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ThemeCode } from '@/utils/style';
|
||||
import { themes } from '@/styles/themes';
|
||||
import { themes, Palette } from '@/styles/themes';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
|
||||
export type ThemeMode = 'auto' | 'light' | 'dark';
|
||||
|
||||
@@ -45,6 +46,7 @@ export const useTheme = () => {
|
||||
bg: defaultPalette['base-100'],
|
||||
fg: defaultPalette['base-content'],
|
||||
primary: defaultPalette.primary,
|
||||
palette: defaultPalette,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -76,16 +78,24 @@ export const useTheme = () => {
|
||||
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
|
||||
const theme = themes.find((t) => t.name === themeColor);
|
||||
const palette = isDarkMode ? theme!.colors.dark : theme!.colors.light;
|
||||
setThemeCode({
|
||||
bg: palette['base-100'],
|
||||
fg: palette['base-content'],
|
||||
primary: palette.primary,
|
||||
});
|
||||
const bg = palette['base-100'];
|
||||
const fg = palette['base-content'];
|
||||
const primary = palette.primary;
|
||||
if (isWebAppPlatform()) {
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', bg);
|
||||
}
|
||||
setThemeCode({ bg, fg, primary, palette });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [themeMode, themeColor, isDarkMode]);
|
||||
|
||||
const updateThemeMode = (mode: ThemeMode) => setThemeMode(mode);
|
||||
const updateThemeColor = (color: string) => setThemeColor(color);
|
||||
const updateAppTheme = (color: keyof Palette) => {
|
||||
if (isWebAppPlatform()) {
|
||||
const { palette } = themeCode;
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', palette[color]);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
themeMode,
|
||||
@@ -94,5 +104,6 @@ export const useTheme = () => {
|
||||
isDarkMode,
|
||||
updateThemeMode,
|
||||
updateThemeColor,
|
||||
updateAppTheme,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import '@/i18n/i18n';
|
||||
import { useTranslation as _useTranslation } from 'react-i18next';
|
||||
|
||||
export type TranslationFunc = (key: string, options?: Record<string, number | string>) => string;
|
||||
|
||||
export const useTranslation = (namespace: string = 'translation') => {
|
||||
const { t } = _useTranslation(namespace);
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ const hashPayload = (payload: EdgeTTSPayload): string => {
|
||||
export class EdgeSpeechTTS {
|
||||
static voices = genVoiceList(EDGE_TTS_VOICES);
|
||||
private static audioCache = new LRUCache<string, AudioBuffer>(200);
|
||||
private audioContext = new AudioContext();
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -274,8 +275,7 @@ export class EdgeSpeechTTS {
|
||||
try {
|
||||
const res = await this.create(payload);
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const audioContext = new AudioContext();
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer.slice(0));
|
||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer.slice(0));
|
||||
EdgeSpeechTTS.audioCache.set(cacheKey, audioBuffer);
|
||||
return audioBuffer;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { Book, BookConfig, BookNote, BookDataRecord } from '@/types/book';
|
||||
import { READEST_WEB_BASE_URL } from '@/services/constants';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { getAPIBaseUrl, isWebAppPlatform } from '@/services/environment';
|
||||
|
||||
// Develop Sync API only in development mode and web platform
|
||||
// with command `pnpm dev-web`
|
||||
const SYNC_API_ENDPOINT =
|
||||
process.env['NODE_ENV'] === 'development' && isWebAppPlatform()
|
||||
? '/api/sync'
|
||||
: `${READEST_WEB_BASE_URL}/api/sync`;
|
||||
const SYNC_API_ENDPOINT = getAPIBaseUrl() + '/sync';
|
||||
|
||||
export type SyncType = 'books' | 'configs' | 'notes';
|
||||
export type SyncOp = 'push' | 'pull' | 'both';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import Providers from '@/components/Providers';
|
||||
|
||||
import '../styles/globals.css';
|
||||
@@ -6,9 +7,29 @@ import '../styles/fonts.css';
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<Providers>
|
||||
<Component {...pageProps} />
|
||||
</Providers>
|
||||
<>
|
||||
<Head>
|
||||
<meta
|
||||
name='viewport'
|
||||
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover'
|
||||
/>
|
||||
<meta name='application-name' content='Readest' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
|
||||
<meta name='apple-mobile-web-app-title' content='Readest' />
|
||||
<meta
|
||||
name='description'
|
||||
content='Readest is an open-source eBook reader supporting EPUB, PDF, and sync across devices.'
|
||||
/>
|
||||
<meta name='format-detection' content='telephone=no' />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<meta name='theme-color' content='white' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
</Head>
|
||||
<Providers>
|
||||
<Component {...pageProps} />
|
||||
</Providers>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
|
||||
const DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
const DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser(token);
|
||||
|
||||
if (error || !user) return {};
|
||||
return { user, token };
|
||||
};
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { user, token } = await getUserAndToken(req.headers['authorization']);
|
||||
const deeplApiUrl = user && token ? DEEPL_PRO_API : DEEPL_FREE_API;
|
||||
const deeplAuthKey =
|
||||
user && token ? process.env['DEEPL_PRO_API_KEY'] : process.env['DEEPL_FREE_API_KEY'];
|
||||
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
|
||||
try {
|
||||
const response = await fetch(deeplApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${deeplAuthKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: req.method === 'POST' ? JSON.stringify(req.body) : undefined,
|
||||
});
|
||||
res.status(response.status);
|
||||
res.json(await response.json());
|
||||
} catch (error) {
|
||||
console.error('Error proxying DeepL request:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -1,4 +1,3 @@
|
||||
import Cors from 'cors';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PostgrestError } from '@supabase/supabase-js';
|
||||
@@ -7,6 +6,7 @@ import { BookDataRecord } from '@/types/book';
|
||||
import { transformBookConfigToDB } from '@/utils/transform';
|
||||
import { transformBookNoteToDB } from '@/utils/transform';
|
||||
import { transformBookToDB } from '@/utils/transform';
|
||||
import { runMiddleware, corsAllMethods } from '@/utils/cors';
|
||||
import { SyncData, SyncResult, SyncType } from '@/libs/sync';
|
||||
|
||||
const transformsToDB = {
|
||||
@@ -219,25 +219,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to wait for a middleware to execute before continuing
|
||||
// And to throw an error when an error happens in a middleware
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: Function) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn(req, res, (result: unknown) => {
|
||||
if (result instanceof Error) {
|
||||
return reject(result);
|
||||
}
|
||||
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const cors = Cors({
|
||||
methods: ['POST', 'GET', 'HEAD'],
|
||||
});
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!req.url) {
|
||||
return res.status(400).json({ error: 'Invalid request URL' });
|
||||
@@ -247,7 +228,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const host = process.env['HOST'] || 'localhost:3000';
|
||||
const url = new URL(req.url, `${protocol}://${host}`);
|
||||
|
||||
await runMiddleware(req, res, cors);
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
|
||||
try {
|
||||
let response: Response;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user