Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2406605871 | |||
| a59a3b633b | |||
| cccd4c2b16 | |||
| 990f61fb72 | |||
| d51f5a632a | |||
| 3e9b89de0e | |||
| f900608d6a | |||
| ca56c5a73d | |||
| 57b72d6d57 | |||
| 76c5f58547 | |||
| e4a6ae01e0 | |||
| 2763c18d6f | |||
| 71d0735917 | |||
| 3484c893df | |||
| e885d22631 | |||
| 3ad26d9d8f | |||
| b699fc98c2 | |||
| 2c9fe8e4f4 | |||
| f425ff8ffa | |||
| d6e0fd3b4d | |||
| 2e2fad849b | |||
| 572ddd923c | |||
| adab6f8d9b | |||
| f3f8df033d | |||
| 50f75c1f18 | |||
| 1ad13d0b7e | |||
| cd67f6ec3f | |||
| 1be567ed4a | |||
| 0a46fe8648 | |||
| abb1b7ae9c | |||
| 75e872af51 | |||
| d1b6a0db1e | |||
| 870607f9c3 | |||
| fc163926ad | |||
| 4a33409559 | |||
| b8af483229 | |||
| ba9490ad00 | |||
| 0e7db64fe1 | |||
| a5f94cced9 | |||
| 71ffe76e29 | |||
| 3dcab5a1ee | |||
| cb2c7b5c89 | |||
| 07b08ee568 | |||
| 4612730474 | |||
| dc500c5bb4 | |||
| 329c907f80 | |||
| acfce9da67 | |||
| 72c6455fca | |||
| 3d0a2b82ba | |||
| 72949e726f | |||
| 23206bd528 | |||
| 8f26f9a7fa | |||
| a16ca22814 | |||
| 82fb92cf86 | |||
| f76bce5575 | |||
| a23447a813 |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Report a bug
|
||||
about: Report a bug or a functional regression
|
||||
title: 'Ex: In DarkMode, a blank square appears in bottom right corner while scrolling'
|
||||
title: 'Example: In DarkMode, a blank square appears in bottom right corner while scrolling'
|
||||
labels: ['type: bug']
|
||||
assignees: ''
|
||||
---
|
||||
@@ -30,9 +30,15 @@ The blank square should be transparent (invisible)
|
||||
|
||||
## Technical inputs
|
||||
|
||||
Operating System:
|
||||
|
||||
Readest Version:
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
- We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
|
||||
- Probably fixable with CSS
|
||||
Operating System: macOS 14.3.1
|
||||
Readest Version: 0.9.0
|
||||
We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
|
||||
Probably fixable with CSS
|
||||
```
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Build Web Application on Pull Request
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
build_web_app:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.15.1
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: install Dependencies
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm install && pnpm setup-pdfjs
|
||||
|
||||
- name: build the web App
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build-web
|
||||
@@ -6,52 +6,79 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
get-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.result }}
|
||||
release_id: ${{ steps.get-release.outputs.release_id }}
|
||||
release_tag: ${{ steps.get-release.outputs.release_tag }}
|
||||
release_note: ${{ steps.get-release-notes.outputs.release_note }}
|
||||
release_version: ${{ steps.get-release-notes.outputs.release_version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
- name: get version
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./apps/readest-app/package.json').version")" >> $GITHUB_ENV
|
||||
- name: create release
|
||||
id: create-release
|
||||
uses: actions/github-script@v6
|
||||
- name: get release
|
||||
id: get-release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
})
|
||||
return data.id
|
||||
core.setOutput('release_id', data.id);
|
||||
core.setOutput('release_tag', data.tag_name);
|
||||
- name: get release notes
|
||||
id: get-release-notes
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const version = require('./apps/readest-app/package.json').version;
|
||||
const releaseNotesFileContent = fs.readFileSync('./apps/readest-app/release-notes.json', 'utf8');
|
||||
const releaseNotes = JSON.parse(releaseNotesFileContent).releases[version] || {};
|
||||
const notes = releaseNotes.notes || [];
|
||||
const releaseNote = notes.map((note, index) => `${index + 1}. ${note}`).join(' ');
|
||||
console.log('Formatted release note:', releaseNote);
|
||||
core.setOutput('release_version', version);
|
||||
core.setOutput('release_note', releaseNote);
|
||||
|
||||
build-tauri:
|
||||
needs: create-release
|
||||
needs: get-release
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04
|
||||
arch: x86_64
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-22.04
|
||||
arch: aarch64
|
||||
rust_target: aarch64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
arch: aarch64
|
||||
rust_target: x86_64-apple-darwin,aarch64-apple-darwin
|
||||
args: '--target universal-apple-darwin'
|
||||
- os: windows-latest
|
||||
arch: x86_64
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
args: '--target x86_64-pc-windows-msvc'
|
||||
- os: windows-latest
|
||||
arch: aarch64
|
||||
rust_target: aarch64-pc-windows-msvc
|
||||
args: '--target aarch64-pc-windows-msvc --bundles nsis'
|
||||
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: initialize git submodules
|
||||
run: git submodule update --init --recursive
|
||||
@@ -62,14 +89,11 @@ jobs:
|
||||
version: 9.14.4
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: install rimraf
|
||||
run: npm install -g rimraf
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -91,13 +115,15 @@ jobs:
|
||||
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
|
||||
|
||||
- name: copy .env.local to apps/readest-app
|
||||
run: cp .env.local apps/readest-app/.env.local
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.config.os == 'ubuntu-latest'
|
||||
if: matrix.config.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
@@ -115,27 +141,59 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
projectPath: apps/readest-app
|
||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||
args: ${{ matrix.config.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||
releaseId: ${{ needs.get-release.outputs.release_id }}
|
||||
releaseBody: ${{ needs.get-release.outputs.release_note }}
|
||||
args: ${{ matrix.config.args || '' }}
|
||||
|
||||
publish-release:
|
||||
- name: upload portable binaries (Windows only)
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Uploading Portable Binaries"
|
||||
arch=${{ matrix.config.arch }}
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
bin_file="Readest_${version}_x64-portable.exe"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
bin_file="Readest_${version}_arm64-portable.exe"
|
||||
else
|
||||
echo "Unknown architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exe_file="apps/readest-app/src-tauri/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"
|
||||
cp $exe_file $bin_file
|
||||
|
||||
echo "Uploading $bin_file to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber
|
||||
|
||||
update-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-release, build-tauri]
|
||||
needs: [get-release, build-tauri]
|
||||
|
||||
steps:
|
||||
- name: publish release
|
||||
id: publish-release
|
||||
uses: actions/github-script@v6
|
||||
- name: update release
|
||||
id: update-release
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
release_id: ${{ needs.create-release.outputs.release_id }}
|
||||
release_id: ${{ needs.get-release.outputs.release_id }}
|
||||
release_note: ${{ needs.get-release.outputs.release_note }}
|
||||
with:
|
||||
script: |
|
||||
const body = `## Release Highlight\n${process.env.release_note}`;
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.release_id,
|
||||
body: body,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Create vercel preview URL on pull request
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
permissions:
|
||||
contents: write
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'true'
|
||||
- uses: amondnet/vercel-action@v25
|
||||
id: vercel-deploy
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
vercel-org-id: ${{ secrets.ORG_ID}}
|
||||
vercel-project-id: ${{ secrets.PROJECT_ID}}
|
||||
- name: preview-url
|
||||
run: |
|
||||
echo ${{ steps.vercel-deploy.outputs.preview-url }}
|
||||
@@ -82,6 +82,13 @@ This project is a monorepo. The code for the `readest-app` is in the `app/reades
|
||||
| `pnpm dev-web` | Starts the development server for the web app only |
|
||||
| `pnpm build-web` | Builds the web app |
|
||||
|
||||
Recommended Visual Studio Code plugins for development:
|
||||
|
||||
- JavaScript and TypeScript Nightly (ms-vscode.vscode-typescript-next)
|
||||
- VS Code ESLint extension (dbaeumer.vscode-eslint)
|
||||
- Prettier - Code formatter (esbenp.prettier-vscode)
|
||||
- rust-analyzer (rust-lang.rust-analyzer) (for Tauri development only)
|
||||
|
||||
### When you're done
|
||||
|
||||
Check that your code follows the project's style guidelines by running:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme" target="_blank">
|
||||
<img src="https://github.com/chrox/readest/blob/main/apps/readest-app/src-tauri/icons/icon.png?raw=true" alt="Readest Logo" width="20%" />
|
||||
<img src="https://github.com/readest/readest/blob/main/apps/readest-app/src-tauri/icons/icon.png?raw=true" alt="Readest Logo" width="20%" />
|
||||
</a>
|
||||
<h1>Readest</h1>
|
||||
<br>
|
||||
@@ -84,7 +84,7 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -118,7 +118,7 @@ To get started with Readest, follow these steps to clone and build the project.
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/chrox/readest.git
|
||||
git clone https://github.com/readest/readest.git
|
||||
cd readest
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
@@ -150,19 +150,29 @@ For Windows targets, “Build Tools for Visual Studio 2022” (or a higher editi
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
For iOS:
|
||||
|
||||
```bash
|
||||
pnpm tauri ios dev
|
||||
```
|
||||
|
||||
### 5. Build for Production
|
||||
|
||||
```bash
|
||||
pnpm tauri build
|
||||
```
|
||||
|
||||
### 6. More information
|
||||
|
||||
Please check the [wiki][link-gh-wiki] of this project for more information on development.
|
||||
|
||||
## Contributors
|
||||
|
||||
Readest is open-source, and contributions are welcome! Feel free to open issues, suggest features, or submit pull requests. Please **review our [contributing guidelines](CONTRIBUTING.md) before you start**. We also welcome you to join our [Discord][link-discord] community for either support or contributing guidance.
|
||||
|
||||
<a href="https://github.com/chrox/readest/graphs/contributors">
|
||||
<a href="https://github.com/readest/readest/graphs/contributors">
|
||||
<p align="left">
|
||||
<img width="100" src="https://contrib.rocks/image?repo=chrox/readest" alt="A table of avatars from the project's contributors" />
|
||||
<img width="200" src="https://contrib.rocks/image?repo=readest/readest" alt="A table of avatars from the project's contributors" />
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -183,18 +193,19 @@ The following JavaScript libraries are bundled in this software:
|
||||
|
||||
[badge-website]: https://img.shields.io/badge/website-readest.com-orange
|
||||
[badge-web-app]: https://img.shields.io/badge/read%20online-web.readest.com-orange
|
||||
[badge-license]: https://img.shields.io/github/license/chrox/readest?color=teal
|
||||
[badge-release]: https://img.shields.io/github/release/chrox/readest?color=green
|
||||
[badge-license]: https://img.shields.io/github/license/readest/readest?color=teal
|
||||
[badge-release]: https://img.shields.io/github/release/readest/readest?color=green
|
||||
[badge-platforms]: https://img.shields.io/badge/OS-macOS%2C%20Windows%2C%20Linux%2C%20Web-green
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/chrox/readest?color=green
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/chrox/readest
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=green
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
|
||||
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
|
||||
[link-macos-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
|
||||
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
|
||||
[link-web-readest]: https://web.readest.com
|
||||
[link-gh-releases]: https://github.com/chrox/readest/releases
|
||||
[link-gh-commits]: https://github.com/chrox/readest/commits/main
|
||||
[link-gh-pulse]: https://github.com/chrox/readest/pulse
|
||||
[link-gh-releases]: https://github.com/readest/readest/releases
|
||||
[link-gh-commits]: https://github.com/readest/readest/commits/main
|
||||
[link-gh-pulse]: https://github.com/readest/readest/pulse
|
||||
[link-gh-wiki]: https://github.com/readest/readest/wiki
|
||||
[link-discord]: https://discord.gg/gntyVNk3BJ
|
||||
[link-parallel-read]: https://readest.com/#parallel-read
|
||||
[link-koreader]: https://github.com/koreader/koreader
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.test.{js,jsx,ts,tsx}'],
|
||||
output: '.',
|
||||
options: {
|
||||
debug: false,
|
||||
sort: true,
|
||||
func: {
|
||||
list: ['_'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
lngs: ['de', 'ja', 'es', 'fr', 'it', 'ko', 'pt', 'ru', 'tr', 'id', 'vi', 'zh-CN', 'zh-TW'],
|
||||
ns: ['translation'],
|
||||
defaultNs: 'translation',
|
||||
defaultValue: '__STRING_NOT_TRANSLATED__',
|
||||
resource: {
|
||||
loadPath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
savePath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
jsonIndent: 2,
|
||||
lineEnding: '\n',
|
||||
},
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
interpolation: {
|
||||
prefix: '{{',
|
||||
suffix: '}}',
|
||||
},
|
||||
metadata: {},
|
||||
allowDynamicKeys: true,
|
||||
removeUnusedKeys: true,
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.8.5",
|
||||
"version": "0.9.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -9,6 +9,7 @@
|
||||
"dev-web": "dotenv -e .env.web -- next dev",
|
||||
"build-web": "dotenv -e .env.web -- next build",
|
||||
"start-web": "dotenv -e .env.web -- next start",
|
||||
"i18n:extract": "i18next-scanner",
|
||||
"lint": "next lint",
|
||||
"tauri": "tauri",
|
||||
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs",
|
||||
@@ -34,10 +35,12 @@
|
||||
"@supabase/supabase-js": "^2.47.7",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-http": "^2.2.0",
|
||||
"@tauri-apps/plugin-log": "^2.2.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.2",
|
||||
"@tauri-apps/plugin-os": "^2.2.0",
|
||||
"@tauri-apps/plugin-process": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "~2.2.0",
|
||||
@@ -47,11 +50,15 @@
|
||||
"cors": "^2.8.5",
|
||||
"cssbeautify": "^0.3.1",
|
||||
"foliate-js": "workspace:*",
|
||||
"i18next": "^24.2.0",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"next": "15.1.0",
|
||||
"posthog-js": "^1.194.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.3.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
@@ -70,6 +77,7 @@
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"node-env-run": "^4.0.2",
|
||||
"postcss": "^8.4.49",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(erkannt)",
|
||||
"About Readest": "Über Readest",
|
||||
"Add your notes here...": "Fügen Sie hier Ihre Notizen hinzu...",
|
||||
"Animation": "Animation",
|
||||
"Apply": "Anwenden",
|
||||
"Are you sure to delete the selected books?": "Möchten Sie die ausgewählten Bücher wirklich löschen?",
|
||||
"Auto Mode": "Automatischer Modus",
|
||||
"Behavior": "Verhalten",
|
||||
"Book": "Buch",
|
||||
"Book Cover": "Buchcover",
|
||||
"Bookmark": "Lesezeichen",
|
||||
"Cancel": "Abbrechen",
|
||||
"Chapter": "Kapitel",
|
||||
"Cherry": "Kirschrot",
|
||||
"Color": "Farbe",
|
||||
"Confirm": "Bestätigen",
|
||||
"Confirm Deletion": "Löschen bestätigen",
|
||||
"Custom CSS": "Benutzerdefiniertes CSS",
|
||||
"Dark Mode": "Dunkelmodus",
|
||||
"Default": "Standard",
|
||||
"Default Font": "Standardschriftart",
|
||||
"Default Font Size": "Standard-Schriftgröße",
|
||||
"Delete": "Löschen",
|
||||
"Disable Click-to-Flip": "Klick zum Blättern deaktivieren",
|
||||
"Download Readest": "Readest herunterladen",
|
||||
"Edit": "Bearbeiten",
|
||||
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
|
||||
"Excerpts": "Auszüge",
|
||||
"Failed to import book(s): {{filenames}}": "Fehler beim Importieren von Buch/Büchern: {{filenames}}",
|
||||
"Font": "Schriftart",
|
||||
"Font & Layout": "Schrift & Layout",
|
||||
"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",
|
||||
"Go Left": "Nach links",
|
||||
"Go Right": "Nach rechts",
|
||||
"Grass": "Grasgrün",
|
||||
"Gray": "Grau",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Horizontale Richtung",
|
||||
"Hyphenation": "Silbentrennung",
|
||||
"Identifier:": "Kennung:",
|
||||
"Import Books": "Bücher importieren",
|
||||
"Invert Colors in Dark Mode": "Farben im Dunkelmodus invertieren",
|
||||
"Language:": "Sprache:",
|
||||
"Layout": "Layout",
|
||||
"Light Mode": "Hellmodus",
|
||||
"Line Height": "Zeilenhöhe",
|
||||
"Loading...": "Wird geladen...",
|
||||
"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",
|
||||
"Maximum Block Size": "Maximale Blockgröße",
|
||||
"Maximum Inline Size": "Maximale Zeilengröße",
|
||||
"Maximum Number of Columns": "Maximale Spaltenanzahl",
|
||||
"Minimum Font Size": "Minimale Schriftgröße",
|
||||
"Misc": "Sonstiges",
|
||||
"Monospace Font": "Monospace-Schriftart",
|
||||
"More Info": "Weitere Informationen",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Notizbuch",
|
||||
"Notes": "Notizen",
|
||||
"Open": "Öffnen",
|
||||
"Open Book": "Buch öffnen",
|
||||
"Original Text": "Originaltext",
|
||||
"Override Publisher Font": "Verleger-Schriftart überschreiben",
|
||||
"Page": "Seite",
|
||||
"Paging Animation": "Blätter-Animation",
|
||||
"Paragraph": "Absatz",
|
||||
"Parallel Read": "Paralleles Lesen",
|
||||
"Published:": "Veröffentlicht:",
|
||||
"Publisher:": "Verlag:",
|
||||
"Reading progress synced": "Lesevorgang synchronisiert",
|
||||
"Reload Page": "Seite neu laden",
|
||||
"Reveal in File Explorer": "Im Datei-Explorer anzeigen",
|
||||
"Reveal in Finder": "Im Finder anzeigen",
|
||||
"Reveal in Folder": "Im Ordner anzeigen",
|
||||
"Sans-Serif Font": "Serifenlose Schriftart",
|
||||
"Save": "Speichern",
|
||||
"Scrolled Mode": "Scroll-Modus",
|
||||
"Search books...": "Bücher suchen...",
|
||||
"Search...": "Suchen...",
|
||||
"Select Book": "Buch auswählen",
|
||||
"Select books": "Bücher auswählen",
|
||||
"Select multiple books": "Mehrere Bücher auswählen",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Serifenschrift",
|
||||
"Show Book Details": "Buchdetails anzeigen",
|
||||
"Sidebar": "Seitenleiste",
|
||||
"Sign In": "Anmelden",
|
||||
"Sign Out": "Abmelden",
|
||||
"Sky": "Himmelblau",
|
||||
"Solarized": "Solarisiert",
|
||||
"Subjects:": "Themen:",
|
||||
"System Fonts": "System-Schriftarten",
|
||||
"Theme Color": "Themenfarbe",
|
||||
"Theme Mode": "Themenmodus",
|
||||
"Translated Text": "Übersetzter Text",
|
||||
"Unknown": "Unbekannt",
|
||||
"Untitled": "Ohne Titel",
|
||||
"Updated:": "Aktualisiert:",
|
||||
"User avatar": "Benutzer-Avatar",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Vertikale Richtung",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
|
||||
"Writing Mode": "Schreibmodus",
|
||||
"Your Library": "Ihre Bibliothek"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(detectado)",
|
||||
"About Readest": "Acerca de Readest",
|
||||
"Add your notes here...": "Añade tus notas aquí...",
|
||||
"Animation": "Animación",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "¿Estás seguro de eliminar los libros seleccionados?",
|
||||
"Auto Mode": "Modo automático",
|
||||
"Behavior": "Comportamiento",
|
||||
"Book": "Libro",
|
||||
"Book Cover": "Portada del libro",
|
||||
"Bookmark": "Marcador",
|
||||
"Cancel": "Cancelar",
|
||||
"Chapter": "Capítulo",
|
||||
"Cherry": "Cereza",
|
||||
"Color": "Color",
|
||||
"Confirm": "Confirmar",
|
||||
"Confirm Deletion": "Confirmar eliminación",
|
||||
"Custom CSS": "CSS personalizado",
|
||||
"Dark Mode": "Modo oscuro",
|
||||
"Default": "Predeterminado",
|
||||
"Default Font": "Fuente predeterminada",
|
||||
"Default Font Size": "Tamaño de fuente predeterminado",
|
||||
"Delete": "Eliminar",
|
||||
"Disable Click-to-Flip": "Desactivar clic para voltear",
|
||||
"Download Readest": "Descargar Readest",
|
||||
"Edit": "Editar",
|
||||
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
|
||||
"Excerpts": "Extractos",
|
||||
"Failed to import book(s): {{filenames}}": "Error al importar libro(s): {{filenames}}",
|
||||
"Font": "Fuente",
|
||||
"Font & Layout": "Fuente y diseño",
|
||||
"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",
|
||||
"Go Left": "Ir a la izquierda",
|
||||
"Go Right": "Ir a la derecha",
|
||||
"Grass": "Hierba",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Dirección horizontal",
|
||||
"Hyphenation": "Guionización",
|
||||
"Identifier:": "Identificador:",
|
||||
"Import Books": "Importar libros",
|
||||
"Invert Colors in Dark Mode": "Invertir colores en modo oscuro",
|
||||
"Language:": "Idioma:",
|
||||
"Layout": "Diseño",
|
||||
"Light Mode": "Modo claro",
|
||||
"Line Height": "Altura de línea",
|
||||
"Loading...": "Cargando...",
|
||||
"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",
|
||||
"Maximum Block Size": "Tamaño máximo de bloque",
|
||||
"Maximum Inline Size": "Tamaño máximo en línea",
|
||||
"Maximum Number of Columns": "Número máximo de columnas",
|
||||
"Minimum Font Size": "Tamaño de fuente mínimo",
|
||||
"Misc": "Varios",
|
||||
"Monospace Font": "Fuente monoespaciada",
|
||||
"More Info": "Más información",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Cuaderno",
|
||||
"Notes": "Notas",
|
||||
"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",
|
||||
"Parallel Read": "Lectura paralela",
|
||||
"Published:": "Publicado:",
|
||||
"Publisher:": "Editorial:",
|
||||
"Reading progress synced": "Progreso de lectura sincronizado",
|
||||
"Reload Page": "Recargar página",
|
||||
"Reveal in File Explorer": "Mostrar en el Explorador de archivos",
|
||||
"Reveal in Finder": "Mostrar en Finder",
|
||||
"Reveal in Folder": "Mostrar en carpeta",
|
||||
"Sans-Serif Font": "Fuente sans-serif",
|
||||
"Save": "Guardar",
|
||||
"Scrolled Mode": "Modo desplazamiento",
|
||||
"Search books...": "Buscar libros...",
|
||||
"Search...": "Buscar...",
|
||||
"Select Book": "Seleccionar libro",
|
||||
"Select books": "Seleccionar libros",
|
||||
"Select multiple books": "Seleccionar varios libros",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Fuente serif",
|
||||
"Show Book Details": "Mostrar detalles del libro",
|
||||
"Sidebar": "Barra lateral",
|
||||
"Sign In": "Iniciar sesión",
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Sky": "Cielo",
|
||||
"Solarized": "Solarizado",
|
||||
"Subjects:": "Temas:",
|
||||
"System Fonts": "Fuentes del sistema",
|
||||
"Theme Color": "Color del tema",
|
||||
"Theme Mode": "Modo del tema",
|
||||
"Translated Text": "Texto traducido",
|
||||
"Unknown": "Desconocido",
|
||||
"Untitled": "Sin título",
|
||||
"Updated:": "Actualizado:",
|
||||
"User avatar": "Avatar del usuario",
|
||||
"Version {{version}}": "Versión {{version}}",
|
||||
"Vertical Direction": "Dirección vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
|
||||
"Writing Mode": "Modo de escritura",
|
||||
"Your Library": "Tu biblioteca"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(détecté)",
|
||||
"About Readest": "À propos de Readest",
|
||||
"Add your notes here...": "Ajoutez vos notes ici...",
|
||||
"Animation": "Animation",
|
||||
"Apply": "Appliquer",
|
||||
"Are you sure to delete the selected books?": "Êtes-vous sûr de vouloir supprimer les livres sélectionnés ?",
|
||||
"Auto Mode": "Mode automatique",
|
||||
"Behavior": "Comportement",
|
||||
"Book": "Livre",
|
||||
"Book Cover": "Couverture du livre",
|
||||
"Bookmark": "Signet",
|
||||
"Cancel": "Annuler",
|
||||
"Chapter": "Chapitre",
|
||||
"Cherry": "Cerise",
|
||||
"Color": "Couleur",
|
||||
"Confirm": "Confirmer",
|
||||
"Confirm Deletion": "Confirmer la suppression",
|
||||
"Custom CSS": "CSS personnalisé",
|
||||
"Dark Mode": "Mode sombre",
|
||||
"Default": "Par défaut",
|
||||
"Default Font": "Police par défaut",
|
||||
"Default Font Size": "Taille de police par défaut",
|
||||
"Delete": "Supprimer",
|
||||
"Disable Click-to-Flip": "Désactiver le clic pour tourner",
|
||||
"Download Readest": "Télécharger Readest",
|
||||
"Edit": "Modifier",
|
||||
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
|
||||
"Excerpts": "Extraits",
|
||||
"Failed to import book(s): {{filenames}}": "Échec de l'importation du/des livre(s) : {{filenames}}",
|
||||
"Font": "Police",
|
||||
"Font & Layout": "Police et mise en page",
|
||||
"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",
|
||||
"Go Left": "Aller à gauche",
|
||||
"Go Right": "Aller à droite",
|
||||
"Grass": "Herbe",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Direction horizontale",
|
||||
"Hyphenation": "Césure",
|
||||
"Identifier:": "Identifiant :",
|
||||
"Import Books": "Importer des livres",
|
||||
"Invert Colors in Dark Mode": "Inverser les couleurs en mode sombre",
|
||||
"Language:": "Langue :",
|
||||
"Layout": "Mise en page",
|
||||
"Light Mode": "Mode clair",
|
||||
"Line Height": "Hauteur de ligne",
|
||||
"Loading...": "Chargement...",
|
||||
"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",
|
||||
"Maximum Block Size": "Taille maximale du bloc",
|
||||
"Maximum Inline Size": "Taille maximale en ligne",
|
||||
"Maximum Number of Columns": "Nombre maximal de colonnes",
|
||||
"Minimum Font Size": "Taille minimale de police",
|
||||
"Misc": "Divers",
|
||||
"Monospace Font": "Police monospace",
|
||||
"More Info": "Plus d'informations",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Carnet de notes",
|
||||
"Notes": "Notes",
|
||||
"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",
|
||||
"Parallel Read": "Lecture parallèle",
|
||||
"Published:": "Publié :",
|
||||
"Publisher:": "Éditeur :",
|
||||
"Reading progress synced": "Progression de lecture synchronisée",
|
||||
"Reload Page": "Recharger la page",
|
||||
"Reveal in File Explorer": "Afficher dans l'explorateur de fichiers",
|
||||
"Reveal in Finder": "Afficher dans le Finder",
|
||||
"Reveal in Folder": "Afficher dans le dossier",
|
||||
"Sans-Serif Font": "Police sans empattement",
|
||||
"Save": "Enregistrer",
|
||||
"Scrolled Mode": "Mode défilement",
|
||||
"Search books...": "Rechercher des livres...",
|
||||
"Search...": "Rechercher...",
|
||||
"Select Book": "Sélectionner un livre",
|
||||
"Select books": "Sélectionner des livres",
|
||||
"Select multiple books": "Sélectionner plusieurs livres",
|
||||
"Sepia": "Sépia",
|
||||
"Serif Font": "Police avec empattement",
|
||||
"Show Book Details": "Afficher les détails du livre",
|
||||
"Sidebar": "Barre latérale",
|
||||
"Sign In": "Se connecter",
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Sky": "Ciel",
|
||||
"Solarized": "Solarisé",
|
||||
"Subjects:": "Sujets :",
|
||||
"System Fonts": "Polices système",
|
||||
"Theme Color": "Couleur du thème",
|
||||
"Theme Mode": "Mode du thème",
|
||||
"Translated Text": "Texte traduit",
|
||||
"Unknown": "Inconnu",
|
||||
"Untitled": "Sans titre",
|
||||
"Updated:": "Mis à jour :",
|
||||
"User avatar": "Avatar de l'utilisateur",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Direction verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
|
||||
"Writing Mode": "Mode écriture",
|
||||
"Your Library": "Votre bibliothèque"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(terdeteksi)",
|
||||
"About Readest": "Tentang Readest",
|
||||
"Add your notes here...": "Tambahkan catatan Anda di sini...",
|
||||
"Animation": "Animasi",
|
||||
"Apply": "Terapkan",
|
||||
"Are you sure to delete the selected books?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Auto Mode": "Mode Otomatis",
|
||||
"Behavior": "Perilaku",
|
||||
"Book": "Buku",
|
||||
"Book Cover": "Sampul Buku",
|
||||
"Bookmark": "Penanda",
|
||||
"Cancel": "Batal",
|
||||
"Chapter": "Bab",
|
||||
"Cherry": "Ceri",
|
||||
"Color": "Warna",
|
||||
"Confirm": "Konfirmasi",
|
||||
"Confirm Deletion": "Konfirmasi Penghapusan",
|
||||
"Custom CSS": "CSS Kustom",
|
||||
"Dark Mode": "Mode Gelap",
|
||||
"Default": "Default",
|
||||
"Default Font": "Font Default",
|
||||
"Default Font Size": "Ukuran Font Default",
|
||||
"Delete": "Hapus",
|
||||
"Disable Click-to-Flip": "Nonaktifkan Klik untuk Membalik",
|
||||
"Download Readest": "Unduh Readest",
|
||||
"Edit": "Edit",
|
||||
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
|
||||
"Excerpts": "Kutipan",
|
||||
"Failed to import book(s): {{filenames}}": "Gagal mengimpor buku: {{filenames}}",
|
||||
"Font": "Font",
|
||||
"Font & Layout": "Font & Tata Letak",
|
||||
"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",
|
||||
"Go Left": "Ke Kiri",
|
||||
"Go Right": "Ke Kanan",
|
||||
"Grass": "Rumput",
|
||||
"Gray": "Abu-abu",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Arah Horizontal",
|
||||
"Hyphenation": "Pemenggalan Kata",
|
||||
"Identifier:": "Pengenal:",
|
||||
"Import Books": "Impor Buku",
|
||||
"Invert Colors in Dark Mode": "Balik Warna di Mode Gelap",
|
||||
"Language:": "Bahasa:",
|
||||
"Layout": "Tata Letak",
|
||||
"Light Mode": "Mode Terang",
|
||||
"Line Height": "Tinggi Baris",
|
||||
"Loading...": "Memuat...",
|
||||
"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",
|
||||
"Maximum Block Size": "Ukuran Blok Maksimum",
|
||||
"Maximum Inline Size": "Ukuran Sebaris Maksimum",
|
||||
"Maximum Number of Columns": "Jumlah Kolom Maksimum",
|
||||
"Minimum Font Size": "Ukuran Font Minimum",
|
||||
"Misc": "Lain-lain",
|
||||
"Monospace Font": "Font Monospace",
|
||||
"More Info": "Info Lebih Lanjut",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Buku Catatan",
|
||||
"Notes": "Catatan",
|
||||
"Open": "Buka",
|
||||
"Open Book": "Buka Buku",
|
||||
"Original Text": "Teks Asli",
|
||||
"Override Publisher Font": "Ganti Font Penerbit",
|
||||
"Page": "Halaman",
|
||||
"Paging Animation": "Animasi Halaman",
|
||||
"Paragraph": "Paragraf",
|
||||
"Parallel Read": "Baca Paralel",
|
||||
"Published:": "Diterbitkan:",
|
||||
"Publisher:": "Penerbit:",
|
||||
"Reading progress synced": "Progres membaca disinkronkan",
|
||||
"Reload Page": "Muat Ulang Halaman",
|
||||
"Reveal in File Explorer": "Tampilkan di File Explorer",
|
||||
"Reveal in Finder": "Tampilkan di Finder",
|
||||
"Reveal in Folder": "Tampilkan di Folder",
|
||||
"Sans-Serif Font": "Font Sans-Serif",
|
||||
"Save": "Simpan",
|
||||
"Scrolled Mode": "Mode Gulir",
|
||||
"Search books...": "Cari buku...",
|
||||
"Search...": "Cari...",
|
||||
"Select Book": "Pilih Buku",
|
||||
"Select books": "Pilih buku",
|
||||
"Select multiple books": "Pilih beberapa buku",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Font Serif",
|
||||
"Show Book Details": "Tampilkan Detail Buku",
|
||||
"Sidebar": "Bilah Samping",
|
||||
"Sign In": "Masuk",
|
||||
"Sign Out": "Keluar",
|
||||
"Sky": "Langit",
|
||||
"Solarized": "Solarized",
|
||||
"Subjects:": "Subjek:",
|
||||
"System Fonts": "Font Sistem",
|
||||
"Theme Color": "Warna Tema",
|
||||
"Theme Mode": "Mode Tema",
|
||||
"Translated Text": "Teks Terjemahan",
|
||||
"Unknown": "Tidak Diketahui",
|
||||
"Untitled": "Tanpa Judul",
|
||||
"Updated:": "Diperbarui:",
|
||||
"User avatar": "Avatar Pengguna",
|
||||
"Version {{version}}": "Versi {{version}}",
|
||||
"Vertical Direction": "Arah Vertikal",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
|
||||
"Writing Mode": "Mode Menulis",
|
||||
"Your Library": "Perpustakaan Anda"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(rilevato)",
|
||||
"About Readest": "Informazioni su Readest",
|
||||
"Add your notes here...": "Aggiungi qui le tue note...",
|
||||
"Animation": "Animazione",
|
||||
"Apply": "Applica",
|
||||
"Are you sure to delete the selected books?": "Sei sicuro di voler eliminare i libri selezionati?",
|
||||
"Auto Mode": "Modalità automatica",
|
||||
"Behavior": "Comportamento",
|
||||
"Book": "Libro",
|
||||
"Book Cover": "Copertina",
|
||||
"Bookmark": "Segnalibro",
|
||||
"Cancel": "Annulla",
|
||||
"Chapter": "Capitolo",
|
||||
"Cherry": "Ciliegia",
|
||||
"Color": "Colore",
|
||||
"Confirm": "Conferma",
|
||||
"Confirm Deletion": "Conferma eliminazione",
|
||||
"Custom CSS": "CSS personalizzato",
|
||||
"Dark Mode": "Modalità scura",
|
||||
"Default": "Predefinito",
|
||||
"Default Font": "Font predefinito",
|
||||
"Default Font Size": "Dimensione font predefinita",
|
||||
"Delete": "Elimina",
|
||||
"Disable Click-to-Flip": "Disattiva click per voltare pagina",
|
||||
"Download Readest": "Scarica Readest",
|
||||
"Edit": "Modifica",
|
||||
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
|
||||
"Excerpts": "Estratti",
|
||||
"Failed to import book(s): {{filenames}}": "Impossibile importare il/i libro/i: {{filenames}}",
|
||||
"Font": "Font",
|
||||
"Font & Layout": "Font e Layout",
|
||||
"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",
|
||||
"Go Left": "Vai a sinistra",
|
||||
"Go Right": "Vai a destra",
|
||||
"Grass": "Erba",
|
||||
"Gray": "Grigio",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Direzione orizzontale",
|
||||
"Hyphenation": "Sillabazione",
|
||||
"Identifier:": "Identificatore:",
|
||||
"Import Books": "Importa libri",
|
||||
"Invert Colors in Dark Mode": "Inverti colori in modalità scura",
|
||||
"Language:": "Lingua:",
|
||||
"Layout": "Layout",
|
||||
"Light Mode": "Modalità chiara",
|
||||
"Line Height": "Altezza riga",
|
||||
"Loading...": "Caricamento...",
|
||||
"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",
|
||||
"Maximum Block Size": "Dimensione massima blocco",
|
||||
"Maximum Inline Size": "Dimensione massima in linea",
|
||||
"Maximum Number of Columns": "Numero massimo di colonne",
|
||||
"Minimum Font Size": "Dimensione minima font",
|
||||
"Misc": "Varie",
|
||||
"Monospace Font": "Font monospazio",
|
||||
"More Info": "Maggiori informazioni",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Quaderno",
|
||||
"Notes": "Note",
|
||||
"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",
|
||||
"Parallel Read": "Lettura parallela",
|
||||
"Published:": "Pubblicato:",
|
||||
"Publisher:": "Editore:",
|
||||
"Reading progress synced": "Progresso lettura sincronizzato",
|
||||
"Reload Page": "Ricarica pagina",
|
||||
"Reveal in File Explorer": "Mostra in Esplora file",
|
||||
"Reveal in Finder": "Mostra nel Finder",
|
||||
"Reveal in Folder": "Mostra nella cartella",
|
||||
"Sans-Serif Font": "Font sans-serif",
|
||||
"Save": "Salva",
|
||||
"Scrolled Mode": "Modalità scorrimento",
|
||||
"Search books...": "Cerca libri...",
|
||||
"Search...": "Cerca...",
|
||||
"Select Book": "Seleziona libro",
|
||||
"Select books": "Seleziona libri",
|
||||
"Select multiple books": "Seleziona più libri",
|
||||
"Sepia": "Seppia",
|
||||
"Serif Font": "Font serif",
|
||||
"Show Book Details": "Mostra dettagli libro",
|
||||
"Sidebar": "Barra laterale",
|
||||
"Sign In": "Accedi",
|
||||
"Sign Out": "Esci",
|
||||
"Sky": "Cielo",
|
||||
"Solarized": "Solarizzato",
|
||||
"Subjects:": "Argomenti:",
|
||||
"System Fonts": "Font di sistema",
|
||||
"Theme Color": "Colore tema",
|
||||
"Theme Mode": "Modalità tema",
|
||||
"Translated Text": "Testo tradotto",
|
||||
"Unknown": "Sconosciuto",
|
||||
"Untitled": "Senza titolo",
|
||||
"Updated:": "Aggiornato:",
|
||||
"User avatar": "Avatar utente",
|
||||
"Version {{version}}": "Versione {{version}}",
|
||||
"Vertical Direction": "Direzione verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
|
||||
"Writing Mode": "Modalità scrittura",
|
||||
"Your Library": "La tua biblioteca"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(検出)",
|
||||
"About Readest": "Readestについて",
|
||||
"Add your notes here...": "ここにメモを追加...",
|
||||
"Animation": "アニメーション",
|
||||
"Apply": "適用",
|
||||
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
|
||||
"Auto Mode": "自動モード",
|
||||
"Behavior": "動作",
|
||||
"Book": "書籍",
|
||||
"Book Cover": "表紙",
|
||||
"Bookmark": "ブックマーク",
|
||||
"Cancel": "キャンセル",
|
||||
"Chapter": "章",
|
||||
"Cherry": "チェリー",
|
||||
"Color": "色",
|
||||
"Confirm": "確認",
|
||||
"Confirm Deletion": "削除の確認",
|
||||
"Custom CSS": "カスタムCSS",
|
||||
"Dark Mode": "ダークモード",
|
||||
"Default": "デフォルト",
|
||||
"Default Font": "デフォルトフォント",
|
||||
"Default Font Size": "デフォルトフォントサイズ",
|
||||
"Delete": "削除",
|
||||
"Disable Click-to-Flip": "クリックめくりを無効化",
|
||||
"Download Readest": "Readestをダウンロード",
|
||||
"Edit": "編集",
|
||||
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
|
||||
"Excerpts": "抜粋",
|
||||
"Failed to import book(s): {{filenames}}": "書籍のインポートに失敗しました:{{filenames}}",
|
||||
"Font": "フォント",
|
||||
"Font & Layout": "フォントとレイアウト",
|
||||
"Font Face": "書体",
|
||||
"Font Family": "フォントファミリー",
|
||||
"Font Size": "フォントサイズ",
|
||||
"From Local File": "ローカルファイルから",
|
||||
"Full Justification": "両端揃え",
|
||||
"Gaps (%)": "間隔 (%)",
|
||||
"Global Settings": "全体設定",
|
||||
"Go Back": "戻る",
|
||||
"Go Forward": "進む",
|
||||
"Go Left": "左へ",
|
||||
"Go Right": "右へ",
|
||||
"Grass": "グリーン",
|
||||
"Gray": "グレー",
|
||||
"Gruvbox": "グルーブボックス",
|
||||
"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}}としてログイン中",
|
||||
"Margins (px)": "余白 (px)",
|
||||
"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": "原文",
|
||||
"Override Publisher Font": "出版社フォントを上書き",
|
||||
"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 books...": "書籍を検索...",
|
||||
"Search...": "検索...",
|
||||
"Select Book": "書籍を選択",
|
||||
"Select books": "書籍を選択",
|
||||
"Select multiple books": "複数の書籍を選択",
|
||||
"Sepia": "セピア",
|
||||
"Serif Font": "明朝体",
|
||||
"Show Book Details": "書籍の詳細を表示",
|
||||
"Sidebar": "サイドバー",
|
||||
"Sign In": "サインイン",
|
||||
"Sign Out": "サインアウト",
|
||||
"Sky": "スカイ",
|
||||
"Solarized": "ソーラライズド",
|
||||
"Subjects:": "主題:",
|
||||
"System Fonts": "システムフォント",
|
||||
"Theme Color": "テーマカラー",
|
||||
"Theme Mode": "テーマモード",
|
||||
"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.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
|
||||
"Writing Mode": "書き込みモード",
|
||||
"Your Library": "ライブラリー"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(감지됨)",
|
||||
"About Readest": "Readest 정보",
|
||||
"Add your notes here...": "여기에 메모를 추가하세요...",
|
||||
"Animation": "애니메이션",
|
||||
"Apply": "적용",
|
||||
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Auto Mode": "자동 모드",
|
||||
"Behavior": "동작",
|
||||
"Book": "책",
|
||||
"Book Cover": "책 표지",
|
||||
"Bookmark": "북마크",
|
||||
"Cancel": "취소",
|
||||
"Chapter": "챕터",
|
||||
"Cherry": "벚꽃색",
|
||||
"Color": "색상",
|
||||
"Confirm": "확인",
|
||||
"Confirm Deletion": "삭제 확인",
|
||||
"Custom CSS": "사용자 정의 CSS",
|
||||
"Dark Mode": "다크 모드",
|
||||
"Default": "기본값",
|
||||
"Default Font": "기본 글꼴",
|
||||
"Default Font Size": "기본 글꼴 크기",
|
||||
"Delete": "삭제",
|
||||
"Disable Click-to-Flip": "클릭 넘기기 비활성화",
|
||||
"Download Readest": "Readest 다운로드",
|
||||
"Edit": "편집",
|
||||
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
|
||||
"Excerpts": "발췌",
|
||||
"Failed to import book(s): {{filenames}}": "책 가져오기 실패: {{filenames}}",
|
||||
"Font": "글꼴",
|
||||
"Font & Layout": "글꼴 및 레이아웃",
|
||||
"Font Face": "글꼴 스타일",
|
||||
"Font Family": "글꼴 패밀리",
|
||||
"Font Size": "글꼴 크기",
|
||||
"From Local File": "로컬 파일에서",
|
||||
"Full Justification": "양쪽 정렬",
|
||||
"Gaps (%)": "간격 (%)",
|
||||
"Global Settings": "전역 설정",
|
||||
"Go Back": "뒤로",
|
||||
"Go Forward": "앞으로",
|
||||
"Go Left": "왼쪽으로",
|
||||
"Go Right": "오른쪽으로",
|
||||
"Grass": "잔디색",
|
||||
"Gray": "회색",
|
||||
"Gruvbox": "그러브박스",
|
||||
"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}}(으)로 로그인됨",
|
||||
"Margins (px)": "여백 (px)",
|
||||
"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": "원본 텍스트",
|
||||
"Override Publisher Font": "출판사 글꼴 재정의",
|
||||
"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 books...": "책 검색...",
|
||||
"Search...": "검색...",
|
||||
"Select Book": "책 선택",
|
||||
"Select books": "책 선택",
|
||||
"Select multiple books": "여러 책 선택",
|
||||
"Sepia": "세피아",
|
||||
"Serif Font": "세리프체",
|
||||
"Show Book Details": "책 세부 정보 표시",
|
||||
"Sidebar": "사이드바",
|
||||
"Sign In": "로그인",
|
||||
"Sign Out": "로그아웃",
|
||||
"Sky": "하늘색",
|
||||
"Solarized": "솔라라이즈드",
|
||||
"Subjects:": "주제:",
|
||||
"System Fonts": "시스템 글꼴",
|
||||
"Theme Color": "테마 색상",
|
||||
"Theme Mode": "테마 모드",
|
||||
"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.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
|
||||
"Writing Mode": "글쓰기 모드",
|
||||
"Your Library": "내 서재"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(detectado)",
|
||||
"About Readest": "Sobre o Readest",
|
||||
"Add your notes here...": "Adicione suas notas aqui...",
|
||||
"Animation": "Animação",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "Tem certeza de que deseja excluir os livros selecionados?",
|
||||
"Auto Mode": "Modo Automático",
|
||||
"Behavior": "Comportamento",
|
||||
"Book": "Livro",
|
||||
"Book Cover": "Capa do Livro",
|
||||
"Bookmark": "Marcador",
|
||||
"Cancel": "Cancelar",
|
||||
"Chapter": "Capítulo",
|
||||
"Cherry": "Cereja",
|
||||
"Color": "Cor",
|
||||
"Confirm": "Confirmar",
|
||||
"Confirm Deletion": "Confirmar Exclusão",
|
||||
"Custom CSS": "CSS Personalizado",
|
||||
"Dark Mode": "Modo Escuro",
|
||||
"Default": "Padrão",
|
||||
"Default Font": "Fonte Padrão",
|
||||
"Default Font Size": "Tamanho da Fonte Padrão",
|
||||
"Delete": "Excluir",
|
||||
"Disable Click-to-Flip": "Desativar Clique para Virar",
|
||||
"Download Readest": "Baixar Readest",
|
||||
"Edit": "Editar",
|
||||
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
|
||||
"Excerpts": "Trechos",
|
||||
"Failed to import book(s): {{filenames}}": "Falha ao importar livro(s): {{filenames}}",
|
||||
"Font": "Fonte",
|
||||
"Font & Layout": "Fonte e Layout",
|
||||
"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",
|
||||
"Go Left": "Ir para Esquerda",
|
||||
"Go Right": "Ir para Direita",
|
||||
"Grass": "Grama",
|
||||
"Gray": "Cinza",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Direção Horizontal",
|
||||
"Hyphenation": "Hifenização",
|
||||
"Identifier:": "Identificador:",
|
||||
"Import Books": "Importar Livros",
|
||||
"Invert Colors in Dark Mode": "Inverter Cores no Modo Escuro",
|
||||
"Language:": "Idioma:",
|
||||
"Layout": "Layout",
|
||||
"Light Mode": "Modo Claro",
|
||||
"Line Height": "Altura da Linha",
|
||||
"Loading...": "Carregando...",
|
||||
"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",
|
||||
"Maximum Block Size": "Tamanho Máximo do Bloco",
|
||||
"Maximum Inline Size": "Tamanho Máximo em Linha",
|
||||
"Maximum Number of Columns": "Número Máximo de Colunas",
|
||||
"Minimum Font Size": "Tamanho Mínimo da Fonte",
|
||||
"Misc": "Diversos",
|
||||
"Monospace Font": "Fonte Monoespaçada",
|
||||
"More Info": "Mais Informações",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Caderno",
|
||||
"Notes": "Notas",
|
||||
"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",
|
||||
"Parallel Read": "Leitura Paralela",
|
||||
"Published:": "Publicado:",
|
||||
"Publisher:": "Editora:",
|
||||
"Reading progress synced": "Progresso de leitura sincronizado",
|
||||
"Reload Page": "Recarregar Página",
|
||||
"Reveal in File Explorer": "Mostrar no Explorador de Arquivos",
|
||||
"Reveal in Finder": "Mostrar no Finder",
|
||||
"Reveal in Folder": "Mostrar na Pasta",
|
||||
"Sans-Serif Font": "Fonte Sans-Serif",
|
||||
"Save": "Salvar",
|
||||
"Scrolled Mode": "Modo de Rolagem",
|
||||
"Search books...": "Procurar livros...",
|
||||
"Search...": "Pesquisar...",
|
||||
"Select Book": "Selecionar Livro",
|
||||
"Select books": "Selecionar livros",
|
||||
"Select multiple books": "Selecionar múltiplos livros",
|
||||
"Sepia": "Sépia",
|
||||
"Serif Font": "Fonte Serif",
|
||||
"Show Book Details": "Mostrar Detalhes do Livro",
|
||||
"Sidebar": "Barra Lateral",
|
||||
"Sign In": "Entrar",
|
||||
"Sign Out": "Sair",
|
||||
"Sky": "Céu",
|
||||
"Solarized": "Solarizado",
|
||||
"Subjects:": "Assuntos:",
|
||||
"System Fonts": "Fontes do Sistema",
|
||||
"Theme Color": "Cor do Tema",
|
||||
"Theme Mode": "Modo do Tema",
|
||||
"Translated Text": "Texto Traduzido",
|
||||
"Unknown": "Desconhecido",
|
||||
"Untitled": "Sem Título",
|
||||
"Updated:": "Atualizado:",
|
||||
"User avatar": "Avatar do usuário",
|
||||
"Version {{version}}": "Versão {{version}}",
|
||||
"Vertical Direction": "Direção Vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
|
||||
"Writing Mode": "Modo de Escrita",
|
||||
"Your Library": "Sua Biblioteca"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(обнаружено)",
|
||||
"About Readest": "О Readest",
|
||||
"Add your notes here...": "Добавьте свои заметки здесь...",
|
||||
"Animation": "Анимация",
|
||||
"Apply": "Применить",
|
||||
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
|
||||
"Auto Mode": "Автоматический режим",
|
||||
"Behavior": "Поведение",
|
||||
"Book": "Книга",
|
||||
"Book Cover": "Обложка книги",
|
||||
"Bookmark": "Закладка",
|
||||
"Cancel": "Отмена",
|
||||
"Chapter": "Глава",
|
||||
"Cherry": "Вишня",
|
||||
"Color": "Цвет",
|
||||
"Confirm": "Подтвердить",
|
||||
"Confirm Deletion": "Подтвердить удаление",
|
||||
"Custom CSS": "Пользовательский CSS",
|
||||
"Dark Mode": "Тёмная тема",
|
||||
"Default": "По умолчанию",
|
||||
"Default Font": "Шрифт по умолчанию",
|
||||
"Default Font Size": "Размер шрифта по умолчанию",
|
||||
"Delete": "Удалить",
|
||||
"Disable Click-to-Flip": "Отключить переворот по клику",
|
||||
"Download Readest": "Скачать Readest",
|
||||
"Edit": "Редактировать",
|
||||
"Enter your custom CSS here...": "Введите ваш пользовательский CSS здесь...",
|
||||
"Excerpts": "Отрывки",
|
||||
"Failed to import book(s): {{filenames}}": "Не удалось импортировать книгу(и): {{filenames}}",
|
||||
"Font": "Шрифт",
|
||||
"Font & Layout": "Шрифт и макет",
|
||||
"Font Face": "Начертание шрифта",
|
||||
"Font Family": "Семейство шрифтов",
|
||||
"Font Size": "Размер шрифта",
|
||||
"From Local File": "Из локального файла",
|
||||
"Full Justification": "Полное выравнивание",
|
||||
"Gaps (%)": "Промежутки (%)",
|
||||
"Global Settings": "Общие настройки",
|
||||
"Go Back": "Назад",
|
||||
"Go Forward": "Вперёд",
|
||||
"Go Left": "Влево",
|
||||
"Go Right": "Вправо",
|
||||
"Grass": "Трава",
|
||||
"Gray": "Серый",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"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}}",
|
||||
"Margins (px)": "Отступы (px)",
|
||||
"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": "Оригинальный текст",
|
||||
"Override Publisher Font": "Переопределить шрифт издателя",
|
||||
"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 books...": "Поиск книг...",
|
||||
"Search...": "Поиск...",
|
||||
"Select Book": "Выбрать книгу",
|
||||
"Select books": "Выбрать книги",
|
||||
"Select multiple books": "Выбрать несколько книг",
|
||||
"Sepia": "Сепия",
|
||||
"Serif Font": "Шрифт с засечками",
|
||||
"Show Book Details": "Показать детали книги",
|
||||
"Sidebar": "Боковая панель",
|
||||
"Sign In": "Войти",
|
||||
"Sign Out": "Выйти",
|
||||
"Sky": "Небесный",
|
||||
"Solarized": "Солнечный",
|
||||
"Subjects:": "Темы:",
|
||||
"System Fonts": "Системные шрифты",
|
||||
"Theme Color": "Цвет темы",
|
||||
"Theme Mode": "Режим темы",
|
||||
"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.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
|
||||
"Writing Mode": "Режим записи",
|
||||
"Your Library": "Ваша библиотека"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(algılandı)",
|
||||
"About Readest": "Readest Hakkında",
|
||||
"Add your notes here...": "Notlarınızı buraya ekleyin...",
|
||||
"Animation": "Animasyon",
|
||||
"Apply": "Uygula",
|
||||
"Are you sure to delete the selected books?": "Seçili kitapları silmek istediğinizden emin misiniz?",
|
||||
"Auto Mode": "Otomatik Mod",
|
||||
"Behavior": "Davranış",
|
||||
"Book": "Kitap",
|
||||
"Book Cover": "Kitap Kapağı",
|
||||
"Bookmark": "Yer İmi",
|
||||
"Cancel": "İptal",
|
||||
"Chapter": "Bölüm",
|
||||
"Cherry": "Kiraz",
|
||||
"Color": "Renk",
|
||||
"Confirm": "Onayla",
|
||||
"Confirm Deletion": "Silmeyi Onayla",
|
||||
"Custom CSS": "Özel CSS",
|
||||
"Dark Mode": "Karanlık Mod",
|
||||
"Default": "Varsayılan",
|
||||
"Default Font": "Varsayılan Yazı Tipi",
|
||||
"Default Font Size": "Varsayılan Yazı Boyutu",
|
||||
"Delete": "Sil",
|
||||
"Disable Click-to-Flip": "Tıkla-Çevir'i Devre Dışı Bırak",
|
||||
"Download Readest": "Readest'i İndir",
|
||||
"Edit": "Düzenle",
|
||||
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
|
||||
"Excerpts": "Alıntılar",
|
||||
"Failed to import book(s): {{filenames}}": "Kitap(lar) içe aktarılamadı: {{filenames}}",
|
||||
"Font": "Yazı Tipi",
|
||||
"Font & Layout": "Yazı Tipi ve Düzen",
|
||||
"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",
|
||||
"Go Left": "Sola Git",
|
||||
"Go Right": "Sağa Git",
|
||||
"Grass": "Çimen",
|
||||
"Gray": "Gri",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Yatay Yön",
|
||||
"Hyphenation": "Heceleme",
|
||||
"Identifier:": "Tanımlayıcı:",
|
||||
"Import Books": "Kitapları İçe Aktar",
|
||||
"Invert Colors in Dark Mode": "Karanlık Modda Renkleri Ters Çevir",
|
||||
"Language:": "Dil:",
|
||||
"Layout": "Düzen",
|
||||
"Light Mode": "Aydınlık Mod",
|
||||
"Line Height": "Satır Yüksekliği",
|
||||
"Loading...": "Yükleniyor...",
|
||||
"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",
|
||||
"Maximum Block Size": "Maksimum Blok Boyutu",
|
||||
"Maximum Inline Size": "Maksimum Satır İçi Boyut",
|
||||
"Maximum Number of Columns": "Maksimum Sütun Sayısı",
|
||||
"Minimum Font Size": "Minimum Yazı Boyutu",
|
||||
"Misc": "Diğer",
|
||||
"Monospace Font": "Eş Aralıklı Yazı Tipi",
|
||||
"More Info": "Daha Fazla Bilgi",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Not Defteri",
|
||||
"Notes": "Notlar",
|
||||
"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",
|
||||
"Parallel Read": "Paralel Okuma",
|
||||
"Published:": "Yayınlanma:",
|
||||
"Publisher:": "Yayıncı:",
|
||||
"Reading progress synced": "Okuma ilerlemesi senkronize edildi",
|
||||
"Reload Page": "Sayfayı Yenile",
|
||||
"Reveal in File Explorer": "Dosya Gezgininde Göster",
|
||||
"Reveal in Finder": "Finder'da Göster",
|
||||
"Reveal in Folder": "Klasörde Göster",
|
||||
"Sans-Serif Font": "Sans-Serif Yazı Tipi",
|
||||
"Save": "Kaydet",
|
||||
"Scrolled Mode": "Kaydırma Modu",
|
||||
"Search books...": "Kitap ara...",
|
||||
"Search...": "Ara...",
|
||||
"Select Book": "Kitap Seç",
|
||||
"Select books": "Kitapları seç",
|
||||
"Select multiple books": "Birden fazla kitap seç",
|
||||
"Sepia": "Sepya",
|
||||
"Serif Font": "Serif Yazı Tipi",
|
||||
"Show Book Details": "Kitap Detaylarını Göster",
|
||||
"Sidebar": "Kenar Çubuğu",
|
||||
"Sign In": "Giriş Yap",
|
||||
"Sign Out": "Çıkış Yap",
|
||||
"Sky": "Gökyüzü",
|
||||
"Solarized": "Solarized",
|
||||
"Subjects:": "Konular:",
|
||||
"System Fonts": "Sistem Yazı Tipleri",
|
||||
"Theme Color": "Tema Rengi",
|
||||
"Theme Mode": "Tema Modu",
|
||||
"Translated Text": "Çevrilen Metin",
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Untitled": "Başlıksız",
|
||||
"Updated:": "Güncellendi:",
|
||||
"User avatar": "Kullanıcı avatarı",
|
||||
"Version {{version}}": "Sürüm {{version}}",
|
||||
"Vertical Direction": "Dikey Yön",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
|
||||
"Writing Mode": "Yazma Modu",
|
||||
"Your Library": "Kütüphaneniz"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(đã phát hiện)",
|
||||
"About Readest": "Về Readest",
|
||||
"Add your notes here...": "Thêm ghi chú của bạn vào đây...",
|
||||
"Animation": "Hiệu ứng động",
|
||||
"Apply": "Áp dụng",
|
||||
"Are you sure to delete the selected books?": "Bạn có chắc chắn muốn xóa các sách đã chọn?",
|
||||
"Auto Mode": "Chế độ tự động",
|
||||
"Behavior": "Hành vi",
|
||||
"Book": "Sách",
|
||||
"Book Cover": "Bìa sách",
|
||||
"Bookmark": "Đánh dấu",
|
||||
"Cancel": "Hủy",
|
||||
"Chapter": "Chương",
|
||||
"Cherry": "Đỏ anh đào",
|
||||
"Color": "Màu sắc",
|
||||
"Confirm": "Xác nhận",
|
||||
"Confirm Deletion": "Xác nhận xóa",
|
||||
"Custom CSS": "CSS tùy chỉnh",
|
||||
"Dark Mode": "Chế độ tối",
|
||||
"Default": "Mặc định",
|
||||
"Default Font": "Phông chữ mặc định",
|
||||
"Default Font Size": "Cỡ chữ mặc định",
|
||||
"Delete": "Xóa",
|
||||
"Disable Click-to-Flip": "Tắt tính năng nhấp để lật trang",
|
||||
"Download Readest": "Tải Readest",
|
||||
"Edit": "Chỉnh sửa",
|
||||
"Enter your custom CSS here...": "Nhập CSS tùy chỉnh của bạn vào đây...",
|
||||
"Excerpts": "Trích dẫn",
|
||||
"Failed to import book(s): {{filenames}}": "Không thể nhập sách: {{filenames}}",
|
||||
"Font": "Phông chữ",
|
||||
"Font & Layout": "Phông chữ & Bố cục",
|
||||
"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",
|
||||
"Go Left": "Sang trái",
|
||||
"Go Right": "Sang phải",
|
||||
"Grass": "Xanh cỏ",
|
||||
"Gray": "Xám",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Hướng ngang",
|
||||
"Hyphenation": "Gạch nối từ",
|
||||
"Identifier:": "Định danh:",
|
||||
"Import Books": "Nhập sách",
|
||||
"Invert Colors in Dark Mode": "Đảo màu trong chế độ tối",
|
||||
"Language:": "Ngôn ngữ:",
|
||||
"Layout": "Bố cục",
|
||||
"Light Mode": "Chế độ sáng",
|
||||
"Line Height": "Độ cao dòng",
|
||||
"Loading...": "Đang tải...",
|
||||
"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ừ",
|
||||
"Maximum Block Size": "Kích thước khối tối đa",
|
||||
"Maximum Inline Size": "Kích thước nội tuyến tối đa",
|
||||
"Maximum Number of Columns": "Số cột tối đa",
|
||||
"Minimum Font Size": "Cỡ chữ tối thiểu",
|
||||
"Misc": "Khác",
|
||||
"Monospace Font": "Phông chữ đơn cách",
|
||||
"More Info": "Thông tin thêm",
|
||||
"Nord": "Nord",
|
||||
"Notebook": "Sổ ghi chép",
|
||||
"Notes": "Ghi chú",
|
||||
"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",
|
||||
"Parallel Read": "Đọc song song",
|
||||
"Published:": "Xuất bản:",
|
||||
"Publisher:": "Nhà xuất bản:",
|
||||
"Reading progress synced": "Tiến độ đọc đã được đồng bộ",
|
||||
"Reload Page": "Tải lại trang",
|
||||
"Reveal in File Explorer": "Hiển thị trong File Explorer",
|
||||
"Reveal in Finder": "Hiển thị trong Finder",
|
||||
"Reveal in Folder": "Hiển thị trong thư mục",
|
||||
"Sans-Serif Font": "Phông chữ không chân",
|
||||
"Save": "Lưu",
|
||||
"Scrolled Mode": "Chế độ cuộn",
|
||||
"Search books...": "Tìm kiếm sách...",
|
||||
"Search...": "Tìm kiếm...",
|
||||
"Select Book": "Chọn sách",
|
||||
"Select books": "Chọn sách",
|
||||
"Select multiple books": "Chọn nhiều sách",
|
||||
"Sepia": "Nâu cổ",
|
||||
"Serif Font": "Phông chữ có chân",
|
||||
"Show Book Details": "Hiển thị chi tiết sách",
|
||||
"Sidebar": "Thanh bên",
|
||||
"Sign In": "Đăng nhập",
|
||||
"Sign Out": "Đăng xuất",
|
||||
"Sky": "Xanh trời",
|
||||
"Solarized": "Solarized",
|
||||
"Subjects:": "Chủ đề:",
|
||||
"System Fonts": "Phông chữ hệ thống",
|
||||
"Theme Color": "Màu chủ đề",
|
||||
"Theme Mode": "Chế độ chủ đề",
|
||||
"Translated Text": "Văn bản dịch",
|
||||
"Unknown": "Không xác định",
|
||||
"Untitled": "Không có tiêu đề",
|
||||
"Updated:": "Cập nhật:",
|
||||
"User avatar": "Ảnh đại diện",
|
||||
"Version {{version}}": "Phiên bản {{version}}",
|
||||
"Vertical Direction": "Hướng dọc",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
|
||||
"Writing Mode": "Chế độ viết",
|
||||
"Your Library": "Thư viện của bạn"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(检测到)",
|
||||
"About Readest": "关于 Readest",
|
||||
"Add your notes here...": "在这里添加您的笔记...",
|
||||
"Animation": "动画",
|
||||
"Apply": "应用",
|
||||
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
|
||||
"Auto Mode": "自动主题",
|
||||
"Behavior": "行为",
|
||||
"Book": "书籍",
|
||||
"Book Cover": "书籍封面",
|
||||
"Bookmark": "书签",
|
||||
"Cancel": "取消",
|
||||
"Chapter": "章节",
|
||||
"Cherry": "樱粉",
|
||||
"Color": "颜色",
|
||||
"Confirm": "确认",
|
||||
"Confirm Deletion": "确认删除",
|
||||
"Custom CSS": "自定义 CSS",
|
||||
"Dark Mode": "深色主题",
|
||||
"Default": "默认",
|
||||
"Default Font": "默认字体",
|
||||
"Default Font Size": "默认字号",
|
||||
"Delete": "删除",
|
||||
"Disable Click-to-Flip": "禁用点击翻页",
|
||||
"Download Readest": "下载 Readest",
|
||||
"Edit": "编辑",
|
||||
"Enter your custom CSS here...": "在此处输入您的自定义 CSS...",
|
||||
"Excerpts": "摘录",
|
||||
"Failed to import book(s): {{filenames}}": "导入书籍失败:{{filenames}}",
|
||||
"Font": "字体",
|
||||
"Font & Layout": "字体和布局",
|
||||
"Font Face": "字型",
|
||||
"Font Family": "字族",
|
||||
"Font Size": "字号",
|
||||
"From Local File": "从本地文件导入",
|
||||
"Full Justification": "两端对齐",
|
||||
"Gaps (%)": "间距 (%)",
|
||||
"Global Settings": "全局设置",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前进",
|
||||
"Go Left": "向左",
|
||||
"Go Right": "向右",
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"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}} 已登录",
|
||||
"Margins (px)": "页边距 (px)",
|
||||
"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": "原文",
|
||||
"Override Publisher Font": "覆盖内置字体",
|
||||
"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 books...": "搜索书籍...",
|
||||
"Search...": "搜索...",
|
||||
"Select Book": "选择书籍",
|
||||
"Select books": "选择书籍",
|
||||
"Select multiple books": "选择多本书籍",
|
||||
"Sepia": "旧韵",
|
||||
"Serif Font": "衬线字体",
|
||||
"Show Book Details": "显示书籍详情",
|
||||
"Sidebar": "侧边栏",
|
||||
"Sign In": "登录",
|
||||
"Sign Out": "登出",
|
||||
"Sky": "天青",
|
||||
"Solarized": "日晖",
|
||||
"Subjects:": "主题",
|
||||
"System Fonts": "系统字体",
|
||||
"Theme Color": "主题颜色",
|
||||
"Theme Mode": "主题模式",
|
||||
"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.": "书库空空如也,您可以导入您的书籍并随时阅读。",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "书库"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"(detected)": "(檢測到)",
|
||||
"About Readest": "關於 Readest",
|
||||
"Add your notes here...": "在這裡添加您的筆記...",
|
||||
"Animation": "動畫",
|
||||
"Apply": "應用",
|
||||
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
|
||||
"Auto Mode": "自動主題",
|
||||
"Behavior": "行為",
|
||||
"Book": "書籍",
|
||||
"Book Cover": "書籍封面",
|
||||
"Bookmark": "書籤",
|
||||
"Cancel": "取消",
|
||||
"Chapter": "章節",
|
||||
"Cherry": "櫻粉",
|
||||
"Color": "顏色",
|
||||
"Confirm": "確認",
|
||||
"Confirm Deletion": "確認刪除",
|
||||
"Custom CSS": "自定義 CSS",
|
||||
"Dark Mode": "深色主題",
|
||||
"Default": "預設",
|
||||
"Default Font": "預設字體",
|
||||
"Default Font Size": "預設字號",
|
||||
"Delete": "刪除",
|
||||
"Disable Click-to-Flip": "禁用點擊翻頁",
|
||||
"Download Readest": "下載 Readest",
|
||||
"Edit": "編輯",
|
||||
"Enter your custom CSS here...": "在此處輸入您的自定義 CSS...",
|
||||
"Excerpts": "摘錄",
|
||||
"Failed to import book(s): {{filenames}}": "導入書籍失敗:{{filenames}}",
|
||||
"Font": "字體",
|
||||
"Font & Layout": "字體和版面",
|
||||
"Font Face": "字型",
|
||||
"Font Family": "字族",
|
||||
"Font Size": "字號",
|
||||
"From Local File": "從本地檔案導入",
|
||||
"Full Justification": "兩端對齊",
|
||||
"Gaps (%)": "間距 (%)",
|
||||
"Global Settings": "全局設置",
|
||||
"Go Back": "返回",
|
||||
"Go Forward": "前進",
|
||||
"Go Left": "向左",
|
||||
"Go Right": "向右",
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"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}} 已登入",
|
||||
"Margins (px)": "頁邊距 (px)",
|
||||
"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": "原文",
|
||||
"Override Publisher Font": "覆蓋內置字體",
|
||||
"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 books...": "搜索書籍...",
|
||||
"Search...": "搜索...",
|
||||
"Select Book": "選擇書籍",
|
||||
"Select books": "選擇書籍",
|
||||
"Select multiple books": "選擇多本書籍",
|
||||
"Sepia": "舊韻",
|
||||
"Serif Font": "襯線字體",
|
||||
"Show Book Details": "顯示書籍詳情",
|
||||
"Sidebar": "側邊欄",
|
||||
"Sign In": "登入",
|
||||
"Sign Out": "登出",
|
||||
"Sky": "天青",
|
||||
"Solarized": "日暉",
|
||||
"Subjects:": "主題",
|
||||
"System Fonts": "系統字體",
|
||||
"Theme Color": "主題顏色",
|
||||
"Theme Mode": "主題模式",
|
||||
"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.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "書庫"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.1": {
|
||||
"date": "2025-01-06",
|
||||
"notes": [
|
||||
"Restore window position and size when reopening app.",
|
||||
"Swap font when downloading online fonts.",
|
||||
"Fix layout issues on Windows and Linux."
|
||||
]
|
||||
},
|
||||
"0.9.0": {
|
||||
"date": "2025-01-04",
|
||||
"notes": [
|
||||
"Better Custom CSS editor and OAuth process.",
|
||||
"Add vertical/horizontal switch for CJK books.",
|
||||
"Fix dropdown close overlay not working on Windows/Linux."
|
||||
]
|
||||
},
|
||||
"0.8.9": {
|
||||
"date": "2025-01-02",
|
||||
"notes": [
|
||||
"Context menu on bookshelf for quick actions.",
|
||||
"Add more system fonts as custom fonts.",
|
||||
"Fix google and github login for native apps."
|
||||
]
|
||||
},
|
||||
"0.8.8": {
|
||||
"date": "2024-12-29",
|
||||
"notes": [
|
||||
"Use readest binary name for better CLI interface.",
|
||||
"Some CSS tweaks to better support dark mode and embedded fonts."
|
||||
]
|
||||
},
|
||||
"0.8.7": {
|
||||
"date": "2024-12-27",
|
||||
"notes": [
|
||||
"Support i18n/l10n with popular languages.",
|
||||
"Support information modal for book details.",
|
||||
"Various fixes and enhancements."
|
||||
]
|
||||
},
|
||||
"0.8.6": {
|
||||
"date": "2024-12-25",
|
||||
"notes": [
|
||||
"Support back navigation in login page.",
|
||||
"Support context menu on book cover for desktop version.",
|
||||
"Resolve the issue with restoring footnotes and bookmarks after deletion."
|
||||
]
|
||||
},
|
||||
"0.8.5": {
|
||||
"date": "2024-12-24",
|
||||
"notes": [
|
||||
"Support progress and notes sync across devices.",
|
||||
"Support for Non-ASCII characters in custom CSS.",
|
||||
"Support to disable click-to-flip."
|
||||
]
|
||||
},
|
||||
"0.8.3": {
|
||||
"date": "2024-12-12",
|
||||
"notes": [
|
||||
"Support popover footnotes.",
|
||||
"Support left / right popover on vertical writing documents.",
|
||||
"Support loading additional fonts."
|
||||
]
|
||||
},
|
||||
"0.8.2": {
|
||||
"date": "2024-12-06",
|
||||
"notes": [
|
||||
"Support file associations for one-click open with Readest.",
|
||||
"Support APP auto updater.",
|
||||
"Support online web version."
|
||||
]
|
||||
},
|
||||
"0.7.9": {
|
||||
"date": "2024-12-01",
|
||||
"notes": [
|
||||
"Initial release with full functionality for ePub rendering.",
|
||||
"Support multi-book reading with Parallel Read."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "Readest"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
description = "Your online library"
|
||||
authors = ["Bilingify LLC"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.71"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -33,6 +33,8 @@ tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-oauth = "2"
|
||||
tauri-plugin-opener = "2.2.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
[patch.crates-io]
|
||||
tauri = { path = "../../../packages/tauri/crates/tauri" }
|
||||
|
||||
@@ -43,4 +45,6 @@ rand = "0.8"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
tauri-plugin-single-instance = "2.2.0"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-window-state = "2.2.0"
|
||||
|
||||
@@ -51,15 +51,21 @@
|
||||
"os:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-center",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"shell:default",
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"process:allow-exit",
|
||||
"process:allow-restart",
|
||||
"cli:default",
|
||||
"oauth:allow-start",
|
||||
"oauth:allow-cancel"
|
||||
"oauth:allow-cancel",
|
||||
"opener:default",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "desktop-capability",
|
||||
"windows": ["main"],
|
||||
"platforms": ["linux", "macOS", "windows"],
|
||||
"permissions": [
|
||||
"updater:default",
|
||||
"cli:default"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
@@ -9,7 +9,7 @@ extern crate objc;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod tauri_traffic_light_positioner_plugin;
|
||||
mod traffic_light_plugin;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
@@ -70,20 +70,45 @@ async fn start_server(window: Window) -> Result<u16, String> {
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
args: Vec<String>,
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_oauth::init())
|
||||
.invoke_handler(tauri::generate_handler![start_server])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
app.emit("single-instance", Payload { args: argv, cwd })
|
||||
.unwrap();
|
||||
}));
|
||||
|
||||
let builder = builder.plugin(tauri_plugin_deep_link::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.plugin(tauri_traffic_light_positioner_plugin::init());
|
||||
let builder = builder.plugin(traffic_light_plugin::init());
|
||||
|
||||
builder
|
||||
.setup(|#[allow(unused_variables)] app| {
|
||||
@@ -92,7 +117,7 @@ pub fn run() {
|
||||
let mut files = Vec::new();
|
||||
// NOTICE: `args` may include URL protocol (`your-app-protocol://`)
|
||||
// or arguments (`--`) if your app supports them.
|
||||
// files may aslo be passed as `file://path/to/file`
|
||||
// files may also be passed as `file://path/to/file`
|
||||
for maybe_file in std::env::args().skip(1) {
|
||||
// skip flags like -f or --flag
|
||||
if maybe_file.starts_with("-") {
|
||||
@@ -118,8 +143,25 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
app.listen("window-ready", move |_| {
|
||||
app_handle.get_webview_window("main").unwrap()
|
||||
.eval("window.__READEST_CLI_ACCESS = true; window.__READEST_UPDATER_ACCESS = true;")
|
||||
.expect("Failed to set cli access config");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
@@ -128,18 +170,27 @@ pub fn run() {
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
|
||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let win_builder = win_builder
|
||||
.inner_size(800.0, 600.0)
|
||||
.resizable(true)
|
||||
.maximized(true);
|
||||
.resizable(true);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win_builder = win_builder
|
||||
.decorations(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay).title("");
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.title("");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let win_builder = win_builder.decorations(false).transparent(true).title("Readest");
|
||||
#[cfg(all(not(target_os = "macos"), desktop))]
|
||||
let win_builder = win_builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.shadow(true)
|
||||
.title("Readest");
|
||||
|
||||
win_builder.build().unwrap();
|
||||
// let win = win_builder.build().unwrap();
|
||||
@@ -157,7 +208,7 @@ pub fn run() {
|
||||
.run(
|
||||
#[allow(unused_variables)]
|
||||
|app_handle, event| {
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Opened { urls } = event {
|
||||
let files = urls
|
||||
.into_iter()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tauri::menu::MenuEvent;
|
||||
use tauri::menu::{SubmenuBuilder, HELP_SUBMENU_ID};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
pub fn setup_macos_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
let global_menu = app.menu().unwrap();
|
||||
@@ -27,17 +27,12 @@ pub fn setup_macos_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
}
|
||||
|
||||
pub fn handle_menu_event(app: &AppHandle, event: &MenuEvent) {
|
||||
let opener = app.opener();
|
||||
if event.id() == "privacy_policy" {
|
||||
if let Err(e) = app.shell().open("https://readest.com/privacy-policy", None) {
|
||||
eprintln!("Failed to open privacy policy: {}", e);
|
||||
}
|
||||
let _ = opener.open_url("https://readest.com/privacy-policy", None::<&str>);
|
||||
} else if event.id() == "report_issue" {
|
||||
if let Err(e) = app.shell().open("mailto:support@bilingify.com", None) {
|
||||
eprintln!("Failed to open mail client: {}", e);
|
||||
}
|
||||
let _ = opener.open_url("https://github.com/readest/readest/issues", None::<&str>);
|
||||
} else if event.id() == "readest_help" {
|
||||
if let Err(e) = app.shell().open("https://readest.com/support", None) {
|
||||
eprintln!("Failed to open support page: {}", e);
|
||||
}
|
||||
let _ = opener.open_url("https://readest.com/support", None::<&str>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Readest",
|
||||
"version": "../package.json",
|
||||
"mainBinaryName": "readest",
|
||||
"identifier": "com.bilingify.readest",
|
||||
"version": "../package.json",
|
||||
"build": {
|
||||
"frontendDist": "../out",
|
||||
"devUrl": "http://localhost:3000",
|
||||
@@ -49,6 +50,11 @@
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "12.0"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"section": "text"
|
||||
}
|
||||
},
|
||||
"fileAssociations": [
|
||||
{
|
||||
"name": "epub",
|
||||
@@ -124,9 +130,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"deep-link": {
|
||||
"mobile": [{ "host": "web.readest.com" }],
|
||||
"desktop": {
|
||||
"schemes": ["readest"]
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0OTAxMURGQkUzQjFENTQKUldSVUhUdSszeEdRNUExdmFkWnlvYWNYNG5wamkxMmUxRk5SejlMOTJVd28yNXlVTDh6Wld4OC8K",
|
||||
"endpoints": ["https://github.com/chrox/readest/releases/latest/download/latest.json"]
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
|
||||
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,26 @@ import { FcGoogle } from 'react-icons/fc';
|
||||
import { FaApple } from 'react-icons/fa';
|
||||
import { VscAzure } from 'react-icons/vsc';
|
||||
import { FaGithub } from 'react-icons/fa';
|
||||
import { IoArrowBack } from 'react-icons/io5';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
|
||||
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
|
||||
|
||||
interface SingleInstancePayload {
|
||||
args: string[];
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
interface ProviderLoginProp {
|
||||
provider: OAuthProvider;
|
||||
handleSignIn: (provider: OAuthProvider) => void;
|
||||
@@ -45,9 +54,12 @@ const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Ic
|
||||
export default function AuthPage() {
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
const { envConfig } = useEnv();
|
||||
const { isDarkMode } = useTheme();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const [port, setPort] = useState<number | null>(null);
|
||||
const isOAuthServerRunning = useRef(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const signIn = async (provider: OAuthProvider) => {
|
||||
if (!supabase) {
|
||||
@@ -58,7 +70,10 @@ export default function AuthPage() {
|
||||
provider,
|
||||
options: {
|
||||
skipBrowserRedirect: true,
|
||||
redirectTo: `http://localhost:${port}`,
|
||||
redirectTo:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? 'readest://auth/callback'
|
||||
: `http://localhost:${port}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -66,37 +81,57 @@ export default function AuthPage() {
|
||||
console.error('Authentication error:', error);
|
||||
return;
|
||||
}
|
||||
open(data.url);
|
||||
openUrl(data.url);
|
||||
};
|
||||
|
||||
const startOAuthServer = async () => {
|
||||
const handleOAuthUrl = async (url: string) => {
|
||||
console.log('Received OAuth URL:', url);
|
||||
const hashMatch = url.match(/#(.*)/);
|
||||
if (hashMatch) {
|
||||
const hash = hashMatch[1];
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get('access_token');
|
||||
const refreshToken = params.get('refresh_token');
|
||||
const next = params.get('next') ?? '/';
|
||||
if (accessToken) {
|
||||
handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startTauriOAuth = async () => {
|
||||
try {
|
||||
const port = await start();
|
||||
setPort(port);
|
||||
console.log(`OAuth server started on port ${port}`);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
currentWindow.listen('single-instance', ({ event, payload }) => {
|
||||
console.log('Received deep link:', event, payload);
|
||||
const { args } = payload as SingleInstancePayload;
|
||||
if (args?.[1]) {
|
||||
handleOAuthUrl(args[1]);
|
||||
}
|
||||
});
|
||||
await onOpenUrl((urls) => {
|
||||
urls.forEach((url) => {
|
||||
handleOAuthUrl(url);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const port = await start();
|
||||
setPort(port);
|
||||
console.log(`OAuth server started on port ${port}`);
|
||||
|
||||
await onUrl((url) => {
|
||||
console.log('Received OAuth URL:', url);
|
||||
const hashMatch = url.match(/#(.*)/);
|
||||
if (hashMatch) {
|
||||
const hash = hashMatch[1];
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get('access_token');
|
||||
const refreshToken = params.get('refresh_token');
|
||||
const next = params.get('next') ?? '/';
|
||||
handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push });
|
||||
}
|
||||
});
|
||||
|
||||
await onInvalidUrl((url) => {
|
||||
console.log('Received invalid OAuth URL:', url);
|
||||
});
|
||||
await onUrl(handleOAuthUrl);
|
||||
await onInvalidUrl((url) => {
|
||||
console.log('Received invalid OAuth URL:', url);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting OAuth server:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const stopOAuthServer = async () => {
|
||||
const stopTauriOAuth = async () => {
|
||||
try {
|
||||
if (port) {
|
||||
await cancel(port);
|
||||
@@ -107,15 +142,23 @@ export default function AuthPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
// Keep login false to avoid infinite loop to redirect to the login page
|
||||
settings.keepLogin = false;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
router.back();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriAppPlatform()) return;
|
||||
if (isOAuthServerRunning.current) return;
|
||||
isOAuthServerRunning.current = true;
|
||||
|
||||
startOAuthServer();
|
||||
startTauriOAuth();
|
||||
return () => {
|
||||
isOAuthServerRunning.current = false;
|
||||
stopOAuthServer();
|
||||
stopTauriOAuth();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -134,8 +177,25 @@ export default function AuthPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For tauri app development, use a custom OAuth server to handle the OAuth callback
|
||||
// For tauri app production, use deeplink to handle the OAuth callback
|
||||
// For web app, use the built-in OAuth callback page /auth/callback
|
||||
return isTauriAppPlatform() ? (
|
||||
<div className='flex pt-11'>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className='btn btn-ghost fixed left-3 top-11 h-8 min-h-8 w-8 p-0'
|
||||
>
|
||||
<IoArrowBack size={20} />
|
||||
</button>
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
@@ -173,7 +233,13 @@ export default function AuthPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem', paddingTop: '4rem' }}>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className='btn btn-ghost fixed left-10 top-6 h-8 min-h-8 w-8 p-0'
|
||||
>
|
||||
<IoArrowBack size={20} />
|
||||
</button>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
|
||||
@@ -15,7 +15,7 @@ const previewImage = 'https://cdn.readest.com/images/open_graph_preview_read_now
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
|
||||
@@ -4,16 +4,27 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { MdDelete, MdOpenInNew } from 'react-icons/md';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import { CiCircleMore } from 'react-icons/ci';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Menu, MenuItem } from '@tauri-apps/api/menu';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getFilename } from '@/utils/book';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import BookDetailModal from '@/components/BookDetailModal';
|
||||
|
||||
type BookshelfItem = Book | BooksGroup;
|
||||
|
||||
@@ -48,9 +59,11 @@ interface BookshelfProps {
|
||||
}
|
||||
|
||||
const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImportBooks }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { deleteBook } = useLibraryStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
|
||||
@@ -58,6 +71,15 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
const [clickedImage, setClickedImage] = useState<string | null>(null);
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
const isImportingBook = useRef(false);
|
||||
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
|
||||
|
||||
const showBookDetailsModal = (book: Book) => {
|
||||
setShowDetailsBook(book);
|
||||
};
|
||||
|
||||
const dismissBookDetailsModal = () => {
|
||||
setShowDetailsBook(null);
|
||||
};
|
||||
|
||||
const { setLibrary } = useLibraryStore();
|
||||
|
||||
@@ -123,21 +145,65 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
|
||||
const bookContextMenuHandler = async (book: Book, e: React.MouseEvent) => {
|
||||
if (!isTauriAppPlatform()) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const osPlatform = getOSPlatform();
|
||||
const fileRevealLabel =
|
||||
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
|
||||
const openBookMenuItem = await MenuItem.new({
|
||||
text: isSelectMode ? _('Select Book') : _('Open Book'),
|
||||
action: async () => {
|
||||
handleBookClick(book.hash);
|
||||
},
|
||||
});
|
||||
const showBookInFinderMenuItem = await MenuItem.new({
|
||||
text: _(fileRevealLabel),
|
||||
action: async () => {
|
||||
const folder = `${settings.localBooksDir}/${getFilename(book)}`;
|
||||
revealItemInDir(folder);
|
||||
},
|
||||
});
|
||||
const showBookDetailsMenuItem = await MenuItem.new({
|
||||
text: _('Show Book Details'),
|
||||
action: async () => {
|
||||
showBookDetailsModal(book);
|
||||
},
|
||||
});
|
||||
const deleteBookMenuItem = await MenuItem.new({
|
||||
text: _('Delete'),
|
||||
action: async () => {
|
||||
deleteBook(envConfig, book);
|
||||
},
|
||||
});
|
||||
const menu = await Menu.new();
|
||||
menu.append(openBookMenuItem);
|
||||
menu.append(showBookDetailsMenuItem);
|
||||
menu.append(showBookInFinderMenuItem);
|
||||
menu.append(deleteBookMenuItem);
|
||||
menu.popup();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='bookshelf'>
|
||||
<div className='grid grid-cols-3 gap-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
|
||||
{bookshelfItems.map((item, index) => (
|
||||
<div
|
||||
key={`library-item-${index}`}
|
||||
className='hover:bg-base-300/50 flex h-full flex-col p-4'
|
||||
className='hover:bg-base-300/50 group flex h-full flex-col p-4'
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<div
|
||||
className='book-item cursor-pointer'
|
||||
onClick={() => handleBookClick(item.hash)}
|
||||
onContextMenu={bookContextMenuHandler.bind(null, item as Book)}
|
||||
>
|
||||
<div key={(item as Book).hash} className='bg-base-100 shadow-md'>
|
||||
<div
|
||||
key={(item as Book).hash}
|
||||
className='bg-base-100 shadow-md'
|
||||
onClick={() => handleBookClick(item.hash)}
|
||||
>
|
||||
<div className='relative aspect-[28/41]'>
|
||||
<Image
|
||||
src={item.coverImageUrl!}
|
||||
@@ -176,10 +242,19 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='card-body p-0 pt-2'>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -218,11 +293,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
<div className='text-base-content bg-base-300 fixed bottom-4 left-1/2 flex -translate-x-1/2 transform space-x-4 rounded-lg p-4 shadow-lg'>
|
||||
<button onClick={openSelectedBooks} className='flex items-center space-x-2'>
|
||||
<MdOpenInNew />
|
||||
<span>Open</span>
|
||||
<span>{_('Open')}</span>
|
||||
</button>
|
||||
<button onClick={deleteSelectedBooks} className='flex items-center space-x-2'>
|
||||
<MdDelete className='fill-red-500' />
|
||||
<span className='text-red-500'>Delete</span>
|
||||
<span className='text-red-500'>{_('Delete')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -233,12 +308,20 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
)}
|
||||
{showDeleteAlert && (
|
||||
<Alert
|
||||
title='Confirm Deletion'
|
||||
message='Are you sure to delete the selected books?'
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete the selected books?')}
|
||||
onClickCancel={() => setShowDeleteAlert(false)}
|
||||
onClickConfirm={confirmDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDetailsBook && (
|
||||
<BookDetailModal
|
||||
isOpen={!!showDetailsBook}
|
||||
book={showDetailsBook}
|
||||
onClose={dismissBookDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,12 +3,13 @@ import React, { useEffect, useRef } from 'react';
|
||||
import { FaSearch } from 'react-icons/fa';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { PiSelectionAllDuotone } from 'react-icons/pi';
|
||||
import { MdOutlineMenu } from 'react-icons/md';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import { MdOutlineMenu } from 'react-icons/md';
|
||||
import SettingsMenu from './SettingsMenu';
|
||||
|
||||
interface LibraryHeaderProps {
|
||||
@@ -22,6 +23,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
onImportBooks,
|
||||
onToggleSelectMode,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -53,7 +55,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Search books...'
|
||||
placeholder={_('Search books...')}
|
||||
spellCheck='false'
|
||||
className={clsx(
|
||||
'input rounded-badge bg-base-300/50 h-7 w-full pl-10 pr-10',
|
||||
@@ -69,17 +71,24 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={-1}
|
||||
className='dropdown-content dropdown-center menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
|
||||
className='dropdown-content dropdown-center bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
|
||||
>
|
||||
<li>
|
||||
<button className='text-base-content' onClick={onImportBooks}>
|
||||
From Local File
|
||||
{_('From Local File')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button onClick={onToggleSelectMode} aria-label='Select Multiple Books' className='h-6'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom cursor-pointer' data-tip='Select books'>
|
||||
<button
|
||||
onClick={onToggleSelectMode}
|
||||
aria-label={_('Select multiple books')}
|
||||
className='h-6'
|
||||
>
|
||||
<div
|
||||
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
|
||||
data-tip={_('Select books')}
|
||||
>
|
||||
<PiSelectionAllDuotone
|
||||
role='button'
|
||||
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
interface BookMenuProps {
|
||||
@@ -17,6 +18,7 @@ interface BookMenuProps {
|
||||
}
|
||||
|
||||
const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const { envConfig } = useEnv();
|
||||
const { user, logout } = useAuth();
|
||||
@@ -56,13 +58,17 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
>
|
||||
{user ? (
|
||||
<MenuItem
|
||||
label={userDisplayName ? `Logged in as ${userDisplayName}` : 'Logged in'}
|
||||
label={
|
||||
userDisplayName
|
||||
? _('Logged in as {{userDisplayName}}', { userDisplayName })
|
||||
: _('Logged in')
|
||||
}
|
||||
labelClass='!max-w-40'
|
||||
icon={
|
||||
avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt='User Avatar'
|
||||
alt={_('User avatar')}
|
||||
className='h-5 w-5 rounded-full'
|
||||
referrerPolicy='no-referrer'
|
||||
width={20}
|
||||
@@ -74,19 +80,19 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
<MenuItem label='Sign Out' noIcon onClick={handleUserLogout} />
|
||||
<MenuItem label={_('Sign Out')} noIcon onClick={handleUserLogout} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
label='Sign In'
|
||||
label={_('Sign In')}
|
||||
icon={<PiUserCircle size={20} />}
|
||||
onClick={handleUserLogin}
|
||||
></MenuItem>
|
||||
)}
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebApp && <MenuItem label='Download Readest' onClick={downloadReadest} />}
|
||||
<MenuItem label='About Readest' onClick={showAboutReadest} />
|
||||
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,14 +7,16 @@ import { useRouter } from 'next/navigation';
|
||||
import { Book } from '@/types/book';
|
||||
import { AppService } from '@/types/system';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
import { getBaseFilename, listFormater } from '@/utils/book';
|
||||
import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { isTauriAppPlatform, hasUpdater } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useDemoBooks } from './hooks/useDemoBooks';
|
||||
@@ -23,6 +25,7 @@ import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from './components/LibraryHeader';
|
||||
import Bookshelf from './components/Bookshelf';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import Toast from '@/components/Toast';
|
||||
|
||||
const LibraryPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -35,6 +38,7 @@ const LibraryPage = () => {
|
||||
clearOpenWithBooks,
|
||||
} = useLibraryStore();
|
||||
useTheme();
|
||||
const _ = useTranslation();
|
||||
const { setSettings, saveSettings } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
@@ -42,9 +46,19 @@ const LibraryPage = () => {
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
const demoBooks = useDemoBooks();
|
||||
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
const toastDismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
useEffect(() => {
|
||||
if (toastDismissTimeout.current) clearTimeout(toastDismissTimeout.current);
|
||||
toastDismissTimeout.current = setTimeout(() => setToastMessage(''), 5000);
|
||||
return () => {
|
||||
if (toastDismissTimeout.current) clearTimeout(toastDismissTimeout.current);
|
||||
};
|
||||
}, [toastMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
const doAppUpdates = async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
if (hasUpdater()) {
|
||||
await checkForAppUpdates();
|
||||
}
|
||||
};
|
||||
@@ -55,9 +69,14 @@ const LibraryPage = () => {
|
||||
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
|
||||
const bookIds: string[] = [];
|
||||
for (const file of openWithFiles) {
|
||||
const book = await appService.importBook(file, libraryBooks);
|
||||
if (book) {
|
||||
bookIds.push(book.hash);
|
||||
console.log('Open with book:', file);
|
||||
try {
|
||||
const book = await appService.importBook(file, libraryBooks);
|
||||
if (book) {
|
||||
bookIds.push(book.hash);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to import book:', file, error);
|
||||
}
|
||||
}
|
||||
setLibrary(libraryBooks);
|
||||
@@ -147,9 +166,22 @@ const LibraryPage = () => {
|
||||
|
||||
const importBooks = async (files: [string | File]) => {
|
||||
setLoading(true);
|
||||
const failedFiles = [];
|
||||
for (const file of files) {
|
||||
await appService?.importBook(file, libraryBooks);
|
||||
setLibrary(libraryBooks);
|
||||
try {
|
||||
await appService?.importBook(file, libraryBooks);
|
||||
setLibrary(libraryBooks);
|
||||
} catch (error) {
|
||||
const filename = typeof file === 'string' ? file : file.name;
|
||||
const baseFilename = getBaseFilename(filename);
|
||||
failedFiles.push(baseFilename);
|
||||
setToastMessage(
|
||||
_('Failed to import book(s): {{filenames}}', {
|
||||
filenames: listFormater(false).format(failedFiles),
|
||||
}),
|
||||
);
|
||||
console.error('Failed to import book:', filename, error);
|
||||
}
|
||||
}
|
||||
appService?.saveLibraryBooks(libraryBooks);
|
||||
setLoading(false);
|
||||
@@ -237,18 +269,27 @@ const LibraryPage = () => {
|
||||
<div className='hero h-screen items-center justify-center'>
|
||||
<div className='hero-content text-neutral-content text-center'>
|
||||
<div className='max-w-md'>
|
||||
<h1 className='mb-5 text-5xl font-bold'>Your Library</h1>
|
||||
<h1 className='mb-5 text-5xl font-bold'>{_('Your Library')}</h1>
|
||||
<p className='mb-5'>
|
||||
Welcome to your library. You can import your books here and read them anytime.
|
||||
{_(
|
||||
'Welcome to your library. You can import your books here and read them anytime.',
|
||||
)}
|
||||
</p>
|
||||
<button className='btn btn-primary rounded-xl' onClick={handleImportBooks}>
|
||||
Import Books
|
||||
{_('Import Books')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<AboutWindow />
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
message={toastMessage}
|
||||
toastClass='toast-top toast-end pt-11'
|
||||
alertClass='alert-error max-w-80'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,22 +5,26 @@ import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import Button from '@/components/Button';
|
||||
import { getCurrentPage } from '@/utils/book';
|
||||
|
||||
interface BookmarkTogglerProps {
|
||||
bookKey: string;
|
||||
}
|
||||
|
||||
const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore();
|
||||
const { getProgress, setBookmarkRibbonVisibility } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
|
||||
@@ -36,12 +40,21 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
id: uniqueId(),
|
||||
type: 'bookmark',
|
||||
cfi,
|
||||
text: truncatedText,
|
||||
text: truncatedText ? truncatedText : `${getCurrentPage(bookData.book!, progress)}`,
|
||||
note: '',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
bookmarks.push(bookmark);
|
||||
const existingBookmark = bookmarks.find(
|
||||
(item) => item.type === 'bookmark' && item.cfi === cfi,
|
||||
);
|
||||
if (existingBookmark) {
|
||||
existingBookmark.deletedAt = null;
|
||||
existingBookmark.updatedAt = Date.now();
|
||||
existingBookmark.text = bookmark.text;
|
||||
} else {
|
||||
bookmarks.push(bookmark);
|
||||
}
|
||||
const updatedConfig = updateBooknotes(bookKey, bookmarks);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
@@ -50,14 +63,15 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
setIsBookmarked(false);
|
||||
const start = CFI.collapse(cfi);
|
||||
const end = CFI.collapse(cfi, true);
|
||||
const updatedConfig = updateBooknotes(
|
||||
bookKey,
|
||||
bookmarks.filter(
|
||||
(item) =>
|
||||
item.type !== 'bookmark' ||
|
||||
CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) > 0,
|
||||
),
|
||||
);
|
||||
bookmarks.forEach((item) => {
|
||||
if (
|
||||
item.type === 'bookmark' &&
|
||||
CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) <= 0
|
||||
) {
|
||||
item.deletedAt = Date.now();
|
||||
}
|
||||
});
|
||||
const updatedConfig = updateBooknotes(bookKey, bookmarks);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
@@ -72,7 +86,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
const start = CFI.collapse(cfi);
|
||||
const end = CFI.collapse(cfi, true);
|
||||
const locationBookmarked = booknotes
|
||||
.filter((booknote) => booknote.type === 'bookmark')
|
||||
.filter((booknote) => booknote.type === 'bookmark' && !booknote.deletedAt)
|
||||
.some((item) => CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) <= 0);
|
||||
setIsBookmarked(locationBookmarked);
|
||||
setBookmarkRibbonVisibility(bookKey, locationBookmarked);
|
||||
@@ -89,7 +103,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
)
|
||||
}
|
||||
onClick={toggleBookmark}
|
||||
tooltip='Bookmark'
|
||||
tooltip={_('Bookmark')}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<div
|
||||
id={`gridcell-${bookKey}`}
|
||||
key={bookKey}
|
||||
className='relative h-full w-full overflow-hidden'
|
||||
className='relative h-full w-full rounded-window overflow-hidden'
|
||||
>
|
||||
{isBookmarked && <Ribbon width={marginGap} />}
|
||||
<HeaderBar
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface FooterBarProps {
|
||||
@@ -14,6 +15,7 @@ interface FooterBarProps {
|
||||
}
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
|
||||
const _ = useTranslation();
|
||||
const { hoveredBookKey, setHoveredBookKey, getView } = useReaderStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const view = getView(bookKey);
|
||||
@@ -53,17 +55,21 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<Button icon={<RiArrowLeftWideLine size={20} />} onClick={handleGoPrev} tooltip='Go Left' />
|
||||
<Button
|
||||
icon={<RiArrowLeftWideLine size={20} />}
|
||||
onClick={handleGoPrev}
|
||||
tooltip={_('Go Left')}
|
||||
/>
|
||||
<Button
|
||||
icon={<RiArrowGoBackLine size={20} />}
|
||||
onClick={handleGoBack}
|
||||
tooltip='Go Back'
|
||||
tooltip={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={<RiArrowGoForwardLine size={20} />}
|
||||
onClick={handleGoForward}
|
||||
tooltip='Go Forward'
|
||||
tooltip={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<span className='mx-2 text-center text-sm'>
|
||||
@@ -77,7 +83,11 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
value={pageinfoValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
<Button icon={<RiArrowRightWideLine size={20} />} onClick={handleGoNext} tooltip='Go Right' />
|
||||
<Button
|
||||
icon={<RiArrowRightWideLine size={20} />}
|
||||
onClick={handleGoNext}
|
||||
tooltip={_('Go Right')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const { themeCode } = useTheme();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const footnoteHandler = new FootnoteHandler();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,7 +74,13 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const triangPos = getPosition(detail.a, rect, viewSettings.vertical);
|
||||
const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding);
|
||||
const popupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
viewSettings.vertical ? popupHeight : popupWidth,
|
||||
viewSettings.vertical ? popupWidth : popupHeight,
|
||||
popupPadding,
|
||||
);
|
||||
setTrianglePosition(triangPos);
|
||||
setPopupPosition(popupPos);
|
||||
|
||||
@@ -107,6 +114,9 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
}
|
||||
}, [footnoteRef]);
|
||||
|
||||
const width = viewSettings.vertical ? popupHeight : popupWidth;
|
||||
const height = viewSettings.vertical ? popupWidth : popupHeight;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showPopup && (
|
||||
@@ -117,8 +127,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
/>
|
||||
)}
|
||||
<Popup
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
width={width}
|
||||
height={height}
|
||||
position={showPopup ? popupPosition! : undefined}
|
||||
trianglePosition={showPopup ? trianglePosition! : undefined}
|
||||
className='select-text overflow-y-auto'
|
||||
@@ -127,8 +137,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
className=''
|
||||
ref={footnoteRef}
|
||||
style={{
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
></div>
|
||||
</Popup>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TbLayoutSidebarRight, TbLayoutSidebarRightFilled } from 'react-icons/tb
|
||||
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface NotebookTogglerProps {
|
||||
@@ -10,6 +11,7 @@ interface NotebookTogglerProps {
|
||||
}
|
||||
|
||||
const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
|
||||
const { isNotebookVisible, toggleNotebook } = useNotebookStore();
|
||||
const handleToggleSidebar = () => {
|
||||
@@ -30,7 +32,7 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
tooltip='Notebook'
|
||||
tooltip={_('Notebook')}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PageInfo } from '@/types/book';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { PageInfo } from '@/types/book';
|
||||
|
||||
interface PageInfoProps {
|
||||
bookFormat: string;
|
||||
@@ -9,13 +10,17 @@ interface PageInfoProps {
|
||||
}
|
||||
|
||||
const PageInfoView: React.FC<PageInfoProps> = ({ bookFormat, section, pageinfo, gapRight }) => {
|
||||
const _ = useTranslation();
|
||||
const pageInfo =
|
||||
bookFormat === 'PDF'
|
||||
? section
|
||||
? `${section.current + 1} / ${section.total}`
|
||||
: ''
|
||||
: pageinfo
|
||||
? `Loc. ${pageinfo.current + 1} / ${pageinfo.total}`
|
||||
? _('Loc. {{currentPage}} / {{totalPage}}', {
|
||||
currentPage: pageinfo.current + 1,
|
||||
totalPage: pageinfo.total,
|
||||
})
|
||||
: '';
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
return (
|
||||
library.length > 0 &&
|
||||
settings.globalReadSettings && (
|
||||
<div className='reader-page bg-base-100 text-base-content min-h-screen select-none'>
|
||||
<div className='reader-page rounded-window bg-base-100 text-base-content min-h-screen select-none'>
|
||||
<Suspense>
|
||||
<ReaderContent ids={ids} settings={settings} />
|
||||
<AboutWindow />
|
||||
|
||||
@@ -9,15 +9,19 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { Book } from '@/types/book';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { tauriHandleClose } from '@/utils/window';
|
||||
import { tauriHandleClose, tauriHandleOnCloseWindow } from '@/utils/window';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import { BOOK_IDS_SEPARATOR } from '@/services/constants';
|
||||
|
||||
import useBooksManager from '../hooks/useBooksManager';
|
||||
import useBookShortcuts from '../hooks/useBookShortcuts';
|
||||
import BookDetailModal from '@/components/BookDetailModal';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import SideBar from './sidebar/SideBar';
|
||||
import Notebook from './notebook/Notebook';
|
||||
@@ -33,6 +37,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
const { getConfig, getBookData, saveConfig } = useBookDataStore();
|
||||
const { getView, setBookKeys } = useReaderStore();
|
||||
const { initViewState, getViewState, clearViewState } = useReaderStore();
|
||||
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
|
||||
const isInitiating = useRef(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -57,31 +62,46 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
if (index === 0) setSideBarBookKey(key);
|
||||
}
|
||||
});
|
||||
|
||||
const handleShowBookDetails = (event: CustomEvent) => {
|
||||
const book = event.detail as Book;
|
||||
setShowDetailsBook(book);
|
||||
return true;
|
||||
};
|
||||
eventDispatcher.onSync('show-book-details', handleShowBookDetails);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTauriAppPlatform()) tauriHandleOnCloseWindow(handleCloseBooks);
|
||||
window.addEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.on('quit-app', handleCloseBooks);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.off('quit-app', handleCloseBooks);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKeys]);
|
||||
|
||||
const saveBookConfig = (bookKey: string) => {
|
||||
const saveBookConfig = async (bookKey: string) => {
|
||||
const config = getConfig(bookKey);
|
||||
const { book } = getBookData(bookKey) || {};
|
||||
const { isPrimary } = getViewState(bookKey) || {};
|
||||
if (isPrimary && book && config) {
|
||||
saveConfig(envConfig, bookKey, config, settings);
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
await saveConfig(envConfig, bookKey, config, settings);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfigAndCloseBook = async (bookKey: string) => {
|
||||
console.log('Closing book', bookKey);
|
||||
getView(bookKey)?.close();
|
||||
getView(bookKey)?.remove();
|
||||
saveBookConfig(bookKey);
|
||||
try {
|
||||
getView(bookKey)?.close();
|
||||
getView(bookKey)?.remove();
|
||||
} catch {
|
||||
console.info('Error closing book', bookKey);
|
||||
}
|
||||
await saveBookConfig(bookKey);
|
||||
clearViewState(bookKey);
|
||||
};
|
||||
|
||||
@@ -90,11 +110,10 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
navigateToLibrary(router);
|
||||
};
|
||||
|
||||
const handleCloseBooks = () => {
|
||||
bookKeys.forEach((key) => {
|
||||
saveConfigAndCloseBook(key);
|
||||
});
|
||||
saveSettings(envConfig, settings);
|
||||
const handleCloseBooks = async () => {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
await Promise.all(bookKeys.map((key) => saveConfigAndCloseBook(key)));
|
||||
await saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
const handleCloseBooksToLibrary = () => {
|
||||
@@ -136,6 +155,13 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<Notebook />
|
||||
{showDetailsBook && (
|
||||
<BookDetailModal
|
||||
isOpen={!!showDetailsBook}
|
||||
book={showDetailsBook}
|
||||
onClose={() => setShowDetailsBook(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { TbLayoutSidebar, TbLayoutSidebarFilled } from 'react-icons/tb';
|
||||
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface SidebarTogglerProps {
|
||||
@@ -9,6 +10,7 @@ interface SidebarTogglerProps {
|
||||
}
|
||||
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { sideBarBookKey, isSideBarVisible, setSideBarBookKey, toggleSideBar } = useSidebarStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
@@ -28,7 +30,7 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
tooltip='Sidebar'
|
||||
tooltip={_('Sidebar')}
|
||||
tooltipDirection='bottom'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
|
||||
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme, ThemeMode } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
|
||||
@@ -22,6 +23,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
setIsDropdownOpen,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { getView, getViews, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
@@ -30,8 +32,8 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
const [isInvertedColors, setInvertedColors] = useState(viewSettings!.invert);
|
||||
const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!);
|
||||
|
||||
const zoomIn = () => setZoomLevel((prev) => Math.min(prev + 10, 200));
|
||||
const zoomOut = () => setZoomLevel((prev) => Math.max(prev - 10, 50));
|
||||
const zoomIn = () => setZoomLevel((prev) => Math.min(prev + 1, MAX_ZOOM_LEVEL));
|
||||
const zoomOut = () => setZoomLevel((prev) => Math.max(prev - 1, MIN_ZOOM_LEVEL));
|
||||
const resetZoom = () => setZoomLevel(100);
|
||||
const toggleScrolledMode = () => setScrolledMode(!isScrolledMode);
|
||||
const toggleInvertedColors = () => setInvertedColors(!isInvertedColors);
|
||||
@@ -75,31 +77,23 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
useEffect(() => {
|
||||
const view = getView(bookKey);
|
||||
if (!view) return;
|
||||
// FIXME: zoom level is not working in paginated mode
|
||||
if (viewSettings?.scrolled) {
|
||||
view.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
}
|
||||
viewSettings!.zoomLevel = zoomLevel;
|
||||
setViewSettings(bookKey, viewSettings!);
|
||||
view.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [zoomLevel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='view-menu dropdown-content dropdown-right no-triangle border-base-100 z-20 mt-1 w-72 border shadow-2xl'
|
||||
className='view-menu dropdown-content bgcolor-base-200 dropdown-right no-triangle border-base-200 z-20 mt-1 w-72 border shadow-2xl'
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-between rounded-md',
|
||||
!isScrolledMode && 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
<div className={clsx('flex items-center justify-between rounded-md')}>
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
!isScrolledMode && 'btn-disabled text-gray-400',
|
||||
zoomLevel <= MIN_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomOut size={20} />
|
||||
@@ -107,17 +101,16 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
|
||||
!isScrolledMode && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
onClick={resetZoom}
|
||||
>
|
||||
{zoomLevel}%
|
||||
{100 - (100 - zoomLevel) * 10}%
|
||||
</button>
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
!isScrolledMode && 'btn-disabled text-gray-400',
|
||||
zoomLevel >= MAX_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomIn size={20} />
|
||||
@@ -126,10 +119,10 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
<hr className='border-base-200 my-1' />
|
||||
|
||||
<MenuItem label='Font & Layout' shortcut='Shift+F' onClick={openFontLayoutMenu} />
|
||||
<MenuItem label={_('Font & Layout')} shortcut='Shift+F' onClick={openFontLayoutMenu} />
|
||||
|
||||
<MenuItem
|
||||
label='Scrolled Mode'
|
||||
label={_('Scrolled Mode')}
|
||||
shortcut='Shift+J'
|
||||
icon={isScrolledMode ? <MdCheck size={20} /> : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
@@ -139,7 +132,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
<MenuItem
|
||||
label={
|
||||
themeMode === 'dark' ? 'Dark Mode' : themeMode === 'light' ? 'Light Mode' : 'Auto Mode'
|
||||
themeMode === 'dark'
|
||||
? _('Dark Mode')
|
||||
: themeMode === 'light'
|
||||
? _('Light Mode')
|
||||
: _('Auto Mode')
|
||||
}
|
||||
icon={
|
||||
themeMode === 'dark' ? (
|
||||
@@ -153,7 +150,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<MenuItem
|
||||
label='Invert Colors in Dark Mode'
|
||||
label={_('Invert Colors in Dark Mode')}
|
||||
icon={isInvertedColors ? <MdCheck size={20} className='text-base-content' /> : undefined}
|
||||
onClick={toggleInvertedColors}
|
||||
disabled={!isDarkMode}
|
||||
|
||||
@@ -82,7 +82,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelection({ key: bookKey, text: sel.toString(), range: sel.getRangeAt(0), index });
|
||||
}
|
||||
};
|
||||
detail.doc?.addEventListener('pointerup', handlePointerup);
|
||||
if (bookData.book?.format !== 'PDF') {
|
||||
detail.doc?.addEventListener('pointerup', handlePointerup);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrawAnnotation = (event: Event) => {
|
||||
@@ -104,7 +106,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { value: cfi, index, range } = detail;
|
||||
const { booknotes = [] } = config;
|
||||
const annotations = booknotes.filter((booknote) => booknote.type === 'annotation');
|
||||
const annotations = booknotes.filter(
|
||||
(booknote) => booknote.type === 'annotation' && !booknote.deletedAt,
|
||||
);
|
||||
const annotation = annotations.find((annotation) => annotation.cfi === cfi);
|
||||
if (!annotation) return;
|
||||
const selection = { key: bookKey, annotated: true, text: annotation.text ?? '', range, index };
|
||||
@@ -235,7 +239,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
};
|
||||
|
||||
const existingIndex = annotations.findIndex(
|
||||
(annotation) => annotation.cfi === cfi && annotation.type === 'excerpt',
|
||||
(annotation) =>
|
||||
annotation.cfi === cfi && annotation.type === 'excerpt' && !annotation.deletedAt,
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
annotations[existingIndex] = annotation;
|
||||
@@ -270,7 +275,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const existingIndex = annotations.findIndex(
|
||||
(annotation) => annotation.cfi === cfi && annotation.type === 'annotation',
|
||||
(annotation) =>
|
||||
annotation.cfi === cfi && annotation.type === 'annotation' && !annotation.deletedAt,
|
||||
);
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
if (existingIndex !== -1) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
|
||||
const LANGUAGES = {
|
||||
@@ -37,6 +38,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const [sourceLang, setSourceLang] = useState('AUTO');
|
||||
const [targetLang, setTargetLang] = useState(settings.globalReadSettings.translateTargetLang);
|
||||
@@ -124,7 +126,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
>
|
||||
<div className='text-neutral-content relative h-[50%] overflow-y-auto border-b border-neutral-400/75 p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h1 className='text-base font-semibold'>Original Text</h1>
|
||||
<h1 className='text-base font-semibold'>{_('Original Text')}</h1>
|
||||
<select
|
||||
value={sourceLang}
|
||||
onChange={handleSourceLangChange}
|
||||
@@ -134,7 +136,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{detectedSourceLang && sourceLang === 'AUTO' && code === 'AUTO'
|
||||
? `${LANGUAGES[detectedSourceLang] || detectedSourceLang} (detected)`
|
||||
? `${LANGUAGES[detectedSourceLang] || detectedSourceLang} ` + _('(detected)')
|
||||
: name}
|
||||
</option>
|
||||
);
|
||||
@@ -146,7 +148,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
|
||||
<div className='text-neutral-content relative h-[50%] overflow-y-auto p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h2 className='text-base font-semibold'>Translated Text</h2>
|
||||
<h2 className='text-base font-semibold'>{_('Translated Text')}</h2>
|
||||
<select
|
||||
value={targetLang}
|
||||
onChange={handleTargetLangChange}
|
||||
@@ -163,7 +165,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className='text-base italic text-gray-500'>Loading...</p>
|
||||
<p className='text-base italic text-gray-500'>{_('Loading...')}</p>
|
||||
) : error ? (
|
||||
<p className='text-base text-red-600'>{error}</p>
|
||||
) : (
|
||||
|
||||
@@ -2,27 +2,31 @@ import React from 'react';
|
||||
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const NotebookHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
handleTogglePin: () => void;
|
||||
}> = ({ isPinned, handleTogglePin }) => (
|
||||
<div className='notebook-header relative flex h-11 items-center px-3'>
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<div className='notebook-title text-sm font-medium'>Notebook</div>
|
||||
}> = ({ isPinned, handleTogglePin }) => {
|
||||
const _ = useTranslation();
|
||||
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='notebook-title text-sm font-medium'>{_('Notebook')}</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center space-x-3'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center space-x-3'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default NotebookHeader;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
|
||||
interface NoteEditorProps {
|
||||
onSave: (selction: TextSelection, note: string) => void;
|
||||
onSave: (selection: TextSelection, note: string) => void;
|
||||
onEdit: (annotation: BookNote) => void;
|
||||
}
|
||||
|
||||
const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
const _ = useTranslation();
|
||||
const { notebookNewAnnotation, notebookEditAnnotation } = useNotebookStore();
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [note, setNote] = React.useState('');
|
||||
@@ -55,25 +57,25 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
className={clsx(
|
||||
'note-editor textarea textarea-ghost min-h-[1em] resize-none !outline-none',
|
||||
'inset-0 w-full rounded-none border-0 bg-transparent p-0 leading-normal',
|
||||
'text-base',
|
||||
'text-sm',
|
||||
)}
|
||||
ref={editorRef}
|
||||
value={note}
|
||||
rows={1}
|
||||
spellCheck={false}
|
||||
onChange={handleChange}
|
||||
placeholder='Add your notes here...'
|
||||
placeholder={_('Add your notes here...')}
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost settings-content hover:bg-transparent',
|
||||
'flex h-[1.5em] min-h-[1.5em] items-end p-0',
|
||||
'flex h-[1.3em] min-h-[1.3em] items-end p-0',
|
||||
editorRef.current && editorRef.current.value ? '' : 'btn-disabled !bg-opacity-0',
|
||||
)}
|
||||
onClick={handleSaveNote}
|
||||
>
|
||||
<div className='pr-1 align-bottom text-xs text-blue-400'>Save</div>
|
||||
<div className='pr-1 align-bottom text-xs text-blue-400'>{_('Save')}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex items-start pt-2'>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import NotebookHeader from './Header';
|
||||
@@ -19,6 +20,7 @@ const MIN_NOTEBOOK_WIDTH = 0.15;
|
||||
const MAX_NOTEBOOK_WIDTH = 0.45;
|
||||
|
||||
const Notebook: React.FC = ({}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
@@ -84,14 +86,18 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookNewAnnotation(null);
|
||||
};
|
||||
|
||||
const handleEditNote = (annotation: BookNote) => {
|
||||
const handleEditNote = (note: BookNote, isDelete: boolean) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const config = getConfig(sideBarBookKey)!;
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex((item) => item.id === annotation.id);
|
||||
const existingIndex = annotations.findIndex((item) => item.id === note.id);
|
||||
if (existingIndex === -1) return;
|
||||
annotation.updatedAt = Date.now();
|
||||
annotations[existingIndex] = annotation;
|
||||
if (isDelete) {
|
||||
note.deletedAt = Date.now();
|
||||
} else {
|
||||
note.updatedAt = Date.now();
|
||||
}
|
||||
annotations[existingIndex] = note;
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, annotations);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
@@ -100,27 +106,16 @@ const Notebook: React.FC = ({}) => {
|
||||
};
|
||||
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
const deleteNote = (note: BookNote) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const config = getConfig(sideBarBookKey);
|
||||
if (!config) return;
|
||||
const { booknotes = [] } = config;
|
||||
const updatedNotes = booknotes.filter((item) => item.id !== note.id);
|
||||
const updatedConfig = updateBooknotes(sideBarBookKey, updatedNotes);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, sideBarBookKey, updatedConfig, settings);
|
||||
}
|
||||
};
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const { booknotes: allNotes = [] } = config || {};
|
||||
const annotationNotes = allNotes
|
||||
.filter((note) => note.type === 'annotation' && note.note)
|
||||
.filter((note) => note.type === 'annotation' && note.note && !note.deletedAt)
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
const excerptNotes = allNotes
|
||||
.filter((note) => note.type === 'excerpt')
|
||||
.filter((note) => note.type === 'excerpt' && note.text && !note.deletedAt)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
||||
return isNotebookVisible ? (
|
||||
@@ -146,7 +141,7 @@ const Notebook: React.FC = ({}) => {
|
||||
/>
|
||||
<NotebookHeader isPinned={isNotebookPinned} 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>
|
||||
<div>{excerptNotes.length > 0 && <p className='pt-1 text-sm'>{_('Excerpts')}</p>}</div>
|
||||
<ul className=''>
|
||||
{excerptNotes.map((item, index) => (
|
||||
<li key={`${index}-${item.id}`} className='my-2'>
|
||||
@@ -165,21 +160,15 @@ const Notebook: React.FC = ({}) => {
|
||||
>
|
||||
<p className='line-clamp-1'>{item.text || `Excerpt ${index + 1}`}</p>
|
||||
</div>
|
||||
<div
|
||||
className='collapse-content select-text px-3 pb-0 text-xs'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='collapse-content select-text px-3 pb-0 text-xs'>
|
||||
<p className='hyphens-auto text-justify'>{item.text}</p>
|
||||
<div className='flex justify-end'>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost settings-content hover:bg-transparent',
|
||||
'flex h-[1em] min-h-[1em] items-end p-0',
|
||||
)}
|
||||
onClick={deleteNote.bind(null, item)}
|
||||
<div
|
||||
className='cursor-pointer align-bottom text-xs text-red-400'
|
||||
onClick={handleEditNote.bind(null, item, true)}
|
||||
>
|
||||
<div className='align-bottom text-xs text-red-400'>Delete</div>
|
||||
</button>
|
||||
{_('Delete')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,20 +177,15 @@ const Notebook: React.FC = ({}) => {
|
||||
</ul>
|
||||
<div>
|
||||
{(notebookNewAnnotation || annotationNotes.length > 0) && (
|
||||
<p className='pt-1 text-sm'>Notes</p>
|
||||
<p className='pt-1 text-sm'>{_('Notes')}</p>
|
||||
)}
|
||||
</div>
|
||||
{(notebookNewAnnotation || notebookEditAnnotation) && (
|
||||
<NoteEditor onSave={handleSaveNote} onEdit={handleEditNote} />
|
||||
<NoteEditor onSave={handleSaveNote} onEdit={(item) => handleEditNote(item, false)} />
|
||||
)}
|
||||
<ul className=''>
|
||||
{annotationNotes.map((item, index) => (
|
||||
<BooknoteItem
|
||||
key={`${index}-${item.cfi}`}
|
||||
bookKey={sideBarBookKey}
|
||||
item={item}
|
||||
editable={true}
|
||||
/>
|
||||
<BooknoteItem key={`${index}-${item.cfi}`} bookKey={sideBarBookKey} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,11 @@ import { TbSunMoon } from 'react-icons/tb';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { themes } from '@/styles/themes';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getStyles } from '@/utils/style';
|
||||
|
||||
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { themeMode, themeColor, themeCode, isDarkMode, updateThemeMode, updateThemeColor } =
|
||||
useTheme();
|
||||
const { getViews, getViewSettings } = useReaderStore();
|
||||
@@ -24,9 +26,9 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>Theme Mode</h2>
|
||||
<h2 className='font-medium'>{_('Theme Mode')}</h2>
|
||||
<div className='flex gap-2'>
|
||||
<div className='tooltip tooltip-bottom' data-tip='Auto Mode'>
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Auto Mode')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('auto')}
|
||||
@@ -35,7 +37,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip='Light Mode'>
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Light Mode')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('light')}
|
||||
@@ -44,7 +46,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip='Dark Mode'>
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Dark Mode')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('dark')}
|
||||
@@ -56,7 +58,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>Theme Color</h2>
|
||||
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
{themes.map(({ name, label, colors }) => (
|
||||
<label
|
||||
@@ -82,7 +84,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
) : (
|
||||
<MdRadioButtonUnchecked size={24} />
|
||||
)}
|
||||
<span>{label}</span>
|
||||
<span>{_(label)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface DialogMenuProps {
|
||||
toggleDropdown?: () => void;
|
||||
}
|
||||
|
||||
const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
const _ = useTranslation();
|
||||
const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useSettingsStore();
|
||||
|
||||
const handleToggleGlobal = () => {
|
||||
@@ -28,7 +30,7 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
{isFontLayoutSettingsGlobal && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<div className='tooltip' data-tip='Uncheck for current book settings'>
|
||||
<span className='ml-2'>Global Settings</span>
|
||||
<span className='ml-2'>{_('Global Settings')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FiChevronDown } from 'react-icons/fi';
|
||||
import { FiChevronUp, FiChevronLeft } from 'react-icons/fi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface DropdownProps {
|
||||
family?: string;
|
||||
selected: string;
|
||||
options: string[];
|
||||
moreOptions?: string[];
|
||||
onSelect: (option: string) => void;
|
||||
onGetFontFamily: (option: string, family: string) => string;
|
||||
}
|
||||
@@ -14,21 +17,23 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
family,
|
||||
selected,
|
||||
options,
|
||||
moreOptions,
|
||||
onSelect,
|
||||
onGetFontFamily,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div className='dropdown dropdown-end'>
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
|
||||
>
|
||||
<span style={{ fontFamily: onGetFontFamily(selected, family ?? '') }}>{selected}</span>
|
||||
<FiChevronDown className='h-4 w-4' />
|
||||
<FiChevronUp className='h-4 w-4' />
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className='dropdown-content dropdown-right menu bg-base-100 rounded-box z-[1] mt-4 w-44 shadow'
|
||||
className='dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute right-0 z-[1] mt-4 w-44 shadow'
|
||||
>
|
||||
{options.map((option) => (
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
@@ -40,6 +45,36 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{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>
|
||||
<span>{_('System Fonts')}</span>
|
||||
</div>
|
||||
<ul
|
||||
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]',
|
||||
)}
|
||||
>
|
||||
{moreOptions.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>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
|
||||
{option}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,17 +3,27 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import NumberInput from './NumberInput';
|
||||
import FontDropdown from './FontDropDown';
|
||||
import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS } from '@/services/constants';
|
||||
import {
|
||||
LINUX_FONTS,
|
||||
MACOS_FONTS,
|
||||
MONOSPACE_FONTS,
|
||||
SANS_SERIF_FONTS,
|
||||
SERIF_FONTS,
|
||||
WINDOWS_FONTS,
|
||||
} from '@/services/constants';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
|
||||
interface FontFaceProps {
|
||||
className?: string;
|
||||
family: string;
|
||||
label: string;
|
||||
options: string[];
|
||||
moreOptions?: string[];
|
||||
selected: string;
|
||||
onSelect: (option: string) => void;
|
||||
}
|
||||
@@ -24,12 +34,21 @@ const handleFontFaceFont = (option: string, family: string) => {
|
||||
return `'${option}', ${family}`;
|
||||
};
|
||||
|
||||
const FontFace = ({ className, family, label, options, selected, onSelect }: FontFaceProps) => (
|
||||
const FontFace = ({
|
||||
className,
|
||||
family,
|
||||
label,
|
||||
options,
|
||||
moreOptions,
|
||||
selected,
|
||||
onSelect,
|
||||
}: FontFaceProps) => (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className=''>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options}
|
||||
moreOptions={moreOptions}
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
onGetFontFamily={handleFontFaceFont}
|
||||
@@ -38,12 +57,28 @@ const FontFace = ({ className, family, label, options, selected, onSelect }: Fon
|
||||
);
|
||||
|
||||
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const osPlatform = getOSPlatform();
|
||||
let moreFonts: string[] = [];
|
||||
switch (osPlatform) {
|
||||
case 'macos':
|
||||
moreFonts = MACOS_FONTS;
|
||||
break;
|
||||
case 'windows':
|
||||
moreFonts = WINDOWS_FONTS;
|
||||
break;
|
||||
case 'linux':
|
||||
moreFonts = LINUX_FONTS;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
|
||||
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
|
||||
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
|
||||
@@ -145,12 +180,12 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Size</h2>
|
||||
<h2 className='mb-2 font-medium'>{_('Font Size')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Default Font Size'
|
||||
label={_('Default Font Size')}
|
||||
value={defaultFontSize}
|
||||
onChange={setDefaultFontSize}
|
||||
min={minFontSize}
|
||||
@@ -158,7 +193,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
/>
|
||||
<NumberInput
|
||||
className='config-item-bottom'
|
||||
label='Minimum Font Size'
|
||||
label={_('Minimum Font Size')}
|
||||
value={minFontSize}
|
||||
onChange={setMinFontSize}
|
||||
min={1}
|
||||
@@ -169,11 +204,11 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Family</h2>
|
||||
<h2 className='mb-2 font-medium'>{_('Font Family')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item config-item-top'>
|
||||
<span className=''>Default Font</span>
|
||||
<span className=''>{_('Default Font')}</span>
|
||||
<FontDropdown
|
||||
options={fontFamilyOptions}
|
||||
selected={defaultFont}
|
||||
@@ -183,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 Publisher Font')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -196,29 +231,32 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Face</h2>
|
||||
<h2 className='mb-2 font-medium'>{_('Font Face')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<FontFace
|
||||
className='config-item-top'
|
||||
family='serif'
|
||||
label='Serif Font'
|
||||
label={_('Serif Font')}
|
||||
options={SERIF_FONTS}
|
||||
moreOptions={moreFonts}
|
||||
selected={serifFont}
|
||||
onSelect={setSerifFont}
|
||||
/>
|
||||
<FontFace
|
||||
family='sans-serif'
|
||||
label='Sans-Serif Font'
|
||||
label={_('Sans-Serif Font')}
|
||||
options={SANS_SERIF_FONTS}
|
||||
moreOptions={moreFonts}
|
||||
selected={sansSerifFont}
|
||||
onSelect={setSansSerifFont}
|
||||
/>
|
||||
<FontFace
|
||||
className='config-item-bottom'
|
||||
family='monospace'
|
||||
label='Monospace Font'
|
||||
label={_('Monospace Font')}
|
||||
options={MONOSPACE_FONTS}
|
||||
moreOptions={moreFonts}
|
||||
selected={monospaceFont}
|
||||
onSelect={setMonospaceFont}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MdOutlineAutoMode } from 'react-icons/md';
|
||||
import { MdOutlineTextRotationDown, MdOutlineTextRotationNone } from 'react-icons/md';
|
||||
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import NumberInput from './NumberInput';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { getBookLangCode } from '@/utils/book';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const view = getView(bookKey);
|
||||
const bookData = getBookData(bookKey)!;
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
@@ -21,6 +30,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [maxColumnCount, setMaxColumnCount] = useState(viewSettings.maxColumnCount!);
|
||||
const [maxInlineSize, setMaxInlineSize] = useState(viewSettings.maxInlineSize!);
|
||||
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!);
|
||||
const [writingMode, setWritingMode] = useState(viewSettings.writingMode!);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.lineHeight = lineHeight;
|
||||
@@ -109,15 +119,62 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [maxInlineSize]);
|
||||
|
||||
useEffect(() => {
|
||||
// global settings are not supported for writing mode
|
||||
viewSettings.writingMode = writingMode;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [writingMode]);
|
||||
|
||||
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
|
||||
const isCJKBook = langCode === 'zh' || langCode === 'ja' || langCode === 'ko';
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
{isCJKBook && (
|
||||
<div className='w-full'>
|
||||
<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')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('auto')}
|
||||
>
|
||||
<MdOutlineAutoMode size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip 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} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip 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} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Paragraph</h2>
|
||||
<h2 className='mb-2 font-medium'>{_('Paragraph')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Line Height'
|
||||
label={_('Line Height')}
|
||||
value={lineHeight}
|
||||
onChange={setLineHeight}
|
||||
min={1.0}
|
||||
@@ -125,7 +182,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
step={0.1}
|
||||
/>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className=''>Full Justification</span>
|
||||
<span className=''>{_('Full Justification')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -134,7 +191,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className=''>Hyphenation</span>
|
||||
<span className=''>{_('Hyphenation')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -147,12 +204,12 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Page</h2>
|
||||
<h2 className='mb-2 font-medium'>{_('Page')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Margins (px)'
|
||||
label={_('Margins (px)')}
|
||||
value={marginPx}
|
||||
onChange={setMarginPx}
|
||||
min={0}
|
||||
@@ -160,21 +217,21 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
step={4}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Gaps (%)'
|
||||
label={_('Gaps (%)')}
|
||||
value={gapPercent}
|
||||
onChange={setGapPercent}
|
||||
min={0}
|
||||
max={30}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Maximum Number of Columns'
|
||||
label={_('Maximum Number of Columns')}
|
||||
value={maxColumnCount}
|
||||
onChange={setMaxColumnCount}
|
||||
min={1}
|
||||
max={2}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Maximum Inline Size'
|
||||
label={_('Maximum Inline Size')}
|
||||
value={maxInlineSize}
|
||||
onChange={setMaxInlineSize}
|
||||
min={500}
|
||||
@@ -182,7 +239,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
step={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Maximum Block Size'
|
||||
label={_('Maximum Block Size')}
|
||||
value={maxBlockSize}
|
||||
onChange={setMaxBlockSize}
|
||||
min={500}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
const cssRegex =
|
||||
/((?:\s*)([\w#.@*,:\-.:>+~$$$$\"=(),*\s]+)\s*{(?:[\s]*)((?:[^\}]+[:][^\}]+;?)*)*\s*}(?:\s*))/gim;
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import cssValidate from '@/utils/css';
|
||||
|
||||
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
@@ -16,44 +17,51 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const [animated, setAnimated] = useState(viewSettings.animated!);
|
||||
const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick!);
|
||||
const [userStylesheet, setUserStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
let cssInput = userStylesheet;
|
||||
|
||||
const validateCSS = (css: string) => {
|
||||
return cssRegex.test(css);
|
||||
};
|
||||
|
||||
const handleUserStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
cssInput = e.target.value;
|
||||
const cssInput = e.target.value;
|
||||
setDraftStylesheet(cssInput);
|
||||
setDraftStylesheetSaved(false);
|
||||
|
||||
try {
|
||||
const formattedCSS = cssbeautify(cssInput, {
|
||||
indent: ' ',
|
||||
openbrace: 'end-of-line',
|
||||
autosemicolon: true,
|
||||
});
|
||||
setUserStylesheet(formattedCSS);
|
||||
|
||||
if (cssInput && !validateCSS(cssInput)) {
|
||||
throw new Error('Invalid CSS');
|
||||
const { isValid, error } = cssValidate(cssInput);
|
||||
if (cssInput && !isValid) {
|
||||
throw new Error(error || 'Invalid CSS');
|
||||
}
|
||||
setError(null);
|
||||
|
||||
viewSettings.userStylesheet = formattedCSS;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
}
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
} catch (err) {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
console.log('CSS Error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const applyStyles = () => {
|
||||
const formattedCSS = cssbeautify(draftStylesheet, {
|
||||
indent: ' ',
|
||||
openbrace: 'end-of-line',
|
||||
autosemicolon: true,
|
||||
});
|
||||
|
||||
setDraftStylesheet(formattedCSS);
|
||||
setDraftStylesheetSaved(true);
|
||||
viewSettings.userStylesheet = formattedCSS;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
}
|
||||
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
};
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
@@ -87,11 +95,11 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Animation</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='config-item config-item-top config-item-bottom'>
|
||||
<span className='text-gray-700'>Paging Animation</span>
|
||||
<span className='text-gray-700'>{_('Paging Animation')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -104,11 +112,11 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Behavior</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<h2 className='mb-2 font-medium'>{_('Behavior')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='config-item config-item-top config-item-bottom'>
|
||||
<span className='text-gray-700'>Disable Click-to-Flip</span>
|
||||
<span className='text-gray-700'>{_('Disable Click-to-Flip')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -121,23 +129,35 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Custom CSS</h2>
|
||||
<div className={`card bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}>
|
||||
<div className='divide-y'>
|
||||
<div className='css-text-area config-item-top config-item-bottom p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
placeholder='Enter your custom CSS here...'
|
||||
spellCheck='false'
|
||||
value={cssInput}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onChange={handleUserStylesheetChange}
|
||||
/>
|
||||
</div>
|
||||
<h2 className='mb-2 font-medium'>{_('Custom CSS')}</h2>
|
||||
<div
|
||||
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
|
||||
>
|
||||
<div className='relative p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={draftStylesheet}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onChange={handleUserStylesheetChange}
|
||||
/>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
|
||||
draftStylesheetSaved ? 'hidden' : '',
|
||||
error ? 'btn-disabled' : '',
|
||||
)}
|
||||
onClick={applyStyles}
|
||||
disabled={!!error}
|
||||
>
|
||||
{_('Apply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { RiFontSize } from 'react-icons/ri';
|
||||
import { RiDashboardLine } from 'react-icons/ri';
|
||||
import { VscSymbolColor } from 'react-icons/vsc';
|
||||
@@ -15,8 +16,11 @@ import Dropdown from '@/components/Dropdown';
|
||||
import DialogMenu from './DialogMenu';
|
||||
import MiscPanel from './MiscPanel';
|
||||
|
||||
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Misc';
|
||||
|
||||
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
|
||||
const [activePanel, setActivePanel] = useState('Font');
|
||||
const _ = useTranslation();
|
||||
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -34,8 +38,8 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dialog className='modal modal-open min-w-90 w-full'>
|
||||
<div className='modal-box settings-content flex h-[60%] w-1/2 min-w-[480px] max-w-full flex-col p-0'>
|
||||
<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'>
|
||||
<button
|
||||
@@ -43,33 +47,33 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
onClick={() => setActivePanel('Font')}
|
||||
>
|
||||
<RiFontSize size={20} className='mr-0' />
|
||||
Font
|
||||
{_('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
|
||||
{_('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
|
||||
{_('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
|
||||
{_('Misc')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end absolute right-[7%]'
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { MdInfoOutline } from 'react-icons/md';
|
||||
import { Book } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
interface BookCardProps {
|
||||
cover: string;
|
||||
title: string;
|
||||
author: string;
|
||||
}
|
||||
const BookCard = ({ book }: { book: Book }) => {
|
||||
const { coverImageUrl, title, author } = book;
|
||||
const _ = useTranslation();
|
||||
|
||||
const showBookDetails = () => {
|
||||
eventDispatcher.dispatchSync('show-book-details', book);
|
||||
};
|
||||
|
||||
const BookCard: React.FC<BookCardProps> = ({ cover, title, author }) => {
|
||||
return (
|
||||
<div className='flex h-20 w-full items-center'>
|
||||
<Image
|
||||
src={cover}
|
||||
alt='Book cover'
|
||||
src={coverImageUrl!}
|
||||
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'
|
||||
@@ -27,9 +31,9 @@ const BookCard: React.FC<BookCardProps> = ({ cover, title, author }) => {
|
||||
</div>
|
||||
<button
|
||||
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'
|
||||
aria-label={_('More Info')}
|
||||
>
|
||||
<MdInfoOutline size={18} className='fill-base-content' />
|
||||
<MdInfoOutline size={18} className='fill-base-content' onClick={showBookDetails} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import Image from 'next/image';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import useBooksManager from '../../hooks/useBooksManager';
|
||||
@@ -13,6 +14,7 @@ interface BookMenuProps {
|
||||
}
|
||||
|
||||
const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const { library } = useLibraryStore();
|
||||
const { openParallelView } = useBooksManager();
|
||||
const handleParallelView = (id: string) => {
|
||||
@@ -39,7 +41,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
tabIndex={0}
|
||||
className='book-menu dropdown-content dropdown-center border-base-100 z-20 mt-3 w-60 shadow-2xl'
|
||||
>
|
||||
<MenuItem label='Parallel Read' noIcon>
|
||||
<MenuItem label={_('Parallel Read')} noIcon>
|
||||
<ul className='max-h-60 overflow-y-auto'>
|
||||
{library.slice(0, 20).map((book) => (
|
||||
<MenuItem
|
||||
@@ -62,10 +64,10 @@ const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
))}
|
||||
</ul>
|
||||
</MenuItem>
|
||||
<MenuItem label='Reload Page' noIcon shortcut='Shift+R' onClick={handleReloadPage} />
|
||||
<MenuItem label={_('Reload Page')} noIcon shortcut='Shift+R' onClick={handleReloadPage} />
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebApp && <MenuItem label='Download Readest' noIcon onClick={downloadReadest} />}
|
||||
<MenuItem label='About Readest' noIcon onClick={showAboutReadest} />
|
||||
{isWebApp && <MenuItem label={_('Download Readest')} noIcon onClick={downloadReadest} />}
|
||||
<MenuItem label={_('About Readest')} noIcon onClick={showAboutReadest} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,20 +6,21 @@ import { BookNote } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import useScrollToItem from '../../hooks/useScrollToItem';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import useScrollToItem from '../../hooks/useScrollToItem';
|
||||
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
item: BookNote;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = false }) => {
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const { getProgress, getView, getViewsById } = useReaderStore();
|
||||
const { setNotebookEditAnnotation, setNotebookVisible } = useNotebookStore();
|
||||
|
||||
const { text, cfi, note } = item;
|
||||
@@ -39,14 +40,21 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
|
||||
const config = getConfig(bookKey);
|
||||
if (!config) return;
|
||||
const { booknotes = [] } = config;
|
||||
const updatedNotes = booknotes.filter((item) => item.id !== note.id);
|
||||
const updatedConfig = updateBooknotes(bookKey, updatedNotes);
|
||||
booknotes.forEach((item) => {
|
||||
if (item.id === note.id) {
|
||||
item.deletedAt = Date.now();
|
||||
const views = getViewsById(bookKey.split('-')[0]!);
|
||||
views.forEach((view) => view?.addAnnotation(item, true));
|
||||
}
|
||||
});
|
||||
const updatedConfig = updateBooknotes(bookKey, booknotes);
|
||||
if (updatedConfig) {
|
||||
saveConfig(envConfig, bookKey, updatedConfig, settings);
|
||||
}
|
||||
};
|
||||
|
||||
const editNote = (note: BookNote) => {
|
||||
setNotebookVisible(true);
|
||||
setNotebookEditAnnotation(note);
|
||||
};
|
||||
|
||||
@@ -54,15 +62,15 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
|
||||
<li
|
||||
ref={viewRef}
|
||||
className={clsx(
|
||||
'border-base-300 my-2 cursor-pointer rounded-lg p-2 text-sm',
|
||||
editable && 'collapse-arrow collapse',
|
||||
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-200 bg-base-100',
|
||||
'border-base-300 group relative my-2 cursor-pointer rounded-lg p-2 text-sm',
|
||||
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/55 bg-base-100',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
)}
|
||||
tabIndex={0}
|
||||
onClick={handleClickItem}
|
||||
>
|
||||
<div
|
||||
className={clsx('collapse-title min-h-4 p-0', editable && 'pr-4')}
|
||||
className={clsx('min-h-4 p-0 transition-all duration-300 ease-in-out')}
|
||||
style={
|
||||
{
|
||||
'--top-override': '0.7rem',
|
||||
@@ -92,40 +100,42 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{editable && (
|
||||
<div
|
||||
className={clsx('collapse-content invisible !p-0 text-xs')}
|
||||
style={
|
||||
{
|
||||
'--bottom-override': 0,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='flex justify-end space-x-3'>
|
||||
{item.note && (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost settings-content hover:bg-transparent',
|
||||
'flex h-4 min-h-4 items-end p-0',
|
||||
)}
|
||||
onClick={editNote.bind(null, item)}
|
||||
>
|
||||
<div className='align-bottom text-blue-400'>Edit</div>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'max-h-0 overflow-hidden p-0 text-xs',
|
||||
'transition-[max-height] duration-300 ease-in-out',
|
||||
'group-hover:max-h-8 group-hover:overflow-visible',
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--bottom-override': 0,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='flex justify-end space-x-3 p-2'>
|
||||
{item.note && (
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost settings-content hover:bg-transparent',
|
||||
'flex h-4 min-h-4 items-end p-0',
|
||||
)}
|
||||
onClick={deleteNote.bind(null, item)}
|
||||
onClick={editNote.bind(null, item)}
|
||||
>
|
||||
<div className='align-bottom text-red-400'>Delete</div>
|
||||
<div className='align-bottom text-blue-400'>{_('Edit')}</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost settings-content hover:bg-transparent',
|
||||
'flex h-4 min-h-4 items-end p-0',
|
||||
)}
|
||||
onClick={deleteNote.bind(null, item)}
|
||||
>
|
||||
<div className='align-bottom text-red-400'>{_('Delete')}</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,11 +22,11 @@ const BooknoteView: React.FC<{
|
||||
const { getConfig } = useBookDataStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const { booknotes: allNotes = [] } = config;
|
||||
const booknotes = allNotes.filter((note) => note.type === type);
|
||||
const booknotes = allNotes.filter((note) => note.type === type && !note.deletedAt);
|
||||
|
||||
const booknoteGroups: { [href: string]: BooknoteGroup } = {};
|
||||
for (const booknote of booknotes) {
|
||||
const tocItem = findTocItemBS(toc, booknote.cfi);
|
||||
const tocItem = findTocItemBS(toc ?? [], booknote.cfi);
|
||||
const href = tocItem?.href || '';
|
||||
const label = tocItem?.label || '';
|
||||
const id = tocItem?.id || 0;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import TOCView from './TOCView';
|
||||
import BooknoteView from './BooknoteView';
|
||||
import TabNavigation from './TabNavigation';
|
||||
|
||||
const SidebarContent: React.FC<{
|
||||
activeTab: string;
|
||||
bookDoc: BookDoc;
|
||||
sideBarBookKey: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
}> = ({ activeTab, bookDoc, sideBarBookKey, onTabChange }) => {
|
||||
}> = ({ bookDoc, sideBarBookKey }) => {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const [activeTab, setActiveTab] = useState(config?.viewSettings?.sideBarTab || 'toc');
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
@@ -39,6 +41,20 @@ const SidebarContent: React.FC<{
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
const config = getConfig(sideBarBookKey!)!;
|
||||
setActiveTab(config.viewSettings!.sideBarTab!);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
const config = getConfig(sideBarBookKey!)!;
|
||||
config.viewSettings!.sideBarTab = tab;
|
||||
setConfig(sideBarBookKey!, config);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -52,15 +68,15 @@ const SidebarContent: React.FC<{
|
||||
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
{activeTab === 'annotations' && (
|
||||
<BooknoteView type='annotation' toc={bookDoc.toc} bookKey={sideBarBookKey} />
|
||||
<BooknoteView type='annotation' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
{activeTab === 'bookmarks' && (
|
||||
<BooknoteView type='bookmark' toc={bookDoc.toc} bookKey={sideBarBookKey} />
|
||||
<BooknoteView type='bookmark' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-shrink-0'>
|
||||
<TabNavigation activeTab={activeTab} onTabChange={onTabChange} />
|
||||
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,13 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SearchOptions from './SearchOptions';
|
||||
|
||||
const MINIMUM_SEARCH_TERM_LENGTH = 2;
|
||||
const MINIMUM_SEARCH_TERM_LENGTH_DEFAULT = 2;
|
||||
const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1;
|
||||
|
||||
interface SearchBarProps {
|
||||
isVisible: boolean;
|
||||
@@ -24,6 +26,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
searchTerm: term,
|
||||
onSearchResultChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig } = useBookDataStore();
|
||||
@@ -95,8 +98,15 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
handleSearchTermChange(searchTerm);
|
||||
};
|
||||
|
||||
const exceedMinSearchTermLength = (searchTerm: string) => {
|
||||
const isCJK = /[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/.test(searchTerm);
|
||||
const minLength = isCJK ? MINIMUM_SEARCH_TERM_LENGTH_CJK : MINIMUM_SEARCH_TERM_LENGTH_DEFAULT;
|
||||
|
||||
return searchTerm.length >= minLength;
|
||||
};
|
||||
|
||||
const handleSearchTermChange = (term: string) => {
|
||||
if (term.length >= MINIMUM_SEARCH_TERM_LENGTH) {
|
||||
if (exceedMinSearchTermLength(term)) {
|
||||
handleSearch(term);
|
||||
} else {
|
||||
resetSearch();
|
||||
@@ -116,7 +126,10 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
onSearchResultChange([...results]);
|
||||
isSearchPending.current = false;
|
||||
console.log('search done');
|
||||
if (queuedSearchTerm.current !== term && queuedSearchTerm.current.length > 2) {
|
||||
if (
|
||||
queuedSearchTerm.current !== term &&
|
||||
exceedMinSearchTermLength(queuedSearchTerm.current)
|
||||
) {
|
||||
handleSearch(queuedSearchTerm.current);
|
||||
}
|
||||
}
|
||||
@@ -149,7 +162,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
value={searchTerm}
|
||||
spellCheck={false}
|
||||
onChange={handleInputChange}
|
||||
placeholder='Search...'
|
||||
placeholder={_('Search...')}
|
||||
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { BookSearchConfig } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface SearchOptionsProps {
|
||||
searchConfig: BookSearchConfig;
|
||||
@@ -13,6 +14,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
onSearchConfigChanged,
|
||||
setIsDropdownOpen,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const handleSetScope = () => {
|
||||
onSearchConfigChanged({
|
||||
...searchConfig,
|
||||
@@ -46,7 +48,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.scope === 'book' && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>Book</span>
|
||||
<span className='ml-2'>{_('Book')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -60,7 +62,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
<MdCheck size={20} className='text-base-content' />
|
||||
)}
|
||||
</span>
|
||||
<span className='ml-2'>Chapter</span>
|
||||
<span className='ml-2'>{_('Chapter')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -74,7 +76,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchCase && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>Match Case</span>
|
||||
<span className='ml-2'>{_('Match Case')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -86,7 +88,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchWholeWords && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>Match Whole Words</span>
|
||||
<span className='ml-2'>{_('Match Whole Words')}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -98,7 +100,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{searchConfig.matchDiacritics && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>Match Diacritics</span>
|
||||
<span className='ml-2'>{_('Match Diacritics')}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const SearchResultItem: React.FC<SearchResultItemProps> = ({
|
||||
ref={viewRef}
|
||||
className={clsx(
|
||||
'my-2 cursor-pointer rounded-lg p-2 text-sm',
|
||||
isCurrent ? 'bg-base-300 hover:bg-gray-300/70' : 'hover:bg-base-300 bg-white',
|
||||
isCurrent ? 'bg-base-300 hover:bg-gray-300/70' : 'hover:bg-base-300 bg-base-100',
|
||||
)}
|
||||
onClick={() => onSelectResult(cfi)}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
@@ -23,15 +22,13 @@ const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
const SideBar: React.FC<{
|
||||
onGoToLibrary: () => void;
|
||||
}> = ({ onGoToLibrary }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, saveSettings } = useSettingsStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView } = useReaderStore();
|
||||
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [activeTab, setActiveTab] = useState(settings.globalReadSettings.sideBarTab);
|
||||
const {
|
||||
sideBarWidth,
|
||||
isSideBarPinned,
|
||||
@@ -70,12 +67,6 @@ const SideBar: React.FC<{
|
||||
setSideBarVisible(false);
|
||||
};
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
settings.globalReadSettings.sideBarTab = tab;
|
||||
saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
const handleToggleSearchBar = () => {
|
||||
setIsSearchBarVisible((prev) => !prev);
|
||||
if (isSearchBarVisible) {
|
||||
@@ -134,7 +125,7 @@ const SideBar: React.FC<{
|
||||
/>
|
||||
</div>
|
||||
<div className='border-base-300/50 border-b px-3'>
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
<BookCard book={book} />
|
||||
</div>
|
||||
</div>
|
||||
{isSearchBarVisible && searchResults ? (
|
||||
@@ -144,12 +135,7 @@ const SideBar: React.FC<{
|
||||
onSelectResult={handleSearchResultClick}
|
||||
/>
|
||||
) : (
|
||||
<SidebarContent
|
||||
activeTab={activeTab}
|
||||
bookDoc={bookDoc}
|
||||
sideBarBookKey={sideBarBookKey!}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
<SidebarContent bookDoc={bookDoc} sideBarBookKey={sideBarBookKey!} />
|
||||
)}
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import useBooksManager from './useBooksManager';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '@/services/constants';
|
||||
|
||||
interface UseBookShortcutsProps {
|
||||
sideBarBookKey: string | null;
|
||||
@@ -16,6 +21,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const { toggleNotebook } = useNotebookStore();
|
||||
const { getNextBookKey } = useBooksManager();
|
||||
const { themeCode } = useTheme();
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
const fontSize = viewSettings?.defaultFontSize ?? 16;
|
||||
const lineHeight = viewSettings?.lineHeight ?? 1.6;
|
||||
@@ -51,10 +57,59 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
getView(sideBarBookKey)?.next(distance);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
getView(sideBarBookKey)?.history.back();
|
||||
};
|
||||
|
||||
const goForward = () => {
|
||||
getView(sideBarBookKey)?.history.forward();
|
||||
};
|
||||
|
||||
const reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const quitApp = async () => {
|
||||
// on web platform use browser's default shortcut to close the tab
|
||||
if (isTauriAppPlatform()) {
|
||||
await eventDispatcher.dispatch('quit-app');
|
||||
const { exit } = await import('@tauri-apps/plugin-process');
|
||||
await exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
if (!view?.renderer?.setStyles) return;
|
||||
const viewSettings = getViewSettings(sideBarBookKey)!;
|
||||
const zoomLevel = viewSettings!.zoomLevel + 1;
|
||||
viewSettings!.zoomLevel = Math.min(zoomLevel, MAX_ZOOM_LEVEL);
|
||||
setViewSettings(sideBarBookKey, viewSettings!);
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
if (!view?.renderer?.setStyles) return;
|
||||
const viewSettings = getViewSettings(sideBarBookKey)!;
|
||||
const zoomLevel = viewSettings!.zoomLevel - 1;
|
||||
viewSettings!.zoomLevel = Math.max(zoomLevel, MIN_ZOOM_LEVEL);
|
||||
setViewSettings(sideBarBookKey, viewSettings!);
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
};
|
||||
|
||||
const resetZoom = () => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
if (!view?.renderer?.setStyles) return;
|
||||
const viewSettings = getViewSettings(sideBarBookKey)!;
|
||||
viewSettings!.zoomLevel = 100;
|
||||
setViewSettings(sideBarBookKey, viewSettings!);
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
};
|
||||
|
||||
useShortcuts(
|
||||
{
|
||||
onSwitchSideBar: switchSideBar,
|
||||
@@ -63,10 +118,16 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
onToggleScrollMode: toggleScrollMode,
|
||||
onOpenFontLayoutSettings: () => setFontLayoutSettingsDialogOpen(true),
|
||||
onReloadPage: reloadPage,
|
||||
onQuitApp: quitApp,
|
||||
onGoLeft: goLeft,
|
||||
onGoRight: goRight,
|
||||
onGoPrev: goPrev,
|
||||
onGoNext: goNext,
|
||||
onGoBack: goBack,
|
||||
onGoForward: goForward,
|
||||
onZoomIn: zoomIn,
|
||||
onZoomOut: zoomOut,
|
||||
onResetZoom: resetZoom,
|
||||
},
|
||||
[sideBarBookKey, bookKeys],
|
||||
);
|
||||
|
||||
@@ -13,15 +13,16 @@ export const useNotesSync = (bookKey: string) => {
|
||||
const bookHash = bookKey.split('-')[0]!;
|
||||
|
||||
useEffect(() => {
|
||||
if (!config || !user) return;
|
||||
if (!user) return;
|
||||
syncNotes([], bookHash, 'pull');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const lastSyncTime = useRef<number>(0);
|
||||
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
useEffect(() => {
|
||||
if (!config || !user) return;
|
||||
|
||||
const getNewNotes = () => {
|
||||
if (!config?.location || !user) return [];
|
||||
const bookNotes = config.booknotes ?? [];
|
||||
const newNotes = bookNotes.filter(
|
||||
(note) => lastSyncedAtNotes < note.updatedAt || lastSyncedAtNotes < (note.deletedAt ?? 0),
|
||||
@@ -29,16 +30,23 @@ export const useNotesSync = (bookKey: string) => {
|
||||
newNotes.forEach((note) => {
|
||||
note.bookHash = bookHash;
|
||||
});
|
||||
return newNotes;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!config?.location || !user) return;
|
||||
const now = Date.now();
|
||||
const timeSinceLastSync = now - lastSyncTime.current;
|
||||
if (timeSinceLastSync > SYNC_NOTES_INTERVAL_SEC * 1000) {
|
||||
lastSyncTime.current = now;
|
||||
const newNotes = getNewNotes();
|
||||
syncNotes(newNotes, bookHash, 'both');
|
||||
} else {
|
||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
|
||||
syncTimeoutRef.current = setTimeout(
|
||||
() => {
|
||||
lastSyncTime.current = Date.now();
|
||||
const newNotes = getNewNotes();
|
||||
syncNotes(newNotes, bookHash, 'both');
|
||||
syncTimeoutRef.current = null;
|
||||
},
|
||||
@@ -49,7 +57,7 @@ export const useNotesSync = (bookKey: string) => {
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (syncedNotes?.length && config) {
|
||||
if (syncedNotes?.length && config?.location) {
|
||||
const newNotes = syncedNotes.filter((note) => note.bookHash === bookHash);
|
||||
if (!newNotes.length) return;
|
||||
const oldNotes = config.booknotes ?? [];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BookConfig } from '@/types/book';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants';
|
||||
|
||||
@@ -12,6 +13,7 @@ export const useProgressSync = (
|
||||
bookKey: string,
|
||||
setToastMessage?: React.Dispatch<React.SetStateAction<string>>,
|
||||
) => {
|
||||
const _ = useTranslation();
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const { getView } = useReaderStore();
|
||||
const { settings } = useSettingsStore();
|
||||
@@ -19,6 +21,8 @@ export const useProgressSync = (
|
||||
const { user } = useAuth();
|
||||
const view = getView(bookKey);
|
||||
const config = getConfig(bookKey);
|
||||
// flag to prevent accidental sync without first pulling the config
|
||||
const configSynced = useRef(false);
|
||||
|
||||
const pushConfig = (bookKey: string, config: BookConfig | null) => {
|
||||
if (!config || !user) return;
|
||||
@@ -29,13 +33,26 @@ export const useProgressSync = (
|
||||
);
|
||||
syncConfigs([compressedConfig], bookHash, 'push');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!config || !user) return;
|
||||
const pullConfig = (bookKey: string) => {
|
||||
if (!user) return;
|
||||
const bookHash = bookKey.split('-')[0]!;
|
||||
syncConfigs([], bookHash, 'pull');
|
||||
return () => {
|
||||
};
|
||||
const syncConfig = () => {
|
||||
if (!configSynced.current) {
|
||||
pullConfig(bookKey);
|
||||
} else {
|
||||
pushConfig(bookKey, config);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
pullConfig(bookKey);
|
||||
return () => {
|
||||
if (configSynced.current) {
|
||||
pushConfig(bookKey, config);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -43,19 +60,20 @@ export const useProgressSync = (
|
||||
const lastProgressSyncTime = useRef<number>(0);
|
||||
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
useEffect(() => {
|
||||
if (!config || !user) return;
|
||||
if (!config?.location || !user) return;
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceLastSync = now - lastProgressSyncTime.current;
|
||||
if (configSynced.current && timeSinceLastSync > SYNC_PROGRESS_INTERVAL_SEC * 1000) {
|
||||
if (timeSinceLastSync > SYNC_PROGRESS_INTERVAL_SEC * 1000) {
|
||||
lastProgressSyncTime.current = now;
|
||||
pushConfig(bookKey, config);
|
||||
syncConfig();
|
||||
} else {
|
||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
|
||||
syncTimeoutRef.current = setTimeout(
|
||||
() => {
|
||||
lastProgressSyncTime.current = Date.now();
|
||||
pushConfig(bookKey, config);
|
||||
syncTimeoutRef.current = null;
|
||||
syncConfig();
|
||||
},
|
||||
SYNC_PROGRESS_INTERVAL_SEC * 1000 - timeSinceLastSync,
|
||||
);
|
||||
@@ -64,9 +82,9 @@ export const useProgressSync = (
|
||||
}, [config]);
|
||||
|
||||
// sync progress once when the book is opened
|
||||
const configSynced = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!configSynced.current && syncedConfigs?.length > 0) {
|
||||
if (!configSynced.current && syncedConfigs) {
|
||||
configSynced.current = true;
|
||||
const syncedConfig = syncedConfigs.filter((c) => c.bookHash === bookKey.split('-')[0])[0];
|
||||
if (syncedConfig) {
|
||||
const newConfig = deserializeConfig(
|
||||
@@ -75,13 +93,12 @@ export const useProgressSync = (
|
||||
DEFAULT_BOOK_SEARCH_CONFIG,
|
||||
);
|
||||
setConfig(bookKey, { ...config, ...newConfig });
|
||||
configSynced.current = true;
|
||||
if ((syncedConfig.progress?.[1] ?? 0) > 0 && (config?.progress?.[1] ?? 0) > 0) {
|
||||
const syncedFraction = syncedConfig.progress![0] / syncedConfig.progress![1];
|
||||
const configFraction = config!.progress![0] / config!.progress![1];
|
||||
if (syncedFraction > configFraction) {
|
||||
view?.goToFraction(syncedFraction);
|
||||
setToastMessage?.('Progress synced');
|
||||
setToastMessage?.(_('Reading progress synced'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { hasUpdater } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import Reader from './components/Reader';
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Page() {
|
||||
useTheme();
|
||||
useEffect(() => {
|
||||
const doAppUpdates = async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
if (hasUpdater()) {
|
||||
await checkForAppUpdates();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import packageJson from '../../package.json';
|
||||
import WindowButtons from './WindowButtons';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export const setAboutDialogVisible = (visible: boolean) => {
|
||||
const dialog = document.getElementById('about_window');
|
||||
@@ -13,6 +14,7 @@ export const setAboutDialogVisible = (visible: boolean) => {
|
||||
};
|
||||
|
||||
export const AboutWindow = () => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<dialog id='about_window' className='modal'>
|
||||
<form method='dialog' className='modal-box w-96 max-w-lg p-4'>
|
||||
@@ -30,7 +32,9 @@ export const AboutWindow = () => {
|
||||
</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 {packageJson.version}</span>
|
||||
<span className='badge badge-primary mt-2'>
|
||||
{_('Version {{version}}', { version: packageJson.version })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='divider'></div>
|
||||
@@ -55,7 +59,7 @@ export const AboutWindow = () => {
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<a
|
||||
href='https://github.com/chrox/readest'
|
||||
href='https://github.com/readest/readest'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const Alert: React.FC<{
|
||||
title: string;
|
||||
@@ -7,6 +8,7 @@ const Alert: React.FC<{
|
||||
onClickCancel: () => void;
|
||||
onClickConfirm: () => void;
|
||||
}> = ({ title, message, onClickCancel, onClickConfirm }) => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div
|
||||
role='alert'
|
||||
@@ -37,10 +39,10 @@ const Alert: React.FC<{
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<button className='btn btn-sm' onClick={onClickCancel}>
|
||||
Cancel
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
<button className='btn btn-sm btn-warning' onClick={onClickConfirm}>
|
||||
Confirm
|
||||
{_('Confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
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 Spinner from './Spinner';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
|
||||
interface BookDetailModalProps {
|
||||
book: Book;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
const _ = useTranslation();
|
||||
const [loading, setLoading] = 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]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
||||
const fetchBookDetails = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const details = await appService.fetchBookDetails(book, settings);
|
||||
setBookMeta(details);
|
||||
setLoading(false);
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
};
|
||||
fetchBookDetails();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [book]);
|
||||
|
||||
const handleClose = () => {
|
||||
setBookMeta(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
if (!bookMeta)
|
||||
return (
|
||||
loading && (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
<Spinner loading />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
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} />
|
||||
|
||||
<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',
|
||||
)}
|
||||
>
|
||||
{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>
|
||||
</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='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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookDetailModal;
|
||||
@@ -34,7 +34,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
: children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='dropdown-container'>
|
||||
{isOpen && (
|
||||
<div className='fixed inset-0 bg-transparent' onClick={() => setIsDropdownOpen(false)} />
|
||||
)}
|
||||
@@ -48,7 +48,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
</div>
|
||||
{isOpen && childrenWithToggle}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const Popup = ({
|
||||
}) => (
|
||||
<div>
|
||||
<div
|
||||
className={`triangle text-base-200 absolute z-10 ${triangleClassName}`}
|
||||
className={`triangle text-base-200 absolute z-40 ${triangleClassName}`}
|
||||
style={{
|
||||
left:
|
||||
trianglePosition?.dir === 'left'
|
||||
@@ -67,7 +67,7 @@ const Popup = ({
|
||||
/>
|
||||
<div
|
||||
id='popup-container'
|
||||
className={`bg-base-200 absolute rounded-lg font-sans shadow-xl ${className}`}
|
||||
className={`bg-base-200 absolute z-30 rounded-lg font-sans shadow-xl ${className}`}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const Spinner: React.FC<{
|
||||
loading: boolean;
|
||||
}> = ({ loading }) => {
|
||||
const _ = useTranslation();
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
@@ -11,7 +13,7 @@ const Spinner: React.FC<{
|
||||
role='status'
|
||||
>
|
||||
<span className='loading loading-dots loading-lg'></span>
|
||||
<span className='hidden'>Loading...</span>
|
||||
<span className='hidden'>{_('Loading...')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ const Toast: React.FC<{ message: string; toastClass?: string; alertClass?: strin
|
||||
}) => (
|
||||
<div className={clsx('toast toast-center toast-middle', toastClass)}>
|
||||
<div className={clsx('alert flex items-center justify-center border-0', alertClass)}>
|
||||
<span>{message}</span>
|
||||
<span className='whitespace-normal break-words'>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
|
||||
import { tauriHandleMinimize, tauriHandleToggleMaximize, tauriHandleClose } from '@/utils/window';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
@@ -44,6 +45,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const { appService } = useEnv();
|
||||
|
||||
const handleMouseDown = async (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
@@ -51,6 +53,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
if (
|
||||
target.closest('.btn') ||
|
||||
target.closest('.window-button') ||
|
||||
target.closest('.dropdown-container') ||
|
||||
target.closest('.exclude-title-bar-mousedown')
|
||||
) {
|
||||
return;
|
||||
@@ -110,7 +113,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{showMinimize && (
|
||||
{showMinimize && appService?.hasWindowBar && (
|
||||
<WindowButton onClick={handleMinimize} ariaLabel='Minimize' id='titlebar-minimize'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path fill='currentColor' d='M20 14H4v-2h16' />
|
||||
@@ -118,7 +121,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
</WindowButton>
|
||||
)}
|
||||
|
||||
{showMaximize && (
|
||||
{showMaximize && appService?.hasWindowBar && (
|
||||
<WindowButton onClick={handleMaximize} ariaLabel='Maximize/Restore' id='titlebar-maximize'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path fill='currentColor' d='M4 4h16v16H4zm2 4v10h12V8z' />
|
||||
@@ -126,7 +129,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
</WindowButton>
|
||||
)}
|
||||
|
||||
{showClose && (
|
||||
{showClose && (appService?.hasWindowBar || onClose) && (
|
||||
<WindowButton onClick={handleClose} ariaLabel='Close' id='titlebar-close'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path
|
||||
|
||||
@@ -29,10 +29,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
const syncSession = (session: { access_token: string; user: User } | null) => {
|
||||
console.log('Syncing session');
|
||||
if (session) {
|
||||
const { access_token, user } = session;
|
||||
setToken(access_token);
|
||||
@@ -46,10 +44,20 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
setUser(null);
|
||||
}
|
||||
};
|
||||
const fetchSession = async () => {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
syncSession(data.session);
|
||||
};
|
||||
|
||||
console.log('Fetching session');
|
||||
fetchSession();
|
||||
}, [token]);
|
||||
const { data: subscription } = supabase.auth.onAuthStateChange((_, session) => {
|
||||
syncSession(session);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = (newToken: string, newUser: User) => {
|
||||
console.log('Logging in');
|
||||
@@ -59,13 +67,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
localStorage.setItem('user', JSON.stringify(newUser));
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
const logout = async () => {
|
||||
console.log('Logging out');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
await supabase.auth.refreshSession();
|
||||
await supabase.auth.signOut();
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
supabase.auth.signOut();
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { isWebAppPlatform, hasCli } from '@/services/environment';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -36,7 +36,7 @@ export const parseOpenWithFiles = async () => {
|
||||
if (isWebAppPlatform()) return [];
|
||||
|
||||
let files = parseWindowOpenWithFiles();
|
||||
if (!files) {
|
||||
if (!files && hasCli()) {
|
||||
files = await parseCLIOpenWithFiles();
|
||||
}
|
||||
return files;
|
||||
|
||||
@@ -6,10 +6,16 @@ export interface ShortcutConfig {
|
||||
onToggleScrollMode: string[];
|
||||
onOpenFontLayoutSettings: string[];
|
||||
onReloadPage: string[];
|
||||
onQuitApp: string[];
|
||||
onGoLeft: string[];
|
||||
onGoRight: string[];
|
||||
onGoNext: string[];
|
||||
onGoPrev: string[];
|
||||
onGoBack: string[];
|
||||
onGoForward: string[];
|
||||
onZoomIn: string[];
|
||||
onZoomOut: string[];
|
||||
onResetZoom: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
@@ -20,10 +26,16 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onToggleScrollMode: ['shift+j'],
|
||||
onOpenFontLayoutSettings: ['shift+f'],
|
||||
onReloadPage: ['shift+r'],
|
||||
onQuitApp: ['ctrl+q', 'cmd+q'],
|
||||
onGoLeft: ['ArrowLeft', 'PageUp', 'h'],
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l'],
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l', ' '],
|
||||
onGoNext: ['ArrowDown', 'j'],
|
||||
onGoPrev: ['ArrowUp', 'k'],
|
||||
onGoBack: ['shift+ArrowLeft', 'shift+h'],
|
||||
onGoForward: ['shift+ArrowRight', 'shift+l'],
|
||||
onZoomIn: ['ctrl+=', 'cmd+=', 'shift+='],
|
||||
onZoomOut: ['ctrl+-', 'cmd+-', 'shift+-'],
|
||||
onResetZoom: ['ctrl+0', 'cmd+0'],
|
||||
};
|
||||
|
||||
// Load shortcuts from localStorage or fallback to defaults
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
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';
|
||||
|
||||
const LAST_CHECK_KEY = 'lastAppUpdateCheck';
|
||||
|
||||
export const checkForAppUpdates = async () => {
|
||||
const lastCheck = localStorage.getItem(LAST_CHECK_KEY);
|
||||
const now = Date.now();
|
||||
if (lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000) return;
|
||||
localStorage.setItem(LAST_CHECK_KEY, now.toString());
|
||||
|
||||
const update = await check();
|
||||
console.log('update found', update);
|
||||
if (update) {
|
||||
const yes = await ask(
|
||||
`
|
||||
|
||||