Compare commits

...

17 Commits

Author SHA1 Message Date
Huang Xin b2ef55832c hotfix: fix window cannot be dragged when light traffic widget is shown on macOS (#304)
* fix: fix window cannot be dragged when light traffic widget is shown on macOS

* fix: detecting tauri focus event for deeplink redirect in oauth

* release: version 0.9.9
2025-02-06 20:54:39 +01:00
Huang Xin b879184dc6 ux: incrementally loading new books after synchronization (#303)
* auth: workaround to get deeplink data when completing oauth on Android

* ux: incrementally loading new books after synchronization
2025-02-06 19:07:48 +01:00
Huang Xin 8bd9e18381 auth: add applinks for auth on android and ios (#302) 2025-02-06 16:16:15 +01:00
Huang Xin 4121195fd3 fix: remove symbolic fonts in default sys fonts list (#301)
* chore: fix script for appstore release

* fix: remove symbolic fonts in default sys fonts list
2025-02-06 11:32:56 +01:00
Huang Xin cebe8812d3 i18n: translate Sans-serif and Serif font labels (#300) 2025-02-05 16:57:53 +01:00
Huang Xin 0a13f36e33 sync: add toast error for failed uploading when the cloud quota is insufficient (#299) 2025-02-05 15:48:59 +01:00
Huang Xin ded6053134 fix: UA detection of Safari with AppleWebKit (#298)
* fix: UA detection of Safari with AppleWebKit

* readme: add hello-github badge
2025-02-05 11:24:16 +01:00
Huang Xin 1c6cc2bca0 tts: change voice language at speak time when the book language is not accurate (#296) 2025-02-04 23:44:59 +01:00
Huang Xin 6ca96be917 fix: swap paging keys when writing direction is set in config (#295) 2025-02-04 23:03:20 +01:00
Huang Xin a057af7881 fix: avoid over-highlighting on some blank space when selecting multiple paragraphs, closes #238 (#294) 2025-02-04 22:09:13 +01:00
Huang Xin 279504395d fix: handle zoomed text rect on Safari when highlighting, closes #67 (#293) 2025-02-04 19:59:21 +01:00
Huang Xin cfd5d5d106 mobile: several fixes for Android build (#292) 2025-02-04 00:07:55 +01:00
Huang Xin 93eacf91a0 feat: load system fonts list at runtime on desktop, closes #287 #237 (#290) 2025-02-03 00:41:30 +01:00
Huang Xin dc6cbd2029 chore: bump update of next.js (#289) 2025-02-02 22:30:38 +01:00
Huang Xin 643fdd24a9 chore: bump update of tauri (#288) 2025-02-02 22:19:49 +01:00
Huang Xin 5c55233c5c fix: occasionally the scroll end does not reach to the last subpixel (#284) 2025-02-01 17:54:11 +01:00
Huang Xin 4d7db3592f release: version 0.9.8 2025-02-01 16:53:04 +01:00
77 changed files with 815 additions and 404 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev
sudo apt-get install -y pkg-config libfontconfig-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev
- name: Format
working-directory: apps/readest-app/src-tauri
run: cargo fmt --check
+1 -1
View File
@@ -121,7 +121,7 @@ jobs:
if: matrix.config.os == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- uses: tauri-apps/tauri-action@v0
env:
Generated
+288 -149
View File
File diff suppressed because it is too large Load Diff
+18 -15
View File
@@ -10,6 +10,7 @@
[![Website][badge-website]][link-website]
[![Web App][badge-web-app]][link-web-readest]
[![OS][badge-platforms]][link-website]
[![][badge-hellogithub]][link-hellogithub]
[![][badge-discord]][link-discord]
<br>
[![AGPL Licence][badge-license]](LICENSE)
@@ -39,20 +40,20 @@
<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. | ✅ |
| **Sync across Platforms** | Synchronize reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Text-to-Speech (TTS) Support** | Enable text-to-speech functionality for a more accessible reading experience. | ✅ |
| **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. | ✅ |
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Text-to-Speech (TTS) Support** | Enable text-to-speech functionality for a more accessible reading experience. | ✅ |
## Planned Features
@@ -193,7 +194,7 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
Readest is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file for details.
The following libraries and software are used in this software:
The following libraries and frameworks are used in this software:
- [foliate-js](https://github.com/johnfactotum/foliate-js), which is MIT licensed.
- [zip.js](https://github.com/gildas-lormeau/zip.js), which is licensed under the BSD-3-Clause license.
@@ -220,6 +221,7 @@ The following fonts are utilized in this software, either bundled within the app
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=green
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
[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
@@ -230,3 +232,4 @@ The following fonts are utilized in this software, either bundled within the app
[link-discord]: https://discord.gg/gntyVNk3BJ
[link-parallel-read]: https://readest.com/#parallel-read
[link-koreader]: https://github.com/koreader/koreader
[link-hellogithub]: https://hellogithub.com/repository/8a5b6ade2aee461a8bd94e59200682a7
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.7",
"version": "0.9.9",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -36,18 +36,18 @@
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.47.7",
"@tauri-apps/api": "2.1.1",
"@tauri-apps/api": "2.2.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-http": "^2.2.0",
"@tauri-apps/plugin-log": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.2",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.5",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "~2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-updater": "^2.4.0",
"@zip.js/zip.js": "^2.7.53",
"clsx": "^2.1.1",
"cors": "^2.8.5",
@@ -58,7 +58,7 @@
"i18next-http-backend": "^3.0.1",
"js-md5": "^0.8.3",
"jwt-decode": "^4.0.0",
"next": "15.1.0",
"next": "15.1.6",
"posthog-js": "^1.205.0",
"react": "19.0.0",
"react-dom": "19.0.0",
@@ -69,7 +69,7 @@
"zustand": "5.0.1"
},
"devDependencies": {
"@tauri-apps/cli": "2.1.0",
"@tauri-apps/cli": "2.2.7",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -0,0 +1,17 @@
{
"applinks": {
"details": [
{
"appIDs": [
"J5W48D69VR.com.bilingify.readest"
],
"components": [
{
"/": "/auth/*",
"comment": "Matches any URL whose path starts with /auth/"
}
]
}
]
}
}
@@ -0,0 +1,12 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.bilingify.readest",
"sha256_cert_fingerprints": [
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82"
]
}
}
]
@@ -149,5 +149,6 @@
"Storage": "Speicher",
"Book deleted: {{title}}": "Buch gelöscht: {{title}}",
"Failed to delete book: {{title}}": "Fehler beim Löschen des Buches: {{title}}",
"Check Updates on Start": "Aktualisierungen beim Start prüfen"
"Check Updates on Start": "Aktualisierungen beim Start prüfen",
"Insufficient storage quota": "Unzureichendes Speicherkontingent"
}
@@ -149,5 +149,6 @@
"Storage": "Αποθήκευση",
"Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε",
"Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}",
"Check Updates on Start": "Έλεγχος ενημερώσεων κατά την εκκίνηση"
"Check Updates on Start": "Έλεγχος ενημερώσεων κατά την εκκίνηση",
"Insufficient storage quota": "Ανεπαρκές όριο αποθήκευσης"
}
@@ -149,5 +149,6 @@
"Storage": "Almacenamiento",
"Book deleted: {{title}}": "Libro eliminado: {{title}}",
"Failed to delete book: {{title}}": "Error al eliminar libro: {{title}}",
"Check Updates on Start": "Comprobar actualizaciones al iniciar"
"Check Updates on Start": "Comprobar actualizaciones al iniciar",
"Insufficient storage quota": "Cuota de almacenamiento insuficiente"
}
@@ -149,5 +149,6 @@
"Storage": "Stockage",
"Book deleted: {{title}}": "Livre supprimé : {{title}}",
"Failed to delete book: {{title}}": "Échec de la suppression du livre : {{title}}",
"Check Updates on Start": "Vérifier les mises à jour au démarrage"
"Check Updates on Start": "Vérifier les mises à jour au démarrage",
"Insufficient storage quota": "Quota de stockage insuffisant"
}
@@ -149,5 +149,6 @@
"Storage": "स्टोरेज",
"Book deleted: {{title}}": "पुस्तक हटाई गई: {{title}}",
"Failed to delete book: {{title}}": "पुस्तक हटाने में विफल: {{title}}",
"Check Updates on Start": "शुरू में अपडेट जांचें"
"Check Updates on Start": "शुरू में अपडेट जांचें",
"Insufficient storage quota": "अपर्याप्त स्टोरेज कोटा"
}
@@ -149,5 +149,6 @@
"Storage": "Penyimpanan",
"Book deleted: {{title}}": "Buku dihapus: {{title}}",
"Failed to delete book: {{title}}": "Gagal menghapus buku: {{title}}",
"Check Updates on Start": "Periksa Pembaruan saat Memulai"
"Check Updates on Start": "Periksa Pembaruan saat Memulai",
"Insufficient storage quota": "Kuota penyimpanan tidak mencukupi"
}
@@ -149,5 +149,6 @@
"Storage": "Archiviazione",
"Book deleted: {{title}}": "Libro eliminato: {{title}}",
"Failed to delete book: {{title}}": "Eliminazione libro non riuscita: {{title}}",
"Check Updates on Start": "Controlla aggiornamenti all'avvio"
"Check Updates on Start": "Controlla aggiornamenti all'avvio",
"Insufficient storage quota": "Quota di archiviazione insufficiente"
}
@@ -99,7 +99,7 @@
"Select Books": "書籍を選択",
"Select Multiple Books": "複数の書籍を選択",
"Sepia": "セピア",
"Serif Font": "明朝体",
"Serif Font": "セリフ体",
"Show Book Details": "書籍の詳細を表示",
"Sidebar": "サイドバー",
"Sign In": "サインイン",
@@ -149,5 +149,6 @@
"Storage": "ストレージ",
"Book deleted: {{title}}": "書籍が削除されました:{{title}}",
"Failed to delete book: {{title}}": "書籍の削除に失敗しました:{{title}}",
"Check Updates on Start": "開始時に更新を確認"
"Check Updates on Start": "開始時に更新を確認",
"Insufficient storage quota": "ストレージクォータが不足しています"
}
@@ -149,5 +149,6 @@
"Storage": "저장소",
"Book deleted: {{title}}": "책 삭제됨: {{title}}",
"Failed to delete book: {{title}}": "책 삭제 실패: {{title}}",
"Check Updates on Start": "시작 시 업데이트 확인"
"Check Updates on Start": "시작 시 업데이트 확인",
"Insufficient storage quota": "저장소 할당량 부족"
}
@@ -149,5 +149,6 @@
"Storage": "Przechowywanie",
"Book deleted: {{title}}": "Książka usunięta: {{title}}",
"Failed to delete book: {{title}}": "Nie udało się usunąć książki: {{title}}",
"Check Updates on Start": "Sprawdź aktualizacje przy starcie"
"Check Updates on Start": "Sprawdź aktualizacje przy starcie",
"Insufficient storage quota": "Niewystarczająca kwota przechowywania"
}
@@ -149,5 +149,6 @@
"Storage": "Armazenamento",
"Book deleted: {{title}}": "Livro excluído: {{title}}",
"Failed to delete book: {{title}}": "Falha ao excluir livro: {{title}}",
"Check Updates on Start": "Verificar Atualizações ao Iniciar"
"Check Updates on Start": "Verificar Atualizações ao Iniciar",
"Insufficient storage quota": "Cota de armazenamento insuficiente"
}
@@ -149,5 +149,6 @@
"Storage": "Хранилище",
"Book deleted: {{title}}": "Книга удалена: {{title}}",
"Failed to delete book: {{title}}": "Не удалось удалить книгу: {{title}}",
"Check Updates on Start": "Проверять обновления при запуске"
"Check Updates on Start": "Проверять обновления при запуске",
"Insufficient storage quota": "Недостаточно квоты хранилища"
}
@@ -149,5 +149,6 @@
"Storage": "Depolama",
"Book deleted: {{title}}": "Kitap silindi: {{title}}",
"Failed to delete book: {{title}}": "Kitap silinemedi: {{title}}",
"Check Updates on Start": "Başlangıçta Güncellemeleri Kontrol Et"
"Check Updates on Start": "Başlangıçta Güncellemeleri Kontrol Et",
"Insufficient storage quota": "Yetersiz depolama kotası"
}
@@ -149,5 +149,6 @@
"Storage": "Сховище",
"Book deleted: {{title}}": "Книгу видалено: {{title}}",
"Failed to delete book: {{title}}": "Не вдалося видалити книгу: {{title}}",
"Check Updates on Start": "Перевіряти оновлення при запуску"
"Check Updates on Start": "Перевіряти оновлення при запуску",
"Insufficient storage quota": "Недостатньо квоти сховища"
}
@@ -149,5 +149,6 @@
"Storage": "Lưu trữ",
"Book deleted: {{title}}": "Sách đã xóa: {{title}}",
"Failed to delete book: {{title}}": "Không thể xóa sách: {{title}}",
"Check Updates on Start": "Kiểm tra cập nhật khi khởi động"
"Check Updates on Start": "Kiểm tra cập nhật khi khởi động",
"Insufficient storage quota": "Không đủ lượng lưu trữ"
}
@@ -149,5 +149,6 @@
"Storage": "存储空间",
"Book deleted: {{title}}": "书籍已删除:{{title}}",
"Failed to delete book: {{title}}": "删除书籍失败:{{title}}",
"Check Updates on Start": "启动时检查更新"
"Check Updates on Start": "启动时检查更新",
"Insufficient storage quota": "云存储空间不足"
}
@@ -149,5 +149,6 @@
"Storage": "存儲空間",
"Book deleted: {{title}}": "書籍已刪除:{{title}}",
"Failed to delete book: {{title}}": "書籍刪除失敗:{{title}}",
"Check Updates on Start": "啟動時檢查更新"
"Check Updates on Start": "啟動時檢查更新",
"Insufficient storage quota": "雲存儲空間不足"
}
+17
View File
@@ -1,5 +1,22 @@
{
"releases": {
"0.9.9": {
"date": "2025-02-06",
"notes": [
"Load system fonts list at runtime on desktop",
"Incrementally load new books in synchronization",
"Change voice language dynamically by detecting text language",
"Various fixes and enhancements on sync, fonts and highlighting"
]
},
"0.9.8": {
"date": "2025-02-01",
"notes": [
"Support cloud sync for book files",
"Use larger font size on mobile devices for better reading experience",
"Various fixes and enhancements on sync, rendering and settings"
]
},
"0.9.7": {
"date": "2025-01-23",
"notes": [
@@ -1,6 +1,6 @@
pnpm run build-macos-universial-appstore
BUNDLE_DIR=src-tauri/target/universal-apple-darwin/release/bundle/macos
BUNDLE_DIR=../../target/universal-apple-darwin/release/bundle/macos
APP_BUNDLE=$BUNDLE_DIR/Readest.app
INSTALLER_BUNDLE=$BUNDLE_DIR/Readest.pkg
+1
View File
@@ -57,3 +57,4 @@ tauri-plugin-cli = "2"
tauri-plugin-single-instance = "2.2.0"
tauri-plugin-updater = "2"
tauri-plugin-window-state = "2.2.0"
font-enumeration = "0.9.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

+14 -1
View File
@@ -72,6 +72,17 @@ async fn start_server(window: Window) -> Result<u16, String> {
.map_err(|err| err.to_string())
}
#[cfg(desktop)]
#[command]
async fn list_fonts() -> Result<Vec<String>, String> {
let font_collection = font_enumeration::Collection::new().unwrap();
let mut fonts = Vec::new();
for font in font_collection.all() {
fonts.push(font.font_name.clone());
}
Ok(fonts)
}
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
@@ -86,7 +97,9 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
start_server,
download_file,
upload_file
upload_file,
#[cfg(desktop)]
list_fonts
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
@@ -56,6 +56,9 @@
"section": "text"
}
},
"android": {
"minSdkVersion": 26
},
"fileAssociations": [
{
"name": "epub",
+34 -18
View File
@@ -7,7 +7,6 @@ import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import { FcGoogle } from 'react-icons/fc';
import { FaApple } from 'react-icons/fa';
import { VscAzure } from 'react-icons/vsc';
import { FaGithub } from 'react-icons/fa';
import { IoArrowBack } from 'react-icons/io5';
@@ -17,10 +16,12 @@ import { useTheme } from '@/hooks/useTheme';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { isTauriAppPlatform } from '@/services/environment';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { onOpenUrl, getCurrent } from '@tauri-apps/plugin-deep-link';
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
import { openUrl } from '@tauri-apps/plugin-opener';
import { tauriHandleOnWindowFocus } from '@/utils/window';
import { handleAuthCallback } from '@/helpers/auth';
import { getOSPlatform } from '@/utils/misc';
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
@@ -60,8 +61,17 @@ export default function AuthPage() {
const [port, setPort] = useState<number | null>(null);
const isOAuthServerRunning = useRef(false);
const [isMounted, setIsMounted] = useState(false);
const osPlatform = getOSPlatform();
const signIn = async (provider: OAuthProvider) => {
const getTauriRedirectTo = () => {
return process.env.NODE_ENV === 'production'
? ['android', 'ios'].includes(osPlatform)
? 'https://web.readest.com/auth/callback'
: 'readest://auth/callback'
: `http://localhost:${port}`;
};
const tauriSignIn = async (provider: OAuthProvider) => {
if (!supabase) {
throw new Error('No backend connected');
}
@@ -70,10 +80,7 @@ export default function AuthPage() {
provider,
options: {
skipBrowserRedirect: true,
redirectTo:
process.env.NODE_ENV === 'production'
? 'readest://auth/callback'
: `http://localhost:${port}`,
redirectTo: getTauriRedirectTo(),
},
});
@@ -82,6 +89,21 @@ export default function AuthPage() {
return;
}
openUrl(data.url);
// FIXME: For Android we need a better way to trigger the deeplink redirect
if (isTauriAppPlatform() && osPlatform === 'android') {
tauriHandleOnWindowFocus(async () => {
const urls = await getCurrent();
if (urls && urls.length > 0) {
urls.forEach((url) => {
handleOAuthUrl(url);
});
}
});
setTimeout(() => {
router.back();
}, 5000);
}
};
const handleOAuthUrl = async (url: string) => {
@@ -200,25 +222,19 @@ export default function AuthPage() {
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
<ProviderLogin
provider='google'
handleSignIn={signIn}
handleSignIn={tauriSignIn}
Icon={FcGoogle}
label='Sign in with Google'
/>
<ProviderLogin
provider='apple'
handleSignIn={signIn}
handleSignIn={tauriSignIn}
Icon={FaApple}
label='Sign in with Apple'
/>
<ProviderLogin
provider='azure'
handleSignIn={signIn}
Icon={VscAzure}
label='Sign in with Azure'
/>
<ProviderLogin
provider='github'
handleSignIn={signIn}
handleSignIn={tauriSignIn}
Icon={FaGithub}
label='Sign in with GitHub'
/>
@@ -229,7 +245,7 @@ export default function AuthPage() {
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={[]}
redirectTo={`http://localhost:${port}`}
redirectTo={getTauriRedirectTo()}
/>
</div>
</div>
@@ -246,7 +262,7 @@ export default function AuthPage() {
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={['google', 'apple', 'azure', 'github']}
providers={['google', 'apple', 'github']}
redirectTo='/auth/callback'
/>
</div>
@@ -5,6 +5,7 @@ import { CiCircleMore } from 'react-icons/ci';
import { LiaCloudUploadAltSolid, LiaCloudDownloadAltSolid } from 'react-icons/lia';
import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import ReadingProgress from './ReadingProgress';
@@ -34,10 +35,11 @@ const BookItem: React.FC<BookItemProps> = ({
bookContextMenuHandler,
}) => {
const iconSize15 = useResponsiveSize(15);
const { appService } = useEnv();
return (
<div
className='book-item cursor-pointer'
className={clsx('book-item', appService?.hasContextMenu ? 'cursor-pointer' : '')}
onContextMenu={bookContextMenuHandler.bind(null, book)}
>
<div className='bg-base-100 shadow-md' onClick={() => handleBookClick(book)}>
@@ -15,7 +15,6 @@ import { navigateToReader } from '@/utils/nav';
import { getOSPlatform } from '@/utils/misc';
import { getFilename } from '@/utils/book';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { isTauriAppPlatform } from '@/services/environment';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
@@ -174,7 +173,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
};
const bookContextMenuHandler = async (book: Book, e: React.MouseEvent) => {
if (!isTauriAppPlatform()) return;
if (!appService?.hasContextMenu) return;
e.preventDefault();
e.stopPropagation();
const osPlatform = getOSPlatform();
@@ -43,6 +43,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
};
}, [onToggleSelectMode]);
const windowButtonVisible = appService?.appPlatform !== 'web' && !isTrafficLightVisible;
return (
<div
ref={headerRef}
@@ -104,14 +106,12 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
>
<SettingsMenu />
</Dropdown>
{!isTrafficLightVisible && appService?.appPlatform !== 'web' && (
<WindowButtons
headerRef={headerRef}
showMinimize={true}
showMaximize={true}
showClose={true}
/>
)}
<WindowButtons
headerRef={headerRef}
showMinimize={windowButtonVisible}
showMaximize={windowButtonVisible}
showClose={windowButtonVisible}
/>
</div>
</div>
</div>
@@ -6,7 +6,12 @@ import { useLibraryStore } from '@/store/libraryStore';
import { SYNC_BOOKS_INTERVAL_SEC } from '@/services/constants';
import { Book } from '@/types/book';
export const useBooksSync = () => {
export interface UseBooksSyncProps {
onSyncStart?: () => void;
onSyncEnd?: () => void;
}
export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
const { user } = useAuth();
const { appService } = useEnv();
const { library, setLibrary } = useLibraryStore();
@@ -91,16 +96,23 @@ export const useBooksSync = () => {
if (!updatedLibrary.some((oldBook) => oldBook.hash === newBook.hash)) {
if (newBook.uploadedAt && !newBook.deletedAt) {
try {
updatedLibrary.push(newBook);
await appService?.downloadBook(newBook, true);
newBook.coverImageUrl = await appService?.generateCoverImageUrl(newBook);
updatedLibrary.push(newBook);
setLibrary(updatedLibrary);
} catch {
console.error('Failed to download book:', newBook);
}
}
}
};
await Promise.all(syncedBooks.map(processNewBook));
onSyncStart?.();
const batchSize = 3;
for (let i = 0; i < syncedBooks.length; i += batchSize) {
const batch = syncedBooks.slice(i, i + batchSize);
await Promise.all(batch.map(processNewBook));
}
onSyncEnd?.();
setLibrary(updatedLibrary);
appService?.saveLibraryBooks(updatedLibrary);
};
+18 -3
View File
@@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation';
import { Book } from '@/types/book';
import { AppService } from '@/types/system';
import { navigateToReader } from '@/utils/nav';
import { navigateToLogin, navigateToReader } from '@/utils/nav';
import { getBaseFilename, listFormater } from '@/utils/book';
import { eventDispatcher } from '@/utils/event';
import { parseOpenWithFiles } from '@/helpers/cli';
@@ -57,7 +57,10 @@ const LibraryPage = () => {
const demoBooks = useDemoBooks();
const containerRef = useRef<HTMLDivElement>(null);
const { pullLibrary, pushLibrary } = useBooksSync();
const { pullLibrary, pushLibrary } = useBooksSync({
onSyncStart: () => setLoading(true),
onSyncEnd: () => setLoading(false),
});
usePullToRefresh(containerRef, pullLibrary);
@@ -244,7 +247,19 @@ const LibraryPage = () => {
title: book.title,
}),
});
} catch {
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('Not authenticated')) {
navigateToLogin(router);
return;
} else if (err.message.includes('Insufficient storage quota')) {
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Insufficient storage quota'),
});
return;
}
}
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Failed to upload book: {{title}}', {
@@ -9,6 +9,7 @@ import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar';
import { getStyles, mountAdditionalFonts } from '@/utils/style';
import { getBookDirFromWritingMode } from '@/utils/book';
import { useTheme } from '@/hooks/useTheme';
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
import {
@@ -86,7 +87,8 @@ const FoliateViewer: React.FC<{
if (renderer) {
if (renderer.start <= 0) {
viewRef.current?.prev(1);
} else if (renderer.end >= renderer.viewSize) {
// sometimes viewSize has subpixel value that the end never reaches
} else if (renderer.end + 1 >= renderer.viewSize) {
viewRef.current?.next(1);
}
}
@@ -140,6 +142,11 @@ const FoliateViewer: React.FC<{
const viewSettings = getViewSettings(bookKey)!;
view.renderer.setStyles?.(getStyles(viewSettings, themeCode));
const writingMode = viewSettings.writingMode;
if (writingMode) {
view.book.dir = getBookDirFromWritingMode(writingMode);
}
const isScrolled = viewSettings.scrolled!;
const marginPx = viewSettings.marginPx!;
const gapPercent = viewSettings.gapPercent!;
@@ -5,7 +5,12 @@ import { BiMoon, BiSun } from 'react-icons/bi';
import { TbSunMoon } from 'react-icons/tb';
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
import {
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
ONE_COLUMN_MAX_INLINE_SIZE,
ZOOM_STEP,
} from '@/services/constants';
import MenuItem from '@/components/MenuItem';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -32,8 +37,8 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
const [isInvertedColors, setInvertedColors] = useState(viewSettings!.invert);
const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!);
const zoomIn = () => setZoomLevel((prev) => Math.min(prev + 1, MAX_ZOOM_LEVEL));
const zoomOut = () => setZoomLevel((prev) => Math.max(prev - 1, MIN_ZOOM_LEVEL));
const zoomIn = () => setZoomLevel((prev) => Math.min(prev + ZOOM_STEP, MAX_ZOOM_LEVEL));
const zoomOut = () => setZoomLevel((prev) => Math.max(prev - ZOOM_STEP, MIN_ZOOM_LEVEL));
const resetZoom = () => setZoomLevel(100);
const toggleScrolledMode = () => setScrolledMode(!isScrolledMode);
const toggleInvertedColors = () => setInvertedColors(!isInvertedColors);
@@ -104,7 +109,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
)}
onClick={resetZoom}
>
{100 - (100 - zoomLevel) * 10}%
{zoomLevel}%
</button>
<button
onClick={zoomIn}
@@ -8,7 +8,7 @@ import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize
interface DropdownProps {
family?: string;
selected: string;
options: string[];
options: { option: string; label: string }[];
moreOptions?: string[];
onSelect: (option: string) => void;
onGetFontFamily: (option: string, family: string) => string;
@@ -25,26 +25,31 @@ const FontDropdown: React.FC<DropdownProps> = ({
const _ = useTranslation();
const iconSize16 = useResponsiveSize(16);
const defaultIconSize = useDefaultIconSize();
const selectedOption = options.find((option) => option.option === selected)!;
return (
<div className='dropdown dropdown-top'>
<button
tabIndex={0}
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
>
<span style={{ fontFamily: onGetFontFamily(selected, family ?? '') }}>{selected}</span>
<span style={{ fontFamily: onGetFontFamily(selectedOption.option, family ?? '') }}>
{selectedOption.label}
</span>
<FiChevronUp size={iconSize16} />
</button>
<ul
tabIndex={0}
className='dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute right-[-32px] z-[1] mt-4 w-44 shadow sm:right-0'
>
{options.map((option) => (
{options.map(({ option, label }) => (
<li key={option} onClick={() => onSelect(option)}>
<div className='flex items-center px-0'>
<span style={{ minWidth: `${defaultIconSize}px` }}>
{selected === option && <MdCheck className='text-base-content' />}
</span>
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>{option}</span>
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
{label || option}
</span>
</div>
</li>
))}
@@ -19,6 +19,8 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getOSPlatform } from '@/utils/misc';
import { FONT_ENUM_SUPPORTED_OS_PLATFORMS, getSysFontsList } from '@/utils/font';
import { isTauriAppPlatform } from '@/services/environment';
interface FontFaceProps {
className?: string;
@@ -30,8 +32,6 @@ interface FontFaceProps {
onSelect: (option: string) => void;
}
const fontFamilyOptions = ['Serif', 'Sans-serif'];
const handleFontFaceFont = (option: string, family: string) => {
return `'${option}', ${family}`;
};
@@ -49,7 +49,7 @@ const FontFace = ({
<span className=''>{label}</span>
<FontDropdown
family={family}
options={options}
options={options.map((option) => ({ option, label: option }))}
moreOptions={moreOptions}
selected={selected}
onSelect={onSelect}
@@ -66,27 +66,39 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const viewSettings = getViewSettings(bookKey)!;
const { themeCode } = useTheme();
const fontFamilyOptions = [
{
option: 'Serif',
label: _('Serif Font'),
},
{
option: 'Sans-serif',
label: _('Sans-Serif Font'),
},
];
const osPlatform = getOSPlatform();
let moreFonts: string[] = [];
let defaultSysFonts: string[] = [];
switch (osPlatform) {
case 'macos':
moreFonts = MACOS_FONTS;
defaultSysFonts = MACOS_FONTS;
break;
case 'windows':
moreFonts = WINDOWS_FONTS;
defaultSysFonts = WINDOWS_FONTS;
break;
case 'linux':
moreFonts = LINUX_FONTS;
defaultSysFonts = LINUX_FONTS;
break;
case 'ios':
moreFonts = IOS_FONTS;
defaultSysFonts = IOS_FONTS;
break;
case 'android':
moreFonts = ANDROID_FONTS;
defaultSysFonts = ANDROID_FONTS;
break;
default:
break;
}
const [sysFonts, setSysFonts] = useState<string[]>(defaultSysFonts);
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
@@ -95,6 +107,15 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
const [monospaceFont, setMonospaceFont] = useState(viewSettings.monospaceFont!);
useEffect(() => {
if (isTauriAppPlatform() && FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
getSysFontsList().then((fonts) => {
setSysFonts(fonts);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
viewSettings.defaultFont = defaultFont;
setViewSettings(bookKey, viewSettings);
@@ -247,7 +268,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
family='serif'
label={_('Serif Font')}
options={SERIF_FONTS}
moreOptions={moreFonts}
moreOptions={sysFonts}
selected={serifFont}
onSelect={setSerifFont}
/>
@@ -255,7 +276,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
family='sans-serif'
label={_('Sans-Serif Font')}
options={SANS_SERIF_FONTS}
moreOptions={moreFonts}
moreOptions={sysFonts}
selected={sansSerifFont}
onSelect={setSansSerifFont}
/>
@@ -264,7 +285,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
family='monospace'
label={_('Monospace Font')}
options={MONOSPACE_FONTS}
moreOptions={moreFonts}
moreOptions={sysFonts}
selected={monospaceFont}
onSelect={setMonospaceFont}
/>
@@ -9,7 +9,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getBookLangCode } from '@/utils/book';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
@@ -134,7 +134,10 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// global settings are not supported for writing mode
viewSettings.writingMode = writingMode;
setViewSettings(bookKey, viewSettings);
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
if (view) {
view.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view.book.dir = getBookDirFromWritingMode(writingMode);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [writingMode]);
@@ -8,7 +8,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { eventDispatcher } from '@/utils/event';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '@/services/constants';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
interface UseBookShortcutsProps {
sideBarBookKey: string | null;
@@ -83,7 +83,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const view = getView(sideBarBookKey);
if (!view?.renderer?.setStyles) return;
const viewSettings = getViewSettings(sideBarBookKey)!;
const zoomLevel = viewSettings!.zoomLevel + 1;
const zoomLevel = viewSettings!.zoomLevel + ZOOM_STEP;
viewSettings!.zoomLevel = Math.min(zoomLevel, MAX_ZOOM_LEVEL);
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
@@ -94,7 +94,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const view = getView(sideBarBookKey);
if (!view?.renderer?.setStyles) return;
const viewSettings = getViewSettings(sideBarBookKey)!;
const zoomLevel = viewSettings!.zoomLevel - 1;
const zoomLevel = viewSettings!.zoomLevel - ZOOM_STEP;
viewSettings!.zoomLevel = Math.max(zoomLevel, MIN_ZOOM_LEVEL);
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
+1
View File
@@ -63,6 +63,7 @@ export interface BookDoc {
subject?: string[];
identifier?: string;
};
dir: string;
toc?: Array<TOCItem>;
sections?: Array<SectionItem>;
splitTOCHref(href: string): Array<string | number>;
+3
View File
@@ -82,6 +82,9 @@ export const uploadFile = async (
}
} catch (error) {
console.error('File upload failed:', error);
if (error instanceof Error) {
throw error;
}
throw new Error('File upload failed');
}
};
+3 -1
View File
@@ -44,6 +44,7 @@ export abstract class BaseAppService implements AppService {
abstract isAppDataSandbox: boolean;
abstract hasTrafficLight: boolean;
abstract hasWindowBar: boolean;
abstract hasContextMenu: boolean;
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
abstract getCoverImageUrl(book: Book): string;
@@ -319,7 +320,8 @@ export abstract class BaseAppService implements AppService {
let file: File;
const fp = getFilename(book);
if (await this.fs.exists(fp, 'Books')) {
if (this.appPlatform === 'web') {
// TODO: fix random access for android
if (this.appPlatform === 'web' || getOSPlatform() === 'android') {
const content = await this.fs.readFile(fp, 'Books', 'binary');
file = new File([content], fp);
} else {
+4 -8
View File
@@ -18,7 +18,7 @@ export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).j
export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
keepLogin: false,
autoUpload: false,
autoUpload: true,
autoCheckUpdates: true,
lastSyncedAtBooks: 0,
@@ -175,13 +175,10 @@ export const WINDOWS_FONTS = [
'SimSun-ExtB',
'Sitka',
'Sylfaen',
'Symbol',
'Tahoma',
'Times New Roman',
'Trebuchet MS',
'Verdana',
'Webdings',
'Wingdings',
'XiHeiti',
'Yu Gothic',
'Yu Mincho',
@@ -309,11 +306,9 @@ export const LINUX_FONTS = [
'Sazanami Mincho',
'Source Han Sans',
'Source Han Serif',
'Symbola',
'Times New Roman',
'Ubuntu',
'Ubuntu Mono',
'Wingdings',
'WenQuanYi Micro Hei',
'WenQuanYi Zen Hei',
'XiHeiti',
@@ -384,8 +379,9 @@ export const SYNC_NOTES_INTERVAL_SEC = 10;
export const SYNC_BOOKS_INTERVAL_SEC = 10;
export const CHECK_UPDATE_INTERVAL_SEC = 24 * 60 * 60;
export const MAX_ZOOM_LEVEL = 140;
export const MIN_ZOOM_LEVEL = 95;
export const MAX_ZOOM_LEVEL = 500;
export const MIN_ZOOM_LEVEL = 50;
export const ZOOM_STEP = 10;
export const DEFAULT_STORAGE_QUOTA: UserStorageQuota = {
free: 500 * 1024 * 1024,
@@ -120,6 +120,7 @@ export class NativeAppService extends BaseAppService {
isAppDataSandbox = isMobile;
hasTrafficLight = osType() === 'macos';
hasWindowBar = !(osType() === 'ios' || osType() === 'android');
hasContextMenu = !(osType() === 'ios' || osType() === 'android');
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
@@ -8,6 +8,7 @@ export class EdgeTTSClient implements TTSClient {
#rate = 1.0;
#pitch = 1.0;
#voice: TTSVoice | null = null;
#currentVoiceLang = '';
#voices: TTSVoice[] = [];
#edgeTTS: EdgeSpeechTTS;
@@ -60,12 +61,17 @@ export class EdgeTTSClient implements TTSClient {
preload = false,
): AsyncGenerator<TTSMessageEvent> {
const { marks } = parseSSMLMarks(ssml);
const lang = parseSSMLLang(ssml) || 'en';
const ssmlLang = parseSSMLLang(ssml) || 'en';
let lang = ssmlLang;
if (lang === 'en' && /[\p{Script=Han}]/u.test(ssml)) {
lang = 'zh';
}
let voiceId = 'en-US-AriaNeural';
if (!this.#voice) {
if (!this.#voice || ssmlLang !== lang) {
const voices = await this.getVoices(lang);
this.#voice = voices[0] ? voices[0] : this.#voices.find((v) => v.id === voiceId) || null;
this.#currentVoiceLang = lang;
}
if (this.#voice) {
voiceId = this.#voice.id;
@@ -229,6 +235,9 @@ export class EdgeTTSClient implements TTSClient {
}
async getVoices(lang: string): Promise<TTSVoice[]> {
if (this.#currentVoiceLang) {
lang = this.#currentVoiceLang;
}
const locale = lang === 'en' ? getUserLocale(lang) || lang : lang;
const voices = await this.getAllVoices();
return voices.filter((v) => v.lang.startsWith(locale));
@@ -175,6 +175,7 @@ export class WebSpeechClient implements TTSClient {
#rate = 1.0;
#pitch = 1.0;
#voice: SpeechSynthesisVoice | null = null;
#currentVoiceLang = '';
#voices: SpeechSynthesisVoice[] = [];
#synth = window.speechSynthesis;
available = true;
@@ -213,11 +214,16 @@ export class WebSpeechClient implements TTSClient {
// no need to preload for web speech
if (preload) return;
const lang = parseSSMLLang(ssml) || 'en';
if (!this.#voice) {
const ssmlLang = parseSSMLLang(ssml) || 'en';
let lang = ssmlLang;
if (lang === 'en' && /[\p{Script=Han}]/u.test(ssml)) {
lang = 'zh';
}
if (!this.#voice || ssmlLang !== lang) {
const voices = await this.getVoices(lang);
const voiceId = voices[0]?.id ?? '';
this.#voice = this.#voices.find((v) => v.voiceURI === voiceId) || null;
this.#currentVoiceLang = lang;
}
for await (const ev of speakWithMarks(
ssml,
@@ -286,6 +292,9 @@ export class WebSpeechClient implements TTSClient {
}
async getVoices(lang: string) {
if (this.#currentVoiceLang) {
lang = this.#currentVoiceLang;
}
const locale = lang === 'en' ? getUserLocale(lang) || lang : lang;
const isValidVoice = (id: string) => {
return !id.includes('com.apple') || id.includes('com.apple.voice.compact');
@@ -181,6 +181,7 @@ export class WebAppService extends BaseAppService {
isAppDataSandbox = false;
hasTrafficLight = false;
hasWindowBar = false;
hasContextMenu = false;
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
+3 -1
View File
@@ -46,6 +46,8 @@ export interface BookNote {
deletedAt?: number | null;
}
export type WritingMode = 'auto' | 'horizontal-tb' | 'horizontal-rl' | 'vertical-rl';
export interface BookLayout {
marginPx: number;
gapPercent: number;
@@ -55,7 +57,7 @@ export interface BookLayout {
maxInlineSize: number;
maxBlockSize: number;
animated: boolean;
writingMode: string;
writingMode: WritingMode;
vertical: boolean;
}
+1
View File
@@ -25,6 +25,7 @@ export interface AppService {
appPlatform: AppPlatform;
hasTrafficLight: boolean;
hasWindowBar: boolean;
hasContextMenu: boolean;
isMobile: boolean;
isAppDataSandbox: boolean;
+1
View File
@@ -21,6 +21,7 @@ export interface FoliateView extends HTMLElement {
select: (target: string | number | { fraction: number }) => void;
deselect: () => void;
initTTS: (granularity?: TTSGranularity) => Promise<void>;
book: BookDoc;
tts: TTS | null;
language: {
locale?: string;
+13 -1
View File
@@ -1,5 +1,5 @@
import { EXTS } from '@/libs/document';
import { Book, BookConfig, BookProgress } from '@/types/book';
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
import { getUserLang, makeSafeFilename } from './misc';
export const getDir = (book: Book) => {
@@ -119,3 +119,15 @@ export const getCurrentPage = (book: Book, progress: BookProgress) => {
? pageinfo.current + 1
: 0;
};
export const getBookDirFromWritingMode = (writingMode: WritingMode) => {
switch (writingMode) {
case 'horizontal-tb':
return 'ltr';
case 'horizontal-rl':
case 'vertical-rl':
return 'rtl';
default:
return 'auto';
}
};
+22 -1
View File
@@ -1,3 +1,5 @@
import { getOSPlatform } from './misc';
class RemoteBlobSlice extends Blob {
#dataPromise: Promise<ArrayBuffer>;
#type: string;
@@ -83,7 +85,7 @@ export class RemoteFile extends File {
return this.#lastModified;
}
async open() {
async _open_with_head() {
const response = await fetch(this.url, { method: 'HEAD' });
if (!response.ok) {
throw new Error(`Failed to fetch file size: ${response.status}`);
@@ -93,6 +95,25 @@ export class RemoteFile extends File {
return this;
}
async _open_with_range() {
const response = await fetch(this.url, { headers: { Range: `bytes=${0}-${1023}` } });
if (!response.ok) {
throw new Error(`Failed to fetch file size: ${response.status}`);
}
this.#size = Number(response.headers.get('content-range')?.split('/')[1]);
this.#type = response.headers.get('content-type') || '';
return this;
}
async open() {
// FIXME: currently HEAD request in asset protocol is not supported on Android
if (getOSPlatform() === 'android') {
return this._open_with_range();
} else {
return this._open_with_head();
}
}
async fetchRangePart(start: number, end: number) {
start = Math.max(0, start);
end = Math.min(this.size - 1, end);
+31
View File
@@ -0,0 +1,31 @@
import { invoke } from '@tauri-apps/api/core';
import { getOSPlatform } from './misc';
let cachedSysFonts: string[] | null = null;
export const FONT_ENUM_SUPPORTED_OS_PLATFORMS = ['macos', 'windows', 'linux'];
const isSymbolicFontName = (font: string) =>
/emoji|icons|symbol|dingbats|ornaments|webdings|wingdings/i.test(font);
export const getSysFontsList = async (): Promise<string[]> => {
if (cachedSysFonts) {
return cachedSysFonts;
}
try {
const osPlatform = getOSPlatform();
if (FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
const fonts = await invoke<string[]>('list_fonts');
cachedSysFonts = fonts.filter((font) => !isSymbolicFontName(font)).sort();
console.log('Fetched font list:', cachedSysFonts);
return cachedSysFonts;
} else {
console.warn(`Unsupported platform: ${osPlatform}`);
return [];
}
} catch (error) {
console.error('Error fetching font list:', error);
return [];
}
};
+8 -3
View File
@@ -10,7 +10,8 @@ export const makeSafeFilename = (filename: string, replacement = '_') => {
// Windows restricted characters + control characters and reserved names
const unsafeCharacters = /[<>:"\/\\|?*\x00-\x1F]/g;
const reservedFilenames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
const maxFilenameLength = 255;
// Unsafe to use filename including file extensions over 255 bytes on Android
const maxFilenameBytes = 250;
let safeName = filename.replace(unsafeCharacters, replacement);
@@ -18,8 +19,12 @@ export const makeSafeFilename = (filename: string, replacement = '_') => {
safeName = `${safeName}${replacement}`;
}
if (safeName.length > maxFilenameLength) {
safeName = safeName.substring(0, maxFilenameLength);
const encoder = new TextEncoder();
let utf8Bytes = encoder.encode(safeName);
while (utf8Bytes.length > maxFilenameBytes) {
safeName = safeName.slice(0, -1);
utf8Bytes = encoder.encode(safeName);
}
return safeName.trim();
+1 -1
View File
@@ -153,7 +153,7 @@ const getLayoutStyles = (
${bg === '#ffffff' ? '' : `color: inherit;`}
${bg === '#ffffff' ? '' : `background-color: ${bg} !important;`}
}
body * {
body {
zoom: ${zoomLevel};
}
svg, img {
+9
View File
@@ -24,3 +24,12 @@ export const tauriHandleOnCloseWindow = async (callback: () => void) => {
await exit(0);
});
};
export const tauriHandleOnWindowFocus = async (callback: () => void) => {
const { TauriEvent } = await import('@tauri-apps/api/event');
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
return currentWindow.listen(TauriEvent.WINDOW_FOCUS, async () => {
await callback();
});
};
+127 -127
View File
@@ -40,7 +40,7 @@ importers:
version: 3.735.0
'@ducanh2912/next-pwa':
specifier: ^10.2.9
version: 10.2.9(next@15.1.0(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(webpack@5.97.1)
version: 10.2.9(next@15.1.6(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(webpack@5.97.1)
'@fabianlars/tauri-plugin-oauth':
specifier: '2'
version: 2.0.0
@@ -54,8 +54,8 @@ importers:
specifier: ^2.47.7
version: 2.47.7
'@tauri-apps/api':
specifier: 2.1.1
version: 2.1.1
specifier: 2.2.0
version: 2.2.0
'@tauri-apps/plugin-cli':
specifier: ^2.2.0
version: 2.2.0
@@ -69,14 +69,14 @@ importers:
specifier: ^2.2.0
version: 2.2.0
'@tauri-apps/plugin-http':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.3.0
version: 2.3.0
'@tauri-apps/plugin-log':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-opener':
specifier: ^2.2.2
version: 2.2.2
specifier: ^2.2.5
version: 2.2.5
'@tauri-apps/plugin-os':
specifier: ^2.2.0
version: 2.2.0
@@ -87,8 +87,8 @@ importers:
specifier: ~2.2.0
version: 2.2.0
'@tauri-apps/plugin-updater':
specifier: ^2.3.0
version: 2.3.0
specifier: ^2.4.0
version: 2.4.0
'@zip.js/zip.js':
specifier: ^2.7.53
version: 2.7.53
@@ -120,8 +120,8 @@ importers:
specifier: ^4.0.0
version: 4.0.0
next:
specifier: 15.1.0
version: 15.1.0(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: 15.1.6
version: 15.1.6(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
posthog-js:
specifier: ^1.205.0
version: 1.205.0
@@ -148,8 +148,8 @@ importers:
version: 5.0.1(@types/react@18.3.12)(react@19.0.0)
devDependencies:
'@tauri-apps/cli':
specifier: 2.1.0
version: 2.1.0
specifier: 2.2.7
version: 2.2.7
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
@@ -1127,60 +1127,60 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@next/env@15.1.0':
resolution: {integrity: sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==}
'@next/env@15.1.6':
resolution: {integrity: sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==}
'@next/eslint-plugin-next@15.0.3':
resolution: {integrity: sha512-3Ln/nHq2V+v8uIaxCR6YfYo7ceRgZNXfTd3yW1ukTaFbO+/I8jNakrjYWODvG9BuR2v5kgVtH/C8r0i11quOgw==}
'@next/swc-darwin-arm64@15.1.0':
resolution: {integrity: sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==}
'@next/swc-darwin-arm64@15.1.6':
resolution: {integrity: sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.1.0':
resolution: {integrity: sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==}
'@next/swc-darwin-x64@15.1.6':
resolution: {integrity: sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.1.0':
resolution: {integrity: sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==}
'@next/swc-linux-arm64-gnu@15.1.6':
resolution: {integrity: sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.1.0':
resolution: {integrity: sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==}
'@next/swc-linux-arm64-musl@15.1.6':
resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.1.0':
resolution: {integrity: sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==}
'@next/swc-linux-x64-gnu@15.1.6':
resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.1.0':
resolution: {integrity: sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==}
'@next/swc-linux-x64-musl@15.1.6':
resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.1.0':
resolution: {integrity: sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==}
'@next/swc-win32-arm64-msvc@15.1.6':
resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.1.0':
resolution: {integrity: sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==}
'@next/swc-win32-x64-msvc@15.1.6':
resolution: {integrity: sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1623,75 +1623,75 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tauri-apps/api@2.1.1':
resolution: {integrity: sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==}
'@tauri-apps/api@2.2.0':
resolution: {integrity: sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==}
'@tauri-apps/cli-darwin-arm64@2.1.0':
resolution: {integrity: sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==}
'@tauri-apps/cli-darwin-arm64@2.2.7':
resolution: {integrity: sha512-54kcpxZ3X1Rq+pPTzk3iIcjEVY4yv493uRx/80rLoAA95vAC0c//31Whz75UVddDjJfZvXlXZ3uSZ+bnCOnt0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.1.0':
resolution: {integrity: sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==}
'@tauri-apps/cli-darwin-x64@2.2.7':
resolution: {integrity: sha512-Vgu2XtBWemLnarB+6LqQeLanDlRj7CeFN//H8bVVdjbNzxcSxsvbLYMBP8+3boa7eBnjDrqMImRySSgL6IrwTw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.1.0':
resolution: {integrity: sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.2.7':
resolution: {integrity: sha512-+Clha2iQAiK9zoY/KKW0KLHkR0k36O78YLx5Sl98tWkwI3OBZFg5H5WT1plH/4sbZIS2aLFN6dw58/JlY9Bu/g==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.1.0':
resolution: {integrity: sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==}
'@tauri-apps/cli-linux-arm64-gnu@2.2.7':
resolution: {integrity: sha512-Z/Lp4SQe6BUEOays9BQAEum2pvZF4w9igyXijP+WbkOejZx4cDvarFJ5qXrqSLmBh7vxrdZcLwoLk9U//+yQrg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.1.0':
resolution: {integrity: sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==}
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.1.0':
resolution: {integrity: sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==}
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.1.0':
resolution: {integrity: sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==}
'@tauri-apps/cli-linux-x64-musl@2.2.7':
resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.1.0':
resolution: {integrity: sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==}
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.1.0':
resolution: {integrity: sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==}
'@tauri-apps/cli-win32-ia32-msvc@2.2.7':
resolution: {integrity: sha512-EiJ5/25tLSQOSGvv+t6o3ZBfOTKB5S3vb+hHQuKbfmKdRF0XQu2YPdIi1CQw1DU97ZAE0Dq4frvnyYEKWgMzVQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.1.0':
resolution: {integrity: sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==}
'@tauri-apps/cli-win32-x64-msvc@2.2.7':
resolution: {integrity: sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.1.0':
resolution: {integrity: sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==}
'@tauri-apps/cli@2.2.7':
resolution: {integrity: sha512-ZnsS2B4BplwXP37celanNANiIy8TCYhvg5RT09n72uR/o+navFZtGpFSqljV8fy1Y4ixIPds8FrGSXJCN2BerA==}
engines: {node: '>= 10'}
hasBin: true
@@ -1707,14 +1707,14 @@ packages:
'@tauri-apps/plugin-fs@2.2.0':
resolution: {integrity: sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==}
'@tauri-apps/plugin-http@2.2.0':
resolution: {integrity: sha512-ZY6sIHhgu8hcu6BkkegoiOEbvOsQFSVcK8J7l+g9RNHrkhl5uzpNIytR4R/H50fj7gyG80DJvrXDx/LBo7Easw==}
'@tauri-apps/plugin-http@2.3.0':
resolution: {integrity: sha512-pigTvz+zzAqbIhCzRiR1GE98Jw7A03j2V+Eiexr9thBI8VfMiwFQMcbgON51xlwnVaI72LdbYKNajU84im8tlg==}
'@tauri-apps/plugin-log@2.2.0':
resolution: {integrity: sha512-g6CsQAR1lsm5ABSZZxpM/iCn86GrMDTTlhj7GPkZkYBRSm3+WczfOAl7SV7HDn77tOKCzhZffwI5uHfRoHutrw==}
'@tauri-apps/plugin-log@2.2.1':
resolution: {integrity: sha512-bOz9w0hhlXLGLc1ZR37GqkXvTqkykl4A3GEKLjRIs0dq3n0BzLyoRDMPcpt7PdUHqaq6WISME+zEX2bqjSbJ2A==}
'@tauri-apps/plugin-opener@2.2.2':
resolution: {integrity: sha512-E/XIHKqGV+FT8PDdkfMETmgPUxcR79Rk8USuzbadD/ZdvsKCfQR5q+6rpZC9zEnG2wzi9lVQM4D3xwrtGGIB8A==}
'@tauri-apps/plugin-opener@2.2.5':
resolution: {integrity: sha512-hHsJ9RPWpZvZEPVFaL+d25gABMUMOf/A6ESXnvf/ii9guTukj58WXsAE/SOysXRIhej7kseRCxnOnIMpSCdUsQ==}
'@tauri-apps/plugin-os@2.2.0':
resolution: {integrity: sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==}
@@ -1725,8 +1725,8 @@ packages:
'@tauri-apps/plugin-shell@2.2.0':
resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==}
'@tauri-apps/plugin-updater@2.3.0':
resolution: {integrity: sha512-qdzyZEUN69FZQ/nRx51fBub10tT6wffJl3DLVo9q922Gvw8Wk++rZhoD9eethPlZYbog/7RGgT8JkrfLh5BKAg==}
'@tauri-apps/plugin-updater@2.4.0':
resolution: {integrity: sha512-BkeKN2WObAjobf2G77HyW/DxAfI0In+VSqWGnw/0cVPlM+VmA7fw9dKUnSunryZOG7ys9y07tj7FQa1ABMXGZQ==}
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@@ -3356,8 +3356,8 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
next@15.1.0:
resolution: {integrity: sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==}
next@15.1.6:
resolution: {integrity: sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -5705,10 +5705,10 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.9
optional: true
'@ducanh2912/next-pwa@10.2.9(next@15.1.0(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(webpack@5.97.1)':
'@ducanh2912/next-pwa@10.2.9(next@15.1.6(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(webpack@5.97.1)':
dependencies:
fast-glob: 3.3.2
next: 15.1.0(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next: 15.1.6(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
semver: 7.6.3
webpack: 5.97.1
workbox-build: 7.1.1
@@ -5765,7 +5765,7 @@ snapshots:
'@fabianlars/tauri-plugin-oauth@2.0.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@gulpjs/to-absolute-glob@4.0.0':
dependencies:
@@ -5896,34 +5896,34 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.0
optional: true
'@next/env@15.1.0': {}
'@next/env@15.1.6': {}
'@next/eslint-plugin-next@15.0.3':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@15.1.0':
'@next/swc-darwin-arm64@15.1.6':
optional: true
'@next/swc-darwin-x64@15.1.0':
'@next/swc-darwin-x64@15.1.6':
optional: true
'@next/swc-linux-arm64-gnu@15.1.0':
'@next/swc-linux-arm64-gnu@15.1.6':
optional: true
'@next/swc-linux-arm64-musl@15.1.0':
'@next/swc-linux-arm64-musl@15.1.6':
optional: true
'@next/swc-linux-x64-gnu@15.1.0':
'@next/swc-linux-x64-gnu@15.1.6':
optional: true
'@next/swc-linux-x64-musl@15.1.0':
'@next/swc-linux-x64-musl@15.1.6':
optional: true
'@next/swc-win32-arm64-msvc@15.1.0':
'@next/swc-win32-arm64-msvc@15.1.6':
optional: true
'@next/swc-win32-x64-msvc@15.1.0':
'@next/swc-win32-x64-msvc@15.1.6':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -6480,94 +6480,94 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tauri-apps/api@2.1.1': {}
'@tauri-apps/api@2.2.0': {}
'@tauri-apps/cli-darwin-arm64@2.1.0':
'@tauri-apps/cli-darwin-arm64@2.2.7':
optional: true
'@tauri-apps/cli-darwin-x64@2.1.0':
'@tauri-apps/cli-darwin-x64@2.2.7':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.1.0':
'@tauri-apps/cli-linux-arm-gnueabihf@2.2.7':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.1.0':
'@tauri-apps/cli-linux-arm64-gnu@2.2.7':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.1.0':
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.1.0':
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.1.0':
'@tauri-apps/cli-linux-x64-musl@2.2.7':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.1.0':
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.1.0':
'@tauri-apps/cli-win32-ia32-msvc@2.2.7':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.1.0':
'@tauri-apps/cli-win32-x64-msvc@2.2.7':
optional: true
'@tauri-apps/cli@2.1.0':
'@tauri-apps/cli@2.2.7':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.1.0
'@tauri-apps/cli-darwin-x64': 2.1.0
'@tauri-apps/cli-linux-arm-gnueabihf': 2.1.0
'@tauri-apps/cli-linux-arm64-gnu': 2.1.0
'@tauri-apps/cli-linux-arm64-musl': 2.1.0
'@tauri-apps/cli-linux-x64-gnu': 2.1.0
'@tauri-apps/cli-linux-x64-musl': 2.1.0
'@tauri-apps/cli-win32-arm64-msvc': 2.1.0
'@tauri-apps/cli-win32-ia32-msvc': 2.1.0
'@tauri-apps/cli-win32-x64-msvc': 2.1.0
'@tauri-apps/cli-darwin-arm64': 2.2.7
'@tauri-apps/cli-darwin-x64': 2.2.7
'@tauri-apps/cli-linux-arm-gnueabihf': 2.2.7
'@tauri-apps/cli-linux-arm64-gnu': 2.2.7
'@tauri-apps/cli-linux-arm64-musl': 2.2.7
'@tauri-apps/cli-linux-x64-gnu': 2.2.7
'@tauri-apps/cli-linux-x64-musl': 2.2.7
'@tauri-apps/cli-win32-arm64-msvc': 2.2.7
'@tauri-apps/cli-win32-ia32-msvc': 2.2.7
'@tauri-apps/cli-win32-x64-msvc': 2.2.7
'@tauri-apps/plugin-cli@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-deep-link@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-dialog@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-fs@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-http@2.2.0':
'@tauri-apps/plugin-http@2.3.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-log@2.2.0':
'@tauri-apps/plugin-log@2.2.1':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-opener@2.2.2':
'@tauri-apps/plugin-opener@2.2.5':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-os@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-process@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-shell@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tauri-apps/plugin-updater@2.3.0':
'@tauri-apps/plugin-updater@2.4.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/api': 2.2.0
'@tsconfig/node10@1.0.11':
optional: true
@@ -8378,26 +8378,26 @@ snapshots:
neo-async@2.6.2: {}
next@15.1.0(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
next@15.1.6(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@next/env': 15.1.0
'@next/env': 15.1.6
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
caniuse-lite: 1.0.30001684
caniuse-lite: 1.0.30001695
postcss: 8.4.31
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(@babel/core@7.26.7)(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.1.0
'@next/swc-darwin-x64': 15.1.0
'@next/swc-linux-arm64-gnu': 15.1.0
'@next/swc-linux-arm64-musl': 15.1.0
'@next/swc-linux-x64-gnu': 15.1.0
'@next/swc-linux-x64-musl': 15.1.0
'@next/swc-win32-arm64-msvc': 15.1.0
'@next/swc-win32-x64-msvc': 15.1.0
'@next/swc-darwin-arm64': 15.1.6
'@next/swc-darwin-x64': 15.1.6
'@next/swc-linux-arm64-gnu': 15.1.6
'@next/swc-linux-arm64-musl': 15.1.6
'@next/swc-linux-x64-gnu': 15.1.6
'@next/swc-linux-x64-musl': 15.1.6
'@next/swc-win32-arm64-msvc': 15.1.6
'@next/swc-win32-x64-msvc': 15.1.6
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'