diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 6760e4d1..4a97b220 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -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}} كتاب" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 42d209aa..d2ae50e2 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -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}}টি বই আমদানি করা হয়েছে" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index f05fddbf..b19be887 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -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}} དེབ་འདེམས་སོང་" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index bf3e9951..99d9be25 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index b51e4eb0..36338055 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -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}} βιβλίων" } diff --git a/apps/readest-app/public/locales/en/translation.json b/apps/readest-app/public/locales/en/translation.json index d9800817..8d2214d3 100644 --- a/apps/readest-app/public/locales/en/translation.json +++ b/apps/readest-app/public/locales/en/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index f0da8c84..6b963fa8 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index b40bc4d7..2166aa43 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -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}} کتاب وارد شد" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 7e99d120..1413ab83 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 7985a65a..5ef44c4c 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -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}} पुस्तकों का आयात किया गया" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index ce09ec58..f2830cfc 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index a593fdd0..d3c1c4c4 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index e46e4577..3f63ee08 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -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}}冊の本をインポートしました" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index b10c4600..54e06d9b 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -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}} 권의 책이 가져와졌습니다" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 5d3ee15c..151c5497 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 76390609..a0692d76 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 6c2afaa5..0381b51b 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 67b3dce3..f300273b 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 943f9b2a..352c9ac0 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -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}} книг" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index bb252ec6..75d81d9c 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -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}} පොත්" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 5e358ca5..90987c12 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 8f770b4b..213f7dec 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -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}} புத்தகங்கள் இறக்குமதி செய்யப்பட்டது" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 4ba5b894..15bb8454 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -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}} หนังสือเรียบร้อยแล้ว" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 3ae885ad..746236cf 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -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ı" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index a26339d3..5f679766 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -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}} книг" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 8c8ebad8..8a2b63e8 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index ad6fa445..2f93cbd6 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -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}} 本书" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 74ec29e3..eeb02fb8 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -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}} 本書" } diff --git a/apps/readest-app/src-tauri/build.rs b/apps/readest-app/src-tauri/build.rs index 901abda0..46afa548 100644 --- a/apps/readest-app/src-tauri/build.rs +++ b/apps/readest-app/src-tauri/build.rs @@ -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()); } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs index 8a4da421..09fb772d 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs @@ -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( #[command] pub(crate) async fn select_directory( app: AppHandle, + callback_state: State<'_, DirectoryCallbackState>, ) -> Result { - 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] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs index 0d6ee5ac..cef8dfdc 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs @@ -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> crate::NativeBridgeExt for T { } } +type DirectoryCallback = Box, &PathBuf) + Send + Sync>; + +pub struct DirectoryCallbackState { + pub callback: Arc>>>, +} + +impl Default for DirectoryCallbackState { + fn default() -> Self { + Self { + callback: Arc::new(Mutex::new(None)), + } + } +} + /// Initializes the plugin. pub fn init() -> TauriPlugin { Builder::new("native-bridge") @@ -66,7 +84,18 @@ pub fn init() -> TauriPlugin { #[cfg(desktop)] let native_bridge = desktop::init(app, api)?; app.manage(native_bridge); + app.manage(DirectoryCallbackState::::default()); Ok(()) }) .build() } + +pub fn register_select_directory_callback( + app: &AppHandle, + callback: impl Fn(&AppHandle, &PathBuf) + Send + Sync + 'static, +) { + if let Some(state) = app.try_state::>() { + let mut cb = state.callback.lock().unwrap(); + *cb = Some(Box::new(callback)); + } +} diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index e2a475be..cf63d775 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -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) { } } -#[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())?; diff --git a/apps/readest-app/src/app/library/components/ImportMenu.tsx b/apps/readest-app/src/app/library/components/ImportMenu.tsx index f2646243..e4816fc1 100644 --- a/apps/readest-app/src/app/library/components/ImportMenu.tsx +++ b/apps/readest-app/src/app/library/components/ImportMenu.tsx @@ -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 = ({ 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 = ({ return ( setIsDropdownOpen?.(false)} > } - onClick={handleImportBooks} + onClick={handleImportFromFiles} /> + {onImportBooksFromDirectory && ( + } + onClick={handleImportFromDirectory} + /> + )} } diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index 05e331de..77309ba7 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -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 = ({ isSelectMode, isSelectAll, - onImportBooks, + onImportBooksFromFiles, + onImportBooksFromDirectory, onOpenCatalogManager, onToggleSelectMode, onSelectAll, @@ -152,14 +154,14 @@ const LibraryHeader: React.FC = ({ } > diff --git a/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx b/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx index 96d7c12d..bd38ebfb 100644 --- a/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx +++ b/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx @@ -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('plugin:native-bridge|checkPermissions'); - if (permission.manageStorage !== 'granted') { - permission = await invoke( - 'plugin:native-bridge|request_manage_storage_permission', - ); - } - if (permission.manageStorage !== 'granted') return; + if (!(await requestStoragePermission())) return; } try { diff --git a/apps/readest-app/src/app/library/components/SettingsMenu.tsx b/apps/readest-app/src/app/library/components/SettingsMenu.tsx index 4215b2e4..8628f76c 100644 --- a/apps/readest-app/src/app/library/components/SettingsMenu.tsx +++ b/apps/readest-app/src/app/library/components/SettingsMenu.tsx @@ -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 = ({ setIsDropdownOpen }) => { }; const handleSetSavedBookCoverForLockScreen = async () => { - let permission = await invoke('plugin:native-bridge|checkPermissions'); - if (permission.manageStorage !== 'granted') { - permission = await invoke( - '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) { diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 060f46ef..54ece2a4 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -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 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.', )}

- diff --git a/apps/readest-app/src/hooks/useFileSelector.ts b/apps/readest-app/src/hooks/useFileSelector.ts index 3fc90dc9..11b1f067 100644 --- a/apps/readest-app/src/hooks/useFileSelector.ts +++ b/apps/readest-app/src/hooks/useFileSelector.ts @@ -18,6 +18,7 @@ export interface SelectedFile { // For Tauri file path?: string; + basePath?: string; } export interface FileSelectionResult { diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index bb99cad8..ad8900d3 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -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; } } diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 1835429d..0a642df2 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -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; diff --git a/apps/readest-app/src/styles/globals.css b/apps/readest-app/src/styles/globals.css index 81ea8f0f..f362192c 100644 --- a/apps/readest-app/src/styles/globals.css +++ b/apps/readest-app/src/styles/globals.css @@ -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; diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index 5677e45f..d69b4271 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -66,6 +66,7 @@ export interface AppService { isPortableApp: boolean; isDesktopApp: boolean; canCustomizeRootDir: boolean; + canReadExternalDir: boolean; distChannel: DistChannel; init(): Promise; diff --git a/apps/readest-app/src/utils/path.ts b/apps/readest-app/src/utils/path.ts index 8e43ba7e..34f345d3 100644 --- a/apps/readest-app/src/utils/path.ts +++ b/apps/readest-app/src/utils/path.ts @@ -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); +}; diff --git a/apps/readest-app/src/utils/permission.ts b/apps/readest-app/src/utils/permission.ts new file mode 100644 index 00000000..89fc2fa9 --- /dev/null +++ b/apps/readest-app/src/utils/permission.ts @@ -0,0 +1,15 @@ +import { invoke, PermissionState } from '@tauri-apps/api/core'; + +interface Permissions { + manageStorage: PermissionState; +} + +export const requestStoragePermission = async (): Promise => { + let permission = await invoke('plugin:native-bridge|checkPermissions'); + if (permission.manageStorage !== 'granted') { + permission = await invoke( + 'plugin:native-bridge|request_manage_storage_permission', + ); + } + return permission.manageStorage === 'granted'; +};