Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f20ae4967 | |||
| bdf619f2e6 | |||
| 53db2777af | |||
| a10bde2254 | |||
| cf7390915b | |||
| b694203d26 | |||
| 242fbf6743 | |||
| f21f3a3fc0 | |||
| aa16bc09f1 | |||
| 23eb2514bc | |||
| 9eacf7e203 | |||
| 32ffaa0e05 | |||
| 999e4828db | |||
| 626dd2fbd7 | |||
| ec1ec2265d | |||
| 1429f111bc | |||
| 5ed74fe9c5 | |||
| abb3ace57e | |||
| f897067683 | |||
| a189cd6d4f | |||
| 0076bab4e3 | |||
| 433f707455 | |||
| dbb8347554 | |||
| f1a31b9590 | |||
| 22fb45e54b | |||
| 3d09eb761c | |||
| 0d50edbff1 | |||
| 96fd2752ff | |||
| 4c4f067857 | |||
| d6bfbbc0b6 | |||
| e851582264 | |||
| c02d63367a |
@@ -0,0 +1,38 @@
|
||||
---
|
||||
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'
|
||||
labels: ['type: bug']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
|
||||
A clear and concise description of what the current behavior is.
|
||||
Please also add **screenshots** of the existing application.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
|
||||
[screenshot]
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what the expected behavior is.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
The blank square should be transparent (invisible)
|
||||
```
|
||||
|
||||
## Technical inputs
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
- We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
|
||||
- Probably fixable with CSS
|
||||
```
|
||||
@@ -0,0 +1,138 @@
|
||||
name: Release Readest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.result }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
- 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
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
})
|
||||
return data.id
|
||||
|
||||
build-tauri:
|
||||
needs: create-release
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
arch: x86_64
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
arch: aarch64
|
||||
rust_target: x86_64-apple-darwin,aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
arch: x86_64
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: initialize git submodules
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.14.4
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: copy pdfjs-dist to public directory
|
||||
run: pnpm --filter @readest/readest-app setup-pdfjs
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.config.rust_target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('apps/readest-app/src-tauri/Cargo.lock') }}
|
||||
workspaces: apps/readest-app/src-tauri -> target
|
||||
|
||||
- name: Create .env.local file for Next.js
|
||||
run: |
|
||||
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_DEEPL_API_KEY=${{ secrets.NEXT_PUBLIC_DEEPL_API_KEY }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_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'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
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' || '' }}
|
||||
|
||||
publish-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-release, build-tauri]
|
||||
|
||||
steps:
|
||||
- name: publish release
|
||||
id: publish-release
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
release_id: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.release_id,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
[submodule "packages/foliate-js"]
|
||||
path = packages/foliate-js
|
||||
url = git@github.com:johnfactotum/foliate-js.git
|
||||
url = https://github.com/chrox/foliate-js.git
|
||||
[submodule "packages/tauri"]
|
||||
path = packages/tauri
|
||||
url = git@github.com:chrox/tauri.git
|
||||
url = https://github.com/chrox/tauri.git
|
||||
|
||||
@@ -1,27 +1,109 @@
|
||||
# Readest
|
||||
<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%" />
|
||||
</a>
|
||||
<h1>Readest</h1>
|
||||
<br>
|
||||
|
||||
Readest is an open-source ebook reading software designed for immersive and deep reading experiences. It supports EPUB and PDF document formats, and with Tauri v2, Readest is cross-platform, running seamlessly on macOS, Windows, and Linux.
|
||||
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 15](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to offer a seamless cross-platform experience on macOS, Windows, Linux and Web, with support for mobile platforms coming soon.
|
||||
|
||||
[![Website][badge-website]][link-website]
|
||||
[![AGPL Licence][badge-license]](LICENSE)
|
||||
[![OS][badge-platforms]][link-website]
|
||||
[![][badge-discord]][link-discord]
|
||||
<br>
|
||||
[![Latest release][badge-release]][link-gh-releases]
|
||||
[![Last commit][badge-last-commit]][link-gh-commits]
|
||||
[![Commits][badge-commit-activity]][link-gh-pulse]
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="#features">Features</a> •
|
||||
<a href="#planned-features">Planned Features</a> •
|
||||
<a href="#screenshots">Screenshots</a> •
|
||||
<a href="#downloads">Downloads</a> •
|
||||
<a href="#getting-started">Getting Started</a> •
|
||||
<a href="#contributing">Contributing</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://readest.com" target="_blank">
|
||||
<img src="./data/screenshots/readest_landing_preview.png" alt="Readest Banner" width="100%" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
- **EPUB and PDF Support**: Enjoy both EPUB and PDF formats, making Readest versatile for all your reading needs.
|
||||
- **Cross-Platform Compatibility**: Runs on macOS, Windows, and Linux with Tauri v2.
|
||||
- **Immersive Reading Experience**: Supports advanced reading features like note-taking, highlighting, and progress syncing.
|
||||
- **Optimized Performance**: Readest is lightweight, ensuring smooth performance even with large files.
|
||||
- **Customizable Interface**: Built with daisyUI for a modern and user-friendly UI.
|
||||
<div align="left">✅ Implemented</div>
|
||||
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| --------------------------------------- | -------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, PDF (experimental) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
|
||||
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **Translate with DeepL** | Translate selected text instantly using DeepL for accurate translations. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
|
||||
## Planned Features
|
||||
|
||||
<div align="left">🛠 Building</div>
|
||||
<div align="left">🔄 Planned</div>
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| **Support iOS and Android** | Expand the APP to work on iOS and Android devices. | 🛠 |
|
||||
| **Sync Across Platforms** | Synchronize reading progress, notes, and bookmarks across all supported platforms. | 🛠 |
|
||||
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | 🔄 |
|
||||
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
|
||||
| **Text-to-Speech (TTS) Support** | Enable text-to-speech functionality for a more accessible reading experience. | 🔄 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🔄 |
|
||||
|
||||
Stay tuned for continuous improvements and updates! Contributions and suggestions are always welcome—let's build the ultimate reading experience together. 😊
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
The Readest app is available for download! 🥳 🚀
|
||||
|
||||
- macOS : Search for "Readest" on the [macOS App Store][link-macos-appstore].
|
||||
- Windows / Linux: Visit [https://readest.com][link-website] or the [Releases on GitHub][link-gh-releases].
|
||||
- Web: Visit [https://web.readest.com][link-web-readest].
|
||||
- iOS / Android: coming soon 👀
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js** and **pnpm** for Next.js development
|
||||
- **Rust and Cargo** for Tauri development
|
||||
|
||||
For the best experience, use a recent version of Node.js and Rust. Refer to the [Tauri documentation](https://v2.tauri.app/start/prerequisites/) for details on setting up the development environment prerequisites on different platforms.
|
||||
For the best experience to build Readest for yourself, use a recent version of Node.js and Rust. Refer to the [Tauri documentation](https://v2.tauri.app/start/prerequisites/) for details on setting up the development environment prerequisites on different platforms.
|
||||
|
||||
```bash
|
||||
nvm install v22
|
||||
nvm use v22
|
||||
npm install -g pnpm
|
||||
rustup update
|
||||
```
|
||||
|
||||
@@ -40,7 +122,7 @@ git submodule update --init --recursive
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
# might need to rerun this when code is updated
|
||||
pnpm install
|
||||
# copy pdfjs-dist to Next.js public directory
|
||||
pnpm --filter @readest/readest-app setup-pdfjs
|
||||
@@ -58,7 +140,7 @@ This command will display information about the installed Tauri dependencies and
|
||||
|
||||
For Windows targets, “Build Tools for Visual Studio 2022” (or a higher edition of Visual Studio) and the “Desktop development with C++” workflow must be installed. For Windows ARM64 targets, the “VS 2022 C++ ARM64 build tools” and "C++ Clang Compiler for Windows" components must be installed. And make sure `clang` can be found in the path by adding `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\Llvm\x64\bin` for example in the environment variable `Path`.
|
||||
|
||||
### 4. Build the Development
|
||||
### 4. Build for Development
|
||||
|
||||
```bash
|
||||
pnpm tauri dev
|
||||
@@ -74,10 +156,39 @@ pnpm tauri build
|
||||
|
||||
Readest is open-source, and contributions are welcome! Feel free to open issues, suggest features, or submit pull requests. Please review our contributing guidelines before you start.
|
||||
|
||||
## Contributors
|
||||
|
||||
<!---
|
||||
npx contributor-faces --exclude "*bot*" --limit 100"
|
||||
--->
|
||||
|
||||
[//]: contributor-faces
|
||||
|
||||
<a href="https://github.com/chrox"><img src="https://avatars.githubusercontent.com/u/751535?v=4" title="chrox" width="50" height="50"></a>
|
||||
|
||||
[//]: contributor-faces
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPL V3 License. See the LICENSE file for details.
|
||||
Readest is distributed under the AGPL-3.0 License. See the [LICENSE](<(LICENSE)>) file for details.
|
||||
|
||||
---
|
||||
|
||||
Happy reading with Readest!
|
||||
<div align="center" style="color: gray;">Happy reading with Readest!</div>
|
||||
|
||||
[badge-website]: https://img.shields.io/badge/website-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-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-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-discord]: https://discord.gg/jb2nzDts
|
||||
[link-parallel-read]: https://readest.com/#parallel-read
|
||||
[link-koreader]: https://github.com/koreader/koreader
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=tauri
|
||||
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=web
|
||||
@@ -7,7 +7,7 @@ const internalHost = process.env.TAURI_DEV_HOST || 'localhost';
|
||||
const nextConfig = {
|
||||
// Ensure Next.js uses SSG instead of SSR
|
||||
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
|
||||
output: 'export',
|
||||
output: process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web' ? undefined : 'output',
|
||||
// Note: This feature is required to use the Next.js Image component in SSG mode.
|
||||
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
|
||||
images: {
|
||||
@@ -19,6 +19,14 @@ const nextConfig = {
|
||||
// Configure assetPrefix or else the server won't properly resolve your assets.
|
||||
assetPrefix: isProd ? '' : `http://${internalHost}:3000`,
|
||||
reactStrictMode: true,
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/deepl/:path*',
|
||||
destination: 'https://api-free.deepl.com/v2/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
"build": "dotenv -e .env.tauri -- next build",
|
||||
"start": "dotenv -e .env.tauri -- next start",
|
||||
"dev-web": "dotenv -e .env.web -- next dev",
|
||||
"build-web": "dotenv -e .env.web -- next build",
|
||||
"start-web": "dotenv -e .env.web -- next start",
|
||||
"lint": "next lint",
|
||||
"tauri": "tauri",
|
||||
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs",
|
||||
"copy-pdfjs-js": "dotenv -- cross-var cpx \"%PDFJS_BUILD_PATH%/pdf*\" ./public/vendor/pdfjs",
|
||||
"copy-pdfjs-fonts": "dotenv -- cross-var cpx \"%PDFJS_FONTS_PATH%/{cmaps,standard_fonts}/*\" ./public/vendor/pdfjs",
|
||||
"copy-pdfjs-css": "dotenv -- cross-var cpx \"%PDFJS_STYLE_PATH%/{annotation_layer_builder,text_layer_builder}.css\" ./public/vendor/pdfjs",
|
||||
"copy-pdfjs": "pnpm copy-pdfjs-js && pnpm copy-pdfjs-fonts && pnpm copy-pdfjs-css",
|
||||
"copy-flatten-pdfjs-annotation-layer-css": "dotenv -- cross-var npx postcss \"%PDFJS_STYLE_PATH%/annotation_layer_builder.css\" --no-map -u postcss-nested > ./public/vendor/pdfjs/annotation_layer_builder.css",
|
||||
"copy-flatten-pdfjs-text-layer-css": "dotenv -- cross-var npx postcss \"%PDFJS_STYLE_PATH%/text_layer_builder.css\" --no-map -u postcss-nested > ./public/vendor/pdfjs/text_layer_builder.css",
|
||||
"copy-flatten-pdfjs-css": "pnpm copy-flatten-pdfjs-annotation-layer-css && pnpm copy-flatten-pdfjs-text-layer-css",
|
||||
"copy-pdfjs": "pnpm copy-pdfjs-js && pnpm copy-pdfjs-fonts && pnpm copy-flatten-pdfjs-css",
|
||||
"setup-pdfjs": "pnpm prepare-public-vendor && pnpm copy-pdfjs",
|
||||
"build-win-x64": "tauri build --target i686-pc-windows-msvc --bundles nsis",
|
||||
"build-win-arm64": "tauri build --target aarch64-pc-windows-msvc --bundles nsis",
|
||||
"build-linux-x64": "tauri build --target x86_64-unknown-linux-gnu --bundles appimage",
|
||||
"build-macos-universial": "dotenv -e .env.apple-nonstore.local -- tauri build -t universal-apple-darwin --bundles dmg",
|
||||
"build-macos-universial-appstore": "dotenv -e .env.apple-appstore.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore.conf.json",
|
||||
"build-macos-universial-appstore-dev": "dotenv -e .env.apple-appstore-dev.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore-dev.conf.json",
|
||||
"release-macos-universial-appstore": "dotenv -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh"
|
||||
"build-win-x64": "dotenv -e .env.tauri.local -- tauri build --target i686-pc-windows-msvc --bundles nsis",
|
||||
"build-win-arm64": "dotenv -e .env.tauri.local -- tauri build --target aarch64-pc-windows-msvc --bundles nsis",
|
||||
"build-linux-x64": "dotenv -e .env.tauri.local -- tauri build --target x86_64-unknown-linux-gnu --bundles appimage",
|
||||
"build-macos-universial": "dotenv -e .env.tauri.local -e .env.apple-nonstore.local -- tauri build -t universal-apple-darwin --bundles dmg",
|
||||
"build-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore.conf.json",
|
||||
"build-macos-universial-appstore-dev": "dotenv -e .env.tauri.local -e .env.apple-appstore-dev.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore-dev.conf.json",
|
||||
"release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-cli": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.2",
|
||||
"@tauri-apps/plugin-fs": "^2.0.3",
|
||||
"@tauri-apps/plugin-http": "^2.0.1",
|
||||
"@tauri-apps/plugin-log": "^2.0.0",
|
||||
"@tauri-apps/plugin-log": "^2.0.1",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "~2.0.1",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
@@ -61,6 +68,8 @@
|
||||
"eslint-config-next": "15.0.3",
|
||||
"mkdirp": "^3.0.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -31,6 +31,7 @@ tauri-plugin-os = "2.0.1"
|
||||
tauri-plugin-http = "2.0.3"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-process = "2"
|
||||
[patch.crates-io]
|
||||
tauri = { path = "../../../packages/tauri/crates/tauri" }
|
||||
|
||||
@@ -39,8 +40,6 @@ cocoa = "0.25"
|
||||
objc = "0.2.7"
|
||||
rand = "0.8"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"shell:default"
|
||||
"shell:default",
|
||||
"updater:default",
|
||||
"process:default",
|
||||
"process:allow-restart",
|
||||
"cli:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,37 +6,62 @@ extern crate cocoa;
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod tauri_traffic_light_positioner_plugin;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use {
|
||||
tauri::menu::{SubmenuBuilder, HELP_SUBMENU_ID},
|
||||
tauri::TitleBarStyle,
|
||||
tauri_plugin_shell::ShellExt,
|
||||
};
|
||||
use tauri::TitleBarStyle;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Emitter, Manager, Url};
|
||||
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri_plugin_dialog;
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
fn handle_menu_event(app: &tauri::AppHandle, event: &tauri::menu::MenuEvent) {
|
||||
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);
|
||||
#[cfg(desktop)]
|
||||
use tauri::Listener;
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
let fs_scope = app.fs_scope();
|
||||
let asset_protocol_scope = app.asset_protocol_scope();
|
||||
for file in &files {
|
||||
if let Err(e) = fs_scope.allow_file(&file) {
|
||||
eprintln!("Failed to allow file in fs_scope: {}", e);
|
||||
} else {
|
||||
println!("Allowed file in fs_scope: {:?}", file);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} 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);
|
||||
if let Err(e) = asset_protocol_scope.allow_file(&file) {
|
||||
eprintln!("Failed to allow file in asset_protocol_scope: {}", e);
|
||||
} else {
|
||||
println!("Allowed file in asset_protocol_scope: {:?}", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
let files = files
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let file = f.to_string_lossy().replace("\\", "\\\\");
|
||||
format!("\"{file}\"",)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let script = format!("window.OPEN_WITH_FILES = [{}];", files);
|
||||
if let Err(e) = window.eval(&script) {
|
||||
eprintln!("Failed to set open files variable: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[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_shell::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
@@ -47,10 +72,41 @@ pub fn run() {
|
||||
let builder = builder.plugin(tauri_traffic_light_positioner_plugin::init());
|
||||
|
||||
builder
|
||||
.setup(|app| {
|
||||
.setup(|#[allow(unused_variables)] app| {
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
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`
|
||||
for maybe_file in std::env::args().skip(1) {
|
||||
// skip flags like -f or --flag
|
||||
if maybe_file.starts_with("-") {
|
||||
continue;
|
||||
}
|
||||
// handle `file://` path urls and skip other urls
|
||||
if let Ok(url) = Url::parse(&maybe_file) {
|
||||
if let Ok(path) = url.to_file_path() {
|
||||
files.push(path);
|
||||
} else {
|
||||
files.push(PathBuf::from(maybe_file))
|
||||
}
|
||||
} else {
|
||||
files.push(PathBuf::from(maybe_file))
|
||||
}
|
||||
}
|
||||
if !files.is_empty() {
|
||||
let app_handle = app.handle().clone();
|
||||
allow_file_in_scopes(&app_handle, files.clone());
|
||||
app.listen("window-ready", move |_| {
|
||||
println!("Window is ready, proceeding to handle files.");
|
||||
set_window_open_with_files(&app_handle, files.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(desktop)]
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
#[cfg(desktop)]
|
||||
app.handle()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
@@ -73,29 +129,35 @@ pub fn run() {
|
||||
let win_builder = win_builder.decorations(false).transparent(true);
|
||||
|
||||
win_builder.build().unwrap();
|
||||
// let win = win_builder.build().unwrap();
|
||||
// win.open_devtools();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let global_menu = app.menu().unwrap();
|
||||
if let Some(item) = global_menu.get(HELP_SUBMENU_ID) {
|
||||
global_menu.remove(&item)?;
|
||||
}
|
||||
global_menu.append(
|
||||
&SubmenuBuilder::new(app, "Help")
|
||||
.text("privacy_policy", "Privacy Policy")
|
||||
.separator()
|
||||
.text("report_issue", "Report An Issue...")
|
||||
.text("readest_help", "Readest Help")
|
||||
.build()?,
|
||||
)?;
|
||||
menu::setup_macos_menu(&app.handle())?;
|
||||
|
||||
app.on_menu_event(move |app, event| {
|
||||
handle_menu_event(app, &event);
|
||||
});
|
||||
}
|
||||
app.handle().emit("window-ready", {}).unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application")
|
||||
.run(
|
||||
#[allow(unused_variables)]
|
||||
|app_handle, event| {
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
if let tauri::RunEvent::Opened { urls } = event {
|
||||
let files = urls
|
||||
.into_iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let app_handler_clone = app_handle.clone();
|
||||
allow_file_in_scopes(&app_handle, files.clone());
|
||||
app_handle.listen("window-ready", move |_| {
|
||||
println!("Window is ready, proceeding to handle files.");
|
||||
set_window_open_with_files(&app_handler_clone, files.clone());
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use tauri::menu::MenuEvent;
|
||||
use tauri::menu::{SubmenuBuilder, HELP_SUBMENU_ID};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
pub fn setup_macos_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
let global_menu = app.menu().unwrap();
|
||||
|
||||
if let Some(item) = global_menu.get(HELP_SUBMENU_ID) {
|
||||
global_menu.remove(&item)?;
|
||||
}
|
||||
|
||||
global_menu.append(
|
||||
&SubmenuBuilder::new(app, "Help")
|
||||
.text("privacy_policy", "Privacy Policy")
|
||||
.separator()
|
||||
.text("report_issue", "Report An Issue...")
|
||||
.text("readest_help", "Readest Help")
|
||||
.build()?,
|
||||
)?;
|
||||
|
||||
app.on_menu_event(|app, event| {
|
||||
handle_menu_event(app, &event);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_menu_event(app: &AppHandle, event: &MenuEvent) {
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,14 +49,83 @@
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "12.0"
|
||||
},
|
||||
"fileAssociations": [
|
||||
{
|
||||
"name": "epub",
|
||||
"ext": ["epub"],
|
||||
"description": "EPUB file",
|
||||
"mimeType": "application/epub+zip",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "mobi",
|
||||
"ext": ["mobi"],
|
||||
"description": "MOBI file",
|
||||
"mimeType": "application/x-mobipocket-ebook",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "azw",
|
||||
"ext": ["azw", "azw3"],
|
||||
"description": "AZW file",
|
||||
"mimeType": "application/vnd.amazon.ebook",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "fb2",
|
||||
"ext": ["fb2"],
|
||||
"description": "FB2 file",
|
||||
"mimeType": "application/x-fictionbook+xml",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "cbz",
|
||||
"ext": ["cbz"],
|
||||
"description": "CBZ file",
|
||||
"mimeType": "application/vnd.comicbook+zip",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "pdf",
|
||||
"ext": ["pdf"],
|
||||
"description": "PDF file",
|
||||
"mimeType": "application/pdf",
|
||||
"role": "Viewer"
|
||||
}
|
||||
],
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"fs": {
|
||||
"requireLiteralLeadingDot": false
|
||||
},
|
||||
"cli": {
|
||||
"description": "Readest CLI",
|
||||
"args": [
|
||||
{
|
||||
"name": "file1",
|
||||
"index": 1,
|
||||
"takesValue": true
|
||||
},
|
||||
{
|
||||
"name": "file2",
|
||||
"index": 2,
|
||||
"takesValue": true
|
||||
},
|
||||
{
|
||||
"name": "file3",
|
||||
"index": 3,
|
||||
"takesValue": true
|
||||
},
|
||||
{
|
||||
"name": "file4",
|
||||
"index": 4,
|
||||
"takesValue": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDMDdERDIyMTQ1QzE5NUQKUldSZEdWd1VJdDBIUE50UXVDeHZtUXVyb1lnNUtVUFVKS0Fva2FDRVc0K0NubWpmajlxY2I1bCsK",
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0OTAxMURGQkUzQjFENTQKUldSVUhUdSszeEdRNUExdmFkWnlvYWNYNG5wamkxMmUxRk5SejlMOTJVd28yNXlVTDh6Wld4OC8K",
|
||||
"endpoints": ["https://github.com/chrox/readest/releases/latest/download/latest.json"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { EnvProvider } from '@/context/EnvContext';
|
||||
import { CSPostHogProvider } from '@/context/PHContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
import '../styles/globals.css';
|
||||
import '../styles/fonts.css';
|
||||
|
||||
const url = 'https://web.readest.com/';
|
||||
const title = 'Readest — Where You Read, Digest and Get Insight';
|
||||
const description = 'Readest brings your entire library to your fingertips.';
|
||||
const previewImage = 'https://cdn.readest.com/images/open_graph_preview_read_now.png';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
useTheme();
|
||||
const pathname = usePathname();
|
||||
React.useEffect(() => {
|
||||
document.documentElement.setAttribute('data-page', pathname.replace('/', '') || 'default');
|
||||
}, [pathname]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// TODO: disabled for now
|
||||
// if (process.env['NODE_ENV'] === 'production') {
|
||||
// document.oncontextmenu = (event) => {
|
||||
// event.preventDefault();
|
||||
// };
|
||||
// }
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
<meta name='description' content={description} />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:image' content={previewImage} />
|
||||
<meta name='twitter:card' content='summary_large_image' />
|
||||
<meta property='twitter:domain' content='web.readest.com' />
|
||||
<meta property='twitter:url' content={url} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:image' content={previewImage} />
|
||||
</head>
|
||||
<CSPostHogProvider>
|
||||
<body>
|
||||
|
||||
@@ -11,6 +11,7 @@ import Image from 'next/image';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
|
||||
@@ -68,7 +69,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
setClickedImage(id);
|
||||
setTimeout(() => setClickedImage(null), 300);
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
router.push(`/reader?ids=${id}`);
|
||||
navigateToReader(router, [id]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,7 +81,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
|
||||
const openSelectedBooks = () => {
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
router.push(`/reader?ids=${selectedBooks.join(',')}`);
|
||||
navigateToReader(router, selectedBooks);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FaSearch } from 'react-icons/fa';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { PiSelectionAllDuotone } from 'react-icons/pi';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
|
||||
@@ -18,6 +19,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
onImportBooks,
|
||||
onToggleSelectMode,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -33,21 +35,6 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
};
|
||||
}, [onToggleSelectMode]);
|
||||
|
||||
const handleMinimize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().minimize();
|
||||
};
|
||||
|
||||
const handleToggleMaximize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().toggleMaximize();
|
||||
};
|
||||
|
||||
const handleClose = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={headerRef}
|
||||
@@ -57,7 +44,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center justify-between space-x-6'>
|
||||
<div className='sm:w relative flex w-full items-center pl-4'>
|
||||
<div className='exclude-title-bar-mousedown sm:w relative flex w-full items-center pl-4'>
|
||||
<span className='absolute left-8 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
</span>
|
||||
@@ -100,12 +87,9 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</div>
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible}
|
||||
showMaximize={!isTrafficLightVisible}
|
||||
showClose={!isTrafficLightVisible}
|
||||
onMinimize={handleMinimize}
|
||||
onToggleMaximize={handleToggleMaximize}
|
||||
onClose={handleClose}
|
||||
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,32 +2,109 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { AppService } from '@/types/system';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from '@/app/library/components/LibraryHeader';
|
||||
import Bookshelf from '@/app/library/components/Bookshelf';
|
||||
|
||||
const LibraryPage = () => {
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { library: libraryBooks, setLibrary } = useLibraryStore();
|
||||
const {
|
||||
library: libraryBooks,
|
||||
setLibrary,
|
||||
checkOpenWithBooks,
|
||||
clearOpenWithBooks,
|
||||
} = useLibraryStore();
|
||||
useTheme();
|
||||
const { setSettings } = useSettingsStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const doAppUpdates = async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await checkForAppUpdates();
|
||||
}
|
||||
};
|
||||
doAppUpdates();
|
||||
}, []);
|
||||
|
||||
const processOpenWithFiles = React.useCallback(
|
||||
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);
|
||||
}
|
||||
}
|
||||
setLibrary(libraryBooks);
|
||||
appService.saveLibraryBooks(libraryBooks);
|
||||
|
||||
console.log('Opening books:', bookIds);
|
||||
if (bookIds.length > 0) {
|
||||
navigateToReader(router, bookIds);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 200);
|
||||
envConfig.getAppService().then(async (appService) => {
|
||||
console.log('Loading library books...');
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
||||
const initLibrary = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const settings = await appService.loadSettings();
|
||||
setSettings(settings);
|
||||
|
||||
const libraryBooks = await appService.loadLibraryBooks();
|
||||
if (checkOpenWithBooks && isTauriAppPlatform()) {
|
||||
await handleOpenWithBooks(appService, libraryBooks);
|
||||
} else {
|
||||
clearOpenWithBooks();
|
||||
setLibrary(libraryBooks);
|
||||
}
|
||||
|
||||
setLibraryLoaded(true);
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenWithBooks = async (appService: AppService, libraryBooks: Book[]) => {
|
||||
const openWithFiles = (await parseOpenWithFiles()) || [];
|
||||
|
||||
if (openWithFiles.length > 0) {
|
||||
await processOpenWithFiles(appService, openWithFiles, libraryBooks);
|
||||
} else {
|
||||
clearOpenWithBooks();
|
||||
setLibrary(libraryBooks);
|
||||
}
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
return () => {
|
||||
clearOpenWithBooks();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -42,14 +119,14 @@ const LibraryPage = () => {
|
||||
};
|
||||
|
||||
const selectFilesTauri = async () => {
|
||||
return appService?.selectFiles('Select Books', ['epub', 'pdf']);
|
||||
return appService?.selectFiles('Select Books', SUPPORTED_FILE_EXTS);
|
||||
};
|
||||
|
||||
const selectFilesWeb = () => {
|
||||
return new Promise((resolve) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.epub, .pdf';
|
||||
fileInput.accept = FILE_ACCEPT_FORMATS;
|
||||
fileInput.multiple = true;
|
||||
fileInput.click();
|
||||
|
||||
@@ -61,12 +138,17 @@ const LibraryPage = () => {
|
||||
|
||||
const handleImportBooks = async () => {
|
||||
console.log('Importing books...');
|
||||
const { type } = await import('@tauri-apps/plugin-os');
|
||||
let files;
|
||||
if (['android', 'ios'].includes(type())) {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
|
||||
if (isTauriAppPlatform()) {
|
||||
const { type } = await import('@tauri-apps/plugin-os');
|
||||
if (['android', 'ios'].includes(type())) {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
} else {
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
}
|
||||
} else {
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
}
|
||||
importBooks(files);
|
||||
};
|
||||
@@ -79,6 +161,16 @@ const LibraryPage = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (checkOpenWithBooks) {
|
||||
return (
|
||||
loading && (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
<Spinner loading />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='library-page rounded-window bg-base-200/50 text-base-content flex h-full min-h-screen select-none flex-col overflow-hidden'>
|
||||
<div className='fixed top-0 z-40 w-full'>
|
||||
@@ -93,29 +185,30 @@ const LibraryPage = () => {
|
||||
<Spinner loading />
|
||||
</div>
|
||||
)}
|
||||
{libraryBooks.length > 0 ? (
|
||||
<div className='mt-12 flex-grow overflow-auto px-2'>
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
onImportBooks={handleImportBooks}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
<p className='mb-5'>
|
||||
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
|
||||
</button>
|
||||
{libraryLoaded &&
|
||||
(libraryBooks.length > 0 ? (
|
||||
<div className='mt-12 flex-grow overflow-auto px-2'>
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
onImportBooks={handleImportBooks}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
<p className='mb-5'>
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
|
||||
@@ -9,7 +10,7 @@ const HomePage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.push('/library');
|
||||
navigateToLibrary(router);
|
||||
}, [router]);
|
||||
|
||||
return <Spinner loading={true} />;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import Reader from '../components/Reader';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ ids: string }> }) {
|
||||
const ids = decodeURIComponent((await params).ids);
|
||||
return <Reader ids={ids} />;
|
||||
}
|
||||
@@ -4,14 +4,16 @@ import { BookDoc } from '@/libs/document';
|
||||
import { BookConfig, BookNote, BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import {
|
||||
handleKeydown,
|
||||
handleMousedown,
|
||||
handleClick,
|
||||
handleMouseup,
|
||||
handleClick,
|
||||
handleWheel,
|
||||
} from '../utils/iframeEventHandlers';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
@@ -45,7 +47,7 @@ export interface FoliateView extends HTMLElement {
|
||||
removeAttribute: (name: string) => void;
|
||||
next: () => Promise<void>;
|
||||
prev: () => Promise<void>;
|
||||
scrollTo?: (offset: number, reason: string, smooth: boolean) => void;
|
||||
goTo?: (params: { index: number; anchor: number }) => void;
|
||||
setStyles?: (css: string) => void;
|
||||
addEventListener: (type: string, listener: EventListener) => void;
|
||||
removeEventListener: (type: string, listener: EventListener) => void;
|
||||
@@ -73,71 +75,121 @@ const FoliateViewer: React.FC<{
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<FoliateView | null>(null);
|
||||
const isViewCreated = useRef(false);
|
||||
const isScrolling = useRef(false);
|
||||
const { getView, setView: setFoliateView, setProgress, getViewSettings } = useReaderStore();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const shouldAutoHideScrollbar = ['macos', 'ios'].includes(getOSPlatform());
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
// console.log('relocate:', detail);
|
||||
setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range);
|
||||
};
|
||||
|
||||
const handleScrollbarAutoHide = (doc: Document) => {
|
||||
if (doc && doc.defaultView && doc.defaultView.frameElement) {
|
||||
const iframe = doc.defaultView.frameElement as HTMLIFrameElement;
|
||||
const container = iframe.parentElement?.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
let hideScrollbarTimeout: ReturnType<typeof setTimeout>;
|
||||
const showScrollbar = () => {
|
||||
container.style.overflow = 'auto';
|
||||
container.style.scrollbarWidth = 'thin';
|
||||
};
|
||||
|
||||
const hideScrollbar = () => {
|
||||
container.style.overflow = 'hidden';
|
||||
container.style.scrollbarWidth = 'none';
|
||||
requestAnimationFrame(() => {
|
||||
container.style.overflow = 'auto';
|
||||
});
|
||||
};
|
||||
container.addEventListener('scroll', () => {
|
||||
showScrollbar();
|
||||
clearTimeout(hideScrollbarTimeout);
|
||||
hideScrollbarTimeout = setTimeout(hideScrollbar, 1000);
|
||||
});
|
||||
hideScrollbar();
|
||||
}
|
||||
};
|
||||
|
||||
const docLoadHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (detail.doc) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
if (viewSettings.scrolled && shouldAutoHideScrollbar) {
|
||||
handleScrollbarAutoHide(detail.doc);
|
||||
}
|
||||
if (!detail.doc.isEventListenersAdded) {
|
||||
detail.doc.isEventListenersAdded = true;
|
||||
detail.doc.addEventListener('keydown', handleKeydown.bind(null, bookKey));
|
||||
detail.doc.addEventListener('mousedown', handleMousedown.bind(null, bookKey));
|
||||
detail.doc.addEventListener('mouseup', handleMouseup.bind(null, bookKey));
|
||||
detail.doc.addEventListener('click', handleClick.bind(null, bookKey));
|
||||
detail.doc.addEventListener('wheel', handleWheel.bind(null, bookKey));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const docScrollHandler = (event: Event) => {
|
||||
setTimeout(() => {
|
||||
isScrolling.current = false;
|
||||
}, 300);
|
||||
if (isScrolling.current) return;
|
||||
isScrolling.current = true;
|
||||
|
||||
const docRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (detail.reason !== 'scroll') return;
|
||||
if (!viewRef.current!.renderer.scrolled) return;
|
||||
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
|
||||
const parallelViews = getParallels(bookKey);
|
||||
if (parallelViews && parallelViews.size > 0) {
|
||||
parallelViews.forEach((key) => {
|
||||
if (key !== bookKey) {
|
||||
const target = getView(key)?.renderer;
|
||||
if (target && target.scrolled && target.viewSize) {
|
||||
target.scrollTo?.(detail.fraction * target.viewSize, 'follow-scroll', true);
|
||||
if (target) {
|
||||
target.goTo?.({ index: detail.index, anchor: detail.fraction });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickTurnPage = (
|
||||
msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => {
|
||||
const handleTurnPage = (msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (msg instanceof MessageEvent) {
|
||||
if (msg.data && msg.data.type === 'iframe-single-click' && msg.data.bookKey === bookKey) {
|
||||
const viewElement = containerRef.current;
|
||||
if (viewElement) {
|
||||
const rect = viewElement.getBoundingClientRect();
|
||||
const { screenX } = msg.data;
|
||||
|
||||
const eventConsumed = eventDispatcher.dispatchSync('iframe-single-click', { screenX });
|
||||
if (!eventConsumed) {
|
||||
if (screenX >= rect.left + rect.width / 2) {
|
||||
viewRef.current?.goRight();
|
||||
} else if (screenX < rect.left + rect.width / 2) {
|
||||
viewRef.current?.goLeft();
|
||||
if (msg.data && msg.data.bookKey === bookKey) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
if (msg.data.type === 'iframe-single-click') {
|
||||
const viewElement = containerRef.current;
|
||||
if (viewElement) {
|
||||
const rect = viewElement.getBoundingClientRect();
|
||||
const { screenX, screenY } = msg.data;
|
||||
const consumed = eventDispatcher.dispatchSync('iframe-single-click', {
|
||||
screenX,
|
||||
screenY,
|
||||
});
|
||||
if (!consumed) {
|
||||
const centerStartX = rect.left + rect.width * 0.375;
|
||||
const centerEndX = rect.left + rect.width * 0.625;
|
||||
const centerStartY = rect.top + rect.height * 0.375;
|
||||
const centerEndY = rect.top + rect.height * 0.625;
|
||||
if (
|
||||
screenX >= centerStartX &&
|
||||
screenX <= centerEndX &&
|
||||
screenY >= centerStartY &&
|
||||
screenY <= centerEndY
|
||||
) {
|
||||
// toggle visibility of the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? '' : bookKey);
|
||||
} else if (screenX >= rect.left + rect.width / 2) {
|
||||
viewRef.current?.goRight();
|
||||
} else if (screenX < rect.left + rect.width / 2) {
|
||||
viewRef.current?.goLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg.data.type === 'iframe-wheel' && !viewSettings.scrolled) {
|
||||
const { deltaY } = msg.data;
|
||||
if (deltaY > 0) {
|
||||
viewRef.current?.next(1);
|
||||
} else if (deltaY < 0) {
|
||||
viewRef.current?.prev(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -153,10 +205,18 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleTurnPage);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleTurnPage);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hoveredBookKey, viewRef]);
|
||||
|
||||
useFoliateEvents(viewRef.current, {
|
||||
onLoad: docLoadHandler,
|
||||
onRelocate: progressRelocateHandler,
|
||||
onRendererRelocate: docScrollHandler,
|
||||
onRendererRelocate: docRelocateHandler,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -213,8 +273,6 @@ const FoliateViewer: React.FC<{
|
||||
} else {
|
||||
await view.goToFraction(0);
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleClickTurnPage);
|
||||
};
|
||||
|
||||
openBook();
|
||||
@@ -224,7 +282,7 @@ const FoliateViewer: React.FC<{
|
||||
return (
|
||||
<div
|
||||
className='foliate-viewer h-[100%] w-[100%]'
|
||||
onClick={(event) => handleClickTurnPage(event)}
|
||||
onClick={(event) => handleTurnPage(event)}
|
||||
ref={containerRef}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
@@ -29,10 +30,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
onCloseBook,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { hoveredBookKey, setHoveredBookKey, bookKeys } = useReaderStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
|
||||
const handleToggleDropdown = (isOpen: boolean) => {
|
||||
@@ -67,7 +69,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<div className='flex h-full items-center space-x-2'>
|
||||
<NotebookToggler bookKey={bookKey} />
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
onToggle={handleToggleDropdown}
|
||||
@@ -78,8 +80,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<WindowButtons
|
||||
className='window-buttons flex h-full items-center'
|
||||
headerRef={headerRef}
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
showMinimize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
|
||||
showMaximize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
|
||||
onClose={() => onCloseBook(bookKey)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense, useRef } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
|
||||
import ReaderContent from './ReaderContent';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { library, setLibrary } = useLibraryStore();
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
const initLibrary = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const settings = await appService.loadSettings();
|
||||
setSettings(settings);
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
library.length > 0 &&
|
||||
settings.globalReadSettings && (
|
||||
<div className='reader-page bg-base-100 text-base-content min-h-screen select-none'>
|
||||
<Suspense>
|
||||
<ReaderContent ids={ids} settings={settings} />
|
||||
<AboutWindow />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default Reader;
|
||||
@@ -10,7 +10,11 @@ import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { tauriHandleClose } from '@/utils/window';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import { BOOK_IDS_SEPARATOR } from '@/services/constants';
|
||||
|
||||
import useBooksManager from '../hooks/useBooksManager';
|
||||
import useBookShortcuts from '../hooks/useBookShortcuts';
|
||||
@@ -19,7 +23,7 @@ import SideBar from './sidebar/SideBar';
|
||||
import Notebook from './notebook/Notebook';
|
||||
import BooksGrid from './BooksGrid';
|
||||
|
||||
const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) => {
|
||||
const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ ids, settings }) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { envConfig } = useEnv();
|
||||
@@ -38,7 +42,8 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
|
||||
const initialIds = (searchParams.get('ids') || '').split(',').filter(Boolean);
|
||||
const bookIds = ids || searchParams.get('ids') || '';
|
||||
const initialIds = bookIds.split(BOOK_IDS_SEPARATOR).filter(Boolean);
|
||||
const initialBookKeys = initialIds.map((id) => `${id}-${uniqueId()}`);
|
||||
setBookKeys(initialBookKeys);
|
||||
const uniqueIds = new Set<string>();
|
||||
@@ -70,7 +75,7 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
|
||||
const saveSettingsAndGoToLibrary = () => {
|
||||
saveSettings(envConfig, settings);
|
||||
router.replace('/library');
|
||||
navigateToLibrary(router);
|
||||
};
|
||||
|
||||
const handleCloseBooks = () => {
|
||||
@@ -80,21 +85,26 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
saveSettingsAndGoToLibrary();
|
||||
};
|
||||
|
||||
const handleCloseBook = (bookKey: string) => {
|
||||
const handleCloseBook = async (bookKey: string) => {
|
||||
saveConfigAndCloseBook(bookKey);
|
||||
if (sideBarBookKey === bookKey) {
|
||||
setSideBarBookKey(getNextBookKey(sideBarBookKey));
|
||||
}
|
||||
dismissBook(bookKey);
|
||||
if (bookKeys.filter((key) => key !== bookKey).length == 0) {
|
||||
saveSettingsAndGoToLibrary();
|
||||
const openWithFiles = (await parseOpenWithFiles()) || [];
|
||||
if (openWithFiles.length > 0) {
|
||||
tauriHandleClose();
|
||||
} else {
|
||||
saveSettingsAndGoToLibrary();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!bookKeys || bookKeys.length === 0) return null;
|
||||
const bookData = getBookData(bookKeys[0]!);
|
||||
if (!bookData || !bookData.book || !bookData.bookDoc) {
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
setTimeout(() => setLoading(true), 300);
|
||||
return (
|
||||
loading && (
|
||||
<div className={'hero hero-content min-h-screen'}>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 MenuItem from '@/components/MenuItem';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTheme, ThemeMode } from '@/hooks/useTheme';
|
||||
@@ -22,7 +23,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
const { getView, getViews, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
const { themeMode, isDarkMode, themeCode, updateThemeMode } = useTheme();
|
||||
const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled);
|
||||
@@ -55,6 +56,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
|
||||
getView(bookKey)?.renderer.setAttribute(
|
||||
'max-inline-size',
|
||||
`${viewSettings.maxColumnCount === 1 || isScrolledMode ? ONE_COLUMN_MAX_INLINE_SIZE : viewSettings.maxInlineSize}px`,
|
||||
);
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
viewSettings!.scrolled = isScrolledMode;
|
||||
setViewSettings(bookKey, viewSettings!);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -80,7 +86,6 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
id='exclude-title-bar-mousedown'
|
||||
tabIndex={0}
|
||||
className='view-menu dropdown-content dropdown-right no-triangle border-base-100 z-20 mt-1 w-72 border shadow-2xl'
|
||||
>
|
||||
|
||||
@@ -58,8 +58,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalReadSettings.highlightStyles[selectedStyle],
|
||||
);
|
||||
|
||||
const dictPopupWidth = 400;
|
||||
const dictPopupHeight = 300;
|
||||
const dictPopupWidth = 480;
|
||||
const dictPopupHeight = 360;
|
||||
const annotPopupWidth = 280;
|
||||
const annotPopupHeight = 44;
|
||||
const popupPadding = 10;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
|
||||
const LANGUAGES = {
|
||||
AUTO: 'Auto Detect',
|
||||
@@ -61,8 +61,19 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
setError(null);
|
||||
setTranslation(null);
|
||||
|
||||
if (!process.env['NEXT_PUBLIC_DEEPL_API_KEY']) {
|
||||
console.error('DeepL API key not found. Set NEXT_PUBLIC_DEEPL_API_KEY in .env.local');
|
||||
}
|
||||
|
||||
const { fetch, url } = isWebAppPlatform()
|
||||
? { fetch: window.fetch, url: '/api/deepl/translate' }
|
||||
: {
|
||||
fetch: (await import('@tauri-apps/plugin-http')).fetch,
|
||||
url: 'https://api-free.deepl.com/v2/translate',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api-free.deepl.com/v2/translate', {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -78,7 +89,6 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch translation');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const translatedText = data.translations[0]?.text;
|
||||
const detectedSource = data.translations[0]?.detected_source_language;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
|
||||
const useBooksManager = () => {
|
||||
const router = useRouter();
|
||||
@@ -18,11 +19,9 @@ const useBooksManager = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldUpdateSearchParams) {
|
||||
const ids = bookKeys.map((key) => key.split('-')[0]).join(',');
|
||||
const ids = bookKeys.map((key) => key.split('-')[0]!);
|
||||
if (ids) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('ids', ids);
|
||||
router.replace(`?${params.toString()}`, { scroll: false });
|
||||
navigateToReader(router, ids, searchParams.toString(), { scroll: false });
|
||||
}
|
||||
setShouldUpdateSearchParams(false);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense, useRef } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
|
||||
import ReaderContent from './components/ReaderContent';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
|
||||
const ReaderPage = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { library, setLibrary } = useLibraryStore();
|
||||
const isInitiating = useRef(false);
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import Reader from './components/Reader';
|
||||
|
||||
export default function Page() {
|
||||
useTheme();
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
const initLibrary = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const settings = await appService.loadSettings();
|
||||
setSettings(settings);
|
||||
console.log('initializing library in reader');
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
const doAppUpdates = async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await checkForAppUpdates();
|
||||
}
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
doAppUpdates();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
library.length > 0 &&
|
||||
settings.globalReadSettings && (
|
||||
<div className='reader-page bg-base-100 text-base-content min-h-screen select-none'>
|
||||
<Suspense>
|
||||
<ReaderContent settings={settings} />
|
||||
<AboutWindow />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ReaderPage;
|
||||
return <Reader />;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,26 @@ export const handleMouseup = (bookKey: string, event: MouseEvent) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const handleWheel = (bookKey: string, event: WheelEvent) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'iframe-wheel',
|
||||
bookKey,
|
||||
deltaMode: event.deltaMode,
|
||||
deltaX: event.deltaX,
|
||||
deltaY: event.deltaY,
|
||||
deltaZ: event.deltaZ,
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
offsetX: event.offsetX,
|
||||
offsetY: event.offsetY,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
|
||||
export const handleClick = (bookKey: string, event: MouseEvent) => {
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { tauriHandleMinimize, tauriHandleToggleMaximize, tauriHandleClose } from '@/utils/window';
|
||||
|
||||
interface WindowButtonsProps {
|
||||
className?: string;
|
||||
headerRef?: React.RefObject<HTMLDivElement>;
|
||||
@@ -12,6 +14,24 @@ interface WindowButtonsProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface WindowButtonProps {
|
||||
id: string;
|
||||
onClick: () => void;
|
||||
ariaLabel: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const WindowButton: React.FC<WindowButtonProps> = ({ onClick, ariaLabel, id, children }) => (
|
||||
<button
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
className='window-button text-base-content/85 hover:text-base-content'
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
className,
|
||||
headerRef,
|
||||
@@ -27,11 +47,11 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
const handleMouseDown = async (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
if (target !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.closest('#exclude-title-bar-mousedown')) {
|
||||
if (
|
||||
target.closest('.btn') ||
|
||||
target.closest('.window-button') ||
|
||||
target.closest('.exclude-title-bar-mousedown')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,18 +78,24 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
const handleMinimize = async () => {
|
||||
if (onMinimize) {
|
||||
onMinimize();
|
||||
} else {
|
||||
tauriHandleMinimize();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaximize = async () => {
|
||||
if (onToggleMaximize) {
|
||||
onToggleMaximize();
|
||||
} else {
|
||||
tauriHandleToggleMaximize();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = async () => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
} else {
|
||||
tauriHandleClose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -83,45 +109,30 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
)}
|
||||
>
|
||||
{showMinimize && (
|
||||
<button
|
||||
onClick={handleMinimize}
|
||||
className='window-button text-base-content'
|
||||
aria-label='Minimize'
|
||||
id='titlebar-minimize'
|
||||
>
|
||||
<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' />
|
||||
</svg>
|
||||
</button>
|
||||
</WindowButton>
|
||||
)}
|
||||
|
||||
{showMaximize && (
|
||||
<button
|
||||
onClick={handleMaximize}
|
||||
className='window-button text-base-content'
|
||||
aria-label='Maximize/Restore'
|
||||
id='titlebar-maximize'
|
||||
>
|
||||
<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' />
|
||||
</svg>
|
||||
</button>
|
||||
</WindowButton>
|
||||
)}
|
||||
|
||||
{showClose && (
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className='window-button text-base-content'
|
||||
aria-label='Close'
|
||||
id='titlebar-close'
|
||||
>
|
||||
<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
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</WindowButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
OPEN_WITH_FILES?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
interface CliArgument {
|
||||
value: string;
|
||||
occurrences: number;
|
||||
}
|
||||
|
||||
const parseWindowOpenWithFiles = () => {
|
||||
return window.OPEN_WITH_FILES;
|
||||
};
|
||||
|
||||
const parseCLIOpenWithFiles = async () => {
|
||||
const { getMatches } = await import('@tauri-apps/plugin-cli');
|
||||
const matches = await getMatches();
|
||||
const args = matches?.args;
|
||||
const files: string[] = [];
|
||||
if (args) {
|
||||
for (const name of ['file1', 'file2', 'file3', 'file4']) {
|
||||
const arg = args[name] as CliArgument;
|
||||
if (arg && arg.occurrences > 0) {
|
||||
files.push(arg.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
export const parseOpenWithFiles = async () => {
|
||||
if (isWebAppPlatform()) return [];
|
||||
|
||||
let files = parseWindowOpenWithFiles();
|
||||
if (!files) {
|
||||
files = await parseCLIOpenWithFiles();
|
||||
}
|
||||
return files;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
|
||||
export const checkForAppUpdates = async () => {
|
||||
const update = await check();
|
||||
if (update) {
|
||||
const yes = await ask(
|
||||
`
|
||||
Update to ${update.version} is available!
|
||||
Release notes: ${update.body}
|
||||
`,
|
||||
{
|
||||
title: 'Update Now!',
|
||||
kind: 'info',
|
||||
okLabel: 'Update',
|
||||
cancelLabel: 'Cancel',
|
||||
},
|
||||
);
|
||||
|
||||
if (yes) {
|
||||
console.log(`found update ${update.version} from ${update.date} with notes ${update.body}`);
|
||||
let downloaded = 0;
|
||||
let contentLength = 0;
|
||||
await update.downloadAndInstall((event) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
contentLength = event.data.contentLength!;
|
||||
console.log(`started downloading ${event.data.contentLength} bytes`);
|
||||
break;
|
||||
case 'Progress':
|
||||
downloaded += event.data.chunkLength;
|
||||
console.log(`downloaded ${downloaded} from ${contentLength}`);
|
||||
break;
|
||||
case 'Finished':
|
||||
console.log('download finished');
|
||||
break;
|
||||
}
|
||||
});
|
||||
console.log('update installed');
|
||||
await relaunch();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -30,7 +30,10 @@ const useTrafficLight = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.hasTrafficLight) return;
|
||||
|
||||
handleSwitchFullScreen();
|
||||
|
||||
return () => {
|
||||
unlistenEnterFullScreen?.();
|
||||
unlistenExitFullScreen?.();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppService, ToastType } from '@/types/system';
|
||||
import { AppPlatform, AppService, ToastType } from '@/types/system';
|
||||
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { FileSystem, BaseDir } from '@/types/system';
|
||||
@@ -26,14 +26,15 @@ import {
|
||||
|
||||
export abstract class BaseAppService implements AppService {
|
||||
localBooksDir: string = '';
|
||||
abstract fs: FileSystem;
|
||||
abstract appPlatform: AppPlatform;
|
||||
abstract isAppDataSandbox: boolean;
|
||||
abstract hasTrafficLight: boolean;
|
||||
abstract fs: FileSystem;
|
||||
|
||||
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
|
||||
abstract getCoverImageUrl(book: Book): string;
|
||||
abstract getCoverImageBlobUrl(book: Book): Promise<string>;
|
||||
abstract getInitBooksDir(): Promise<string>;
|
||||
abstract selectDirectory(title: string): Promise<string>;
|
||||
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
abstract showMessage(
|
||||
msg: string,
|
||||
@@ -93,7 +94,7 @@ export abstract class BaseAppService implements AppService {
|
||||
file: string | File,
|
||||
books: Book[],
|
||||
overwrite: boolean = false,
|
||||
): Promise<Book[]> {
|
||||
): Promise<Book | null> {
|
||||
try {
|
||||
let loadedBook: BookDoc;
|
||||
let format: BookFormat;
|
||||
@@ -133,8 +134,6 @@ export abstract class BaseAppService implements AppService {
|
||||
author: loadedBook.metadata.author,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
book.coverImageUrl = this.getCoverImageUrl(book);
|
||||
|
||||
if (!(await this.fs.exists(getDir(book), 'Books'))) {
|
||||
await this.fs.createDir(getDir(book), 'Books');
|
||||
}
|
||||
@@ -156,11 +155,18 @@ export abstract class BaseAppService implements AppService {
|
||||
await this.saveBookConfig(book, INIT_BOOK_CONFIG);
|
||||
books.splice(0, 0, book);
|
||||
}
|
||||
|
||||
if (this.appPlatform === 'web') {
|
||||
book.coverImageUrl = await this.getCoverImageBlobUrl(book);
|
||||
} else {
|
||||
book.coverImageUrl = this.getCoverImageUrl(book);
|
||||
}
|
||||
|
||||
return book;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return books;
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteBook(book: Book): Promise<void> {
|
||||
@@ -173,7 +179,13 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
async loadBookContent(book: Book, settings: SystemSettings): Promise<BookContent> {
|
||||
const fp = getFilename(book);
|
||||
const file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open();
|
||||
let file: File;
|
||||
if (this.appPlatform === 'web') {
|
||||
const content = await this.fs.readFile(fp, 'Books', 'binary');
|
||||
file = new File([content], fp);
|
||||
} else {
|
||||
file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open();
|
||||
}
|
||||
return { book, file, config: await this.loadBookConfig(book, settings) };
|
||||
}
|
||||
|
||||
@@ -210,6 +222,7 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
async loadLibraryBooks(): Promise<Book[]> {
|
||||
console.log('Loading library books...');
|
||||
let books: Book[] = [];
|
||||
const libraryFilename = getLibraryFilename();
|
||||
|
||||
@@ -221,9 +234,16 @@ export abstract class BaseAppService implements AppService {
|
||||
await this.fs.writeFile(libraryFilename, 'Books', '[]');
|
||||
}
|
||||
|
||||
books.forEach((book) => {
|
||||
book.coverImageUrl = this.getCoverImageUrl(book);
|
||||
});
|
||||
await Promise.all(
|
||||
books.map(async (book) => {
|
||||
if (this.appPlatform === 'web') {
|
||||
book.coverImageUrl = await this.getCoverImageBlobUrl(book);
|
||||
} else {
|
||||
book.coverImageUrl = this.getCoverImageUrl(book);
|
||||
}
|
||||
return book;
|
||||
}),
|
||||
);
|
||||
|
||||
return books;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import { ReadSettings } from '@/types/settings';
|
||||
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
|
||||
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
|
||||
|
||||
export const SUPPORTED_FILE_EXTS = ['epub', 'mobi', 'azw', 'azw3', 'fb2', 'cbz', 'pdf'];
|
||||
export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).join(', ');
|
||||
|
||||
export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
sideBarWidth: '25%',
|
||||
isSideBarPinned: true,
|
||||
@@ -75,3 +78,5 @@ export const SANS_SERIF_FONTS = ['Roboto', 'Noto Sans', 'Open Sans', 'Helvetica'
|
||||
export const MONOSPACE_FONTS = ['Fira Code', 'Lucida Console', 'Consolas', 'Courier New'];
|
||||
|
||||
export const ONE_COLUMN_MAX_INLINE_SIZE = 9999;
|
||||
|
||||
export const BOOK_IDS_SEPARATOR = '+';
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
import { AppService } from '@/types/system';
|
||||
|
||||
let appService: AppService | null = null;
|
||||
export const isTauriAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri';
|
||||
export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web';
|
||||
|
||||
export interface EnvConfigType {
|
||||
getAppService: () => Promise<AppService>;
|
||||
}
|
||||
|
||||
let nativeAppService: AppService | null = null;
|
||||
const getNativeAppService = async () => {
|
||||
if (!nativeAppService) {
|
||||
const { NativeAppService } = await import('@/services/nativeAppService');
|
||||
nativeAppService = new NativeAppService();
|
||||
await nativeAppService.loadSettings();
|
||||
}
|
||||
return nativeAppService;
|
||||
};
|
||||
|
||||
let webAppService: AppService | null = null;
|
||||
const getWebAppService = async () => {
|
||||
if (!webAppService) {
|
||||
const { WebAppService } = await import('@/services/webAppService');
|
||||
webAppService = new WebAppService();
|
||||
await webAppService.loadSettings();
|
||||
}
|
||||
return webAppService;
|
||||
};
|
||||
|
||||
const environmentConfig: EnvConfigType = {
|
||||
getAppService: async () => {
|
||||
if (!appService) {
|
||||
const { NativeAppService } = await import('@/services/nativeAppService');
|
||||
appService = new NativeAppService();
|
||||
await appService.loadSettings();
|
||||
if (isTauriAppPlatform()) {
|
||||
return getNativeAppService();
|
||||
} else {
|
||||
return getWebAppService();
|
||||
}
|
||||
return appService;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { join, appDataDir } from '@tauri-apps/api/path';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { ToastType, FileSystem, BaseDir } from '@/types/system';
|
||||
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
|
||||
import { BaseAppService } from './appService';
|
||||
@@ -24,10 +24,7 @@ import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
export const isMobile = ['android', 'ios'].includes(osType());
|
||||
|
||||
export const resolvePath = (
|
||||
fp: string,
|
||||
base: BaseDir,
|
||||
): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
switch (base) {
|
||||
case 'Settings':
|
||||
return { baseDir: BaseDirectory.AppConfig, fp, base };
|
||||
@@ -56,6 +53,10 @@ export const nativeFileSystem: FileSystem = {
|
||||
getURL(path: string) {
|
||||
return convertFileSrc(path);
|
||||
},
|
||||
async getBlobURL(path: string, base: BaseDir) {
|
||||
const content = await this.readFile(path, base, 'binary');
|
||||
return URL.createObjectURL(new Blob([content]));
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(dstPath, base);
|
||||
await copyFile(srcPath, fp, base && { toPathBaseDir: baseDir });
|
||||
@@ -114,6 +115,7 @@ export const nativeFileSystem: FileSystem = {
|
||||
|
||||
export class NativeAppService extends BaseAppService {
|
||||
fs = nativeFileSystem;
|
||||
appPlatform = 'tauri' as AppPlatform;
|
||||
isAppDataSandbox = isMobile;
|
||||
hasTrafficLight = osType() === 'macos';
|
||||
|
||||
@@ -125,14 +127,6 @@ export class NativeAppService extends BaseAppService {
|
||||
return join(await appDataDir(), LOCAL_BOOKS_SUBDIR);
|
||||
}
|
||||
|
||||
async selectDirectory(title: string): Promise<string> {
|
||||
const selected = await open({
|
||||
title,
|
||||
directory: true,
|
||||
});
|
||||
return selected as string;
|
||||
}
|
||||
|
||||
async selectFiles(name: string, extensions: string[]): Promise<string[]> {
|
||||
const selected = await open({
|
||||
multiple: true,
|
||||
@@ -153,4 +147,8 @@ export class NativeAppService extends BaseAppService {
|
||||
getCoverImageUrl = (book: Book): string => {
|
||||
return this.fs.getURL(`${this.localBooksDir}/${getCoverFilename(book)}`);
|
||||
};
|
||||
|
||||
getCoverImageBlobUrl = async (book: Book): Promise<string> => {
|
||||
return this.fs.getBlobURL(`${this.localBooksDir}/${getCoverFilename(book)}`, 'None');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { Book } from '@/types/book';
|
||||
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
|
||||
import { BaseAppService } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
switch (base) {
|
||||
case 'Books':
|
||||
return { baseDir: 0, fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`, base };
|
||||
case 'None':
|
||||
return { baseDir: 0, fp, base };
|
||||
default:
|
||||
return { baseDir: 0, fp: `${base}/${fp}`, base };
|
||||
}
|
||||
};
|
||||
|
||||
const dbName = 'AppFileSystem';
|
||||
const dbVersion = 1;
|
||||
|
||||
async function openIndexedDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(dbName, dbVersion);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains('files')) {
|
||||
db.createObjectStore('files', { keyPath: 'path' });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
const indexedDBFileSystem: FileSystem = {
|
||||
getURL(path: string) {
|
||||
return URL.createObjectURL(new Blob([path]));
|
||||
},
|
||||
async getBlobURL(path: string, base: BaseDir) {
|
||||
try {
|
||||
const content = await this.readFile(path, base, 'binary');
|
||||
return URL.createObjectURL(new Blob([content]));
|
||||
} catch {
|
||||
return path;
|
||||
}
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(dstPath, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const transaction = db.transaction('files', 'readwrite');
|
||||
const store = transaction.objectStore('files');
|
||||
const getRequest = store.get(srcPath);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
const data = getRequest.result;
|
||||
if (data) {
|
||||
store.put({ path: fp, content: data.content });
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`File not found: ${srcPath}`));
|
||||
}
|
||||
};
|
||||
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
},
|
||||
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<string | ArrayBuffer>((resolve, reject) => {
|
||||
const transaction = db.transaction('files', 'readonly');
|
||||
const store = transaction.objectStore('files');
|
||||
const request = store.get(fp);
|
||||
|
||||
request.onsuccess = async () => {
|
||||
if (request.result) {
|
||||
const content = request.result.content;
|
||||
if (mode === 'text') resolve(content);
|
||||
else {
|
||||
if (content instanceof Blob) {
|
||||
const arrayBuffer = await content.arrayBuffer();
|
||||
resolve(arrayBuffer);
|
||||
} else if (content instanceof ArrayBuffer) {
|
||||
resolve(content);
|
||||
} else if (typeof content === 'string') {
|
||||
resolve(new TextEncoder().encode(content).buffer as ArrayBuffer);
|
||||
} else {
|
||||
reject(new Error('Unsupported content type in IndexedDB'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`File not found: ${fp}`));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const transaction = db.transaction('files', 'readwrite');
|
||||
const store = transaction.objectStore('files');
|
||||
|
||||
store.put({ path: fp, content });
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
},
|
||||
async removeFile(path: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const transaction = db.transaction('files', 'readwrite');
|
||||
const store = transaction.objectStore('files');
|
||||
|
||||
store.delete(fp);
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
},
|
||||
async createDir() {
|
||||
// Directories are virtual in IndexedDB; no-op
|
||||
},
|
||||
async removeDir() {
|
||||
// Directories are virtual in IndexedDB; no-op
|
||||
},
|
||||
async readDir(path: string) {
|
||||
const db = await openIndexedDB();
|
||||
return new Promise<{ path: string; isDir: boolean }[]>((resolve, reject) => {
|
||||
const transaction = db.transaction('files', 'readonly');
|
||||
const store = transaction.objectStore('files');
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const files = request.result as { path: string }[];
|
||||
resolve(
|
||||
files
|
||||
.filter((file) => file.path.startsWith(path))
|
||||
.map((file) => ({ path: file.path, isDir: false })),
|
||||
);
|
||||
};
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
async exists(path: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const transaction = db.transaction('files', 'readonly');
|
||||
const store = transaction.objectStore('files');
|
||||
const request = store.get(fp);
|
||||
|
||||
request.onsuccess = () => resolve(!!request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export class WebAppService extends BaseAppService {
|
||||
fs = indexedDBFileSystem;
|
||||
appPlatform = 'web' as AppPlatform;
|
||||
isAppDataSandbox = false;
|
||||
hasTrafficLight = false;
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
}
|
||||
|
||||
async getInitBooksDir(): Promise<string> {
|
||||
return LOCAL_BOOKS_SUBDIR;
|
||||
}
|
||||
|
||||
async selectDirectory(): Promise<string> {
|
||||
throw new Error('selectDirectory is not supported in browser');
|
||||
}
|
||||
|
||||
async selectFiles(): Promise<string[]> {
|
||||
throw new Error('selectFiles is not supported in browser');
|
||||
}
|
||||
|
||||
async showMessage(msg: string, kind: ToastType = 'info'): Promise<void> {
|
||||
alert(`${kind.toUpperCase()}: ${msg}`);
|
||||
}
|
||||
|
||||
getCoverImageUrl = (book: Book): string => {
|
||||
return this.fs.getURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`);
|
||||
};
|
||||
|
||||
getCoverImageBlobUrl = async (book: Book): Promise<string> => {
|
||||
return this.fs.getBlobURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`, 'None');
|
||||
};
|
||||
}
|
||||
@@ -4,12 +4,16 @@ import { EnvConfigType } from '@/services/environment';
|
||||
|
||||
interface LibraryState {
|
||||
library: Book[];
|
||||
checkOpenWithBooks: boolean;
|
||||
clearOpenWithBooks: () => void;
|
||||
setLibrary: (books: Book[]) => void;
|
||||
deleteBook: (envConfig: EnvConfigType, book: Book) => void;
|
||||
}
|
||||
|
||||
export const useLibraryStore = create<LibraryState>((set, get) => ({
|
||||
library: [],
|
||||
checkOpenWithBooks: true,
|
||||
clearOpenWithBooks: () => set({ checkOpenWithBooks: false }),
|
||||
setLibrary: (books) => set({ library: books }),
|
||||
deleteBook: async (envConfig: EnvConfigType, book: Book) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { SystemSettings } from './settings';
|
||||
import { Book, BookConfig, BookContent } from './book';
|
||||
|
||||
export type AppPlatform = 'web' | 'tauri';
|
||||
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Log' | 'Cache' | 'None';
|
||||
export type ToastType = 'info' | 'warning' | 'error';
|
||||
|
||||
export interface FileSystem {
|
||||
getURL(path: string): string;
|
||||
getBlobURL(path: string, base: BaseDir): Promise<string>;
|
||||
copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise<void>;
|
||||
readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise<string | ArrayBuffer>;
|
||||
writeFile(path: string, base: BaseDir, content: string | ArrayBuffer): Promise<void>;
|
||||
@@ -18,16 +20,16 @@ export interface FileSystem {
|
||||
|
||||
export interface AppService {
|
||||
fs: FileSystem;
|
||||
appPlatform: AppPlatform;
|
||||
hasTrafficLight: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
|
||||
selectDirectory(title: string): Promise<string>;
|
||||
selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise<void>;
|
||||
|
||||
loadSettings(): Promise<SystemSettings>;
|
||||
saveSettings(settings: SystemSettings): Promise<void>;
|
||||
importBook(file: string | File, books: Book[], overwrite?: boolean): Promise<Book[]>;
|
||||
importBook(file: string | File, books: Book[], overwrite?: boolean): Promise<Book | null>;
|
||||
deleteBook(book: Book): Promise<void>;
|
||||
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
|
||||
saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings): Promise<void>;
|
||||
@@ -35,4 +37,5 @@ export interface AppService {
|
||||
loadLibraryBooks(): Promise<Book[]>;
|
||||
saveLibraryBooks(books: Book[]): Promise<void>;
|
||||
getCoverImageUrl(book: Book): string;
|
||||
getCoverImageBlobUrl(book: Book): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -22,3 +22,15 @@ export const makeSafeFilename = (filename: string, replacement = '_') => {
|
||||
|
||||
return safeName.trim();
|
||||
};
|
||||
|
||||
export const getOSPlatform = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.includes('macintosh') || userAgent.includes('mac os x')) return 'macos';
|
||||
if (userAgent.includes('windows nt')) return 'windows';
|
||||
if (userAgent.includes('linux')) return 'linux';
|
||||
if (/iphone|ipad|ipod/.test(userAgent)) return 'ios';
|
||||
if (userAgent.includes('android')) return 'android';
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { BOOK_IDS_SEPARATOR } from '@/services/constants';
|
||||
|
||||
export const navigateToReader = (
|
||||
router: ReturnType<typeof useRouter>,
|
||||
bookIds: string[],
|
||||
queryParams?: string,
|
||||
navOptions?: { scroll?: boolean },
|
||||
) => {
|
||||
const ids = bookIds.join(BOOK_IDS_SEPARATOR);
|
||||
if (isWebAppPlatform()) {
|
||||
router.push(`/reader/${ids}${queryParams ? `?${queryParams}` : ''}`, navOptions);
|
||||
} else {
|
||||
const params = new URLSearchParams(queryParams || '');
|
||||
params.set('ids', ids);
|
||||
router.push(`/reader?${params.toString()}`, navOptions);
|
||||
}
|
||||
};
|
||||
|
||||
export const navigateToLibrary = (router: ReturnType<typeof useRouter>) => {
|
||||
router.push('/library');
|
||||
};
|
||||
@@ -75,6 +75,7 @@ const getLayoutStyles = (
|
||||
tab-size: 2;
|
||||
}
|
||||
body {
|
||||
color: ${fg};
|
||||
zoom: ${zoomLevel}%;
|
||||
background-color: ${bg} !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const tauriHandleMinimize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().minimize();
|
||||
};
|
||||
|
||||
export const tauriHandleToggleMaximize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().toggleMaximize();
|
||||
};
|
||||
|
||||
export const tauriHandleClose = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().close();
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "@sindresorhus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
||||
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 948 KiB |
|
After Width: | Height: | Size: 286 KiB |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@readest/monorepo",
|
||||
"private": true,
|
||||
"repository": "chrox/readest",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"tauri": "pnpm --filter @readest/readest-app tauri"
|
||||
|
||||
@@ -10,7 +10,7 @@ importers:
|
||||
devDependencies:
|
||||
'@sindresorhus/tsconfig':
|
||||
specifier: ^6.0.0
|
||||
version: 7.0.0
|
||||
version: 6.0.0
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
@@ -38,19 +38,25 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
'@tauri-apps/plugin-cli':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@tauri-apps/plugin-log':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@tauri-apps/plugin-os':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@tauri-apps/plugin-os':
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@tauri-apps/plugin-shell':
|
||||
@@ -144,6 +150,12 @@ importers:
|
||||
postcss:
|
||||
specifier: ^8.4.49
|
||||
version: 8.4.49
|
||||
postcss-cli:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0(jiti@1.21.6)(postcss@8.4.49)
|
||||
postcss-nested:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2(postcss@8.4.49)
|
||||
raw-loader:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(webpack@5.96.1)
|
||||
@@ -610,8 +622,12 @@ packages:
|
||||
'@rushstack/eslint-patch@1.10.4':
|
||||
resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==}
|
||||
|
||||
'@sindresorhus/tsconfig@7.0.0':
|
||||
resolution: {integrity: sha512-i5K04hLAP44Af16zmDjG07E1NHuDgCM07SJAT4gY0LZSRrWYzwt4qkLem6TIbIVh0k51RkN2bF+lP+lM5eC9fw==}
|
||||
'@sindresorhus/merge-streams@2.3.0':
|
||||
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sindresorhus/tsconfig@6.0.0':
|
||||
resolution: {integrity: sha512-+fUdfuDd/7O2OZ9/UvJy76IEWn2Tpvm2l+rwUoS2Yz4jCUTSNOQQv2PLWrwekt8cPLwHmpHaBpay34bkBmVl2Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
@@ -692,21 +708,27 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-cli@2.0.0':
|
||||
resolution: {integrity: sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==}
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.0.1':
|
||||
resolution: {integrity: sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==}
|
||||
|
||||
'@tauri-apps/plugin-fs@2.0.2':
|
||||
resolution: {integrity: sha512-4YZaX2j7ta81M5/DL8aN10kTnpUkEpkPo1FTYPT8Dd0ImHe3azM8i8MrtjrDGoyBYLPO3zFv7df/mSCYF8oA0Q==}
|
||||
'@tauri-apps/plugin-fs@2.0.3':
|
||||
resolution: {integrity: sha512-3Xv9cg5krj/2gSo1HFn9GbKUE0keh+DMx7O+hQeSzg1T4yGY5Fv/+yBISmrPa2j9thi3qcjgPj8PnMH4rbl/Dw==}
|
||||
|
||||
'@tauri-apps/plugin-http@2.0.1':
|
||||
resolution: {integrity: sha512-j6IA3pVBybSCwPpsihpX4z8bs6PluuGtr06ahL/xy4D8HunNBTmRmadJrFOQi0gOAbaig4MkQ15nzNLBLy8R1A==}
|
||||
|
||||
'@tauri-apps/plugin-log@2.0.0':
|
||||
resolution: {integrity: sha512-C+NII9vzswqnOQE8k7oRtnaw0z5TZsMmnirRhXkCKDEhQQH9841Us/PC1WHtGiAaJ8za1A1JB2xXndEq/47X/w==}
|
||||
'@tauri-apps/plugin-log@2.0.1':
|
||||
resolution: {integrity: sha512-zFT3S/Q5liMKIjPPdUZL8aNbjvhHvyfkhuqct3/MQ6/7rk4ODstcUPu320KJBjZrNNH/UM+kaQBGrioIwtmP/Q==}
|
||||
|
||||
'@tauri-apps/plugin-os@2.0.0':
|
||||
resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==}
|
||||
|
||||
'@tauri-apps/plugin-process@2.0.0':
|
||||
resolution: {integrity: sha512-OYzi0GnkrF4NAnsHZU7U3tjSoP0PbeAlO7T1Z+vJoBUH9sFQ1NSLqWYWQyf8hcb3gVWe7P1JggjiskO+LST1ug==}
|
||||
|
||||
'@tauri-apps/plugin-shell@2.0.1':
|
||||
resolution: {integrity: sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==}
|
||||
|
||||
@@ -747,9 +769,6 @@ packages:
|
||||
resolution: {integrity: sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==}
|
||||
deprecated: This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed!
|
||||
|
||||
'@types/node@20.17.9':
|
||||
resolution: {integrity: sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==}
|
||||
|
||||
'@types/node@22.10.1':
|
||||
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
|
||||
|
||||
@@ -1391,6 +1410,10 @@ packages:
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1583,6 +1606,10 @@ packages:
|
||||
resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dependency-graph@0.11.0:
|
||||
resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
detect-indent@4.0.0:
|
||||
resolution: {integrity: sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2005,10 +2032,18 @@ packages:
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-intrinsic@1.2.4:
|
||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-stdin@9.0.0:
|
||||
resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
get-symbol-description@1.0.2:
|
||||
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2069,6 +2104,10 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
globby@14.0.2:
|
||||
resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gopd@1.1.0:
|
||||
resolution: {integrity: sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2795,6 +2834,10 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-type@5.0.0:
|
||||
resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
path-webpack@0.0.3:
|
||||
resolution: {integrity: sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ==}
|
||||
|
||||
@@ -2829,6 +2872,13 @@ packages:
|
||||
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
postcss-cli@11.0.0:
|
||||
resolution: {integrity: sha512-xMITAI7M0u1yolVcXJ9XTZiO9aO49mcoKQy6pCDFdMh9kGqhzLVpWxeD/32M/QBmkhcGypZFFOLNLmIW4Pg4RA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.0.0
|
||||
|
||||
postcss-import@15.1.0:
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -2853,16 +2903,47 @@ packages:
|
||||
ts-node:
|
||||
optional: true
|
||||
|
||||
postcss-load-config@5.1.0:
|
||||
resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.0.9'
|
||||
tsx: ^4.8.1
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
|
||||
postcss-nested@6.2.0:
|
||||
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.2.14
|
||||
|
||||
postcss-nested@7.0.2:
|
||||
resolution: {integrity: sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==}
|
||||
engines: {node: '>=18.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.2.14
|
||||
|
||||
postcss-reporter@7.1.0:
|
||||
resolution: {integrity: sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-selector-parser@7.0.0:
|
||||
resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
@@ -2953,6 +3034,10 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-hrtime@1.0.3:
|
||||
resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
private@0.1.8:
|
||||
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3078,6 +3163,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3212,6 +3301,10 @@ packages:
|
||||
resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
slash@5.1.0:
|
||||
resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
smob@1.5.0:
|
||||
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
|
||||
|
||||
@@ -3399,6 +3492,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
thenby@1.3.4:
|
||||
resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -3496,12 +3592,13 @@ packages:
|
||||
unbox-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
unicorn-magic@0.1.0:
|
||||
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
union-value@1.0.1:
|
||||
resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3597,6 +3694,10 @@ packages:
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yallist@2.1.2:
|
||||
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
|
||||
|
||||
@@ -3605,6 +3706,14 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yn@3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3940,7 +4049,9 @@ snapshots:
|
||||
|
||||
'@rushstack/eslint-patch@1.10.4': {}
|
||||
|
||||
'@sindresorhus/tsconfig@7.0.0': {}
|
||||
'@sindresorhus/merge-streams@2.3.0': {}
|
||||
|
||||
'@sindresorhus/tsconfig@6.0.0': {}
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
@@ -3993,11 +4104,15 @@ snapshots:
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.1.0
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.1.0
|
||||
|
||||
'@tauri-apps/plugin-cli@2.0.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.0.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-fs@2.0.2':
|
||||
'@tauri-apps/plugin-fs@2.0.3':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
@@ -4005,7 +4120,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-log@2.0.0':
|
||||
'@tauri-apps/plugin-log@2.0.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
@@ -4013,6 +4128,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-process@2.0.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-shell@2.0.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
@@ -4055,10 +4174,6 @@ snapshots:
|
||||
dependencies:
|
||||
localforage: 1.10.0
|
||||
|
||||
'@types/node@20.17.9':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/node@22.10.1':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
@@ -5100,6 +5215,12 @@ snapshots:
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
collection-visit@1.0.0:
|
||||
@@ -5299,6 +5420,8 @@ snapshots:
|
||||
is-descriptor: 1.0.3
|
||||
isobject: 3.0.1
|
||||
|
||||
dependency-graph@0.11.0: {}
|
||||
|
||||
detect-indent@4.0.0:
|
||||
dependencies:
|
||||
repeating: 2.0.1
|
||||
@@ -5889,6 +6012,8 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.2.4:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -5897,6 +6022,8 @@ snapshots:
|
||||
has-symbols: 1.0.3
|
||||
hasown: 2.0.2
|
||||
|
||||
get-stdin@9.0.0: {}
|
||||
|
||||
get-symbol-description@1.0.2:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@@ -5964,6 +6091,15 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.1.0
|
||||
|
||||
globby@14.0.2:
|
||||
dependencies:
|
||||
'@sindresorhus/merge-streams': 2.3.0
|
||||
fast-glob: 3.3.2
|
||||
ignore: 5.3.2
|
||||
path-type: 5.0.0
|
||||
slash: 5.1.0
|
||||
unicorn-magic: 0.1.0
|
||||
|
||||
gopd@1.1.0:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
@@ -6255,7 +6391,7 @@ snapshots:
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 20.17.9
|
||||
'@types/node': 22.10.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
@@ -6672,6 +6808,8 @@ snapshots:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-type@5.0.0: {}
|
||||
|
||||
path-webpack@0.0.3: {}
|
||||
|
||||
path2d@0.2.1:
|
||||
@@ -6694,6 +6832,25 @@ snapshots:
|
||||
|
||||
possible-typed-array-names@1.0.0: {}
|
||||
|
||||
postcss-cli@11.0.0(jiti@1.21.6)(postcss@8.4.49):
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
dependency-graph: 0.11.0
|
||||
fs-extra: 11.2.0
|
||||
get-stdin: 9.0.0
|
||||
globby: 14.0.2
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.4.49
|
||||
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.49)
|
||||
postcss-reporter: 7.1.0(postcss@8.4.49)
|
||||
pretty-hrtime: 1.0.3
|
||||
read-cache: 1.0.0
|
||||
slash: 5.1.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- tsx
|
||||
|
||||
postcss-import@15.1.0(postcss@8.4.49):
|
||||
dependencies:
|
||||
postcss: 8.4.49
|
||||
@@ -6714,16 +6871,40 @@ snapshots:
|
||||
postcss: 8.4.49
|
||||
ts-node: 10.9.2(@types/node@22.10.1)(typescript@5.7.2)
|
||||
|
||||
postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.49):
|
||||
dependencies:
|
||||
lilconfig: 3.1.2
|
||||
yaml: 2.6.1
|
||||
optionalDependencies:
|
||||
jiti: 1.21.6
|
||||
postcss: 8.4.49
|
||||
|
||||
postcss-nested@6.2.0(postcss@8.4.49):
|
||||
dependencies:
|
||||
postcss: 8.4.49
|
||||
postcss-selector-parser: 6.1.2
|
||||
|
||||
postcss-nested@7.0.2(postcss@8.4.49):
|
||||
dependencies:
|
||||
postcss: 8.4.49
|
||||
postcss-selector-parser: 7.0.0
|
||||
|
||||
postcss-reporter@7.1.0(postcss@8.4.49):
|
||||
dependencies:
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.4.49
|
||||
thenby: 1.3.4
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-selector-parser@7.0.0:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
@@ -6773,6 +6954,8 @@ snapshots:
|
||||
|
||||
prettier@3.4.1: {}
|
||||
|
||||
pretty-hrtime@1.0.3: {}
|
||||
|
||||
private@0.1.8: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
@@ -6926,6 +7109,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-finite: 1.1.0
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -7114,6 +7299,8 @@ snapshots:
|
||||
|
||||
slash@1.0.0: {}
|
||||
|
||||
slash@5.1.0: {}
|
||||
|
||||
smob@1.5.0: {}
|
||||
|
||||
snapdragon-node@2.1.1:
|
||||
@@ -7353,6 +7540,8 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
thenby@1.3.4: {}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
@@ -7474,10 +7663,10 @@ snapshots:
|
||||
has-symbols: 1.0.3
|
||||
which-boxed-primitive: 1.0.2
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
unicorn-magic@0.1.0: {}
|
||||
|
||||
union-value@1.0.1:
|
||||
dependencies:
|
||||
arr-union: 3.1.0
|
||||
@@ -7613,10 +7802,24 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@2.1.2: {}
|
||||
|
||||
yaml@2.6.1: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yn@3.1.1:
|
||||
optional: true
|
||||
|
||||
|
||||