forked from akai/readest
This commit is contained in:
Generated
+659
-667
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" }
|
||||
@@ -152,7 +152,6 @@
|
||||
"Text Indent": "مسافة بادئة للنص",
|
||||
"Paragraph Margin": "هامش الفقرة",
|
||||
"Override Book Layout": "تجاوز تخطيط الكتاب",
|
||||
"Add to Group": "إضافة إلى مجموعة",
|
||||
"Untitled Group": "مجموعة بدون عنوان",
|
||||
"Group Books": "تجميع الكتب",
|
||||
"Remove From Group": "إزالة من المجموعة",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "لم يتم التعرف على فصول.",
|
||||
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
|
||||
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "تعذر جلب الترجمة. يرجى تسجيل الدخول أولاً ثم المحاولة مرة أخرى."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Übersetzung konnte nicht abgerufen werden. Bitte zuerst anmelden und erneut versuchen."
|
||||
"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": "Αφαίρεση από την ομάδα",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "Δεν εντοπίστηκαν κεφάλαια.",
|
||||
"Failed to parse the EPUB file.": "Αποτυχία ανάλυσης του αρχείου EPUB.",
|
||||
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Αδυναμία λήψης της μετάφρασης. Παρακαλώ συνδεθείτε πρώτα και δοκιμάστε ξανά."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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": "समूह से हटाएं",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "कोई अध्याय नहीं मिला।",
|
||||
"Failed to parse the EPUB file.": "EPUB फ़ाइल को पार्स करने में विफल।",
|
||||
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "अनुवाद प्राप्त करने में असमर्थ। कृपया पहले लॉग इन करें और पुनः प्रयास करें।"
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Tidak dapat mengambil terjemahan. Silakan masuk terlebih dahulu dan coba lagi."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Impossibile recuperare la traduzione. Accedi prima e riprova."
|
||||
"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": "グループから削除",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "章が検出されません。",
|
||||
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました。",
|
||||
"This book format is not supported.": "この書籍形式はサポートされていません。",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "翻訳を取得できません。まずログインして再試行してください。"
|
||||
"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": "그룹에서 제거",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "장 발견되지 않음.",
|
||||
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
|
||||
"This book format is not supported.": "이 책 형식은 지원되지 않습니다.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "번역을 가져올 수 없습니다. 먼저 로그인하고 다시 시도하세요."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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": "Удалить из группы",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "Не обнаружено глав.",
|
||||
"Failed to parse the EPUB file.": "Не удалось разобрать файл EPUB.",
|
||||
"This book format is not supported.": "Этот формат книги не поддерживается.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Не удалось получить перевод. Пожалуйста, войдите в систему и попробуйте снова."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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.",
|
||||
"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."
|
||||
"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": "Видалити з групи",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "Не виявлено жодного розділу.",
|
||||
"Failed to parse the EPUB file.": "Не вдалося розібрати файл EPUB.",
|
||||
"This book format is not supported.": "Цей формат книги не підтримується.",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Не вдалося отримати переклад. Будь ласка, спочатку увійдіть у систему та спробуйте ще раз."
|
||||
"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",
|
||||
@@ -265,5 +264,6 @@
|
||||
"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ợ.",
|
||||
"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."
|
||||
"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": "从分组中移除",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "未检测到章节。",
|
||||
"Failed to parse the EPUB file.": "无法解析 EPUB 文件。",
|
||||
"This book format is not supported.": "不支持此书籍格式。",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "无法获取翻译。请登录后重试。"
|
||||
"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": "從組中移除",
|
||||
@@ -265,5 +264,6 @@
|
||||
"No chapters detected.": "未檢測到章節。",
|
||||
"Failed to parse the EPUB file.": "無法解析 EPUB 文件。",
|
||||
"This book format is not supported.": "不支援此書籍格式。",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "無法獲取翻譯。請先登入然後重試。"
|
||||
"Unable to fetch the translation. Please log in first and try again.": "無法獲取翻譯。請先登入然後重試。",
|
||||
"Group": "分組"
|
||||
}
|
||||
|
||||
@@ -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.0", features = [ "protocol-asset", "devtools"] }
|
||||
tauri-build = "2.1.0"
|
||||
tauri-plugin-log = "2.3.1"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-http = "2.4.2"
|
||||
tauri-plugin-shell = "2.2.0"
|
||||
tauri-plugin-process = "2.2.0"
|
||||
tauri-plugin-oauth = "2"
|
||||
tauri-plugin-opener = "2.2.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2.2.3"
|
||||
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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
+35
@@ -1,18 +1,26 @@
|
||||
package com.readest.native_bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
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 {
|
||||
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()
|
||||
@@ -25,4 +33,31 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
ret.put("redirectUrl", implementation.auth_with_safari(args.authUrl ?: ""))
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun copy_uri_to_path(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(CopyURIRequestArgs::class.java)
|
||||
val ret = JSObject()
|
||||
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,8 @@
|
||||
const COMMANDS: &[&str] = &["auth_with_safari"];
|
||||
const COMMANDS: &[&str] = &["auth_with_safari", "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-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"]
|
||||
+27
@@ -3,6 +3,7 @@
|
||||
Default permissions for the plugin
|
||||
|
||||
- `allow-auth-with-safari`
|
||||
- `allow-copy-uri-to-path`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -36,6 +37,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-copy-uri-to-path"]
|
||||
|
||||
+10
@@ -304,6 +304,16 @@
|
||||
"type": "string",
|
||||
"const": "deny-auth-with-safari"
|
||||
},
|
||||
{
|
||||
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-copy-uri-to-path"
|
||||
},
|
||||
{
|
||||
"description": "Denies the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-copy-uri-to-path"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
|
||||
@@ -11,3 +11,11 @@ pub(crate) async fn auth_with_safari<R: Runtime>(
|
||||
) -> Result<SafariAuthResponse> {
|
||||
app.native_bridge().auth_with_safari(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)
|
||||
}
|
||||
|
||||
@@ -20,4 +20,8 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
) -> crate::Result<SafariAuthResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn copy_uri_to_path(&self, _payload: CopyURIRequest) -> crate::Result<CopyURIResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ 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::copy_uri_to_path,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let native_bridge = mobile::init(app, api)?;
|
||||
|
||||
@@ -34,3 +34,11 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,17 @@ pub struct SafariAuthRequest {
|
||||
pub struct SafariAuthResponse {
|
||||
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());
|
||||
|
||||
|
||||
@@ -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,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
};
|
||||
|
||||
const selectFilesTauri = async () => {
|
||||
return appService?.selectFiles('Select Books', SUPPORTED_FILE_EXTS);
|
||||
return (await appService?.selectFiles(_('Select Books'), SUPPORTED_FILE_EXTS)) || [];
|
||||
};
|
||||
|
||||
const selectFilesWeb = () => {
|
||||
@@ -453,7 +453,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];
|
||||
|
||||
@@ -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,7 +32,7 @@ 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 { ProgressHandler } from '@/utils/transfer';
|
||||
@@ -117,6 +117,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 +133,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,
|
||||
@@ -146,8 +157,8 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
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;
|
||||
@@ -185,6 +196,12 @@ 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');
|
||||
}
|
||||
@@ -193,10 +210,14 @@ export abstract class BaseAppService implements AppService {
|
||||
!transient &&
|
||||
(!(await this.fs.exists(getLocalBookFilename(book), 'Books')) || overwrite)
|
||||
) {
|
||||
if (typeof file === 'string' && !isValidURL(file) && !filename.endsWith('.txt')) {
|
||||
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)) {
|
||||
@@ -209,19 +230,18 @@ 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 (transient) {
|
||||
existingBook.filePath = filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (transient) {
|
||||
book.filePath = filename;
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -258,14 +278,8 @@ 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);
|
||||
}
|
||||
@@ -284,7 +298,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;
|
||||
}
|
||||
@@ -376,17 +390,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 new RemoteFile(this.fs.getURL(book.filePath), fp).open();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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), path).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 }],
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
if (isValidURL(path)) {
|
||||
return await new RemoteFile(path).open();
|
||||
} else {
|
||||
const content = await this.readFile(path, base, 'binary');
|
||||
return new File([content], 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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,6 +50,128 @@ class RemoteBlobSlice extends Blob {
|
||||
}
|
||||
}
|
||||
|
||||
export class NativeFile extends File {
|
||||
#handle: FileHandle | null = null;
|
||||
#fp: string;
|
||||
#name: string;
|
||||
#baseDir: BaseDirectory | null;
|
||||
#lastModified: number = 0;
|
||||
#size: number = -1;
|
||||
#type: string = '';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 close(): Promise<void> {
|
||||
return this.#handle?.close();
|
||||
}
|
||||
|
||||
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));
|
||||
await this.#handle.seek(start, SeekMode.Start);
|
||||
const buffer = new Uint8Array(end - start);
|
||||
await this.#handle.read(buffer);
|
||||
return buffer.buffer;
|
||||
}
|
||||
|
||||
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 {
|
||||
url: string;
|
||||
#name: string;
|
||||
@@ -61,10 +184,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 +238,8 @@ export class RemoteFile extends File {
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
|
||||
async fetchRangePart(start: number, end: number) {
|
||||
start = Math.max(0, start);
|
||||
end = Math.min(this.size - 1, end);
|
||||
@@ -125,6 +251,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 +321,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);
|
||||
|
||||
+1
-1
Submodule packages/tauri updated: f235ec0113...30beb6fee7
Generated
+1
-1
@@ -11231,7 +11231,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