forked from akai/readest
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5f18ab024 | |||
| d1f5934947 | |||
| f03d6f6861 | |||
| 20523b7eaf | |||
| f31c9b2a98 | |||
| 93071e727f | |||
| 2d12210606 | |||
| 88ccc528d4 | |||
| 9d3fc07865 | |||
| 6d465a40fb | |||
| 01c7e4c2e2 | |||
| 4005858242 | |||
| c24f951375 | |||
| dbe1efd1e9 | |||
| 0299cce343 | |||
| ca9f504329 | |||
| df3527ceca | |||
| 73d0838291 | |||
| 8e3574509e | |||
| 9bdf375304 | |||
| effa922597 | |||
| fe484257a0 | |||
| cc0cd9a114 | |||
| 916d63806e | |||
| 59d0118067 |
@@ -32,6 +32,11 @@ jobs:
|
||||
|
||||
build_web_app:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- platform: 'web'
|
||||
- platform: 'tauri'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -54,6 +59,13 @@ jobs:
|
||||
pnpm install && pnpm setup-pdfjs
|
||||
|
||||
- name: build the web App
|
||||
if: matrix.config.platform == 'web'
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build-web
|
||||
pnpm build-web && pnpm check:all
|
||||
|
||||
- name: build the Tauri App
|
||||
if: matrix.config.platform == 'tauri'
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build && pnpm check:all
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
[submodule "packages/tauri"]
|
||||
path = packages/tauri
|
||||
url = https://github.com/readest/tauri.git
|
||||
[submodule "packages/tauri-plugins"]
|
||||
path = packages/tauri-plugins
|
||||
url = https://github.com/readest/tauri-plugins-workspace.git
|
||||
|
||||
Generated
+366
-270
File diff suppressed because it is too large
Load Diff
+19
-1
@@ -2,9 +2,24 @@
|
||||
members = [
|
||||
"apps/readest-app/src-tauri",
|
||||
"packages/tauri/crates/tauri",
|
||||
"packages/tauri/crates/tauri-utils",
|
||||
"packages/tauri/crates/tauri-build",
|
||||
"packages/tauri-plugins/plugins/deep-link",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
log = "0.4"
|
||||
tauri = { version = "2", default-features = false }
|
||||
tauri-build = "2"
|
||||
tauri-plugin = "2"
|
||||
tauri-utils = "2"
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
url = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Bilingify LLC"]
|
||||
homepage = "https://readest.com"
|
||||
@@ -15,4 +30,7 @@ edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[patch.crates-io]
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
|
||||
tauri-build = { path = "packages/tauri/crates/tauri-build" }
|
||||
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
|
||||
@@ -19,6 +19,7 @@
|
||||
<br>
|
||||
[![Last commit][badge-last-commit]][link-gh-commits]
|
||||
[![Commits][badge-commit-activity]][link-gh-pulse]
|
||||
[![Ask DeepWiki][badge-deepwiki]][link-deepwiki]
|
||||
|
||||
</div>
|
||||
|
||||
@@ -56,6 +57,7 @@
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ |
|
||||
| **Translate with Yandex** | Instantly translate text or books using Yandex Translate. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
@@ -165,6 +167,8 @@ For Windows targets, “Build Tools for Visual Studio 2022” (or a higher editi
|
||||
pnpm tauri dev
|
||||
# or start development for the Web app
|
||||
pnpm dev-web
|
||||
# preview with OpenNext build for the Web app
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
For Android:
|
||||
@@ -304,6 +308,7 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
|
||||
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
|
||||
[badge-donate]: https://donate.readest.com/badge.svg
|
||||
[badge-deepwiki]: https://deepwiki.com/badge.svg
|
||||
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
|
||||
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
|
||||
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
|
||||
@@ -316,3 +321,4 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[link-parallel-read]: https://readest.com/#parallel-read
|
||||
[link-koreader]: https://github.com/koreader/koreader
|
||||
[link-hellogithub]: https://hellogithub.com/repository/8a5b6ade2aee461a8bd94e59200682a7
|
||||
[link-deepwiki]: https://deepwiki.com/readest/readest
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import withPWAInit from '@ducanh2912/next-pwa';
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
import withBundleAnalyzer from '@next/bundle-analyzer';
|
||||
|
||||
const isDev = process.env['NODE_ENV'] === 'development';
|
||||
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
|
||||
|
||||
if (isDev) {
|
||||
const { initOpenNextCloudflareForDev } = await import('@opennextjs/cloudflare');
|
||||
initOpenNextCloudflareForDev();
|
||||
}
|
||||
|
||||
@@ -25,6 +26,19 @@ const nextConfig = {
|
||||
// Configure assetPrefix or else the server won't properly resolve your assets.
|
||||
assetPrefix: '',
|
||||
reactStrictMode: true,
|
||||
serverExternalPackages: ['isows'],
|
||||
transpilePackages: !isDev
|
||||
? [
|
||||
'i18next-browser-languagedetector',
|
||||
'react-i18next',
|
||||
'i18next',
|
||||
'@ducanh2912/next-pwa',
|
||||
'@tauri-apps',
|
||||
'highlight.js',
|
||||
'foliate-js',
|
||||
'marked',
|
||||
]
|
||||
: [],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -55,4 +69,8 @@ const withPWA = withPWAInit({
|
||||
},
|
||||
});
|
||||
|
||||
export default withPWA(nextConfig);
|
||||
const withAnalyzer = withBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
export default withPWA(withAnalyzer(nextConfig));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.65",
|
||||
"version": "0.9.67",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -35,7 +35,10 @@
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
"check:optional-chaining": "count=$(grep -rno '\\?\\.[a-zA-Z_$]' .next/static/chunks/* out/_next/static/chunks/* | wc -l); if [ \"$count\" -gt 0 ]; then echo '❌ Optional chaining found in output!'; exit 1; else echo '✅ No optional chaining found.'; fi",
|
||||
"check:all": "pnpm check:optional-chaining",
|
||||
"build-check": "pnpm build && pnpm build-web && pnpm check:all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.735.0",
|
||||
@@ -49,18 +52,18 @@
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.50.2",
|
||||
"@tauri-apps/api": "2.6.0",
|
||||
"@tauri-apps/plugin-cli": "^2.3.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.5",
|
||||
"@tauri-apps/plugin-http": "^2.4.4",
|
||||
"@tauri-apps/plugin-log": "^2.5.1",
|
||||
"@tauri-apps/plugin-opener": "^2.3.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.2",
|
||||
"@tauri-apps/plugin-shell": "~2.2.2",
|
||||
"@tauri-apps/plugin-updater": "^2.8.1",
|
||||
"@tauri-apps/plugin-cli": "^2.4.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
"@tauri-apps/plugin-haptics": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "~2.3.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -90,11 +93,12 @@
|
||||
"semver": "^7.7.1",
|
||||
"stripe": "^18.2.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
"zustand": "5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^15.4.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.6.0",
|
||||
"@tauri-apps/cli": "2.7.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
|
||||
@@ -467,5 +467,7 @@
|
||||
"Fiction, Science, History": "خيال، علوم، تاريخ",
|
||||
"Select Cover Image": "اختر صورة الغلاف",
|
||||
"Open Book in New Window": "فتح الكتاب في نافذة جديدة",
|
||||
"Voices for {{lang}}": "الأصوات لـ {{lang}}"
|
||||
"Voices for {{lang}}": "الأصوات لـ {{lang}}",
|
||||
"Yandex Translate": "ترجمة Yandex",
|
||||
"YYYY or YYYY-MM-DD": "YYYY أو YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -451,5 +451,7 @@
|
||||
"Fiction, Science, History": "Belletristik, Wissenschaft, Geschichte",
|
||||
"Select Cover Image": "Coverbild auswählen",
|
||||
"Open Book in New Window": "Buch in neuem Fenster öffnen",
|
||||
"Voices for {{lang}}": "Stimmen für {{lang}}"
|
||||
"Voices for {{lang}}": "Stimmen für {{lang}}",
|
||||
"Yandex Translate": "Yandex Übersetzer",
|
||||
"YYYY or YYYY-MM-DD": "YYYY oder YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -451,5 +451,7 @@
|
||||
"Fiction, Science, History": "Μυθοπλασία, Επιστήμη, Ιστορία",
|
||||
"Select Cover Image": "Επιλέξτε εικόνα εξωφύλλου",
|
||||
"Open Book in New Window": "Άνοιγμα βιβλίου σε νέο παράθυρο",
|
||||
"Voices for {{lang}}": "Φωνές για {{lang}}"
|
||||
"Voices for {{lang}}": "Φωνές για {{lang}}",
|
||||
"Yandex Translate": "Μετάφραση Yandex",
|
||||
"YYYY or YYYY-MM-DD": "YYYY ή YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,7 @@
|
||||
"Fiction, Science, History": "Ficción, Ciencia, Historia",
|
||||
"Select Cover Image": "Seleccionar imagen de portada",
|
||||
"Open Book in New Window": "Abre el libro en una nueva ventana",
|
||||
"Voices for {{lang}}": "Voces para {{lang}}"
|
||||
"Voices for {{lang}}": "Voces para {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY o YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,7 @@
|
||||
"Fiction, Science, History": "Fiction, Science, Histoire",
|
||||
"Select Cover Image": "Sélectionner l'image de couverture",
|
||||
"Open Book in New Window": "Ouvrir dans une nouvelle fenêtre",
|
||||
"Voices for {{lang}}": "Voix pour {{lang}}"
|
||||
"Voices for {{lang}}": "Voix pour {{lang}}",
|
||||
"Yandex Translate": "Yandex Traduction",
|
||||
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -451,5 +451,7 @@
|
||||
"Fiction, Science, History": "काल्पनिक, विज्ञान, इतिहास",
|
||||
"Select Cover Image": "कवर इमेज चुनें",
|
||||
"Open Book in New Window": "नई विंडो में पुस्तक खोलें",
|
||||
"Voices for {{lang}}": "{{lang}} के लिए आवाज़ें"
|
||||
"Voices for {{lang}}": "{{lang}} के लिए आवाज़ें",
|
||||
"Yandex Translate": "यांडेक्स अनुवाद",
|
||||
"YYYY or YYYY-MM-DD": "YYYY या YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"Fiction, Science, History": "Fiksi, Sains, Sejarah",
|
||||
"Select Cover Image": "Pilihan Gambar Sampul",
|
||||
"Open Book in New Window": "Buka Buku di Jendela Baru",
|
||||
"Voices for {{lang}}": "Suara untuk {{lang}}"
|
||||
"Voices for {{lang}}": "Suara untuk {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY atau YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,7 @@
|
||||
"Fiction, Science, History": "Narrativa, Scienza, Storia",
|
||||
"Select Cover Image": "Seleziona immagine di copertina",
|
||||
"Open Book in New Window": "Apri libro in una nuova finestra",
|
||||
"Voices for {{lang}}": "Voci per {{lang}}"
|
||||
"Voices for {{lang}}": "Voci per {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "AAAA o AAAA-MM-GG"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"Fiction, Science, History": "小説、科学、歴史",
|
||||
"Select Cover Image": "表紙画像を選択",
|
||||
"Open Book in New Window": "新しいウィンドウで本を開く",
|
||||
"Voices for {{lang}}": "{{lang}}の音声"
|
||||
"Voices for {{lang}}": "{{lang}}の音声",
|
||||
"Yandex Translate": "Yandex翻訳",
|
||||
"YYYY or YYYY-MM-DD": "YYYYまたはYYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"Fiction, Science, History": "소설, 과학, 역사",
|
||||
"Select Cover Image": "표지 이미지 선택",
|
||||
"Open Book in New Window": "새 창에서 책 열기",
|
||||
"Voices for {{lang}}": "{{lang}}의 음성"
|
||||
"Voices for {{lang}}": "{{lang}}의 음성",
|
||||
"Yandex Translate": "Yandex 번역",
|
||||
"YYYY or YYYY-MM-DD": "YYYY 또는 YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -451,5 +451,7 @@
|
||||
"Fiction, Science, History": "Fictie, Wetenschap, Geschiedenis",
|
||||
"Select Cover Image": "Selecteer omslagafbeelding",
|
||||
"Open Book in New Window": "Open boek in nieuw venster",
|
||||
"Voices for {{lang}}": "Stemmen voor {{lang}}"
|
||||
"Voices for {{lang}}": "Stemmen voor {{lang}}",
|
||||
"Yandex Translate": "Yandex Vertalen",
|
||||
"YYYY or YYYY-MM-DD": "YYYY of YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -459,5 +459,7 @@
|
||||
"Fiction, Science, History": "Beletrystyka, Nauka, Historia",
|
||||
"Select Cover Image": "Zaznacz obraz okładki",
|
||||
"Open Book in New Window": "Otwórz książkę w nowym oknie",
|
||||
"Voices for {{lang}}": "{{lang}} Głosy"
|
||||
"Voices for {{lang}}": "{{lang}} Głosy",
|
||||
"Yandex Translate": "Yandex Tłumacz",
|
||||
"YYYY or YYYY-MM-DD": "YYYY lub YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -455,5 +455,7 @@
|
||||
"Fiction, Science, History": "Ficção, Ciência, História",
|
||||
"Select Cover Image": "Selecionar imagem da capa",
|
||||
"Open Book in New Window": "Abrir livro em nova janela",
|
||||
"Voices for {{lang}}": "Vozes para {{lang}}"
|
||||
"Voices for {{lang}}": "Vozes para {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -459,5 +459,7 @@
|
||||
"Fiction, Science, History": "Художественная литература, Наука, История",
|
||||
"Select Cover Image": "Выбрать изображение обложки",
|
||||
"Open Book in New Window": "Открыть книгу в новом окне",
|
||||
"Voices for {{lang}}": "Голоса для {{lang}}"
|
||||
"Voices for {{lang}}": "Голоса для {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY или YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"Fiction, Science, History": "นิยาย, วิทยาศาสตร์, ประวัติศาสตร์",
|
||||
"Select Cover Image": "เลือกภาพปก",
|
||||
"Open Book in New Window": "เปิดหนังสือในหน้าต่างใหม่",
|
||||
"Voices for {{lang}}": "เสียงสำหรับ {{lang}}"
|
||||
"Voices for {{lang}}": "เสียงสำหรับ {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY หรือ YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -451,5 +451,7 @@
|
||||
"Fiction, Science, History": "Kurgu, Bilim, Tarih",
|
||||
"Select Cover Image": "Kapak Resmi Seç",
|
||||
"Open Book in New Window": "Kitabı Yeni Pencerede Aç",
|
||||
"Voices for {{lang}}": "{{lang}} için Sesler"
|
||||
"Voices for {{lang}}": "{{lang}} için Sesler",
|
||||
"Yandex Translate": "Yandex Çeviri",
|
||||
"YYYY or YYYY-MM-DD": "YYYY veya YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -459,5 +459,7 @@
|
||||
"Fiction, Science, History": "Художня література, Наукова фантастика, Історія",
|
||||
"Select Cover Image": "Оберіть зображення обкладинки",
|
||||
"Open Book in New Window": "Відкрити книгу в новому вікні",
|
||||
"Voices for {{lang}}": "Голоси для {{lang}}"
|
||||
"Voices for {{lang}}": "Голоси для {{lang}}",
|
||||
"Yandex Translate": "Yandex Перекладач",
|
||||
"YYYY or YYYY-MM-DD": "YYYY або YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -446,6 +446,8 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Tiểu thuyết, Khoa học, Lịch sử",
|
||||
"Select Cover Image": "Chọn ảnh bìa",
|
||||
"Open Book in New Window": "Open Sách trong Cửa sổ Mới",
|
||||
"Voices for {{lang}}": "Giọng nói cho {{lang}}"
|
||||
"Open Book in New Window": "Mở Sách trong Cửa sổ Mới",
|
||||
"Voices for {{lang}}": "Giọng nói cho {{lang}}",
|
||||
"Yandex Translate": "Yandex Dịch",
|
||||
"YYYY or YYYY-MM-DD": "YYYY hoặc YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"Fiction, Science, History": "小说,科学,历史",
|
||||
"Select Cover Image": "选择封面图片",
|
||||
"Open Book in New Window": "在新窗口中打开书籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的语音"
|
||||
"Voices for {{lang}}": "{{lang}} 的语音",
|
||||
"Yandex Translate": "Yandex 翻译",
|
||||
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -447,5 +447,7 @@
|
||||
"Fiction, Science, History": "小說,科學,歷史",
|
||||
"Select Cover Image": "選擇封面圖片",
|
||||
"Open Book in New Window": "在新視窗中打開書籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的語音"
|
||||
"Voices for {{lang}}": "{{lang}} 的語音",
|
||||
"Yandex Translate": "Yandex 翻譯",
|
||||
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.67": {
|
||||
"date": "2025-07-22",
|
||||
"notes": [
|
||||
"Reader: Fixed file opening issue when filenames contain commas",
|
||||
"Translation: Added Yandex Translator as a new translation provider",
|
||||
"Layout: Hovering over header now reveals macOS traffic light window controls",
|
||||
"Theme: Reader background color is now customizable for EPUB and PDF",
|
||||
"Library: Search now includes book format, group names, and descriptions",
|
||||
"Library: Various improvements to metadata editor and bookshelf management",
|
||||
"Linux: Fixed social login failure on Linux systems"
|
||||
]
|
||||
},
|
||||
"0.9.66": {
|
||||
"date": "2025-07-21",
|
||||
"notes": [
|
||||
"Layout: Fixed nested table of contents items not expanding in long TOC lists",
|
||||
"Reader: Opening files from the OS now takes highest priority",
|
||||
"TTS: Improved voice filtering for some language codes",
|
||||
"App: Fixed loading issues on older browsers",
|
||||
"App: Fixed wrong MIME types generated on Linux"
|
||||
]
|
||||
},
|
||||
"0.9.65": {
|
||||
"date": "2025-07-19",
|
||||
"notes": [
|
||||
|
||||
@@ -15,7 +15,7 @@ name = "readestlib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.1.1", features = [] }
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
@@ -31,19 +31,19 @@ reqwest = { version = "0.12", default-features = false, features = [
|
||||
"stream",
|
||||
] }
|
||||
tauri = { version = "2.5.1", features = [ "protocol-asset" ] }
|
||||
tauri-build = "2.3.0"
|
||||
tauri-plugin-log = "2.4.0"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-http = "2.4.3"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-build = "2"
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-http = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-oauth = "2"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2.2.4"
|
||||
tauri-plugin-haptics = "2"
|
||||
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
|
||||
|
||||
@@ -60,6 +60,6 @@ objc2-foundation = { version = "0.3", features = ["NSError", "NSArray"] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-updater = "2.7.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
{
|
||||
"url": "https://edge.microsoft.com"
|
||||
},
|
||||
{
|
||||
"url": "https://translate.toil.cc"
|
||||
},
|
||||
{
|
||||
"url": "https://*.microsofttranslator.com"
|
||||
},
|
||||
|
||||
@@ -9,13 +9,13 @@ exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||
links = "tauri-plugin-native-bridge"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.3.1" }
|
||||
tauri = { version = "2" }
|
||||
serde = "1.0"
|
||||
thiserror = "2"
|
||||
schemars = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.4", features = ["build"] }
|
||||
tauri-plugin = { version = "2", features = ["build"] }
|
||||
schemars = "0.8"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
|
||||
@@ -9,11 +9,11 @@ exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||
links = "tauri-plugin-native-tts"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.5.0" }
|
||||
tauri = { version = "2" }
|
||||
serde = "1.0"
|
||||
thiserror = "2"
|
||||
schemars = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.2.0", features = ["build"] }
|
||||
tauri-plugin = { version = "2", features = ["build"] }
|
||||
schemars = "0.8"
|
||||
|
||||
@@ -252,12 +252,24 @@ pub fn run() {
|
||||
.title("");
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), desktop))]
|
||||
let win_builder = win_builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.shadow(true)
|
||||
.title("Readest");
|
||||
let win_builder = {
|
||||
let mut builder = win_builder
|
||||
.decorations(false)
|
||||
.visible(false)
|
||||
.shadow(true)
|
||||
.title("Readest");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
builder = builder.transparent(false);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.transparent(true);
|
||||
}
|
||||
|
||||
builder
|
||||
};
|
||||
|
||||
win_builder.build().unwrap();
|
||||
// let win = win_builder.build().unwrap();
|
||||
|
||||
@@ -365,26 +365,26 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
app_box: *mut c_void = app_box,
|
||||
toolbar: id = cocoa::base::nil,
|
||||
super_delegate: id = current_delegate,
|
||||
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
|
||||
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
|
||||
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
|
||||
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
|
||||
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
|
||||
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
|
||||
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
|
||||
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
|
||||
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
|
||||
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
|
||||
(windowShouldClose:) => on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
(windowWillClose:) => on_window_will_close as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidResize:) => on_window_did_resize::<R> as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidMove:) => on_window_did_move as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidBecomeKey:) => on_window_did_become_key as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidResignKey:) => on_window_did_resign_key as extern "C" fn(&Object, Sel, id),
|
||||
(draggingEntered:) => on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
(performDragOperation:) => on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
(concludeDragOperation:) => on_conclude_drag_operation as extern "C" fn(&Object, Sel, id),
|
||||
(draggingExited:) => on_dragging_exited as extern "C" fn(&Object, Sel, id),
|
||||
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
|
||||
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
||||
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
||||
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
|
||||
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern "C" fn(&Object, Sel, id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
|
||||
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://ik.imagekit.io",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://ik.imagekit.io",
|
||||
|
||||
@@ -334,66 +334,82 @@ export default function AuthPage() {
|
||||
return isTauriAppPlatform() ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 fixed inset-0 flex select-none flex-col items-center overflow-hidden overflow-y-auto',
|
||||
'bg-base-100 inset-0 flex select-none flex-col items-center overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
'flex h-full w-full flex-col items-center overflow-y-auto',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<button onClick={handleGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
|
||||
>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible}
|
||||
showMaximize={!isTrafficLightVisible}
|
||||
showClose={!isTrafficLightVisible}
|
||||
onClose={handleGoBack}
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible}
|
||||
showMaximize={!isTrafficLightVisible}
|
||||
showClose={!isTrafficLightVisible}
|
||||
onClose={handleGoBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'z-20 flex flex-col items-center pb-8',
|
||||
appService?.hasTrafficLight ? 'mt-24' : 'mt-12',
|
||||
)}
|
||||
style={{ maxWidth: '420px' }}
|
||||
>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FcGoogle}
|
||||
label={_('Sign in with Google')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx('z-20 pb-8', appService?.hasTrafficLight ? 'mt-24' : 'mt-12')}
|
||||
style={{ maxWidth: '420px' }}
|
||||
>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FcGoogle}
|
||||
label={_('Sign in with Google')}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='apple'
|
||||
handleSignIn={appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn}
|
||||
Icon={FaApple}
|
||||
label={_('Sign in with Apple')}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='github'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FaGithub}
|
||||
label={_('Sign in with GitHub')}
|
||||
/>
|
||||
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
magicLink={true}
|
||||
providers={[]}
|
||||
redirectTo={getTauriRedirectTo(false)}
|
||||
localization={getAuthLocalization()}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='apple'
|
||||
handleSignIn={
|
||||
appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn
|
||||
}
|
||||
Icon={FaApple}
|
||||
label={_('Sign in with Apple')}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='github'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FaGithub}
|
||||
label={_('Sign in with GitHub')}
|
||||
/>
|
||||
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<div className='w-full'>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
magicLink={true}
|
||||
providers={[]}
|
||||
redirectTo={getTauriRedirectTo(false)}
|
||||
localization={getAuthLocalization()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { parseWebViewVersion } from '@/utils/ua';
|
||||
import { handleGlobalError } from '@/utils/error';
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: Error & { digest?: string };
|
||||
@@ -19,6 +20,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
posthog.captureException(error);
|
||||
handleGlobalError(error);
|
||||
}, [appService, error]);
|
||||
|
||||
const handleGoHome = () => {
|
||||
|
||||
@@ -179,6 +179,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
};
|
||||
|
||||
const openBookDetails = () => {
|
||||
handleSetSelectMode(false);
|
||||
const book = libraryBooks.find((book) => book.hash === selectedBooks[0]);
|
||||
if (book) {
|
||||
handleShowDetailsBook(book);
|
||||
@@ -221,7 +222,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const searchTerm = new RegExp(queryTerm, 'i');
|
||||
const title = formatTitle(item.title);
|
||||
const authors = formatAuthors(item.author);
|
||||
return searchTerm.test(title) || searchTerm.test(authors);
|
||||
return (
|
||||
searchTerm.test(title) ||
|
||||
searchTerm.test(authors) ||
|
||||
searchTerm.test(item.format) ||
|
||||
(item.groupName && searchTerm.test(item.groupName)) ||
|
||||
(item.metadata?.description && searchTerm.test(item.metadata?.description))
|
||||
);
|
||||
};
|
||||
const bookSorter = (a: Book, b: Book) => {
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -8,8 +9,9 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLongPress } from '@/hooks/useLongPress';
|
||||
import { Menu, MenuItem } from '@tauri-apps/api/menu';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getLocalBookFilename } from '@/utils/book';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
@@ -138,28 +140,36 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleBookClick = async (book: Book) => {
|
||||
if (isSelectMode) {
|
||||
toggleSelection(book.hash);
|
||||
} else {
|
||||
if (!(await makeBookAvailable(book))) return;
|
||||
if (appService?.hasWindow && settings.openBookInNewWindow) {
|
||||
showReaderWindow(appService, [book.hash]);
|
||||
const handleBookClick = useCallback(
|
||||
async (book: Book) => {
|
||||
if (isSelectMode) {
|
||||
toggleSelection(book.hash);
|
||||
} else {
|
||||
navigateToReader(router, [book.hash]);
|
||||
if (!(await makeBookAvailable(book))) return;
|
||||
if (appService?.hasWindow && settings.openBookInNewWindow) {
|
||||
showReaderWindow(appService, [book.hash]);
|
||||
} else {
|
||||
navigateToReader(router, [book.hash]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isSelectMode, settings.openBookInNewWindow, appService],
|
||||
);
|
||||
|
||||
const handleGroupClick = (group: BooksGroup) => {
|
||||
if (isSelectMode) {
|
||||
toggleSelection(group.id);
|
||||
} else {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('group', group.id);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}
|
||||
};
|
||||
const handleGroupClick = useCallback(
|
||||
(group: BooksGroup) => {
|
||||
if (isSelectMode) {
|
||||
toggleSelection(group.id);
|
||||
} else {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('group', group.id);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isSelectMode, searchParams],
|
||||
);
|
||||
|
||||
const bookContextMenuHandler = async (book: Book) => {
|
||||
if (!appService?.hasContextMenu) return;
|
||||
@@ -241,8 +251,9 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
menu.popup();
|
||||
};
|
||||
|
||||
const { pressing, handlers } = useLongPress({
|
||||
onLongPress: async () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleSelectItem = useCallback(
|
||||
throttle(() => {
|
||||
if (!isSelectMode) {
|
||||
handleSetSelectMode(true);
|
||||
}
|
||||
@@ -251,22 +262,56 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
} else {
|
||||
toggleSelection((item as BooksGroup).id);
|
||||
}
|
||||
},
|
||||
onTap: () => {
|
||||
}, 100),
|
||||
[isSelectMode],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleOpenItem = useCallback(
|
||||
throttle(() => {
|
||||
if (isSelectMode) {
|
||||
handleSelectItem();
|
||||
return;
|
||||
}
|
||||
if ('format' in item) {
|
||||
handleBookClick(item as Book);
|
||||
} else {
|
||||
handleGroupClick(item as BooksGroup);
|
||||
}
|
||||
},
|
||||
onContextMenu: () => {
|
||||
}, 100),
|
||||
[handleSelectItem, handleBookClick, handleGroupClick],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleContextMenu = useCallback(
|
||||
throttle(() => {
|
||||
if ('format' in item) {
|
||||
bookContextMenuHandler(item as Book);
|
||||
} else {
|
||||
groupContextMenuHandler(item as BooksGroup);
|
||||
}
|
||||
}, 100),
|
||||
[],
|
||||
);
|
||||
|
||||
const { pressing, handlers } = useLongPress(
|
||||
{
|
||||
onLongPress: () => {
|
||||
handleSelectItem();
|
||||
},
|
||||
onTap: () => {
|
||||
handleOpenItem();
|
||||
},
|
||||
onContextMenu: () => {
|
||||
if (appService?.hasContextMenu) {
|
||||
handleContextMenu();
|
||||
} else if (appService?.isAndroidApp) {
|
||||
handleSelectItem();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
[isSelectMode, handleSelectItem, handleOpenItem, handleContextMenu],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
|
||||
@@ -275,6 +320,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
'group',
|
||||
mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
|
||||
appService?.isMobileApp && 'no-context-menu',
|
||||
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -344,12 +344,15 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
|
||||
// Reuse the library from the store when we return from the reader
|
||||
const library = libraryBooks.length > 0 ? libraryBooks : await appService.loadLibraryBooks();
|
||||
setCheckOpenWithBooks(checkOpenWithBooks && (await handleOpenWithBooks(appService, library)));
|
||||
setCheckLastOpenBooks(
|
||||
checkLastOpenBooks &&
|
||||
settings.openLastBooks &&
|
||||
(await handleOpenLastBooks(appService, settings.lastOpenBooks, library)),
|
||||
);
|
||||
let opened = false;
|
||||
if (checkOpenWithBooks) {
|
||||
opened = await handleOpenWithBooks(appService, library);
|
||||
}
|
||||
setCheckOpenWithBooks(opened);
|
||||
if (!opened && checkLastOpenBooks && settings.openLastBooks) {
|
||||
opened = await handleOpenLastBooks(appService, settings.lastOpenBooks, library);
|
||||
}
|
||||
setCheckLastOpenBooks(opened);
|
||||
|
||||
setLibrary(library);
|
||||
setLibraryLoaded(true);
|
||||
@@ -508,9 +511,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookDownload = async (book: Book) => {
|
||||
const handleBookDownload = async (book: Book, redownload = false) => {
|
||||
try {
|
||||
await appService?.downloadBook(book, false, (progress) => {
|
||||
await appService?.downloadBook(book, false, redownload, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
|
||||
@@ -158,6 +158,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
)}
|
||||
{showHeader && (
|
||||
<SectionInfo
|
||||
bookKey={bookKey}
|
||||
section={sectionLabel}
|
||||
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
|
||||
isScrolled={viewSettings.scrolled}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { useProgressSync } from '../hooks/useProgressSync';
|
||||
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
|
||||
import {
|
||||
applyFixedlayoutStyles,
|
||||
applyImageStyle,
|
||||
applyTranslationStyle,
|
||||
getStyles,
|
||||
@@ -134,6 +135,10 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
|
||||
|
||||
if (bookDoc.rendition?.layout === 'pre-paginated') {
|
||||
applyFixedlayoutStyles(detail.doc, viewSettings);
|
||||
}
|
||||
|
||||
applyImageStyle(detail.doc);
|
||||
|
||||
// Inline scripts in tauri platforms are not executed by default
|
||||
@@ -312,9 +317,13 @@ const FoliateViewer: React.FC<{
|
||||
if (viewRef.current && viewRef.current.renderer) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
|
||||
if (bookDoc.rendition?.layout === 'pre-paginated') {
|
||||
const docs = viewRef.current.renderer.getContents();
|
||||
docs.forEach(({ doc }) => applyFixedlayoutStyles(doc, viewSettings));
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [themeCode, isDarkMode]);
|
||||
}, [themeCode, isDarkMode, viewSettings?.overrideColor, viewSettings?.invertImgColorInDark]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current && viewRef.current.renderer && viewSettings) {
|
||||
|
||||
@@ -40,7 +40,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
const { appService } = useEnv();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
isTrafficLightVisible,
|
||||
trafficLightInFullscreen,
|
||||
setTrafficLightVisibility,
|
||||
initializeTrafficLightStore,
|
||||
initializeTrafficLightListeners,
|
||||
@@ -70,12 +70,19 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.hasTrafficLight) return;
|
||||
if (isSideBarVisible) return;
|
||||
|
||||
setTrafficLightVisibility(isSideBarVisible);
|
||||
if (hoveredBookKey === bookKey && isTopLeft) {
|
||||
setTrafficLightVisibility(true, { x: 10, y: 20 });
|
||||
} else if (!hoveredBookKey) {
|
||||
setTrafficLightVisibility(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService, isSideBarVisible]);
|
||||
}, [appService, isSideBarVisible, hoveredBookKey]);
|
||||
|
||||
const isVisible = hoveredBookKey === bookKey || isDropdownOpen;
|
||||
const isHeaderVisible = hoveredBookKey === bookKey || isDropdownOpen;
|
||||
const trafficLightInHeader =
|
||||
appService?.hasTrafficLight && !trafficLightInFullscreen && !isSideBarVisible && isTopLeft;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -93,7 +100,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
className={clsx(
|
||||
'bg-base-100 absolute left-0 right-0 top-0 z-10',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right',
|
||||
isVisible ? 'visible' : 'hidden',
|
||||
isHeaderVisible ? 'visible' : 'hidden',
|
||||
)}
|
||||
style={{
|
||||
height: systemUIVisible ? `${Math.max(gridInsets.top, statusBarHeight)}px` : '0px',
|
||||
@@ -104,11 +111,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
className={clsx(
|
||||
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
|
||||
`shadow-xs transition-[opacity,margin-top] duration-300`,
|
||||
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
|
||||
trafficLightInHeader ? 'pl-20' : 'pl-4',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
isVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
|
||||
isHeaderVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
|
||||
isDropdownOpen && 'header-bar-pinned',
|
||||
)}
|
||||
style={{
|
||||
@@ -148,10 +155,14 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
className='window-buttons flex h-full items-center'
|
||||
headerRef={headerRef}
|
||||
showMinimize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
bookKeys.length == 1 &&
|
||||
!appService?.hasTrafficLight &&
|
||||
appService?.appPlatform !== 'web'
|
||||
}
|
||||
showMaximize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
bookKeys.length == 1 &&
|
||||
!appService?.hasTrafficLight &&
|
||||
appService?.appPlatform !== 'web'
|
||||
}
|
||||
onClose={() => {
|
||||
setHoveredBookKey(null);
|
||||
|
||||
@@ -3,8 +3,10 @@ import React from 'react';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface SectionInfoProps {
|
||||
bookKey: string;
|
||||
section?: string;
|
||||
showDoubleBorder: boolean;
|
||||
isScrolled: boolean;
|
||||
@@ -15,6 +17,7 @@ interface SectionInfoProps {
|
||||
}
|
||||
|
||||
const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
bookKey,
|
||||
section,
|
||||
showDoubleBorder,
|
||||
isScrolled,
|
||||
@@ -24,6 +27,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
gridInsets,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
const topInset = Math.max(
|
||||
gridInsets.top,
|
||||
@@ -68,6 +72,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
className={clsx(
|
||||
'text-neutral-content text-center font-sans text-xs font-light',
|
||||
isVertical ? '' : 'line-clamp-1',
|
||||
!isVertical && hoveredBookKey == bookKey && 'hidden',
|
||||
)}
|
||||
>
|
||||
{section || ''}
|
||||
|
||||
@@ -216,7 +216,7 @@ const Notebook: React.FC = ({}) => {
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3'
|
||||
className='drag-bar absolute left-0 top-0 -m-2 h-full w-0.5 cursor-col-resize bg-transparent p-2'
|
||||
onMouseDown={handleDragStart}
|
||||
onTouchStart={handleDragStart}
|
||||
/>
|
||||
|
||||
@@ -259,7 +259,7 @@ const SideBar: React.FC<{
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'drag-bar absolute -right-1 top-0 -m-1 h-full w-0.5 cursor-col-resize p-1',
|
||||
'drag-bar absolute right-0 top-0 -m-2 h-full w-0.5 cursor-col-resize bg-transparent p-2',
|
||||
isMobile && 'hidden',
|
||||
)}
|
||||
onMouseDown={handleHorizontalDragStart}
|
||||
|
||||
@@ -117,7 +117,7 @@ const TOCView: React.FC<{
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [expandedItems]);
|
||||
|
||||
const activeHref = useMemo(() => progress?.sectionHref || null, [progress?.sectionHref]);
|
||||
const flatItems = useFlattenedTOC(toc, expandedItems);
|
||||
@@ -213,7 +213,11 @@ const TOCView: React.FC<{
|
||||
}, [flatItems, scrollToActiveItem]);
|
||||
|
||||
return flatItems.length > 256 ? (
|
||||
<div className='rounded' data-overlayscrollbars-initialize='' ref={containerRef}>
|
||||
<div
|
||||
className='virtual-list rounded pt-2'
|
||||
data-overlayscrollbars-initialize=''
|
||||
ref={containerRef}
|
||||
>
|
||||
<VirtualList
|
||||
ref={vitualListRef}
|
||||
outerRef={listOuterRef}
|
||||
@@ -233,7 +237,7 @@ const TOCView: React.FC<{
|
||||
</VirtualList>
|
||||
</div>
|
||||
) : (
|
||||
<div className='rounded pt-2' ref={staticListRef}>
|
||||
<div className='static-list rounded pt-2' ref={staticListRef}>
|
||||
{flatItems.map((flatItem, index) => (
|
||||
<StaticListRow
|
||||
key={`static-row-${index}`}
|
||||
|
||||
@@ -330,7 +330,7 @@ const TTSPanel = ({
|
||||
</span>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-base sm:text-sm',
|
||||
'max-w-[180px] overflow-hidden text-ellipsis text-base sm:text-sm',
|
||||
voice.disabled && 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,10 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ onGoBack }) => {
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<button onClick={onGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
|
||||
<button
|
||||
onClick={onGoBack}
|
||||
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
|
||||
>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
|
||||
|
||||
@@ -44,13 +44,16 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
const touchStart = e.touches[0]!.clientX;
|
||||
const touchStartX = e.touches[0]!.clientX;
|
||||
const touchStartY = e.touches[0]!.clientY;
|
||||
const handleTouchMove = (moveEvent: TouchEvent) => {
|
||||
const touchEnd = moveEvent.touches[0]!.clientX;
|
||||
const diff = touchStart - touchEnd;
|
||||
const touchEndX = moveEvent.touches[0]!.clientX;
|
||||
const touchEndY = moveEvent.touches[0]!.clientY;
|
||||
const diffX = touchStartX - touchEndX;
|
||||
const diffY = touchStartY - touchEndY;
|
||||
|
||||
if (Math.abs(diff) > 50) {
|
||||
if (diff > 0) {
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
||||
if (diffX > 0) {
|
||||
handlePlanSwipe('left');
|
||||
} else {
|
||||
handlePlanSwipe('right');
|
||||
|
||||
@@ -191,7 +191,7 @@ const ProfilePage = () => {
|
||||
useEffect(() => {
|
||||
fetch(WEB_STRIPE_PLANS_URL)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setAvailablePlans(data));
|
||||
.then((data) => setAvailablePlans(data || []));
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
@@ -219,14 +219,18 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 fixed inset-0 select-none overflow-hidden',
|
||||
'bg-base-100 inset-0 select-none overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<div className='flex h-full w-full flex-col items-center overflow-y-auto'>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-full w-full flex-col items-center overflow-y-auto',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<ProfileHeader onGoBack={handleGoBack} />
|
||||
<div className='w-full min-w-60 max-w-4xl py-10'>
|
||||
{loading && (
|
||||
|
||||
@@ -48,10 +48,7 @@ const BookCover: React.FC<BookCoverProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={coverRef}
|
||||
className={clsx(
|
||||
'book-cover-container no-context-menu relative flex h-full w-full',
|
||||
className,
|
||||
)}
|
||||
className={clsx('book-cover-container relative flex h-full w-full', className)}
|
||||
>
|
||||
{coverFit === 'crop' ? (
|
||||
<Image
|
||||
|
||||
@@ -114,7 +114,7 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
field: 'published',
|
||||
label: _('Publication Date'),
|
||||
value: metadata.published || '',
|
||||
placeholder: 'YYYY or YYYY-MM-DD',
|
||||
placeholder: _('YYYY or YYYY-MM-DD'),
|
||||
},
|
||||
{
|
||||
field: 'language',
|
||||
@@ -178,9 +178,12 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
let files;
|
||||
if (isTauriAppPlatform()) {
|
||||
files = (await selectImageFileTauri()) as string[];
|
||||
if (files.length > 0) {
|
||||
if (appService && files.length > 0) {
|
||||
metadata.coverImageFile = files[0]!;
|
||||
metadata.coverImageUrl = appService?.fs.getURL(files[0]!);
|
||||
const tempName = 'cover-temp.png';
|
||||
const cachePrefix = appService.fs.getPrefix('Cache');
|
||||
await appService.fs.copyFile(files[0]!, tempName, 'Cache');
|
||||
metadata.coverImageUrl = await appService.fs.getURL(`${cachePrefix}/${tempName}`);
|
||||
setNewCoverImageUrl(metadata.coverImageUrl!);
|
||||
}
|
||||
} else {
|
||||
@@ -196,7 +199,10 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
<div className='bg-base-100 relative w-full rounded-lg'>
|
||||
<div className='mb-6 flex items-start gap-4'>
|
||||
<div className='flex w-[30%] max-w-32 flex-col gap-2'>
|
||||
<div className='aspect-[28/41] h-full shadow-md'>
|
||||
<div
|
||||
className='aspect-[28/41] h-full shadow-md'
|
||||
onClick={!isCoverLocked ? handleSelectLocalImage : undefined}
|
||||
>
|
||||
<BookCover
|
||||
mode='list'
|
||||
book={{
|
||||
@@ -222,7 +228,12 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
)}
|
||||
title={_('Change cover image')}
|
||||
>
|
||||
<MdEdit className='h-5 w-5 sm:h-4 sm:w-4' />
|
||||
<MdEdit
|
||||
className={clsx(
|
||||
'h-5 w-5 sm:h-4 sm:w-4',
|
||||
isCoverLocked ? 'fill-base-content' : 'fill-gray-600',
|
||||
)}
|
||||
/>
|
||||
<span className='hidden sm:inline'>{_('Replace')}</span>
|
||||
</button>
|
||||
|
||||
@@ -364,7 +375,8 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={onSave}
|
||||
className='rounded-md bg-green-500 px-4 py-2 text-white hover:bg-green-600'
|
||||
disabled={fieldErrors && Object.keys(fieldErrors).length > 0}
|
||||
className='rounded-md bg-green-500 px-4 py-2 text-white hover:bg-green-600 disabled:opacity-50'
|
||||
>
|
||||
{_('Save')}
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ interface BookDetailModalProps {
|
||||
book: Book;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleBookDownload?: (book: Book) => void;
|
||||
handleBookDownload?: (book: Book, redownload?: boolean) => void;
|
||||
handleBookUpload?: (book: Book) => void;
|
||||
handleBookDelete?: (book: Book) => void;
|
||||
handleBookDeleteCloudBackup?: (book: Book) => void;
|
||||
@@ -132,7 +132,7 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
const handleRedownload = async () => {
|
||||
handleClose();
|
||||
if (handleBookDownload) {
|
||||
handleBookDownload(book);
|
||||
handleBookDownload(book, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Published')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{formatDate(metadata.published) || _('Unknown')}
|
||||
{formatDate(metadata.published, true) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
|
||||
@@ -14,8 +14,7 @@ interface CliArgument {
|
||||
|
||||
const parseWindowOpenWithFiles = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const filesParams = params.get('files') || '';
|
||||
const files = filesParams ? filesParams.split(',').map(decodeURIComponent) : [];
|
||||
const files = params.getAll('file');
|
||||
return files.length > 0 ? files : window.OPEN_WITH_FILES;
|
||||
};
|
||||
|
||||
|
||||
@@ -21,14 +21,17 @@ interface UseLongPressResult {
|
||||
};
|
||||
}
|
||||
|
||||
export const useLongPress = ({
|
||||
onTap,
|
||||
onLongPress,
|
||||
onContextMenu,
|
||||
onCancel,
|
||||
threshold = 500,
|
||||
moveThreshold = 10,
|
||||
}: UseLongPressOptions): UseLongPressResult => {
|
||||
export const useLongPress = (
|
||||
{
|
||||
onTap,
|
||||
onLongPress,
|
||||
onContextMenu,
|
||||
onCancel,
|
||||
threshold = 500,
|
||||
moveThreshold = 10,
|
||||
}: UseLongPressOptions,
|
||||
deps: React.DependencyList,
|
||||
): UseLongPressResult => {
|
||||
const [pressing, setPressing] = useState(false);
|
||||
const timerRef = useRef<NodeJS.Timeout>();
|
||||
const startPosRef = useRef<{ x: number; y: number } | null>(null);
|
||||
@@ -114,15 +117,17 @@ export const useLongPress = ({
|
||||
e.stopPropagation();
|
||||
onContextMenu();
|
||||
}
|
||||
reset();
|
||||
},
|
||||
[onContextMenu],
|
||||
[onContextMenu, reset],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
|
||||
return {
|
||||
pressing,
|
||||
|
||||
@@ -83,6 +83,10 @@ export type BookMetadata = {
|
||||
|
||||
export interface BookDoc {
|
||||
metadata: BookMetadata;
|
||||
rendition?: {
|
||||
layout?: 'pre-paginated' | 'reflowable';
|
||||
viewport?: { width: number; height: number };
|
||||
};
|
||||
dir: string;
|
||||
toc?: Array<TOCItem>;
|
||||
sections?: Array<SectionItem>;
|
||||
|
||||
@@ -45,5 +45,5 @@ export function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/api/stripe/:path*', '/api/metadata/:path*'],
|
||||
matcher: ['/api/:path*', '/api/stripe/:path*', '/api/metadata/:path*'],
|
||||
};
|
||||
|
||||
@@ -381,13 +381,19 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadBook(book: Book, onlyCover = false, onProgress?: ProgressHandler): Promise<void> {
|
||||
async downloadBook(
|
||||
book: Book,
|
||||
onlyCover = false,
|
||||
redownload = false,
|
||||
onProgress?: ProgressHandler,
|
||||
): Promise<void> {
|
||||
let bookDownloaded = false;
|
||||
let bookCoverDownloaded = false;
|
||||
const completedFiles = { count: 0 };
|
||||
let toDownloadFpCount = 0;
|
||||
const needDownCover = !(await this.fs.exists(getCoverFilename(book), 'Books'));
|
||||
const needDownBook = !onlyCover && !(await this.fs.exists(getLocalBookFilename(book), 'Books'));
|
||||
const needDownCover = !(await this.fs.exists(getCoverFilename(book), 'Books')) || redownload;
|
||||
const needDownBook =
|
||||
(!onlyCover && !(await this.fs.exists(getLocalBookFilename(book), 'Books'))) || redownload;
|
||||
if (needDownCover) {
|
||||
toDownloadFpCount++;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TranslationProvider } from '../types';
|
||||
import { deeplProvider } from './deepl';
|
||||
import { azureProvider } from './azure';
|
||||
import { googleProvider } from './google';
|
||||
import { yandexProvider } from './yandex';
|
||||
|
||||
function createTranslator<T extends string>(
|
||||
name: T,
|
||||
@@ -18,11 +19,13 @@ function createTranslator<T extends string>(
|
||||
const deeplTranslator = createTranslator('deepl', deeplProvider);
|
||||
const azureTranslator = createTranslator('azure', azureProvider);
|
||||
const googleTranslator = createTranslator('google', googleProvider);
|
||||
const yandexTranslator = createTranslator('yandex', yandexProvider);
|
||||
|
||||
const availableTranslators = [
|
||||
deeplTranslator,
|
||||
azureTranslator,
|
||||
googleTranslator,
|
||||
yandexTranslator,
|
||||
// Add more translators here
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { normalizeToShortLang } from '@/utils/lang';
|
||||
import { TranslationProvider } from '../types';
|
||||
|
||||
/**
|
||||
* Based on https://translate.toil.cc/v2/docs API specification
|
||||
*/
|
||||
async function translateSingleTextForService(
|
||||
text: string,
|
||||
lang: string,
|
||||
service: string,
|
||||
): Promise<string[]> {
|
||||
const fetchImpl = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
const url = 'https://translate.toil.cc/v2/translate/';
|
||||
|
||||
const request = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lang: lang,
|
||||
service: service,
|
||||
text: text,
|
||||
})
|
||||
};
|
||||
|
||||
const response = await fetchImpl(url, request);
|
||||
|
||||
if (!response.ok) {
|
||||
const response_json = JSON.stringify(await response.json());
|
||||
throw new Error(`${service} failed with status ${response.status}\n${text.length}\n${JSON.stringify(request)}\n${response_json}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (
|
||||
data &&
|
||||
Array.isArray(data.translations)
|
||||
) {
|
||||
return data.translations;
|
||||
} else {
|
||||
// fallback: return original texts if translation failed
|
||||
return [text];
|
||||
}
|
||||
};
|
||||
|
||||
export const yandexProvider: TranslationProvider = {
|
||||
name: 'yandex',
|
||||
label: _('Yandex Translate'),
|
||||
authRequired: false,
|
||||
translate: async (texts: string[], sourceLang: string, targetLang: string): Promise<string[]> => {
|
||||
if (!texts.length) return [];
|
||||
|
||||
/**
|
||||
Possible options:
|
||||
- yandexcloud: often returns 500: {"error":"The text couldn't be translated, because Forbidden"}
|
||||
- yandexgpt: often better than others
|
||||
- yandextranslate
|
||||
- yandexbrowser
|
||||
*/
|
||||
const service = "yandexgpt";
|
||||
|
||||
// Yandex does not accept "auto" language
|
||||
const source_lang = sourceLang == "AUTO" ? "en" : normalizeToShortLang(sourceLang).toLowerCase();
|
||||
const target_lang = normalizeToShortLang(targetLang).toLowerCase();
|
||||
const lang = `${source_lang}-${target_lang}`;
|
||||
|
||||
const responses = await Promise.all(texts.map(async text => {
|
||||
return await translateSingleTextForService(text, lang, service)
|
||||
}));
|
||||
|
||||
const translatedTexts = responses.flat();
|
||||
return translatedTexts;
|
||||
}
|
||||
};
|
||||
@@ -9,8 +9,9 @@ interface TrafficLightState {
|
||||
appService?: AppService;
|
||||
isTrafficLightVisible: boolean;
|
||||
shouldShowTrafficLight: boolean;
|
||||
trafficLightInFullscreen: boolean;
|
||||
initializeTrafficLightStore: (appService: AppService) => void;
|
||||
setTrafficLightVisibility: (visible: boolean) => void;
|
||||
setTrafficLightVisibility: (visible: boolean, position?: { x: number; y: number }) => void;
|
||||
initializeTrafficLightListeners: () => Promise<void>;
|
||||
cleanupTrafficLightListeners: () => void;
|
||||
unlistenEnterFullScreen?: () => void;
|
||||
@@ -22,6 +23,7 @@ export const useTrafficLightStore = create<TrafficLightState>((set, get) => {
|
||||
appService: undefined,
|
||||
isTrafficLightVisible: false,
|
||||
shouldShowTrafficLight: false,
|
||||
trafficLightInFullscreen: false,
|
||||
|
||||
initializeTrafficLightStore: (appService: AppService) => {
|
||||
set({
|
||||
@@ -31,15 +33,19 @@ export const useTrafficLightStore = create<TrafficLightState>((set, get) => {
|
||||
});
|
||||
},
|
||||
|
||||
setTrafficLightVisibility: async (visible: boolean) => {
|
||||
setTrafficLightVisibility: async (visible: boolean, position?: { x: number; y: number }) => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
const isFullscreen = await currentWindow.isFullscreen();
|
||||
set({ isTrafficLightVisible: !isFullscreen && visible, shouldShowTrafficLight: visible });
|
||||
set({
|
||||
isTrafficLightVisible: !isFullscreen && visible,
|
||||
shouldShowTrafficLight: visible,
|
||||
trafficLightInFullscreen: isFullscreen,
|
||||
});
|
||||
invoke('set_traffic_lights', {
|
||||
visible: visible,
|
||||
x: WINDOW_CONTROL_PAD_X,
|
||||
y: WINDOW_CONTROL_PAD_Y,
|
||||
x: position?.x ?? WINDOW_CONTROL_PAD_X,
|
||||
y: position?.y ?? WINDOW_CONTROL_PAD_Y,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -48,12 +54,12 @@ export const useTrafficLightStore = create<TrafficLightState>((set, get) => {
|
||||
const currentWindow = getCurrentWindow();
|
||||
|
||||
const unlistenEnterFullScreen = await currentWindow.listen('will-enter-fullscreen', () => {
|
||||
set({ isTrafficLightVisible: false });
|
||||
set({ isTrafficLightVisible: false, trafficLightInFullscreen: true });
|
||||
});
|
||||
|
||||
const unlistenExitFullScreen = await currentWindow.listen('will-exit-fullscreen', () => {
|
||||
const { shouldShowTrafficLight } = get();
|
||||
set({ isTrafficLightVisible: shouldShowTrafficLight });
|
||||
set({ isTrafficLightVisible: shouldShowTrafficLight, trafficLightInFullscreen: false });
|
||||
});
|
||||
|
||||
set({ unlistenEnterFullScreen, unlistenExitFullScreen });
|
||||
|
||||
@@ -59,7 +59,12 @@ export interface AppService {
|
||||
): Promise<Book | null>;
|
||||
deleteBook(book: Book, includingUploaded?: boolean, includingLocal?: boolean): Promise<void>;
|
||||
uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void>;
|
||||
downloadBook(book: Book, onlyCover?: boolean, onProgress?: ProgressHandler): Promise<void>;
|
||||
downloadBook(
|
||||
book: Book,
|
||||
onlyCover?: boolean,
|
||||
redownload?: boolean,
|
||||
onProgress?: ProgressHandler,
|
||||
): Promise<void>;
|
||||
isBookAvailable(book: Book): Promise<boolean>;
|
||||
getBookFileSize(book: Book): Promise<number | null>;
|
||||
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EXTS } from '@/libs/document';
|
||||
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
|
||||
import { getUserLang, isContentURI, isFileURI, isValidURL, makeSafeFilename } from './misc';
|
||||
import { getStorageType } from './object';
|
||||
import { getStorageType } from './storage';
|
||||
import { getDirFromLanguage } from './rtl';
|
||||
import { SUPPORTED_LANGS } from '@/services/constants';
|
||||
|
||||
@@ -123,7 +123,7 @@ export const getPrimaryLanguage = (lang: string | string[] | undefined) => {
|
||||
return Array.isArray(lang) ? lang[0] : lang;
|
||||
};
|
||||
|
||||
export const formatDate = (date: string | number | Date | null | undefined) => {
|
||||
export const formatDate = (date: string | number | Date | null | undefined, isUTC = false) => {
|
||||
if (!date) return;
|
||||
const userLang = getUserLang();
|
||||
try {
|
||||
@@ -131,6 +131,7 @@ export const formatDate = (date: string | number | Date | null | undefined) => {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: isUTC ? 'UTC' : undefined,
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const handleGlobalError = (e: Error) => {
|
||||
const isChunkError = e?.message?.includes('Loading chunk');
|
||||
|
||||
if (!isChunkError) {
|
||||
const now = Date.now();
|
||||
const lastReload = Number(sessionStorage.getItem('lastErrorReload') || '0');
|
||||
if (now - lastReload > 60_000) {
|
||||
sessionStorage.setItem('lastErrorReload', String(now));
|
||||
window.location.reload();
|
||||
} else {
|
||||
console.warn('Error detected, but reload suppressed (rate limit)');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -39,8 +39,9 @@ export const showReaderWindow = (appService: AppService, bookIds: string[]) => {
|
||||
};
|
||||
|
||||
export const showLibraryWindow = (appService: AppService, filenames: string[]) => {
|
||||
const files = filenames.map((file) => `${encodeURIComponent(file)}`).join(',');
|
||||
const url = `/library?files=${files}`;
|
||||
const params = new URLSearchParams();
|
||||
filenames.forEach((filename) => params.append('file', filename));
|
||||
const url = `/library?${params.toString()}`;
|
||||
createReaderWindow(appService, url);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
import { s3Storage } from './s3';
|
||||
import { r2Storage } from './r2';
|
||||
import { getStorageType } from './storage';
|
||||
|
||||
type ObjectStorageType = 'r2' | 's3';
|
||||
|
||||
export const getStorageType = (): ObjectStorageType => {
|
||||
// TODO: do not expose storage type to client
|
||||
if (process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE']) {
|
||||
return process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE'] as ObjectStorageType;
|
||||
} else {
|
||||
return 'r2';
|
||||
}
|
||||
};
|
||||
|
||||
export const getDownloadSignedUrl = async (
|
||||
fileKey: string,
|
||||
expiresIn: number,
|
||||
) => {
|
||||
export const getDownloadSignedUrl = async (fileKey: string, expiresIn: number) => {
|
||||
const storageType = getStorageType();
|
||||
if (storageType === 'r2') {
|
||||
const bucketName = process.env['R2_BUCKET_NAME'] || '';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TTSMark } from '@/services/tts/types';
|
||||
import { inferLangFromScript, isSameLang, isValidLang } from './lang';
|
||||
import { code6392to6391, inferLangFromScript, isSameLang, isValidLang } from './lang';
|
||||
|
||||
const cleanTextContent = (text: string) =>
|
||||
text.replace(/\r\n/g, ' ').replace(/\r/g, ' ').replace(/\n/g, ' ').trimStart();
|
||||
@@ -27,10 +27,12 @@ export const parseSSMLLang = (ssml: string, primaryLang?: string): string => {
|
||||
? `${parts[0]!.toLowerCase()}-${parts[1]!.toUpperCase()}`
|
||||
: parts[0]!.toLowerCase();
|
||||
|
||||
lang = code6392to6391(lang) || lang;
|
||||
if (!isValidLang(lang)) {
|
||||
lang = 'en';
|
||||
}
|
||||
}
|
||||
primaryLang = code6392to6391(primaryLang?.toLowerCase() || '') || primaryLang;
|
||||
if (lang === 'en' && primaryLang && !isSameLang(lang, primaryLang)) {
|
||||
lang = primaryLang.split('-')[0]!.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
type ObjectStorageType = 'r2' | 's3';
|
||||
|
||||
export const getStorageType = (): ObjectStorageType => {
|
||||
// TODO: do not expose storage type to client
|
||||
if (process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE']) {
|
||||
return process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE'] as ObjectStorageType;
|
||||
} else {
|
||||
return 'r2';
|
||||
}
|
||||
};
|
||||
@@ -110,6 +110,13 @@ const getColorStyles = (
|
||||
html, body {
|
||||
color: ${fg};
|
||||
}
|
||||
html[has-background], body[has-background] {
|
||||
--background-set: var(--theme-bg-color);
|
||||
}
|
||||
html {
|
||||
background-color: var(--theme-bg-color, transparent);
|
||||
background: var(--background-set, none);
|
||||
}
|
||||
div, p, h1, h2, h3, h4, h5, h6 {
|
||||
${overrideColor ? `background-color: ${bg} !important;` : ''}
|
||||
${overrideColor ? `color: ${fg} !important;` : ''}
|
||||
@@ -129,6 +136,7 @@ const getColorStyles = (
|
||||
}
|
||||
img {
|
||||
${isDarkMode && invertImgColorInDark ? 'filter: invert(100%);' : ''}
|
||||
${!isDarkMode && overrideColor ? 'mix-blend-mode: multiply;' : ''}
|
||||
}
|
||||
/* inline images */
|
||||
p img, span img, sup img {
|
||||
@@ -190,18 +198,11 @@ const getLayoutStyles = (
|
||||
white-space: pre-wrap !important;
|
||||
tab-size: 2;
|
||||
}
|
||||
html[has-background], body[has-background] {
|
||||
--background-set: var(--theme-bg-color);
|
||||
}
|
||||
html, body {
|
||||
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode} !important;`}
|
||||
text-align: var(--default-text-align);
|
||||
max-height: unset;
|
||||
}
|
||||
html {
|
||||
background-color: var(--theme-bg-color, transparent);
|
||||
background: var(--background-set, none);
|
||||
}
|
||||
body {
|
||||
overflow: unset;
|
||||
zoom: ${zoomLevel};
|
||||
@@ -537,3 +538,44 @@ export const applyImageStyle = (document: Document) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const applyFixedlayoutStyles = (
|
||||
document: Document,
|
||||
viewSettings: ViewSettings,
|
||||
themeCode?: ThemeCode,
|
||||
) => {
|
||||
if (!themeCode) {
|
||||
themeCode = getThemeCode();
|
||||
}
|
||||
const { bg, fg, primary, isDarkMode } = themeCode;
|
||||
const overrideColor = viewSettings.overrideColor!;
|
||||
const invertImgColorInDark = viewSettings.invertImgColorInDark!;
|
||||
|
||||
const existingStyleId = 'fixed-layout-styles';
|
||||
let style = document.getElementById(existingStyleId) as HTMLStyleElement;
|
||||
if (style) {
|
||||
style.remove();
|
||||
}
|
||||
style = document.createElement('style');
|
||||
style.id = existingStyleId;
|
||||
style.textContent = `
|
||||
html {
|
||||
--theme-bg-color: ${bg};
|
||||
--theme-fg-color: ${fg};
|
||||
--theme-primary-color: ${primary};
|
||||
color-scheme: ${isDarkMode ? 'dark' : 'light'};
|
||||
}
|
||||
body {
|
||||
position: relative;
|
||||
background-color: var(--theme-bg-color);
|
||||
}
|
||||
img, canvas {
|
||||
${isDarkMode && invertImgColorInDark ? 'filter: invert(100%);' : ''}
|
||||
${!isDarkMode && overrideColor ? 'mix-blend-mode: multiply;' : ''}
|
||||
}
|
||||
img.singlePage {
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
+1
-1
Submodule packages/tauri updated: 8e592e7ad5...b45b75a13f
Submodule
+1
Submodule packages/tauri-plugins added at f1202ecfff
Generated
+393
-277
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user