forked from akai/readest
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 267e1656db | |||
| dcecda2984 | |||
| ce88f0229a | |||
| 316c82ea52 | |||
| 25be60ce2a | |||
| 4703eec045 | |||
| ab22a83332 | |||
| a3d6ebe44d | |||
| f8804b5d3c | |||
| ee3785ad51 | |||
| 5ba40cd2d6 | |||
| 6131180a31 | |||
| 7ccf3d0632 | |||
| 39fb7f759e | |||
| d89834a53d | |||
| 3c4dd2a143 | |||
| 847efb7082 | |||
| 01db8f90fa | |||
| e70c46ff02 | |||
| 7cb1998ebb |
@@ -28,7 +28,7 @@ jobs:
|
||||
run: cargo fmt --check
|
||||
- name: Clippy Check
|
||||
working-directory: apps/readest-app/src-tauri
|
||||
run: cargo clippy -- -D warnings
|
||||
run: cargo clippy -p Readest --no-deps -- -D warnings
|
||||
|
||||
build_web_app:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
Generated
+801
-800
File diff suppressed because it is too large
Load Diff
+16
-1
@@ -1,3 +1,18 @@
|
||||
[workspace]
|
||||
members = ["apps/readest-app/src-tauri"]
|
||||
members = [
|
||||
"apps/readest-app/src-tauri",
|
||||
"packages/tauri/crates/tauri",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Bilingify LLC"]
|
||||
homepage = "https://readest.com"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/readest/readest"
|
||||
categories = []
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[patch.crates-io]
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
@@ -4,6 +4,8 @@ NEXT_PUBLIC_POSTHOG_HOST=YOUR_POSTHOG_HOST
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
|
||||
NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824
|
||||
|
||||
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY
|
||||
|
||||
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.28",
|
||||
"version": "0.9.30",
|
||||
"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.3.0",
|
||||
"@tauri-apps/api": "2.4.1",
|
||||
"@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.3",
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-fs": "^2.2.1",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.4",
|
||||
"@tauri-apps/plugin-http": "^2.4.3",
|
||||
"@tauri-apps/plugin-log": "^2.3.1",
|
||||
"@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.5.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/cloudflare": "^0.5.12",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@tauri-apps/cli": "2.4.1",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "مسافة بادئة للنص",
|
||||
"Paragraph Margin": "هامش الفقرة",
|
||||
"Override Book Layout": "تجاوز تخطيط الكتاب",
|
||||
"Add to Group": "إضافة إلى مجموعة",
|
||||
"Untitled Group": "مجموعة بدون عنوان",
|
||||
"Group Books": "تجميع الكتب",
|
||||
"Remove From Group": "إزالة من المجموعة",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "تبديل منطقة التقليب بالنقر",
|
||||
"No chapters detected.": "لم يتم التعرف على فصول.",
|
||||
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
|
||||
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم."
|
||||
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "تعذر جلب الترجمة. يرجى تسجيل الدخول أولاً ثم المحاولة مرة أخرى.",
|
||||
"Group": "تجميع"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Texteinzug",
|
||||
"Paragraph Margin": "Absatzrand",
|
||||
"Override Book Layout": "Buch-Layout überschreiben",
|
||||
"Add to Group": "Zur Gruppe hinzufügen",
|
||||
"Untitled Group": "Unbenannte Gruppe",
|
||||
"Group Books": "Bücher gruppieren",
|
||||
"Remove From Group": "Aus Gruppe entfernen",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Klick-zu-Blättern-Bereich tauschen",
|
||||
"No chapters detected.": "Keine Kapitel erkannt.",
|
||||
"Failed to parse the EPUB file.": "Fehler beim Parsen der EPUB-Datei.",
|
||||
"This book format is not supported.": "Dieses Buchformat wird nicht unterstützt."
|
||||
"This book format is not supported.": "Dieses Buchformat wird nicht unterstützt.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Übersetzung konnte nicht abgerufen werden. Bitte zuerst anmelden und erneut versuchen.",
|
||||
"Group": "Gruppieren"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Εσοχή κειμένου",
|
||||
"Paragraph Margin": "Περιθώριο παραγράφου",
|
||||
"Override Book Layout": "Παράκαμψη διάταξης βιβλίου",
|
||||
"Add to Group": "Προσθήκη στην ομάδα",
|
||||
"Untitled Group": "Ομάδα χωρίς τίτλο",
|
||||
"Group Books": "Ομαδοποίηση βιβλίων",
|
||||
"Remove From Group": "Αφαίρεση από την ομάδα",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Ανταλλαγή περιοχής κλικ για γύρισμα σελίδας",
|
||||
"No chapters detected.": "Δεν εντοπίστηκαν κεφάλαια.",
|
||||
"Failed to parse the EPUB file.": "Αποτυχία ανάλυσης του αρχείου EPUB.",
|
||||
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται."
|
||||
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Αδυναμία λήψης της μετάφρασης. Παρακαλώ συνδεθείτε πρώτα και δοκιμάστε ξανά.",
|
||||
"Group": "Ομάδα"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Sangría de texto",
|
||||
"Paragraph Margin": "Margen de párrafo",
|
||||
"Override Book Layout": "Sobrescribir diseño del libro",
|
||||
"Add to Group": "Añadir a grupo",
|
||||
"Untitled Group": "Grupo sin título",
|
||||
"Group Books": "Agrupar libros",
|
||||
"Remove From Group": "Eliminar del grupo",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Intercambiar área de clic para voltear",
|
||||
"No chapters detected.": "No se detectaron capítulos.",
|
||||
"Failed to parse the EPUB file.": "Error al analizar el archivo EPUB.",
|
||||
"This book format is not supported.": "Este formato de libro no es compatible."
|
||||
"This book format is not supported.": "Este formato de libro no es compatible.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "No se puede obtener la traducción. Por favor, inicia sesión primero e inténtalo de nuevo.",
|
||||
"Group": "Grupo"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Retrait du texte",
|
||||
"Paragraph Margin": "Marge de paragraphe",
|
||||
"Override Book Layout": "Remplacer la mise en page du livre",
|
||||
"Add to Group": "Ajouter au groupe",
|
||||
"Untitled Group": "Groupe sans titre",
|
||||
"Group Books": "Regrouper les livres",
|
||||
"Remove From Group": "Retirer du groupe",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Échanger la zone de clic pour tourner",
|
||||
"No chapters detected.": "Aucun chapitre détecté.",
|
||||
"Failed to parse the EPUB file.": "Échec de l'analyse du fichier EPUB.",
|
||||
"This book format is not supported.": "Ce format de livre n'est pas pris en charge."
|
||||
"This book format is not supported.": "Ce format de livre n'est pas pris en charge.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Impossible de récupérer la traduction. Veuillez d'abord vous connecter et réessayer.",
|
||||
"Group": "Groupe"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "टेक्स्ट इंडेंट",
|
||||
"Paragraph Margin": "पैराग्राफ मार्जिन",
|
||||
"Override Book Layout": "पुस्तक लेआउट ओवरराइड करें",
|
||||
"Add to Group": "समूह में जोड़ें",
|
||||
"Untitled Group": "अभिनामित समूह",
|
||||
"Group Books": "समूह पुस्तकें",
|
||||
"Remove From Group": "समूह से हटाएं",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "क्लिक-टू-फ्लिप क्षेत्र स्वैप करें",
|
||||
"No chapters detected.": "कोई अध्याय नहीं मिला।",
|
||||
"Failed to parse the EPUB file.": "EPUB फ़ाइल को पार्स करने में विफल।",
|
||||
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।"
|
||||
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "अनुवाद प्राप्त करने में असमर्थ। कृपया पहले लॉग इन करें और पुनः प्रयास करें।",
|
||||
"Group": "समूह"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Indentasi Teks",
|
||||
"Paragraph Margin": "Margin Paragraf",
|
||||
"Override Book Layout": "Ganti Tata Letak Buku",
|
||||
"Add to Group": "Tambahkan ke Grup",
|
||||
"Untitled Group": "Grup Tanpa Judul",
|
||||
"Group Books": "Grupkan Buku",
|
||||
"Remove From Group": "Hapus dari Grup",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Tukar Area Klik untuk Membalik",
|
||||
"No chapters detected.": "Tidak ada bab yang terdeteksi.",
|
||||
"Failed to parse the EPUB file.": "Gagal mengurai file EPUB.",
|
||||
"This book format is not supported.": "Format buku ini tidak didukung."
|
||||
"This book format is not supported.": "Format buku ini tidak didukung.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Tidak dapat mengambil terjemahan. Silakan masuk terlebih dahulu dan coba lagi.",
|
||||
"Group": "Grup"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Rientro testo",
|
||||
"Paragraph Margin": "Margine paragrafo",
|
||||
"Override Book Layout": "Sovrascrivi layout libro",
|
||||
"Add to Group": "Aggiungi al gruppo",
|
||||
"Untitled Group": "Gruppo senza titolo",
|
||||
"Group Books": "Raggruppa libri",
|
||||
"Remove From Group": "Rimuovi dal",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Scambia area click per voltare pagina",
|
||||
"No chapters detected.": "Nessun capitolo rilevato.",
|
||||
"Failed to parse the EPUB file.": "Impossibile analizzare il file EPUB.",
|
||||
"This book format is not supported.": "Questo formato di libro non è supportato."
|
||||
"This book format is not supported.": "Questo formato di libro non è supportato.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Impossibile recuperare la traduzione. Accedi prima e riprova.",
|
||||
"Group": "Gruppo"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "テキストインデント",
|
||||
"Paragraph Margin": "段落マージン",
|
||||
"Override Book Layout": "書籍のレイアウトを上書き",
|
||||
"Add to Group": "グループに追加",
|
||||
"Untitled Group": "無題のグループ",
|
||||
"Group Books": "書籍をグループ化",
|
||||
"Remove From Group": "グループから削除",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "クリックめくりエリアを入れ替え",
|
||||
"No chapters detected.": "章が検出されません。",
|
||||
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました。",
|
||||
"This book format is not supported.": "この書籍形式はサポートされていません。"
|
||||
"This book format is not supported.": "この書籍形式はサポートされていません。",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "翻訳を取得できません。まずログインして再試行してください。",
|
||||
"Group": "グループ"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "들여쓰기",
|
||||
"Paragraph Margin": "단락 여백",
|
||||
"Override Book Layout": "책 레이아웃 덮어쓰기",
|
||||
"Add to Group": "그룹에 추가",
|
||||
"Untitled Group": "제목 없는 그룹",
|
||||
"Group Books": "책 그룹화",
|
||||
"Remove From Group": "그룹에서 제거",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "클릭-뒤집기 영역 교환",
|
||||
"No chapters detected.": "장 발견되지 않음.",
|
||||
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
|
||||
"This book format is not supported.": "이 책 형식은 지원되지 않습니다."
|
||||
"This book format is not supported.": "이 책 형식은 지원되지 않습니다.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "번역을 가져올 수 없습니다. 먼저 로그인하고 다시 시도하세요.",
|
||||
"Group": "그룹"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Wcięcie tekstu",
|
||||
"Paragraph Margin": "Margines akapitu",
|
||||
"Override Book Layout": "Nadpisz układ książki",
|
||||
"Add to Group": "Dodaj do grupy",
|
||||
"Untitled Group": "Grupa bez tytułu",
|
||||
"Group Books": "Grupuj książki",
|
||||
"Remove From Group": "Usuń z grupy",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Zamień obszar kliknięcia na przewracanie",
|
||||
"No chapters detected.": "Nie wykryto rozdziałów.",
|
||||
"Failed to parse the EPUB file.": "Nie udało się przeanalizować pliku EPUB.",
|
||||
"This book format is not supported.": "Ten format książki nie jest obsługiwany."
|
||||
"This book format is not supported.": "Ten format książki nie jest obsługiwany.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Nie można pobrać tłumaczenia. Proszę najpierw się zalogować i spróbować ponownie.",
|
||||
"Group": "Grupa"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Recuo de Texto",
|
||||
"Paragraph Margin": "Margem de Parágrafo",
|
||||
"Override Book Layout": "Sobrescrever Layout do Livro",
|
||||
"Add to Group": "Adicionar ao Grupo",
|
||||
"Untitled Group": "Grupo Sem Título",
|
||||
"Group Books": "Agrupar Livros",
|
||||
"Remove From Group": "Remover do Grupo",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Trocar Área de Clique para Virar",
|
||||
"No chapters detected.": "Nenhum capítulo detectado.",
|
||||
"Failed to parse the EPUB file.": "Falha ao analisar o arquivo EPUB.",
|
||||
"This book format is not supported.": "Este formato de livro não é suportado."
|
||||
"This book format is not supported.": "Este formato de livro não é suportado.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Não foi possível buscar a tradução. Faça login primeiro e tente novamente.",
|
||||
"Group": "Grupo"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Отступ текста",
|
||||
"Paragraph Margin": "Отступ абзаца",
|
||||
"Override Book Layout": "Переопределить макет книги",
|
||||
"Add to Group": "Добавить в группу",
|
||||
"Untitled Group": "Группа без названия",
|
||||
"Group Books": "Группировать книги",
|
||||
"Remove From Group": "Удалить из группы",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Переключить область клика для переворота",
|
||||
"No chapters detected.": "Не обнаружено глав.",
|
||||
"Failed to parse the EPUB file.": "Не удалось разобрать файл EPUB.",
|
||||
"This book format is not supported.": "Этот формат книги не поддерживается."
|
||||
"This book format is not supported.": "Этот формат книги не поддерживается.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Не удалось получить перевод. Пожалуйста, войдите в систему и попробуйте снова.",
|
||||
"Group": "Группа"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Metin Girintisi",
|
||||
"Paragraph Margin": "Paragraf Kenarı",
|
||||
"Override Book Layout": "Kitap Düzenini Geçersiz Kıl",
|
||||
"Add to Group": "Gruba Ekle",
|
||||
"Untitled Group": "Başlıksız Grup",
|
||||
"Group Books": "Kitapları Grupla",
|
||||
"Remove From Group": "Grubu Kaldır",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Tıkla-Çevir Alanını Değiştir",
|
||||
"No chapters detected.": "Bölüm bulunamadı.",
|
||||
"Failed to parse the EPUB file.": "EPUB dosyası ayrıştırılamadı.",
|
||||
"This book format is not supported.": "Bu kitap formatı desteklenmiyor."
|
||||
"This book format is not supported.": "Bu kitap formatı desteklenmiyor.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Çeviri alınamıyor. Lütfen önce giriş yapın ve tekrar deneyin.",
|
||||
"Group": "Grup"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Відступ тексту",
|
||||
"Paragraph Margin": "Відступ абзацу",
|
||||
"Override Book Layout": "Перевизначити макет книги",
|
||||
"Add to Group": "Додати до групи",
|
||||
"Untitled Group": "Група без назви",
|
||||
"Group Books": "Групувати книги",
|
||||
"Remove From Group": "Видалити з групи",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Поміняти місцями область кліку для перевороту",
|
||||
"No chapters detected.": "Не виявлено жодного розділу.",
|
||||
"Failed to parse the EPUB file.": "Не вдалося розібрати файл EPUB.",
|
||||
"This book format is not supported.": "Цей формат книги не підтримується."
|
||||
"This book format is not supported.": "Цей формат книги не підтримується.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Не вдалося отримати переклад. Будь ласка, спочатку увійдіть у систему та спробуйте ще раз.",
|
||||
"Group": "Група"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "Thụt lề",
|
||||
"Paragraph Margin": "Lề đoạn",
|
||||
"Override Book Layout": "Ghi đè bố cục sách",
|
||||
"Add to Group": "Thêm vào nhóm",
|
||||
"Untitled Group": "Nhóm không có tiêu đề",
|
||||
"Group Books": "Nhóm sách",
|
||||
"Remove From Group": "Xóa khỏi nhóm",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "Hoán đổi khu vực nhấp để lật trang",
|
||||
"No chapters detected.": "Không phát hiện thấy chương nào.",
|
||||
"Failed to parse the EPUB file.": "Không thể phân tích tệp EPUB.",
|
||||
"This book format is not supported.": "Định dạng sách này không được hỗ trợ."
|
||||
"This book format is not supported.": "Định dạng sách này không được hỗ trợ.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Không thể tải dịch vụ. Vui lòng đăng nhập trước và thử lại.",
|
||||
"Group": "Nhóm"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "首行缩进",
|
||||
"Paragraph Margin": "段间距",
|
||||
"Override Book Layout": "覆盖版面布局",
|
||||
"Add to Group": "添加到分组",
|
||||
"Untitled Group": "无标题分组",
|
||||
"Group Books": "分组书籍",
|
||||
"Remove From Group": "从分组中移除",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "交换点击翻页区域",
|
||||
"No chapters detected.": "未检测到章节。",
|
||||
"Failed to parse the EPUB file.": "无法解析 EPUB 文件。",
|
||||
"This book format is not supported.": "不支持此书籍格式。"
|
||||
"This book format is not supported.": "不支持此书籍格式。",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "无法获取翻译。请登录后重试。",
|
||||
"Group": "分组"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "首行縮進",
|
||||
"Paragraph Margin": "段間距",
|
||||
"Override Book Layout": "覆蓋版面佈局",
|
||||
"Add to Group": "添加到組",
|
||||
"Untitled Group": "無標題組",
|
||||
"Group Books": "分組書籍",
|
||||
"Remove From Group": "從組中移除",
|
||||
@@ -264,5 +263,7 @@
|
||||
"Swap Click-to-Flip Area": "交換點擊翻頁區域",
|
||||
"No chapters detected.": "未檢測到章節。",
|
||||
"Failed to parse the EPUB file.": "無法解析 EPUB 文件。",
|
||||
"This book format is not supported.": "不支援此書籍格式。"
|
||||
"This book format is not supported.": "不支援此書籍格式。",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "無法獲取翻譯。請先登入然後重試。",
|
||||
"Group": "分組"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.30": {
|
||||
"date": "2025-04-05",
|
||||
"notes": [
|
||||
"Added fixed storage quata when running the app in self-hosted mode",
|
||||
"Link colors will only change when you override fonts",
|
||||
"Added cache for native files for better performance"
|
||||
]
|
||||
},
|
||||
"0.9.29": {
|
||||
"date": "2025-04-02",
|
||||
"notes": [
|
||||
"Improved Login on Android without needing to rely on external Chrome browser",
|
||||
"Local large book files now load more efficiently on Android",
|
||||
"Balanced the load of online service between different types of users to keep things running smoothly for everyone"
|
||||
]
|
||||
},
|
||||
"0.9.28": {
|
||||
"date": "2025-03-30",
|
||||
"notes": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "Readest"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "Your online library"
|
||||
authors = ["Bilingify LLC"]
|
||||
license = ""
|
||||
@@ -15,7 +15,7 @@ name = "readestlib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
tauri-build = { version = "2.1.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
@@ -31,24 +31,21 @@ reqwest = { version = "0.12", default-features = false, features = [
|
||||
"stream",
|
||||
] }
|
||||
# FIXME: remove the devtools feature in production
|
||||
tauri = { version = "2.1.1", features = [ "protocol-asset", "devtools"] }
|
||||
tauri-build = "2.0.3"
|
||||
tauri-plugin-log = "2.0.2"
|
||||
tauri-plugin-fs = "2.0.3"
|
||||
tauri-plugin-dialog = "2.0.3"
|
||||
tauri-plugin-os = "2.0.1"
|
||||
tauri-plugin-http = "2.0.3"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri = { version = "2.4.1", features = [ "protocol-asset", "devtools"] }
|
||||
tauri-build = "2.1.1"
|
||||
tauri-plugin-log = "2.3.1"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-http = "2.4.3"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-oauth = "2"
|
||||
tauri-plugin-opener = "2.2.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2.2.3"
|
||||
tauri-plugin-haptics = "2.2.4"
|
||||
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
[patch.crates-io]
|
||||
tauri = { path = "../../../packages/tauri/crates/tauri" }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
cocoa = "0.25"
|
||||
@@ -57,7 +54,7 @@ rand = "0.8"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
tauri-plugin-single-instance = "2.2.0"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-window-state = "2.2.0"
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-updater = "2.7.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
font-enumeration = "0.9.0"
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"log:default",
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"process:allow-exit",
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="oauth">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="readest" android:host="auth-callback" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
+18
@@ -2,10 +2,14 @@ package com.bilingify.readest
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import app.tauri.plugin.JSObject
|
||||
import com.readest.native_bridge.NativeBridgePlugin
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -15,6 +19,20 @@ class MainActivity : TauriActivity() {
|
||||
hideSystemUI()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "readest" && uri.host == "auth-callback") {
|
||||
val result = JSObject().apply {
|
||||
put("redirectUrl", uri.toString())
|
||||
}
|
||||
|
||||
NativeBridgePlugin.pendingInvoke?.resolve(result)
|
||||
NativeBridgePlugin.pendingInvoke = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.decorView.windowInsetsController?.let { controller ->
|
||||
|
||||
+1
-1
@@ -33,9 +33,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
|
||||
-3
@@ -3,7 +3,4 @@ package com.readest.native_bridge
|
||||
import android.util.Log
|
||||
|
||||
class NativeBridge {
|
||||
fun auth_with_safari(value: String): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -1,28 +1,77 @@
|
||||
package com.readest.native_bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import java.io.*
|
||||
|
||||
@InvokeArg
|
||||
class SafariAuthRequestArgs {
|
||||
class AuthRequestArgs {
|
||||
var authUrl: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class CopyURIRequestArgs {
|
||||
var uri: String? = null
|
||||
var dst: String? = null
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
private val implementation = NativeBridge()
|
||||
private var redirectScheme = "readest"
|
||||
private var redirectHost = "auth-callback"
|
||||
|
||||
companion object {
|
||||
var pendingInvoke: Invoke? = null
|
||||
}
|
||||
|
||||
@Command
|
||||
fun auth_with_safari(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SafariAuthRequestArgs::class.java)
|
||||
fun auth_with_custom_tab(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(AuthRequestArgs::class.java)
|
||||
val uri = Uri.parse(args.authUrl)
|
||||
|
||||
val customTabsIntent = CustomTabsIntent.Builder().build()
|
||||
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
|
||||
Log.d("NativeBridgePlugin", "Launching OAuth URL: ${args.authUrl}")
|
||||
customTabsIntent.launchUrl(activity, uri)
|
||||
|
||||
pendingInvoke = invoke
|
||||
}
|
||||
|
||||
@Command
|
||||
fun copy_uri_to_path(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(CopyURIRequestArgs::class.java)
|
||||
val ret = JSObject()
|
||||
ret.put("redirectUrl", implementation.auth_with_safari(args.authUrl ?: ""))
|
||||
try {
|
||||
val uri = Uri.parse(args.uri ?: "")
|
||||
val dst = File(args.dst ?: "")
|
||||
val inputStream = activity.contentResolver.openInputStream(uri)
|
||||
|
||||
if (inputStream != null) {
|
||||
dst.outputStream().use { output ->
|
||||
inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
ret.put("success", true)
|
||||
} else {
|
||||
ret.put("success", false)
|
||||
ret.put("error", "Failed to open input stream from URI")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ret.put("success", false)
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
const COMMANDS: &[&str] = &["auth_with_safari"];
|
||||
const COMMANDS: &[&str] = &[
|
||||
"auth_with_safari",
|
||||
"auth_with_custom_tab",
|
||||
"copy_uri_to_path",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.build();
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.build();
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-auth-with-custom-tab"
|
||||
description = "Enables the auth_with_custom_tab command without any pre-configured scope."
|
||||
commands.allow = ["auth_with_custom_tab"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-auth-with-custom-tab"
|
||||
description = "Denies the auth_with_custom_tab command without any pre-configured scope."
|
||||
commands.deny = ["auth_with_custom_tab"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-copy-uri-to-path"
|
||||
description = "Enables the copy_uri_to_path command without any pre-configured scope."
|
||||
commands.allow = ["copy_uri_to_path"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-copy-uri-to-path"
|
||||
description = "Denies the copy_uri_to_path command without any pre-configured scope."
|
||||
commands.deny = ["copy_uri_to_path"]
|
||||
+56
@@ -2,7 +2,11 @@
|
||||
|
||||
Default permissions for the plugin
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-auth-with-safari`
|
||||
- `allow-auth-with-custom-tab`
|
||||
- `allow-copy-uri-to-path`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -13,6 +17,32 @@ Default permissions for the plugin
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-auth-with-custom-tab`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the auth_with_custom_tab command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-auth-with-custom-tab`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the auth_with_custom_tab command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -36,6 +66,32 @@ Enables the auth_with_safari command without any pre-configured scope.
|
||||
|
||||
Denies the auth_with_safari command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-copy-uri-to-path`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the copy_uri_to_path command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-copy-uri-to-path`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the copy_uri_to_path command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = ["allow-auth-with-safari"]
|
||||
permissions = ["allow-auth-with-safari", "allow-auth-with-custom-tab", "allow-copy-uri-to-path"]
|
||||
|
||||
+31
-4
@@ -294,20 +294,47 @@
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the auth_with_custom_tab command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-auth-with-custom-tab",
|
||||
"markdownDescription": "Enables the auth_with_custom_tab command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the auth_with_custom_tab command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-auth-with-custom-tab",
|
||||
"markdownDescription": "Denies the auth_with_custom_tab command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the auth_with_safari command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-auth-with-safari"
|
||||
"const": "allow-auth-with-safari",
|
||||
"markdownDescription": "Enables the auth_with_safari command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the auth_with_safari command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-auth-with-safari"
|
||||
"const": "deny-auth-with-safari",
|
||||
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
"const": "allow-copy-uri-to-path",
|
||||
"markdownDescription": "Enables the copy_uri_to_path command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-copy-uri-to-path",
|
||||
"markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,23 @@ use crate::Result;
|
||||
#[command]
|
||||
pub(crate) async fn auth_with_safari<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: SafariAuthRequest,
|
||||
) -> Result<SafariAuthResponse> {
|
||||
payload: AuthRequest,
|
||||
) -> Result<AuthResponse> {
|
||||
app.native_bridge().auth_with_safari(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn auth_with_custom_tab<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: AuthRequest,
|
||||
) -> Result<AuthResponse> {
|
||||
app.native_bridge().auth_with_custom_tab(payload)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn copy_uri_to_path<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: CopyURIRequest,
|
||||
) -> Result<CopyURIResponse> {
|
||||
app.native_bridge().copy_uri_to_path(payload)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,15 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
pub struct NativeBridge<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn auth_with_safari(
|
||||
&self,
|
||||
_payload: SafariAuthRequest,
|
||||
) -> crate::Result<SafariAuthResponse> {
|
||||
pub fn auth_with_safari(&self, _payload: AuthRequest) -> crate::Result<AuthResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn auth_with_custom_tab(&self, _payload: AuthRequest) -> crate::Result<AuthResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn copy_uri_to_path(&self, _payload: CopyURIRequest) -> crate::Result<CopyURIResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ impl<R: Runtime, T: Manager<R>> crate::NativeBridgeExt<R> for T {
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("native-bridge")
|
||||
.invoke_handler(tauri::generate_handler![commands::auth_with_safari])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::auth_with_safari,
|
||||
commands::auth_with_custom_tab,
|
||||
commands::copy_uri_to_path,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let native_bridge = mobile::init(app, api)?;
|
||||
|
||||
@@ -25,12 +25,25 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
pub struct NativeBridge<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn auth_with_safari(
|
||||
&self,
|
||||
payload: SafariAuthRequest,
|
||||
) -> crate::Result<SafariAuthResponse> {
|
||||
pub fn auth_with_safari(&self, payload: AuthRequest) -> crate::Result<AuthResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("auth_with_safari", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn auth_with_custom_tab(&self, payload: AuthRequest) -> crate::Result<AuthResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("auth_with_custom_tab", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn copy_uri_to_path(&self, payload: CopyURIRequest) -> crate::Result<CopyURIResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("copy_uri_to_path", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,26 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SafariAuthRequest {
|
||||
pub struct AuthRequest {
|
||||
pub auth_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SafariAuthResponse {
|
||||
pub struct AuthResponse {
|
||||
pub redirect_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CopyURIRequest {
|
||||
pub uri: String,
|
||||
pub dst: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CopyURIResponse {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -217,14 +217,11 @@ pub fn run() {
|
||||
app.deep_link().register_all()?;
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
|
||||
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oau
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
|
||||
import { authWithSafari } from './utils/safariAuth';
|
||||
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
|
||||
import { READEST_WEB_BASE_URL } from '@/services/constants';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
|
||||
@@ -42,7 +42,7 @@ interface ProviderLoginProp {
|
||||
}
|
||||
|
||||
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
|
||||
const DEEPLINK_CALLBACK = 'readest://auth/callback';
|
||||
const DEEPLINK_CALLBACK = 'readest://auth-callback';
|
||||
|
||||
const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
|
||||
return (
|
||||
@@ -75,10 +75,8 @@ export default function AuthPage() {
|
||||
|
||||
const getTauriRedirectTo = (isOAuth: boolean) => {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
|
||||
if (appService?.isIOSApp) {
|
||||
if (appService?.isMobile) {
|
||||
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
|
||||
} else if (appService?.isAndroidApp) {
|
||||
return WEB_AUTH_CALLBACK;
|
||||
}
|
||||
return DEEPLINK_CALLBACK;
|
||||
}
|
||||
@@ -135,6 +133,11 @@ export default function AuthPage() {
|
||||
if (res) {
|
||||
handleOAuthUrl(res.redirectUrl);
|
||||
}
|
||||
} else if (appService?.isAndroidApp) {
|
||||
const res = await authWithCustomTab({ authUrl: data.url });
|
||||
if (res) {
|
||||
handleOAuthUrl(res.redirectUrl);
|
||||
}
|
||||
} else {
|
||||
await openUrl(data.url);
|
||||
}
|
||||
@@ -304,14 +307,15 @@ export default function AuthPage() {
|
||||
return isTauriAppPlatform() ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 border-base-200 flex h-dvh w-full select-none flex-col items-center border',
|
||||
'fixed inset-0 z-0 flex select-none flex-col items-center overflow-y-auto',
|
||||
'bg-base-100 border-base-200 border',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'fixed flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
@@ -329,7 +333,10 @@ export default function AuthPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
|
||||
<div
|
||||
className={clsx('z-20 pb-8', appService?.hasTrafficLight ? 'mt-24' : 'mt-12')}
|
||||
style={{ maxWidth: '420px' }}
|
||||
>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
handleSignIn={tauriSignIn}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface AuthRequest {
|
||||
authUrl: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
export async function authWithSafari(request: AuthRequest): Promise<AuthResponse> {
|
||||
const result = await invoke<AuthResponse>('plugin:native-bridge|auth_with_safari', {
|
||||
payload: request,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function authWithCustomTab(request: AuthRequest): Promise<AuthResponse> {
|
||||
const result = await invoke<AuthResponse>('plugin:native-bridge|auth_with_custom_tab', {
|
||||
payload: request,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface SafariAuthRequest {
|
||||
authUrl: string;
|
||||
}
|
||||
|
||||
export interface SafariAuthResponse {
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
export async function authWithSafari(request: SafariAuthRequest): Promise<SafariAuthResponse> {
|
||||
const result = await invoke<SafariAuthResponse>('plugin:native-bridge|auth_with_safari', {
|
||||
payload: request,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -210,7 +210,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
)}
|
||||
>
|
||||
<LuFolderPlus />
|
||||
<div>{_('Add to Group')}</div>
|
||||
<div>{_('Group')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteSelectedBooks}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/naviga
|
||||
import { Book } from '@/types/book';
|
||||
import { AppService } from '@/types/system';
|
||||
import { navigateToLogin, navigateToReader } from '@/utils/nav';
|
||||
import { getBaseFilename, listFormater } from '@/utils/book';
|
||||
import { getFilename, listFormater } from '@/utils/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { ProgressPayload } from '@/utils/transfer';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
@@ -311,7 +311,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
}
|
||||
} catch (error) {
|
||||
const filename = typeof file === 'string' ? file : file.name;
|
||||
const baseFilename = getBaseFilename(filename);
|
||||
const baseFilename = getFilename(filename);
|
||||
failedFiles.push(baseFilename);
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
@@ -332,7 +332,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
};
|
||||
|
||||
const selectFilesTauri = async () => {
|
||||
return appService?.selectFiles('Select Books', SUPPORTED_FILE_EXTS);
|
||||
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
|
||||
return files.filter((file) => SUPPORTED_FILE_EXTS.some((ext) => file.endsWith(`.${ext}`)));
|
||||
};
|
||||
|
||||
const selectFilesWeb = () => {
|
||||
@@ -453,7 +455,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
let files;
|
||||
|
||||
if (isTauriAppPlatform()) {
|
||||
if (appService?.isMobile) {
|
||||
if (appService?.isIOSApp) {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
} else {
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
|
||||
@@ -28,7 +28,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
|
||||
@@ -68,7 +68,10 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<div
|
||||
id={`gridcell-${bookKey}`}
|
||||
key={bookKey}
|
||||
className={`${appService?.hasRoundedWindow ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
|
||||
className={clsx(
|
||||
'relative h-full w-full overflow-hidden',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
{isBookmarked && <Ribbon width={`${horizontalGapPercent}%`} />}
|
||||
<HeaderBar
|
||||
@@ -80,7 +83,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
|
||||
/>
|
||||
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
|
||||
<FootnotePopup bookKey={bookKey} bookDoc={bookDoc} />
|
||||
{viewSettings.vertical && viewSettings.scrolled && (
|
||||
<>
|
||||
<div
|
||||
@@ -138,6 +140,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
/>
|
||||
)}
|
||||
<Annotator bookKey={bookKey} />
|
||||
<FootnotePopup bookKey={bookKey} bookDoc={bookDoc} />
|
||||
<FooterBar
|
||||
bookKey={bookKey}
|
||||
bookFormat={book.format}
|
||||
|
||||
@@ -78,7 +78,7 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const docLoadHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
console.log('doc loaded:', detail);
|
||||
console.log('doc index loaded:', detail.index);
|
||||
if (detail.doc) {
|
||||
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
@@ -13,10 +13,12 @@ import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import ReaderContent from './ReaderContent';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const { getVisibleLibrary, setLibrary } = useLibraryStore();
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
@@ -45,7 +47,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
<div
|
||||
className={clsx(
|
||||
`reader-page bg-base-100 text-base-content select-none`,
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
<Suspense>
|
||||
|
||||
@@ -21,9 +21,7 @@ const LANGUAGES = {
|
||||
RU: 'Russian',
|
||||
AR: 'Arabic',
|
||||
TR: 'Turkish',
|
||||
HI: 'Hindi',
|
||||
ID: 'Indonesian',
|
||||
VI: 'Vietnamese',
|
||||
KO: 'Korean',
|
||||
JA: 'Japanese',
|
||||
'ZH-HANS': 'Chinese (Simplified)',
|
||||
@@ -105,7 +103,11 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
setTranslation(translatedText);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(_('Unable to fetch the translation. Try again later.'));
|
||||
if (!token) {
|
||||
setError(_('Unable to fetch the translation. Please log in first and try again.'));
|
||||
} else {
|
||||
setError(_('Unable to fetch the translation. Try again later.'));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const handleInputBlur = () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
setInputFocusInAndroid(false);
|
||||
setTimeout(() => {
|
||||
setInputFocusInAndroid(false);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,7 +147,10 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('my-4 w-full space-y-6', inputFocusInAndroid && 'h-[50%] overflow-y-auto')}
|
||||
className={clsx(
|
||||
'my-4 w-full space-y-6',
|
||||
inputFocusInAndroid && 'h-[50%] overflow-y-auto pb-[200px]',
|
||||
)}
|
||||
>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Language')}</h2>
|
||||
|
||||
@@ -159,7 +159,8 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 border-base-200 flex h-dvh w-full select-none flex-col items-center border',
|
||||
'fixed inset-0 z-0 flex select-none flex-col items-center overflow-y-auto',
|
||||
'bg-base-100 border-base-200 border',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
@@ -184,7 +185,7 @@ const ProfilePage = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='w-full max-w-4xl px-4 py-10'>
|
||||
<div className='w-full min-w-60 max-w-4xl px-4 py-10'>
|
||||
<div className='bg-base-200 overflow-hidden rounded-lg p-2 shadow-md sm:p-6'>
|
||||
<div className='p-2 sm:p-6'>
|
||||
<div className='mb-8 flex flex-col items-center gap-x-6 gap-y-4 md:flex-row md:items-start'>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { supabase } from '@/utils/supabase';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
import { query as deeplQuery } from '@/utils/deepl';
|
||||
|
||||
const DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
const DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
|
||||
const DEFAULT_DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
const DEFAULT_DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
@@ -26,34 +26,46 @@ const getDeepLAPIKey = (keys: string | undefined) => {
|
||||
};
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const { user, token } = await getUserAndToken(req.headers['authorization']);
|
||||
let deeplApiUrl = DEEPL_FREE_API;
|
||||
const { DEEPL_PRO_API, DEEPL_FREE_API } = process.env;
|
||||
const deepFreeApiUrl = DEEPL_FREE_API || DEFAULT_DEEPL_FREE_API;
|
||||
const deeplProApiUrl = DEEPL_PRO_API || DEFAULT_DEEPL_PRO_API;
|
||||
|
||||
let deeplApiUrl = deepFreeApiUrl;
|
||||
let userPlan = 'free';
|
||||
if (user && token) {
|
||||
userPlan = getUserPlan(token);
|
||||
if (userPlan !== 'free') deeplApiUrl = DEEPL_PRO_API;
|
||||
if (userPlan === 'pro') deeplApiUrl = deeplProApiUrl;
|
||||
} else {
|
||||
res.status(403).json({ error: 'Not authenticated' });
|
||||
}
|
||||
const deeplAuthKey =
|
||||
deeplApiUrl === DEEPL_PRO_API
|
||||
deeplApiUrl === deeplProApiUrl
|
||||
? getDeepLAPIKey(process.env['DEEPL_PRO_API_KEYS'])
|
||||
: getDeepLAPIKey(process.env['DEEPL_FREE_API_KEYS']);
|
||||
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
|
||||
const {
|
||||
text,
|
||||
source_lang: sourceLang = 'auto',
|
||||
target_lang: targetLang = 'en',
|
||||
}: { text: string[]; source_lang: string; target_lang: string } = req.body;
|
||||
try {
|
||||
if (targetLang.toLowerCase().includes('zh')) {
|
||||
if (user && token) {
|
||||
console.log('deeplApiUrl', deeplApiUrl);
|
||||
const response = await fetch(deeplApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${deeplAuthKey}`,
|
||||
'x-fingerprint': process.env['DEEPL_X_FINGERPRINT'] || '',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: req.method === 'POST' ? JSON.stringify(req.body) : undefined,
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(response.status);
|
||||
res.json(await response.json());
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
INIT_BOOK_CONFIG,
|
||||
formatTitle,
|
||||
formatAuthors,
|
||||
getFilename,
|
||||
} from '@/utils/book';
|
||||
import { RemoteFile } from '@/utils/file';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
import {
|
||||
@@ -32,9 +32,10 @@ import {
|
||||
DEFAULT_SYSTEM_SETTINGS,
|
||||
DEFAULT_CJK_VIEW_SETTINGS,
|
||||
} from './constants';
|
||||
import { getOSPlatform, isCJKEnv, isValidURL } from '@/utils/misc';
|
||||
import { getOSPlatform, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/libs/storage';
|
||||
import { ClosableFile } from '@/utils/file';
|
||||
import { ProgressHandler } from '@/utils/transfer';
|
||||
import { TxtToEpubConverter } from '@/utils/txt';
|
||||
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
|
||||
@@ -117,6 +118,12 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
this.localBooksDir = settings.localBooksDir;
|
||||
this.fs.getPrefix = (baseDir: BaseDir) => {
|
||||
if (baseDir === 'Books') {
|
||||
return this.localBooksDir;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -127,7 +134,12 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
async importBook(
|
||||
file: string | File, // file path/url or file object
|
||||
// file might be:
|
||||
// 1. absolute path for local file
|
||||
// 2. remote url
|
||||
// 3. content provider uri
|
||||
// 4. File object from browsers
|
||||
file: string | File,
|
||||
books: Book[],
|
||||
saveBook: boolean = true,
|
||||
saveCover: boolean = true,
|
||||
@@ -140,10 +152,14 @@ export abstract class BaseAppService implements AppService {
|
||||
let filename: string;
|
||||
let fileobj: File;
|
||||
|
||||
if (transient && typeof file !== 'string') {
|
||||
throw new Error('Transient import is only supported for file paths');
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof file === 'string') {
|
||||
filename = file;
|
||||
fileobj = await new RemoteFile(this.fs.getURL(file), file).open();
|
||||
filename = getFilename(file);
|
||||
fileobj = await this.fs.openFile(file, 'None');
|
||||
} else {
|
||||
filename = file.name;
|
||||
fileobj = file;
|
||||
@@ -181,14 +197,28 @@ export abstract class BaseAppService implements AppService {
|
||||
downloadedAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
// update book metadata when reimporting the same book
|
||||
if (existingBook) {
|
||||
existingBook.title = book.title;
|
||||
existingBook.author = book.author;
|
||||
}
|
||||
|
||||
if (!(await this.fs.exists(getDir(book), 'Books'))) {
|
||||
await this.fs.createDir(getDir(book), 'Books');
|
||||
}
|
||||
if (saveBook && (!(await this.fs.exists(getLocalBookFilename(book), 'Books')) || overwrite)) {
|
||||
if (typeof file === 'string' && !isValidURL(file) && !filename.endsWith('.txt')) {
|
||||
if (
|
||||
saveBook &&
|
||||
!transient &&
|
||||
(!(await this.fs.exists(getLocalBookFilename(book), 'Books')) || overwrite)
|
||||
) {
|
||||
if (typeof file === 'string' && isContentURI(file)) {
|
||||
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
|
||||
} else if (filename.endsWith('.txt')) {
|
||||
await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj);
|
||||
} else if (typeof file === 'string' && !isValidURL(file)) {
|
||||
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
|
||||
} else {
|
||||
await this.fs.writeFile(getLocalBookFilename(book), 'Books', await fileobj.arrayBuffer());
|
||||
await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj);
|
||||
}
|
||||
}
|
||||
if (saveCover && (!(await this.fs.exists(getCoverFilename(book), 'Books')) || overwrite)) {
|
||||
@@ -201,15 +231,24 @@ export abstract class BaseAppService implements AppService {
|
||||
if (!existingBook) {
|
||||
await this.saveBookConfig(book, INIT_BOOK_CONFIG);
|
||||
books.splice(0, 0, book);
|
||||
} else {
|
||||
existingBook.title = book.title;
|
||||
existingBook.author = book.author;
|
||||
}
|
||||
|
||||
if (typeof file === 'string' && isValidURL(file)) {
|
||||
book.url = file;
|
||||
// update file links with url or path or content uri
|
||||
if (typeof file === 'string') {
|
||||
if (isValidURL(file)) {
|
||||
book.url = file;
|
||||
if (existingBook) existingBook.url = file;
|
||||
}
|
||||
if (transient) {
|
||||
book.filePath = file;
|
||||
if (existingBook) existingBook.filePath = file;
|
||||
}
|
||||
}
|
||||
book.coverImageUrl = await this.generateCoverImageUrl(book);
|
||||
const f = file as ClosableFile;
|
||||
if (f && f.close) {
|
||||
await f.close();
|
||||
}
|
||||
|
||||
return book;
|
||||
} catch (error) {
|
||||
@@ -244,16 +283,14 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
async uploadFileToCloud(lfp: string, cfp: string, handleProgress: ProgressHandler, hash: string) {
|
||||
let file: File;
|
||||
if (this.appPlatform === 'web') {
|
||||
const content = await this.fs.readFile(lfp, 'Books', 'binary');
|
||||
file = new File([content], cfp);
|
||||
} else {
|
||||
file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${lfp}`), cfp).open();
|
||||
}
|
||||
console.log('Uploading file:', lfp, 'to', cfp);
|
||||
const file = await this.fs.openFile(lfp, 'Books', cfp);
|
||||
const localFullpath = `${this.localBooksDir}/${lfp}`;
|
||||
await uploadFile(file, localFullpath, handleProgress, hash);
|
||||
const f = file as ClosableFile;
|
||||
if (f && f.close) {
|
||||
await f.close();
|
||||
}
|
||||
}
|
||||
|
||||
async uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void> {
|
||||
@@ -270,7 +307,7 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
if (!bookFileExist && book.url) {
|
||||
// download the book from the URL
|
||||
const fileobj = await new RemoteFile(book.url).open();
|
||||
const fileobj = await this.fs.openFile(book.url, 'None');
|
||||
await this.fs.writeFile(getLocalBookFilename(book), 'Books', await fileobj.arrayBuffer());
|
||||
bookFileExist = true;
|
||||
}
|
||||
@@ -362,15 +399,11 @@ export abstract class BaseAppService implements AppService {
|
||||
let file: File;
|
||||
const fp = getLocalBookFilename(book);
|
||||
if (await this.fs.exists(fp, 'Books')) {
|
||||
// TODO: fix random access for android
|
||||
if (this.appPlatform === 'web' || getOSPlatform() === 'android') {
|
||||
const content = await this.fs.readFile(fp, 'Books', 'binary');
|
||||
file = new File([content], fp);
|
||||
} else {
|
||||
file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open();
|
||||
}
|
||||
file = await this.fs.openFile(fp, 'Books');
|
||||
} else if (book.filePath) {
|
||||
file = await this.fs.openFile(book.filePath, 'None');
|
||||
} else if (book.url) {
|
||||
file = await new RemoteFile(book.url).open();
|
||||
file = await this.fs.openFile(book.url, 'None');
|
||||
} else {
|
||||
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
|
||||
}
|
||||
@@ -397,6 +430,10 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
const { file } = (await this.loadBookContent(book, settings)) as BookContent;
|
||||
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
|
||||
const f = file as ClosableFile;
|
||||
if (f && f.close) {
|
||||
await f.close();
|
||||
}
|
||||
return bookDoc.metadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
exists,
|
||||
mkdir,
|
||||
open as openFile,
|
||||
readTextFile,
|
||||
readFile,
|
||||
writeTextFile,
|
||||
@@ -9,16 +10,19 @@ import {
|
||||
remove,
|
||||
copyFile,
|
||||
BaseDirectory,
|
||||
WriteFileOptions,
|
||||
} from '@tauri-apps/plugin-fs';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { open, message } from '@tauri-apps/plugin-dialog';
|
||||
import { open as openDialog, message } from '@tauri-apps/plugin-dialog';
|
||||
import { join, appDataDir } from '@tauri-apps/api/path';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
import { isValidURL } from '@/utils/misc';
|
||||
import { isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getCoverFilename, getFilename } from '@/utils/book';
|
||||
import { copyURIToPath } from '@/utils/bridge';
|
||||
import { NativeFile, RemoteFile } from '@/utils/file';
|
||||
|
||||
import { BaseAppService } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
@@ -47,6 +51,12 @@ const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDi
|
||||
fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`,
|
||||
base,
|
||||
};
|
||||
case 'None':
|
||||
return {
|
||||
baseDir: 0,
|
||||
fp,
|
||||
base,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
baseDir: BaseDirectory.Temp,
|
||||
@@ -64,9 +74,45 @@ export const nativeFileSystem: FileSystem = {
|
||||
const content = await this.readFile(path, base, 'binary');
|
||||
return URL.createObjectURL(new Blob([content]));
|
||||
},
|
||||
async openFile(path: string, base: BaseDir, name?: string) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const fname = name || getFilename(fp);
|
||||
if (isValidURL(path)) {
|
||||
return await new RemoteFile(path, name).open();
|
||||
} else if (isContentURI(path)) {
|
||||
return await new NativeFile(fp, fname, base ? baseDir : null).open();
|
||||
} else {
|
||||
const prefix = this.getPrefix(base);
|
||||
if (prefix && OS_TYPE !== 'android') {
|
||||
// NOTE: RemoteFile currently performs about 2× faster than NativeFile
|
||||
// due to an unresolved performance issue in Tauri (see tauri-apps/tauri#9190).
|
||||
// Once the bug is resolved, we should switch back to using NativeFile.
|
||||
// RemoteFile is not usable on Android due to unknown issues of range fetch with Android WebView.
|
||||
const absolutePath = await join(prefix, path);
|
||||
return await new RemoteFile(this.getURL(absolutePath), fname).open();
|
||||
} else {
|
||||
return await new NativeFile(fp, fname, base ? baseDir : null).open();
|
||||
}
|
||||
}
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(dstPath, base);
|
||||
await copyFile(srcPath, fp, base && { toPathBaseDir: baseDir });
|
||||
if (isContentURI(srcPath)) {
|
||||
const prefix = this.getPrefix(base);
|
||||
if (!prefix) {
|
||||
throw new Error('Invalid base directory');
|
||||
}
|
||||
const res = await copyURIToPath({
|
||||
uri: srcPath,
|
||||
dst: `${prefix}/${dstPath}`,
|
||||
});
|
||||
if (!res.success) {
|
||||
console.error('Failed to copy file:', res);
|
||||
throw new Error('Failed to copy file');
|
||||
}
|
||||
} else {
|
||||
const { fp, baseDir } = resolvePath(dstPath, base);
|
||||
await copyFile(srcPath, fp, base && { toPathBaseDir: baseDir });
|
||||
}
|
||||
},
|
||||
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
@@ -75,12 +121,32 @@ export const nativeFileSystem: FileSystem = {
|
||||
? (readTextFile(fp, base && { baseDir }) as Promise<string>)
|
||||
: ((await readFile(fp, base && { baseDir })).buffer as ArrayBuffer);
|
||||
},
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer) {
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File) {
|
||||
// NOTE: this could be very slow for large files and might block the UI thread
|
||||
// so do not use this for large files
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return typeof content === 'string'
|
||||
? writeTextFile(fp, content, base && { baseDir })
|
||||
: writeFile(fp, new Uint8Array(content), base && { baseDir });
|
||||
if (typeof content === 'string') {
|
||||
return writeTextFile(fp, content, base && { baseDir });
|
||||
} else if (content instanceof File) {
|
||||
const writeOptions = { write: true, create: true, baseDir } as WriteFileOptions;
|
||||
// TODO: use writeFile directly when @tauri-apps/plugin-fs@2.2.1 is released
|
||||
// return writeFile(fp, content.stream(), base && writeOptions);
|
||||
const file = await openFile(fp, base && writeOptions);
|
||||
const reader = content.stream().getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
await file.write(value);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
await file.close();
|
||||
}
|
||||
} else {
|
||||
return writeFile(fp, new Uint8Array(content), base && { baseDir });
|
||||
}
|
||||
},
|
||||
async removeFile(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
@@ -118,6 +184,9 @@ export const nativeFileSystem: FileSystem = {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getPrefix() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export class NativeAppService extends BaseAppService {
|
||||
@@ -145,7 +214,7 @@ export class NativeAppService extends BaseAppService {
|
||||
}
|
||||
|
||||
async selectFiles(name: string, extensions: string[]): Promise<string[]> {
|
||||
const selected = await open({
|
||||
const selected = await openDialog({
|
||||
multiple: true,
|
||||
filters: [{ name, extensions }],
|
||||
});
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
import type { Transformer } from './types';
|
||||
|
||||
const punctuationMap: Record<string, string> = {
|
||||
',': '︐', // full-width comma
|
||||
'。': '︒', // full stop
|
||||
'、': '︑', // enumeration comma
|
||||
':': '︓', // colon
|
||||
';': '︔', // semicolon
|
||||
'!': '︕', // exclamation
|
||||
'?': '︖', // question
|
||||
'(': '︵',
|
||||
')': '︶',
|
||||
'《': '︽',
|
||||
'》': '︾',
|
||||
'「': '﹁',
|
||||
'」': '﹂',
|
||||
'『': '﹃',
|
||||
'』': '﹄',
|
||||
'“': '﹁',
|
||||
'”': '﹂',
|
||||
'‘': '﹃',
|
||||
'’': '﹄',
|
||||
'“': '﹃',
|
||||
'”': '﹄',
|
||||
'‘': '﹁',
|
||||
'’': '﹂',
|
||||
};
|
||||
|
||||
export const punctuationTransformer: Transformer = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Book } from '@/types/book';
|
||||
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
import { getOSPlatform, isValidURL } from '@/utils/misc';
|
||||
import { RemoteFile } from '@/utils/file';
|
||||
|
||||
import { isPWA } from './environment';
|
||||
import { BaseAppService } from './appService';
|
||||
@@ -53,6 +54,14 @@ const indexedDBFileSystem: FileSystem = {
|
||||
return path;
|
||||
}
|
||||
},
|
||||
async openFile(path: string, base: BaseDir, filename?: string) {
|
||||
if (isValidURL(path)) {
|
||||
return await new RemoteFile(path, filename).open();
|
||||
} else {
|
||||
const content = await this.readFile(path, base, 'binary');
|
||||
return new File([content], filename || path);
|
||||
}
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(dstPath, base);
|
||||
const db = await openIndexedDB();
|
||||
@@ -174,6 +183,9 @@ const indexedDBFileSystem: FileSystem = {
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
getPrefix() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export class WebAppService extends BaseAppService {
|
||||
|
||||
@@ -4,8 +4,10 @@ export type HighlightStyle = 'highlight' | 'underline' | 'squiggly';
|
||||
export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet';
|
||||
|
||||
export interface Book {
|
||||
// if Book is a remote book we just lazy load the book content
|
||||
// if Book is a remote book we just lazy load the book content via url
|
||||
url?: string;
|
||||
// if Book is a transient local book we can load the book content via filePath
|
||||
filePath?: string;
|
||||
hash: string;
|
||||
format: BookFormat;
|
||||
title: string;
|
||||
|
||||
@@ -10,14 +10,16 @@ export type ToastType = 'info' | 'warning' | 'error';
|
||||
export interface FileSystem {
|
||||
getURL(path: string): string;
|
||||
getBlobURL(path: string, base: BaseDir): Promise<string>;
|
||||
openFile(path: string, base: BaseDir, filename?: string): Promise<File>;
|
||||
copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise<void>;
|
||||
readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise<string | ArrayBuffer>;
|
||||
writeFile(path: string, base: BaseDir, content: string | ArrayBuffer): Promise<void>;
|
||||
writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File): Promise<void>;
|
||||
removeFile(path: string, base: BaseDir): Promise<void>;
|
||||
readDir(path: string, base: BaseDir): Promise<{ path: string; isDir: boolean }[]>;
|
||||
createDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
|
||||
removeDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
|
||||
exists(path: string, base: BaseDir): Promise<boolean>;
|
||||
getPrefix(base: BaseDir): string | null;
|
||||
}
|
||||
|
||||
export interface AppService {
|
||||
|
||||
@@ -19,7 +19,8 @@ export const getStoragePlanData = (token: string) => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
const plan = data['plan'] || 'free';
|
||||
const usage = data['storage_usage_bytes'] || 0;
|
||||
const quota = DEFAULT_STORAGE_QUOTA[plan] || DEFAULT_STORAGE_QUOTA['free'];
|
||||
const fixedQuota = parseInt(process.env['NEXT_PUBLIC_STORAGE_FIXED_QUOTA'] || '0');
|
||||
const quota = fixedQuota || DEFAULT_STORAGE_QUOTA[plan] || DEFAULT_STORAGE_QUOTA['free'];
|
||||
|
||||
return {
|
||||
plan,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EXTS } from '@/libs/document';
|
||||
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
|
||||
import { getUserLang, makeSafeFilename } from './misc';
|
||||
import { getUserLang, isContentURI, isValidURL, makeSafeFilename } from './misc';
|
||||
import { getStorageType } from './object';
|
||||
|
||||
export const getDir = (book: Book) => {
|
||||
@@ -31,6 +31,15 @@ export const getConfigFilename = (book: Book) => {
|
||||
export const isBookFile = (filename: string) => {
|
||||
return Object.values(EXTS).includes(filename.split('.').pop()!);
|
||||
};
|
||||
export const getFilename = (fileOrUri: string) => {
|
||||
if (isValidURL(fileOrUri) || isContentURI(fileOrUri)) {
|
||||
fileOrUri = decodeURI(fileOrUri);
|
||||
}
|
||||
const normalizedPath = fileOrUri.replace(/\\/g, '/');
|
||||
const parts = normalizedPath.split('/');
|
||||
const lastPart = parts.pop()!;
|
||||
return lastPart.split('?')[0]!;
|
||||
};
|
||||
export const getBaseFilename = (filename: string) => {
|
||||
const normalizedPath = filename.replace(/\\/g, '/');
|
||||
const baseName = normalizedPath.split('/').pop()?.split('.').slice(0, -1).join('.') || '';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface CopyURIRequest {
|
||||
uri: string;
|
||||
dst: string;
|
||||
}
|
||||
|
||||
export interface CopyURIResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
|
||||
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
|
||||
payload: request,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FileHandle, open, BaseDirectory, SeekMode } from '@tauri-apps/plugin-fs';
|
||||
import { getOSPlatform } from './misc';
|
||||
|
||||
class RemoteBlobSlice extends Blob {
|
||||
class DeferredBlob extends Blob {
|
||||
#dataPromise: Promise<ArrayBuffer>;
|
||||
#type: string;
|
||||
|
||||
@@ -49,7 +50,192 @@ class RemoteBlobSlice extends Blob {
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteFile extends File {
|
||||
export interface ClosableFile extends File {
|
||||
open(): Promise<this>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class NativeFile extends File implements ClosableFile {
|
||||
#handle: FileHandle | null = null;
|
||||
#fp: string;
|
||||
#name: string;
|
||||
#baseDir: BaseDirectory | null;
|
||||
#lastModified: number = 0;
|
||||
#size: number = -1;
|
||||
#type: string = '';
|
||||
|
||||
static MAX_CACHE_CHUNK_SIZE = 1024 * 1024;
|
||||
static MAX_CACHE_ITEMS_SIZE = 20;
|
||||
#cache: Map<number, ArrayBuffer> = new Map();
|
||||
#order: number[] = [];
|
||||
|
||||
constructor(fp: string, name?: string, baseDir: BaseDirectory | null = null, type = '') {
|
||||
super([], name || fp, { type });
|
||||
this.#fp = fp;
|
||||
this.#baseDir = baseDir;
|
||||
this.#name = name || fp;
|
||||
}
|
||||
|
||||
async open() {
|
||||
this.#handle = await open(this.#fp, this.#baseDir ? { baseDir: this.#baseDir } : undefined);
|
||||
const stats = await this.#handle.stat();
|
||||
this.#size = stats.size;
|
||||
this.#lastModified = stats.mtime ? stats.mtime.getTime() : Date.now();
|
||||
return this;
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.#handle) {
|
||||
await this.#handle.close();
|
||||
this.#handle = null;
|
||||
}
|
||||
this.#cache.clear();
|
||||
this.#order = [];
|
||||
}
|
||||
|
||||
override get name() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
override get type() {
|
||||
return this.#type;
|
||||
}
|
||||
|
||||
override get size() {
|
||||
return this.#size;
|
||||
}
|
||||
|
||||
override get lastModified() {
|
||||
return this.#lastModified;
|
||||
}
|
||||
|
||||
async stat() {
|
||||
return this.#handle?.stat();
|
||||
}
|
||||
|
||||
async seek(offset: number, whence: SeekMode): Promise<number> {
|
||||
if (!this.#handle) {
|
||||
throw new Error('File handle is not open');
|
||||
}
|
||||
return this.#handle.seek(offset, whence);
|
||||
}
|
||||
|
||||
// exclusive reading of the end: [start, end)
|
||||
async readData(start: number, end: number): Promise<ArrayBuffer> {
|
||||
if (!this.#handle) {
|
||||
throw new Error('File handle is not open');
|
||||
}
|
||||
start = Math.max(0, start);
|
||||
end = Math.max(start, Math.min(this.size, end));
|
||||
const size = end - start;
|
||||
|
||||
if (size > NativeFile.MAX_CACHE_CHUNK_SIZE) {
|
||||
await this.#handle.seek(start, SeekMode.Start);
|
||||
const buffer = new Uint8Array(size);
|
||||
await this.#handle.read(buffer);
|
||||
return buffer.buffer;
|
||||
}
|
||||
|
||||
const cachedChunkStart = Array.from(this.#cache.keys()).find((chunkStart) => {
|
||||
const buffer = this.#cache.get(chunkStart)!;
|
||||
return start >= chunkStart && end <= chunkStart + buffer.byteLength;
|
||||
});
|
||||
|
||||
if (cachedChunkStart !== undefined) {
|
||||
this.#updateAccessOrder(cachedChunkStart);
|
||||
const buffer = this.#cache.get(cachedChunkStart)!;
|
||||
const offset = start - cachedChunkStart;
|
||||
return buffer.slice(offset, offset + size);
|
||||
}
|
||||
|
||||
const chunkStart = Math.max(0, start - 1024);
|
||||
const chunkEnd = Math.min(this.size, chunkStart + NativeFile.MAX_CACHE_CHUNK_SIZE);
|
||||
const chunkSize = chunkEnd - chunkStart;
|
||||
|
||||
await this.#handle.seek(chunkStart, SeekMode.Start);
|
||||
const buffer = new Uint8Array(chunkSize);
|
||||
await this.#handle.read(buffer);
|
||||
|
||||
this.#cache.set(chunkStart, buffer.buffer);
|
||||
this.#updateAccessOrder(chunkStart);
|
||||
this.#ensureCacheSize();
|
||||
|
||||
const offset = start - chunkStart;
|
||||
return buffer.buffer.slice(offset, offset + size);
|
||||
}
|
||||
|
||||
#updateAccessOrder(chunkStart: number) {
|
||||
const index = this.#order.indexOf(chunkStart);
|
||||
if (index > -1) {
|
||||
this.#order.splice(index, 1);
|
||||
}
|
||||
this.#order.unshift(chunkStart);
|
||||
}
|
||||
|
||||
#ensureCacheSize() {
|
||||
while (this.#cache.size > NativeFile.MAX_CACHE_ITEMS_SIZE) {
|
||||
const oldestKey = this.#order.pop();
|
||||
if (oldestKey !== undefined) {
|
||||
this.#cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override slice(start = 0, end = this.size, contentType = this.type): Blob {
|
||||
// console.log(`Slicing: ${start}-${end}, size: ${end - start}`);
|
||||
const dataPromise = this.readData(start, end);
|
||||
return new DeferredBlob(dataPromise, contentType);
|
||||
}
|
||||
|
||||
override stream(): ReadableStream<Uint8Array> {
|
||||
const CHUNK_SIZE = 1024 * 1024;
|
||||
let offset = 0;
|
||||
|
||||
return new ReadableStream<Uint8Array>({
|
||||
pull: async (controller) => {
|
||||
if (!this.#handle) {
|
||||
controller.error(new Error('File handle is not open'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset >= this.size) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const end = Math.min(offset + CHUNK_SIZE, this.size);
|
||||
const buffer = new Uint8Array(end - offset);
|
||||
|
||||
await this.#handle.seek(offset, SeekMode.Start);
|
||||
const bytesRead = await this.#handle.read(buffer);
|
||||
|
||||
if (bytesRead === null || bytesRead === 0) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(buffer.subarray(0, bytesRead));
|
||||
offset += bytesRead;
|
||||
},
|
||||
|
||||
cancel: async () => {
|
||||
await this.#handle?.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override async text() {
|
||||
const blob = this.slice(0, this.size);
|
||||
return blob.text();
|
||||
}
|
||||
|
||||
override async arrayBuffer() {
|
||||
const blob = this.slice(0, this.size);
|
||||
return blob.arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteFile extends File implements ClosableFile {
|
||||
url: string;
|
||||
#name: string;
|
||||
#lastModified: number;
|
||||
@@ -61,10 +247,11 @@ export class RemoteFile extends File {
|
||||
static MAX_CACHE_CHUNK_SIZE = 1024 * 128;
|
||||
static MAX_CACHE_ITEMS_SIZE: number = 10;
|
||||
|
||||
constructor(url: string, name = 'remote-file', type = '', lastModified = Date.now()) {
|
||||
super([], name, { type: type, lastModified });
|
||||
constructor(url: string, name?: string, type = '', lastModified = Date.now()) {
|
||||
const basename = url.split('/').pop() || 'remote-file';
|
||||
super([], name || basename, { type, lastModified });
|
||||
this.url = url;
|
||||
this.#name = name;
|
||||
this.#name = name || basename;
|
||||
this.#type = type;
|
||||
this.#lastModified = lastModified;
|
||||
}
|
||||
@@ -114,6 +301,11 @@ export class RemoteFile extends File {
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.#cache.clear();
|
||||
this.#order = [];
|
||||
}
|
||||
|
||||
async fetchRangePart(start: number, end: number) {
|
||||
start = Math.max(0, start);
|
||||
end = Math.min(this.size - 1, end);
|
||||
@@ -125,6 +317,7 @@ export class RemoteFile extends File {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
// inclusive reading of the end: [start, end]
|
||||
async fetchRange(start: number, end: number): Promise<ArrayBuffer> {
|
||||
const rangeSize = end - start + 1;
|
||||
const MAX_RANGE_LEN = 1024 * 1000;
|
||||
@@ -194,7 +387,7 @@ export class RemoteFile extends File {
|
||||
// console.log(`Slicing: ${start}-${end}, size: ${end - start}`);
|
||||
const dataPromise = this.fetchRange(start, end - 1);
|
||||
|
||||
return new RemoteBlobSlice(dataPromise, contentType);
|
||||
return new DeferredBlob(dataPromise, contentType);
|
||||
}
|
||||
|
||||
override async text() {
|
||||
|
||||
@@ -59,6 +59,10 @@ export const getOSPlatform = () => {
|
||||
return '';
|
||||
};
|
||||
|
||||
export const isContentURI = (uri: string) => {
|
||||
return uri.startsWith('content://');
|
||||
};
|
||||
|
||||
export const isValidURL = (url: string, allowedSchemes: string[] = ['http', 'https']) => {
|
||||
try {
|
||||
const { protocol } = new URL(url);
|
||||
|
||||
@@ -26,7 +26,9 @@ const getFontStyles = (
|
||||
minFontSize: number,
|
||||
fontWeight: number,
|
||||
overrideFont: boolean,
|
||||
themeCode: ThemeCode,
|
||||
) => {
|
||||
const { primary } = themeCode;
|
||||
const lastSerifFonts = ['Georgia', 'Times New Roman'];
|
||||
const serifFonts = [
|
||||
serif,
|
||||
@@ -73,8 +75,18 @@ const getFontStyles = (
|
||||
font-size: ${fontSize * 3}px;
|
||||
}
|
||||
body * {
|
||||
font-family: revert ${overrideFont ? '!important' : ''};
|
||||
font-family: inherit;
|
||||
${overrideFont ? 'font-family: revert !important;' : ''}
|
||||
${overrideFont ? 'font-size: revert !important;' : ''}
|
||||
${overrideFont ? 'color: revert !important;' : ''}
|
||||
}
|
||||
a:any-link {
|
||||
${overrideFont ? `color: ${primary};` : ''}
|
||||
}
|
||||
/* https://github.com/whatwg/html/issues/5426 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a:link {
|
||||
${overrideFont ? `color: lightblue;` : ''}
|
||||
}
|
||||
}
|
||||
`;
|
||||
return fontStyles;
|
||||
@@ -141,29 +153,15 @@ const getLayoutStyles = (
|
||||
zoomLevel: number,
|
||||
writingMode: string,
|
||||
vertical: boolean,
|
||||
bg: string,
|
||||
fg: string,
|
||||
primary: string,
|
||||
isDarkMode: boolean,
|
||||
) => `
|
||||
themeCode: ThemeCode,
|
||||
) => {
|
||||
const { bg, fg, primary, isDarkMode } = themeCode;
|
||||
const layoutStyle = `
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
html {
|
||||
color-scheme: ${isDarkMode ? 'dark' : 'light'};
|
||||
color: ${fg};
|
||||
}
|
||||
a:any-link {
|
||||
color: ${primary} ${isDarkMode ? '!important' : ''};
|
||||
}
|
||||
aside[epub|type~="footnote"] {
|
||||
display: none;
|
||||
}
|
||||
/* https://github.com/whatwg/html/issues/5426 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a:link {
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
--theme-bg-color: ${bg};
|
||||
--theme-fg-color: ${fg};
|
||||
@@ -253,6 +251,10 @@ const getLayoutStyles = (
|
||||
${vertical ? `vertical-align: unset;` : ''}
|
||||
}
|
||||
|
||||
aside[epub|type~="footnote"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.duokan-footnote-content,
|
||||
.duokan-footnote-item {
|
||||
display: none;
|
||||
@@ -267,6 +269,8 @@ const getLayoutStyles = (
|
||||
border-color: unset;
|
||||
}
|
||||
`;
|
||||
return layoutStyle;
|
||||
};
|
||||
|
||||
export const getFootnoteStyles = () => `
|
||||
.duokan-footnote-content,
|
||||
@@ -354,10 +358,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
|
||||
viewSettings.zoomLevel! / 100.0,
|
||||
viewSettings.writingMode!,
|
||||
viewSettings.vertical!,
|
||||
themeCode.bg,
|
||||
themeCode.fg,
|
||||
themeCode.primary,
|
||||
themeCode.isDarkMode,
|
||||
themeCode,
|
||||
);
|
||||
// scale the font size on-the-fly so that we can sync the same font size on different devices
|
||||
const isMobile = ['ios', 'android'].includes(getOSPlatform());
|
||||
@@ -371,6 +372,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
|
||||
viewSettings.minimumFontSize!,
|
||||
viewSettings.fontWeight!,
|
||||
viewSettings.overrideFont!,
|
||||
themeCode,
|
||||
);
|
||||
const userStylesheet = viewSettings.userStylesheet!;
|
||||
return `${layoutStyles}\n${fontStyles}\n${fontfacesCSS}\n${userStylesheet}`;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: be37c6c03b...b82b872712
+1
-1
Submodule packages/tauri updated: f235ec0113...b154826881
Generated
+125
-114
@@ -54,44 +54,44 @@ importers:
|
||||
specifier: ^2.47.7
|
||||
version: 2.47.7
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
specifier: 2.4.1
|
||||
version: 2.4.1
|
||||
'@tauri-apps/plugin-cli':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@tauri-apps/plugin-deep-link':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-haptics':
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
'@tauri-apps/plugin-log':
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6
|
||||
'@tauri-apps/plugin-os':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: ~2.2.0
|
||||
version: 2.2.0
|
||||
specifier: ~2.2.1
|
||||
version: 2.2.1
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
'@zip.js/zip.js':
|
||||
specifier: ^2.7.53
|
||||
version: 2.7.53
|
||||
@@ -160,8 +160,8 @@ importers:
|
||||
specifier: ^0.5.12
|
||||
version: 0.5.12(wrangler@4.4.0)
|
||||
'@tauri-apps/cli':
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1
|
||||
specifier: 2.4.1
|
||||
version: 2.4.1
|
||||
'@types/cors':
|
||||
specifier: ^2.8.17
|
||||
version: 2.8.17
|
||||
@@ -2477,113 +2477,120 @@ packages:
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@tauri-apps/api@2.3.0':
|
||||
resolution: {integrity: sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==}
|
||||
'@tauri-apps/api@2.4.1':
|
||||
resolution: {integrity: sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.3.1':
|
||||
resolution: {integrity: sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw==}
|
||||
'@tauri-apps/cli-darwin-arm64@2.4.1':
|
||||
resolution: {integrity: sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@2.3.1':
|
||||
resolution: {integrity: sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA==}
|
||||
'@tauri-apps/cli-darwin-x64@2.4.1':
|
||||
resolution: {integrity: sha512-/r89IcW6Ya1sEsFUEH7wLNruDTj7WmDWKGpPy7gATFtQr5JEY4heernqE82isjTUimnHZD8SCr0jA3NceI4ybw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
|
||||
resolution: {integrity: sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA==}
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.4.1':
|
||||
resolution: {integrity: sha512-9tDijkRB+CchAGjXxYdY9l/XzFpLp1yihUtGXJz9eh+3qIoRI043n3e+6xmU8ZURr7XPnu+R4sCmXs6HD+NCEQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
|
||||
resolution: {integrity: sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg==}
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.4.1':
|
||||
resolution: {integrity: sha512-pnFGDEXBAzS4iDYAVxTRhAzNu3K2XPGflYyBc0czfHDBXopqRgMyj5Q9Wj7HAwv6cM8BqzXINxnb2ZJFGmbSgA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
|
||||
resolution: {integrity: sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w==}
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.4.1':
|
||||
resolution: {integrity: sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
|
||||
resolution: {integrity: sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA==}
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.4.1':
|
||||
resolution: {integrity: sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.4.1':
|
||||
resolution: {integrity: sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.3.1':
|
||||
resolution: {integrity: sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA==}
|
||||
'@tauri-apps/cli-linux-x64-musl@2.4.1':
|
||||
resolution: {integrity: sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
|
||||
resolution: {integrity: sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww==}
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.4.1':
|
||||
resolution: {integrity: sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
|
||||
resolution: {integrity: sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw==}
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.4.1':
|
||||
resolution: {integrity: sha512-9eXfFORehYSCRwxg2KodfmX/mhr50CI7wyBYGbPLePCjr5z0jK/9IyW6r0tC+ZVjwpX48dkk7hKiUgI25jHjzA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
|
||||
resolution: {integrity: sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q==}
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.4.1':
|
||||
resolution: {integrity: sha512-60a4Ov7Jrwqz2hzDltlS7301dhSAmM9dxo+IRBD3xz7yobKrgaHXYpWvnRomYItHcDd51VaKc9292H8/eE/gsw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/cli@2.3.1':
|
||||
resolution: {integrity: sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==}
|
||||
'@tauri-apps/cli@2.4.1':
|
||||
resolution: {integrity: sha512-9Ta81jx9+57FhtU/mPIckDcOBtPTUdKM75t4+aA0X84b8Sclb0jy1xA8NplmcRzp2fsfIHNngU2NiRxsW5+yOQ==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-cli@2.2.0':
|
||||
resolution: {integrity: sha512-rvNhMog9rHr01Xk+trBFKJ0eZICIvPkm9GX6ogB89/0hROU/lf+a/sb4vC0wtSeR7zrJuCSxwxYuvHCZheaYFA==}
|
||||
|
||||
'@tauri-apps/plugin-deep-link@2.2.0':
|
||||
resolution: {integrity: sha512-H6mkxr2KZ3XJcKL44tiq6cOjCw9DL8OgU1xjn3j26Qsn+H/roPFiyhR7CHuB8Ar+sQFj4YVlfmJwtBajK2FETQ==}
|
||||
'@tauri-apps/plugin-deep-link@2.2.1':
|
||||
resolution: {integrity: sha512-8skZ6qIH/kWaV8d6jj3aPvvkIOuqkVk0APRDey9n9N3Ueu3n4MIbuxpAKR2EdoAyQxnXxPTNVyjw2D35/vfGyg==}
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.2.0':
|
||||
resolution: {integrity: sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==}
|
||||
'@tauri-apps/plugin-dialog@2.2.1':
|
||||
resolution: {integrity: sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==}
|
||||
|
||||
'@tauri-apps/plugin-fs@2.2.0':
|
||||
resolution: {integrity: sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==}
|
||||
'@tauri-apps/plugin-fs@2.2.1':
|
||||
resolution: {integrity: sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==}
|
||||
|
||||
'@tauri-apps/plugin-haptics@2.2.3':
|
||||
resolution: {integrity: sha512-tHWAOR0TSOuWIdJ4Fh/4z+L8CvSfHdg5i7XfCqjErPu63PMf+2n856VIGcC6fjF29hf/vq+BiGApWt38n66Gvg==}
|
||||
'@tauri-apps/plugin-haptics@2.2.4':
|
||||
resolution: {integrity: sha512-h+YapjI0o6B1daj4gL7j8qKZUoxuZ09RWq+W8C62TXO5GHxc4YaywzGNGqq/3kJTYwv/3fw5o/pWO3AsN58RuQ==}
|
||||
|
||||
'@tauri-apps/plugin-http@2.3.0':
|
||||
resolution: {integrity: sha512-pigTvz+zzAqbIhCzRiR1GE98Jw7A03j2V+Eiexr9thBI8VfMiwFQMcbgON51xlwnVaI72LdbYKNajU84im8tlg==}
|
||||
'@tauri-apps/plugin-http@2.4.3':
|
||||
resolution: {integrity: sha512-Us8X+FikzpaZRNr4kH4HLwyXascHbM42p6LxAqRTQnHPrrqp1usaH4vxWAZalPvTbHJ3gBEMJPHusFJgtjGJjA==}
|
||||
|
||||
'@tauri-apps/plugin-log@2.2.3':
|
||||
resolution: {integrity: sha512-noDkZXU0kPG1bP3qoF6om+q5TgQ65LWpsV/TnfrHyg2lEn0vE+WJWTItiIh7OUA1/xxC+hdb9ODy2bM2e+jHEw==}
|
||||
'@tauri-apps/plugin-log@2.3.1':
|
||||
resolution: {integrity: sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==}
|
||||
|
||||
'@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==}
|
||||
'@tauri-apps/plugin-os@2.2.1':
|
||||
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
|
||||
|
||||
'@tauri-apps/plugin-process@2.2.0':
|
||||
resolution: {integrity: sha512-uypN2Crmyop9z+KRJr3zl71OyVFgTuvHFjsJ0UxxQ/J5212jVa5w4nPEYjIewcn8bUEXacRebwE6F7owgrbhSw==}
|
||||
'@tauri-apps/plugin-process@2.2.1':
|
||||
resolution: {integrity: sha512-cF/k8J+YjjuowhNG1AboHNTlrGiOwgX5j6NzsX6WFf9FMzyZUchkCgZMxCdSE5NIgFX0vvOgLQhODFJgbMenLg==}
|
||||
|
||||
'@tauri-apps/plugin-shell@2.2.0':
|
||||
resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==}
|
||||
'@tauri-apps/plugin-shell@2.2.1':
|
||||
resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==}
|
||||
|
||||
'@tauri-apps/plugin-updater@2.5.1':
|
||||
resolution: {integrity: sha512-7fNJraKRbKkxguMY5lG2W20pBvAUkLu+cqnbu0UcK7DqeZgrAnNECcGBIDG6fJ6C+0fAp2V2dMIgznhffOBCcg==}
|
||||
'@tauri-apps/plugin-updater@2.7.0':
|
||||
resolution: {integrity: sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ==}
|
||||
|
||||
'@tsconfig/node10@1.0.11':
|
||||
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
|
||||
@@ -8093,7 +8100,7 @@ snapshots:
|
||||
|
||||
'@fabianlars/tauri-plugin-oauth@2.0.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
@@ -9247,98 +9254,102 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tauri-apps/api@2.3.0': {}
|
||||
'@tauri-apps/api@2.4.1': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.3.1':
|
||||
'@tauri-apps/cli-darwin-arm64@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-darwin-x64@2.3.1':
|
||||
'@tauri-apps/cli-darwin-x64@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
|
||||
'@tauri-apps/cli-linux-arm64-gnu@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.3.1':
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
|
||||
'@tauri-apps/cli-linux-x64-musl@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
|
||||
'@tauri-apps/cli-win32-ia32-msvc@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli@2.3.1':
|
||||
'@tauri-apps/cli-win32-x64-msvc@2.4.1':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/cli@2.4.1':
|
||||
optionalDependencies:
|
||||
'@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/cli-darwin-arm64': 2.4.1
|
||||
'@tauri-apps/cli-darwin-x64': 2.4.1
|
||||
'@tauri-apps/cli-linux-arm-gnueabihf': 2.4.1
|
||||
'@tauri-apps/cli-linux-arm64-gnu': 2.4.1
|
||||
'@tauri-apps/cli-linux-arm64-musl': 2.4.1
|
||||
'@tauri-apps/cli-linux-riscv64-gnu': 2.4.1
|
||||
'@tauri-apps/cli-linux-x64-gnu': 2.4.1
|
||||
'@tauri-apps/cli-linux-x64-musl': 2.4.1
|
||||
'@tauri-apps/cli-win32-arm64-msvc': 2.4.1
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.4.1
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-cli@2.2.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-deep-link@2.2.0':
|
||||
'@tauri-apps/plugin-deep-link@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.2.0':
|
||||
'@tauri-apps/plugin-dialog@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-fs@2.2.0':
|
||||
'@tauri-apps/plugin-fs@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-haptics@2.2.3':
|
||||
'@tauri-apps/plugin-haptics@2.2.4':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-http@2.3.0':
|
||||
'@tauri-apps/plugin-http@2.4.3':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-log@2.2.3':
|
||||
'@tauri-apps/plugin-log@2.3.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-opener@2.2.6':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-os@2.2.0':
|
||||
'@tauri-apps/plugin-os@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-process@2.2.0':
|
||||
'@tauri-apps/plugin-process@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-shell@2.2.0':
|
||||
'@tauri-apps/plugin-shell@2.2.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tauri-apps/plugin-updater@2.5.1':
|
||||
'@tauri-apps/plugin-updater@2.7.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.3.0
|
||||
'@tauri-apps/api': 2.4.1
|
||||
|
||||
'@tsconfig/node10@1.0.11':
|
||||
optional: true
|
||||
@@ -11231,7 +11242,7 @@ snapshots:
|
||||
is-weakset@2.0.3:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
get-intrinsic: 1.2.4
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user