Compare commits

..

36 Commits

Author SHA1 Message Date
Huang Xin 4869b756ea release: version 0.9.20 (#549)
* fix: occasionally traffic lights disappear in library page

* release: version 0.9.20
2025-03-09 17:40:30 +01:00
Huang Xin 693c79c9b7 fix: remove seldomly used option to invert colors in dark mode (#548)
This option is hard to understand and explain. Most hardcoded color
in dark mode that affects readability has already been fixed in the
reader so there is no necessity to have this option.
2025-03-09 17:02:01 +01:00
Huang Xin e846875973 doc: update screenshots in readme (#546) 2025-03-09 14:15:41 +01:00
Huang Xin b1f2941247 fix: various fixes for updater, footerbar and note item (#545)
* fix: don't open book if failed to download book

* fix: various fixes for updater, footerbar and note item
1. now footerbar when invisible won't process pointer events
2. manually check updater won't be prevented by check interval
3. get rid of clamped text in note item
2025-03-09 11:33:52 +01:00
Huang Xin 6688c813e6 fix: don't open book if failed to download book (#541) 2025-03-09 10:29:16 +01:00
Huang Xin 664dc45fcc fix: search options menu in mobile & user id in posthog (#539) 2025-03-09 07:07:42 +01:00
Huang Xin 27fa753486 feat: support drag and drop to import books, closes #524 (#536) 2025-03-08 19:12:00 +01:00
Huang Xin 71e392544c fix: rtl for notebook, closes #451 (#535) 2025-03-08 16:51:24 +01:00
Huang Xin 7f24081205 feat: add fullscreen option for desktop app, closes #493 (#534) 2025-03-08 22:36:25 +08:00
Huang Xin 89f1c43984 fix: don't hide horizontal scrollbar at the bottom, closes #515 (#533) 2025-03-08 21:26:10 +08:00
Huang Xin f841004e7a chore: bump update of tauri (#532) 2025-03-08 10:53:31 +01:00
Huang Xin 0922f557bb fix: make the height of the drag handle in dialog stable (#530) 2025-03-08 10:03:00 +01:00
Huang Xin 0b72ab156d refactor: move useTheme hook into separate store, also closes #528 (#529) 2025-03-08 09:12:36 +01:00
Huang Xin dfc6cc9c8a Update feature-request template 2025-03-08 04:02:02 +01:00
Huang Xin 83b2908266 compat: replace absolute font sizes with rem units in css, closes #521 (#523) 2025-03-07 18:37:46 +01:00
Huang Xin c25f2a7abc fix: make continuous scroll an option and disabled by default, closes #516, #373 (#522) 2025-03-07 17:24:01 +01:00
Huang Xin de1e396f6e fix: switch label for column width/height for vertical layout and fix column size calculation (#520)
* fix: scroll mode should be updated when calculating column size

* fix: switch label for column width/height for vertical layout, closes #517
2025-03-07 16:42:40 +01:00
Huajin 80a3861816 Fixes the scrollbar gutter issue for DaisyUI (#514)
* Fixes the scrollbar gutter issue for DaisyUI
https://github.com/saadeghi/daisyui/issues/3040#issuecomment-2250530354
2025-03-07 15:15:52 +01:00
Huang Xin fd8eeb07c2 fix: set sidebar rtl only for mandatory rtl languages, closes #512 (#519) 2025-03-07 15:06:12 +01:00
Huang Xin 93d1fe6946 release: version 0.9.19 (#508) 2025-03-06 17:30:48 +01:00
Huang Xin 3c52a9e0cb feat: customize foliate view styles with css, closes #167 (#507) 2025-03-06 17:22:01 +01:00
Huang Xin 3056a29860 compat: handle epub files without dc metadata, closes #490 (#506) 2025-03-06 16:44:07 +01:00
Huang Xin bd6738e54c fix: release wakelock when window or tab lost focus, closes #502 (#505) 2025-03-06 16:23:58 +01:00
Huang Xin 8c3f49de25 rtl: support rtl for more widgets in sidebar (#504) 2025-03-06 15:38:29 +01:00
Huang Xin 0f44fff7cd feat: support styled reader UI via custom CSS, closes #240 (#503) 2025-03-06 13:56:43 +01:00
Huang Xin 77bba7bd0c fix: use section info for PDF progress bar, closes #158 (#501) 2025-03-06 11:22:06 +01:00
Huang Xin d30d1c17ae rtl: rtl page layout from settings and book language, closes #359 (#500) 2025-03-06 10:58:32 +01:00
nnecec 088c7d2c49 fix: various tweaks on style and layout (#499) 2025-03-06 10:51:06 +01:00
Huang Xin d0e6f3ad50 fix: ensure the progress bar direction is consistent with the book’s writing direction, closes #451 (#498) 2025-03-06 09:14:33 +01:00
Huang Xin 8a83389164 fix: hide traffic lights when sidebar is invisible on macOS, closes #297 (#497) 2025-03-06 07:14:23 +01:00
Huang Xin 04d07856f5 fix: use client timestamp for updated records, closes #468 (#488) 2025-03-04 14:26:30 +01:00
Huang Xin cc8d0ea457 fix: filter deleted books in parallel read menu, closes #476 (#487) 2025-03-04 07:18:54 +01:00
Huang Xin 29e6af7768 fix: handle hardcoded border color of chapter header for Feedbooks, closes #470 (#485) 2025-03-04 06:02:18 +01:00
Huang Xin bc70d07749 fix: support footnotes with links inside of the lists, closes #472 (#479) 2025-03-03 16:27:03 +01:00
Huang Xin 2db83b8569 ui: responsive dialog size for portrait screens (#473)
* fix: handle network error in sync API

* fix: reasonable default column width for iPad

* ui: responsive dialog size for portrait screens
2025-03-01 17:57:29 +01:00
Huang Xin 2f29295c6c fix: open with new files from file browsers when there is already an instance running, closes #208 (#466) 2025-02-28 08:42:49 +01:00
100 changed files with 2038 additions and 1298 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Feature request
about: Share an idea or suggestion
title: 'FR: [a handful of words describing the FR]'
title: 'FR: describing your feature request'
labels: enhancement
assignees: ''
---
+2 -2
View File
@@ -1,6 +1,6 @@
[submodule "packages/foliate-js"]
path = packages/foliate-js
url = https://github.com/chrox/foliate-js.git
url = https://github.com/readest/foliate-js.git
[submodule "packages/tauri"]
path = packages/tauri
url = https://github.com/chrox/tauri.git
url = https://github.com/tauri-apps/tauri.git
Generated
+675 -651
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -32,7 +32,7 @@
<div align="center">
<a href="https://readest.com" target="_blank">
<img src="./data/screenshots/landing_preview.png" alt="Readest Banner" width="100%" />
<img src="./data/screenshots/landing_all_platforms.png" alt="Readest Banner" width="100%" />
</a>
</div>
@@ -80,7 +80,7 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
![DeepL](./data/screenshots/deepl.png)
![TTS](./data/screenshots/tts_control.png)
![TTS](./data/screenshots/tts_speak_aloud.png)
![Footnote](./data/screenshots/footnote_popover.png)
@@ -205,7 +205,7 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
<a href="https://github.com/readest/readest/graphs/contributors">
<p align="left">
<img width="300" src="https://contrib.rocks/image?repo=readest/readest" alt="A table of avatars from the project's contributors" />
<img width="500" src="https://contrib.rocks/image?repo=readest/readest" alt="A table of avatars from the project's contributors" />
</p>
</a>
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.18",
"version": "0.9.20",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -42,19 +42,19 @@
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.47.7",
"@tauri-apps/api": "2.2.0",
"@tauri-apps/api": "2.3.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-haptics": "^2.2.3",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.5",
"@tauri-apps/plugin-log": "^2.2.3",
"@tauri-apps/plugin-opener": "^2.2.6",
"@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.4.0",
"@tauri-apps/plugin-updater": "^2.5.1",
"@zip.js/zip.js": "^2.7.53",
"clsx": "^2.1.1",
"cors": "^2.8.5",
@@ -76,7 +76,7 @@
"zustand": "5.0.1"
},
"devDependencies": {
"@tauri-apps/cli": "2.2.7",
"@tauri-apps/cli": "2.3.1",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -64,8 +64,6 @@
"Match Case": "مطابقة حالة الأحرف",
"Match Diacritics": "مطابقة التشكيل",
"Match Whole Words": "مطابقة الكلمات الكاملة",
"Maximum Block Size": "أقصى مساحة رأسية",
"Maximum Inline Size": "أقصى مساحة أفقية",
"Maximum Number of Columns": "العدد الأقصى للأعمدة",
"Minimum Font Size": "الحد الأدنى لحجم الخط",
"Misc": "أخرى",
@@ -230,5 +228,12 @@
"Delete Permanently": "حذف نهائي",
"Free Tier": "الفئة المجانية",
"Plus Tier": "فئة Plus",
"Pro Tier": "فئة Pro"
"Pro Tier": "فئة Pro",
"RTL Direction": "الاتجاه من اليمين إلى اليسار",
"Maximum Column Height": "الارتفاع الأقصى للعمود",
"Maximum Column Width": "العرض الأقصى للعمود",
"Continuous Scroll": "التمرير المستمر",
"Fullscreen": "ملء الشاشة",
"No supported files found. Supported formats: {{formats}}": "لم يتم العثور على ملفات مدعومة. الصيغ المدعومة: {{formats}}",
"Drop to Import Books": "اسحب لاستيراد الكتب"
}
@@ -64,8 +64,6 @@
"Match Case": "Groß-/Kleinschreibung beachten",
"Match Diacritics": "Akzente beachten",
"Match Whole Words": "Ganze Wörter",
"Maximum Block Size": "Maximale Blockgröße",
"Maximum Inline Size": "Maximale Zeilengröße",
"Maximum Number of Columns": "Maximale Spaltenanzahl",
"Minimum Font Size": "Minimale Schriftgröße",
"Misc": "Sonstiges",
@@ -230,5 +228,12 @@
"Delete Permanently": "Dauerhaft löschen",
"Free Tier": "Kostenlose Stufe",
"Plus Tier": "Plus-Stufe",
"Pro Tier": "Pro-Stufe"
"Pro Tier": "Pro-Stufe",
"RTL Direction": "RTL-Richtung",
"Maximum Column Height": "Maximale Spaltenhöhe",
"Maximum Column Width": "Maximale Spaltenbreite",
"Continuous Scroll": "Kontinuierliches Scrollen",
"Fullscreen": "Vollbild",
"No supported files found. Supported formats: {{formats}}": "Keine unterstützten Dateien gefunden. Unterstützte Formate: {{formats}}",
"Drop to Import Books": "Zum Importieren von Büchern ablegen"
}
@@ -64,8 +64,6 @@
"Match Case": "Ταίριασμα πεζών-κεφαλαίων",
"Match Diacritics": "Ταίριασμα διακριτικών σημείων",
"Match Whole Words": "Ταίριασμα ολόκληρων λέξεων",
"Maximum Block Size": "Μέγιστο μέγεθος μπλοκ",
"Maximum Inline Size": "Μέγιστο μέγεθος γραμμής",
"Maximum Number of Columns": "Μέγιστος αριθμός στηλών",
"Minimum Font Size": "Ελάχιστο μέγεθος γραμματοσειράς",
"Misc": "Διάφορα",
@@ -230,5 +228,12 @@
"Delete Permanently": "Μόνιμη διαγραφή",
"Free Tier": "Δωρεάν βαθμίδα",
"Plus Tier": "Βαθμίδα Plus",
"Pro Tier": "Βαθμίδα Pro"
"Pro Tier": "Βαθμίδα Pro",
"RTL Direction": "Κατεύθυνση RTL",
"Maximum Column Height": "Μέγιστο ύψος στήλης",
"Maximum Column Width": "Μέγιστο πλάτος στήλης",
"Continuous Scroll": "Συνεχής κύλιση",
"Fullscreen": "Πλήρης οθόνη",
"No supported files found. Supported formats: {{formats}}": "Δεν βρέθηκαν υποστηριζόμενα αρχεία. Υποστηριζόμενες μορφές: {{formats}}",
"Drop to Import Books": "Ρίξτε για εισαγωγή βιβλίων"
}
@@ -64,8 +64,6 @@
"Match Case": "Coincidir mayúsculas y minúsculas",
"Match Diacritics": "Coincidir diacríticos",
"Match Whole Words": "Coincidir palabras completas",
"Maximum Block Size": "Tamaño máximo de bloque",
"Maximum Inline Size": "Tamaño máximo en línea",
"Maximum Number of Columns": "Número máximo de columnas",
"Minimum Font Size": "Tamaño de fuente mínimo",
"Misc": "Varios",
@@ -230,5 +228,12 @@
"Delete Permanently": "Eliminar permanentemente",
"Free Tier": "Nivel gratuito",
"Plus Tier": "Nivel Plus",
"Pro Tier": "Nivel Pro"
"Pro Tier": "Nivel Pro",
"RTL Direction": "Dirección RTL",
"Maximum Column Height": "Altura máxima de columna",
"Maximum Column Width": "Ancho máximo de columna",
"Continuous Scroll": "Desplazamiento continuo",
"Fullscreen": "Pantalla completa",
"No supported files found. Supported formats: {{formats}}": "No se encontraron archivos compatibles. Formatos compatibles: {{formats}}",
"Drop to Import Books": "Soltar para importar libros"
}
@@ -64,8 +64,6 @@
"Match Case": "Respecter la casse",
"Match Diacritics": "Respecter les accents",
"Match Whole Words": "Mots entiers",
"Maximum Block Size": "Taille maximale du bloc",
"Maximum Inline Size": "Taille maximale en ligne",
"Maximum Number of Columns": "Nombre maximal de colonnes",
"Minimum Font Size": "Taille minimale de police",
"Misc": "Divers",
@@ -230,5 +228,12 @@
"Delete Permanently": "Supprimer définitivement",
"Free Tier": "Niveau gratuit",
"Plus Tier": "Niveau Plus",
"Pro Tier": "Niveau Pro"
"Pro Tier": "Niveau Pro",
"RTL Direction": "Direction RTL",
"Maximum Column Height": "Hauteur maximale de colonne",
"Maximum Column Width": "Largeur maximale de colonne",
"Continuous Scroll": "Défilement continu",
"Fullscreen": "Plein écran",
"No supported files found. Supported formats: {{formats}}": "Aucun fichier pris en charge trouvé. Formats pris en charge : {{formats}}",
"Drop to Import Books": "Déposez pour importer des livres"
}
@@ -64,8 +64,6 @@
"Match Case": "केस मैच करें",
"Match Diacritics": "डायाक्रिटिक्स मैच करें",
"Match Whole Words": "पूरे शब्द मैच करें",
"Maximum Block Size": "अधिकतम ब्लॉक साइज़",
"Maximum Inline Size": "अधिकतम इनलाइन साइज़",
"Maximum Number of Columns": "कॉलम की अधिकतम संख्या",
"Minimum Font Size": "न्यूनतम फ़ॉन्ट साइज़",
"Misc": "विविध",
@@ -230,5 +228,12 @@
"Delete Permanently": "स्थायी रूप से हटाएं",
"Free Tier": "फ्री टियर",
"Plus Tier": "प्लस टियर",
"Pro Tier": "प्रो टियर"
"Pro Tier": "प्रो टियर",
"RTL Direction": "RTL दिशा",
"Maximum Column Height": "कॉलम की अधिकतम ऊँचाई",
"Maximum Column Width": "कॉलम की अधिकतम चौड़ाई",
"Continuous Scroll": "निरंतर स्क्रॉल",
"Fullscreen": "पूर्णस्क्रीन",
"No supported files found. Supported formats: {{formats}}": "कोई समर्थित फ़ाइलें नहीं मिलीं। समर्थित प्रारूप: {{formats}}",
"Drop to Import Books": "पुस्तकें आयात करने के लिए छोड़ें"
}
@@ -64,8 +64,6 @@
"Match Case": "Cocokkan Huruf Besar/Kecil",
"Match Diacritics": "Cocokkan Diakritik",
"Match Whole Words": "Cocokkan Kata Utuh",
"Maximum Block Size": "Ukuran Blok Maksimum",
"Maximum Inline Size": "Ukuran Sebaris Maksimum",
"Maximum Number of Columns": "Jumlah Kolom Maksimum",
"Minimum Font Size": "Ukuran Font Minimum",
"Misc": "Lain-lain",
@@ -230,5 +228,12 @@
"Delete Permanently": "Hapus Secara Permanen",
"Free Tier": "Tingkat Gratis",
"Plus Tier": "Tingkat Plus",
"Pro Tier": "Tingkat Pro"
"Pro Tier": "Tingkat Pro",
"RTL Direction": "Arah RTL",
"Maximum Column Height": "Tinggi Kolom Maksimum",
"Maximum Column Width": "Lebar Kolom Maksimum",
"Continuous Scroll": "Gulir Terus",
"Fullscreen": "Layar Penuh",
"No supported files found. Supported formats: {{formats}}": "Tidak ada file yang didukung ditemukan. Format yang didukung: {{formats}}",
"Drop to Import Books": "Jatuhkan untuk Mengimpor Buku"
}
@@ -64,8 +64,6 @@
"Match Case": "Maiuscole/minuscole",
"Match Diacritics": "Corrispondenza diacritici",
"Match Whole Words": "Parole intere",
"Maximum Block Size": "Dimensione massima blocco",
"Maximum Inline Size": "Dimensione massima in linea",
"Maximum Number of Columns": "Numero massimo di colonne",
"Minimum Font Size": "Dimensione minima font",
"Misc": "Varie",
@@ -230,5 +228,12 @@
"Delete Permanently": "Elimina definitivamente",
"Free Tier": "Piano gratuito",
"Plus Tier": "Piano Plus",
"Pro Tier": "Piano Pro"
"Pro Tier": "Piano Pro",
"RTL Direction": "Direzione RTL",
"Maximum Column Height": "Altezza massima colonna",
"Maximum Column Width": "Larghezza massima colonna",
"Continuous Scroll": "Scorrimento continuo",
"Fullscreen": "Schermo intero",
"No supported files found. Supported formats: {{formats}}": "Nessun file supportato trovato. Formati supportati: {{formats}}",
"Drop to Import Books": "Rilascia per importare libri"
}
@@ -64,8 +64,6 @@
"Match Case": "大文字・小文字を区別",
"Match Diacritics": "アクセント記号を区別",
"Match Whole Words": "単語全体を一致",
"Maximum Block Size": "最大ブロックサイズ",
"Maximum Inline Size": "最大インラインサイズ",
"Maximum Number of Columns": "最大列数",
"Minimum Font Size": "最小フォントサイズ",
"Misc": "その他",
@@ -230,5 +228,12 @@
"Delete Permanently": "完全に削除",
"Free Tier": "無料プラン",
"Plus Tier": "Plusプラン",
"Pro Tier": "Proプラン"
"Pro Tier": "Proプラン",
"RTL Direction": "RTL方向",
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列幅",
"Continuous Scroll": "連続スクロール",
"Fullscreen": "全画面",
"No supported files found. Supported formats: {{formats}}": "サポートされているファイルが見つかりません。サポートされている形式:{{formats}}",
"Drop to Import Books": "書籍をインポートするにはドロップ"
}
@@ -64,8 +64,6 @@
"Match Case": "대소문자 구분",
"Match Diacritics": "발음 구별 부호 구분",
"Match Whole Words": "전체 단어 일치",
"Maximum Block Size": "최대 블록 크기",
"Maximum Inline Size": "최대 인라인 크기",
"Maximum Number of Columns": "최대 열 수",
"Minimum Font Size": "최소 글꼴 크기",
"Misc": "기타",
@@ -230,5 +228,12 @@
"Delete Permanently": "영구 삭제",
"Free Tier": "무료 등급",
"Plus Tier": "플러스 등급",
"Pro Tier": "프로 등급"
"Pro Tier": "프로 등급",
"RTL Direction": "RTL 방향",
"Maximum Column Height": "최대 열 높이",
"Maximum Column Width": "최대 열 너비",
"Continuous Scroll": "연속 스크롤",
"Fullscreen": "전체 화면",
"No supported files found. Supported formats: {{formats}}": "지원되는 파일이 없습니다. 지원되는 형식: {{formats}}",
"Drop to Import Books": "책 가져오기 위해 드롭"
}
@@ -64,8 +64,6 @@
"Match Case": "Uwzględnij wielkość liter",
"Match Diacritics": "Uwzględnij znaki diakrytyczne",
"Match Whole Words": "Uwzględnij całe słowa",
"Maximum Block Size": "Maksymalny rozmiar bloku",
"Maximum Inline Size": "Maksymalny rozmiar linii",
"Maximum Number of Columns": "Maksymalna liczba kolumn",
"Minimum Font Size": "Minimalny rozmiar czcionki",
"Misc": "Różne",
@@ -230,5 +228,12 @@
"Delete Permanently": "Usuń trwale",
"Free Tier": "Plan darmowy",
"Plus Tier": "Plan Plus",
"Pro Tier": "Plan Pro"
"Pro Tier": "Plan Pro",
"RTL Direction": "Kierunek RTL",
"Maximum Column Height": "Maksymalna wysokość kolumny",
"Maximum Column Width": "Maksymalna szerokość kolumny",
"Continuous Scroll": "Przewijanie ciągłe",
"Fullscreen": "Pełny ekran",
"No supported files found. Supported formats: {{formats}}": "Nie znaleziono obsługiwanych plików. Obsługiwane formaty: {{formats}}",
"Drop to Import Books": "Upuść, aby zaimportować książki"
}
@@ -64,8 +64,6 @@
"Match Case": "Diferenciar Maiúsculas e Minúsculas",
"Match Diacritics": "Corresponder Acentos",
"Match Whole Words": "Corresponder Palavras Inteiras",
"Maximum Block Size": "Tamanho Máximo do Bloco",
"Maximum Inline Size": "Tamanho Máximo em Linha",
"Maximum Number of Columns": "Número Máximo de Colunas",
"Minimum Font Size": "Tamanho Mínimo da Fonte",
"Misc": "Diversos",
@@ -230,5 +228,12 @@
"Delete Permanently": "Excluir permanentemente",
"Free Tier": "Plano gratuito",
"Plus Tier": "Plano Plus",
"Pro Tier": "Plano Pro"
"Pro Tier": "Plano Pro",
"RTL Direction": "Direção RTL",
"Maximum Column Height": "Altura Máxima da Coluna",
"Maximum Column Width": "Largura Máxima da Coluna",
"Continuous Scroll": "Rolagem Contínua",
"Fullscreen": "Tela Cheia",
"No supported files found. Supported formats: {{formats}}": "Nenhum arquivo suportado encontrado. Formatos suportados: {{formats}}",
"Drop to Import Books": "Arraste para Importar Livros"
}
@@ -64,8 +64,6 @@
"Match Case": "Учитывать регистр",
"Match Diacritics": "Учитывать диакритические знаки",
"Match Whole Words": "Только целые слова",
"Maximum Block Size": "Максимальный размер блока",
"Maximum Inline Size": "Максимальный размер строки",
"Maximum Number of Columns": "Максимальное количество колонок",
"Minimum Font Size": "Минимальный размер шрифта",
"Misc": "Разное",
@@ -230,5 +228,12 @@
"Delete Permanently": "Удалить навсегда",
"Free Tier": "Бесплатный тариф",
"Plus Tier": "Тариф Plus",
"Pro Tier": "Тариф Pro"
"Pro Tier": "Тариф Pro",
"RTL Direction": "Направление RTL",
"Maximum Column Height": "Максимальная высота колонки",
"Maximum Column Width": "Максимальная ширина колонки",
"Continuous Scroll": "Непрерывная прокрутка",
"Fullscreen": "Полноэкранный режим",
"No supported files found. Supported formats: {{formats}}": "Поддерживаемые файлы не найдены. Поддерживаемые форматы: {{formats}}",
"Drop to Import Books": "Перетащите сюда книги для импорта"
}
@@ -64,8 +64,6 @@
"Match Case": "Büyük/Küçük Harf Eşleştir",
"Match Diacritics": "Aksan İşaretlerini Eşleştir",
"Match Whole Words": "Tam Kelimeleri Eşleştir",
"Maximum Block Size": "Maksimum Blok Boyutu",
"Maximum Inline Size": "Maksimum Satır İçi Boyut",
"Maximum Number of Columns": "Maksimum Sütun Sayısı",
"Minimum Font Size": "Minimum Yazı Boyutu",
"Misc": "Diğer",
@@ -230,5 +228,12 @@
"Delete Permanently": "Kalıcı Olarak Sil",
"Free Tier": "Ücretsiz Paket",
"Plus Tier": "Plus Paket",
"Pro Tier": "Pro Paket"
"Pro Tier": "Pro Paket",
"RTL Direction": "RTL Yönü",
"Maximum Column Height": "Maksimum Sütun Yüksekliği",
"Maximum Column Width": "Maksimum Sütun Genişliği",
"Continuous Scroll": "Sürekli Kaydırma",
"Fullscreen": "Tam Ekran",
"No supported files found. Supported formats: {{formats}}": "Desteklenen dosya bulunamadı. Desteklenen formatlar: {{formats}}",
"Drop to Import Books": "Kitapları İçe Aktarmak İçin Bırak"
}
@@ -64,8 +64,6 @@
"Match Case": "Враховувати регістр",
"Match Diacritics": "Враховувати діакритичні знаки",
"Match Whole Words": "Шукати цілі слова",
"Maximum Block Size": "Максимальний розмір блоку",
"Maximum Inline Size": "Максимальний розмір рядка",
"Maximum Number of Columns": "Максимальна кількість колонок",
"Minimum Font Size": "Мінімальний розмір шрифту",
"Misc": "Різне",
@@ -230,5 +228,12 @@
"Delete Permanently": "Видалити назавжди",
"Free Tier": "Безкоштовний тариф",
"Plus Tier": "Тариф Plus",
"Pro Tier": "Тариф Pro"
"Pro Tier": "Тариф Pro",
"RTL Direction": "Напрямок RTL",
"Maximum Column Height": "Максимальна висота колонки",
"Maximum Column Width": "Максимальна ширина колонки",
"Continuous Scroll": "Постійна прокрутка",
"Fullscreen": "Повноекранний",
"No supported files found. Supported formats: {{formats}}": "Не знайдено підтримуваних файлів. Підтримувані формати: {{formats}}",
"Drop to Import Books": "Перетягніть для імпорту книг"
}
@@ -64,8 +64,6 @@
"Match Case": "Phân biệt chữ hoa/thường",
"Match Diacritics": "Phân biệt dấu",
"Match Whole Words": "Khớp toàn bộ từ",
"Maximum Block Size": "Kích thước khối tối đa",
"Maximum Inline Size": "Kích thước nội tuyến tối đa",
"Maximum Number of Columns": "Số cột tối đa",
"Minimum Font Size": "Cỡ chữ tối thiểu",
"Misc": "Khác",
@@ -230,5 +228,12 @@
"Delete Permanently": "Xóa vĩnh viễn",
"Free Tier": "Gói miễn phí",
"Plus Tier": "Gói Plus",
"Pro Tier": "Gói Pro"
"Pro Tier": "Gói Pro",
"RTL Direction": "Hướng RTL",
"Maximum Column Height": "Chiều cao cột tối đa",
"Maximum Column Width": "Chiều rộng cột tối đa",
"Continuous Scroll": "Cuộn liên tục",
"Fullscreen": "Toàn màn hình",
"No supported files found. Supported formats: {{formats}}": "Không tìm thấy tệp được hỗ trợ. Định dạng được hỗ trợ: {{formats}}",
"Drop to Import Books": "Kéo và thả để nhập sách"
}
@@ -64,8 +64,6 @@
"Match Case": "匹配大小写",
"Match Diacritics": "匹配重音符号",
"Match Whole Words": "匹配整个单词",
"Maximum Block Size": "内容最大宽度",
"Maximum Inline Size": "每栏最大宽度",
"Maximum Number of Columns": "分栏数",
"Minimum Font Size": "最小字号",
"Misc": "杂项",
@@ -230,5 +228,12 @@
"Delete Permanently": "永久删除",
"Free Tier": "免费版",
"Plus Tier": "增强版",
"Pro Tier": "专业版"
"Pro Tier": "专业版",
"RTL Direction": "从右向左",
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列宽",
"Continuous Scroll": "连续滚动",
"Fullscreen": "全屏",
"No supported files found. Supported formats: {{formats}}": "未找到支持的文件。支持的格式:{{formats}}",
"Drop to Import Books": "拖放导入书籍"
}
@@ -64,8 +64,6 @@
"Match Case": "匹配大小寫",
"Match Diacritics": "匹配重音符號",
"Match Whole Words": "匹配整個單詞",
"Maximum Block Size": "內容最大寬度",
"Maximum Inline Size": "每欄最大寬度",
"Maximum Number of Columns": "分欄數",
"Minimum Font Size": "最小字號",
"Misc": "雜項",
@@ -230,5 +228,12 @@
"Delete Permanently": "永久刪除",
"Free Tier": "免費方案",
"Plus Tier": "Plus方案",
"Pro Tier": "專業方案"
"Pro Tier": "專業方案",
"RTL Direction": "從右向左",
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列寬",
"Continuous Scroll": "連續滾動",
"Fullscreen": "全螢幕",
"No supported files found. Supported formats: {{formats}}": "未找到支援的檔案。支援的格式:{{formats}}",
"Drop to Import Books": "拖放導入書籍"
}
+18
View File
@@ -1,5 +1,23 @@
{
"releases": {
"0.9.20": {
"date": "2025-03-10",
"notes": [
"Fix column height in vertical layout on mobile",
"Fix drag handle height not constant on mobile",
"Add fullscreen option on desktop",
"Add drag and drop to import books on desktop",
"Various fixes and enhancements on updater, footerbar and note"
]
},
"0.9.19": {
"date": "2025-03-07",
"notes": [
"Support custom CSS for Reader UI",
"Initial support of RTL layout for Arabic and Hebrew books",
"Various fixes and enhancements on layout and sync"
]
},
"0.9.18": {
"date": "2025-02-26",
"notes": [
@@ -62,6 +62,7 @@
"core:window:allow-is-maximized",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"core:window:allow-set-fullscreen",
"shell:default",
"process:default",
"process:allow-exit",
+4
View File
@@ -10,6 +10,8 @@ extern crate objc;
mod menu;
#[cfg(target_os = "macos")]
mod traffic_light;
#[cfg(target_os = "macos")]
use traffic_light::set_traffic_lights;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
@@ -98,6 +100,8 @@ pub fn run() {
start_server,
download_file,
upload_file,
#[cfg(target_os = "macos")]
set_traffic_lights,
#[cfg(desktop)]
list_fonts
])
+38 -12
View File
@@ -1,13 +1,14 @@
use objc::{msg_send, sel, sel_impl};
use rand::{distributions::Alphanumeric, Rng};
use tauri::Emitter;
use tauri::{
command,
plugin::{Builder, TauriPlugin},
Runtime, Window,
}; // 0.8
Emitter, Runtime, Window,
};
const WINDOW_CONTROL_PAD_X: f64 = 10.0;
const WINDOW_CONTROL_PAD_Y: f64 = 22.0;
static mut WINDOW_CONTROL_PAD_X: f64 = 10.0;
static mut WINDOW_CONTROL_PAD_Y: f64 = 22.0;
static mut TRAFFIC_LIGHTS_VISIBLE: bool = true;
struct UnsafeWindowHandle(*mut std::ffi::c_void);
unsafe impl Send for UnsafeWindowHandle {}
@@ -30,7 +31,24 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) {
#[command]
pub fn set_traffic_lights(window: Window, visible: bool, x: f64, y: f64) {
unsafe {
TRAFFIC_LIGHTS_VISIBLE = visible;
WINDOW_CONTROL_PAD_X = x;
WINDOW_CONTROL_PAD_Y = y;
position_traffic_lights(
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
TRAFFIC_LIGHTS_VISIBLE,
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
}
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, visible: bool, x: f64, y: f64) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let ns_window = ns_window_handle.0 as cocoa::base::id;
@@ -45,7 +63,10 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64)
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_frame_height = button_height + y;
if !visible {
title_bar_frame_height = 0.0;
}
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
@@ -77,11 +98,14 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
use std::ffi::c_void;
// Do the initial positioning
position_traffic_lights(
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
unsafe {
position_traffic_lights(
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
TRAFFIC_LIGHTS_VISIBLE,
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
}
// Ensure they stay in place while resizing the window.
fn with_window_state<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>(
@@ -127,6 +151,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
#[cfg(target_os = "macos")]
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
TRAFFIC_LIGHTS_VISIBLE,
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
@@ -258,6 +283,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
let id = state.window.ns_window().expect("Failed to emit event") as id;
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
TRAFFIC_LIGHTS_VISIBLE,
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
+3 -3
View File
@@ -12,8 +12,8 @@ import { IoArrowBack } from 'react-icons/io5';
import { useAuth } from '@/context/AuthContext';
import { supabase } from '@/utils/supabase';
import { useTheme } from '@/hooks/useTheme';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { isTauriAppPlatform } from '@/services/environment';
@@ -61,7 +61,7 @@ export default function AuthPage() {
const router = useRouter();
const { login } = useAuth();
const { envConfig, appService } = useEnv();
const { isDarkMode } = useTheme();
const { isDarkMode } = useThemeStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [port, setPort] = useState<number | null>(null);
const isOAuthServerRunning = useRef(false);
@@ -129,7 +129,7 @@ export default function AuthPage() {
};
const handleOAuthUrl = async (url: string) => {
console.log('Received OAuth URL:', url);
console.log('Handle OAuth URL:', url);
const hashMatch = url.match(/#(.*)/);
if (hashMatch) {
const hash = hashMatch[1];
@@ -21,9 +21,9 @@ interface BookshelfProps {
libraryBooks: Book[];
isSelectMode: boolean;
handleImportBooks: () => void;
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
handleBookDelete: (book: Book) => void;
handleBookUpload: (book: Book) => Promise<boolean>;
handleBookDownload: (book: Book) => Promise<boolean>;
handleBookDelete: (book: Book) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
booksTransferProgress: { [key: string]: number | null };
@@ -143,7 +143,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
<div
className={clsx(
'transform-wrapper grid flex-1 gap-x-4 sm:gap-x-0',
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8',
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
)}
>
{currentBookshelfItems.map((item, index) => (
@@ -72,9 +72,9 @@ interface BookshelfItemProps {
transferProgress: number | null;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
toggleSelection: (hash: string) => void;
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
handleBookDelete: (book: Book) => void;
handleBookUpload: (book: Book) => Promise<boolean>;
handleBookDownload: (book: Book) => Promise<boolean>;
handleBookDelete: (book: Book) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
}
@@ -109,9 +109,8 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
let available = false;
const loadingTimeout = setTimeout(() => setLoading(true), 200);
try {
await handleBookDownload(book);
available = await handleBookDownload(book);
updateBook(envConfig, book);
available = true;
} finally {
if (loadingTimeout) clearTimeout(loadingTimeout);
setLoading(false);
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { FaSearch } from 'react-icons/fa';
import { PiPlus } from 'react-icons/pi';
@@ -10,8 +10,8 @@ import { MdArrowBackIosNew } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { navigateToLibrary } from '@/utils/nav';
import useTrafficLight from '@/hooks/useTrafficLight';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
@@ -33,7 +33,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const router = useRouter();
const searchParams = useSearchParams();
const { appService } = useEnv();
const { isTrafficLightVisible } = useTrafficLight();
const {
isTrafficLightVisible,
initializeTrafficLightStore,
initializeTrafficLightListeners,
setTrafficLightVisibility,
cleanupTrafficLightListeners,
} = useTrafficLightStore();
const headerRef = useRef<HTMLDivElement>(null);
const iconSize16 = useResponsiveSize(16);
const iconSize20 = useResponsiveSize(20);
@@ -42,6 +49,18 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
onToggleSelectMode,
});
useEffect(() => {
if (!appService?.hasTrafficLight) return;
initializeTrafficLightStore(appService);
initializeTrafficLightListeners();
setTrafficLightVisibility(true);
return () => {
cleanupTrafficLightListeners();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible;
const isInGroupView = !!searchParams?.get('group');
@@ -14,6 +14,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getStoragePlanData } from '@/utils/access';
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
import { tauriHandleToggleFullScreen } from '@/utils/window';
import { QuotaType } from '@/types/user';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
@@ -25,7 +26,7 @@ interface BookMenuProps {
const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { token, user } = useAuth();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
@@ -57,6 +58,11 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const handleFullScreen = () => {
tauriHandleToggleFullScreen();
setIsDropdownOpen?.(false);
};
const toggleAutoUploadBooks = () => {
settings.autoUpload = !settings.autoUpload;
setSettings(settings);
@@ -157,6 +163,9 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
onClick={toggleScreenWakeLock}
/>
<hr className='border-base-200 my-1' />
{appService?.hasRoundedWindow && (
<MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />
)}
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
+138 -16
View File
@@ -2,8 +2,8 @@
import clsx from 'clsx';
import * as React from 'react';
import { useState, useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useState, useRef, useEffect, Suspense } from 'react';
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation';
import { Book } from '@/types/book';
import { AppService } from '@/types/system';
@@ -17,17 +17,20 @@ import { isTauriAppPlatform, hasUpdater } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
import { getCurrentWebview } from '@tauri-apps/api/webview';
import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useTheme } from '@/hooks/useTheme';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
import { useTheme } from '@/hooks/useTheme';
import { useDemoBooks } from './hooks/useDemoBooks';
import { useBooksSync } from './hooks/useBooksSync';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { tauriQuitApp } from '@/utils/window';
import { AboutWindow } from '@/components/AboutWindow';
@@ -37,8 +40,14 @@ import LibraryHeader from './components/LibraryHeader';
import Bookshelf from './components/Bookshelf';
import BookDetailModal from '@/components/BookDetailModal';
import useShortcuts from '@/hooks/useShortcuts';
import DropIndicator from '@/components/DropIndicator';
const LibraryPage = () => {
const LibraryPageWithSearchParams = () => {
const searchParams = useSearchParams();
return <LibraryPageContent searchParams={searchParams} />;
};
const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchParams | null }) => {
const router = useRouter();
const { envConfig, appService } = useEnv();
const { token, user } = useAuth();
@@ -47,10 +56,11 @@ const LibraryPage = () => {
updateBook,
setLibrary,
checkOpenWithBooks,
clearOpenWithBooks,
setCheckOpenWithBooks,
} = useLibraryStore();
const _ = useTranslation();
const { updateAppTheme } = useTheme();
useTheme();
const { updateAppTheme } = useThemeStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
@@ -60,8 +70,12 @@ const LibraryPage = () => {
const [booksTransferProgress, setBooksTransferProgress] = useState<{
[key: string]: number | null;
}>({});
const [isDragging, setIsDragging] = useState(false);
const demoBooks = useDemoBooks();
const containerRef = useRef<HTMLDivElement>(null);
const pageRef = useRef<HTMLDivElement>(null);
useOpenWithBooks();
const { pullLibrary, pushLibrary } = useBooksSync({
onSyncStart: () => setLoading(true),
@@ -94,6 +108,87 @@ const LibraryPage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
const handleDropedFiles = async (files: File[] | string[]) => {
if (files.length === 0) return;
const supportedFiles = files.filter((file) => {
let fileExt;
if (typeof file === 'string') {
fileExt = file.split('.').pop()?.toLowerCase();
} else {
fileExt = file.name.split('.').pop()?.toLowerCase();
}
return FILE_ACCEPT_FORMATS.includes(`.${fileExt}`);
});
if (supportedFiles.length === 0) {
eventDispatcher.dispatch('toast', {
message: _('No supported files found. Supported formats: {{formats}}', {
formats: FILE_ACCEPT_FORMATS,
}),
type: 'error',
});
return;
}
if (appService?.hasHaptics) {
impactFeedback('medium');
}
await importBooks(supportedFiles);
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement> | DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (event: React.DragEvent<HTMLDivElement> | DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
};
const handleDrop = async (event: React.DragEvent<HTMLDivElement> | DragEvent) => {
event.preventDefault();
event.stopPropagation();
setIsDragging(false);
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
const files = Array.from(event.dataTransfer.files);
handleDropedFiles(files);
}
};
useEffect(() => {
const libraryPage = document.querySelector('.library-page');
libraryPage?.addEventListener('dragover', handleDragOver as unknown as EventListener);
libraryPage?.addEventListener('dragleave', handleDragLeave as unknown as EventListener);
libraryPage?.addEventListener('drop', handleDrop as unknown as EventListener);
if (isTauriAppPlatform()) {
const unlisten = getCurrentWebview().onDragDropEvent((event) => {
if (event.payload.type === 'over') {
setIsDragging(true);
} else if (event.payload.type === 'drop') {
setIsDragging(false);
handleDropedFiles(event.payload.paths);
} else {
setIsDragging(false);
}
});
return () => {
unlisten.then((fn) => fn());
};
}
return () => {
libraryPage?.removeEventListener('dragover', handleDragOver as unknown as EventListener);
libraryPage?.removeEventListener('dragleave', handleDragLeave as unknown as EventListener);
libraryPage?.removeEventListener('drop', handleDrop as unknown as EventListener);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageRef.current]);
const processOpenWithFiles = React.useCallback(
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
const bookIds: string[] = [];
@@ -113,7 +208,9 @@ const LibraryPage = () => {
console.log('Opening books:', bookIds);
if (bookIds.length > 0) {
navigateToReader(router, bookIds);
setTimeout(() => {
navigateToReader(router, bookIds);
}, 0);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -148,7 +245,7 @@ const LibraryPage = () => {
if (checkOpenWithBooks && isTauriAppPlatform()) {
await handleOpenWithBooks(appService, libraryBooks);
} else {
clearOpenWithBooks();
setCheckOpenWithBooks(false);
setLibrary(libraryBooks);
}
@@ -163,7 +260,7 @@ const LibraryPage = () => {
if (openWithFiles.length > 0) {
await processOpenWithFiles(appService, openWithFiles, libraryBooks);
} else {
clearOpenWithBooks();
setCheckOpenWithBooks(false);
setLibrary(libraryBooks);
}
};
@@ -171,10 +268,11 @@ const LibraryPage = () => {
initLogin();
initLibrary();
return () => {
clearOpenWithBooks();
setCheckOpenWithBooks(false);
isInitiating.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [searchParams]);
useEffect(() => {
if (demoBooks.length > 0 && libraryLoaded) {
@@ -193,7 +291,7 @@ const LibraryPage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [demoBooks, libraryLoaded]);
const importBooks = async (files: [string | File]) => {
const importBooks = async (files: (string | File)[]) => {
setLoading(true);
const failedFiles = [];
for (const file of files) {
@@ -262,17 +360,18 @@ const LibraryPage = () => {
title: book.title,
}),
});
return true;
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('Not authenticated') && settings.keepLogin) {
navigateToLogin(router);
return;
return false;
} else if (err.message.includes('Insufficient storage quota')) {
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Insufficient storage quota'),
});
return;
return false;
}
}
eventDispatcher.dispatch('toast', {
@@ -281,6 +380,7 @@ const LibraryPage = () => {
title: book.title,
}),
});
return false;
}
};
@@ -297,6 +397,7 @@ const LibraryPage = () => {
title: book.title,
}),
});
return true;
} catch {
eventDispatcher.dispatch('toast', {
message: _('Failed to download book: {{title}}', {
@@ -304,6 +405,7 @@ const LibraryPage = () => {
}),
type: 'error',
});
return false;
}
};
@@ -319,6 +421,7 @@ const LibraryPage = () => {
title: book.title,
}),
});
return true;
} catch {
eventDispatcher.dispatch('toast', {
message: _('Failed to delete book: {{title}}', {
@@ -326,6 +429,7 @@ const LibraryPage = () => {
}),
type: 'error',
});
return false;
}
};
@@ -379,6 +483,7 @@ const LibraryPage = () => {
return (
<div
ref={pageRef}
className={clsx(
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
@@ -402,11 +507,13 @@ const LibraryPage = () => {
<div
ref={containerRef}
className={clsx(
'scroll-container mt-12 flex-grow overflow-auto px-4 sm:px-2',
'scroll-container drop-zone mt-12 flex-grow overflow-auto px-4 sm:px-2',
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
isDragging && 'drag-over',
)}
>
<DropIndicator />
<Bookshelf
libraryBooks={libraryBooks}
isSelectMode={isSelectMode}
@@ -420,7 +527,8 @@ const LibraryPage = () => {
/>
</div>
) : (
<div className='hero h-screen items-center justify-center'>
<div className='hero drop-zone h-screen items-center justify-center'>
<DropIndicator />
<div className='hero-content text-neutral-content text-center'>
<div className='max-w-md'>
<h1 className='mb-5 text-5xl font-bold'>{_('Your Library')}</h1>
@@ -449,4 +557,18 @@ const LibraryPage = () => {
);
};
const LibraryPage = () => {
return (
<Suspense
fallback={
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<Spinner loading />
</div>
}
>
<LibraryPageWithSearchParams />
</Suspense>
);
};
export default LibraryPage;
@@ -85,14 +85,20 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<HintInfo bookKey={bookKey} gapRight={marginGap} />
<PageInfoView
bookFormat={book.format}
section={section ?? null}
pageinfo={pageinfo ?? null}
section={section}
pageinfo={pageinfo}
gapRight={marginGap}
/>
</>
)}
<Annotator bookKey={bookKey} />
<FooterBar bookKey={bookKey} pageinfo={pageinfo} isHoveredAnim={false} />
<FooterBar
bookKey={bookKey}
bookFormat={book.format}
section={section}
pageinfo={pageinfo}
isHoveredAnim={false}
/>
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
</div>
);
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { BookDoc, getDirection } from '@/libs/document';
import { BookConfig } from '@/types/book';
import { FoliateView, wrappedFoliateView } from '@/types/view';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useParallelViewStore } from '@/store/parallelViewStore';
import { useClickEvent, useTouchEvent } from '../hooks/useIframeEvents';
@@ -10,9 +11,8 @@ import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
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 { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
import { useUICSS } from '@/hooks/useUICSS';
import {
handleKeydown,
handleMousedown,
@@ -23,6 +23,7 @@ import {
handleTouchMove,
handleTouchEnd,
} from '../utils/iframeEventHandlers';
import { getMaxInlineSize } from '@/utils/config';
const FoliateViewer: React.FC<{
bookKey: string;
@@ -35,7 +36,8 @@ const FoliateViewer: React.FC<{
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
const { getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { themeCode } = useTheme();
const { themeCode } = useThemeStore();
const viewSettings = getViewSettings(bookKey)!;
const [toastMessage, setToastMessage] = useState('');
useEffect(() => {
@@ -43,6 +45,7 @@ const FoliateViewer: React.FC<{
return () => clearTimeout(timer);
}, [toastMessage]);
useUICSS(bookKey, viewSettings);
useProgressSync(bookKey);
useProgressAutoSave(bookKey);
@@ -58,7 +61,9 @@ const FoliateViewer: React.FC<{
if (detail.doc) {
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
const viewSettings = getViewSettings(bookKey)!;
viewSettings.vertical = writingDir?.vertical || false;
viewSettings.vertical =
writingDir?.vertical || viewSettings.writingMode.includes('vertical') || false;
viewSettings.rtl = writingDir?.rtl || viewSettings.writingMode.includes('rl') || false;
setViewSettings(bookKey, viewSettings);
if (viewSettings.scrolled && shouldAutoHideScrollbar) {
handleScrollbarAutoHide(detail.doc);
@@ -86,7 +91,8 @@ const FoliateViewer: React.FC<{
if (detail.reason === 'scroll') {
const renderer = viewRef.current?.renderer;
if (renderer) {
const viewSettings = getViewSettings(bookKey)!;
if (renderer && viewSettings.continuousScroll) {
if (renderer.start <= 0) {
viewRef.current?.prev(1);
// sometimes viewSize has subpixel value that the end never reaches
@@ -120,7 +126,7 @@ const FoliateViewer: React.FC<{
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings, themeCode));
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode]);
@@ -133,31 +139,34 @@ const FoliateViewer: React.FC<{
console.log('Opening book', bookKey);
await import('foliate-js/view.js');
const view = wrappedFoliateView(document.createElement('foliate-view') as FoliateView);
view.id = `foliate-view-${bookKey}`;
document.body.append(view);
containerRef.current?.appendChild(view);
const writingMode = viewSettings.writingMode;
if (writingMode) {
const settingsDir = getBookDirFromWritingMode(writingMode);
const languageDir = getBookDirFromLanguage(bookDoc.metadata.language);
if (settingsDir !== 'auto') {
bookDoc.dir = settingsDir;
} else if (languageDir !== 'auto') {
bookDoc.dir = languageDir;
}
}
await view.open(bookDoc);
// make sure we can listen renderer events after opening book
viewRef.current = view;
setFoliateView(bookKey, view);
const viewSettings = getViewSettings(bookKey)!;
view.renderer.setStyles?.(getStyles(viewSettings, themeCode));
const writingMode = viewSettings.writingMode;
if (writingMode) {
view.book.dir = getBookDirFromWritingMode(writingMode);
}
view.renderer.setStyles?.(getStyles(viewSettings));
const isScrolled = viewSettings.scrolled!;
const marginPx = viewSettings.marginPx!;
const gapPercent = viewSettings.gapPercent!;
const animated = viewSettings.animated!;
const maxColumnCount = viewSettings.maxColumnCount!;
const maxInlineSize =
maxColumnCount === 1 || isScrolled
? ONE_COLUMN_MAX_INLINE_SIZE
: viewSettings.maxInlineSize!;
const maxInlineSize = getMaxInlineSize(viewSettings);
const maxBlockSize = viewSettings.maxBlockSize!;
if (animated) {
view.renderer.setAttribute('animated', '');
@@ -9,21 +9,32 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import { PageInfo } from '@/types/book';
import Button from '@/components/Button';
interface FooterBarProps {
bookKey: string;
pageinfo: { current: number; total: number } | undefined;
bookFormat: string;
section?: PageInfo;
pageinfo?: PageInfo;
isHoveredAnim: boolean;
}
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
const FooterBar: React.FC<FooterBarProps> = ({
bookKey,
bookFormat,
section,
pageinfo,
isHoveredAnim,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { hoveredBookKey, setHoveredBookKey, getView, getProgress } = useReaderStore();
const { hoveredBookKey, setHoveredBookKey, getView, getProgress, getViewSettings } =
useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const view = getView(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const handleProgressChange = (event: React.ChangeEvent) => {
const newProgress = parseInt((event.target as HTMLInputElement).value, 10);
@@ -56,53 +67,73 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
}
};
const pageinfoValid = pageinfo && pageinfo.total > 0 && pageinfo.current >= 0;
const progressFraction = pageinfoValid ? pageinfo.current / pageinfo.total : 0;
const isVisible = hoveredBookKey === bookKey;
const progressInfo = bookFormat === 'PDF' ? section : pageinfo;
const progressValid = !!progressInfo;
const progressFraction = progressValid ? (progressInfo!.current + 1) / progressInfo!.total : 0;
return (
<div
className={clsx(
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
'shadow-xs bg-base-100 transition-opacity duration-300',
appService?.hasSafeAreaInset && 'pb-[env(safe-area-inset-bottom)]',
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
)}
onMouseEnter={() => setHoveredBookKey(bookKey)}
onMouseLeave={() => setHoveredBookKey('')}
>
<div className='hidden sm:flex'>
<Button icon={<RiArrowLeftWideLine />} onClick={handleGoPrev} tooltip={_('Go Left')} />
<>
<div
className='absolute bottom-0 left-0 z-10 h-12 w-full'
onMouseEnter={() => setHoveredBookKey(bookKey)}
onTouchStart={() => setHoveredBookKey(bookKey)}
/>
<div
className={clsx(
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
'shadow-xs bg-base-100 transition-opacity duration-300',
appService?.hasSafeAreaInset && 'pb-[env(safe-area-inset-bottom)]',
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
viewSettings?.vertical && viewSettings?.scrolled ? 'mb-3 !h-6' : '',
isVisible ? `pointer-events-auto opacity-100` : `pointer-events-none opacity-0`,
)}
dir={viewSettings?.rtl ? 'rtl' : 'ltr'}
onMouseLeave={() => setHoveredBookKey('')}
aria-hidden={!isVisible}
>
<div className='hidden sm:flex'>
<Button
icon={viewSettings?.rtl ? <RiArrowRightWideLine /> : <RiArrowLeftWideLine />}
onClick={viewSettings?.rtl ? handleGoNext : handleGoPrev}
tooltip={viewSettings?.rtl ? _('Go Right') : _('Go Left')}
/>
</div>
<Button
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
onClick={viewSettings?.rtl ? handleGoForward : handleGoBack}
tooltip={viewSettings?.rtl ? _('Go Forward') : _('Go Back')}
disabled={viewSettings?.rtl ? !view?.history.canGoForward : !view?.history.canGoBack}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
onClick={viewSettings?.rtl ? handleGoBack : handleGoForward}
tooltip={viewSettings?.rtl ? _('Go Back') : _('Go Forward')}
disabled={viewSettings?.rtl ? !view?.history.canGoBack : !view?.history.canGoForward}
/>
<span className='mx-2 text-center text-sm'>
{progressValid ? `${Math.round(progressFraction * 100)}%` : ''}
</span>
<input
type='range'
className='text-base-content mx-2 w-full'
min={0}
max={100}
value={progressValid ? progressFraction * 100 : 0}
onChange={(e) => handleProgressChange(e)}
/>
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
<div className='hidden sm:flex'>
<Button
icon={viewSettings?.rtl ? <RiArrowLeftWideLine /> : <RiArrowRightWideLine />}
onClick={viewSettings?.rtl ? handleGoPrev : handleGoNext}
tooltip={viewSettings?.rtl ? _('Go Left') : _('Go Right')}
/>
</div>
</div>
<Button
icon={<RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={<RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
disabled={!view?.history.canGoForward}
/>
<span className='mx-2 text-center text-sm'>
{pageinfoValid ? `${Math.round(progressFraction * 100)}%` : ''}
</span>
<input
type='range'
className='text-base-content mx-2 w-full'
min={0}
max={100}
value={pageinfoValid ? progressFraction * 100 : 0}
onChange={(e) => handleProgressChange(e)}
/>
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
<div className='hidden sm:flex'>
<Button icon={<RiArrowRightWideLine />} onClick={handleGoNext} tooltip={_('Go Right')} />
</div>
</div>
</>
);
};
@@ -1,9 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import { BookDoc } from '@/libs/document';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useTheme } from '@/hooks/useTheme';
import { getFootnoteStyles, getStyles } from '@/utils/style';
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
@@ -27,7 +27,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const [popupPosition, setPopupPosition] = useState<Position | null>();
const [showPopup, setShowPopup] = useState(false);
const { getView, getViewSettings } = useReaderStore();
const { themeCode } = useTheme();
const { themeCode } = useThemeStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const footnoteHandler = new FootnoteHandler();
@@ -1,12 +1,12 @@
import clsx from 'clsx';
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import useTrafficLight from '@/hooks/useTrafficLight';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SidebarToggler from './SidebarToggler';
@@ -34,7 +34,13 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}) => {
const { appService } = useEnv();
const headerRef = useRef<HTMLDivElement>(null);
const { isTrafficLightVisible } = useTrafficLight();
const {
isTrafficLightVisible,
setTrafficLightVisibility,
initializeTrafficLightStore,
initializeTrafficLightListeners,
cleanupTrafficLightListeners,
} = useTrafficLightStore();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { hoveredBookKey, setHoveredBookKey, bookKeys } = useReaderStore();
const { isSideBarVisible } = useSidebarStore();
@@ -45,6 +51,25 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
if (!isOpen) setHoveredBookKey('');
};
useEffect(() => {
if (!appService?.hasTrafficLight) return;
initializeTrafficLightStore(appService);
initializeTrafficLightListeners();
setTrafficLightVisibility(true);
return () => {
cleanupTrafficLightListeners();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!appService?.hasTrafficLight) return;
setTrafficLightVisibility(isSideBarVisible);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSideBarVisible]);
return (
<div
ref={headerRef}
@@ -1,8 +1,8 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { eventDispatcher } from '@/utils/event';
import useTrafficLight from '@/hooks/useTrafficLight';
interface SectionInfoProps {
bookKey: string;
@@ -11,7 +11,7 @@ interface SectionInfoProps {
const HintInfo: React.FC<SectionInfoProps> = ({ bookKey, gapRight }) => {
const { isSideBarVisible } = useSidebarStore();
const { isTrafficLightVisible } = useTrafficLight();
const { isTrafficLightVisible } = useTrafficLightStore();
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
const hintTimeout = useRef(2000);
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -4,8 +4,8 @@ import { PageInfo } from '@/types/book';
interface PageInfoProps {
bookFormat: string;
section: PageInfo | null;
pageinfo: PageInfo | null;
section?: PageInfo;
pageinfo?: PageInfo;
gapRight: string;
}
@@ -6,6 +6,7 @@ import { useEffect, Suspense, useRef } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useThemeStore } from '@/store/themeStore';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
@@ -16,10 +17,11 @@ import ReaderContent from './ReaderContent';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { envConfig, appService } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { library, setLibrary } = useLibraryStore();
const { getVisibleLibrary, setLibrary } = useLibraryStore();
const isInitiating = useRef(false);
const { updateAppTheme } = useTheme();
const { updateAppTheme } = useThemeStore();
useTheme();
useScreenWakeLock(settings.screenWakeLock);
useEffect(() => {
@@ -38,7 +40,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
}, []);
return (
library.length > 0 &&
getVisibleLibrary().length > 0 &&
settings.globalReadSettings && (
<div
className={clsx(
@@ -2,7 +2,7 @@ import clsx from 'clsx';
import React from 'react';
import { useSidebarStore } from '@/store/sidebarStore';
import useTrafficLight from '@/hooks/useTrafficLight';
import { useTrafficLightStore } from '@/store/trafficLightStore';
interface SectionInfoProps {
section?: string;
@@ -11,7 +11,7 @@ interface SectionInfoProps {
const SectionInfo: React.FC<SectionInfoProps> = ({ section, gapLeft }) => {
const { isSideBarVisible } = useSidebarStore();
const { isTrafficLightVisible } = useTrafficLight();
const { isTrafficLightVisible } = useTrafficLightStore();
return (
<div
className={clsx(
@@ -5,17 +5,16 @@ 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,
ZOOM_STEP,
} from '@/services/constants';
import MenuItem from '@/components/MenuItem';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme, ThemeMode } from '@/hooks/useTheme';
import { ThemeMode } from '@/styles/themes';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { tauriHandleToggleFullScreen } from '@/utils/window';
import MenuItem from '@/components/MenuItem';
interface ViewMenuProps {
bookKey: string;
@@ -29,19 +28,18 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
onSetSettingsDialogOpen,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getView, getViews, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const { themeMode, isDarkMode, themeCode, updateThemeMode } = useTheme();
const { themeMode, themeCode, setThemeMode } = useThemeStore();
const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled);
const [isInvertedColors, setInvertedColors] = useState(viewSettings!.invert);
const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!);
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);
const openFontLayoutMenu = () => {
setIsDropdownOpen?.(false);
@@ -51,40 +49,39 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
const cycleThemeMode = () => {
const nextMode: ThemeMode =
themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
updateThemeMode(nextMode);
setThemeMode(nextMode);
};
const handleFullScreen = () => {
tauriHandleToggleFullScreen();
setIsDropdownOpen?.(false);
};
useEffect(() => {
getViews().forEach((view) => {
view.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
view.renderer.setStyles?.(getStyles(viewSettings!));
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode]);
useEffect(() => {
viewSettings!.scrolled = isScrolledMode;
getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
getView(bookKey)?.renderer.setAttribute(
'max-inline-size',
`${viewSettings.maxColumnCount === 1 || isScrolledMode ? ONE_COLUMN_MAX_INLINE_SIZE : viewSettings.maxInlineSize}px`,
`${getMaxInlineSize(viewSettings)}px`,
);
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
viewSettings!.scrolled = isScrolledMode;
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!));
setViewSettings(bookKey, viewSettings!);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isScrolledMode]);
useEffect(() => {
document.body.classList.toggle('invert', isInvertedColors);
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInvertedColors]);
useEffect(() => {
const view = getView(bookKey);
if (!view) return;
viewSettings!.zoomLevel = zoomLevel;
setViewSettings(bookKey, viewSettings!);
view.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
view.renderer.setStyles?.(getStyles(viewSettings!));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [zoomLevel]);
@@ -135,6 +132,9 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
<hr className='border-base-300 my-1' />
{appService?.hasRoundedWindow && (
<MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />
)}
<MenuItem
label={
themeMode === 'dark'
@@ -146,12 +146,6 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
icon={themeMode === 'dark' ? <BiMoon /> : themeMode === 'light' ? <BiSun /> : <TbSunMoon />}
onClick={cycleThemeMode}
/>
<MenuItem
label={_('Invert Colors in Dark Mode')}
icon={isInvertedColors ? <MdCheck className='text-base-content' /> : undefined}
onClick={toggleInvertedColors}
disabled={!isDarkMode}
/>
</div>
);
};
@@ -7,6 +7,7 @@ import { HighlightColor, HighlightStyle } from '@/types/book';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface AnnotationPopupProps {
dir: 'ltr' | 'rtl';
buttons: Array<{ tooltipText: string; Icon: React.ElementType; onClick: () => void }>;
position: Position;
trianglePosition: Position;
@@ -22,6 +23,7 @@ const OPTIONS_HEIGHT_PIX = 28;
const OPTIONS_PADDING_PIX = 16;
const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
dir,
buttons,
position,
trianglePosition,
@@ -35,7 +37,7 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
const highlightOptionsHeightPx = useResponsiveSize(OPTIONS_HEIGHT_PIX);
const highlightOptionsPaddingPx = useResponsiveSize(OPTIONS_PADDING_PIX);
return (
<div>
<div dir={dir}>
<Popup
width={popupWidth}
height={popupHeight}
@@ -76,7 +76,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const dictPopupHeight = Math.min(300, maxHeight);
const transPopupWidth = Math.min(480, maxWidth);
const transPopupHeight = Math.min(360, maxHeight);
const annotPopupWidth = useResponsiveSize(280);
const annotPopupWidth = useResponsiveSize(300);
const annotPopupHeight = useResponsiveSize(44);
const androidSelectionHandlerHeight = 8;
@@ -487,6 +487,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
)}
{showAnnotPopup && trianglePosition && annotPopupPosition && (
<AnnotationPopup
dir={viewSettings.rtl ? 'rtl' : 'ltr'}
buttons={buttons}
position={annotPopupPosition}
trianglePosition={trianglePosition}
@@ -15,10 +15,10 @@ const NotebookHeader: React.FC<{
const _ = useTranslation();
const iconSize14 = useResponsiveSize(14);
return (
<div className='notebook-header relative flex h-11 items-center px-3'>
<div className='notebook-header relative flex h-11 items-center px-3' dir='ltr'>
<div className='absolute inset-0 flex items-center justify-center space-x-2'>
<LuNotebookPen />
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
<div className='notebook-title hidden text-sm font-medium sm:flex'>{_('Notebook')}</div>
</div>
<div className='z-10 flex items-center gap-x-4'>
<button
@@ -114,6 +114,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
'inset-0 w-full rounded-none border-0 bg-transparent p-0',
'content font-size-sm',
)}
dir='auto'
ref={editorRef}
value={note}
rows={1}
@@ -130,7 +131,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
<span className='content font-size-xs inline text-gray-500'>{getAnnotationText()}</span>
</div>
</div>
<div className='flex justify-end p-2'>
<div className='flex justify-end p-2' dir='ltr'>
<button
className={clsx(
'content btn btn-ghost font-size-sm hover:bg-transparent',
@@ -7,13 +7,14 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useNotebookStore } from '@/store/notebookStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { useThemeStore } from '@/store/themeStore';
import { useEnv } from '@/context/EnvContext';
import { useDrag } from '@/hooks/useDrag';
import { TextSelection } from '@/utils/sel';
import { BookNote } from '@/types/book';
import { uniqueId } from '@/utils/misc';
import { eventDispatcher } from '@/utils/event';
import { getBookDirFromLanguage } from '@/utils/book';
import BooknoteItem from '../sidebar/BooknoteItem';
import NotebookHeader from './Header';
import NoteEditor from './NoteEditor';
@@ -23,14 +24,14 @@ const MAX_NOTEBOOK_WIDTH = 0.45;
const Notebook: React.FC = ({}) => {
const _ = useTranslation();
const { updateAppTheme } = useTheme();
const { updateAppTheme } = useThemeStore();
const { envConfig, appService } = useEnv();
const { settings } = useSettingsStore();
const { sideBarBookKey } = useSidebarStore();
const { notebookWidth, isNotebookVisible, isNotebookPinned } = useNotebookStore();
const { notebookNewAnnotation, notebookEditAnnotation, setNotebookPin } = useNotebookStore();
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
const { getView } = useReaderStore();
const { getBookData, getConfig, saveConfig, updateBooknotes } = useBookDataStore();
const { getView, getViewSettings } = useReaderStore();
const { setNotebookWidth, setNotebookVisible, toggleNotebookPin } = useNotebookStore();
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
@@ -136,6 +137,14 @@ const Notebook: React.FC = ({}) => {
if (!sideBarBookKey) return null;
const bookData = getBookData(sideBarBookKey);
const viewSettings = getViewSettings(sideBarBookKey);
if (!bookData || !bookData.bookDoc) {
return null;
}
const { bookDoc } = bookData;
const languageDir = getBookDirFromLanguage(bookDoc.metadata.language);
const config = getConfig(sideBarBookKey);
const { booknotes: allNotes = [] } = config || {};
const annotationNotes = allNotes
@@ -159,6 +168,7 @@ const Notebook: React.FC = ({}) => {
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
!isNotebookPinned && 'shadow-2xl',
)}
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
style={{
width: `${notebookWidth}`,
maxWidth: `${MAX_NOTEBOOK_WIDTH * 100}%`,
@@ -183,7 +193,7 @@ const Notebook: React.FC = ({}) => {
handleTogglePin={handleTogglePin}
/>
<div className='max-h-[calc(100vh-44px)] overflow-y-auto px-3'>
<div>
<div dir='ltr'>
{excerptNotes.length > 0 && (
<p className='content font-size-base pt-1'>{_('Excerpts')}</p>
)}
@@ -196,7 +206,7 @@ const Notebook: React.FC = ({}) => {
className='collapse-arrow border-base-300 bg-base-100 collapse border'
>
<div
className='collapse-title font-size-sm h-9 min-h-9 p-2 pr-8 font-medium'
className='collapse-title font-size-sm h-9 min-h-9 p-2 pe-8 font-medium'
style={
{
'--top-override': '1.2rem',
@@ -208,7 +218,7 @@ const Notebook: React.FC = ({}) => {
</div>
<div className='collapse-content font-size-xs select-text px-3 pb-0'>
<p className='hyphens-auto text-justify'>{item.text}</p>
<div className='flex justify-end'>
<div className='flex justify-end' dir='ltr'>
<div
className='font-size-xs cursor-pointer align-bottom text-red-500 hover:text-red-600'
onClick={handleEditNote.bind(null, item, true)}
@@ -221,7 +231,7 @@ const Notebook: React.FC = ({}) => {
</li>
))}
</ul>
<div>
<div dir='ltr'>
{(notebookNewAnnotation || annotationNotes.length > 0) && (
<p className='content font-size-base pt-1'>{_('Notes')}</p>
)}
@@ -3,24 +3,24 @@ import { MdOutlineLightMode, MdOutlineDarkMode } from 'react-icons/md';
import { MdRadioButtonUnchecked, MdRadioButtonChecked } from 'react-icons/md';
import { TbSunMoon } from 'react-icons/tb';
import { useTheme } from '@/hooks/useTheme';
import { themes } from '@/styles/themes';
import { getStyles } from '@/utils/style';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { themeMode, themeColor, themeCode, isDarkMode, updateThemeMode, updateThemeColor } =
useTheme();
const { themeMode, themeColor, themeCode, isDarkMode, setThemeMode, setThemeColor } =
useThemeStore();
const { getViews, getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const iconSize24 = useResponsiveSize(24);
useEffect(() => {
getViews().forEach((view) => {
view.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
view.renderer.setStyles?.(getStyles(viewSettings!));
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode]);
@@ -29,11 +29,11 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='my-4 w-full space-y-6'>
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Theme Mode')}</h2>
<div className='flex gap-2'>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto Mode')}>
<button
className={`btn btn-ghost btn-circle ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => updateThemeMode('auto')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('auto')}
>
<TbSunMoon />
</button>
@@ -41,8 +41,8 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Light Mode')}>
<button
className={`btn btn-ghost btn-circle ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => updateThemeMode('light')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('light')}
>
<MdOutlineLightMode />
</button>
@@ -50,8 +50,8 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Dark Mode')}>
<button
className={`btn btn-ghost btn-circle ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => updateThemeMode('dark')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('dark')}
>
<MdOutlineDarkMode />
</button>
@@ -78,7 +78,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
name='theme'
value={name}
checked={themeColor === name}
onChange={() => updateThemeColor(name)}
onChange={() => setThemeColor(name)}
className='hidden'
/>
{themeColor === name ? (
@@ -16,10 +16,10 @@ import {
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { useEnv } from '@/context/EnvContext';
import { getStyles } from '@/utils/style';
import { getOSPlatform } from '@/utils/misc';
import { FONT_ENUM_SUPPORTED_OS_PLATFORMS, getSysFontsList } from '@/utils/font';
import { getSysFontsList } from '@/utils/font';
import { isTauriAppPlatform } from '@/services/environment';
interface FontFaceProps {
@@ -60,11 +60,11 @@ const FontFace = ({
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const { themeCode } = useTheme();
const fontFamilyOptions = [
{
@@ -109,7 +109,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [fontWeight, setFontWeight] = useState(viewSettings.fontWeight!);
useEffect(() => {
if (isTauriAppPlatform() && FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
if (isTauriAppPlatform() && appService?.hasSysFontsList) {
getSysFontsList().then((fonts) => {
setSysFonts(fonts);
});
@@ -124,7 +124,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.defaultFont = defaultFont;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultFont]);
@@ -135,7 +135,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.defaultFontSize = defaultFontSize;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultFontSize]);
@@ -146,7 +146,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.minimumFontSize = minFontSize;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minFontSize]);
@@ -157,7 +157,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.fontWeight = fontWeight;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fontWeight]);
@@ -168,7 +168,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.serifFont = serifFont;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serifFont]);
@@ -179,7 +179,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.sansSerifFont = sansSerifFont;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sansSerifFont]);
@@ -190,7 +190,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.monospaceFont = monospaceFont;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [monospaceFont]);
@@ -201,7 +201,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.overrideFont = overrideFont;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrideFont]);
@@ -1,14 +1,14 @@
import React, { useEffect, useState } from 'react';
import { MdOutlineAutoMode } from 'react-icons/md';
import { MdOutlineTextRotationDown, MdOutlineTextRotationNone } from 'react-icons/md';
import { MdOutlineTextRotationNone, MdTextRotateVertical } from 'react-icons/md';
import { TbTextDirectionRtl } from 'react-icons/tb';
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import NumberInput from './NumberInput';
@@ -20,7 +20,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const view = getView(bookKey);
const bookData = getBookData(bookKey)!;
const viewSettings = getViewSettings(bookKey)!;
const { themeCode } = useTheme();
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
@@ -44,7 +43,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.paragraphMargin = paragraphMargin;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paragraphMargin]);
@@ -55,7 +54,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.lineHeight = lineHeight;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lineHeight]);
@@ -66,7 +65,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.wordSpacing = wordSpacing;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wordSpacing]);
@@ -77,7 +76,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.letterSpacing = letterSpacing;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [letterSpacing]);
@@ -88,7 +87,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.textIndent = textIndent;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textIndent]);
@@ -99,7 +98,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.fullJustification = fullJustification;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fullJustification]);
@@ -110,7 +109,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.hyphenation = hyphenation;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hyphenation]);
@@ -147,10 +146,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setSettings(settings);
}
view?.renderer.setAttribute('max-column-count', maxColumnCount);
view?.renderer.setAttribute(
'max-inline-size',
`${maxColumnCount === 1 || viewSettings.scrolled ? ONE_COLUMN_MAX_INLINE_SIZE : maxInlineSize}px`,
);
view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxColumnCount]);
@@ -161,10 +157,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.maxInlineSize = maxInlineSize;
setSettings(settings);
}
view?.renderer.setAttribute(
'max-inline-size',
`${maxColumnCount === 1 || viewSettings.scrolled ? ONE_COLUMN_MAX_INLINE_SIZE : maxInlineSize}px`,
);
view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxInlineSize]);
@@ -181,12 +174,19 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
// global settings are not supported for writing mode
const prevWritingMode = viewSettings.writingMode;
viewSettings.writingMode = writingMode;
setViewSettings(bookKey, viewSettings);
if (view) {
view.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view.renderer.setStyles?.(getStyles(viewSettings));
view.book.dir = getBookDirFromWritingMode(writingMode);
}
if (
prevWritingMode !== writingMode &&
(writingMode === 'horizontal-rl' || prevWritingMode === 'horizontal-rl')
) {
setTimeout(() => window.location.reload(), 100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [writingMode]);
@@ -197,23 +197,24 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.overrideLayout = overrideLayout;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrideLayout]);
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
const isCJKBook = langCode === 'zh' || langCode === 'ja' || langCode === 'ko';
const mightBeRTLBook =
langCode === 'zh' || langCode === 'ja' || langCode === 'ko' || langCode === '';
return (
<div className='my-4 w-full space-y-6'>
{isCJKBook && (
{mightBeRTLBook && (
<div className='w-full'>
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Writing Mode')}</h2>
<div className='flex gap-2'>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Default')}>
<button
className={`btn btn-ghost btn-circle ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('auto')}
>
<MdOutlineAutoMode />
@@ -222,7 +223,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Horizontal Direction')}>
<button
className={`btn btn-ghost btn-circle ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-tb')}
>
<MdOutlineTextRotationNone />
@@ -231,10 +232,19 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Vertical Direction')}>
<button
className={`btn btn-ghost btn-circle ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('vertical-rl')}
>
<MdOutlineTextRotationDown />
<MdTextRotateVertical />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('RTL Direction')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'horizontal-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-rl')}
>
<TbTextDirectionRtl />
</button>
</div>
</div>
@@ -256,7 +266,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
step={0.5}
/>
<NumberInput
className='config-item-top'
label={_('Line Spacing')}
value={lineHeight}
onChange={setLineHeight}
@@ -265,7 +274,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
step={0.1}
/>
<NumberInput
className='config-item-top'
label={_('Word Spacing')}
value={wordSpacing}
onChange={setWordSpacing}
@@ -274,7 +282,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
step={0.5}
/>
<NumberInput
className='config-item-top'
label={_('Letter Spacing')}
value={letterSpacing}
onChange={setLetterSpacing}
@@ -283,7 +290,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
step={0.1}
/>
<NumberInput
className='config-item-top'
label={_('Text Indent')}
value={textIndent}
onChange={setTextIndent}
@@ -350,18 +356,20 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
max={4}
/>
<NumberInput
label={_('Maximum Inline Size')}
label={viewSettings.vertical ? _('Maximum Column Height') : _('Maximum Column Width')}
value={maxInlineSize}
onChange={setMaxInlineSize}
min={500}
disabled={maxColumnCount === 1 || viewSettings.scrolled}
min={400}
max={9999}
step={100}
/>
<NumberInput
label={_('Maximum Block Size')}
label={viewSettings.vertical ? _('Maximum Column Width') : _('Maximum Column Height')}
value={maxBlockSize}
onChange={setMaxBlockSize}
min={500}
disabled={maxColumnCount === 1 || viewSettings.scrolled}
min={400}
max={9999}
step={100}
/>
@@ -4,7 +4,6 @@ import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getStyles } from '@/utils/style';
import { useTheme } from '@/hooks/useTheme';
import cssbeautify from 'cssbeautify';
import cssValidate from '@/utils/css';
@@ -13,10 +12,10 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const { themeCode } = useTheme();
const [animated, setAnimated] = useState(viewSettings.animated!);
const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick!);
const [isContinuousScroll, setIsContinuousScroll] = useState(viewSettings.continuousScroll!);
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -52,14 +51,14 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setDraftStylesheet(formattedCSS);
setDraftStylesheetSaved(true);
viewSettings.userStylesheet = formattedCSS;
setViewSettings(bookKey, viewSettings);
setViewSettings(bookKey, { ...viewSettings });
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.userStylesheet = formattedCSS;
setSettings(settings);
}
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings));
};
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
@@ -92,12 +91,22 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDisableClick]);
useEffect(() => {
viewSettings.continuousScroll = isContinuousScroll;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.continuousScroll = isContinuousScroll;
setSettings(settings);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isContinuousScroll]);
return (
<div className='my-4 w-full space-y-6'>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-y'>
<div className='divide-base-200 divide-y'>
<div className='config-item config-item-top config-item-bottom'>
<span className=''>{_('Paging Animation')}</span>
<input
@@ -114,8 +123,17 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Behavior')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-y'>
<div className='config-item config-item-top config-item-bottom'>
<div className='divide-base-200 divide-y'>
<div className='config-item config-item-top'>
<span className=''>{_('Continuous Scroll')}</span>
<input
type='checkbox'
className='toggle'
checked={isContinuousScroll}
onChange={() => setIsContinuousScroll(!isContinuousScroll)}
/>
</div>
<div className='config-item config-item-bottom'>
<span className=''>{_('Disable Click-to-Flip')}</span>
<input
type='checkbox'
@@ -9,6 +9,7 @@ interface NumberInputProps {
min: number;
max: number;
step?: number;
disabled?: boolean;
onChange: (value: number) => void;
}
@@ -20,6 +21,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
min,
max,
step,
disabled,
}) => {
const [localValue, setLocalValue] = useState(value);
const numberStep = step || 1;
@@ -74,13 +76,13 @@ const NumberInput: React.FC<NumberInputProps> = ({
/>
<button
onClick={decrement}
className={`btn btn-circle btn-sm ${value === min ? 'btn-disabled !bg-opacity-5' : ''}`}
className={`btn btn-circle btn-sm ${value <= min || disabled ? 'btn-disabled !bg-opacity-5' : ''}`}
>
<FiMinus className='h-4 w-4' />
</button>
<button
onClick={increment}
className={`btn btn-circle btn-sm ${value === max ? 'btn-disabled !bg-opacity-5' : ''}`}
className={`btn btn-circle btn-sm ${value >= max || disabled ? 'btn-disabled !bg-opacity-5' : ''}`}
>
<FiPlus className='h-4 w-4' />
</button>
@@ -64,7 +64,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
isOpen={true}
onClose={handleClose}
className='modal-open'
boxClassName='sm:w-1/2 sm:min-w-[480px] sm:h-[65%]'
boxClassName='sm:min-w-[520px]'
header={
<div className='flex w-full items-center justify-between'>
<button
@@ -76,12 +76,12 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
>
<MdArrowBackIosNew />
</button>
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center justify-around pl-4'>
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center pl-4 gap-2'>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
className={clsx(
'btn btn-ghost text-base-content h-8 min-h-8',
'btn btn-ghost text-base-content btn-sm',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => setActivePanel(tab)}
@@ -1,7 +1,9 @@
import clsx from 'clsx';
import React from 'react';
import Image from 'next/image';
import { MdInfoOutline } from 'react-icons/md';
import { Book } from '@/types/book';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
@@ -10,6 +12,7 @@ import { formatAuthors, formatTitle } from '@/utils/book';
const BookCard = ({ book }: { book: Book }) => {
const { coverImageUrl, title, author } = book;
const _ = useTranslation();
const { isDarkMode } = useThemeStore();
const iconSize18 = useResponsiveSize(18);
const showBookDetails = () => {
@@ -23,14 +26,17 @@ const BookCard = ({ book }: { book: Book }) => {
alt={_('Book Cover')}
width={56}
height={80}
className='mr-4 aspect-auto max-h-16 w-[15%] max-w-12 rounded-sm object-cover shadow-md'
className={clsx(
'me-4 aspect-auto max-h-16 w-[15%] max-w-12 rounded-sm object-cover shadow-md',
isDarkMode ? 'mix-blend-screen' : 'mix-blend-multiply',
)}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<div className='min-w-0 flex-1'>
<h4 className='line-clamp-2 w-[90%] text-sm font-semibold'>{formatTitle(title)}</h4>
<p className='text-neutral-content truncate text-xs'>{formatAuthors(author)}</p>
<p className='truncate text-xs opacity-75'>{formatAuthors(author)}</p>
</div>
<button
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
@@ -17,7 +17,7 @@ interface BookMenuProps {
const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen }) => {
const _ = useTranslation();
const { library } = useLibraryStore();
const { getVisibleLibrary } = useLibraryStore();
const { openParallelView } = useBooksManager();
const handleParallelView = (id: string) => {
openParallelView(id);
@@ -48,26 +48,28 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
>
<MenuItem label={_('Parallel Read')} noIcon>
<ul className='max-h-60 overflow-y-auto'>
{library.slice(0, 20).map((book) => (
<MenuItem
key={book.hash}
icon={
<Image
src={book.coverImageUrl!}
alt={book.title}
width={56}
height={80}
className='aspect-auto max-h-8 max-w-6 rounded-sm shadow-md'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
}
label={book.title}
labelClass='max-w-36'
onClick={() => handleParallelView(book.hash)}
/>
))}
{getVisibleLibrary()
.slice(0, 20)
.map((book) => (
<MenuItem
key={book.hash}
icon={
<Image
src={book.coverImageUrl!}
alt={book.title}
width={56}
height={80}
className='aspect-auto max-h-8 max-w-6 rounded-sm shadow-md'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
}
label={book.title}
labelClass='max-w-36'
onClick={() => handleParallelView(book.hash)}
/>
))}
</ul>
</MenuItem>
<MenuItem label={_('Reload Page')} noIcon shortcut='Shift+R' onClick={handleReloadPage} />
@@ -81,12 +81,16 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
} as React.CSSProperties
}
>
{item.note && <span className='content font-size-sm font-normal'>{item.note}</span>}
{item.note && (
<span className='content font-size-sm font-normal' dir='auto'>
{item.note}
</span>
)}
<div className='flex items-start'>
{item.note && (
<div className='my-1 mr-2 min-h-full self-stretch border-l-2 border-gray-300'></div>
<div className='my-1 me-2 min-h-full self-stretch border-l-2 border-gray-300'></div>
)}
<div className={clsx('content font-size-sm line-clamp-3', item.note && 'py-2')}>
<div className={clsx('content font-size-sm line-clamp-3', item.note && 'my-2')}>
<span
className={clsx(
'inline',
@@ -116,7 +120,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
}
onClick={(e) => e.stopPropagation()}
>
<div className='flex justify-end space-x-3 p-2'>
<div className='flex justify-end space-x-3 p-2' dir='ltr'>
{item.note && (
<button
className={clsx(
@@ -5,8 +5,8 @@ import { FiSearch } from 'react-icons/fi';
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { MdArrowBackIosNew } from 'react-icons/md';
import useTrafficLight from '@/hooks/useTrafficLight';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import Dropdown from '@/components/Dropdown';
import BookMenu from './BookMenu';
@@ -18,16 +18,18 @@ const SidebarHeader: React.FC<{
onTogglePin: () => void;
onToggleSearchBar: () => void;
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
const { isTrafficLightVisible } = useTrafficLight();
const { isTrafficLightVisible } = useTrafficLightStore();
const iconSize14 = useResponsiveSize(14);
const iconSize18 = useResponsiveSize(18);
const iconSize22 = useResponsiveSize(22);
return (
<div
className={clsx(
'sidebar-header flex h-11 items-center justify-between pr-2',
isTrafficLightVisible ? 'pl-20' : 'pl-1.5',
'sidebar-header flex h-11 items-center justify-between pe-2',
isTrafficLightVisible ? 'pl-20' : 'ps-1.5',
)}
dir='ltr'
>
<div className='flex items-center gap-x-8'>
<button
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { FaSearch, FaChevronDown } from 'react-icons/fa';
@@ -7,9 +8,9 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { BookSearchConfig, BookSearchResult } from '@/types/book';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import Dropdown from '@/components/Dropdown';
import SearchOptions from './SearchOptions';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
const MINIMUM_SEARCH_TERM_LENGTH_DEFAULT = 2;
const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1;
@@ -172,7 +173,11 @@ const SearchBar: React.FC<SearchBarProps> = ({
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
<Dropdown
className='dropdown-bottom flex justify-center'
className={clsx(
window.innerWidth < 640 && 'dropdown-end',
'dropdown-bottom flex justify-center',
)}
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3'}
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg'
toggleButton={<FaChevronDown size={iconSize12} className='text-gray-500' />}
>
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';
import { MdCheck } from 'react-icons/md';
import { BookSearchConfig } from '@/types/book';
@@ -6,6 +7,7 @@ import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
interface SearchOptionsProps {
searchConfig: BookSearchConfig;
menuClassName?: string;
onSearchConfigChanged: (searchConfig: BookSearchConfig) => void;
setIsDropdownOpen?: (isOpen: boolean) => void;
}
@@ -32,6 +34,7 @@ const Option: React.FC<OptionProps> = ({ label, isActive, onClick }) => (
const SearchOptions: React.FC<SearchOptionsProps> = ({
searchConfig,
menuClassName,
onSearchConfigChanged,
setIsDropdownOpen,
}) => {
@@ -44,7 +47,10 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
return (
<div
tabIndex={0}
className='book-menu dropdown-content dropdown-center border-base-200 z-20 mt-3 w-56 border shadow-2xl'
className={clsx(
'book-menu dropdown-content dropdown-center border-base-200 z-20 w-56 border shadow-2xl',
menuClassName,
)}
>
<Option
label={_('Book')}
@@ -8,9 +8,10 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { BookSearchResult } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import { getBookDirFromLanguage } from '@/utils/book';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useDrag } from '@/hooks/useDrag';
import { useThemeStore } from '@/store/themeStore';
import SidebarHeader from './Header';
import SidebarContent from './Content';
import BookCard from './BookCard';
@@ -28,11 +29,11 @@ const SideBar: React.FC<{
onGoToLibrary: () => void;
}> = ({ onGoToLibrary }) => {
const { appService } = useEnv();
const { updateAppTheme } = useTheme();
const { updateAppTheme } = useThemeStore();
const { settings } = useSettingsStore();
const { sideBarBookKey } = useSidebarStore();
const { getBookData } = useBookDataStore();
const { getView } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
const [searchTerm, setSearchTerm] = useState('');
@@ -162,11 +163,13 @@ const SideBar: React.FC<{
if (!sideBarBookKey) return null;
const viewSettings = getViewSettings(sideBarBookKey);
const bookData = getBookData(sideBarBookKey);
if (!bookData || !bookData.book || !bookData.bookDoc) {
return null;
}
const { book, bookDoc } = bookData;
const languageDir = getBookDirFromLanguage(bookDoc.metadata.language);
return isSideBarVisible ? (
<>
@@ -178,6 +181,7 @@ const SideBar: React.FC<{
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
!isSideBarPinned && 'shadow-2xl',
)}
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
style={{
width: `${sideBarWidth}`,
maxWidth: `${MAX_SIDEBAR_WIDTH * 100}%`,
@@ -15,10 +15,11 @@ const TabNavigation: React.FC<{
return (
<div
className={clsx('bottom-tab border-base-300/50 bg-base-200 relative flex w-full border-t')}
dir='ltr'
>
<div
className={clsx(
'bg-base-300 absolute bottom-1.5 left-1 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
'bg-base-300 absolute bottom-1.5 start-1 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
'transform transition-transform duration-300',
activeTab === 'toc' && 'translate-x-0',
activeTab === 'annotations' && 'translate-x-[calc(100%+8px)]',
@@ -5,7 +5,6 @@ import useShortcuts from '@/hooks/useShortcuts';
import useBooksManager from './useBooksManager';
import { useSidebarStore } from '@/store/sidebarStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { tauriQuitApp } from '@/utils/window';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
@@ -21,7 +20,6 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { toggleNotebook } = useNotebookStore();
const { getNextBookKey } = useBooksManager();
const { themeCode } = useTheme();
const viewSettings = getViewSettings(sideBarBookKey ?? '');
const fontSize = viewSettings?.defaultFontSize ?? 16;
const lineHeight = viewSettings?.lineHeight ?? 1.6;
@@ -100,7 +98,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const zoomLevel = viewSettings!.zoomLevel + ZOOM_STEP;
viewSettings!.zoomLevel = Math.min(zoomLevel, MAX_ZOOM_LEVEL);
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
const zoomOut = () => {
@@ -111,7 +109,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const zoomLevel = viewSettings!.zoomLevel - ZOOM_STEP;
viewSettings!.zoomLevel = Math.max(zoomLevel, MIN_ZOOM_LEVEL);
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
const resetZoom = () => {
@@ -121,7 +119,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const viewSettings = getViewSettings(sideBarBookKey)!;
viewSettings!.zoomLevel = 100;
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
useShortcuts(
@@ -58,12 +58,15 @@ export const useClickEvent = (
}
}
}
} else if (msg.data.type === 'iframe-wheel' && !viewSettings.scrolled) {
} else if (msg.data.type === 'iframe-wheel') {
const { deltaY } = msg.data;
const fontSize = viewSettings?.defaultFontSize ?? 16;
const lineHeight = viewSettings?.lineHeight ?? 1.6;
const distance = fontSize * lineHeight * 3;
if (deltaY > 0) {
viewRef.current?.next(1);
viewRef.current?.next(viewSettings.scrolled ? distance : 1);
} else if (deltaY < 0) {
viewRef.current?.prev(1);
viewRef.current?.prev(viewSettings.scrolled ? distance : 1);
}
} else if (msg.data.type === 'iframe-mouseup') {
if (msg.data.button === 3) {
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react';
import { useAuth } from '@/context/AuthContext';
import { useSync } from '@/hooks/useSync';
import { BookNote } from '@/types/book';
import { useBookDataStore } from '@/store/bookDataStore';
import { SYNC_NOTES_INTERVAL_SEC } from '@/services/constants';
@@ -12,12 +13,6 @@ export const useNotesSync = (bookKey: string) => {
const config = getConfig(bookKey);
const bookHash = bookKey.split('-')[0]!;
useEffect(() => {
if (!user) return;
syncNotes([], bookHash, 'pull');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const lastSyncTime = useRef<number>(0);
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -57,13 +52,25 @@ export const useNotesSync = (bookKey: string) => {
}, [config]);
useEffect(() => {
if (syncedNotes?.length && config?.location) {
const processNewNote = (note: BookNote) => {
const oldNotes = config?.booknotes ?? [];
const existingNote = oldNotes.find((oldNote) => oldNote.id === note.id);
if (existingNote) {
if (existingNote.updatedAt < note.updatedAt) {
return { ...existingNote, ...note };
} else {
return { ...note, ...existingNote };
}
}
return note;
};
if (syncedNotes?.length && config) {
const newNotes = syncedNotes.filter((note) => note.bookHash === bookHash);
if (!newNotes.length) return;
const oldNotes = config.booknotes ?? [];
const mergedNotes = [
...oldNotes.filter((oldNote) => !newNotes.some((newNote) => newNote.id === oldNote.id)),
...newNotes,
...newNotes.map(processNewNote),
];
setConfig(bookKey, { ...config, booknotes: mergedNotes });
}
@@ -6,7 +6,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { throttle } from '@/utils/throttle';
export const useProgressAutoSave = (bookKey: string) => {
const { envConfig, appService } = useEnv();
const { envConfig } = useEnv();
const { getConfig, saveConfig } = useBookDataStore();
const { getProgress } = useReaderStore();
const progress = getProgress(bookKey);
@@ -22,9 +22,6 @@ export const useProgressAutoSave = (bookKey: string) => {
);
useEffect(() => {
// FIXME: On Android and iOS we need a better way to be notified
// when the app is about to go in background
if (!appService?.isMobile || !progress) return;
saveBookConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress, bookKey]);
+2 -2
View File
@@ -1,10 +1,10 @@
'use client';
import { useEffect } from 'react';
import { useTheme } from '@/hooks/useTheme';
import { hasUpdater } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import { useTranslation } from '@/hooks/useTranslation';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { useSettingsStore } from '@/store/settingsStore';
import Reader from './components/Reader';
@@ -12,7 +12,7 @@ export default function Page() {
const _ = useTranslation();
const { settings } = useSettingsStore();
useTheme();
useOpenWithBooks();
useEffect(() => {
const doCheckAppUpdates = async () => {
@@ -20,7 +20,7 @@ export const AboutWindow = () => {
const [isUpdated, setIsUpdated] = React.useState(false);
const handleCheckUpdate = async () => {
const update = await checkForAppUpdates(_);
const update = await checkForAppUpdates(_, false);
if (!update) {
setIsUpdated(true);
}
+5 -2
View File
@@ -110,14 +110,17 @@ const Dialog: React.FC<DialogProps> = ({
<div
className={clsx(
'modal-box settings-content z-20 flex flex-col rounded-none rounded-tl-2xl rounded-tr-2xl p-0 sm:rounded-2xl',
'h-full max-h-full w-full max-w-full sm:w-[65%] sm:max-w-[600px]',
'h-full max-h-full w-full max-w-full',
window.innerWidth < window.innerHeight
? 'sm:h-[50%] sm:w-3/4'
: 'sm:h-[65%] sm:w-1/2 sm:max-w-[600px]',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)] sm:pt-0',
boxClassName,
)}
>
{window.innerWidth < 640 && (
<div
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
className='drag-handle flex h-10 max-h-10 min-h-10 w-full cursor-row-resize items-center justify-center'
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
>
@@ -0,0 +1,20 @@
import React from 'react';
import { useTranslation } from '@/hooks/useTranslation';
import { HiArrowDownTray } from 'react-icons/hi2';
const DropIndicator: React.FC = () => {
const _ = useTranslation();
return (
<>
<div className='drag-overlay'></div>
<div className='drop-indicator'>
<div className='flex flex-col items-center justify-center'>
<HiArrowDownTray className='h-12 w-12' />
<p className='mt-2 font-medium'>{_('Drop to Import Books')}</p>
</div>
</div>
</>
);
};
export default DropIndicator;
@@ -3,6 +3,7 @@
import React, { createContext, useState, useContext, ReactNode, useEffect } from 'react';
import { User } from '@supabase/supabase-js';
import { supabase } from '@/utils/supabase';
import posthog from 'posthog-js';
interface AuthContextType {
token: string | null;
@@ -38,6 +39,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
localStorage.setItem('token', access_token);
localStorage.setItem('refresh_token', refresh_token);
localStorage.setItem('user', JSON.stringify(user));
posthog.identify(user.id);
setToken(access_token);
setUser(user);
} else {
+3 -2
View File
@@ -6,10 +6,11 @@ import { TranslationFunc } from '@/hooks/useTranslation';
const LAST_CHECK_KEY = 'lastAppUpdateCheck';
export const checkForAppUpdates = async (_: TranslationFunc) => {
export const checkForAppUpdates = async (_: TranslationFunc, autoCheck = true) => {
const lastCheck = localStorage.getItem(LAST_CHECK_KEY);
const now = Date.now();
if (lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000) return;
if (autoCheck && lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000)
return;
localStorage.setItem(LAST_CHECK_KEY, now.toString());
console.log('Checking for updates');
@@ -0,0 +1,44 @@
import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { isTauriAppPlatform } from '@/services/environment';
import { useLibraryStore } from '@/store/libraryStore';
import { navigateToLibrary } from '@/utils/nav';
export function useOpenWithBooks() {
const router = useRouter();
const { setCheckOpenWithBooks } = useLibraryStore();
const listenedOpenWithBooks = useRef(false);
const handleOpenWithFileUrl = (url: string) => {
let filePath = url;
if (filePath.startsWith('file://')) {
filePath = decodeURI(filePath.replace('file://', ''));
}
if (filePath.startsWith('/')) {
window.OPEN_WITH_FILES = [filePath];
setCheckOpenWithBooks(true);
navigateToLibrary(router, `reload=${Date.now()}`);
}
};
useEffect(() => {
if (!isTauriAppPlatform()) return;
if (listenedOpenWithBooks.current) return;
listenedOpenWithBooks.current = true;
const listenOpenWithFiles = async () => {
return await onOpenUrl((urls) => {
urls.forEach((url) => {
handleOpenWithFileUrl(url);
});
});
};
const unlisten = listenOpenWithFiles();
return () => {
unlisten.then((f) => f());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}
@@ -1,7 +1,10 @@
import { useEffect, useRef } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
export const useScreenWakeLock = (lock: boolean) => {
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
const unlistenOnFocusChangedRef = useRef<Promise<() => void> | null>(null);
useEffect(() => {
const requestWakeLock = async () => {
@@ -20,20 +23,50 @@ export const useScreenWakeLock = (lock: boolean) => {
}
};
if (lock) {
requestWakeLock();
} else if (wakeLockRef.current) {
wakeLockRef.current.release();
wakeLockRef.current = null;
console.log('Wake lock released');
}
return () => {
const releaseWakeLock = () => {
if (wakeLockRef.current) {
wakeLockRef.current.release();
wakeLockRef.current = null;
console.log('Wake lock released');
}
};
const handleVisibilityChange = () => {
if (document.hidden) {
releaseWakeLock();
} else {
requestWakeLock();
}
};
if (lock) {
requestWakeLock();
} else if (wakeLockRef.current) {
releaseWakeLock();
}
if (isWebAppPlatform() && lock) {
document.addEventListener('visibilitychange', handleVisibilityChange);
} else if (isTauriAppPlatform() && lock) {
unlistenOnFocusChangedRef.current = getCurrentWindow().onFocusChanged(
({ payload: focused }) => {
if (focused) {
requestWakeLock();
} else {
releaseWakeLock();
}
},
);
}
return () => {
releaseWakeLock();
if (isWebAppPlatform() && lock) {
document.removeEventListener('visibilitychange', handleVisibilityChange);
}
if (unlistenOnFocusChangedRef.current) {
unlistenOnFocusChangedRef.current.then((f) => f());
}
};
}, [lock]);
};
+3 -100
View File
@@ -1,109 +1,12 @@
'use client';
import { useEffect, useState } from 'react';
import { ThemeCode } from '@/utils/style';
import { themes, Palette } from '@/styles/themes';
import { isWebAppPlatform } from '@/services/environment';
export type ThemeMode = 'auto' | 'light' | 'dark';
import { useEffect } from 'react';
import { useThemeStore } from '@/store/themeStore';
export const useTheme = () => {
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
if (typeof window !== 'undefined' && localStorage) {
return (localStorage.getItem('themeMode') as ThemeMode) || 'auto';
}
return 'auto';
});
const [themeColor, setThemeColor] = useState(() => {
if (typeof window !== 'undefined' && localStorage) {
return localStorage.getItem('themeColor') || 'default';
}
return 'default';
});
const [systemIsDarkMode, setSystemIsDarkMode] = useState(() => {
return (
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
);
});
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
const [themeCode, setThemeCode] = useState<ThemeCode>(() => {
let themeMode = 'auto';
let themeColor = 'default';
let systemIsDarkMode = false;
if (typeof window !== 'undefined') {
themeColor = localStorage.getItem('themeColor') || 'default';
themeMode = localStorage.getItem('themeMode') as ThemeMode;
systemIsDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
const defaultTheme = themes.find((theme) => theme.name === themeColor);
const defaultPalette = isDarkMode ? defaultTheme!.colors.dark : defaultTheme!.colors.light;
return {
bg: defaultPalette['base-100'],
fg: defaultPalette['base-content'],
primary: defaultPalette.primary,
palette: defaultPalette,
};
});
useEffect(() => {
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = () => {
setSystemIsDarkMode(mediaQuery.matches);
};
mediaQuery.addEventListener('change', handleSystemThemeChange);
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
}
return undefined;
}, []);
const { themeColor, isDarkMode } = useThemeStore();
useEffect(() => {
document.documentElement.setAttribute(
'data-theme',
`${themeColor}-${isDarkMode ? 'dark' : 'light'}`,
);
}, [themeColor, isDarkMode]);
useEffect(() => {
if (typeof window !== 'undefined' && localStorage) {
localStorage.setItem('themeMode', themeMode);
localStorage.setItem('themeColor', themeColor);
}
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
const theme = themes.find((t) => t.name === themeColor);
const palette = isDarkMode ? theme!.colors.dark : theme!.colors.light;
const bg = palette['base-100'];
const fg = palette['base-content'];
const primary = palette.primary;
if (isWebAppPlatform()) {
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', bg);
}
setThemeCode({ bg, fg, primary, palette });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeMode, themeColor, isDarkMode]);
const updateThemeMode = (mode: ThemeMode) => setThemeMode(mode);
const updateThemeColor = (color: string) => setThemeColor(color);
const updateAppTheme = (color: keyof Palette) => {
if (isWebAppPlatform()) {
const { palette } = themeCode;
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', palette[color]);
}
};
return {
themeMode,
themeColor,
themeCode,
isDarkMode,
updateThemeMode,
updateThemeColor,
updateAppTheme,
};
};
@@ -1,47 +0,0 @@
import { useEffect, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
/**
* Custom hook to get the visibility of the traffic light (window control buttons on macOS)
* based on the fullscreen state of the application window.
*
* @returns {Object} An object containing:
* - `isTrafficLightVisible` (boolean): A state indicating whether the traffic light is visible.
*/
const useTrafficLight = () => {
const { appService } = useEnv();
const [isTrafficLightVisible, setVisible] = useState(appService?.hasTrafficLight ?? false);
let unlistenEnterFullScreen: () => void;
let unlistenExitFullScreen: () => void;
const handleSwitchFullScreen = async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
const isFullscreen = await currentWindow.isFullscreen();
if (appService?.hasTrafficLight) setVisible(!isFullscreen);
unlistenEnterFullScreen = await currentWindow.listen('will-enter-fullscreen', () => {
if (appService?.hasTrafficLight) setVisible(false);
});
unlistenExitFullScreen = await currentWindow.listen('will-exit-fullscreen', () => {
if (appService?.hasTrafficLight) setVisible(true);
});
};
useEffect(() => {
if (!appService?.hasTrafficLight) return;
handleSwitchFullScreen();
return () => {
unlistenEnterFullScreen?.();
unlistenExitFullScreen?.();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { isTrafficLightVisible };
};
export default useTrafficLight;
+26
View File
@@ -0,0 +1,26 @@
import { ViewSettings } from '@/types/book';
import { useEffect, useState } from 'react';
// This hook allows you to inject custom CSS into the reader UI.
// Note that the book content is rendered in an iframe, so UI CSS won't affect book rendering.
export const useUICSS = (bookKey: string, viewSettings: ViewSettings) => {
const [styleElement, setStyleElement] = useState<HTMLStyleElement | null>(null);
useEffect(() => {
if (!viewSettings) return;
if (styleElement) {
styleElement.remove();
}
const rawCSS = viewSettings.userStylesheet;
const newStyleEl = document.createElement('style');
newStyleEl.textContent = rawCSS.replace('foliate-view', `#foliate-view-${bookKey}`);
document.head.appendChild(newStyleEl);
setStyleElement(newStyleEl);
return () => {
newStyleEl.remove();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewSettings]);
};
+20 -15
View File
@@ -30,19 +30,26 @@ const getUserAndToken = async (req: NextRequest) => {
if (!authHeader) return {};
const token = authHeader.replace('Bearer ', '');
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error || !user) return {};
return { user, token };
try {
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error?.message === 'fetch failed') {
return { error: 'Network error' };
} else if (error || !user) {
return { error: 'Not authenticated' };
}
return { user, token };
} catch {
return { error: 'Network error' };
}
};
export async function GET(req: NextRequest) {
const { user, token } = await getUserAndToken(req);
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
const { user, token, error } = await getUserAndToken(req);
if (!user || !token || error) {
return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 });
}
const supabase = createSupabaseClient(token);
@@ -114,9 +121,9 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const { user, token } = await getUserAndToken(req);
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
const { user, token, error } = await getUserAndToken(req);
if (!user || !token || error) {
return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 });
}
const supabase = createSupabaseClient(token);
@@ -173,8 +180,6 @@ export async function POST(req: NextRequest) {
clientDeletedAt > serverDeletedAt || clientUpdatedAt > serverUpdatedAt;
if (clientIsNewer) {
// use server updated_at for updated records
dbRec.updated_at = new Date().toISOString();
const { data: updated, error: updateError } = await supabase
.from(table)
.update(dbRec)
@@ -53,6 +53,7 @@ export abstract class BaseAppService implements AppService {
abstract hasRoundedWindow: boolean;
abstract hasSafeAreaInset: boolean;
abstract hasHaptics: boolean;
abstract hasSysFontsList: boolean;
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
abstract getCoverImageUrl(book: Book): string;
+5 -4
View File
@@ -10,6 +10,7 @@ import {
} from '@/types/book';
import { ReadSettings, SystemSettings } from '@/types/settings';
import { UserStorageQuota } from '@/types/user';
import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from '@/utils/config';
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
@@ -61,12 +62,14 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
gapPercent: 5,
scrolled: false,
disableClick: false,
continuousScroll: false,
maxColumnCount: 2,
maxInlineSize: 720,
maxBlockSize: 1440,
maxInlineSize: getDefaultMaxInlineSize(),
maxBlockSize: getDefaultMaxBlockSize(),
animated: false,
writingMode: 'auto',
vertical: false,
rtl: false,
};
export const DEFAULT_BOOK_STYLE: BookStyle = {
@@ -381,8 +384,6 @@ export const ANDROID_FONTS = [
'XiHeiti',
];
export const ONE_COLUMN_MAX_INLINE_SIZE = 9999;
export const BOOK_IDS_SEPARATOR = '+';
export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web';
@@ -127,6 +127,7 @@ export class NativeAppService extends BaseAppService {
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
hasSysFontsList = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
@@ -189,6 +189,7 @@ export class WebAppService extends BaseAppService {
hasRoundedWindow = false;
hasSafeAreaInset = isPWA();
hasHaptics = false;
hasSysFontsList = false;
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
+5 -3
View File
@@ -3,9 +3,10 @@ import { Book } from '@/types/book';
import { EnvConfigType } from '@/services/environment';
interface LibraryState {
library: Book[];
library: Book[]; // might contain deleted books
checkOpenWithBooks: boolean;
clearOpenWithBooks: () => void;
getVisibleLibrary: () => Book[];
setCheckOpenWithBooks: (check: boolean) => void;
setLibrary: (books: Book[]) => void;
updateBook: (envConfig: EnvConfigType, book: Book) => void;
}
@@ -13,7 +14,8 @@ interface LibraryState {
export const useLibraryStore = create<LibraryState>((set, get) => ({
library: [],
checkOpenWithBooks: true,
clearOpenWithBooks: () => set({ checkOpenWithBooks: false }),
getVisibleLibrary: () => get().library.filter((book) => !book.deletedAt),
setCheckOpenWithBooks: (check) => set({ checkOpenWithBooks: check }),
setLibrary: (books) => set({ library: books }),
updateBook: async (envConfig: EnvConfigType, book: Book) => {
const appService = await envConfig.getAppService();
+85
View File
@@ -0,0 +1,85 @@
import { create } from 'zustand';
import { getThemeCode, ThemeCode } from '@/utils/style';
import { Palette, ThemeMode } from '@/styles/themes';
import { isWebAppPlatform } from '@/services/environment';
interface ThemeState {
themeMode: ThemeMode;
themeColor: string;
systemIsDarkMode: boolean;
themeCode: ThemeCode;
isDarkMode: boolean;
setThemeMode: (mode: ThemeMode) => void;
setThemeColor: (color: string) => void;
updateAppTheme: (color: keyof Palette) => void;
}
const getInitialThemeMode = (): ThemeMode => {
if (typeof window !== 'undefined' && localStorage) {
return (localStorage.getItem('themeMode') as ThemeMode) || 'auto';
}
return 'auto';
};
const getInitialThemeColor = (): string => {
if (typeof window !== 'undefined' && localStorage) {
return localStorage.getItem('themeColor') || 'default';
}
return 'default';
};
export const useThemeStore = create<ThemeState>((set, get) => {
const initialThemeMode = getInitialThemeMode();
const initialThemeColor = getInitialThemeColor();
const systemIsDarkMode =
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDarkMode =
initialThemeMode === 'dark' || (initialThemeMode === 'auto' && systemIsDarkMode);
const themeCode = getThemeCode();
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = () => {
set({ systemIsDarkMode: mediaQuery.matches });
};
mediaQuery.addEventListener('change', handleSystemThemeChange);
}
return {
themeMode: initialThemeMode,
themeColor: initialThemeColor,
systemIsDarkMode,
isDarkMode,
themeCode,
setThemeMode: (mode) => {
if (typeof window !== 'undefined' && localStorage) {
localStorage.setItem('themeMode', mode);
}
const isDarkMode = mode === 'dark' || (mode === 'auto' && get().systemIsDarkMode);
document.documentElement.setAttribute(
'data-theme',
`${get().themeColor}-${isDarkMode ? 'dark' : 'light'}`,
);
set({ themeMode: mode, isDarkMode });
set({ themeCode: getThemeCode() });
},
setThemeColor: (color) => {
if (typeof window !== 'undefined' && localStorage) {
localStorage.setItem('themeColor', color);
}
document.documentElement.setAttribute(
'data-theme',
`${color}-${get().isDarkMode ? 'dark' : 'light'}`,
);
set({ themeColor: color });
set({ themeCode: getThemeCode() });
},
updateAppTheme: (color) => {
if (isWebAppPlatform()) {
const { palette } = get().themeCode;
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', palette[color]);
}
},
};
});
@@ -0,0 +1,69 @@
import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';
import { AppService } from '@/types/system';
const WINDOW_CONTROL_PAD_X = 10.0;
const WINDOW_CONTROL_PAD_Y = 22.0;
interface TrafficLightState {
appService?: AppService;
isTrafficLightVisible: boolean;
shouldShowTrafficLight: boolean;
initializeTrafficLightStore: (appService: AppService) => void;
setTrafficLightVisibility: (visible: boolean) => void;
initializeTrafficLightListeners: () => Promise<void>;
cleanupTrafficLightListeners: () => void;
unlistenEnterFullScreen?: () => void;
unlistenExitFullScreen?: () => void;
}
export const useTrafficLightStore = create<TrafficLightState>((set, get) => {
return {
appService: undefined,
isTrafficLightVisible: false,
shouldShowTrafficLight: false,
initializeTrafficLightStore: (appService: AppService) => {
set({
appService,
isTrafficLightVisible: appService.hasTrafficLight,
shouldShowTrafficLight: appService.hasTrafficLight,
});
},
setTrafficLightVisibility: async (visible: boolean) => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
const isFullscreen = await currentWindow.isFullscreen();
set({ isTrafficLightVisible: !isFullscreen && visible, shouldShowTrafficLight: visible });
invoke('set_traffic_lights', {
visible: visible,
x: WINDOW_CONTROL_PAD_X,
y: WINDOW_CONTROL_PAD_Y,
});
},
initializeTrafficLightListeners: async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
const unlistenEnterFullScreen = await currentWindow.listen('will-enter-fullscreen', () => {
set({ isTrafficLightVisible: false });
});
const unlistenExitFullScreen = await currentWindow.listen('will-exit-fullscreen', () => {
const { shouldShowTrafficLight } = get();
set({ isTrafficLightVisible: shouldShowTrafficLight });
});
set({ unlistenEnterFullScreen, unlistenExitFullScreen });
},
cleanupTrafficLightListeners: () => {
const { unlistenEnterFullScreen, unlistenExitFullScreen } = get();
if (unlistenEnterFullScreen) unlistenEnterFullScreen();
if (unlistenExitFullScreen) unlistenExitFullScreen();
set({ unlistenEnterFullScreen: undefined, unlistenExitFullScreen: undefined });
},
};
});
+63
View File
@@ -7,6 +7,20 @@
--foreground: #171717;
border-radius: 10px;
scrollbar-gutter: auto !important;
overscroll-behavior: none;
}
/* Fixes the scrollbar gutter issue for DaisyUI */
/* https://github.com/saadeghi/daisyui/issues/3040#issuecomment-2250530354 */
:root:has(
:is(
.modal-open,
.modal:target,
.modal-toggle:checked + .modal,
.modal[open]
)
) {
scrollbar-gutter: unset !important;
}
@media (prefers-color-scheme: dark) {
@@ -35,6 +49,7 @@ body {
sans-serif;
border-radius: 10px;
background-color: transparent;
cursor: default;
}
@layer utilities {
@@ -262,3 +277,51 @@ foliate-view {
font-size: 0.75em !important;
line-height: 1em !important;
}
.direction-rtl {
direction: rtl;
}
.drop-zone {
position: relative;
}
.drag-over {
border: 2px dashed #4a90e2 !important;
background-color: rgba(74, 144, 226, 0.1) !important;
transition: all 0.2s ease-in-out;
}
.drag-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 20;
pointer-events: none;
}
.drop-zone.drag-over .drag-overlay {
display: block;
}
.drop-indicator {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 50;
padding: 20px;
background-color: theme('colors.base-300');
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
pointer-events: none;
}
.drop-zone.drag-over .drop-indicator {
display: block;
}
+1
View File
@@ -7,6 +7,7 @@ type BaseColor = {
primary: string;
};
export type ThemeMode = 'auto' | 'light' | 'dark';
export type Palette = {
'base-100': string;
'base-200': string;
+2
View File
@@ -60,12 +60,14 @@ export interface BookLayout {
gapPercent: number;
scrolled: boolean;
disableClick: boolean;
continuousScroll: boolean;
maxColumnCount: number;
maxInlineSize: number;
maxBlockSize: number;
animated: boolean;
writingMode: WritingMode;
vertical: boolean;
rtl: boolean;
}
export interface BookStyle {
+1
View File
@@ -30,6 +30,7 @@ export interface AppService {
hasRoundedWindow: boolean;
hasSafeAreaInset: boolean;
hasHaptics: boolean;
hasSysFontsList: boolean;
isMobile: boolean;
isAppDataSandbox: boolean;
isAndroidApp: boolean;
+15 -3
View File
@@ -57,9 +57,9 @@ export const listFormater = (narrow = false, lang = userLang) => {
export const getBookLangCode = (lang: string | string[] | undefined) => {
try {
const bookLang = typeof lang === 'string' ? lang : lang?.[0];
return bookLang ? bookLang.split('-')[0]! : 'en';
return bookLang ? bookLang.split('-')[0]! : '';
} catch {
return 'en';
return '';
}
};
@@ -67,7 +67,7 @@ export const formatAuthors = (
contributors: string | Contributor | [string | Contributor],
bookLang?: string | string[],
) => {
const langCode = getBookLangCode(bookLang);
const langCode = getBookLangCode(bookLang) || 'en';
return Array.isArray(contributors)
? listFormater(langCode === 'zh', langCode).format(
contributors.map((contributor) =>
@@ -90,6 +90,10 @@ export const formatLanguage = (lang: string | string[] | undefined) => {
return Array.isArray(lang) ? lang.join(', ') : lang;
};
export const primaryLanguage = (lang: string | string[] | undefined) => {
return Array.isArray(lang) ? lang[0] : lang;
};
export const formatDate = (date: string | number | Date | undefined) => {
if (!date) return;
try {
@@ -131,3 +135,11 @@ export const getBookDirFromWritingMode = (writingMode: WritingMode) => {
return 'auto';
}
};
export const getBookDirFromLanguage = (language: string | string[] | undefined) => {
const lang = primaryLanguage(language);
if (!lang) return 'auto';
const rtlLanguages = new Set(['ar', 'he', 'fa', 'ur', 'dv', 'ps', 'sd', 'yi']);
const primaryLang = lang.split('-')[0]!.toLowerCase();
return rtlLanguages.has(primaryLang) ? 'rtl' : 'auto';
};
+28
View File
@@ -0,0 +1,28 @@
import { ViewSettings } from '@/types/book';
export const getMaxInlineSize = (viewSettings: ViewSettings) => {
const isScrolled = viewSettings.scrolled!;
const maxColumnCount = viewSettings.maxColumnCount!;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
return maxColumnCount === 1 || isScrolled
? Math.max(screenWidth, screenHeight, 720)
: viewSettings.maxInlineSize!;
};
export const getDefaultMaxInlineSize = () => {
if (typeof window === 'undefined') return 720;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
return screenWidth < screenHeight ? Math.max(screenWidth, 720) : 720;
};
export const getDefaultMaxBlockSize = () => {
if (typeof window === 'undefined') return 1440;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
return Math.max(screenWidth, screenHeight, 1440);
};
+31 -4
View File
@@ -5,7 +5,7 @@ import {
FALLBACK_FONTS,
} from '@/services/constants';
import { ViewSettings } from '@/types/book';
import { Palette } from '@/styles/themes';
import { themes, Palette, ThemeMode } from '@/styles/themes';
import fontfacesCSS from '!!raw-loader!../styles/fonts.css';
import { getOSPlatform } from './misc';
@@ -178,11 +178,10 @@ const getLayoutStyles = (
background: var(--background-set, none);
}
body *:not(a):not(#b1):not(#b1 *):not(#b2):not(#b2 *):not(.bg):not(.bg *):not(.vol):not(.vol *):not(.background):not(.background *) {
border-color: currentColor !important;
${bg === '#ffffff' ? '' : `color: inherit;`}
${bg === '#ffffff' ? '' : `background-color: ${bg} !important;`}
}
body {
overflow: unset;
zoom: ${zoomLevel};
}
svg, img {
@@ -224,9 +223,14 @@ const getLayoutStyles = (
display: none;
}
/* Now begins really dirty hacks to fix some badly designed epubs */
.calibre {
color: unset;
}
.chapterHeader {
border-color: unset;
}
`;
export const getFootnoteStyles = () => `
@@ -252,7 +256,30 @@ export interface ThemeCode {
palette: Palette;
}
export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
export const getThemeCode = () => {
let themeMode = 'auto';
let themeColor = 'default';
let systemIsDarkMode = false;
if (typeof window !== 'undefined') {
themeColor = localStorage.getItem('themeColor') || 'default';
themeMode = localStorage.getItem('themeMode') as ThemeMode;
systemIsDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
const defaultTheme = themes.find((theme) => theme.name === themeColor);
const defaultPalette = isDarkMode ? defaultTheme!.colors.dark : defaultTheme!.colors.light;
return {
bg: defaultPalette['base-100'],
fg: defaultPalette['base-content'],
primary: defaultPalette.primary,
palette: defaultPalette,
} as ThemeCode;
};
export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) => {
if (!themeCode) {
themeCode = getThemeCode();
}
const layoutStyles = getLayoutStyles(
viewSettings.overrideLayout!,
viewSettings.paragraphMargin!,
+10 -10
View File
@@ -1,7 +1,9 @@
import { getCurrentWindow } from '@tauri-apps/api/window';
import { TauriEvent } from '@tauri-apps/api/event';
import { exit } from '@tauri-apps/plugin-process';
import { eventDispatcher } from './event';
export const tauriGetWindowLogicalPosition = async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
const factor = await currentWindow.scaleFactor();
const physicalPos = await currentWindow.outerPosition();
@@ -9,24 +11,18 @@ export const tauriGetWindowLogicalPosition = async () => {
};
export const tauriHandleMinimize = async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
getCurrentWindow().minimize();
};
export const tauriHandleToggleMaximize = async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
getCurrentWindow().toggleMaximize();
};
export const tauriHandleClose = async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
getCurrentWindow().close();
};
export const tauriHandleOnCloseWindow = async (callback: () => void) => {
const { TauriEvent } = await import('@tauri-apps/api/event');
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const { exit } = await import('@tauri-apps/plugin-process');
const currentWindow = getCurrentWindow();
return currentWindow.listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await callback();
@@ -35,9 +31,14 @@ export const tauriHandleOnCloseWindow = async (callback: () => void) => {
});
};
export const tauriHandleToggleFullScreen = async () => {
const currentWindow = getCurrentWindow();
const isFullscreen = await currentWindow.isFullscreen();
const newIsFullscreen = !isFullscreen;
await currentWindow.setFullscreen(newIsFullscreen);
};
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();
@@ -46,6 +47,5 @@ export const tauriHandleOnWindowFocus = async (callback: () => void) => {
export const tauriQuitApp = async () => {
await eventDispatcher.dispatch('quit-app');
const { exit } = await import('@tauri-apps/plugin-process');
await exit(0);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 991 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+78 -78
View File
@@ -54,8 +54,8 @@ importers:
specifier: ^2.47.7
version: 2.47.7
'@tauri-apps/api':
specifier: 2.2.0
version: 2.2.0
specifier: 2.3.0
version: 2.3.0
'@tauri-apps/plugin-cli':
specifier: ^2.2.0
version: 2.2.0
@@ -75,11 +75,11 @@ importers:
specifier: ^2.3.0
version: 2.3.0
'@tauri-apps/plugin-log':
specifier: ^2.2.1
version: 2.2.1
specifier: ^2.2.3
version: 2.2.3
'@tauri-apps/plugin-opener':
specifier: ^2.2.5
version: 2.2.5
specifier: ^2.2.6
version: 2.2.6
'@tauri-apps/plugin-os':
specifier: ^2.2.0
version: 2.2.0
@@ -90,8 +90,8 @@ importers:
specifier: ~2.2.0
version: 2.2.0
'@tauri-apps/plugin-updater':
specifier: ^2.4.0
version: 2.4.0
specifier: ^2.5.1
version: 2.5.1
'@zip.js/zip.js':
specifier: ^2.7.53
version: 2.7.53
@@ -151,8 +151,8 @@ importers:
version: 5.0.1(@types/react@18.3.12)(react@19.0.0)
devDependencies:
'@tauri-apps/cli':
specifier: 2.2.7
version: 2.2.7
specifier: 2.3.1
version: 2.3.1
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
@@ -1654,75 +1654,75 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tauri-apps/api@2.2.0':
resolution: {integrity: sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==}
'@tauri-apps/api@2.3.0':
resolution: {integrity: sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==}
'@tauri-apps/cli-darwin-arm64@2.2.7':
resolution: {integrity: sha512-54kcpxZ3X1Rq+pPTzk3iIcjEVY4yv493uRx/80rLoAA95vAC0c//31Whz75UVddDjJfZvXlXZ3uSZ+bnCOnt0A==}
'@tauri-apps/cli-darwin-arm64@2.3.1':
resolution: {integrity: sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.2.7':
resolution: {integrity: sha512-Vgu2XtBWemLnarB+6LqQeLanDlRj7CeFN//H8bVVdjbNzxcSxsvbLYMBP8+3boa7eBnjDrqMImRySSgL6IrwTw==}
'@tauri-apps/cli-darwin-x64@2.3.1':
resolution: {integrity: sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.2.7':
resolution: {integrity: sha512-+Clha2iQAiK9zoY/KKW0KLHkR0k36O78YLx5Sl98tWkwI3OBZFg5H5WT1plH/4sbZIS2aLFN6dw58/JlY9Bu/g==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
resolution: {integrity: sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.2.7':
resolution: {integrity: sha512-Z/Lp4SQe6BUEOays9BQAEum2pvZF4w9igyXijP+WbkOejZx4cDvarFJ5qXrqSLmBh7vxrdZcLwoLk9U//+yQrg==}
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
resolution: {integrity: sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==}
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
resolution: {integrity: sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==}
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
resolution: {integrity: sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.2.7':
resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==}
'@tauri-apps/cli-linux-x64-musl@2.3.1':
resolution: {integrity: sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==}
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
resolution: {integrity: sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.2.7':
resolution: {integrity: sha512-EiJ5/25tLSQOSGvv+t6o3ZBfOTKB5S3vb+hHQuKbfmKdRF0XQu2YPdIi1CQw1DU97ZAE0Dq4frvnyYEKWgMzVQ==}
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
resolution: {integrity: sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.2.7':
resolution: {integrity: sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg==}
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
resolution: {integrity: sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.2.7':
resolution: {integrity: sha512-ZnsS2B4BplwXP37celanNANiIy8TCYhvg5RT09n72uR/o+navFZtGpFSqljV8fy1Y4ixIPds8FrGSXJCN2BerA==}
'@tauri-apps/cli@2.3.1':
resolution: {integrity: sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==}
engines: {node: '>= 10'}
hasBin: true
@@ -1744,11 +1744,11 @@ packages:
'@tauri-apps/plugin-http@2.3.0':
resolution: {integrity: sha512-pigTvz+zzAqbIhCzRiR1GE98Jw7A03j2V+Eiexr9thBI8VfMiwFQMcbgON51xlwnVaI72LdbYKNajU84im8tlg==}
'@tauri-apps/plugin-log@2.2.1':
resolution: {integrity: sha512-bOz9w0hhlXLGLc1ZR37GqkXvTqkykl4A3GEKLjRIs0dq3n0BzLyoRDMPcpt7PdUHqaq6WISME+zEX2bqjSbJ2A==}
'@tauri-apps/plugin-log@2.2.3':
resolution: {integrity: sha512-noDkZXU0kPG1bP3qoF6om+q5TgQ65LWpsV/TnfrHyg2lEn0vE+WJWTItiIh7OUA1/xxC+hdb9ODy2bM2e+jHEw==}
'@tauri-apps/plugin-opener@2.2.5':
resolution: {integrity: sha512-hHsJ9RPWpZvZEPVFaL+d25gABMUMOf/A6ESXnvf/ii9guTukj58WXsAE/SOysXRIhej7kseRCxnOnIMpSCdUsQ==}
'@tauri-apps/plugin-opener@2.2.6':
resolution: {integrity: sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==}
'@tauri-apps/plugin-os@2.2.0':
resolution: {integrity: sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==}
@@ -1759,8 +1759,8 @@ packages:
'@tauri-apps/plugin-shell@2.2.0':
resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==}
'@tauri-apps/plugin-updater@2.4.0':
resolution: {integrity: sha512-BkeKN2WObAjobf2G77HyW/DxAfI0In+VSqWGnw/0cVPlM+VmA7fw9dKUnSunryZOG7ys9y07tj7FQa1ABMXGZQ==}
'@tauri-apps/plugin-updater@2.5.1':
resolution: {integrity: sha512-7fNJraKRbKkxguMY5lG2W20pBvAUkLu+cqnbu0UcK7DqeZgrAnNECcGBIDG6fJ6C+0fAp2V2dMIgznhffOBCcg==}
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@@ -5799,7 +5799,7 @@ snapshots:
'@fabianlars/tauri-plugin-oauth@2.0.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@gulpjs/to-absolute-glob@4.0.0':
dependencies:
@@ -6581,98 +6581,98 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tauri-apps/api@2.2.0': {}
'@tauri-apps/api@2.3.0': {}
'@tauri-apps/cli-darwin-arm64@2.2.7':
'@tauri-apps/cli-darwin-arm64@2.3.1':
optional: true
'@tauri-apps/cli-darwin-x64@2.2.7':
'@tauri-apps/cli-darwin-x64@2.3.1':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.2.7':
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.2.7':
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.2.7':
'@tauri-apps/cli-linux-x64-musl@2.3.1':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.2.7':
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.2.7':
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
optional: true
'@tauri-apps/cli@2.2.7':
'@tauri-apps/cli@2.3.1':
optionalDependencies:
'@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/cli-darwin-arm64': 2.3.1
'@tauri-apps/cli-darwin-x64': 2.3.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.3.1
'@tauri-apps/cli-linux-arm64-gnu': 2.3.1
'@tauri-apps/cli-linux-arm64-musl': 2.3.1
'@tauri-apps/cli-linux-x64-gnu': 2.3.1
'@tauri-apps/cli-linux-x64-musl': 2.3.1
'@tauri-apps/cli-win32-arm64-msvc': 2.3.1
'@tauri-apps/cli-win32-ia32-msvc': 2.3.1
'@tauri-apps/cli-win32-x64-msvc': 2.3.1
'@tauri-apps/plugin-cli@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-deep-link@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-dialog@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-fs@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-haptics@2.2.3':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-http@2.3.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-log@2.2.1':
'@tauri-apps/plugin-log@2.2.3':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-opener@2.2.5':
'@tauri-apps/plugin-opener@2.2.6':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-os@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-process@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-shell@2.2.0':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tauri-apps/plugin-updater@2.4.0':
'@tauri-apps/plugin-updater@2.5.1':
dependencies:
'@tauri-apps/api': 2.2.0
'@tauri-apps/api': 2.3.0
'@tsconfig/node10@1.0.11':
optional: true