feat: export annotations in markdown format, closes #152 (#689)

This commit is contained in:
Huang Xin
2025-03-24 02:33:05 +08:00
committed by GitHub
parent d098487cd1
commit ebc23c4ce3
22 changed files with 250 additions and 27 deletions
@@ -251,5 +251,12 @@
"Small": "صغير",
"Large": "كبير",
"Auto": "تلقائي",
"Language": "اللغة"
"Language": "اللغة",
"No annotations to export": "لا توجد تعليقات للتصدير",
"Author": "المؤلف",
"Exported from Readest": "تم التصدير من ريديست",
"Highlights & Annotations": "التمييزات والتعليقات",
"Note": "ملاحظة",
"Copied to clipboard": "تم النسخ إلى الحافظة",
"Export Annotations": "تصدير التعليقات"
}
@@ -251,5 +251,12 @@
"Small": "Klein",
"Large": "Groß",
"Auto": "Automatisch",
"Language": "Sprache"
"Language": "Sprache",
"No annotations to export": "Keine Anmerkungen zum Exportieren",
"Author": "Autor",
"Exported from Readest": "Exportiert von Readest",
"Highlights & Annotations": "Hervorhebungen & Anmerkungen",
"Note": "Notiz",
"Copied to clipboard": "In die Zwischenablage kopiert",
"Export Annotations": "Anmerkungen exportieren"
}
@@ -251,5 +251,12 @@
"Small": "Μικρό",
"Large": "Μεγάλο",
"Auto": "Αυτόματο",
"Language": "Γλώσσα"
"Language": "Γλώσσα",
"No annotations to export": "Δεν υπάρχουν σχολιασμοί για εξαγωγή",
"Author": "Συγγραφέας",
"Exported from Readest": "Εξήχθη από το Readest",
"Highlights & Annotations": "Επισημάνσεις & Σχολιασμοί",
"Note": "Σημείωση",
"Copied to clipboard": "Αντιγράφηκε στο πρόχειρο",
"Export Annotations": "Εξαγωγή σχολιασμών"
}
@@ -251,5 +251,12 @@
"Small": "Pequeño",
"Large": "Grande",
"Auto": "Automático",
"Language": "Idioma"
"Language": "Idioma",
"No annotations to export": "No hay anotaciones para exportar",
"Author": "Autor",
"Exported from Readest": "Exportado desde Readest",
"Highlights & Annotations": "Resaltados y anotaciones",
"Note": "Nota",
"Copied to clipboard": "Copiado al portapapeles",
"Export Annotations": "Exportar anotaciones"
}
@@ -251,5 +251,12 @@
"Small": "Petit",
"Large": "Grand",
"Auto": "Automatique",
"Language": "Langue"
"Language": "Langue",
"No annotations to export": "Aucune annotation à exporter",
"Author": "Auteur",
"Exported from Readest": "Exporté depuis Readest",
"Highlights & Annotations": "Surlignages et annotations",
"Note": "Note",
"Copied to clipboard": "Copié dans le presse-papiers",
"Export Annotations": "Exporter les annotations"
}
@@ -251,5 +251,12 @@
"Small": "छोटा",
"Large": "बड़ा",
"Auto": "स्वचालित",
"Language": "भाषा"
"Language": "भाषा",
"No annotations to export": "निर्यात करने के लिए कोई टिप्पणियाँ नहीं हैं",
"Author": "लेखक",
"Exported from Readest": "Readest से निर्यात किया गया",
"Highlights & Annotations": "हाइलाइट और टिप्पणियाँ",
"Note": "टिप्पणी",
"Copied to clipboard": "क्लिपबोर्ड पर कॉपी किया गया",
"Export Annotations": "टिप्पणियाँ निर्यात करें"
}
@@ -251,5 +251,12 @@
"Small": "Kecil",
"Large": "Besar",
"Auto": "Otomatis",
"Language": "Bahasa"
"Language": "Bahasa",
"No annotations to export": "Tidak ada anotasi yang diekspor",
"Author": "Penulis",
"Exported from Readest": "Diekspor dari Readest",
"Highlights & Annotations": "Sorotan & Anotasi",
"Note": "Catatan",
"Copied to clipboard": "Disalin ke papan klip",
"Export Annotations": "Ekspor Anotasi"
}
@@ -251,5 +251,12 @@
"Small": "Piccolo",
"Large": "Grande",
"Auto": "Automatico",
"Language": "Lingua"
"Language": "Lingua",
"No annotations to export": "Nessuna annotazione da esportare",
"Author": "Autore",
"Exported from Readest": "Esportato da Readest",
"Highlights & Annotations": "Evidenziazioni e annotazioni",
"Note": "Nota",
"Copied to clipboard": "Copiato negli appunti",
"Export Annotations": "Esporta annotazioni"
}
@@ -251,5 +251,12 @@
"Small": "小",
"Large": "大",
"Auto": "自動",
"Language": "言語"
"Language": "言語",
"No annotations to export": "エクスポートする注釈はありません",
"Author": "著者",
"Exported from Readest": "Readestからエクスポート",
"Highlights & Annotations": "ハイライトと注釈",
"Note": "メモ",
"Copied to clipboard": "クリップボードにコピー",
"Export Annotations": "注釈をエクスポート"
}
@@ -251,5 +251,12 @@
"Small": "작음",
"Large": "큼",
"Auto": "자동",
"Language": "언어"
"Language": "언어",
"No annotations to export": "내보낼 주석이 없습니다",
"Author": "저자",
"Exported from Readest": "Readest에서 내보냄",
"Highlights & Annotations": "하이라이트 및 주석",
"Note": "노트",
"Copied to clipboard": "클립보드에 복사",
"Export Annotations": "주석 내보내기"
}
@@ -251,5 +251,12 @@
"Small": "Mały",
"Large": "Duży",
"Auto": "Automatyczny",
"Language": "Język"
"Language": "Język",
"No annotations to export": "Brak adnotacji do eksportu",
"Author": "Autor",
"Exported from Readest": "Eksportowane z Readest",
"Highlights & Annotations": "Zaznaczenia i adnotacje",
"Note": "Notatka",
"Copied to clipboard": "Skopiowano do schowka",
"Export Annotations": "Eksportuj adnotacje"
}
@@ -251,5 +251,12 @@
"Small": "Pequeno",
"Large": "Grande",
"Auto": "Automático",
"Language": "Idioma"
"Language": "Idioma",
"No annotations to export": "Nenhuma anotação para exportar",
"Author": "Autor",
"Exported from Readest": "Exportado do Readest",
"Highlights & Annotations": "Destaques e Anotações",
"Note": "Nota",
"Copied to clipboard": "Copiado para a área de transferência",
"Export Annotations": "Exportar Anotações"
}
@@ -251,5 +251,12 @@
"Small": "Маленький",
"Large": "Большой",
"Auto": "Авто",
"Language": "Язык"
"Language": "Язык",
"No annotations to export": "Нет аннотаций для экспорта",
"Author": "Автор",
"Exported from Readest": "Экспортировано из Readest",
"Highlights & Annotations": "Выделения и аннотации",
"Note": "Заметка",
"Copied to clipboard": "Скопировано в буфер обмена",
"Export Annotations": "Экспортировать аннотации"
}
@@ -251,5 +251,12 @@
"Small": "Küçük",
"Large": "Büyük",
"Auto": "Otomatik",
"Language": "Dil"
"Language": "Dil",
"No annotations to export": "Dışa aktarılacak not yok",
"Author": "Yazar",
"Exported from Readest": "Readest'ten dışa aktarıldı",
"Highlights & Annotations": "Vurgular ve Notlar",
"Note": "Not",
"Copied to clipboard": "Panoya kopyalandı",
"Export Annotations": "Notları Dışa Aktar"
}
@@ -251,5 +251,12 @@
"Small": "Малий",
"Large": "Великий",
"Auto": "Автоматичний",
"Language": "Мова"
"Language": "Мова",
"No annotations to export": "Немає анотацій для експорту",
"Author": "Автор",
"Exported from Readest": "Експортовано з Readest",
"Highlights & Annotations": "Виділення та анотації",
"Note": "Примітка",
"Copied to clipboard": "Скопійовано в буфер обміну",
"Export Annotations": "Експортувати анотації"
}
@@ -251,5 +251,12 @@
"Small": "Nhỏ",
"Large": "Lớn",
"Auto": "Tự động",
"Language": "Ngôn ngữ"
"Language": "Ngôn ngữ",
"No annotations to export": "Không có chú thích để xuất",
"Author": "Tác giả",
"Exported from Readest": "Đã xuất từ Readest",
"Highlights & Annotations": "Điểm nổi bật & Chú thích",
"Note": "Ghi chú",
"Copied to clipboard": "Đã sao chép vào bảng ghi",
"Export Annotations": "Xuất chú thích"
}
@@ -251,5 +251,12 @@
"Small": "小",
"Large": "大",
"Auto": "自动",
"Language": "语言"
"Language": "语言",
"No annotations to export": "没有要导出的笔记",
"Author": "作者",
"Exported from Readest": "从 Readest 导出",
"Highlights & Annotations": "划线和笔记",
"Note": "笔记",
"Copied to clipboard": "已复制到剪贴板",
"Export Annotations": "导出笔记"
}
@@ -251,5 +251,12 @@
"Small": "小",
"Large": "大",
"Auto": "自動",
"Language": "語言"
"Language": "語言",
"No annotations to export": "沒有要匯出的筆記",
"Author": "作者",
"Exported from Readest": "從 Readest 匯出",
"Highlights & Annotations": "高亮和筆記",
"Note": "筆記",
"Copied to clipboard": "已複製到剪貼板",
"Export Annotations": "匯出筆記"
}
@@ -12,7 +12,7 @@ import { FaHeadphones } from 'react-icons/fa6';
import * as CFI from 'foliate-js/epubcfi.js';
import { Overlayer } from 'foliate-js/overlayer.js';
import { useEnv } from '@/context/EnvContext';
import { BookNote, HighlightColor, HighlightStyle } from '@/types/book';
import { BookNote, BooknoteGroup, HighlightColor, HighlightStyle } from '@/types/book';
import { getOSPlatform, uniqueId } from '@/utils/misc';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSettingsStore } from '@/store/settingsStore';
@@ -24,6 +24,7 @@ import { useFoliateEvents } from '../../hooks/useFoliateEvents';
import { useNotesSync } from '../../hooks/useNotesSync';
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { findTocItemBS } from '@/utils/toc';
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
import AnnotationPopup from './AnnotationPopup';
import WiktionaryPopup from './WiktionaryPopup';
@@ -240,8 +241,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
};
eventDispatcher.onSync('iframe-single-click', handleSingleClick);
eventDispatcher.on('export-annotations', handleExportMarkdown);
return () => {
eventDispatcher.offSync('iframe-single-click', handleSingleClick);
eventDispatcher.off('export-annotations', handleExportMarkdown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -438,6 +441,90 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
eventDispatcher.dispatch('tts-speak', { bookKey, range: selection.range });
};
const handleExportMarkdown = (event: CustomEvent) => {
const { bookKey: exportBookKey } = event.detail;
if (bookKey !== exportBookKey) return;
const { bookDoc, book } = bookData;
if (!bookDoc || !book || !bookDoc.toc) return;
const { booknotes: allNotes = [] } = config;
const booknotes = allNotes.filter((note) => !note.deletedAt);
if (booknotes.length === 0) {
eventDispatcher.dispatch('toast', {
type: 'info',
message: _('No annotations to export'),
className: 'whitespace-nowrap',
timeout: 2000,
});
return;
}
const booknoteGroups: { [href: string]: BooknoteGroup } = {};
for (const booknote of booknotes) {
const tocItem = findTocItemBS(bookDoc.toc ?? [], booknote.cfi);
const href = tocItem?.href || '';
const label = tocItem?.label || '';
const id = tocItem?.id || 0;
if (!booknoteGroups[href]) {
booknoteGroups[href] = { id, href, label, booknotes: [] };
}
booknoteGroups[href].booknotes.push(booknote);
}
Object.values(booknoteGroups).forEach((group) => {
group.booknotes.sort((a, b) => {
return CFI.compare(a.cfi, b.cfi);
});
});
const sortedGroups = Object.values(booknoteGroups).sort((a, b) => {
return a.id - b.id;
});
const lines: string[] = [];
lines.push(`# ${book.title}`);
lines.push(`**${_('Author')}**: ${book.author || ''}`);
lines.push('');
lines.push(`**${_('Exported from Readest')}**: ${new Date().toISOString().slice(0, 10)}`);
lines.push('');
lines.push('---');
lines.push('');
lines.push(`## ${_('Highlights & Annotations')}`);
lines.push('');
for (const group of sortedGroups) {
const chapterTitle = group.label || _('Untitled');
lines.push(`### ${chapterTitle}`);
for (const note of group.booknotes) {
lines.push(`> "${note.text}"`);
if (note.note) {
lines.push(`**${_('Note')}**:: ${note.note}`);
}
lines.push('');
}
lines.push('---');
lines.push('');
}
const markdownContent = lines.join('\n');
navigator.clipboard?.writeText(markdownContent);
eventDispatcher.dispatch('toast', {
type: 'info',
message: _('Copied to clipboard'),
className: 'whitespace-nowrap',
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;
const buttons = [
{ tooltipText: _('Copy'), Icon: FiCopy, onClick: handleCopy },
@@ -5,8 +5,10 @@ import Image from 'next/image';
import MenuItem from '@/components/MenuItem';
import { setAboutDialogVisible } from '@/components/AboutWindow';
import { useLibraryStore } from '@/store/libraryStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { isWebAppPlatform } from '@/services/environment';
import { eventDispatcher } from '@/utils/event';
import { DOWNLOAD_READEST_URL } from '@/services/constants';
import useBooksManager from '../../hooks/useBooksManager';
@@ -19,6 +21,8 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
const _ = useTranslation();
const { getVisibleLibrary } = useLibraryStore();
const { openParallelView } = useBooksManager();
const { sideBarBookKey } = useSidebarStore();
const handleParallelView = (id: string) => {
openParallelView(id);
setIsDropdownOpen?.(false);
@@ -35,6 +39,10 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
window.open(DOWNLOAD_READEST_URL, '_blank');
setIsDropdownOpen?.(false);
};
const handleExportAnnotations = () => {
eventDispatcher.dispatch('export-annotations', { bookKey: sideBarBookKey });
setIsDropdownOpen?.(false);
};
const isWebApp = isWebAppPlatform();
@@ -49,6 +57,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
<MenuItem label={_('Parallel Read')} noIcon>
<ul className='max-h-60 overflow-y-auto'>
{getVisibleLibrary()
.filter((book) => book.format !== 'PDF' && book.format !== 'CBZ')
.slice(0, 20)
.map((book) => (
<MenuItem
@@ -72,6 +81,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
))}
</ul>
</MenuItem>
<MenuItem label={_('Export Annotations')} noIcon onClick={handleExportAnnotations} />
<MenuItem label={_('Reload Page')} noIcon shortcut='Shift+R' onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} noIcon onClick={downloadReadest} />}
@@ -4,16 +4,9 @@ import * as CFI from 'foliate-js/epubcfi.js';
import { useBookDataStore } from '@/store/bookDataStore';
import { findTocItemBS } from '@/utils/toc';
import { TOCItem } from '@/libs/document';
import { BookNote, BookNoteType } from '@/types/book';
import { BooknoteGroup, BookNoteType } from '@/types/book';
import BooknoteItem from './BooknoteItem';
interface BooknoteGroup {
id: number;
href: string;
label: string;
booknotes: BookNote[];
}
const BooknoteView: React.FC<{
type: BookNoteType;
bookKey: string;
+7
View File
@@ -53,6 +53,13 @@ export interface BookNote {
deletedAt?: number | null;
}
export interface BooknoteGroup {
id: number;
href: string;
label: string;
booknotes: BookNote[];
}
export type WritingMode = 'auto' | 'horizontal-tb' | 'horizontal-rl' | 'vertical-rl';
export interface BookLayout {