From dced42912ffb7de082996ef37a9977f3999c1148 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 26 Jun 2026 17:44:34 +0800 Subject: [PATCH] feat(reader): filter exported annotations by color and style (#4801) (#4806) 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) --- .../public/locales/ar/translation.json | 5 +- .../public/locales/bn/translation.json | 5 +- .../public/locales/bo/translation.json | 5 +- .../public/locales/de/translation.json | 5 +- .../public/locales/el/translation.json | 5 +- .../public/locales/es/translation.json | 5 +- .../public/locales/fa/translation.json | 5 +- .../public/locales/fr/translation.json | 5 +- .../public/locales/he/translation.json | 5 +- .../public/locales/hi/translation.json | 5 +- .../public/locales/hu/translation.json | 5 +- .../public/locales/id/translation.json | 5 +- .../public/locales/it/translation.json | 5 +- .../public/locales/ja/translation.json | 5 +- .../public/locales/ko/translation.json | 5 +- .../public/locales/ms/translation.json | 5 +- .../public/locales/nl/translation.json | 5 +- .../public/locales/pl/translation.json | 5 +- .../public/locales/pt-BR/translation.json | 5 +- .../public/locales/pt/translation.json | 5 +- .../public/locales/ro/translation.json | 5 +- .../public/locales/ru/translation.json | 5 +- .../public/locales/si/translation.json | 5 +- .../public/locales/sl/translation.json | 5 +- .../public/locales/sv/translation.json | 5 +- .../public/locales/ta/translation.json | 5 +- .../public/locales/th/translation.json | 5 +- .../public/locales/tr/translation.json | 5 +- .../public/locales/uk/translation.json | 5 +- .../public/locales/uz/translation.json | 5 +- .../public/locales/vi/translation.json | 5 +- .../public/locales/zh-CN/translation.json | 5 +- .../public/locales/zh-TW/translation.json | 5 +- .../app/reader/utils/annotatorUtil.test.ts | 105 +++++++++++++- .../reader/components/annotator/Annotator.tsx | 4 +- .../annotator/ExportMarkdownDialog.tsx | 131 +++++++++++++++++- .../src/app/reader/utils/annotatorUtil.ts | 69 ++++++++- apps/readest-app/src/services/constants.ts | 2 + apps/readest-app/src/types/book.ts | 5 + 39 files changed, 436 insertions(+), 45 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index af63869a..6cda2658 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1932,5 +1932,8 @@ "Date modified": "تاريخ التعديل", "Date created": "تاريخ الإنشاء", "Sort ascending": "ترتيب تصاعدي", - "Sort descending": "ترتيب تنازلي" + "Sort descending": "ترتيب تنازلي", + "Filter Annotations": "تصفية التعليقات التوضيحية", + "Colors": "الألوان", + "Styles": "الأنماط" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index f26a38da..3ab70a28 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1800,5 +1800,8 @@ "Date modified": "পরিবর্তনের তারিখ", "Date created": "তৈরির তারিখ", "Sort ascending": "ঊর্ধ্বক্রমে সাজান", - "Sort descending": "অধঃক্রমে সাজান" + "Sort descending": "অধঃক্রমে সাজান", + "Filter Annotations": "টীকা ফিল্টার করুন", + "Colors": "রঙ", + "Styles": "শৈলী" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 3e6fa4c9..5c96a8ac 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -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": "རྣམ་པ" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index dc180c80..f83c2d34 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 548a4ed5..b086e451 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1800,5 +1800,8 @@ "Date modified": "Ημερομηνία τροποποίησης", "Date created": "Ημερομηνία δημιουργίας", "Sort ascending": "Αύξουσα ταξινόμηση", - "Sort descending": "Φθίνουσα ταξινόμηση" + "Sort descending": "Φθίνουσα ταξινόμηση", + "Filter Annotations": "Φιλτράρισμα σχολιασμών", + "Colors": "Χρώματα", + "Styles": "Στυλ" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index ad84348f..81f0c6e5 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 192826b9..c5387cc2 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1800,5 +1800,8 @@ "Date modified": "تاریخ تغییر", "Date created": "تاریخ ایجاد", "Sort ascending": "مرتب‌سازی صعودی", - "Sort descending": "مرتب‌سازی نزولی" + "Sort descending": "مرتب‌سازی نزولی", + "Filter Annotations": "فیلتر یادداشت‌ها", + "Colors": "رنگ‌ها", + "Styles": "سبک‌ها" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index b21233da..6c035eb6 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index bcd3732b..0acdcfc8 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1833,5 +1833,8 @@ "Date modified": "תאריך שינוי", "Date created": "תאריך יצירה", "Sort ascending": "מיון עולה", - "Sort descending": "מיון יורד" + "Sort descending": "מיון יורד", + "Filter Annotations": "סינון הערות", + "Colors": "צבעים", + "Styles": "סגנונות" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index ab866d83..77545c39 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1800,5 +1800,8 @@ "Date modified": "संशोधन तिथि", "Date created": "निर्माण तिथि", "Sort ascending": "आरोही क्रम", - "Sort descending": "अवरोही क्रम" + "Sort descending": "अवरोही क्रम", + "Filter Annotations": "टिप्पणियाँ फ़िल्टर करें", + "Colors": "रंग", + "Styles": "शैलियाँ" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 5c435f17..770ac1a9 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index dd87e931..c66db0f3 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 65bdc6de..cdcd2ffe 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 9e140f62..07c99168 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1767,5 +1767,8 @@ "Date modified": "更新日", "Date created": "作成日", "Sort ascending": "昇順で並べ替え", - "Sort descending": "降順で並べ替え" + "Sort descending": "降順で並べ替え", + "Filter Annotations": "注釈をフィルター", + "Colors": "色", + "Styles": "スタイル" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index a0ebe4db..edbbc7bd 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1767,5 +1767,8 @@ "Date modified": "수정한 날짜", "Date created": "만든 날짜", "Sort ascending": "오름차순 정렬", - "Sort descending": "내림차순 정렬" + "Sort descending": "내림차순 정렬", + "Filter Annotations": "주석 필터링", + "Colors": "색상", + "Styles": "스타일" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 1334e5cd..28753d19 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index db2ef335..d5965afb 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 29949420..fec2c7db 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index e9782ca6..764b56df 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 8135800e..4eb4eecd 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index 7eac417e..8e9787d2 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 1720d6e9..34a6df17 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1866,5 +1866,8 @@ "Date modified": "Дата изменения", "Date created": "Дата создания", "Sort ascending": "По возрастанию", - "Sort descending": "По убыванию" + "Sort descending": "По убыванию", + "Filter Annotations": "Фильтровать аннотации", + "Colors": "Цвета", + "Styles": "Стили" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 9b69fe26..ff19bef1 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1800,5 +1800,8 @@ "Date modified": "වෙනස් කළ දිනය", "Date created": "සෑදූ දිනය", "Sort ascending": "ආරෝහණව සකසන්න", - "Sort descending": "අවරෝහණව සකසන්න" + "Sort descending": "අවරෝහණව සකසන්න", + "Filter Annotations": "සටහන් පෙරහන් කරන්න", + "Colors": "වර්ණ", + "Styles": "මෝස්තර" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 42ad8c0a..1322239e 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index a1fdf948..5ac61e7e 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 7d2bad1b..c3c7d2fd 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1800,5 +1800,8 @@ "Date modified": "மாற்றிய தேதி", "Date created": "உருவாக்கிய தேதி", "Sort ascending": "ஏறுவரிசை", - "Sort descending": "இறங்குவரிசை" + "Sort descending": "இறங்குவரிசை", + "Filter Annotations": "சிறுகுறிப்புகளை வடிகட்டு", + "Colors": "நிறங்கள்", + "Styles": "பாணிகள்" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 406d708a..ec41949f 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1767,5 +1767,8 @@ "Date modified": "วันที่แก้ไข", "Date created": "วันที่สร้าง", "Sort ascending": "เรียงจากน้อยไปมาก", - "Sort descending": "เรียงจากมากไปน้อย" + "Sort descending": "เรียงจากมากไปน้อย", + "Filter Annotations": "กรองคำอธิบายประกอบ", + "Colors": "สี", + "Styles": "สไตล์" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 2319e954..ea701f6f 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 9483214c..9f2f7eb8 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1866,5 +1866,8 @@ "Date modified": "Дата зміни", "Date created": "Дата створення", "Sort ascending": "За зростанням", - "Sort descending": "За спаданням" + "Sort descending": "За спаданням", + "Filter Annotations": "Фільтрувати анотації", + "Colors": "Кольори", + "Styles": "Стилі" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index fb7b5d83..cd9787ba 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 7a092e34..33d4cab8 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 1bc45108..856a958c 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1767,5 +1767,8 @@ "Date modified": "修改日期", "Date created": "创建日期", "Sort ascending": "升序排列", - "Sort descending": "降序排列" + "Sort descending": "降序排列", + "Filter Annotations": "筛选笔记", + "Colors": "颜色", + "Styles": "样式" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 7716636f..c04ad959 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1767,5 +1767,8 @@ "Date modified": "修改日期", "Date created": "建立日期", "Sort ascending": "升序排列", - "Sort descending": "降序排列" + "Sort descending": "降序排列", + "Filter Annotations": "篩選筆記", + "Colors": "顏色", + "Styles": "樣式" } diff --git a/apps/readest-app/src/__tests__/app/reader/utils/annotatorUtil.test.ts b/apps/readest-app/src/__tests__/app/reader/utils/annotatorUtil.test.ts index 16ea1197..5c5e831f 100644 --- a/apps/readest-app/src/__tests__/app/reader/utils/annotatorUtil.test.ts +++ b/apps/readest-app/src/__tests__/app/reader/utils/annotatorUtil.test.ts @@ -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 => ({ @@ -80,3 +81,105 @@ describe('mergeRestyledAnnotation', () => { expect(merged.updatedAt).toBe(200); }); }); + +describe('filterExportGroups', () => { + const group = (booknotes: BookNote[], over: Partial = {}): 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']); + }); +}); 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 b95cb807..42443681 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -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} diff --git a/apps/readest-app/src/app/reader/components/annotator/ExportMarkdownDialog.tsx b/apps/readest-app/src/app/reader/components/annotator/ExportMarkdownDialog.tsx index fb5869ab..eac2aef0 100644 --- a/apps/readest-app/src/app/reader/components/annotator/ExportMarkdownDialog.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/ExportMarkdownDialog.tsx @@ -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 = ({ 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 = ({ // 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 = ({ .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 = ({ 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 = ({ } 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 = ({ + {/* Filter by color / style */} + {(applyColorFilter || applyStyleFilter) && ( +
+

{_('Filter Annotations')}

+ + {applyColorFilter && ( +
+ {_('Colors')} +
+ {distinctColors.map((color) => { + const included = !exportConfig.excludedColors.includes(color); + const hex = getHighlightColorHex(settings, color) ?? color; + const label = resolveColorLabel(color); + return ( + + ); + })} +
+
+ )} + + {applyStyleFilter && ( +
+ {_('Styles')} +
+ {distinctStyles.map((style) => { + const included = !exportConfig.excludedStyles.includes(style); + return ( + + ); + })} +
+
+ )} +
+ )} + {/* Advanced Options */}
@@ -714,7 +831,7 @@ const ExportMarkdownDialog: React.FC = ({ diff --git a/apps/readest-app/src/app/reader/utils/annotatorUtil.ts b/apps/readest-app/src/app/reader/utils/annotatorUtil.ts index 6c6b73de..8b95bc5b 100644 --- a/apps/readest-app/src/app/reader/utils/annotatorUtil.ts +++ b/apps/readest-app/src/app/reader/utils/annotatorUtil.ts @@ -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(); + const stylesSeen = new Set(); + 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, diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index c850b310..45cb8072 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -401,6 +401,8 @@ export const DEFAULT_NOTE_EXPORT_CONFIG: NoteExportConfig = { useCustomTemplate: false, customTemplate: '', exportAsPlainText: false, + excludedColors: [], + excludedStyles: [], }; export const DEFAULT_ANNOTATOR_CONFIG: AnnotatorConfig = { diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index 99fd5d07..a41b3f72 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -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 {