From a5e09e845446cfba3790d3cf56a9522b284cb61a Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 3 Jan 2026 08:10:23 +0100 Subject: [PATCH] feat(search): add search results navigation bar, closes #1183 (#2842) Add a floating navigation component that appears when search results exist. The component includes: - Top bar: displays search term and current section with TOC and close buttons - Bottom bar: search results, previous, and next navigation buttons - Page-based navigation using isCfiInLocation to skip between pages with results --- .../public/locales/ar/translation.json | 7 +- .../public/locales/bn/translation.json | 7 +- .../public/locales/bo/translation.json | 7 +- .../public/locales/de/translation.json | 7 +- .../public/locales/el/translation.json | 7 +- .../public/locales/es/translation.json | 7 +- .../public/locales/fa/translation.json | 7 +- .../public/locales/fr/translation.json | 7 +- .../public/locales/hi/translation.json | 7 +- .../public/locales/id/translation.json | 7 +- .../public/locales/it/translation.json | 7 +- .../public/locales/ja/translation.json | 7 +- .../public/locales/ko/translation.json | 7 +- .../public/locales/ms/translation.json | 7 +- .../public/locales/nl/translation.json | 7 +- .../public/locales/pl/translation.json | 7 +- .../public/locales/pt/translation.json | 7 +- .../public/locales/ru/translation.json | 7 +- .../public/locales/si/translation.json | 7 +- .../public/locales/sv/translation.json | 7 +- .../public/locales/ta/translation.json | 7 +- .../public/locales/th/translation.json | 7 +- .../public/locales/tr/translation.json | 7 +- .../public/locales/uk/translation.json | 7 +- .../public/locales/vi/translation.json | 7 +- .../public/locales/zh-CN/translation.json | 7 +- .../public/locales/zh-TW/translation.json | 7 +- .../app/reader/components/BookmarkToggler.tsx | 13 +- .../src/app/reader/components/BooksGrid.tsx | 2 + .../components/annotator/AnnotationTools.tsx | 8 +- .../reader/components/annotator/Annotator.tsx | 12 +- .../reader/components/sidebar/SearchBar.tsx | 32 ++-- .../components/sidebar/SearchResultsNav.tsx | 153 ++++++++++++++++++ .../app/reader/components/sidebar/SideBar.tsx | 13 +- .../app/reader/components/tts/TTSControl.tsx | 6 +- .../src/app/reader/hooks/useScrollToItem.ts | 11 +- .../src/app/reader/hooks/useSearchNav.ts | 135 ++++++++++++++++ apps/readest-app/src/store/sidebarStore.ts | 19 +++ apps/readest-app/src/utils/cfi.ts | 10 ++ 39 files changed, 514 insertions(+), 89 deletions(-) create mode 100644 apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx create mode 100644 apps/readest-app/src/app/reader/hooks/useSearchNav.ts create mode 100644 apps/readest-app/src/utils/cfi.ts diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index f344d5d1..4e01a3bc 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -864,5 +864,10 @@ "Book not found in library": "الكتاب غير موجود في المكتبة", "Unknown error": "خطأ غير معروف", "Please log in to continue": "يرجى تسجيل الدخول للمتابعة", - "Cloud File Transfers": "نقل الملفات السحابية" + "Cloud File Transfers": "نقل الملفات السحابية", + "Show Search Results": "عرض نتائج البحث", + "Search results for '{{term}}'": "نتائج البحث عن «{{term}}»", + "Close Search": "إغلاق البحث", + "Previous Result": "النتيجة السابقة", + "Next Result": "النتيجة التالية" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 676a5c5d..4ca1d13a 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "লাইব্রেরিতে বই পাওয়া যায়নি", "Unknown error": "অজানা ত্রুটি", "Please log in to continue": "চালিয়ে যেতে লগইন করুন", - "Cloud File Transfers": "ক্লাউড ফাইল স্থানান্তরসমূহ" + "Cloud File Transfers": "ক্লাউড ফাইল স্থানান্তরসমূহ", + "Show Search Results": "অনুসন্ধান ফলাফল দেখান", + "Search results for '{{term}}'": "'{{term}}' এর ফলাফল", + "Close Search": "অনুসন্ধান বন্ধ করুন", + "Previous Result": "আগের ফলাফল", + "Next Result": "পরবর্তী ফলাফল" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index cb0fcae1..79dd3dfb 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "དཔེ་མཛོད་ནང་དཔེ་ཆ་རྙེད་མ་སོང་", "Unknown error": "མ་ཤེས་པའི་ནོར་འཁྲུལ་", "Please log in to continue": "མུ་མཐུད་པར་ནང་འཛུལ་བྱོས་", - "Cloud File Transfers": "སྤྲིན་གནས་ཡིག་ཆ་སྐྱེལ་འདྲེན།" + "Cloud File Transfers": "སྤྲིན་གནས་ཡིག་ཆ་སྐྱེལ་འདྲེན།", + "Show Search Results": "འཚོལ་བཤེར་འབྲས་བུ་སྟོན།", + "Search results for '{{term}}'": "'{{term}}'ཡི་འབྲས་བུ།", + "Close Search": "འཚོལ་བཤེར་སྒོ་རྒྱག", + "Previous Result": "སྔོན་མའི་འབྲས་བུ།", + "Next Result": "རྗེས་མའི་འབྲས་བུ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 3e6ee535..e8d55126 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "Buch nicht in der Bibliothek gefunden", "Unknown error": "Unbekannter Fehler", "Please log in to continue": "Bitte melden Sie sich an, um fortzufahren", - "Cloud File Transfers": "Cloud-Dateiübertragungen" + "Cloud File Transfers": "Cloud-Dateiübertragungen", + "Show Search Results": "Suchergebnisse anzeigen", + "Search results for '{{term}}'": "Ergebnisse für '{{term}}'", + "Close Search": "Suche schließen", + "Previous Result": "Vorheriges Ergebnis", + "Next Result": "Nächstes Ergebnis" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 2e06870b..b1ca910e 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "Το βιβλίο δεν βρέθηκε στη βιβλιοθήκη", "Unknown error": "Άγνωστο σφάλμα", "Please log in to continue": "Παρακαλώ συνδεθείτε για να συνεχίσετε", - "Cloud File Transfers": "Μεταφορές αρχείων στο cloud" + "Cloud File Transfers": "Μεταφορές αρχείων στο cloud", + "Show Search Results": "Εμφάνιση αποτελεσμάτων", + "Search results for '{{term}}'": "Αποτελέσματα για '{{term}}'", + "Close Search": "Κλείσιμο αναζήτησης", + "Previous Result": "Προηγούμενο αποτέλεσμα", + "Next Result": "Επόμενο αποτέλεσμα" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index e3e98f06..05fb3458 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -834,5 +834,10 @@ "Book not found in library": "Libro no encontrado en la biblioteca", "Unknown error": "Error desconocido", "Please log in to continue": "Inicia sesión para continuar", - "Cloud File Transfers": "Transferencias en la nube" + "Cloud File Transfers": "Transferencias en la nube", + "Show Search Results": "Mostrar resultados", + "Search results for '{{term}}'": "Resultados para '{{term}}'", + "Close Search": "Cerrar búsqueda", + "Previous Result": "Resultado anterior", + "Next Result": "Resultado siguiente" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index cb719d96..3737202d 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "کتاب در کتابخانه یافت نشد", "Unknown error": "خطای ناشناخته", "Please log in to continue": "لطفاً برای ادامه وارد شوید", - "Cloud File Transfers": "انتقال فایل‌های ابری" + "Cloud File Transfers": "انتقال فایل‌های ابری", + "Show Search Results": "نمایش نتایج جستجو", + "Search results for '{{term}}'": "نتایج برای «{{term}}»", + "Close Search": "بستن جستجو", + "Previous Result": "نتیجه قبلی", + "Next Result": "نتیجه بعدی" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 3123147a..422383c0 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -834,5 +834,10 @@ "Book not found in library": "Livre non trouvé dans la bibliothèque", "Unknown error": "Erreur inconnue", "Please log in to continue": "Veuillez vous connecter pour continuer", - "Cloud File Transfers": "Transferts de fichiers cloud" + "Cloud File Transfers": "Transferts de fichiers cloud", + "Show Search Results": "Afficher les résultats", + "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" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 31337cf1..bcb85a8b 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "पुस्तकालय में पुस्तक नहीं मिली", "Unknown error": "अज्ञात त्रुटि", "Please log in to continue": "जारी रखने के लिए लॉगिन करें", - "Cloud File Transfers": "क्लाउड फ़ाइल स्थानांतरण" + "Cloud File Transfers": "क्लाउड फ़ाइल स्थानांतरण", + "Show Search Results": "खोज परिणाम दिखाएं", + "Search results for '{{term}}'": "'{{term}}' के परिणाम", + "Close Search": "खोज बंद करें", + "Previous Result": "पिछला परिणाम", + "Next Result": "अगला परिणाम" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 4c214e94..59d70e7a 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "Buku tidak ditemukan di perpustakaan", "Unknown error": "Kesalahan tidak diketahui", "Please log in to continue": "Silakan masuk untuk melanjutkan", - "Cloud File Transfers": "Transfer File Cloud" + "Cloud File Transfers": "Transfer File Cloud", + "Show Search Results": "Tampilkan hasil pencarian", + "Search results for '{{term}}'": "Hasil untuk '{{term}}'", + "Close Search": "Tutup pencarian", + "Previous Result": "Hasil sebelumnya", + "Next Result": "Hasil berikutnya" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 1bf47b1c..6cc01f96 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -834,5 +834,10 @@ "Book not found in library": "Libro non trovato nella libreria", "Unknown error": "Errore sconosciuto", "Please log in to continue": "Accedi per continuare", - "Cloud File Transfers": "Trasferimenti file cloud" + "Cloud File Transfers": "Trasferimenti file cloud", + "Show Search Results": "Mostra risultati", + "Search results for '{{term}}'": "Risultati per '{{term}}'", + "Close Search": "Chiudi ricerca", + "Previous Result": "Risultato precedente", + "Next Result": "Risultato successivo" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index f1e65c49..22115c48 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "ライブラリに本が見つかりません", "Unknown error": "不明なエラー", "Please log in to continue": "続行するにはログインしてください", - "Cloud File Transfers": "クラウドファイル転送" + "Cloud File Transfers": "クラウドファイル転送", + "Show Search Results": "検索結果を表示", + "Search results for '{{term}}'": "「{{term}}」を含む結果", + "Close Search": "検索を閉じる", + "Previous Result": "前の結果", + "Next Result": "次の結果" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index bcf3fd0b..8d2bcabc 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "라이브러리에서 책을 찾을 수 없음", "Unknown error": "알 수 없는 오류", "Please log in to continue": "계속하려면 로그인하세요", - "Cloud File Transfers": "클라우드 파일 전송" + "Cloud File Transfers": "클라우드 파일 전송", + "Show Search Results": "검색 결과 보기", + "Search results for '{{term}}'": "'{{term}}' 검색 결과", + "Close Search": "검색 닫기", + "Previous Result": "이전 결과", + "Next Result": "다음 결과" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 5db12a63..1155a7f8 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "Buku tidak dijumpai dalam perpustakaan", "Unknown error": "Ralat tidak diketahui", "Please log in to continue": "Sila log masuk untuk meneruskan", - "Cloud File Transfers": "Pemindahan Fail Awan" + "Cloud File Transfers": "Pemindahan Fail Awan", + "Show Search Results": "Tunjukkan hasil carian", + "Search results for '{{term}}'": "Hasil untuk '{{term}}'", + "Close Search": "Tutup carian", + "Previous Result": "Hasil sebelumnya", + "Next Result": "Hasil seterusnya" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 66be397c..871ab598 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "Boek niet gevonden in bibliotheek", "Unknown error": "Onbekende fout", "Please log in to continue": "Log in om door te gaan", - "Cloud File Transfers": "Cloud Bestandoverdrachten" + "Cloud File Transfers": "Cloud Bestandoverdrachten", + "Show Search Results": "Zoekresultaten tonen", + "Search results for '{{term}}'": "Resultaten voor '{{term}}'", + "Close Search": "Zoekopdracht sluiten", + "Previous Result": "Vorig resultaat", + "Next Result": "Volgend resultaat" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 779f8833..ed1b1bd8 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -844,5 +844,10 @@ "Book not found in library": "Książka nie została znaleziona w bibliotece", "Unknown error": "Nieznany błąd", "Please log in to continue": "Zaloguj się, aby kontynuować", - "Cloud File Transfers": "Transfery plików w chmurze" + "Cloud File Transfers": "Transfery plików w chmurze", + "Show Search Results": "Pokaż wyniki wyszukiwania", + "Search results for '{{term}}'": "Wyniki dla '{{term}}'", + "Close Search": "Zamknij wyszukiwanie", + "Previous Result": "Poprzedni wynik", + "Next Result": "Następny wynik" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index bea55fe1..b9cfa997 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -834,5 +834,10 @@ "Book not found in library": "Livro não encontrado na biblioteca", "Unknown error": "Erro desconhecido", "Please log in to continue": "Faça login para continuar", - "Cloud File Transfers": "Transferências de Arquivos" + "Cloud File Transfers": "Transferências de Arquivos", + "Show Search Results": "Mostrar resultados", + "Search results for '{{term}}'": "Resultados para '{{term}}'", + "Close Search": "Fechar pesquisa", + "Previous Result": "Resultado anterior", + "Next Result": "Próximo resultado" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index d4b75301..b3bbffcf 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -844,5 +844,10 @@ "Book not found in library": "Книга не найдена в библиотеке", "Unknown error": "Неизвестная ошибка", "Please log in to continue": "Войдите, чтобы продолжить", - "Cloud File Transfers": "Передача файлов в облако" + "Cloud File Transfers": "Передача файлов в облако", + "Show Search Results": "Показать результаты", + "Search results for '{{term}}'": "Результаты для «{{term}}»", + "Close Search": "Закрыть поиск", + "Previous Result": "Предыдущий результат", + "Next Result": "Следующий результат" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 73dae473..6405f41c 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "පුස්තකාලයේ පොත හමු නොවීය", "Unknown error": "නොදන්නා දෝෂයක්", "Please log in to continue": "ඉදිරියට යාමට පිවිසෙන්න", - "Cloud File Transfers": "කලාප ගොනු හුවමාරු" + "Cloud File Transfers": "කලාප ගොනු හුවමාරු", + "Show Search Results": "සෙවුම් ප්‍රතිඵල පෙන්වන්න", + "Search results for '{{term}}'": "'{{term}}' සඳහා ප්‍රතිඵල", + "Close Search": "සෙවුම වසන්න", + "Previous Result": "පෙර ප්‍රතිඵලය", + "Next Result": "ඊළඟ ප්‍රතිඵලය" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 93f99abf..9a9037be 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "Boken hittades inte i biblioteket", "Unknown error": "Okänt fel", "Please log in to continue": "Logga in för att fortsätta", - "Cloud File Transfers": "Molnfilöverföringar" + "Cloud File Transfers": "Molnfilöverföringar", + "Show Search Results": "Visa sökresultat", + "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" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 9c9c50bb..d388c414 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "நூலகத்தில் புத்தகம் கிடைக்கவில்லை", "Unknown error": "தெரியாத பிழை", "Please log in to continue": "தொடர உள்நுழையவும்", - "Cloud File Transfers": "மேகக் கோப்பு பரிமாற்றங்கள்" + "Cloud File Transfers": "மேகக் கோப்பு பரிமாற்றங்கள்", + "Show Search Results": "தேடல் முடிவுகளைக் காட்டு", + "Search results for '{{term}}'": "'{{term}}' க்கான முடிவுகள்", + "Close Search": "தேடலை மூடு", + "Previous Result": "முந்தைய முடிவு", + "Next Result": "அடுத்த முடிவு" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index f3c47177..3d31781e 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "ไม่พบหนังสือในห้องสมุด", "Unknown error": "ข้อผิดพลาดที่ไม่รู้จัก", "Please log in to continue": "กรุณาเข้าสู่ระบบเพื่อดำเนินการต่อ", - "Cloud File Transfers": "การถ่ายโอนไฟล์คลาวด์" + "Cloud File Transfers": "การถ่ายโอนไฟล์คลาวด์", + "Show Search Results": "แสดงผลการค้นหา", + "Search results for '{{term}}'": "ผลลัพธ์สำหรับ '{{term}}'", + "Close Search": "ปิดการค้นหา", + "Previous Result": "ผลลัพธ์ก่อนหน้า", + "Next Result": "ผลลัพธ์ถัดไป" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 0c0530f2..567c2bb1 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -824,5 +824,10 @@ "Book not found in library": "Kitap kütüphanede bulunamadı", "Unknown error": "Bilinmeyen hata", "Please log in to continue": "Devam etmek için giriş yapın", - "Cloud File Transfers": "Bulut Dosya Transferleri" + "Cloud File Transfers": "Bulut Dosya Transferleri", + "Show Search Results": "Arama sonuçlarını göster", + "Search results for '{{term}}'": "'{{term}}' için sonuçlar", + "Close Search": "Aramayı kapat", + "Previous Result": "Önceki sonuç", + "Next Result": "Sonraki sonuç" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 5d6afc9d..1b64d763 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -844,5 +844,10 @@ "Book not found in library": "Книгу не знайдено в бібліотеці", "Unknown error": "Невідома помилка", "Please log in to continue": "Увійдіть, щоб продовжити", - "Cloud File Transfers": "Передача файлів у хмару" + "Cloud File Transfers": "Передача файлів у хмару", + "Show Search Results": "Показати результати", + "Search results for '{{term}}'": "Результати для «{{term}}»", + "Close Search": "Закрити пошук", + "Previous Result": "Попередній результат", + "Next Result": "Наступний результат" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index ba389c4e..0e83776c 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "Không tìm thấy sách trong thư viện", "Unknown error": "Lỗi không xác định", "Please log in to continue": "Vui lòng đăng nhập để tiếp tục", - "Cloud File Transfers": "Truyền tệp đám mây" + "Cloud File Transfers": "Truyền tệp đám mây", + "Show Search Results": "Hiển thị kết quả tìm kiếm", + "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" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index b9f6498e..ad8721fa 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "在书库中找不到书籍", "Unknown error": "未知错误", "Please log in to continue": "请登录以继续", - "Cloud File Transfers": "云文件传输" + "Cloud File Transfers": "云文件传输", + "Show Search Results": "显示搜索结果", + "Search results for '{{term}}'": "包含"{{term}}"的结果", + "Close Search": "关闭搜索", + "Previous Result": "上一个结果", + "Next Result": "下一个结果" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index d8d9c431..25a1b8d2 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -814,5 +814,10 @@ "Book not found in library": "在書庫中找不到書籍", "Unknown error": "未知錯誤", "Please log in to continue": "請登入以繼續", - "Cloud File Transfers": "雲端檔案傳輸" + "Cloud File Transfers": "雲端檔案傳輸", + "Show Search Results": "顯示搜尋結果", + "Search results for '{{term}}'": "包含「{{term}}」的結果", + "Close Search": "關閉搜尋", + "Previous Result": "上一個結果", + "Next Result": "下一個結果" } diff --git a/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx b/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx index d16ef986..ca1c5136 100644 --- a/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx +++ b/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { MdOutlineBookmarkAdd, MdOutlineBookmark } from 'react-icons/md'; -import * as CFI from 'foliate-js/epubcfi.js'; import { useSettingsStore } from '@/store/settingsStore'; import { useBookDataStore } from '@/store/bookDataStore'; @@ -12,6 +11,7 @@ import { uniqueId } from '@/utils/misc'; import Button from '@/components/Button'; import { getCurrentPage } from '@/utils/book'; import { eventDispatcher } from '@/utils/event'; +import { isCfiInLocation } from '@/utils/cfi'; interface BookmarkTogglerProps { bookKey: string; @@ -66,13 +66,8 @@ const BookmarkToggler: React.FC = ({ bookKey }) => { } } else { setIsBookmarked(false); - const start = CFI.collapse(cfi); - const end = CFI.collapse(cfi, true); bookmarks.forEach((item) => { - if ( - item.type === 'bookmark' && - CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) <= 0 - ) { + if (item.type === 'bookmark' && isCfiInLocation(item.cfi, cfi)) { item.deletedAt = Date.now(); } }); @@ -101,11 +96,9 @@ const BookmarkToggler: React.FC = ({ bookKey }) => { const { location: cfi } = progress || {}; if (!cfi) return; - const start = CFI.collapse(cfi); - const end = CFI.collapse(cfi, true); const locationBookmarked = booknotes .filter((booknote) => booknote.type === 'bookmark' && !booknote.deletedAt) - .some((item) => CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) <= 0); + .some((item) => isCfiInLocation(item.cfi, cfi)); setIsBookmarked(locationBookmarked); setBookmarkRibbonVisibility(bookKey, locationBookmarked); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/readest-app/src/app/reader/components/BooksGrid.tsx b/apps/readest-app/src/app/reader/components/BooksGrid.tsx index 474213f2..2c8a4ac7 100644 --- a/apps/readest-app/src/app/reader/components/BooksGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BooksGrid.tsx @@ -11,6 +11,7 @@ import { useTranslation } from '@/hooks/useTranslation'; import { getGridTemplate, getInsetEdges } from '@/utils/grid'; import { getViewInsets } from '@/utils/insets'; import SettingsDialog from '@/components/settings/SettingsDialog'; +import SearchResultsNav from './sidebar/SearchResultsNav'; import FoliateViewer from './FoliateViewer'; import SectionInfo from './SectionInfo'; import HeaderBar from './HeaderBar'; @@ -198,6 +199,7 @@ const BooksGrid: React.FC = ({ bookKeys, onCloseBook }) => { /> )} + = ({ bookKey }) => { handleDismissPopupAndSelection(); }, 0); break; + case 'search': + handleSearch(); + break; case 'dictionary': handleDictionary(); break; @@ -460,16 +464,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { useEffect(() => { if (!progress) return; const { location } = progress; - const start = CFI.collapse(location); - const end = CFI.collapse(location, true); const { booknotes = [] } = config; const annotations = booknotes.filter( (item) => !item.deletedAt && item.type === 'annotation' && item.style && - CFI.compare(item.cfi, start) >= 0 && - CFI.compare(item.cfi, end) <= 0, + isCfiInLocation(item.cfi, location), ); const notes = booknotes.filter( (item) => @@ -477,8 +478,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { item.type === 'annotation' && item.note && item.note.trim().length > 0 && - CFI.compare(item.cfi, start) >= 0 && - CFI.compare(item.cfi, end) <= 0, + isCfiInLocation(item.cfi, location), ); try { Promise.all(annotations.map((annotation) => view?.addAnnotation(annotation))); 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 63bc1808..dcafa412 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchBar.tsx @@ -1,11 +1,12 @@ import clsx from 'clsx'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { FaSearch, FaChevronDown } from 'react-icons/fa'; import { useEnv } from '@/context/EnvContext'; import { useSettingsStore } from '@/store/settingsStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; +import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; import { BookSearchConfig, BookSearchResult } from '@/types/book'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; @@ -21,25 +22,17 @@ const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1; interface SearchBarProps { isVisible: boolean; bookKey: string; - searchTerm: string; - onSearchResultChange: (results: BookSearchResult[]) => void; onHideSearchBar: () => void; } -const SearchBar: React.FC = ({ - isVisible, - bookKey, - searchTerm: term, - onSearchResultChange, - onHideSearchBar, -}) => { +const SearchBar: React.FC = ({ isVisible, bookKey, onHideSearchBar }) => { const _ = useTranslation(); const { envConfig } = useEnv(); const { settings } = useSettingsStore(); const { getBookData } = useBookDataStore(); const { getConfig, saveConfig } = useBookDataStore(); const { getView, getProgress } = useReaderStore(); - const [searchTerm, setSearchTerm] = useState(term); + const { searchTerm, setSearchTerm, setSearchResults } = useSidebarStore(); const queuedSearchTerm = useRef(''); const inputRef = useRef(null); const inputFocusedRef = useRef(false); @@ -59,12 +52,6 @@ const SearchBar: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [bookKey]); - useEffect(() => { - setSearchTerm(term); - handleSearchTermChange(term); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [term]); - useEffect(() => { if (isVisible && inputRef.current) { inputRef.current.onblur = () => { @@ -138,7 +125,7 @@ const SearchBar: React.FC = ({ for await (const result of generator) { if (typeof result === 'string') { if (result === 'done') { - onSearchResultChange([...results]); + setSearchResults([...results]); console.log('search done'); } } else { @@ -155,7 +142,7 @@ const SearchBar: React.FC = ({ } } else { results.push(result); - onSearchResultChange([...results]); + setSearchResults([...results]); } } @@ -166,14 +153,13 @@ const SearchBar: React.FC = ({ processResults(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [progress, searchConfig], + [progress, searchConfig, setSearchResults], ); const resetSearch = useCallback(() => { - onSearchResultChange([]); + setSearchResults([]); view?.clearSearch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [view]); + }, [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 new file mode 100644 index 00000000..35968da7 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/sidebar/SearchResultsNav.tsx @@ -0,0 +1,153 @@ +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'; + +interface SearchResultsNavProps { + bookKey: string; + gridInsets: Insets; +} + +const SearchResultsNav: React.FC = ({ bookKey, gridInsets }) => { + const { + searchTerm, + currentSection, + showSearchNav, + hasPreviousPage, + hasNextPage, + handleShowResults, + handleCloseSearch, + 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 */} +
+ + + +
+
+ ); +}; + +export default SearchResultsNav; + +// Helper function to flatten search results into a single array of matches with section labels +export function flattenSearchResults( + results: BookSearchResult[] | BookSearchMatch[], +): { cfi: string; sectionLabel: string }[] { + const flattened: { cfi: string; sectionLabel: string }[] = []; + + for (const result of results) { + if ('subitems' in result) { + // BookSearchResult with subitems + for (const item of result.subitems) { + flattened.push({ cfi: item.cfi, sectionLabel: result.label }); + } + } else { + // BookSearchMatch + flattened.push({ cfi: result.cfi, sectionLabel: '' }); + } + } + + return flattened; +} + +// Helper function to find the index of current result based on CFI +export function findCurrentResultIndex( + flattenedResults: { cfi: string; sectionLabel: string }[], + currentLocation: string | undefined, +): number { + if (!currentLocation || flattenedResults.length === 0) return 0; + + // Try to find exact match or closest match + for (let i = 0; i < flattenedResults.length; i++) { + if (flattenedResults[i]!.cfi === currentLocation) { + return i; + } + } + + return 0; +} 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 3f4d25ee..b97fd67e 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx @@ -7,7 +7,6 @@ import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; -import { BookSearchResult } from '@/types/book'; import { eventDispatcher } from '@/utils/event'; import { getBookDirFromLanguage } from '@/utils/book'; import { useEnv } from '@/context/EnvContext'; @@ -34,12 +33,11 @@ const SideBar: React.FC<{ const { appService } = useEnv(); const { updateAppTheme, safeAreaInsets } = useThemeStore(); const { settings } = useSettingsStore(); - const { sideBarBookKey } = useSidebarStore(); + const { sideBarBookKey, searchTerm, searchResults, setSearchTerm, clearSearch } = + useSidebarStore(); const { getBookData } = useBookDataStore(); const { getView, getViewSettings } = useReaderStore(); const [isSearchBarVisible, setIsSearchBarVisible] = useState(false); - const [searchResults, setSearchResults] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); const searchTermRef = useRef(searchTerm); const sidebarHeight = useRef(1.0); const isMobile = window.innerWidth < 640; @@ -193,13 +191,12 @@ const SideBar: React.FC<{ const handleHideSearchBar = useCallback(() => { setIsSearchBarVisible(false); - setSearchResults(null); setTimeout(() => { - setSearchTerm(''); + clearSearch(); }, 100); getView(sideBarBookKey)?.clearSearch(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sideBarBookKey]); + }, [sideBarBookKey, clearSearch]); const handleHideSideBar = useCallback(() => { if (searchTermRef.current) { @@ -317,8 +314,6 @@ const SideBar: React.FC<{ diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index 63441afe..e31adc45 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -14,11 +14,11 @@ import { getPopupPosition, Position } from '@/utils/sel'; import { eventDispatcher } from '@/utils/event'; import { parseSSMLLang } from '@/utils/ssml'; import { throttle } from '@/utils/throttle'; -import { CFI } from '@/libs/document'; import { Insets } from '@/types/misc'; import { Overlay } from '@/components/Overlay'; import { fetchImageAsBase64 } from '@/utils/image'; import { invokeUseBackgroundAudio } from '@/utils/bridge'; +import { isCfiInLocation } from '@/utils/cfi'; import { getLocale } from '@/utils/misc'; import Popup from '@/components/Popup'; import TTSPanel from './TTSPanel'; @@ -306,9 +306,7 @@ const TTSControl: React.FC = ({ bookKey, gridInsets }) => { if (!ttsFromRange && viewSettings.ttsLocation) { const { location } = progress; const ttsCfi = viewSettings.ttsLocation; - const start = CFI.collapse(location); - const end = CFI.collapse(location, true); - if (CFI.compare(start, ttsCfi) * CFI.compare(end, ttsCfi) <= 0) { + if (isCfiInLocation(ttsCfi, location)) { const { index, anchor } = view.resolveCFI(ttsCfi); const { doc } = view.renderer.getContents().find((x) => x.index === index) || {}; if (doc) { diff --git a/apps/readest-app/src/app/reader/hooks/useScrollToItem.ts b/apps/readest-app/src/app/reader/hooks/useScrollToItem.ts index 87b74178..6d830f05 100644 --- a/apps/readest-app/src/app/reader/hooks/useScrollToItem.ts +++ b/apps/readest-app/src/app/reader/hooks/useScrollToItem.ts @@ -1,18 +1,11 @@ import { useEffect, useMemo, useRef } from 'react'; import { BookProgress } from '@/types/book'; -import * as CFI from 'foliate-js/epubcfi.js'; +import { isCfiInLocation } from '@/utils/cfi'; const useScrollToItem = (cfi: string, progress: BookProgress | null) => { const viewRef = useRef(null); - const isCurrent = useMemo(() => { - if (!progress) return false; - - const { location } = progress; - const start = CFI.collapse(location); - const end = CFI.collapse(location, true); - return CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0; - }, [cfi, progress]); + const isCurrent = useMemo(() => isCfiInLocation(cfi, progress?.location), [cfi, progress]); useEffect(() => { if (!viewRef.current || !isCurrent) return; diff --git a/apps/readest-app/src/app/reader/hooks/useSearchNav.ts b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts new file mode 100644 index 00000000..402ae65e --- /dev/null +++ b/apps/readest-app/src/app/reader/hooks/useSearchNav.ts @@ -0,0 +1,135 @@ +import { useCallback, useMemo } from 'react'; +import { useSidebarStore } from '@/store/sidebarStore'; +import { useReaderStore } from '@/store/readerStore'; +import { isCfiInLocation } from '@/utils/cfi'; +import { flattenSearchResults } from '../components/sidebar/SearchResultsNav'; + +export function useSearchNav(bookKey: string) { + const { getView, getProgress } = useReaderStore(); + const { + sideBarBookKey, + setSideBarVisible, + searchTerm, + searchResults, + searchResultIndex, + setSearchResultIndex, + clearSearch, + } = useSidebarStore(); + + const progress = getProgress(bookKey); + + const currentLocation = useMemo(() => { + return progress?.location; + }, [progress]); + + // Flatten search results for navigation + const flattenedResults = useMemo(() => { + if (!searchResults) return []; + return flattenSearchResults(searchResults); + }, [searchResults]); + + const totalResults = flattenedResults.length; + const hasSearchResults = searchResults && totalResults > 0; + const showSearchNav = hasSearchResults; + + // Get current section label + const currentSection = useMemo(() => { + if (!flattenedResults.length || searchResultIndex >= flattenedResults.length) return ''; + return flattenedResults[searchResultIndex]?.sectionLabel || ''; + }, [flattenedResults, searchResultIndex]); + + // Find results on the current page + const currentPageResults = useMemo(() => { + if (!flattenedResults.length || !currentLocation) return { firstIndex: -1, lastIndex: -1 }; + + let firstIndex = -1; + let lastIndex = -1; + + for (let i = 0; i < flattenedResults.length; i++) { + const result = flattenedResults[i]; + if (result && isCfiInLocation(result.cfi, currentLocation)) { + if (firstIndex === -1) firstIndex = i; + lastIndex = i; + } + } + if (firstIndex !== -1) { + setTimeout(() => setSearchResultIndex(firstIndex), 0); + } + + return { firstIndex, lastIndex }; + }, [flattenedResults, currentLocation, setSearchResultIndex]); + + // Navigate to a specific search result + const navigateToResult = useCallback( + (index: number) => { + if (!sideBarBookKey || !flattenedResults.length) return; + if (index < 0 || index >= flattenedResults.length) return; + + const result = flattenedResults[index]; + if (result) { + setSearchResultIndex(index); + getView(sideBarBookKey)?.goTo(result.cfi); + } + }, + [sideBarBookKey, flattenedResults, setSearchResultIndex, getView], + ); + + const handleShowResults = useCallback(() => { + setSideBarVisible(true); + }, [setSideBarVisible]); + + const handleCloseSearch = useCallback(() => { + clearSearch(); + if (sideBarBookKey) { + getView(sideBarBookKey)?.clearSearch(); + } + }, [clearSearch, sideBarBookKey, getView]); + + // Navigate to the previous page with results (last result before current page) + const handlePreviousResult = useCallback(() => { + const { firstIndex } = currentPageResults; + + if (firstIndex > 0) { + // Navigate to the result just before the first result on current page + navigateToResult(firstIndex - 1); + } else if (firstIndex === -1 && searchResultIndex > 0) { + // No results on current page, just go to previous result + navigateToResult(searchResultIndex - 1); + } + }, [currentPageResults, searchResultIndex, navigateToResult]); + + // Navigate to the next page with results (first result after current page) + const handleNextResult = useCallback(() => { + const { lastIndex } = currentPageResults; + + if (lastIndex >= 0 && lastIndex < totalResults - 1) { + // Navigate to the result just after the last result on current page + navigateToResult(lastIndex + 1); + } else if (lastIndex === -1 && searchResultIndex < totalResults - 1) { + // No results on current page, just go to next result + navigateToResult(searchResultIndex + 1); + } + }, [currentPageResults, totalResults, searchResultIndex, navigateToResult]); + + // Check if there are results before/after the current page + const hasPreviousPage = + currentPageResults.firstIndex > 0 || + (currentPageResults.firstIndex === -1 && searchResultIndex > 0); + const hasNextPage = + (currentPageResults.lastIndex >= 0 && currentPageResults.lastIndex < totalResults - 1) || + (currentPageResults.lastIndex === -1 && searchResultIndex < totalResults - 1); + + return { + searchTerm, + currentSection, + searchResultIndex, + totalResults, + showSearchNav, + hasPreviousPage, + hasNextPage, + handleShowResults, + handleCloseSearch, + handlePreviousResult, + handleNextResult, + }; +} diff --git a/apps/readest-app/src/store/sidebarStore.ts b/apps/readest-app/src/store/sidebarStore.ts index 16e57ee4..e2ecc389 100644 --- a/apps/readest-app/src/store/sidebarStore.ts +++ b/apps/readest-app/src/store/sidebarStore.ts @@ -1,10 +1,15 @@ import { create } from 'zustand'; +import { BookSearchResult } from '@/types/book'; interface SidebarState { sideBarBookKey: string | null; sideBarWidth: string; isSideBarVisible: boolean; isSideBarPinned: boolean; + // Search state + searchTerm: string; + searchResults: BookSearchResult[] | null; + searchResultIndex: number; getIsSideBarVisible: () => boolean; getSideBarWidth: () => string; setSideBarBookKey: (key: string) => void; @@ -13,6 +18,11 @@ 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; } export const useSidebarStore = create((set, get) => ({ @@ -20,6 +30,10 @@ export const useSidebarStore = create((set, get) => ({ sideBarWidth: '', isSideBarVisible: false, isSideBarPinned: false, + // Search state + searchTerm: '', + searchResults: null, + searchResultIndex: 0, getIsSideBarVisible: () => get().isSideBarVisible, getSideBarWidth: () => get().sideBarWidth, setSideBarBookKey: (key: string) => set({ sideBarBookKey: key }), @@ -28,4 +42,9 @@ export const useSidebarStore = create((set, get) => ({ toggleSideBarPin: () => set((state) => ({ isSideBarPinned: !state.isSideBarPinned })), 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 }), })); diff --git a/apps/readest-app/src/utils/cfi.ts b/apps/readest-app/src/utils/cfi.ts new file mode 100644 index 00000000..950a0ec4 --- /dev/null +++ b/apps/readest-app/src/utils/cfi.ts @@ -0,0 +1,10 @@ +import * as CFI from 'foliate-js/epubcfi.js'; + +export function isCfiInLocation(cfi: string, location: string | null | undefined): boolean { + if (!location) return false; + + const start = CFI.collapse(location); + const end = CFI.collapse(location, true); + + return CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0; +}