From 8a263235ed55cdbcf1c0c26fcc6d4d3be4005946 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 3 Jan 2026 14:57:10 +0100 Subject: [PATCH] feat(search): add annotations navigation bar, closes #2060 (#2849) --- .../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/hi/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/translation.json | 5 +- .../public/locales/ru/translation.json | 5 +- .../public/locales/si/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/vi/translation.json | 5 +- .../public/locales/zh-CN/translation.json | 7 +- .../public/locales/zh-TW/translation.json | 5 +- .../src/app/reader/components/BooksGrid.tsx | 2 + .../src/app/reader/components/HeaderBar.tsx | 2 +- .../reader/components/annotator/Annotator.tsx | 2 +- .../reader/components/footerbar/FooterBar.tsx | 2 +- .../components/sidebar/BooknoteItem.tsx | 4 +- .../components/sidebar/BooknoteView.tsx | 19 +- .../components/sidebar/BooknotesNav.tsx | 66 +++++++ .../components/sidebar/ContentNavBar.tsx | 133 +++++++++++++ .../reader/components/sidebar/SearchBar.tsx | 16 +- .../components/sidebar/SearchResultsNav.tsx | 97 ++------- .../app/reader/components/sidebar/SideBar.tsx | 15 +- .../src/app/reader/hooks/useBookShortcuts.ts | 2 +- .../src/app/reader/hooks/useBooknotesNav.ts | 185 ++++++++++++++++++ .../src/app/reader/hooks/useSearchNav.ts | 28 ++- apps/readest-app/src/store/sidebarStore.ts | 144 ++++++++++++-- apps/readest-app/src/utils/toc.ts | 10 +- 43 files changed, 701 insertions(+), 163 deletions(-) create mode 100644 apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx create mode 100644 apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx create mode 100644 apps/readest-app/src/app/reader/hooks/useBooknotesNav.ts diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 4e01a3bc..ad0046fd 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -869,5 +869,8 @@ "Search results for '{{term}}'": "نتائج البحث عن «{{term}}»", "Close Search": "إغلاق البحث", "Previous Result": "النتيجة السابقة", - "Next Result": "النتيجة التالية" + "Next Result": "النتيجة التالية", + "Bookmarks": "الإشارات المرجعية", + "Annotations": "التعليقات التوضيحية", + "Show Results": "عرض النتائج" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 4ca1d13a..12376737 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "'{{term}}' এর ফলাফল", "Close Search": "অনুসন্ধান বন্ধ করুন", "Previous Result": "আগের ফলাফল", - "Next Result": "পরবর্তী ফলাফল" + "Next Result": "পরবর্তী ফলাফল", + "Bookmarks": "বুকমার্ক", + "Annotations": "টীকা", + "Show Results": "ফলাফল দেখুন" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 79dd3dfb..a3a947f0 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "'{{term}}'ཡི་འབྲས་བུ།", "Close Search": "འཚོལ་བཤེར་སྒོ་རྒྱག", "Previous Result": "སྔོན་མའི་འབྲས་བུ།", - "Next Result": "རྗེས་མའི་འབྲས་བུ།" + "Next Result": "རྗེས་མའི་འབྲས་བུ།", + "Bookmarks": "དཔེ་རྟགས།", + "Annotations": "མཆན།", + "Show Results": "འབྲས་བུ་སྟོན།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index e8d55126..75d0ab8f 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "Ergebnisse für '{{term}}'", "Close Search": "Suche schließen", "Previous Result": "Vorheriges Ergebnis", - "Next Result": "Nächstes Ergebnis" + "Next Result": "Nächstes Ergebnis", + "Bookmarks": "Lesezeichen", + "Annotations": "Anmerkungen", + "Show Results": "Ergebnisse anzeigen" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index b1ca910e..3c88b8c6 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "Αποτελέσματα για '{{term}}'", "Close Search": "Κλείσιμο αναζήτησης", "Previous Result": "Προηγούμενο αποτέλεσμα", - "Next Result": "Επόμενο αποτέλεσμα" + "Next Result": "Επόμενο αποτέλεσμα", + "Bookmarks": "Σελιδοδείκτες", + "Annotations": "Σχολιασμοί", + "Show Results": "Εμφάνιση αποτελεσμάτων" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 05fb3458..783eed66 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -839,5 +839,8 @@ "Search results for '{{term}}'": "Resultados para '{{term}}'", "Close Search": "Cerrar búsqueda", "Previous Result": "Resultado anterior", - "Next Result": "Resultado siguiente" + "Next Result": "Resultado siguiente", + "Bookmarks": "Marcadores", + "Annotations": "Anotaciones", + "Show Results": "Mostrar resultados" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 3737202d..13a722b9 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "نتایج برای «{{term}}»", "Close Search": "بستن جستجو", "Previous Result": "نتیجه قبلی", - "Next Result": "نتیجه بعدی" + "Next Result": "نتیجه بعدی", + "Bookmarks": "نشانک‌ها", + "Annotations": "یادداشت‌ها", + "Show Results": "نمایش نتایج" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 422383c0..b558c9d6 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -839,5 +839,8 @@ "Search results for '{{term}}'": "Résultats pour « {{term}} »", "Close Search": "Fermer la recherche", "Previous Result": "Résultat précédent", - "Next Result": "Résultat suivant" + "Next Result": "Résultat suivant", + "Bookmarks": "Signets", + "Annotations": "Annotations", + "Show Results": "Afficher les résultats" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index bcb85a8b..d6ef1790 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "'{{term}}' के परिणाम", "Close Search": "खोज बंद करें", "Previous Result": "पिछला परिणाम", - "Next Result": "अगला परिणाम" + "Next Result": "अगला परिणाम", + "Bookmarks": "बुकमार्क", + "Annotations": "टिप्पणियाँ", + "Show Results": "परिणाम दिखाएं" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 59d70e7a..39bbf86d 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "Hasil untuk '{{term}}'", "Close Search": "Tutup pencarian", "Previous Result": "Hasil sebelumnya", - "Next Result": "Hasil berikutnya" + "Next Result": "Hasil berikutnya", + "Bookmarks": "Penanda", + "Annotations": "Anotasi", + "Show Results": "Tampilkan Hasil" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 6cc01f96..3b242080 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -839,5 +839,8 @@ "Search results for '{{term}}'": "Risultati per '{{term}}'", "Close Search": "Chiudi ricerca", "Previous Result": "Risultato precedente", - "Next Result": "Risultato successivo" + "Next Result": "Risultato successivo", + "Bookmarks": "Segnalibri", + "Annotations": "Annotazioni", + "Show Results": "Mostra risultati" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 22115c48..8ccbf337 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "「{{term}}」を含む結果", "Close Search": "検索を閉じる", "Previous Result": "前の結果", - "Next Result": "次の結果" + "Next Result": "次の結果", + "Bookmarks": "ブックマーク", + "Annotations": "注釈", + "Show Results": "結果を表示" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 8d2bcabc..564a3c00 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "'{{term}}' 검색 결과", "Close Search": "검색 닫기", "Previous Result": "이전 결과", - "Next Result": "다음 결과" + "Next Result": "다음 결과", + "Bookmarks": "북마크", + "Annotations": "주석", + "Show Results": "결과 표시" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 1155a7f8..1ceb6527 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "Hasil untuk '{{term}}'", "Close Search": "Tutup carian", "Previous Result": "Hasil sebelumnya", - "Next Result": "Hasil seterusnya" + "Next Result": "Hasil seterusnya", + "Bookmarks": "Penanda buku", + "Annotations": "Anotasi", + "Show Results": "Tunjukkan Hasil" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 871ab598..61d0e237 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "Resultaten voor '{{term}}'", "Close Search": "Zoekopdracht sluiten", "Previous Result": "Vorig resultaat", - "Next Result": "Volgend resultaat" + "Next Result": "Volgend resultaat", + "Bookmarks": "Bladwijzers", + "Annotations": "Aantekeningen", + "Show Results": "Resultaten tonen" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index ed1b1bd8..1d1c774a 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -849,5 +849,8 @@ "Search results for '{{term}}'": "Wyniki dla '{{term}}'", "Close Search": "Zamknij wyszukiwanie", "Previous Result": "Poprzedni wynik", - "Next Result": "Następny wynik" + "Next Result": "Następny wynik", + "Bookmarks": "Zakładki", + "Annotations": "Adnotacje", + "Show Results": "Pokaż wyniki" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index b9cfa997..5b63fb60 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -839,5 +839,8 @@ "Search results for '{{term}}'": "Resultados para '{{term}}'", "Close Search": "Fechar pesquisa", "Previous Result": "Resultado anterior", - "Next Result": "Próximo resultado" + "Next Result": "Próximo resultado", + "Bookmarks": "Marcadores", + "Annotations": "Anotações", + "Show Results": "Mostrar resultados" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index b3bbffcf..062e2f33 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -849,5 +849,8 @@ "Search results for '{{term}}'": "Результаты для «{{term}}»", "Close Search": "Закрыть поиск", "Previous Result": "Предыдущий результат", - "Next Result": "Следующий результат" + "Next Result": "Следующий результат", + "Bookmarks": "Закладки", + "Annotations": "Аннотации", + "Show Results": "Показать результаты" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 6405f41c..77c452a0 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "'{{term}}' සඳහා ප්‍රතිඵල", "Close Search": "සෙවුම වසන්න", "Previous Result": "පෙර ප්‍රතිඵලය", - "Next Result": "ඊළඟ ප්‍රතිඵලය" + "Next Result": "ඊළඟ ප්‍රතිඵලය", + "Bookmarks": "පොත් සලකුණු", + "Annotations": "අනුසටහන්", + "Show Results": "ප්‍රතිඵල පෙන්වන්න" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 9a9037be..71806e13 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "Resultat för '{{term}}'", "Close Search": "Stäng sökning", "Previous Result": "Föregående resultat", - "Next Result": "Nästa resultat" + "Next Result": "Nästa resultat", + "Bookmarks": "Bokmärken", + "Annotations": "Anteckningar", + "Show Results": "Visa resultat" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index d388c414..01b35325 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "'{{term}}' க்கான முடிவுகள்", "Close Search": "தேடலை மூடு", "Previous Result": "முந்தைய முடிவு", - "Next Result": "அடுத்த முடிவு" + "Next Result": "அடுத்த முடிவு", + "Bookmarks": "புக்மார்க்குகள்", + "Annotations": "சிறுகுறிப்புகள்", + "Show Results": "முடிவுகளைக் காட்டு" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 3d31781e..f54a1309 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "ผลลัพธ์สำหรับ '{{term}}'", "Close Search": "ปิดการค้นหา", "Previous Result": "ผลลัพธ์ก่อนหน้า", - "Next Result": "ผลลัพธ์ถัดไป" + "Next Result": "ผลลัพธ์ถัดไป", + "Bookmarks": "บุ๊กมาร์ก", + "Annotations": "คำอธิบายประกอบ", + "Show Results": "แสดงผลลัพธ์" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 567c2bb1..eea11a46 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -829,5 +829,8 @@ "Search results for '{{term}}'": "'{{term}}' için sonuçlar", "Close Search": "Aramayı kapat", "Previous Result": "Önceki sonuç", - "Next Result": "Sonraki sonuç" + "Next Result": "Sonraki sonuç", + "Bookmarks": "Yer İşaretleri", + "Annotations": "Açıklamalar", + "Show Results": "Sonuçları Göster" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 1b64d763..88744a8b 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -849,5 +849,8 @@ "Search results for '{{term}}'": "Результати для «{{term}}»", "Close Search": "Закрити пошук", "Previous Result": "Попередній результат", - "Next Result": "Наступний результат" + "Next Result": "Наступний результат", + "Bookmarks": "Закладки", + "Annotations": "Анотації", + "Show Results": "Показати результати" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 0e83776c..18ffce35 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "Kết quả cho '{{term}}'", "Close Search": "Đóng tìm kiếm", "Previous Result": "Kết quả trước", - "Next Result": "Kết quả tiếp theo" + "Next Result": "Kết quả tiếp theo", + "Bookmarks": "Dấu trang", + "Annotations": "Chú thích", + "Show Results": "Hiển thị kết quả" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index ad8721fa..adff23b7 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -816,8 +816,11 @@ "Please log in to continue": "请登录以继续", "Cloud File Transfers": "云文件传输", "Show Search Results": "显示搜索结果", - "Search results for '{{term}}'": "包含"{{term}}"的结果", + "Search results for '{{term}}'": "包含\"{{term}}\"的结果", "Close Search": "关闭搜索", "Previous Result": "上一个结果", - "Next Result": "下一个结果" + "Next Result": "下一个结果", + "Bookmarks": "书签", + "Annotations": "注释", + "Show Results": "显示结果" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 25a1b8d2..c077f4ab 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -819,5 +819,8 @@ "Search results for '{{term}}'": "包含「{{term}}」的結果", "Close Search": "關閉搜尋", "Previous Result": "上一個結果", - "Next Result": "下一個結果" + "Next Result": "下一個結果", + "Bookmarks": "書籤", + "Annotations": "註解", + "Show Results": "顯示結果" } diff --git a/apps/readest-app/src/app/reader/components/BooksGrid.tsx b/apps/readest-app/src/app/reader/components/BooksGrid.tsx index 2c8a4ac7..fe000a5b 100644 --- a/apps/readest-app/src/app/reader/components/BooksGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BooksGrid.tsx @@ -12,6 +12,7 @@ import { getGridTemplate, getInsetEdges } from '@/utils/grid'; import { getViewInsets } from '@/utils/insets'; import SettingsDialog from '@/components/settings/SettingsDialog'; import SearchResultsNav from './sidebar/SearchResultsNav'; +import BooknotesNav from './sidebar/BooknotesNav'; import FoliateViewer from './FoliateViewer'; import SectionInfo from './SectionInfo'; import HeaderBar from './HeaderBar'; @@ -200,6 +201,7 @@ const BooksGrid: React.FC = ({ bookKeys, onCloseBook }) => { )} + = ({ const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible; const docs = view?.renderer.getContents() ?? []; - const pointerInDoc = docs.some(({ doc }) => doc.body?.style.cursor === 'pointer'); + const pointerInDoc = docs.some(({ doc }) => doc?.body?.style.cursor === 'pointer'); const enableAnnotationQuickActions = viewSettings?.enableAnnotationQuickActions; const annotationQuickActionButton = 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 1af25cab..3d6a9c33 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -629,7 +629,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { if (convertChineseVariant && convertChineseVariant !== 'none') { term = runSimpleCC(term, convertChineseVariant, true); } - eventDispatcher.dispatch('search', { term }); + eventDispatcher.dispatch('search-term', { term, bookKey }); }; const handleDictionary = () => { diff --git a/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx b/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx index 31520d34..84d5b5ee 100644 --- a/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/footerbar/FooterBar.tsx @@ -42,7 +42,7 @@ const FooterBar: React.FC = ({ const isVisible = hoveredBookKey === bookKey; const docs = view?.renderer.getContents() ?? []; - const pointerInDoc = docs.some(({ doc }) => doc.body.style.cursor === 'pointer'); + const pointerInDoc = docs.some(({ doc }) => doc?.body?.style.cursor === 'pointer'); const progressInfo = useMemo( () => (bookFormat === 'PDF' ? section : pageinfo), diff --git a/apps/readest-app/src/app/reader/components/sidebar/BooknoteItem.tsx b/apps/readest-app/src/app/reader/components/sidebar/BooknoteItem.tsx index 838ce066..80d0a5a2 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BooknoteItem.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BooknoteItem.tsx @@ -20,9 +20,10 @@ import TextEditor, { TextEditorRef } from '@/components/TextEditor'; interface BooknoteItemProps { bookKey: string; item: BookNote; + onClick?: () => void; } -const BooknoteItem: React.FC = ({ bookKey, item }) => { +const BooknoteItem: React.FC = ({ bookKey, item, onClick }) => { const _ = useTranslation(); const { envConfig } = useEnv(); const { settings } = useSettingsStore(); @@ -43,6 +44,7 @@ const BooknoteItem: React.FC = ({ bookKey, item }) => { event.preventDefault(); eventDispatcher.dispatch('navigate', { bookKey, cfi }); + onClick?.(); getView(bookKey)?.goTo(cfi); if (note) { setNotebookVisible(true); diff --git a/apps/readest-app/src/app/reader/components/sidebar/BooknoteView.tsx b/apps/readest-app/src/app/reader/components/sidebar/BooknoteView.tsx index fd1dd58b..2ac161d6 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BooknoteView.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BooknoteView.tsx @@ -1,7 +1,7 @@ import React from 'react'; - import * as CFI from 'foliate-js/epubcfi.js'; import { useBookDataStore } from '@/store/bookDataStore'; +import { useSidebarStore } from '@/store/sidebarStore'; import { findTocItemBS } from '@/utils/toc'; import { TOCItem } from '@/libs/document'; import { BooknoteGroup, BookNoteType } from '@/types/book'; @@ -13,7 +13,9 @@ const BooknoteView: React.FC<{ toc: TOCItem[]; }> = ({ type, bookKey, toc }) => { const { getConfig } = useBookDataStore(); + const { setActiveBooknoteType, setBooknoteResults } = useSidebarStore(); const config = getConfig(bookKey)!; + const { booknotes: allNotes = [] } = config; const booknotes = allNotes.filter((note) => note.type === type && !note.deletedAt); @@ -39,6 +41,14 @@ const BooknoteView: React.FC<{ return a.id - b.id; }); + const handleBrowseBookNotes = () => { + if (booknotes.length === 0) return; + + const sorted = [...booknotes].sort((a, b) => CFI.compare(a.cfi, b.cfi)); + setActiveBooknoteType(bookKey, type); + setBooknoteResults(bookKey, sorted); + }; + return (
    @@ -47,7 +57,12 @@ const BooknoteView: React.FC<{

    {group.label}

      {group.booknotes.map((item, index) => ( - + ))}
    diff --git a/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx b/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx new file mode 100644 index 00000000..16af0ef2 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/sidebar/BooknotesNav.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import { Insets } from '@/types/misc'; +import { TOCItem } from '@/libs/document'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useBooknotesNav } from '../../hooks/useBooknotesNav'; +import ContentNavBar from './ContentNavBar'; + +interface BooknotesNavProps { + bookKey: string; + gridInsets: Insets; + toc: TOCItem[]; +} + +const BooknotesNav: React.FC = ({ bookKey, gridInsets, toc }) => { + const { + activeBooknoteType, + currentSection, + showBooknotesNav, + hasPreviousPage, + hasNextPage, + handleShowResults, + handleClose, + handlePrevious, + handleNext, + } = useBooknotesNav(bookKey, toc); + const _ = useTranslation(); + + if (!showBooknotesNav) { + return null; + } + + const getShowResultsTitle = () => { + switch (activeBooknoteType) { + case 'bookmark': + return _('Bookmarks'); + case 'annotation': + return _('Annotations'); + case 'excerpt': + return _('Excerpts'); + default: + return ''; + } + }; + + return ( + + ); +}; + +export default BooknotesNav; diff --git a/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx new file mode 100644 index 00000000..a40baece --- /dev/null +++ b/apps/readest-app/src/app/reader/components/sidebar/ContentNavBar.tsx @@ -0,0 +1,133 @@ +import clsx from 'clsx'; +import React from 'react'; +import { IoIosList, IoMdCloseCircle } from 'react-icons/io'; +import { HiArrowLongLeft, HiArrowLongRight } from 'react-icons/hi2'; + +import { Insets } from '@/types/misc'; +import { useEnv } from '@/context/EnvContext'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useResponsiveSize } from '@/hooks/useResponsiveSize'; +import { useReaderStore } from '@/store/readerStore'; + +interface ContentNavBarProps { + bookKey: string; + gridInsets: Insets; + title: string; + section?: string; + showListButton?: boolean; + hasPrevious: boolean; + hasNext: boolean; + previousLabel?: string; + nextLabel?: string; + previousTitle?: string; + nextTitle?: string; + showResultsTitle?: string; + closeTitle?: string; + onShowResults?: () => void; + onClose: () => void; + onPrevious: () => void; + onNext: () => void; +} + +const ContentNavBar: React.FC = ({ + bookKey, + gridInsets, + title, + section, + showListButton = true, + hasPrevious, + hasNext, + previousLabel, + nextLabel, + previousTitle, + nextTitle, + showResultsTitle, + closeTitle, + onShowResults, + onClose, + onPrevious, + onNext, +}) => { + const { appService } = useEnv(); + const _ = useTranslation(); + const { getViewSettings } = useReaderStore(); + const viewSettings = getViewSettings(bookKey); + const iconSize16 = useResponsiveSize(16); + const iconSize18 = useResponsiveSize(18); + const iconSize20 = useResponsiveSize(20); + + const showSection = appService?.isMobile || !viewSettings?.showHeader; + + return ( +
    + {/* Top bar: Info */} +
    + {showListButton && onShowResults ? ( + + ) : ( +
    + )} + +
    + {title} + {section && showSection && ( + {section} + )} +
    + + +
    + + {/* Bottom bar: Navigation buttons */} +
    + + + +
    +
    + ); +}; + +export default ContentNavBar; diff --git a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx index dcafa412..ffd6f409 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx @@ -32,7 +32,9 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB const { getBookData } = useBookDataStore(); const { getConfig, saveConfig } = useBookDataStore(); const { getView, getProgress } = useReaderStore(); - const { searchTerm, setSearchTerm, setSearchResults } = useSidebarStore(); + const { getSearchNavState, setSearchTerm, setSearchResults } = useSidebarStore(); + const searchNavState = getSearchNavState(bookKey); + const { searchTerm } = searchNavState; const queuedSearchTerm = useRef(''); const inputRef = useRef(null); const inputFocusedRef = useRef(false); @@ -50,7 +52,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB useEffect(() => { handleSearchTermChange(searchTerm); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [bookKey]); + }, [bookKey, searchTerm]); useEffect(() => { if (isVisible && inputRef.current) { @@ -86,7 +88,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; - setSearchTerm(value); + setSearchTerm(bookKey, value); handleSearchTermChange(value); queuedSearchTerm.current = value; }; @@ -125,7 +127,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB for await (const result of generator) { if (typeof result === 'string') { if (result === 'done') { - setSearchResults([...results]); + setSearchResults(bookKey, [...results]); console.log('search done'); } } else { @@ -142,7 +144,7 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB } } else { results.push(result); - setSearchResults([...results]); + setSearchResults(bookKey, [...results]); } } @@ -157,9 +159,9 @@ const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchB ); const resetSearch = useCallback(() => { - setSearchResults([]); + setSearchResults(bookKey, []); view?.clearSearch(); - }, [view, setSearchResults]); + }, [bookKey, view, setSearchResults]); // eslint-disable-next-line react-hooks/exhaustive-deps const handleSearchTermChange = useCallback( diff --git a/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx b/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx index 35968da7..9b015484 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx @@ -1,15 +1,10 @@ -import clsx from 'clsx'; import React from 'react'; -import { IoIosList, IoMdCloseCircle } from 'react-icons/io'; -import { HiArrowLongLeft, HiArrowLongRight } from 'react-icons/hi2'; import { Insets } from '@/types/misc'; import { BookSearchMatch, BookSearchResult } from '@/types/book'; -import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; -import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { useSearchNav } from '../../hooks/useSearchNav'; -import { useReaderStore } from '@/store/readerStore'; +import ContentNavBar from './ContentNavBar'; interface SearchResultsNavProps { bookKey: string; @@ -28,87 +23,29 @@ const SearchResultsNav: React.FC = ({ bookKey, gridInsets handlePreviousResult, handleNextResult, } = useSearchNav(bookKey); - const { appService } = useEnv(); const _ = useTranslation(); - const { getViewSettings } = useReaderStore(); - const viewSettings = getViewSettings(bookKey); - const iconSize16 = useResponsiveSize(16); - const iconSize18 = useResponsiveSize(18); - const iconSize20 = useResponsiveSize(20); if (!showSearchNav) { return null; } - const showSection = appService?.isMobile || !viewSettings?.showHeader; - return ( -
    - {/* Top bar: Search info */} -
    - - -
    - - {_("Search results for '{{term}}'", { term: searchTerm })} - - {currentSection && showSection && ( - {currentSection} - )} -
    - - -
    - - {/* Bottom bar: Navigation buttons */} -
    - - - -
    -
    + ); }; diff --git a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx index b97fd67e..73940ed6 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx @@ -33,8 +33,10 @@ const SideBar: React.FC<{ const { appService } = useEnv(); const { updateAppTheme, safeAreaInsets } = useThemeStore(); const { settings } = useSettingsStore(); - const { sideBarBookKey, searchTerm, searchResults, setSearchTerm, clearSearch } = + const { sideBarBookKey, setSideBarBookKey, getSearchNavState, setSearchTerm, clearSearch } = useSidebarStore(); + const searchNavState = sideBarBookKey ? getSearchNavState(sideBarBookKey) : null; + const { searchTerm = '', searchResults = null } = searchNavState || {}; const { getBookData } = useBookDataStore(); const { getView, getViewSettings } = useReaderStore(); const [isSearchBarVisible, setIsSearchBarVisible] = useState(false); @@ -55,11 +57,12 @@ const SideBar: React.FC<{ ); const onSearchEvent = async (event: CustomEvent) => { - const { term } = event.detail; + const { term, bookKey } = event.detail; setSideBarVisible(true); + setSideBarBookKey(bookKey); setIsSearchBarVisible(true); if (term !== undefined && term !== null) { - setSearchTerm(term); + setSearchTerm(bookKey, term); } }; @@ -85,10 +88,10 @@ const SideBar: React.FC<{ }, [searchTerm]); useEffect(() => { - eventDispatcher.on('search', onSearchEvent); + eventDispatcher.on('search-term', onSearchEvent); eventDispatcher.on('navigate', onNavigateEvent); return () => { - eventDispatcher.off('search', onSearchEvent); + eventDispatcher.off('search-term', onSearchEvent); eventDispatcher.off('navigate', onNavigateEvent); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -192,7 +195,7 @@ const SideBar: React.FC<{ const handleHideSearchBar = useCallback(() => { setIsSearchBarVisible(false); setTimeout(() => { - clearSearch(); + if (sideBarBookKey) clearSearch(sideBarBookKey); }, 100); getView(sideBarBookKey)?.clearSearch(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts b/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts index 77a127eb..e64753b6 100644 --- a/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts +++ b/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts @@ -157,7 +157,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) = const showSearchBar = () => { setTimeout(() => { - eventDispatcher.dispatch('search', { term: null }); + eventDispatcher.dispatch('search-term', { term: null, bookKey: sideBarBookKey }); }, 100); }; diff --git a/apps/readest-app/src/app/reader/hooks/useBooknotesNav.ts b/apps/readest-app/src/app/reader/hooks/useBooknotesNav.ts new file mode 100644 index 00000000..a847b7d1 --- /dev/null +++ b/apps/readest-app/src/app/reader/hooks/useBooknotesNav.ts @@ -0,0 +1,185 @@ +import { useCallback, useMemo } from 'react'; +import * as CFI from 'foliate-js/epubcfi.js'; +import { useSidebarStore } from '@/store/sidebarStore'; +import { useReaderStore } from '@/store/readerStore'; +import { useBookDataStore } from '@/store/bookDataStore'; +import { isCfiInLocation } from '@/utils/cfi'; +import { findTocItemBS } from '@/utils/toc'; +import { BookNoteType } from '@/types/book'; +import { TOCItem } from '@/libs/document'; + +export function useBooknotesNav(bookKey: string, toc: TOCItem[]) { + const { getView, getProgress } = useReaderStore(); + const { getConfig } = useBookDataStore(); + const { + setSideBarVisible, + getBooknotesNavState, + setActiveBooknoteType, + setBooknoteResults, + setBooknoteIndex, + clearBooknotesNav, + } = useSidebarStore(); + + const booknotesNavState = getBooknotesNavState(bookKey); + const { activeBooknoteType, booknoteResults, booknoteIndex } = booknotesNavState; + + const progress = getProgress(bookKey); + const currentLocation = progress?.location; + + // Get booknotes from config and filter by type + const allBooknotes = useMemo(() => { + const config = getConfig(bookKey); + return config?.booknotes?.filter((note) => !note.deletedAt) || []; + }, [bookKey, getConfig]); + + // Sort booknotes by CFI order + const sortedBooknotes = useMemo(() => { + if (!booknoteResults) return []; + return [...booknoteResults].sort((a, b) => CFI.compare(a.cfi, b.cfi)); + }, [booknoteResults]); + + const totalResults = sortedBooknotes.length; + const hasBooknotes = booknoteResults && totalResults > 0; + const showBooknotesNav = hasBooknotes && activeBooknoteType !== null; + + // Get current section label + const currentSection = useMemo(() => { + if (!sortedBooknotes.length || booknoteIndex >= sortedBooknotes.length) return ''; + const currentNote = sortedBooknotes[booknoteIndex]; + if (!currentNote) return ''; + const tocItem = findTocItemBS(toc, currentNote.cfi); + return tocItem?.label || ''; + }, [sortedBooknotes, booknoteIndex, toc]); + + // Find booknotes on the current page + const currentPageResults = useMemo(() => { + if (!sortedBooknotes.length || !currentLocation) return { firstIndex: -1, lastIndex: -1 }; + + let firstIndex = -1; + let lastIndex = -1; + + for (let i = 0; i < sortedBooknotes.length; i++) { + const note = sortedBooknotes[i]; + if (note && isCfiInLocation(note.cfi, currentLocation)) { + if (firstIndex === -1) firstIndex = i; + lastIndex = i; + } + } + if (firstIndex !== -1) { + setTimeout(() => setBooknoteIndex(bookKey, firstIndex), 0); + } + + return { firstIndex, lastIndex }; + }, [sortedBooknotes, currentLocation, bookKey, setBooknoteIndex]); + + // Navigate to a specific booknote + const navigateToBooknote = useCallback( + (index: number) => { + if (!sortedBooknotes.length) return; + if (index < 0 || index >= sortedBooknotes.length) return; + + const note = sortedBooknotes[index]; + if (note) { + setBooknoteIndex(bookKey, index); + getView(bookKey)?.goTo(note.cfi); + } + }, + [bookKey, sortedBooknotes, setBooknoteIndex, getView], + ); + + // Start navigation for a specific booknote type + const startNavigation = useCallback( + (type: BookNoteType) => { + const filtered = allBooknotes.filter((note) => note.type === type); + if (filtered.length === 0) return; + + const sorted = [...filtered].sort((a, b) => CFI.compare(a.cfi, b.cfi)); + setActiveBooknoteType(bookKey, type); + setBooknoteResults(bookKey, sorted); + setBooknoteIndex(bookKey, 0); + + // Navigate to first booknote + if (sorted.length > 0) { + getView(bookKey)?.goTo(sorted[0]!.cfi); + } + }, + [ + allBooknotes, + bookKey, + setActiveBooknoteType, + setBooknoteResults, + setBooknoteIndex, + getView, + ], + ); + + const handleShowResults = useCallback(() => { + setSideBarVisible(true); + }, [setSideBarVisible]); + + const handleClose = useCallback(() => { + clearBooknotesNav(bookKey); + }, [clearBooknotesNav, bookKey]); + + // Navigate to the previous page with booknotes + const handlePrevious = useCallback(() => { + const { firstIndex } = currentPageResults; + + if (firstIndex > 0) { + navigateToBooknote(firstIndex - 1); + } else if (firstIndex === -1 && booknoteIndex > 0) { + navigateToBooknote(booknoteIndex - 1); + } + }, [currentPageResults, booknoteIndex, navigateToBooknote]); + + // Navigate to the next page with booknotes + const handleNext = useCallback(() => { + const { lastIndex } = currentPageResults; + + if (lastIndex >= 0 && lastIndex < totalResults - 1) { + navigateToBooknote(lastIndex + 1); + } else if (lastIndex === -1 && booknoteIndex < totalResults - 1) { + navigateToBooknote(booknoteIndex + 1); + } + }, [currentPageResults, totalResults, booknoteIndex, navigateToBooknote]); + + // Check if there are booknotes before/after the current page + const hasPreviousPage = + currentPageResults.firstIndex > 0 || + (currentPageResults.firstIndex === -1 && booknoteIndex > 0); + const hasNextPage = + (currentPageResults.lastIndex >= 0 && currentPageResults.lastIndex < totalResults - 1) || + (currentPageResults.lastIndex === -1 && booknoteIndex < totalResults - 1); + + // Get counts for each booknote type + const bookmarkCount = useMemo( + () => allBooknotes.filter((n) => n.type === 'bookmark').length, + [allBooknotes], + ); + const annotationCount = useMemo( + () => allBooknotes.filter((n) => n.type === 'annotation').length, + [allBooknotes], + ); + const excerptCount = useMemo( + () => allBooknotes.filter((n) => n.type === 'excerpt').length, + [allBooknotes], + ); + + return { + activeBooknoteType, + currentSection, + booknoteIndex, + totalResults, + showBooknotesNav, + hasPreviousPage, + hasNextPage, + bookmarkCount, + annotationCount, + excerptCount, + startNavigation, + handleShowResults, + handleClose, + handlePrevious, + handleNext, + }; +} diff --git a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts index 402ae65e..0893f5cb 100644 --- a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts +++ b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts @@ -7,15 +7,15 @@ import { flattenSearchResults } from '../components/sidebar/SearchResultsNav'; export function useSearchNav(bookKey: string) { const { getView, getProgress } = useReaderStore(); const { - sideBarBookKey, setSideBarVisible, - searchTerm, - searchResults, - searchResultIndex, + getSearchNavState, setSearchResultIndex, clearSearch, } = useSidebarStore(); + const searchNavState = getSearchNavState(bookKey); + const { searchTerm, searchResults, searchResultIndex } = searchNavState; + const progress = getProgress(bookKey); const currentLocation = useMemo(() => { @@ -53,25 +53,25 @@ export function useSearchNav(bookKey: string) { } } if (firstIndex !== -1) { - setTimeout(() => setSearchResultIndex(firstIndex), 0); + setTimeout(() => setSearchResultIndex(bookKey, firstIndex), 0); } return { firstIndex, lastIndex }; - }, [flattenedResults, currentLocation, setSearchResultIndex]); + }, [flattenedResults, currentLocation, bookKey, setSearchResultIndex]); // Navigate to a specific search result const navigateToResult = useCallback( (index: number) => { - if (!sideBarBookKey || !flattenedResults.length) return; + if (!flattenedResults.length) return; if (index < 0 || index >= flattenedResults.length) return; const result = flattenedResults[index]; if (result) { - setSearchResultIndex(index); - getView(sideBarBookKey)?.goTo(result.cfi); + setSearchResultIndex(bookKey, index); + getView(bookKey)?.goTo(result.cfi); } }, - [sideBarBookKey, flattenedResults, setSearchResultIndex, getView], + [bookKey, flattenedResults, setSearchResultIndex, getView], ); const handleShowResults = useCallback(() => { @@ -79,11 +79,9 @@ export function useSearchNav(bookKey: string) { }, [setSideBarVisible]); const handleCloseSearch = useCallback(() => { - clearSearch(); - if (sideBarBookKey) { - getView(sideBarBookKey)?.clearSearch(); - } - }, [clearSearch, sideBarBookKey, getView]); + clearSearch(bookKey); + getView(bookKey)?.clearSearch(); + }, [clearSearch, bookKey, getView]); // Navigate to the previous page with results (last result before current page) const handlePreviousResult = useCallback(() => { diff --git a/apps/readest-app/src/store/sidebarStore.ts b/apps/readest-app/src/store/sidebarStore.ts index e2ecc389..dd91b307 100644 --- a/apps/readest-app/src/store/sidebarStore.ts +++ b/apps/readest-app/src/store/sidebarStore.ts @@ -1,15 +1,28 @@ import { create } from 'zustand'; -import { BookSearchResult } from '@/types/book'; +import { BookNote, BookNoteType, BookSearchResult } from '@/types/book'; + +// Per-book search navigation state +interface SearchNavState { + searchTerm: string; + searchResults: BookSearchResult[] | null; + searchResultIndex: number; +} + +// Per-book booknotes navigation state +interface BooknotesNavState { + activeBooknoteType: BookNoteType | null; + booknoteResults: BookNote[] | null; + booknoteIndex: number; +} interface SidebarState { sideBarBookKey: string | null; sideBarWidth: string; isSideBarVisible: boolean; isSideBarPinned: boolean; - // Search state - searchTerm: string; - searchResults: BookSearchResult[] | null; - searchResultIndex: number; + // Per-book navigation states + searchNavStates: Record; + booknotesNavStates: Record; getIsSideBarVisible: () => boolean; getSideBarWidth: () => string; setSideBarBookKey: (key: string) => void; @@ -18,22 +31,40 @@ interface SidebarState { toggleSideBarPin: () => void; setSideBarVisible: (visible: boolean) => void; setSideBarPin: (pinned: boolean) => void; - // Search actions - setSearchTerm: (term: string) => void; - setSearchResults: (results: BookSearchResult[] | null) => void; - setSearchResultIndex: (index: number) => void; - clearSearch: () => void; + // Search actions (per bookKey) + getSearchNavState: (bookKey: string) => SearchNavState; + setSearchTerm: (bookKey: string, term: string) => void; + setSearchResults: (bookKey: string, results: BookSearchResult[] | null) => void; + setSearchResultIndex: (bookKey: string, index: number) => void; + clearSearch: (bookKey: string) => void; + // Booknotes navigation actions (per bookKey) + getBooknotesNavState: (bookKey: string) => BooknotesNavState; + setActiveBooknoteType: (bookKey: string, type: BookNoteType | null) => void; + setBooknoteResults: (bookKey: string, results: BookNote[] | null) => void; + setBooknoteIndex: (bookKey: string, index: number) => void; + clearBooknotesNav: (bookKey: string) => void; } +const defaultSearchNavState: SearchNavState = { + searchTerm: '', + searchResults: null, + searchResultIndex: 0, +}; + +const defaultBooknotesNavState: BooknotesNavState = { + activeBooknoteType: null, + booknoteResults: null, + booknoteIndex: 0, +}; + export const useSidebarStore = create((set, get) => ({ sideBarBookKey: null, sideBarWidth: '', isSideBarVisible: false, isSideBarPinned: false, - // Search state - searchTerm: '', - searchResults: null, - searchResultIndex: 0, + // Per-book navigation states + searchNavStates: {}, + booknotesNavStates: {}, getIsSideBarVisible: () => get().isSideBarVisible, getSideBarWidth: () => get().sideBarWidth, setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }), @@ -43,8 +74,85 @@ export const useSidebarStore = create((set, get) => ({ setSideBarVisible: (visible: boolean) => set({ isSideBarVisible: visible }), setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }), // Search actions - setSearchTerm: (term: string) => set({ searchTerm: term }), - setSearchResults: (results: BookSearchResult[] | null) => set({ searchResults: results }), - setSearchResultIndex: (index: number) => set({ searchResultIndex: index }), - clearSearch: () => set({ searchTerm: '', searchResults: null, searchResultIndex: 0 }), + getSearchNavState: (bookKey: string) => { + return get().searchNavStates[bookKey] || defaultSearchNavState; + }, + setSearchTerm: (bookKey: string, term: string) => + set((state) => ({ + searchNavStates: { + ...state.searchNavStates, + [bookKey]: { + ...(state.searchNavStates[bookKey] || defaultSearchNavState), + searchTerm: term, + }, + }, + })), + setSearchResults: (bookKey: string, results: BookSearchResult[] | null) => + set((state) => ({ + searchNavStates: { + ...state.searchNavStates, + [bookKey]: { + ...(state.searchNavStates[bookKey] || defaultSearchNavState), + searchResults: results, + }, + }, + })), + setSearchResultIndex: (bookKey: string, index: number) => + set((state) => ({ + searchNavStates: { + ...state.searchNavStates, + [bookKey]: { + ...(state.searchNavStates[bookKey] || defaultSearchNavState), + searchResultIndex: index, + }, + }, + })), + clearSearch: (bookKey: string) => + set((state) => ({ + searchNavStates: { + ...state.searchNavStates, + [bookKey]: { ...defaultSearchNavState }, + }, + })), + // Booknotes navigation actions + getBooknotesNavState: (bookKey: string) => { + return get().booknotesNavStates[bookKey] || defaultBooknotesNavState; + }, + setActiveBooknoteType: (bookKey: string, type: BookNoteType | null) => + set((state) => ({ + booknotesNavStates: { + ...state.booknotesNavStates, + [bookKey]: { + ...(state.booknotesNavStates[bookKey] || defaultBooknotesNavState), + activeBooknoteType: type, + }, + }, + })), + setBooknoteResults: (bookKey: string, results: BookNote[] | null) => + set((state) => ({ + booknotesNavStates: { + ...state.booknotesNavStates, + [bookKey]: { + ...(state.booknotesNavStates[bookKey] || defaultBooknotesNavState), + booknoteResults: results, + }, + }, + })), + setBooknoteIndex: (bookKey: string, index: number) => + set((state) => ({ + booknotesNavStates: { + ...state.booknotesNavStates, + [bookKey]: { + ...(state.booknotesNavStates[bookKey] || defaultBooknotesNavState), + booknoteIndex: index, + }, + }, + })), + clearBooknotesNav: (bookKey: string) => + set((state) => ({ + booknotesNavStates: { + ...state.booknotesNavStates, + [bookKey]: { ...defaultBooknotesNavState }, + }, + })), })); diff --git a/apps/readest-app/src/utils/toc.ts b/apps/readest-app/src/utils/toc.ts index a0f3bce7..54aa7205 100644 --- a/apps/readest-app/src/utils/toc.ts +++ b/apps/readest-app/src/utils/toc.ts @@ -24,12 +24,13 @@ export const findTocItemBS = (toc: TOCItem[], cfi: string): TOCItem | null => { while (left <= right) { const mid = Math.floor((left + right) / 2); + const item = toc[mid]!; const currentCfi = toc[mid]!.cfi || ''; const comparison = CFI.compare(currentCfi, cfi); if (comparison === 0) { - return toc[mid]!; + return findInSubitems(item, cfi) ?? item; } else if (comparison < 0) { - result = toc[mid]!; + result = findInSubitems(item, cfi) ?? item; left = mid + 1; } else { right = mid - 1; @@ -39,6 +40,11 @@ export const findTocItemBS = (toc: TOCItem[], cfi: string): TOCItem | null => { return result; }; +const findInSubitems = (item: TOCItem, cfi: string): TOCItem | null => { + if (!item.subitems?.length) return null; + return findTocItemBS(item.subitems, cfi); +}; + export const updateToc = async ( bookDoc: BookDoc, sortedTOC: boolean,