Add a Filter section to the annotation export dialog so users who color-code highlights (e.g. red for important, yellow for difficult words) can export only selected colors and styles. The selection is stored as exclusions in NoteExportConfig, so an empty filter exports everything and any color or style added later is included by default. A new pure helper filterExportGroups applies the filter to both the default formatter and the custom-template paths, and only filters a dimension when at least two distinct values are present so a hidden row never silently drops notes. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1932,5 +1932,8 @@
|
||||
"Date modified": "تاريخ التعديل",
|
||||
"Date created": "تاريخ الإنشاء",
|
||||
"Sort ascending": "ترتيب تصاعدي",
|
||||
"Sort descending": "ترتيب تنازلي"
|
||||
"Sort descending": "ترتيب تنازلي",
|
||||
"Filter Annotations": "تصفية التعليقات التوضيحية",
|
||||
"Colors": "الألوان",
|
||||
"Styles": "الأنماط"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "পরিবর্তনের তারিখ",
|
||||
"Date created": "তৈরির তারিখ",
|
||||
"Sort ascending": "ঊর্ধ্বক্রমে সাজান",
|
||||
"Sort descending": "অধঃক্রমে সাজান"
|
||||
"Sort descending": "অধঃক্রমে সাজান",
|
||||
"Filter Annotations": "টীকা ফিল্টার করুন",
|
||||
"Colors": "রঙ",
|
||||
"Styles": "শৈলী"
|
||||
}
|
||||
|
||||
@@ -1761,5 +1761,8 @@
|
||||
"Background Image (Reader)": "གནས་སྟངས་ཀྱི་རི་མོ། (ཀློག་ངོས།)",
|
||||
"updated metadata for {{n}} book(s)": "དཔེ་ཆ་{{n}}་ཡི་གནད་སྨིན་གོ་དོན་གསར་བསྒྱུར་བྱས།",
|
||||
"{{n}} metadata": "{{n}} གནད་སྨིན་གོ་དོན།",
|
||||
"Metadata updated": "གནད་སྨིན་གོ་དོན་གསར་བསྒྱུར་བྱས།"
|
||||
"Metadata updated": "གནད་སྨིན་གོ་དོན་གསར་བསྒྱུར་བྱས།",
|
||||
"Filter Annotations": "མཆན་འདེམས་སྒྲིག",
|
||||
"Colors": "ཁ་དོག",
|
||||
"Styles": "རྣམ་པ"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Änderungsdatum",
|
||||
"Date created": "Erstellungsdatum",
|
||||
"Sort ascending": "Aufsteigend sortieren",
|
||||
"Sort descending": "Absteigend sortieren"
|
||||
"Sort descending": "Absteigend sortieren",
|
||||
"Filter Annotations": "Anmerkungen filtern",
|
||||
"Colors": "Farben",
|
||||
"Styles": "Stile"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Ημερομηνία τροποποίησης",
|
||||
"Date created": "Ημερομηνία δημιουργίας",
|
||||
"Sort ascending": "Αύξουσα ταξινόμηση",
|
||||
"Sort descending": "Φθίνουσα ταξινόμηση"
|
||||
"Sort descending": "Φθίνουσα ταξινόμηση",
|
||||
"Filter Annotations": "Φιλτράρισμα σχολιασμών",
|
||||
"Colors": "Χρώματα",
|
||||
"Styles": "Στυλ"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "Fecha de modificación",
|
||||
"Date created": "Fecha de creación",
|
||||
"Sort ascending": "Orden ascendente",
|
||||
"Sort descending": "Orden descendente"
|
||||
"Sort descending": "Orden descendente",
|
||||
"Filter Annotations": "Filtrar anotaciones",
|
||||
"Colors": "Colores",
|
||||
"Styles": "Estilos"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "تاریخ تغییر",
|
||||
"Date created": "تاریخ ایجاد",
|
||||
"Sort ascending": "مرتبسازی صعودی",
|
||||
"Sort descending": "مرتبسازی نزولی"
|
||||
"Sort descending": "مرتبسازی نزولی",
|
||||
"Filter Annotations": "فیلتر یادداشتها",
|
||||
"Colors": "رنگها",
|
||||
"Styles": "سبکها"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "Date de modification",
|
||||
"Date created": "Date de création",
|
||||
"Sort ascending": "Tri croissant",
|
||||
"Sort descending": "Tri décroissant"
|
||||
"Sort descending": "Tri décroissant",
|
||||
"Filter Annotations": "Filtrer les annotations",
|
||||
"Colors": "Couleurs",
|
||||
"Styles": "Styles"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "תאריך שינוי",
|
||||
"Date created": "תאריך יצירה",
|
||||
"Sort ascending": "מיון עולה",
|
||||
"Sort descending": "מיון יורד"
|
||||
"Sort descending": "מיון יורד",
|
||||
"Filter Annotations": "סינון הערות",
|
||||
"Colors": "צבעים",
|
||||
"Styles": "סגנונות"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "संशोधन तिथि",
|
||||
"Date created": "निर्माण तिथि",
|
||||
"Sort ascending": "आरोही क्रम",
|
||||
"Sort descending": "अवरोही क्रम"
|
||||
"Sort descending": "अवरोही क्रम",
|
||||
"Filter Annotations": "टिप्पणियाँ फ़िल्टर करें",
|
||||
"Colors": "रंग",
|
||||
"Styles": "शैलियाँ"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Módosítás dátuma",
|
||||
"Date created": "Létrehozás dátuma",
|
||||
"Sort ascending": "Növekvő sorrend",
|
||||
"Sort descending": "Csökkenő sorrend"
|
||||
"Sort descending": "Csökkenő sorrend",
|
||||
"Filter Annotations": "Jegyzetek szűrése",
|
||||
"Colors": "Színek",
|
||||
"Styles": "Stílusok"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "Tanggal diubah",
|
||||
"Date created": "Tanggal dibuat",
|
||||
"Sort ascending": "Urutkan menaik",
|
||||
"Sort descending": "Urutkan menurun"
|
||||
"Sort descending": "Urutkan menurun",
|
||||
"Filter Annotations": "Filter Anotasi",
|
||||
"Colors": "Warna",
|
||||
"Styles": "Gaya"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "Data di modifica",
|
||||
"Date created": "Data di creazione",
|
||||
"Sort ascending": "Ordine crescente",
|
||||
"Sort descending": "Ordine decrescente"
|
||||
"Sort descending": "Ordine decrescente",
|
||||
"Filter Annotations": "Filtra annotazioni",
|
||||
"Colors": "Colori",
|
||||
"Styles": "Stili"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "更新日",
|
||||
"Date created": "作成日",
|
||||
"Sort ascending": "昇順で並べ替え",
|
||||
"Sort descending": "降順で並べ替え"
|
||||
"Sort descending": "降順で並べ替え",
|
||||
"Filter Annotations": "注釈をフィルター",
|
||||
"Colors": "色",
|
||||
"Styles": "スタイル"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "수정한 날짜",
|
||||
"Date created": "만든 날짜",
|
||||
"Sort ascending": "오름차순 정렬",
|
||||
"Sort descending": "내림차순 정렬"
|
||||
"Sort descending": "내림차순 정렬",
|
||||
"Filter Annotations": "주석 필터링",
|
||||
"Colors": "색상",
|
||||
"Styles": "스타일"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "Tarikh diubah suai",
|
||||
"Date created": "Tarikh dicipta",
|
||||
"Sort ascending": "Isih menaik",
|
||||
"Sort descending": "Isih menurun"
|
||||
"Sort descending": "Isih menurun",
|
||||
"Filter Annotations": "Tapis Anotasi",
|
||||
"Colors": "Warna",
|
||||
"Styles": "Gaya"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Wijzigingsdatum",
|
||||
"Date created": "Aanmaakdatum",
|
||||
"Sort ascending": "Oplopend sorteren",
|
||||
"Sort descending": "Aflopend sorteren"
|
||||
"Sort descending": "Aflopend sorteren",
|
||||
"Filter Annotations": "Aantekeningen filteren",
|
||||
"Colors": "Kleuren",
|
||||
"Styles": "Stijlen"
|
||||
}
|
||||
|
||||
@@ -1866,5 +1866,8 @@
|
||||
"Date modified": "Data modyfikacji",
|
||||
"Date created": "Data utworzenia",
|
||||
"Sort ascending": "Sortuj rosnąco",
|
||||
"Sort descending": "Sortuj malejąco"
|
||||
"Sort descending": "Sortuj malejąco",
|
||||
"Filter Annotations": "Filtruj adnotacje",
|
||||
"Colors": "Kolory",
|
||||
"Styles": "Style"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "Data de modificação",
|
||||
"Date created": "Data de criação",
|
||||
"Sort ascending": "Ordem crescente",
|
||||
"Sort descending": "Ordem decrescente"
|
||||
"Sort descending": "Ordem decrescente",
|
||||
"Filter Annotations": "Filtrar anotações",
|
||||
"Colors": "Cores",
|
||||
"Styles": "Estilos"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "Data de modificação",
|
||||
"Date created": "Data de criação",
|
||||
"Sort ascending": "Ordem crescente",
|
||||
"Sort descending": "Ordem decrescente"
|
||||
"Sort descending": "Ordem decrescente",
|
||||
"Filter Annotations": "Filtrar anotações",
|
||||
"Colors": "Cores",
|
||||
"Styles": "Estilos"
|
||||
}
|
||||
|
||||
@@ -1833,5 +1833,8 @@
|
||||
"Date modified": "Data modificării",
|
||||
"Date created": "Data creării",
|
||||
"Sort ascending": "Sortare crescătoare",
|
||||
"Sort descending": "Sortare descrescătoare"
|
||||
"Sort descending": "Sortare descrescătoare",
|
||||
"Filter Annotations": "Filtrează adnotările",
|
||||
"Colors": "Culori",
|
||||
"Styles": "Stiluri"
|
||||
}
|
||||
|
||||
@@ -1866,5 +1866,8 @@
|
||||
"Date modified": "Дата изменения",
|
||||
"Date created": "Дата создания",
|
||||
"Sort ascending": "По возрастанию",
|
||||
"Sort descending": "По убыванию"
|
||||
"Sort descending": "По убыванию",
|
||||
"Filter Annotations": "Фильтровать аннотации",
|
||||
"Colors": "Цвета",
|
||||
"Styles": "Стили"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "වෙනස් කළ දිනය",
|
||||
"Date created": "සෑදූ දිනය",
|
||||
"Sort ascending": "ආරෝහණව සකසන්න",
|
||||
"Sort descending": "අවරෝහණව සකසන්න"
|
||||
"Sort descending": "අවරෝහණව සකසන්න",
|
||||
"Filter Annotations": "සටහන් පෙරහන් කරන්න",
|
||||
"Colors": "වර්ණ",
|
||||
"Styles": "මෝස්තර"
|
||||
}
|
||||
|
||||
@@ -1866,5 +1866,8 @@
|
||||
"Date modified": "Datum spremembe",
|
||||
"Date created": "Datum ustvarjanja",
|
||||
"Sort ascending": "Naraščajoče razvrščanje",
|
||||
"Sort descending": "Padajoče razvrščanje"
|
||||
"Sort descending": "Padajoče razvrščanje",
|
||||
"Filter Annotations": "Filtriraj opombe",
|
||||
"Colors": "Barve",
|
||||
"Styles": "Slogi"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Ändringsdatum",
|
||||
"Date created": "Skapandedatum",
|
||||
"Sort ascending": "Stigande sortering",
|
||||
"Sort descending": "Fallande sortering"
|
||||
"Sort descending": "Fallande sortering",
|
||||
"Filter Annotations": "Filtrera anteckningar",
|
||||
"Colors": "Färger",
|
||||
"Styles": "Stilar"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "மாற்றிய தேதி",
|
||||
"Date created": "உருவாக்கிய தேதி",
|
||||
"Sort ascending": "ஏறுவரிசை",
|
||||
"Sort descending": "இறங்குவரிசை"
|
||||
"Sort descending": "இறங்குவரிசை",
|
||||
"Filter Annotations": "சிறுகுறிப்புகளை வடிகட்டு",
|
||||
"Colors": "நிறங்கள்",
|
||||
"Styles": "பாணிகள்"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "วันที่แก้ไข",
|
||||
"Date created": "วันที่สร้าง",
|
||||
"Sort ascending": "เรียงจากน้อยไปมาก",
|
||||
"Sort descending": "เรียงจากมากไปน้อย"
|
||||
"Sort descending": "เรียงจากมากไปน้อย",
|
||||
"Filter Annotations": "กรองคำอธิบายประกอบ",
|
||||
"Colors": "สี",
|
||||
"Styles": "สไตล์"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Değiştirilme tarihi",
|
||||
"Date created": "Oluşturulma tarihi",
|
||||
"Sort ascending": "Artan sıralama",
|
||||
"Sort descending": "Azalan sıralama"
|
||||
"Sort descending": "Azalan sıralama",
|
||||
"Filter Annotations": "Açıklamaları Filtrele",
|
||||
"Colors": "Renkler",
|
||||
"Styles": "Stiller"
|
||||
}
|
||||
|
||||
@@ -1866,5 +1866,8 @@
|
||||
"Date modified": "Дата зміни",
|
||||
"Date created": "Дата створення",
|
||||
"Sort ascending": "За зростанням",
|
||||
"Sort descending": "За спаданням"
|
||||
"Sort descending": "За спаданням",
|
||||
"Filter Annotations": "Фільтрувати анотації",
|
||||
"Colors": "Кольори",
|
||||
"Styles": "Стилі"
|
||||
}
|
||||
|
||||
@@ -1800,5 +1800,8 @@
|
||||
"Date modified": "Oʻzgartirilgan sana",
|
||||
"Date created": "Yaratilgan sana",
|
||||
"Sort ascending": "Oʻsish boʻyicha",
|
||||
"Sort descending": "Kamayish boʻyicha"
|
||||
"Sort descending": "Kamayish boʻyicha",
|
||||
"Filter Annotations": "Izohlarni filtrlash",
|
||||
"Colors": "Ranglar",
|
||||
"Styles": "Uslublar"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "Ngày sửa đổi",
|
||||
"Date created": "Ngày tạo",
|
||||
"Sort ascending": "Sắp xếp tăng dần",
|
||||
"Sort descending": "Sắp xếp giảm dần"
|
||||
"Sort descending": "Sắp xếp giảm dần",
|
||||
"Filter Annotations": "Lọc chú thích",
|
||||
"Colors": "Màu sắc",
|
||||
"Styles": "Kiểu"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "修改日期",
|
||||
"Date created": "创建日期",
|
||||
"Sort ascending": "升序排列",
|
||||
"Sort descending": "降序排列"
|
||||
"Sort descending": "降序排列",
|
||||
"Filter Annotations": "筛选笔记",
|
||||
"Colors": "颜色",
|
||||
"Styles": "样式"
|
||||
}
|
||||
|
||||
@@ -1767,5 +1767,8 @@
|
||||
"Date modified": "修改日期",
|
||||
"Date created": "建立日期",
|
||||
"Sort ascending": "升序排列",
|
||||
"Sort descending": "降序排列"
|
||||
"Sort descending": "降序排列",
|
||||
"Filter Annotations": "篩選筆記",
|
||||
"Colors": "顏色",
|
||||
"Styles": "樣式"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
decideAnnotationDraw,
|
||||
filterExportGroups,
|
||||
findAnnotationAtCfi,
|
||||
mergeRestyledAnnotation,
|
||||
} from '@/app/reader/utils/annotatorUtil';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { BookNote, BooknoteGroup } from '@/types/book';
|
||||
import { NOTE_PREFIX } from '@/types/view';
|
||||
|
||||
const makeNote = (over: Partial<BookNote>): BookNote => ({
|
||||
@@ -80,3 +81,105 @@ describe('mergeRestyledAnnotation', () => {
|
||||
expect(merged.updatedAt).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterExportGroups', () => {
|
||||
const group = (booknotes: BookNote[], over: Partial<BooknoteGroup> = {}): BooknoteGroup => ({
|
||||
id: 0,
|
||||
href: 'h',
|
||||
label: 'Chapter',
|
||||
booknotes,
|
||||
...over,
|
||||
});
|
||||
|
||||
it('keeps everything when nothing is excluded', () => {
|
||||
const groups = [group([makeNote({ color: 'yellow' }), makeNote({ color: 'red' })])];
|
||||
const result = filterExportGroups(groups, { excludedColors: [], excludedStyles: [] });
|
||||
expect(result.groups[0]!.booknotes).toHaveLength(2);
|
||||
expect(result.applyColorFilter).toBe(true);
|
||||
expect(result.distinctColors).toEqual(['red', 'yellow']);
|
||||
});
|
||||
|
||||
it('excludes a color and keeps the others', () => {
|
||||
const groups = [
|
||||
group([makeNote({ id: 'a', color: 'yellow' }), makeNote({ id: 'b', color: 'red' })]),
|
||||
];
|
||||
const result = filterExportGroups(groups, { excludedColors: ['red'], excludedStyles: [] });
|
||||
expect(result.groups[0]!.booknotes.map((n) => n.id)).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('excludes a style and keeps the others', () => {
|
||||
const groups = [
|
||||
group([
|
||||
makeNote({ id: 'a', color: 'yellow', style: 'highlight' }),
|
||||
makeNote({ id: 'b', color: 'yellow', style: 'underline' }),
|
||||
]),
|
||||
];
|
||||
const result = filterExportGroups(groups, {
|
||||
excludedColors: [],
|
||||
excludedStyles: ['underline'],
|
||||
});
|
||||
expect(result.groups[0]!.booknotes.map((n) => n.id)).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('combines color and style filters with AND', () => {
|
||||
const groups = [
|
||||
group([
|
||||
makeNote({ id: 'a', color: 'yellow', style: 'highlight' }),
|
||||
makeNote({ id: 'b', color: 'red', style: 'highlight' }),
|
||||
makeNote({ id: 'c', color: 'yellow', style: 'underline' }),
|
||||
]),
|
||||
];
|
||||
const result = filterExportGroups(groups, {
|
||||
excludedColors: ['red'],
|
||||
excludedStyles: ['underline'],
|
||||
});
|
||||
expect(result.groups[0]!.booknotes.map((n) => n.id)).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('drops groups that become empty', () => {
|
||||
const groups = [
|
||||
group([makeNote({ id: 'a', color: 'red' })], { href: 'h1' }),
|
||||
group([makeNote({ id: 'b', color: 'yellow' })], { href: 'h2' }),
|
||||
];
|
||||
const result = filterExportGroups(groups, { excludedColors: ['red'], excludedStyles: [] });
|
||||
expect(result.groups).toHaveLength(1);
|
||||
expect(result.groups[0]!.href).toBe('h2');
|
||||
});
|
||||
|
||||
it('always keeps notes without a color or style (e.g. bookmarks)', () => {
|
||||
const groups = [
|
||||
group([
|
||||
makeNote({ id: 'a', color: 'yellow' }),
|
||||
makeNote({ id: 'b', color: 'red' }),
|
||||
makeNote({ id: 'bm', type: 'bookmark', color: undefined, style: undefined }),
|
||||
]),
|
||||
];
|
||||
const result = filterExportGroups(groups, {
|
||||
excludedColors: ['red', 'yellow'],
|
||||
excludedStyles: [],
|
||||
});
|
||||
expect(result.groups[0]!.booknotes.map((n) => n.id)).toEqual(['bm']);
|
||||
});
|
||||
|
||||
it('does not apply the color filter when fewer than two colors are present', () => {
|
||||
const groups = [
|
||||
group([makeNote({ id: 'a', color: 'yellow' }), makeNote({ id: 'b', color: 'yellow' })]),
|
||||
];
|
||||
const result = filterExportGroups(groups, { excludedColors: ['yellow'], excludedStyles: [] });
|
||||
expect(result.applyColorFilter).toBe(false);
|
||||
expect(result.groups[0]!.booknotes).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('orders distinct colors by default palette then custom, and styles canonically', () => {
|
||||
const groups = [
|
||||
group([
|
||||
makeNote({ color: '#abcdef', style: 'squiggly' }),
|
||||
makeNote({ color: 'blue', style: 'highlight' }),
|
||||
makeNote({ color: 'red', style: 'underline' }),
|
||||
]),
|
||||
];
|
||||
const result = filterExportGroups(groups, { excludedColors: [], excludedStyles: [] });
|
||||
expect(result.distinctColors).toEqual(['red', 'blue', '#abcdef']);
|
||||
expect(result.distinctStyles).toEqual(['highlight', 'underline', 'squiggly']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,7 +164,6 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
// would otherwise tear down the dialog state immediately.
|
||||
const [clearAnnotationsCount, setClearAnnotationsCount] = useState(0);
|
||||
const [exportData, setExportData] = useState<{
|
||||
booknotes: BookNote[];
|
||||
booknoteGroups: { [href: string]: BooknoteGroup };
|
||||
} | null>(null);
|
||||
|
||||
@@ -1519,7 +1518,7 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
});
|
||||
});
|
||||
|
||||
setExportData({ booknotes, booknoteGroups });
|
||||
setExportData({ booknoteGroups });
|
||||
setShowExportDialog(true);
|
||||
};
|
||||
|
||||
@@ -1795,7 +1794,6 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
bookHash={bookData.book.hash}
|
||||
bookTitle={bookData.book.title}
|
||||
bookAuthor={bookData.book.author || ''}
|
||||
booknotes={exportData.booknotes}
|
||||
booknoteGroups={exportData.booknoteGroups}
|
||||
onCancel={handleCancelExport}
|
||||
onExport={handleConfirmExport}
|
||||
|
||||
@@ -4,9 +4,15 @@ import { marked } from 'marked';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { BookNote, BooknoteGroup, NoteExportConfig } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { BooknoteGroup, HighlightColor, HighlightStyle, NoteExportConfig } from '@/types/book';
|
||||
import { DEFAULT_NOTE_EXPORT_CONFIG } from '@/services/constants';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import {
|
||||
filterExportGroups,
|
||||
getHighlightColorHex,
|
||||
getHighlightColorLabel,
|
||||
} from '@/app/reader/utils/annotatorUtil';
|
||||
import { renderNoteTemplate, formatBlockQuote } from '@/utils/note';
|
||||
import {
|
||||
AnnotationLinkType,
|
||||
@@ -22,7 +28,6 @@ interface ExportMarkdownDialogProps {
|
||||
bookHash: string;
|
||||
bookTitle: string;
|
||||
bookAuthor: string;
|
||||
booknotes: BookNote[];
|
||||
booknoteGroups: { [href: string]: BooknoteGroup };
|
||||
onCancel: () => void;
|
||||
onExport: (
|
||||
@@ -38,13 +43,13 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
bookHash,
|
||||
bookTitle,
|
||||
bookAuthor,
|
||||
booknotes,
|
||||
booknoteGroups,
|
||||
onCancel,
|
||||
onExport,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
@@ -90,6 +95,10 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
// platform-aware default (app in the native app, web on the web).
|
||||
linkType: noteExportConfig.linkType ?? DEFAULT_NOTE_EXPORT_CONFIG.linkType,
|
||||
customTemplate: noteExportConfig.customTemplate || defaultTemplate,
|
||||
// Configs persisted before color/style filtering existed have no
|
||||
// exclusion arrays; default to exporting everything.
|
||||
excludedColors: noteExportConfig.excludedColors ?? [],
|
||||
excludedStyles: noteExportConfig.excludedStyles ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -119,13 +128,64 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Apply the color/style filter once; both the default formatter and the custom
|
||||
// template render the filtered groups, and the same metadata drives the filter UI.
|
||||
const {
|
||||
groups: filteredGroups,
|
||||
distinctColors,
|
||||
distinctStyles,
|
||||
applyColorFilter,
|
||||
applyStyleFilter,
|
||||
} = useMemo(
|
||||
() =>
|
||||
filterExportGroups(
|
||||
Object.values(booknoteGroups).sort((a, b) => a.id - b.id),
|
||||
{
|
||||
excludedColors: exportConfig.excludedColors,
|
||||
excludedStyles: exportConfig.excludedStyles,
|
||||
},
|
||||
),
|
||||
[booknoteGroups, exportConfig.excludedColors, exportConfig.excludedStyles],
|
||||
);
|
||||
|
||||
const filteredNotesCount = useMemo(
|
||||
() => filteredGroups.reduce((count, group) => count + group.booknotes.length, 0),
|
||||
[filteredGroups],
|
||||
);
|
||||
|
||||
const toggleExcludedColor = (color: HighlightColor) => {
|
||||
setExportConfig((prev) => ({
|
||||
...prev,
|
||||
excludedColors: prev.excludedColors.includes(color)
|
||||
? prev.excludedColors.filter((c) => c !== color)
|
||||
: [...prev.excludedColors, color],
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleExcludedStyle = (style: HighlightStyle) => {
|
||||
setExportConfig((prev) => ({
|
||||
...prev,
|
||||
excludedStyles: prev.excludedStyles.includes(style)
|
||||
? prev.excludedStyles.filter((s) => s !== style)
|
||||
: [...prev.excludedStyles, style],
|
||||
}));
|
||||
};
|
||||
|
||||
// Mirror HighlightOptions: user label, else translated default name, else the raw hex.
|
||||
const resolveColorLabel = (color: HighlightColor): string => {
|
||||
const userLabel = getHighlightColorLabel(settings, color);
|
||||
if (userLabel) return userLabel;
|
||||
if (!color.startsWith('#')) return _(color);
|
||||
return color;
|
||||
};
|
||||
|
||||
// Generate markdown preview based on current format settings
|
||||
const markdownPreview = useMemo(() => {
|
||||
let output = '';
|
||||
|
||||
if (exportConfig.useCustomTemplate) {
|
||||
// Prepare data for template rendering
|
||||
const sortedGroups = Object.values(booknoteGroups).sort((a, b) => a.id - b.id);
|
||||
const sortedGroups = filteredGroups;
|
||||
|
||||
const templateData = {
|
||||
title: bookTitle,
|
||||
@@ -157,7 +217,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
output = renderNoteTemplate(exportConfig.customTemplate, templateData);
|
||||
} else {
|
||||
// Default formatting (non-template mode)
|
||||
const sortedGroups = Object.values(booknoteGroups).sort((a, b) => a.id - b.id);
|
||||
const sortedGroups = filteredGroups;
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -247,7 +307,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [exportConfig, booknoteGroups, bookTitle, bookAuthor, bookHash, _]);
|
||||
}, [exportConfig, filteredGroups, bookTitle, bookAuthor, bookHash, _]);
|
||||
|
||||
// Convert markdown to HTML for preview
|
||||
const htmlPreview = useMemo(() => {
|
||||
@@ -415,6 +475,63 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter by color / style */}
|
||||
{(applyColorFilter || applyStyleFilter) && (
|
||||
<div className='space-y-3'>
|
||||
<h3 className='font-bold'>{_('Filter Annotations')}</h3>
|
||||
|
||||
{applyColorFilter && (
|
||||
<div className='space-y-2'>
|
||||
<span className='text-sm font-medium'>{_('Colors')}</span>
|
||||
<div className='flex flex-wrap gap-x-6 gap-y-2'>
|
||||
{distinctColors.map((color) => {
|
||||
const included = !exportConfig.excludedColors.includes(color);
|
||||
const hex = getHighlightColorHex(settings, color) ?? color;
|
||||
const label = resolveColorLabel(color);
|
||||
return (
|
||||
<label key={color} className='flex cursor-pointer items-center gap-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={included}
|
||||
onChange={() => toggleExcludedColor(color)}
|
||||
className='checkbox checkbox-sm'
|
||||
/>
|
||||
<span
|
||||
className='border-base-content/20 h-3 w-3 shrink-0 rounded-full border'
|
||||
style={{ backgroundColor: hex }}
|
||||
/>
|
||||
<span className='text-sm'>{label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{applyStyleFilter && (
|
||||
<div className='space-y-2'>
|
||||
<span className='text-sm font-medium'>{_('Styles')}</span>
|
||||
<div className='flex flex-wrap gap-x-6 gap-y-2'>
|
||||
{distinctStyles.map((style) => {
|
||||
const included = !exportConfig.excludedStyles.includes(style);
|
||||
return (
|
||||
<label key={style} className='flex cursor-pointer items-center gap-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={included}
|
||||
onChange={() => toggleExcludedStyle(style)}
|
||||
className='checkbox checkbox-sm'
|
||||
/>
|
||||
<span className='text-sm'>{_(style)}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Options */}
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
@@ -714,7 +831,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className='btn btn-primary btn-sm'
|
||||
disabled={booknotes.length === 0}
|
||||
disabled={filteredNotesCount === 0}
|
||||
>
|
||||
{_('Export')}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
|
||||
import { BookNote, DEFAULT_HIGHLIGHT_COLORS, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import {
|
||||
BookNote,
|
||||
BooknoteGroup,
|
||||
DEFAULT_HIGHLIGHT_COLORS,
|
||||
HighlightColor,
|
||||
HighlightStyle,
|
||||
} from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { FoliateView, NOTE_PREFIX } from '@/types/view';
|
||||
@@ -42,6 +48,67 @@ export const getHighlightColorLabel = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const ALL_HIGHLIGHT_STYLES: readonly HighlightStyle[] = ['highlight', 'underline', 'squiggly'];
|
||||
|
||||
export interface ExportFilter {
|
||||
excludedColors: HighlightColor[];
|
||||
excludedStyles: HighlightStyle[];
|
||||
}
|
||||
|
||||
export interface FilteredExportGroups {
|
||||
groups: BooknoteGroup[];
|
||||
distinctColors: HighlightColor[];
|
||||
distinctStyles: HighlightStyle[];
|
||||
applyColorFilter: boolean;
|
||||
applyStyleFilter: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter chapter groups for annotation export by highlight color and style (#4801).
|
||||
*
|
||||
* Exclusions (not inclusions) are stored so an empty filter exports everything and
|
||||
* colors/styles introduced later are included by default. A dimension is only
|
||||
* filtered when at least two distinct values are present, so a filter row the user
|
||||
* cannot see can never silently drop notes. Notes without a color/style (e.g.
|
||||
* bookmarks) always pass. Groups left empty by the filter are dropped.
|
||||
*
|
||||
* `distinctColors` is ordered by the default palette first, then custom colors in
|
||||
* first-seen order; `distinctStyles` follows the canonical highlight/underline/
|
||||
* squiggly order — both drive the filter UI.
|
||||
*/
|
||||
export function filterExportGroups(
|
||||
groups: BooknoteGroup[],
|
||||
{ excludedColors, excludedStyles }: ExportFilter,
|
||||
): FilteredExportGroups {
|
||||
const colorsSeen = new Set<HighlightColor>();
|
||||
const stylesSeen = new Set<HighlightStyle>();
|
||||
for (const group of groups) {
|
||||
for (const note of group.booknotes) {
|
||||
if (note.color) colorsSeen.add(note.color);
|
||||
if (note.style) stylesSeen.add(note.style);
|
||||
}
|
||||
}
|
||||
|
||||
const distinctColors = [
|
||||
...DEFAULT_HIGHLIGHT_COLORS.filter((color) => colorsSeen.has(color)),
|
||||
...[...colorsSeen].filter((color) => !isDefaultHighlightColor(color)),
|
||||
];
|
||||
const distinctStyles = ALL_HIGHLIGHT_STYLES.filter((style) => stylesSeen.has(style));
|
||||
|
||||
const applyColorFilter = distinctColors.length >= 2;
|
||||
const applyStyleFilter = distinctStyles.length >= 2;
|
||||
|
||||
const keep = (note: BookNote) =>
|
||||
(!applyColorFilter || !note.color || !excludedColors.includes(note.color)) &&
|
||||
(!applyStyleFilter || !note.style || !excludedStyles.includes(note.style));
|
||||
|
||||
const filtered = groups
|
||||
.map((group) => ({ ...group, booknotes: group.booknotes.filter(keep) }))
|
||||
.filter((group) => group.booknotes.length > 0);
|
||||
|
||||
return { groups: filtered, distinctColors, distinctStyles, applyColorFilter, applyStyleFilter };
|
||||
}
|
||||
|
||||
export function getExternalDragHandle(
|
||||
currentStart: Point,
|
||||
currentEnd: Point,
|
||||
|
||||
@@ -401,6 +401,8 @@ export const DEFAULT_NOTE_EXPORT_CONFIG: NoteExportConfig = {
|
||||
useCustomTemplate: false,
|
||||
customTemplate: '',
|
||||
exportAsPlainText: false,
|
||||
excludedColors: [],
|
||||
excludedStyles: [],
|
||||
};
|
||||
|
||||
export const DEFAULT_ANNOTATOR_CONFIG: AnnotatorConfig = {
|
||||
|
||||
@@ -340,6 +340,11 @@ export interface NoteExportConfig {
|
||||
useCustomTemplate: boolean;
|
||||
customTemplate: string;
|
||||
exportAsPlainText: boolean;
|
||||
// Highlight colors/styles to omit from the export. Empty arrays export
|
||||
// everything; storing exclusions keeps colors/styles added later included
|
||||
// by default (#4801).
|
||||
excludedColors: HighlightColor[];
|
||||
excludedStyles: HighlightStyle[];
|
||||
}
|
||||
|
||||
export interface AnnotatorConfig {
|
||||
|
||||
Reference in New Issue
Block a user