feat: add support for exporting book files in book details dialog, closes #2919 (#2930)

This commit is contained in:
Huang Xin
2026-01-12 16:26:32 +01:00
committed by GitHub
parent 4a624e3902
commit c04f19ffb4
42 changed files with 324 additions and 49 deletions
Generated
+24
View File
@@ -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"
+1
View File
@@ -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",
@@ -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": "تصدير الكتاب"
}
@@ -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": "বই রপ্তানি করুন"
}
@@ -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": "དཔེ་དེབ་ཕྱིར་འདོན།"
}
@@ -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"
}
@@ -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": "Εξαγωγή βιβλίου"
}
@@ -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"
}
@@ -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": "صادر کردن کتاب"
}
@@ -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"
}
@@ -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": "पुस्तक निर्यात करें"
}
@@ -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"
}
@@ -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"
}
@@ -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": "書籍をエクスポート"
}
@@ -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": "책 내보내기"
}
@@ -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"
}
@@ -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"
}
@@ -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ę"
}
@@ -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"
}
@@ -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": "Экспорт книги"
}
@@ -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": "පොත අපනයනය කරන්න"
}
@@ -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"
}
@@ -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": "புத்தகத்தை ஏற்றுமதி செய்"
}
@@ -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": "ส่งออกหนังสือ"
}
@@ -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"
}
@@ -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": "Експортувати книгу"
}
@@ -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"
}
@@ -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": "导出书籍"
}
@@ -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": "匯出書籍"
}
+1
View File
@@ -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"
@@ -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"
]
}
+1
View File
@@ -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());
@@ -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;
@@ -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<BookDetailModalProps> = ({
handleBookMetadataUpdate,
}) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { safeAreaInsets } = useThemeStore();
const [activeDeleteAction, setActiveDeleteAction] = useState<DeleteAction | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [editMode, setEditMode] = useState(false);
const [bookMeta, setBookMeta] = useState<BookMetadata | null>(null);
const [fileSize, setFileSize] = useState<number | null>(null);
@@ -167,6 +171,20 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
}
};
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<BookDetailModalProps> = ({
onDeleteLocalCopy={handleBookDeleteLocalCopy ? handleDeleteLocalCopy : undefined}
onDownload={handleBookDownload ? handleRedownload : undefined}
onUpload={handleBookUpload ? handleReupload : undefined}
onExport={handleBookExport}
/>
)}
</div>
@@ -228,6 +247,12 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
/>
)}
{isLoading && (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<Spinner loading />
</div>
)}
{activeDeleteAction && currentDeleteConfig && (
<div
className={clsx('fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
@@ -5,6 +5,7 @@ import {
MdOutlineCloudUpload,
MdOutlineDelete,
MdOutlineEdit,
MdSaveAlt,
} from 'react-icons/md';
import { Book } from '@/types/book';
@@ -32,6 +33,7 @@ interface BookDetailViewProps {
onDeleteLocalCopy?: () => void;
onDownload?: () => void;
onUpload?: () => void;
onExport?: () => void;
}
const BookDetailView: React.FC<BookDetailViewProps> = ({
@@ -44,13 +46,14 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
onDeleteLocalCopy,
onDownload,
onUpload,
onExport,
}) => {
const _ = useTranslation();
return (
<div className='relative w-full rounded-lg'>
<div className='mb-6 me-4 flex h-32 items-start'>
<div className='me-10 aspect-[28/41] h-32 shadow-lg'>
<div className='me-6 aspect-[28/41] h-32 shadow-lg sm:me-10'>
<BookCover mode='list' book={book} />
</div>
<div className='title-author flex h-32 flex-col justify-between'>
@@ -62,7 +65,7 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
{formatAuthors(book.author, book.primaryLanguage) || _('Unknown')}
</p>
</div>
<div className='flex flex-wrap items-center gap-x-4'>
<div className='flex flex-nowrap items-center gap-3 sm:gap-x-4'>
{onEdit && (
<button
onClick={onEdit}
@@ -118,6 +121,11 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
<MdOutlineCloudUpload className='fill-base-content' />
</button>
)}
{book.downloadedAt && onExport && (
<button onClick={onExport} title={_('Export Book')}>
<MdSaveAlt className='fill-base-content' />
</button>
)}
</div>
</div>
</div>
+15
View File
@@ -79,6 +79,8 @@ export const EXTS: Record<BookFormat, string> = {
CBZ: 'cbz',
FB2: 'fb2',
FBZ: 'fbz',
TXT: 'txt',
MD: 'md',
};
export const MIMETYPES: Record<BookFormat, string[]> = {
@@ -90,6 +92,8 @@ export const MIMETYPES: Record<BookFormat, string[]> = {
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';
};
+23 -1
View File
@@ -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<void>;
abstract selectDirectory(mode: SelectDirectoryMode): Promise<string>;
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
abstract saveFile(
filename: string,
content: string | ArrayBuffer,
filepath: string,
mimeType?: string,
): Promise<boolean>;
protected async runMigrations(lastMigrationVersion: number): Promise<void> {
if (lastMigrationVersion < 20251124) {
@@ -661,6 +674,15 @@ export abstract class BaseAppService implements AppService {
}
}
async exportBook(book: Book): Promise<boolean> {
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<boolean> {
const fp = getLocalBookFilename(book);
if (await this.fs.exists(fp, 'Books')) {
@@ -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<boolean> {
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');
@@ -321,4 +321,22 @@ export class WebAppService extends BaseAppService {
async selectFiles(): Promise<string[]> {
throw new Error('selectFiles is not supported in browser');
}
async saveFile(filename: string, content: string | ArrayBuffer, mimeType?: string): Promise<boolean> {
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;
}
}
}
+11 -1
View File
@@ -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';
+2
View File
@@ -107,6 +107,7 @@ export interface AppService {
selectDirectory(mode: SelectDirectoryMode): Promise<string>;
selectFiles(name: string, extensions: string[]): Promise<string[]>;
readDirectory(path: string, base: BaseDir): Promise<FileItem[]>;
saveFile(filename: string, content: string | ArrayBuffer, mimeType?: string): Promise<boolean>;
getDefaultViewSettings(): ViewSettings;
loadSettings(): Promise<SystemSettings>;
@@ -132,6 +133,7 @@ export interface AppService {
onProgress?: ProgressHandler,
): Promise<void>;
downloadBookCovers(books: Book[], redownload?: boolean): Promise<void>;
exportBook(book: Book): Promise<boolean>;
isBookAvailable(book: Book): Promise<boolean>;
getBookFileSize(book: Book): Promise<number | null>;
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
+11 -1
View File
@@ -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