feat: add support for importing from a directory recursively, closes #179 (#2642)

This commit is contained in:
Huang Xin
2025-12-08 14:16:57 +08:00
committed by GitHub
parent 11bc7497e8
commit ba3f060cc4
44 changed files with 341 additions and 97 deletions
@@ -796,5 +796,12 @@
"Are you sure to delete {{count}} selected file(s)?_many": "هل أنت متأكد من حذف {{count}} ملفًا محددًا؟",
"Are you sure to delete {{count}} selected file(s)?_other": "هل أنت متأكد من حذف {{count}} ملف محدد؟",
"Cloud Storage Usage": "استخدام التخزين السحابي",
"Rename Group": "إعادة تسمية المجموعة"
"Rename Group": "إعادة تسمية المجموعة",
"From Directory": "من الدليل",
"Successfully imported {{count}} book(s)_zero": "لم يتم استيراد أي كتب",
"Successfully imported {{count}} book(s)_one": "تم استيراد كتاب واحد",
"Successfully imported {{count}} book(s)_two": "تم استيراد كتابين",
"Successfully imported {{count}} book(s)_few": "تم استيراد {{count}} كتب",
"Successfully imported {{count}} book(s)_many": "تم استيراد {{count}} كتابًا",
"Successfully imported {{count}} book(s)_other": "تم استيراد {{count}} كتاب"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "আপনি কি নিশ্চিত যে {{count}}টি নির্বাচিত ফাইল মুছতে চান?",
"Are you sure to delete {{count}} selected file(s)?_other": "আপনি কি নিশ্চিত যে {{count}}টি নির্বাচিত ফাইল মুছতে চান?",
"Cloud Storage Usage": "ক্লাউড স্টোরেজ ব্যবহৃত",
"Rename Group": "গ্রুপের নাম পরিবর্তন করুন"
"Rename Group": "গ্রুপের নাম পরিবর্তন করুন",
"From Directory": "ডিরেক্টরি থেকে",
"Successfully imported {{count}} book(s)_one": "সফলভাবে ১টি বই আমদানি করা হয়েছে",
"Successfully imported {{count}} book(s)_other": "সফলভাবে {{count}}টি বই আমদানি করা হয়েছে"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "ཤོག་ངོས་ {{total}} ནས་ {{current}}",
"Are you sure to delete {{count}} selected file(s)?_other": "ཁྱེད་ཀྱིས་འདེམས་པའི་ཡིག་ཆ་ {{count}} བསུབས་དགོས་པ་ངེས་ཡིན་ན?",
"Cloud Storage Usage": "སྤྲིན་གནས་སྣོད་གསོག་ལུས་སྐོར།",
"Rename Group": "ཚོགས་མིང་བསྒྱུར་བ།"
"Rename Group": "ཚོགས་མིང་བསྒྱུར་བ།",
"From Directory": "སྐོར་འདེམས་པ་ནས།",
"Successfully imported {{count}} book(s)_other": "སྤྲིན་ནས་ཕབ་སྟེ་འབེབས་ {{count}} དེབ་འདེམས་སོང་"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "Möchten Sie {{count}} ausgewählte Datei wirklich löschen?",
"Are you sure to delete {{count}} selected file(s)?_other": "Möchten Sie {{count}} ausgewählte Dateien wirklich löschen?",
"Cloud Storage Usage": "Cloud-Speichernutzung",
"Rename Group": "Gruppe umbenennen"
"Rename Group": "Gruppe umbenennen",
"From Directory": "Aus Verzeichnis",
"Successfully imported {{count}} book(s)_one": "Erfolgreich 1 Buch importiert",
"Successfully imported {{count}} book(s)_other": "Erfolgreich {{count}} Bücher importiert"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "Σίγουρα θέλετε να διαγράψετε {{count}} επιλεγμένο αρχείο;",
"Are you sure to delete {{count}} selected file(s)?_other": "Σίγουρα θέλετε να διαγράψετε {{count}} επιλεγμένα αρχεία;",
"Cloud Storage Usage": "Χρήση αποθήκευσης cloud",
"Rename Group": "Μετονομασία ομάδας"
"Rename Group": "Μετονομασία ομάδας",
"From Directory": "Από κατάλογο",
"Successfully imported {{count}} book(s)_one": "Επιτυχής εισαγωγή 1 βιβλίου",
"Successfully imported {{count}} book(s)_other": "Επιτυχής εισαγωγή {{count}} βιβλίων"
}
@@ -17,5 +17,7 @@
"{{count}} selected_one": "{{count}} selected",
"{{count}} selected_other": "{{count}} selected",
"Are you sure to delete {{count}} selected file(s)?_one": "Are you sure to delete {{count}} selected file?",
"Are you sure to delete {{count}} selected file(s)?_other": "Are you sure to delete {{count}} selected files?"
"Are you sure to delete {{count}} selected file(s)?_other": "Are you sure to delete {{count}} selected files?",
"Successfully imported {{count}} book(s)_one": "Successfully imported {{count}} book",
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books"
}
@@ -769,5 +769,9 @@
"Are you sure to delete {{count}} selected file(s)?_many": "¿Seguro que deseas eliminar {{count}} archivos seleccionados?",
"Are you sure to delete {{count}} selected file(s)?_other": "¿Seguro que deseas eliminar {{count}} archivos seleccionados?",
"Cloud Storage Usage": "Uso de almacenamiento en la nube",
"Rename Group": "Renombrar grupo"
"Rename Group": "Renombrar grupo",
"From Directory": "Desde el directorio",
"Successfully imported {{count}} book(s)_one": "Se importó correctamente 1 libro",
"Successfully imported {{count}} book(s)_many": "Se importaron correctamente {{count}} libros",
"Successfully imported {{count}} book(s)_other": "Se importaron correctamente {{count}} libros"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "آیا از حذف {{count}} فایل انتخاب‌شده مطمئن هستید؟",
"Are you sure to delete {{count}} selected file(s)?_other": "آیا از حذف {{count}} فایل انتخاب‌شده مطمئن هستید؟",
"Cloud Storage Usage": "استفاده از فضای ذخیره‌سازی ابری",
"Rename Group": "تغییر نام گروه"
"Rename Group": "تغییر نام گروه",
"From Directory": "از مسیر",
"Successfully imported {{count}} book(s)_one": "با موفقیت 1 کتاب وارد شد",
"Successfully imported {{count}} book(s)_other": "با موفقیت {{count}} کتاب وارد شد"
}
@@ -769,5 +769,9 @@
"Are you sure to delete {{count}} selected file(s)?_many": "Voulez-vous vraiment supprimer {{count}} fichiers sélectionnés ?",
"Are you sure to delete {{count}} selected file(s)?_other": "Voulez-vous vraiment supprimer {{count}} fichiers sélectionnés ?",
"Cloud Storage Usage": "Utilisation du stockage cloud",
"Rename Group": "Renommer le groupe"
"Rename Group": "Renommer le groupe",
"From Directory": "Depuis le répertoire",
"Successfully imported {{count}} book(s)_one": "Importation réussie de 1 livre",
"Successfully imported {{count}} book(s)_many": "Importation réussie de {{count}} livres",
"Successfully imported {{count}} book(s)_other": "Importation réussie de {{count}} livres"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "क्या आप वाकई {{count}} चयनित फ़ाइल हटाना चाहते हैं?",
"Are you sure to delete {{count}} selected file(s)?_other": "क्या आप वाकई {{count}} चयनित फ़ाइलें हटाना चाहते हैं?",
"Cloud Storage Usage": "क्लाउड स्टोरेज उपयोग",
"Rename Group": "समूह का नाम बदलें"
"Rename Group": "समूह का नाम बदलें",
"From Directory": "निर्देशिका से",
"Successfully imported {{count}} book(s)_one": "सफलतापूर्वक 1 पुस्तक आयात की गई",
"Successfully imported {{count}} book(s)_other": "सफलतापूर्वक {{count}} पुस्तकों का आयात किया गया"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "Halaman {{current}} dari {{total}}",
"Are you sure to delete {{count}} selected file(s)?_other": "Yakin ingin menghapus {{count}} file yang dipilih?",
"Cloud Storage Usage": "Penggunaan Penyimpanan Cloud",
"Rename Group": "Ganti Nama Grup"
"Rename Group": "Ganti Nama Grup",
"From Directory": "Dari Direktori",
"Successfully imported {{count}} book(s)_other": "Berhasil mengimpor {{count}} buku"
}
@@ -769,5 +769,9 @@
"Are you sure to delete {{count}} selected file(s)?_many": "Sei sicuro di voler eliminare {{count}} file selezionati?",
"Are you sure to delete {{count}} selected file(s)?_other": "Sei sicuro di voler eliminare {{count}} file selezionati?",
"Cloud Storage Usage": "Utilizzo archiviazione cloud",
"Rename Group": "Rinomina gruppo"
"Rename Group": "Rinomina gruppo",
"From Directory": "Da directory",
"Successfully imported {{count}} book(s)_one": "Importato con successo 1 libro",
"Successfully imported {{count}} book(s)_many": "Importati con successo {{count}} libri",
"Successfully imported {{count}} book(s)_other": "Importati con successo {{count}} libri"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "ページ {{current}} / {{total}}",
"Are you sure to delete {{count}} selected file(s)?_other": "選択した {{count}} 件のファイルを削除してもよろしいですか?",
"Cloud Storage Usage": "クラウドストレージ使用量",
"Rename Group": "グループ名を変更"
"Rename Group": "グループ名を変更",
"From Directory": "ディレクトリから",
"Successfully imported {{count}} book(s)_other": "成功裏に{{count}}冊の本をインポートしました"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "페이지 {{current}} / {{total}}",
"Are you sure to delete {{count}} selected file(s)?_other": "선택한 {{count}}개의 파일을 삭제하시겠습니까?",
"Cloud Storage Usage": "클라우드 저장소 사용량",
"Rename Group": "그룹 이름 바꾸기"
"Rename Group": "그룹 이름 바꾸기",
"From Directory": "디렉토리에서",
"Successfully imported {{count}} book(s)_other": "성공적으로 {{count}} 권의 책이 가져와졌습니다"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "Halaman {{current}} daripada {{total}}",
"Are you sure to delete {{count}} selected file(s)?_other": "Adakah anda pasti mahu memadam {{count}} fail yang dipilih?",
"Cloud Storage Usage": "Penggunaan Storan Awan",
"Rename Group": "Namakan Semula Kumpulan"
"Rename Group": "Namakan Semula Kumpulan",
"From Directory": "Dari Direktori",
"Successfully imported {{count}} book(s)_other": "Berjaya mengimport {{count}} buku"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "Weet je zeker dat je {{count}} geselecteerd bestand wilt verwijderen?",
"Are you sure to delete {{count}} selected file(s)?_other": "Weet je zeker dat je {{count}} geselecteerde bestanden wilt verwijderen?",
"Cloud Storage Usage": "Cloudopslaggebruik",
"Rename Group": "Groep hernoemen"
"Rename Group": "Groep hernoemen",
"From Directory": "Vanuit map",
"Successfully imported {{count}} book(s)_one": "Succesvol 1 boek geïmporteerd",
"Successfully imported {{count}} book(s)_other": "Succesvol {{count}} boeken geïmporteerd"
}
@@ -778,5 +778,10 @@
"Are you sure to delete {{count}} selected file(s)?_many": "Czy na pewno chcesz usunąć {{count}} wybranych plików?",
"Are you sure to delete {{count}} selected file(s)?_other": "Czy na pewno chcesz usunąć {{count}} wybrany plik?",
"Cloud Storage Usage": "Użycie pamięci w chmurze",
"Rename Group": "Zmień nazwę grupy"
"Rename Group": "Zmień nazwę grupy",
"From Directory": "Z katalogu",
"Successfully imported {{count}} book(s)_one": "Pomyślnie zaimportowano 1 książkę",
"Successfully imported {{count}} book(s)_few": "Pomyślnie zaimportowano {{count}} książki",
"Successfully imported {{count}} book(s)_many": "Pomyślnie zaimportowano {{count}} książek",
"Successfully imported {{count}} book(s)_other": "Pomyślnie zaimportowano {{count}} książek"
}
@@ -769,5 +769,9 @@
"Are you sure to delete {{count}} selected file(s)?_many": "Tem certeza de que deseja apagar {{count}} ficheiros selecionados?",
"Are you sure to delete {{count}} selected file(s)?_other": "Tem certeza de que deseja apagar {{count}} ficheiro selecionado?",
"Cloud Storage Usage": "Uso de Armazenamento na Nuvem",
"Rename Group": "Renomear Grupo"
"Rename Group": "Renomear Grupo",
"From Directory": "Do Diretório",
"Successfully imported {{count}} book(s)_one": "Importado com sucesso 1 livro",
"Successfully imported {{count}} book(s)_many": "Importados com sucesso {{count}} livros",
"Successfully imported {{count}} book(s)_other": "Importados com sucesso {{count}} livros"
}
@@ -778,5 +778,10 @@
"Are you sure to delete {{count}} selected file(s)?_many": "Вы уверены, что хотите удалить {{count}} выбранных файлов?",
"Are you sure to delete {{count}} selected file(s)?_other": "Вы уверены, что хотите удалить {{count}} выбранный файл?",
"Cloud Storage Usage": "Использование облачного хранилища",
"Rename Group": "Переименовать группу"
"Rename Group": "Переименовать группу",
"From Directory": "Из каталога",
"Successfully imported {{count}} book(s)_one": "Успешно импортирован 1 книга",
"Successfully imported {{count}} book(s)_few": "Успешно импортировано {{count}} книги",
"Successfully imported {{count}} book(s)_many": "Успешно импортировано {{count}} книг",
"Successfully imported {{count}} book(s)_other": "Успешно импортировано {{count}} книг"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "තෝරාගත් ගොනුව මකන්නට විශ්වාසද?",
"Are you sure to delete {{count}} selected file(s)?_other": "තෝරාගත් ගොනු මකන්නට විශ්වාසද?",
"Cloud Storage Usage": "කලාප ගබඩා භාවිතය",
"Rename Group": "කණ්ඩායම නැවත නම් කරන්න"
"Rename Group": "කණ්ඩායම නැවත නම් කරන්න",
"From Directory": "ෆෝල්ඩරයෙන්",
"Successfully imported {{count}} book(s)_one": "සාර්ථකව ආයාත කළ 1 පොත",
"Successfully imported {{count}} book(s)_other": "සාර්ථකව ආයාත කළ {{count}} පොත්"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "Är du säker på att du vill radera filen?",
"Are you sure to delete {{count}} selected file(s)?_other": "Är du säker på att du vill radera filerna?",
"Cloud Storage Usage": "Användning av molnlagring",
"Rename Group": "Byt namn på grupp"
"Rename Group": "Byt namn på grupp",
"From Directory": "Från katalog",
"Successfully imported {{count}} book(s)_one": "Importerat 1 bok",
"Successfully imported {{count}} book(s)_other": "Importerat {{count}} böcker"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "இந்த கோப்பை நீக்க விரும்புகிறீர்களா?",
"Are you sure to delete {{count}} selected file(s)?_other": "இந்த கோப்புகளை நீக்க விரும்புகிறீர்களா?",
"Cloud Storage Usage": "மேக சேமிப்பு பயன்பாடு",
"Rename Group": "குழுவை மறுபெயரிடவும்"
"Rename Group": "குழுவை மறுபெயரிடவும்",
"From Directory": "கோப்புறையிலிருந்து",
"Successfully imported {{count}} book(s)_one": "வெற்றிகரமாக 1 புத்தகம் இறக்குமதி செய்யப்பட்டது",
"Successfully imported {{count}} book(s)_other": "வெற்றிகரமாக {{count}} புத்தகங்கள் இறக்குமதி செய்யப்பட்டது"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "หน้า {{current}} จาก {{total}}",
"Are you sure to delete {{count}} selected file(s)?_other": "คุณแน่ใจหรือว่าต้องการลบไฟล์ที่เลือก?",
"Cloud Storage Usage": "การใช้งานพื้นที่เก็บข้อมูลคลาวด์",
"Rename Group": "เปลี่ยนชื่อกลุ่ม"
"Rename Group": "เปลี่ยนชื่อกลุ่ม",
"From Directory": "จากไดเรกทอรี",
"Successfully imported {{count}} book(s)_other": "นำเข้า {{count}} หนังสือเรียบร้อยแล้ว"
}
@@ -760,5 +760,8 @@
"Are you sure to delete {{count}} selected file(s)?_one": "Seçilen {{count}} dosyayı silmek istediğinizden emin misiniz?",
"Are you sure to delete {{count}} selected file(s)?_other": "Seçilen {{count}} dosyayı silmek istediğinizden emin misiniz?",
"Cloud Storage Usage": "Bulut Depolama Kullanımı",
"Rename Group": "Grubu Yeniden Adlandır"
"Rename Group": "Grubu Yeniden Adlandır",
"From Directory": "Dizinden",
"Successfully imported {{count}} book(s)_one": "Başarıyla 1 kitap içe aktarıldı",
"Successfully imported {{count}} book(s)_other": "Başarıyla {{count}} kitap içe aktarıldı"
}
@@ -778,5 +778,10 @@
"Are you sure to delete {{count}} selected file(s)?_many": "Ви впевнені, що хочете видалити {{count}} файлів?",
"Are you sure to delete {{count}} selected file(s)?_other": "Ви впевнені, що хочете видалити {{count}} файлів?",
"Cloud Storage Usage": "Використання хмарного сховища",
"Rename Group": "Перейменувати групу"
"Rename Group": "Перейменувати групу",
"From Directory": "З каталогу",
"Successfully imported {{count}} book(s)_one": "Успішно імпортовано 1 книгу",
"Successfully imported {{count}} book(s)_few": "Успішно імпортовано {{count}} книги",
"Successfully imported {{count}} book(s)_many": "Успішно імпортовано {{count}} книг",
"Successfully imported {{count}} book(s)_other": "Успішно імпортовано {{count}} книг"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "Trang {{current}} trên {{total}}",
"Are you sure to delete {{count}} selected file(s)?_other": "Bạn có chắc muốn xóa {{count}} tệp đã chọn không?",
"Cloud Storage Usage": "Sử dụng lưu trữ đám mây",
"Rename Group": "Đổi tên nhóm"
"Rename Group": "Đổi tên nhóm",
"From Directory": "Từ thư mục",
"Successfully imported {{count}} book(s)_other": "Đã nhập thành công {{count}} sách"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "第 {{current}} 页,共 {{total}} 页",
"Are you sure to delete {{count}} selected file(s)?_other": "确定要删除已选择的 {{count}} 个文件吗?",
"Cloud Storage Usage": "云存储使用情况",
"Rename Group": "重命名分组"
"Rename Group": "重命名分组",
"From Directory": "从文件夹导入",
"Successfully imported {{count}} book(s)_other": "成功导入 {{count}} 本书"
}
@@ -751,5 +751,7 @@
"Page {{current}} of {{total}}": "第 {{current}} 頁,共 {{total}} 頁",
"Are you sure to delete {{count}} selected file(s)?_other": "確定要刪除已選擇的 {{count}} 個檔案嗎?",
"Cloud Storage Usage": "雲端儲存使用情況",
"Rename Group": "重新命名"
"Rename Group": "重新命名",
"From Directory": "從目錄導入",
"Successfully imported {{count}} book(s)_other": "成功導入 {{count}} 本書"
}
+12 -7
View File
@@ -44,17 +44,22 @@ fn build_windows_thumbnail() {
}
let dll_name = "windows_thumbnail.dll";
let dll_src = if !target_triple.is_empty() {
let candidate_paths = [
dll_crate_dir.join("target").join(&profile).join(dll_name),
dll_crate_dir
.join("target")
.join(&target_triple)
.join(&profile)
.join(dll_name)
} else {
dll_crate_dir.join("target").join(&profile).join(dll_name)
};
let dll_dest = dll_crate_dir.join("target").join(dll_name);
.join(dll_name),
];
fs::copy(&dll_src, &dll_dest).expect("Failed to copy windows_thumbnail DLL");
let dll_src = candidate_paths
.iter()
.find(|p| p.exists())
.expect("Failed to find built windows_thumbnail DLL");
let dll_dest = &dll_crate_dir.join("target").join(dll_name);
fs::copy(dll_src, dll_dest).expect("Failed to copy windows_thumbnail DLL");
println!("cargo:rerun-if-changed={}", dll_dest.display());
}
@@ -1,6 +1,8 @@
use tauri::{command, AppHandle, Runtime};
use std::path::PathBuf;
use tauri::{command, AppHandle, Runtime, State};
use crate::models::*;
use crate::DirectoryCallbackState;
use crate::NativeBridgeExt;
use crate::Result;
@@ -160,8 +162,21 @@ pub(crate) async fn open_external_url<R: Runtime>(
#[command]
pub(crate) async fn select_directory<R: Runtime>(
app: AppHandle<R>,
callback_state: State<'_, DirectoryCallbackState<R>>,
) -> Result<SelectDirectoryResponse> {
app.native_bridge().select_directory()
let result = app.native_bridge().select_directory()?;
if let Some(dir_path) = &result.path {
let path = PathBuf::from(dir_path);
if let Ok(callback_guard) = callback_state.callback.lock() {
if let Some(callback) = callback_guard.as_ref() {
callback(&app, &path);
}
}
}
Ok(result)
}
#[command]
@@ -1,3 +1,4 @@
use std::sync::{Arc, Mutex};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
@@ -17,6 +18,9 @@ mod platform;
pub use error::{Error, Result};
use std::path::PathBuf;
use tauri::AppHandle;
#[cfg(desktop)]
use desktop::NativeBridge;
#[cfg(mobile)]
@@ -33,6 +37,20 @@ impl<R: Runtime, T: Manager<R>> crate::NativeBridgeExt<R> for T {
}
}
type DirectoryCallback<R> = Box<dyn Fn(&AppHandle<R>, &PathBuf) + Send + Sync>;
pub struct DirectoryCallbackState<R: Runtime> {
pub callback: Arc<Mutex<Option<DirectoryCallback<R>>>>,
}
impl<R: Runtime> Default for DirectoryCallbackState<R> {
fn default() -> Self {
Self {
callback: Arc::new(Mutex::new(None)),
}
}
}
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("native-bridge")
@@ -66,7 +84,18 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[cfg(desktop)]
let native_bridge = desktop::init(app, api)?;
app.manage(native_bridge);
app.manage(DirectoryCallbackState::<R>::default());
Ok(())
})
.build()
}
pub fn register_select_directory_callback<R: Runtime>(
app: &AppHandle<R>,
callback: impl Fn(&AppHandle<R>, &PathBuf) + Send + Sync + 'static,
) {
if let Some(state) = app.try_state::<DirectoryCallbackState<R>>() {
let mut cb = state.callback.lock().unwrap();
*cb = Some(Box::new(callback));
}
}
+10 -5
View File
@@ -13,18 +13,19 @@ use tauri::utils::config::BackgroundThrottlingPolicy;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
#[cfg(desktop)]
use std::path::PathBuf;
#[cfg(desktop)]
use tauri::{AppHandle, Listener, Manager, Url};
#[cfg(desktop)]
use tauri::{AppHandle, Manager};
use tauri_plugin_fs::FsExt;
#[cfg(desktop)]
use tauri::{Listener, Url};
#[cfg(target_os = "macos")]
mod macos;
mod transfer_file;
use tauri::{command, Emitter, WebviewUrl, WebviewWindowBuilder, Window};
#[cfg(target_os = "android")]
use tauri_plugin_native_bridge::register_select_directory_callback;
#[cfg(target_os = "android")]
use tauri_plugin_native_bridge::{NativeBridgeExt, OpenExternalUrlRequest};
use tauri_plugin_oauth::start;
#[cfg(not(target_os = "android"))]
@@ -49,7 +50,6 @@ fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
}
}
#[cfg(desktop)]
fn allow_dir_in_scopes(app: &AppHandle, dir: &PathBuf) {
let fs_scope = app.fs_scope();
let asset_protocol_scope = app.asset_protocol_scope();
@@ -223,6 +223,11 @@ pub fn run() {
allow_dir_in_scopes(app.handle(), &PathBuf::from(get_executable_dir()));
}
#[cfg(target_os = "android")]
register_select_directory_callback(app.handle(), move |app, path| {
allow_dir_in_scopes(app, path);
});
#[cfg(desktop)]
{
app.handle().plugin(tauri_plugin_cli::init())?;
@@ -1,5 +1,4 @@
import clsx from 'clsx';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { IoFileTray } from 'react-icons/io5';
import { MdRssFeed } from 'react-icons/md';
@@ -9,20 +8,26 @@ import Menu from '@/components/Menu';
interface ImportMenuProps {
setIsDropdownOpen?: (open: boolean) => void;
onImportBooks: () => void;
onImportBooksFromFiles: () => void;
onImportBooksFromDirectory?: () => void;
onOpenCatalogManager: () => void;
}
const ImportMenu: React.FC<ImportMenuProps> = ({
setIsDropdownOpen,
onImportBooks,
onImportBooksFromFiles,
onImportBooksFromDirectory,
onOpenCatalogManager,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const handleImportBooks = () => {
onImportBooks();
const handleImportFromFiles = () => {
onImportBooksFromFiles();
setIsDropdownOpen?.(false);
};
const handleImportFromDirectory = () => {
onImportBooksFromDirectory?.();
setIsDropdownOpen?.(false);
};
@@ -33,17 +38,21 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
return (
<Menu
className={clsx(
'dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow',
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
)}
className={clsx('dropdown-content bg-base-100 rounded-box z-[1] mt-3 p-2 shadow')}
onCancel={() => setIsDropdownOpen?.(false)}
>
<MenuItem
label={_('From Local File')}
Icon={<IoFileTray className='h-5 w-5' />}
onClick={handleImportBooks}
onClick={handleImportFromFiles}
/>
{onImportBooksFromDirectory && (
<MenuItem
label={_('From Directory')}
Icon={<IoFileTray className='h-5 w-5' />}
onClick={handleImportFromDirectory}
/>
)}
<MenuItem
label={_('Online Library')}
Icon={<MdRssFeed className='h-5 w-5' />}
@@ -26,7 +26,8 @@ import ViewMenu from './ViewMenu';
interface LibraryHeaderProps {
isSelectMode: boolean;
isSelectAll: boolean;
onImportBooks: () => void;
onImportBooksFromFiles: () => void;
onImportBooksFromDirectory?: () => void;
onOpenCatalogManager: () => void;
onToggleSelectMode: () => void;
onSelectAll: () => void;
@@ -36,7 +37,8 @@ interface LibraryHeaderProps {
const LibraryHeader: React.FC<LibraryHeaderProps> = ({
isSelectMode,
isSelectAll,
onImportBooks,
onImportBooksFromFiles,
onImportBooksFromDirectory,
onOpenCatalogManager,
onToggleSelectMode,
onSelectAll,
@@ -152,14 +154,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
<Dropdown
label={_('Import Books')}
className={clsx(
'exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center',
isMobile ? 'dropdown-end' : 'dropdown-center',
'exclude-title-bar-mousedown dropdown-bottom dropdown-center flex h-6 cursor-pointer justify-center',
)}
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center !bg-transparent'
buttonClassName='p-0 h-6 min-h-6 w-6 flex touch-target items-center justify-center !bg-transparent'
toggleButton={<PiPlus role='none' className='m-0.5 h-5 w-5' />}
>
<ImportMenu
onImportBooks={onImportBooks}
onImportBooksFromFiles={onImportBooksFromFiles}
onImportBooksFromDirectory={onImportBooksFromDirectory}
onOpenCatalogManager={onOpenCatalogManager}
/>
</Dropdown>
@@ -7,7 +7,6 @@ import {
RiLoader2Line,
} from 'react-icons/ri';
import { documentDir, join } from '@tauri-apps/api/path';
import { invoke, PermissionState } from '@tauri-apps/api/core';
import { relaunch } from '@tauri-apps/plugin-process';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
@@ -20,6 +19,7 @@ import { formatBytes } from '@/utils/book';
import { getOSPlatform } from '@/utils/misc';
import { getExternalSDCardPath } from '@/utils/bridge';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { requestStoragePermission } from '@/utils/permission';
import Dialog from '@/components/Dialog';
import Dropdown from '@/components/Dropdown';
import MenuItem from '@/components/MenuItem';
@@ -42,10 +42,6 @@ interface MigrationProgress {
currentFile?: string;
}
interface Permissions {
manageStorage: PermissionState;
}
export const MigrateDataWindow = () => {
const _ = useTranslation();
const { appService, envConfig } = useEnv();
@@ -158,13 +154,7 @@ export const MigrateDataWindow = () => {
setErrorMessage('');
if (!dir.includes('Android/data')) {
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
if (permission.manageStorage !== 'granted') {
permission = await invoke<Permissions>(
'plugin:native-bridge|request_manage_storage_permission',
);
}
if (permission.manageStorage !== 'granted') return;
if (!(await requestStoragePermission())) return;
}
try {
@@ -20,6 +20,7 @@ import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
import { setAboutDialogVisible } from '@/components/AboutWindow';
import { setMigrateDataDirDialogVisible } from '@/app/library/components/MigrateDataWindow';
import { requestStoragePermission } from '@/utils/permission';
import { saveSysSettings } from '@/helpers/settings';
import { selectDirectory } from '@/utils/bridge';
import UserAvatar from '@/components/UserAvatar';
@@ -175,13 +176,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
};
const handleSetSavedBookCoverForLockScreen = async () => {
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
if (permission.manageStorage !== 'granted') {
permission = await invoke<Permissions>(
'plugin:native-bridge|request_manage_storage_permission',
);
}
if (permission.manageStorage !== 'granted' && appService?.distChannel === 'readest') return;
if (!(await requestStoragePermission()) && appService?.distChannel === 'readest') return;
const newValue = settings.savedBookCoverForLockScreen ? '' : 'default';
if (newValue) {
+67 -8
View File
@@ -15,7 +15,7 @@ import { formatAuthors, formatTitle, getPrimaryLanguage, listFormater } from '@/
import { eventDispatcher } from '@/utils/event';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import { getFilename } from '@/utils/path';
import { getDirPath, getFilename, joinPaths } from '@/utils/path';
import { parseOpenWithFiles } from '@/helpers/openWith';
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
@@ -38,7 +38,9 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { SelectedFile, useFileSelector } from '@/hooks/useFileSelector';
import { lockScreenOrientation } from '@/utils/bridge';
import { lockScreenOrientation, selectDirectory } from '@/utils/bridge';
import { requestStoragePermission } from '@/utils/permission';
import { SUPPORTED_BOOK_EXTS } from '@/services/constants';
import {
tauriHandleClose,
tauriHandleSetAlwaysOnTop,
@@ -143,7 +145,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
setSettingsDialogOpen(true);
},
onOpenBooks: () => {
handleImportBooks();
handleImportBooksFromFiles();
},
});
@@ -366,6 +368,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
setLoading(true);
const { library } = useLibraryStore.getState();
const failedImports: Array<{ filename: string; errorMessage: string }> = [];
const successfulImports: string[] = [];
const errorMap: [string, string][] = [
['No chapters detected', _('No chapters detected')],
['Failed to parse EPUB', _('Failed to parse the EPUB file')],
@@ -380,15 +383,25 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
if (!file) return;
try {
const book = await appService?.importBook(file, library);
const { path, basePath } = selectedFile;
if (book && groupId) {
book.groupId = groupId;
book.groupName = getGroupName(groupId);
await updateBook(envConfig, book);
} else if (book && path && basePath) {
const rootPath = getDirPath(basePath);
const groupName = getDirPath(path).replace(rootPath, '').replace(/^\//, '');
book.groupName = groupName;
book.groupId = getGroupId(groupName);
await updateBook(envConfig, book);
}
if (user && book && !book.uploadedAt && settings.autoUpload) {
console.log('Uploading book:', book.title);
handleBookUpload(book, false);
}
if (book) {
successfulImports.push(book.title);
}
} catch (error) {
const filename = typeof file === 'string' ? file : file.name;
const baseFilename = getFilename(filename);
@@ -417,8 +430,17 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
_('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(filenames),
}) + (errorMessage ? `\n${errorMessage}` : ''),
timeout: 5000,
type: 'error',
});
} else if (successfulImports.length > 0) {
eventDispatcher.dispatch('toast', {
message: _('Successfully imported {{count}} book(s)', {
count: successfulImports.length,
}),
timeout: 2000,
type: 'success',
});
}
setLibrary([...library]);
@@ -582,9 +604,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
await updateBook(envConfig, book);
};
const handleImportBooks = async () => {
const handleImportBooksFromFiles = async () => {
setIsSelectMode(false);
console.log('Importing books...');
console.log('Importing books from files...');
selectFiles({ type: 'books', multiple: true }).then((result) => {
if (result.files.length === 0 || result.error) return;
const groupId = searchParams?.get('group') || '';
@@ -592,6 +614,40 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
});
};
const handleImportBooksFromDirectory = async () => {
if (!appService || !isTauriAppPlatform()) return;
setIsSelectMode(false);
console.log('Importing books from directory...');
let importDirectory: string | undefined = '';
if (appService.isAndroidApp) {
if (!(await requestStoragePermission())) return;
const response = await selectDirectory();
importDirectory = response.path;
} else {
const selectedDir = await appService.selectDirectory?.('read');
importDirectory = selectedDir;
}
if (!importDirectory) {
console.log('No directory selected');
return;
}
const files = await appService.readDirectory(importDirectory, 'None');
const supportedFiles = files.filter((file) => {
const ext = file.path.split('.').pop()?.toLowerCase() || '';
return SUPPORTED_BOOK_EXTS.includes(ext);
});
const toImportFiles = await Promise.all(
supportedFiles.map(async (file) => {
return {
path: await joinPaths(importDirectory, file.path),
basePath: importDirectory,
};
}),
);
importBooks(toImportFiles, undefined);
};
const handleSetSelectMode = (selectMode: boolean) => {
if (selectMode && appService?.hasHaptics) {
impactFeedback('medium');
@@ -656,7 +712,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<LibraryHeader
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
onImportBooks={handleImportBooks}
onImportBooksFromFiles={handleImportBooksFromFiles}
onImportBooksFromDirectory={
appService?.canReadExternalDir ? handleImportBooksFromDirectory : undefined
}
onOpenCatalogManager={() => setShowCatalogManager(true)}
onToggleSelectMode={() => handleSetSelectMode(!isSelectMode)}
onSelectAll={handleSelectAll}
@@ -742,7 +801,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
isSelectNone={isSelectNone}
handleImportBooks={handleImportBooks}
handleImportBooks={handleImportBooksFromFiles}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete('both')}
@@ -764,7 +823,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
'Welcome to your library. You can import your books here and read them anytime.',
)}
</p>
<button className='btn btn-primary rounded-xl' onClick={handleImportBooks}>
<button className='btn btn-primary rounded-xl' onClick={handleImportBooksFromFiles}>
{_('Import Books')}
</button>
</div>
@@ -18,6 +18,7 @@ export interface SelectedFile {
// For Tauri file
path?: string;
basePath?: string;
}
export interface FileSelectionResult {
+10 -2
View File
@@ -98,6 +98,7 @@ export abstract class BaseAppService implements AppService {
hasScreenBrightness = false;
hasIAP = false;
canCustomizeRootDir = false;
canReadExternalDir = false;
distChannel = 'readest' as DistChannel;
protected CURRENT_MIGRATION_VERSION = 20251124;
@@ -354,7 +355,6 @@ export abstract class BaseAppService implements AppService {
loadedBook.metadata.title = getBaseFilename(filename);
}
} catch (error) {
console.error(error);
throw new Error(`Failed to open the book: ${(error as Error).message || error}`);
}
@@ -405,7 +405,14 @@ export abstract class BaseAppService implements AppService {
} else if (typeof file === 'string' && isContentURI(file)) {
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
} else if (typeof file === 'string' && !isValidURL(file)) {
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
try {
// try to copy the file directly first in case of large files to avoid memory issues
// on desktop when reading recursively from selected directory the direct copy will fail
// due to permission issues, then fallback to read and write files
await this.fs.copyFile(file, getLocalBookFilename(book), 'Books');
} catch {
await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj);
}
} else {
await this.fs.writeFile(getLocalBookFilename(book), 'Books', fileobj);
}
@@ -441,6 +448,7 @@ export abstract class BaseAppService implements AppService {
return existingBook || book;
} catch (error) {
console.error('Error importing book:', error);
throw error;
}
}
@@ -34,7 +34,7 @@ import {
FileItem,
DistChannel,
} from '@/types/system';
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
import { getOSPlatform, isContentURI, isFileURI, isValidURL } from '@/utils/misc';
import { getDirPath, getFilename } from '@/utils/path';
import { NativeFile, RemoteFile } from '@/utils/file';
import { copyURIToPath } from '@/utils/bridge';
@@ -212,17 +212,20 @@ export const nativeFileSystem: FileSystem = {
}
return await new NativeFile(dst, fname, baseDir ? baseDir : null).open();
}
} else if (isFileURI(path)) {
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
} else {
const prefix = await this.getPrefix(base);
const absolutePath = path.startsWith('/') ? path : prefix ? await join(prefix, path) : null;
if (absolutePath && OS_TYPE !== 'android') {
if (OS_TYPE === 'android') {
// NOTE: RemoteFile is not usable on Android due to a known issue of range request in Android WebView.
// see https://issues.chromium.org/issues/40739128
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
} else {
// 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 prefix = await this.getPrefix(base);
const absolutePath = prefix ? await join(prefix, path) : path;
return await new RemoteFile(this.getURL(absolutePath), fname).open();
} else {
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
}
}
},
@@ -319,10 +322,13 @@ export const nativeFileSystem: FileSystem = {
} else {
const filePath = await join(parent, entry.name);
const relativePath = relative ? await join(relative, entry.name) : entry.name;
const fileInfo = await stat(filePath, baseDir ? { baseDir } : undefined);
const opts = baseDir ? { baseDir } : undefined;
const fileSize = await stat(filePath, opts)
.then((info) => info.size)
.catch(() => 0);
fileList.push({
path: relativePath,
size: fileInfo.size,
size: fileSize,
});
}
}
@@ -374,6 +380,7 @@ export class NativeAppService extends BaseAppService {
// CustomizeRootDir has a blocker on macOS App Store builds due to Security Scoped Resource restrictions.
// See: https://github.com/tauri-apps/tauri/issues/3716
override canCustomizeRootDir = DIST_CHANNEL !== 'appstore';
override canReadExternalDir = DIST_CHANNEL !== 'appstore' && DIST_CHANNEL !== 'playstore';
override distChannel = DIST_CHANNEL;
private execDir?: string = undefined;
+10
View File
@@ -371,6 +371,16 @@ foliate-fxl {
display: block;
}
.touch-target {
position: relative;
}
.touch-target::before {
content: "";
position: absolute;
inset: -12px;
}
.no-context-menu {
-webkit-touch-callout: none;
-webkit-user-select: none;
+1
View File
@@ -66,6 +66,7 @@ export interface AppService {
isPortableApp: boolean;
isDesktopApp: boolean;
canCustomizeRootDir: boolean;
canReadExternalDir: boolean;
distChannel: DistChannel;
init(): Promise<void>;
+5
View File
@@ -1,3 +1,4 @@
import { join } from '@tauri-apps/api/path';
import { isContentURI, isFileURI, isValidURL } from './misc';
export const getFilename = (fileOrUri: string) => {
@@ -28,3 +29,7 @@ export const getDirPath = (filePath: string) => {
parts.pop();
return parts.join('/');
};
export const joinPaths = async (...paths: string[]) => {
return await join(...paths);
};
+15
View File
@@ -0,0 +1,15 @@
import { invoke, PermissionState } from '@tauri-apps/api/core';
interface Permissions {
manageStorage: PermissionState;
}
export const requestStoragePermission = async (): Promise<boolean> => {
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
if (permission.manageStorage !== 'granted') {
permission = await invoke<Permissions>(
'plugin:native-bridge|request_manage_storage_permission',
);
}
return permission.manageStorage === 'granted';
};