forked from akai/readest
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4869b756ea | |||
| 693c79c9b7 | |||
| e846875973 | |||
| b1f2941247 | |||
| 6688c813e6 | |||
| 664dc45fcc | |||
| 27fa753486 | |||
| 71e392544c | |||
| 7f24081205 | |||
| 89f1c43984 | |||
| f841004e7a | |||
| 0922f557bb | |||
| 0b72ab156d | |||
| dfc6cc9c8a | |||
| 83b2908266 | |||
| c25f2a7abc | |||
| de1e396f6e | |||
| 80a3861816 | |||
| fd8eeb07c2 | |||
| 93d1fe6946 | |||
| 3c52a9e0cb | |||
| 3056a29860 | |||
| bd6738e54c | |||
| 8c3f49de25 | |||
| 0f44fff7cd | |||
| 77bba7bd0c | |||
| d30d1c17ae | |||
| 088c7d2c49 | |||
| d0e6f3ad50 | |||
| 8a83389164 | |||
| 04d07856f5 | |||
| cc8d0ea457 | |||
| 29e6af7768 | |||
| bc70d07749 | |||
| 2db83b8569 | |||
| 2f29295c6c |
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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": "拖放導入書籍"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
])
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ type BaseColor = {
|
||||
primary: string;
|
||||
};
|
||||
|
||||
export type ThemeMode = 'auto' | 'light' | 'dark';
|
||||
export type Palette = {
|
||||
'base-100': string;
|
||||
'base-200': string;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface AppService {
|
||||
hasRoundedWindow: boolean;
|
||||
hasSafeAreaInset: boolean;
|
||||
hasHaptics: boolean;
|
||||
hasSysFontsList: boolean;
|
||||
isMobile: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
isAndroidApp: boolean;
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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!,
|
||||
|
||||
@@ -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 |
+1
-1
Submodule packages/foliate-js updated: 601d70dbe4...0b2fdf1a5b
+1
-1
Submodule packages/tauri updated: e3b0260871...2d029a9f53
Generated
+78
-78
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user