From c04f19ffb44d0280c864f84f0fc8db650428889a Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 12 Jan 2026 16:26:32 +0100 Subject: [PATCH] feat: add support for exporting book files in book details dialog, closes #2919 (#2930) --- Cargo.lock | 24 +++++++++++++ apps/readest-app/package.json | 1 + .../public/locales/ar/translation.json | 6 +++- .../public/locales/bn/translation.json | 6 +++- .../public/locales/bo/translation.json | 6 +++- .../public/locales/de/translation.json | 6 +++- .../public/locales/el/translation.json | 6 +++- .../public/locales/es/translation.json | 6 +++- .../public/locales/fa/translation.json | 6 +++- .../public/locales/fr/translation.json | 6 +++- .../public/locales/hi/translation.json | 6 +++- .../public/locales/id/translation.json | 6 +++- .../public/locales/it/translation.json | 6 +++- .../public/locales/ja/translation.json | 6 +++- .../public/locales/ko/translation.json | 6 +++- .../public/locales/ms/translation.json | 6 +++- .../public/locales/nl/translation.json | 6 +++- .../public/locales/pl/translation.json | 6 +++- .../public/locales/pt/translation.json | 6 +++- .../public/locales/ru/translation.json | 6 +++- .../public/locales/si/translation.json | 6 +++- .../public/locales/sv/translation.json | 6 +++- .../public/locales/ta/translation.json | 6 +++- .../public/locales/th/translation.json | 6 +++- .../public/locales/tr/translation.json | 6 +++- .../public/locales/uk/translation.json | 6 +++- .../public/locales/vi/translation.json | 6 +++- .../public/locales/zh-CN/translation.json | 6 +++- .../public/locales/zh-TW/translation.json | 6 +++- apps/readest-app/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 5 +-- apps/readest-app/src-tauri/src/lib.rs | 1 + .../reader/components/annotator/Annotator.tsx | 22 +++++------- .../components/metadata/BookDetailModal.tsx | 27 +++++++++++++- .../components/metadata/BookDetailView.tsx | 12 +++++-- apps/readest-app/src/libs/document.ts | 15 ++++++++ apps/readest-app/src/services/appService.ts | 24 ++++++++++++- .../src/services/nativeAppService.ts | 35 ++++++++++++++++++- .../readest-app/src/services/webAppService.ts | 18 ++++++++++ apps/readest-app/src/types/book.ts | 12 ++++++- apps/readest-app/src/types/system.ts | 2 ++ pnpm-lock.yaml | 12 ++++++- 42 files changed, 324 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc446759..e1c036a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-persisted-scope", "tauri-plugin-process", + "tauri-plugin-sharekit", "tauri-plugin-shell", "tauri-plugin-sign-in-with-apple", "tauri-plugin-single-instance", @@ -3085,7 +3086,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.3", ] @@ -5674,6 +5677,27 @@ dependencies = [ "tauri-plugin", ] +[[package]] +name = "tauri-plugin-sharekit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d52e8e7f240043bb8c2cb0275feb8fb78b90ea6647a0105c043989197e69d3" +dependencies = [ + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "windows", + "windows-collections", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.3" diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 4d231108..2b33d140 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -53,6 +53,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.735.0", "@aws-sdk/s3-request-presigner": "^3.735.0", + "@choochmeque/tauri-plugin-sharekit-api": "^0.3.0", "@fabianlars/tauri-plugin-oauth": "2", "@opennextjs/cloudflare": "^1.14.7", "@serwist/next": "^9.4.2", diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index ef42e163..5797109a 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -876,5 +876,9 @@ "Clear search": "مسح البحث", "Clear search history": "مسح سجل البحث", "Quick action disabled": "الإجراء السريع معطل", - "Tap to Toggle Footer": "انقر لتبديل التذييل" + "Tap to Toggle Footer": "انقر لتبديل التذييل", + "Exported successfully": "تم التصدير بنجاح", + "Book exported successfully.": "تم تصدير الكتاب بنجاح.", + "Failed to export the book.": "فشل تصدير الكتاب.", + "Export Book": "تصدير الكتاب" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index bdec132e..a92ee16e 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -836,5 +836,9 @@ "Clear search": "অনুসন্ধান সাফ করুন", "Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন", "Quick action disabled": "দ্রুত কার্যক্রম অক্ষম", - "Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন" + "Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন", + "Exported successfully": "সফলভাবে রপ্তানি হয়েছে", + "Book exported successfully.": "বইটি সফলভাবে রপ্তানি হয়েছে।", + "Failed to export the book.": "বই রপ্তানি করতে ব্যর্থ।", + "Export Book": "বই রপ্তানি করুন" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 41108251..093e37a6 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -826,5 +826,9 @@ "Clear search": "འཚོལ་བཤེར་གཙང་སེལ།", "Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།", "Quick action disabled": "མྱུར་མགྱོགས་བྱ་འགུལ་བཀག་འགོག", - "Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།" + "Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།", + "Exported successfully": "ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།", + "Book exported successfully.": "དཔེ་དེབ་ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།", + "Failed to export the book.": "དཔེ་དེབ་ཕྱིར་འདོན་མི་ཐུབ།", + "Export Book": "དཔེ་དེབ་ཕྱིར་འདོན།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index b0db9d81..b3761fae 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -836,5 +836,9 @@ "Clear search": "Suche löschen", "Clear search history": "Suchverlauf löschen", "Quick action disabled": "Schnellaktion deaktiviert", - "Tap to Toggle Footer": "Tippen, um Fußzeile umzuschalten" + "Tap to Toggle Footer": "Tippen, um Fußzeile umzuschalten", + "Exported successfully": "Erfolgreich exportiert", + "Book exported successfully.": "Buch erfolgreich exportiert.", + "Failed to export the book.": "Buch konnte nicht exportiert werden.", + "Export Book": "Buch exportieren" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 48412560..5c73b78f 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -836,5 +836,9 @@ "Clear search": "Εκκαθάριση αναζήτησης", "Clear search history": "Εκκαθάριση ιστορικού αναζήτησης", "Quick action disabled": "Η γρήγορη ενέργεια απενεργοποιήθηκε", - "Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου" + "Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου", + "Exported successfully": "Εξαγωγή επιτυχής", + "Book exported successfully.": "Το βιβλίο εξήχθη επιτυχώς.", + "Failed to export the book.": "Αποτυχία εξαγωγής του βιβλίου.", + "Export Book": "Εξαγωγή βιβλίου" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 0e4fad3d..6ba481b3 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -846,5 +846,9 @@ "Clear search": "Borrar búsqueda", "Clear search history": "Borrar historial de búsqueda", "Quick action disabled": "Acción rápida desactivada", - "Tap to Toggle Footer": "Toca para alternar pie de página" + "Tap to Toggle Footer": "Toca para alternar pie de página", + "Exported successfully": "Exportado con éxito", + "Book exported successfully.": "Libro exportado con éxito.", + "Failed to export the book.": "Error al exportar el libro.", + "Export Book": "Exportar libro" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 71a2583f..80b8599b 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -836,5 +836,9 @@ "Clear search": "پاک کردن جستجو", "Clear search history": "پاک کردن تاریخچه جستجو", "Quick action disabled": "عملکرد سریع غیرفعال است", - "Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید" + "Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید", + "Exported successfully": "صادرات موفق", + "Book exported successfully.": "کتاب با موفقیت صادر شد.", + "Failed to export the book.": "صادر کردن کتاب ناموفق بود.", + "Export Book": "صادر کردن کتاب" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 17d5276f..9314a476 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -846,5 +846,9 @@ "Clear search": "Effacer la recherche", "Clear search history": "Effacer l'historique de recherche", "Quick action disabled": "Action rapide désactivée", - "Tap to Toggle Footer": "Appuyez pour afficher/masquer le pied de page" + "Tap to Toggle Footer": "Appuyez pour afficher/masquer le pied de page", + "Exported successfully": "Exporté avec succès", + "Book exported successfully.": "Livre exporté avec succès.", + "Failed to export the book.": "Échec de l'exportation du livre.", + "Export Book": "Exporter le livre" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index cd993600..10fdcaa1 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -836,5 +836,9 @@ "Clear search": "खोज साफ़ करें", "Clear search history": "खोज इतिहास साफ़ करें", "Quick action disabled": "त्वरित क्रिया अक्षम", - "Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें" + "Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें", + "Exported successfully": "सफलतापूर्वक निर्यात किया गया", + "Book exported successfully.": "पुस्तक सफलतापूर्वक निर्यात की गई।", + "Failed to export the book.": "पुस्तक निर्यात करने में विफल।", + "Export Book": "पुस्तक निर्यात करें" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 56c64698..6cb26e12 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -826,5 +826,9 @@ "Clear search": "Hapus pencarian", "Clear search history": "Hapus riwayat pencarian", "Quick action disabled": "Tindakan cepat dinonaktifkan", - "Tap to Toggle Footer": "Ketuk untuk beralih footer" + "Tap to Toggle Footer": "Ketuk untuk beralih footer", + "Exported successfully": "Berhasil diekspor", + "Book exported successfully.": "Buku berhasil diekspor.", + "Failed to export the book.": "Gagal mengekspor buku.", + "Export Book": "Ekspor Buku" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 5c21d32e..5f8e80c7 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -846,5 +846,9 @@ "Clear search": "Cancella ricerca", "Clear search history": "Cancella cronologia ricerche", "Quick action disabled": "Azione rapida disattivata", - "Tap to Toggle Footer": "Tocca per mostrare/nascondere il piè di pagina" + "Tap to Toggle Footer": "Tocca per mostrare/nascondere il piè di pagina", + "Exported successfully": "Esportato con successo", + "Book exported successfully.": "Libro esportato con successo.", + "Failed to export the book.": "Impossibile esportare il libro.", + "Export Book": "Esporta libro" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 91b49129..4e9a738f 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -826,5 +826,9 @@ "Clear search": "検索をクリア", "Clear search history": "検索履歴をクリア", "Quick action disabled": "クイックアクション無効", - "Tap to Toggle Footer": "タップでフッターを切り替え" + "Tap to Toggle Footer": "タップでフッターを切り替え", + "Exported successfully": "エクスポート成功", + "Book exported successfully.": "書籍のエクスポートに成功しました。", + "Failed to export the book.": "書籍のエクスポートに失敗しました。", + "Export Book": "書籍をエクスポート" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index c77ca3d9..6dd12635 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -826,5 +826,9 @@ "Clear search": "검색 지우기", "Clear search history": "검색 기록 지우기", "Quick action disabled": "빠른 작업 비활성화됨", - "Tap to Toggle Footer": "탭하여 바닥글 전환" + "Tap to Toggle Footer": "탭하여 바닥글 전환", + "Exported successfully": "내보내기 성공", + "Book exported successfully.": "책을 성공적으로 내보냈습니다.", + "Failed to export the book.": "책 내보내기에 실패했습니다.", + "Export Book": "책 내보내기" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 2c3cfee1..d3f3dda9 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -826,5 +826,9 @@ "Clear search": "Kosongkan carian", "Clear search history": "Kosongkan sejarah carian", "Quick action disabled": "Tindakan pantas dilumpuhkan", - "Tap to Toggle Footer": "Ketik untuk togol pengaki" + "Tap to Toggle Footer": "Ketik untuk togol pengaki", + "Exported successfully": "Berjaya dieksport", + "Book exported successfully.": "Buku berjaya dieksport.", + "Failed to export the book.": "Gagal mengeksport buku.", + "Export Book": "Eksport Buku" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index cdcca191..f28acb43 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -836,5 +836,9 @@ "Clear search": "Zoekopdracht wissen", "Clear search history": "Zoekgeschiedenis wissen", "Quick action disabled": "Snelle actie uitgeschakeld", - "Tap to Toggle Footer": "Tik om voettekst te wisselen" + "Tap to Toggle Footer": "Tik om voettekst te wisselen", + "Exported successfully": "Succesvol geëxporteerd", + "Book exported successfully.": "Boek succesvol geëxporteerd.", + "Failed to export the book.": "Exporteren van het boek mislukt.", + "Export Book": "Boek exporteren" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 6edec167..aceebf8e 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -856,5 +856,9 @@ "Clear search": "Wyczyść wyszukiwanie", "Clear search history": "Wyczyść historię wyszukiwania", "Quick action disabled": "Szybka akcja wyłączona", - "Tap to Toggle Footer": "Dotknij, aby przełączyć stopkę" + "Tap to Toggle Footer": "Dotknij, aby przełączyć stopkę", + "Exported successfully": "Wyeksportowano pomyślnie", + "Book exported successfully.": "Książka została wyeksportowana pomyślnie.", + "Failed to export the book.": "Nie udało się wyeksportować książki.", + "Export Book": "Eksportuj książkę" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index e2f28fae..ea227948 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -846,5 +846,9 @@ "Clear search": "Limpar pesquisa", "Clear search history": "Limpar histórico de pesquisa", "Quick action disabled": "Ação rápida desativada", - "Tap to Toggle Footer": "Toque para alternar rodapé" + "Tap to Toggle Footer": "Toque para alternar rodapé", + "Exported successfully": "Exportado com sucesso", + "Book exported successfully.": "Livro exportado com sucesso.", + "Failed to export the book.": "Falha ao exportar o livro.", + "Export Book": "Exportar livro" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 04e7789c..ef1de359 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -856,5 +856,9 @@ "Clear search": "Очистить поиск", "Clear search history": "Очистить историю поиска", "Quick action disabled": "Быстрое действие отключено", - "Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула" + "Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула", + "Exported successfully": "Успешно экспортировано", + "Book exported successfully.": "Книга успешно экспортирована.", + "Failed to export the book.": "Не удалось экспортировать книгу.", + "Export Book": "Экспорт книги" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index b66509f3..18f9f27f 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -836,5 +836,9 @@ "Clear search": "සෙවුම හිස් කරන්න", "Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න", "Quick action disabled": "ඉක්මන් ක්‍රියාව අක්‍රීයයි", - "Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න" + "Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න", + "Exported successfully": "සාර්ථකව අපනයනය කරන ලදී", + "Book exported successfully.": "පොත සාර්ථකව අපනයනය කරන ලදී.", + "Failed to export the book.": "පොත අපනයනය කිරීමට අසමත් විය.", + "Export Book": "පොත අපනයනය කරන්න" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index c3b482fc..683d5828 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -836,5 +836,9 @@ "Clear search": "Rensa sökning", "Clear search history": "Rensa sökhistorik", "Quick action disabled": "Snabbåtgärd inaktiverad", - "Tap to Toggle Footer": "Tryck för att växla sidfot" + "Tap to Toggle Footer": "Tryck för att växla sidfot", + "Exported successfully": "Exporterad", + "Book exported successfully.": "Boken har exporterats.", + "Failed to export the book.": "Det gick inte att exportera boken.", + "Export Book": "Exportera bok" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 152b9792..36726067 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -836,5 +836,9 @@ "Clear search": "தேடலை அழி", "Clear search history": "தேடல் வரலாற்றை அழி", "Quick action disabled": "விரைவு செயல் முடக்கப்பட்டது", - "Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்" + "Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்", + "Exported successfully": "ஏற்றுமதி வெற்றி", + "Book exported successfully.": "புத்தகம் வெற்றிகரமாக ஏற்றுமதி செய்யப்பட்டது.", + "Failed to export the book.": "புத்தகத்தை ஏற்றுமதி செய்ய இயலவில்லை.", + "Export Book": "புத்தகத்தை ஏற்றுமதி செய்" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 6b4507da..12b79ad4 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -826,5 +826,9 @@ "Clear search": "ล้างการค้นหา", "Clear search history": "ล้างประวัติการค้นหา", "Quick action disabled": "ปิดใช้งานการดำเนินการด่วน", - "Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย" + "Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย", + "Exported successfully": "ส่งออกสำเร็จ", + "Book exported successfully.": "ส่งออกหนังสือสำเร็จ", + "Failed to export the book.": "ส่งออกหนังสือล้มเหลว", + "Export Book": "ส่งออกหนังสือ" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 245d2b9f..a2140be0 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -836,5 +836,9 @@ "Clear search": "Aramayı temizle", "Clear search history": "Arama geçmişini temizle", "Quick action disabled": "Hızlı eylem devre dışı", - "Tap to Toggle Footer": "Altbilgiyi değiştirmek için dokunun" + "Tap to Toggle Footer": "Altbilgiyi değiştirmek için dokunun", + "Exported successfully": "Başarıyla dışa aktarıldı", + "Book exported successfully.": "Kitap başarıyla dışa aktarıldı.", + "Failed to export the book.": "Kitap dışa aktarılamadı.", + "Export Book": "Kitabı Dışa Aktar" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index c3c23bdd..da31aaed 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -856,5 +856,9 @@ "Clear search": "Очистити пошук", "Clear search history": "Очистити історію пошуку", "Quick action disabled": "Швидку дію вимкнено", - "Tap to Toggle Footer": "Торкніться, щоб перемкнути нижній колонтитул" + "Tap to Toggle Footer": "Торкніться, щоб перемкнути нижній колонтитул", + "Exported successfully": "Успішно експортовано", + "Book exported successfully.": "Книгу успішно експортовано.", + "Failed to export the book.": "Не вдалося експортувати книгу.", + "Export Book": "Експортувати книгу" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 3316ceaf..8ee38164 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -826,5 +826,9 @@ "Clear search": "Xóa tìm kiếm", "Clear search history": "Xóa lịch sử tìm kiếm", "Quick action disabled": "Đã tắt thao tác nhanh", - "Tap to Toggle Footer": "Nhấn để bật/tắt chân trang" + "Tap to Toggle Footer": "Nhấn để bật/tắt chân trang", + "Exported successfully": "Xuất thành công", + "Book exported successfully.": "Sách đã được xuất thành công.", + "Failed to export the book.": "Xuất sách thất bại.", + "Export Book": "Xuất 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 c0b23f69..af9fc191 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -826,5 +826,9 @@ "Clear search": "清除搜索", "Clear search history": "清除搜索历史", "Quick action disabled": "快捷操作已禁用", - "Tap to Toggle Footer": "点击切换页脚" + "Tap to Toggle Footer": "点击切换页脚", + "Exported successfully": "导出成功", + "Book exported successfully.": "书籍导出成功。", + "Failed to export the book.": "书籍导出失败。", + "Export Book": "导出书籍" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 44a420b3..3aa356cb 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -826,5 +826,9 @@ "Clear search": "清除搜尋", "Clear search history": "清除搜尋歷史", "Quick action disabled": "快捷操作已停用", - "Tap to Toggle Footer": "點擊切換頁尾" + "Tap to Toggle Footer": "點擊切換頁尾", + "Exported successfully": "匯出成功", + "Book exported successfully.": "書籍匯出成功。", + "Failed to export the book.": "書籍匯出失敗。", + "Export Book": "匯出書籍" } diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml index 67192574..676e3a5a 100644 --- a/apps/readest-app/src-tauri/Cargo.toml +++ b/apps/readest-app/src-tauri/Cargo.toml @@ -53,6 +53,7 @@ tauri-plugin-persisted-scope = "2" tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" } tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" } tauri-plugin-websocket = "2" +tauri-plugin-sharekit = "0.3" [target."cfg(target_os = \"macos\")".dependencies] rand = "0.8" diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 710d6c8e..d2a175d0 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -156,8 +156,9 @@ "haptics:allow-impact-feedback", "haptics:allow-notification-feedback", "haptics:allow-selection-feedback", - "native-bridge:default", + "deep-link:default", + "sharekit:default", "native-tts:default", - "deep-link:default" + "native-bridge:default" ] } diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index ce62d5dd..c7ec9b85 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -170,6 +170,7 @@ pub fn run() { .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_sharekit::init()) .plugin(tauri_plugin_native_bridge::init()) .plugin(tauri_plugin_native_tts::init()); diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index f771a022..9805bf92 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -7,7 +7,7 @@ import { useEnv } from '@/context/EnvContext'; import { BookNote, BooknoteGroup, HighlightColor, HighlightStyle } from '@/types/book'; import { NOTE_PREFIX } from '@/types/view'; import { NativeTouchEventType } from '@/types/system'; -import { getLocale, getOSPlatform, uniqueId } from '@/utils/misc'; +import { getLocale, getOSPlatform, makeSafeFilename, uniqueId } from '@/utils/misc'; import { useThemeStore } from '@/store/themeStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useSettingsStore } from '@/store/settingsStore'; @@ -746,7 +746,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { [selection?.text], ); - const handleExportMarkdown = (event: CustomEvent) => { + const handleExportMarkdown = async (event: CustomEvent) => { const { bookKey: exportBookKey } = event.detail; if (bookKey !== exportBookKey) return; @@ -813,22 +813,18 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { } const markdownContent = lines.join('\n'); + setTimeout(() => { + // Delay to ensure it won't be overridden by system clipboard actions + navigator.clipboard?.writeText(markdownContent); + }, 100); - navigator.clipboard?.writeText(markdownContent); + const filename = `${makeSafeFilename(book.title)}.md`; + const saved = await appService?.saveFile(filename, markdownContent, 'text/markdown'); eventDispatcher.dispatch('toast', { type: 'info', - message: _('Copied to clipboard'), - className: 'whitespace-nowrap', + message: saved ? _('Exported successfully') : _('Copied to clipboard'), timeout: 2000, }); - if (appService?.isMobile) return; - const blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${book.title.replace(/\s+/g, '_')}.md`; - a.click(); - URL.revokeObjectURL(url); }; const selectionAnnotated = selection?.annotated; diff --git a/apps/readest-app/src/components/metadata/BookDetailModal.tsx b/apps/readest-app/src/components/metadata/BookDetailModal.tsx index 983fc3e4..573ab00e 100644 --- a/apps/readest-app/src/components/metadata/BookDetailModal.tsx +++ b/apps/readest-app/src/components/metadata/BookDetailModal.tsx @@ -8,11 +8,14 @@ import { useThemeStore } from '@/store/themeStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useMetadataEdit } from './useMetadataEdit'; import { DeleteAction } from '@/types/system'; +import { eventDispatcher } from '@/utils/event'; +import { isWebAppPlatform } from '@/services/environment'; import Alert from '@/components/Alert'; import Dialog from '@/components/Dialog'; import BookDetailView from './BookDetailView'; import BookDetailEdit from './BookDetailEdit'; import SourceSelector from './SourceSelector'; +import Spinner from '../Spinner'; interface BookDetailModalProps { book: Book; @@ -44,9 +47,10 @@ const BookDetailModal: React.FC = ({ handleBookMetadataUpdate, }) => { const _ = useTranslation(); - const { envConfig } = useEnv(); + const { envConfig, appService } = useEnv(); const { safeAreaInsets } = useThemeStore(); const [activeDeleteAction, setActiveDeleteAction] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [editMode, setEditMode] = useState(false); const [bookMeta, setBookMeta] = useState(null); const [fileSize, setFileSize] = useState(null); @@ -167,6 +171,20 @@ const BookDetailModal: React.FC = ({ } }; + const handleBookExport = async () => { + setIsLoading(true); + setTimeout(async () => { + const success = await appService?.exportBook(book); + setIsLoading(false); + if (!isWebAppPlatform()) { + eventDispatcher.dispatch('toast', { + type: success ? 'info' : 'error', + message: success ? _('Book exported successfully.') : _('Failed to export the book.'), + }); + } + }, 0); + }; + const currentDeleteConfig = activeDeleteAction ? deleteConfigs[activeDeleteAction] : null; return ( @@ -213,6 +231,7 @@ const BookDetailModal: React.FC = ({ onDeleteLocalCopy={handleBookDeleteLocalCopy ? handleDeleteLocalCopy : undefined} onDownload={handleBookDownload ? handleRedownload : undefined} onUpload={handleBookUpload ? handleReupload : undefined} + onExport={handleBookExport} /> )} @@ -228,6 +247,12 @@ const BookDetailModal: React.FC = ({ /> )} + {isLoading && ( +
+ +
+ )} + {activeDeleteAction && currentDeleteConfig && (
void; onDownload?: () => void; onUpload?: () => void; + onExport?: () => void; } const BookDetailView: React.FC = ({ @@ -44,13 +46,14 @@ const BookDetailView: React.FC = ({ onDeleteLocalCopy, onDownload, onUpload, + onExport, }) => { const _ = useTranslation(); return (
-
+
@@ -62,7 +65,7 @@ const BookDetailView: React.FC = ({ {formatAuthors(book.author, book.primaryLanguage) || _('Unknown')}

-
+
{onEdit && ( )} + {book.downloadedAt && onExport && ( + + )}
diff --git a/apps/readest-app/src/libs/document.ts b/apps/readest-app/src/libs/document.ts index 4fc06776..a38fd27f 100644 --- a/apps/readest-app/src/libs/document.ts +++ b/apps/readest-app/src/libs/document.ts @@ -79,6 +79,8 @@ export const EXTS: Record = { CBZ: 'cbz', FB2: 'fb2', FBZ: 'fbz', + TXT: 'txt', + MD: 'md', }; export const MIMETYPES: Record = { @@ -90,6 +92,8 @@ export const MIMETYPES: Record = { CBZ: ['application/vnd.comicbook+zip', 'application/zip', 'application/x-cbz'], FB2: ['application/x-fictionbook+xml', 'text/xml', 'application/xml'], FBZ: ['application/x-zip-compressed-fb2', 'application/zip'], + TXT: ['text/plain'], + MD: ['text/markdown', 'text/x-markdown'], }; export class DocumentLoader { @@ -254,3 +258,14 @@ export const getFileExtFromMimeType = (mimeType?: string): string => { } return ''; }; + +export const getMimeTypeFromFileExt = (ext: string): string => { + ext = ext.toLowerCase(); + for (const format in EXTS) { + if (EXTS[format as BookFormat] === ext) { + const mimeTypes = MIMETYPES[format as BookFormat]; + return mimeTypes[0] || 'application/octet-stream'; + } + } + return 'application/octet-stream'; +}; diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 28a99d1e..bcf51a19 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -58,7 +58,14 @@ import { DEFAULT_EINK_VIEW_SETTINGS, } from './constants'; import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; -import { getOSPlatform, getTargetLang, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc'; +import { + getOSPlatform, + getTargetLang, + isCJKEnv, + isContentURI, + isValidURL, + makeSafeFilename, +} from '@/utils/misc'; import { deserializeConfig, serializeConfig } from '@/utils/serializer'; import { downloadFile, @@ -114,6 +121,12 @@ export abstract class BaseAppService implements AppService { abstract setCustomRootDir(customRootDir: string): Promise; abstract selectDirectory(mode: SelectDirectoryMode): Promise; abstract selectFiles(name: string, extensions: string[]): Promise; + abstract saveFile( + filename: string, + content: string | ArrayBuffer, + filepath: string, + mimeType?: string, + ): Promise; protected async runMigrations(lastMigrationVersion: number): Promise { if (lastMigrationVersion < 20251124) { @@ -661,6 +674,15 @@ export abstract class BaseAppService implements AppService { } } + async exportBook(book: Book): Promise { + const { file } = await this.loadBookContent(book); + const content = await file.arrayBuffer(); + const filename = `${makeSafeFilename(book.title)}.${book.format.toLowerCase()}`; + const filepath = await this.resolveFilePath(getLocalBookFilename(book), 'Books'); + const fileType = file.type || 'application/octet-stream'; + return await this.saveFile(filename, content, filepath, fileType); + } + async isBookAvailable(book: Book): Promise { const fp = getLocalBookFilename(book); if (await this.fs.exists(fp, 'Books')) { diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index cc2ad710..0d652a9c 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -14,7 +14,7 @@ import { DirEntry, } from '@tauri-apps/plugin-fs'; import { invoke, convertFileSrc } from '@tauri-apps/api/core'; -import { open as openDialog } from '@tauri-apps/plugin-dialog'; +import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog'; import { join, basename, @@ -25,6 +25,7 @@ import { tempDir, } from '@tauri-apps/api/path'; import { type as osType } from '@tauri-apps/plugin-os'; +import { shareFile } from '@choochmeque/tauri-plugin-sharekit-api'; import { FileSystem, @@ -474,6 +475,38 @@ export class NativeAppService extends BaseAppService { return Array.isArray(selected) ? selected : selected ? [selected] : []; } + async saveFile( + filename: string, + content: string | ArrayBuffer, + filepath: string, + mimeType?: string, + ): Promise { + try { + const ext = filename.split('.').pop() || ''; + if (this.isIOSApp) { + await shareFile(filepath, { + mimeType: mimeType || 'application/octet-stream', + }); + } else { + const filePath = await saveDialog({ + defaultPath: filename, + filters: [{ name: ext.toUpperCase(), extensions: [ext] }], + }); + if (!filePath) return false; + + if (typeof content === 'string') { + await writeTextFile(filePath, content); + } else { + await writeFile(filePath, new Uint8Array(content)); + } + } + return true; + } catch (error) { + console.error('Failed to save file:', error); + return false; + } + } + async migrate20251029() { console.log('Running migration 20251029 to update paths in Images dir...'); const rootPath = await this.resolveFilePath('..', 'Data'); diff --git a/apps/readest-app/src/services/webAppService.ts b/apps/readest-app/src/services/webAppService.ts index f686e822..182bc490 100644 --- a/apps/readest-app/src/services/webAppService.ts +++ b/apps/readest-app/src/services/webAppService.ts @@ -321,4 +321,22 @@ export class WebAppService extends BaseAppService { async selectFiles(): Promise { throw new Error('selectFiles is not supported in browser'); } + + async saveFile(filename: string, content: string | ArrayBuffer, mimeType?: string): Promise { + try { + const blob = new Blob([content], { type: mimeType || 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + return true; + } catch (error) { + console.error('Failed to save file:', error); + return false; + } + } } diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index 208d5758..24a0433b 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -2,7 +2,17 @@ import { BookMetadata } from '@/libs/document'; import { TTSHighlightOptions } from '@/services/tts/types'; import { AnnotationToolType } from './annotator'; -export type BookFormat = 'EPUB' | 'PDF' | 'MOBI' | 'AZW' | 'AZW3' | 'CBZ' | 'FB2' | 'FBZ'; +export type BookFormat = + | 'EPUB' + | 'PDF' + | 'MOBI' + | 'AZW' + | 'AZW3' + | 'CBZ' + | 'FB2' + | 'FBZ' + | 'TXT' + | 'MD'; export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt'; export type HighlightStyle = 'highlight' | 'underline' | 'squiggly'; export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet'; diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index 1aa49b05..3ecf3c0c 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -107,6 +107,7 @@ export interface AppService { selectDirectory(mode: SelectDirectoryMode): Promise; selectFiles(name: string, extensions: string[]): Promise; readDirectory(path: string, base: BaseDir): Promise; + saveFile(filename: string, content: string | ArrayBuffer, mimeType?: string): Promise; getDefaultViewSettings(): ViewSettings; loadSettings(): Promise; @@ -132,6 +133,7 @@ export interface AppService { onProgress?: ProgressHandler, ): Promise; downloadBookCovers(books: Book[], redownload?: boolean): Promise; + exportBook(book: Book): Promise; isBookAvailable(book: Book): Promise; getBookFileSize(book: Book): Promise; loadBookConfig(book: Book, settings: SystemSettings): Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d80cbac..05459a88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: '@aws-sdk/s3-request-presigner': specifier: ^3.735.0 version: 3.735.0 + '@choochmeque/tauri-plugin-sharekit-api': + specifier: ^0.3.0 + version: 0.3.0 '@fabianlars/tauri-plugin-oauth': specifier: '2' version: 2.0.0 @@ -954,6 +957,9 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@choochmeque/tauri-plugin-sharekit-api@0.3.0': + resolution: {integrity: sha512-YuFbBOZIVQF2080stK4llB9l2KFKY3YuhurRVS0TeHEAiLWMbblsXuuwC4IufEM9hTChRGQyJIOnxNCn7+T/EQ==} + '@cloudflare/kv-asset-handler@0.4.1': resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} engines: {node: '>=18.0.0'} @@ -8237,6 +8243,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@choochmeque/tauri-plugin-sharekit-api@0.3.0': + dependencies: + '@tauri-apps/api': 2.9.1 + '@cloudflare/kv-asset-handler@0.4.1': dependencies: mime: 3.0.0 @@ -14287,7 +14297,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.24.4 + browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0