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
This commit is contained in:
@@ -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": "النتيجة التالية"
|
||||
}
|
||||
|
||||
@@ -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": "পরবর্তী ফলাফল"
|
||||
}
|
||||
|
||||
@@ -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": "རྗེས་མའི་འབྲས་བུ།"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Επόμενο αποτέλεσμα"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "نتیجه بعدی"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "अगला परिणाम"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "次の結果"
|
||||
}
|
||||
|
||||
@@ -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": "다음 결과"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Следующий результат"
|
||||
}
|
||||
|
||||
@@ -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": "ඊළඟ ප්රතිඵලය"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "அடுத்த முடிவு"
|
||||
}
|
||||
|
||||
@@ -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": "ผลลัพธ์ถัดไป"
|
||||
}
|
||||
|
||||
@@ -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ç"
|
||||
}
|
||||
|
||||
@@ -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": "Наступний результат"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "下一个结果"
|
||||
}
|
||||
|
||||
@@ -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": "下一個結果"
|
||||
}
|
||||
|
||||
@@ -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<BookmarkTogglerProps> = ({ 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<BookmarkTogglerProps> = ({ 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
|
||||
|
||||
@@ -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<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
/>
|
||||
)}
|
||||
<Annotator bookKey={bookKey} />
|
||||
<SearchResultsNav bookKey={bookKey} gridInsets={gridInsets} />
|
||||
<FootnotePopup bookKey={bookKey} bookDoc={bookDoc} />
|
||||
<FooterBar
|
||||
bookKey={bookKey}
|
||||
|
||||
@@ -50,7 +50,13 @@ export const annotationToolButtons = createAnnotationToolButtons([
|
||||
tooltip: _('Annotate text after selection'),
|
||||
Icon: BsPencilSquare,
|
||||
},
|
||||
{ type: 'search', label: _('Search'), tooltip: _('Search text after selection'), Icon: FiSearch },
|
||||
{
|
||||
type: 'search',
|
||||
label: _('Search'),
|
||||
tooltip: _('Search text after selection'),
|
||||
Icon: FiSearch,
|
||||
quickAction: true,
|
||||
},
|
||||
{
|
||||
type: 'dictionary',
|
||||
label: _('Dictionary'),
|
||||
|
||||
@@ -24,6 +24,7 @@ import { findTocItemBS } from '@/utils/toc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { runSimpleCC } from '@/utils/simplecc';
|
||||
import { getWordCount } from '@/utils/word';
|
||||
import { isCfiInLocation } from '@/utils/cfi';
|
||||
import { getHighlightColorHex } from '../../utils/annotatorUtil';
|
||||
import { annotationToolButtons } from './AnnotationTools';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
@@ -386,6 +387,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ 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)));
|
||||
|
||||
@@ -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<SearchBarProps> = ({
|
||||
isVisible,
|
||||
bookKey,
|
||||
searchTerm: term,
|
||||
onSearchResultChange,
|
||||
onHideSearchBar,
|
||||
}) => {
|
||||
const SearchBar: React.FC<SearchBarProps> = ({ 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<HTMLInputElement>(null);
|
||||
const inputFocusedRef = useRef(false);
|
||||
@@ -59,12 +52,6 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
// 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<SearchBarProps> = ({
|
||||
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<SearchBarProps> = ({
|
||||
}
|
||||
} else {
|
||||
results.push(result);
|
||||
onSearchResultChange([...results]);
|
||||
setSearchResults([...results]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,14 +153,13 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
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(
|
||||
|
||||
@@ -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<SearchResultsNavProps> = ({ 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 (
|
||||
<div
|
||||
className='search-results-nav pointer-events-none absolute inset-0 z-30 flex flex-col items-center justify-between px-4 py-1'
|
||||
style={{
|
||||
top: gridInsets.top,
|
||||
right: gridInsets.right,
|
||||
bottom: gridInsets.bottom / 4,
|
||||
left: gridInsets.left,
|
||||
}}
|
||||
>
|
||||
{/* Top bar: Search info */}
|
||||
<div className='bg-base-200/95 pointer-events-auto flex items-center justify-between rounded-xl px-4 py-1 shadow-lg backdrop-blur-sm sm:gap-6'>
|
||||
<button
|
||||
title={_('Show Search Results')}
|
||||
onClick={handleShowResults}
|
||||
className='btn btn-ghost h-8 min-h-8 w-8 p-0 hover:bg-transparent'
|
||||
>
|
||||
<IoIosList size={iconSize20} className='text-base-content' />
|
||||
</button>
|
||||
|
||||
<div className='flex flex-1 flex-col items-center px-2'>
|
||||
<span className='line-clamp-1 text-sm font-medium'>
|
||||
{_("Search results for '{{term}}'", { term: searchTerm })}
|
||||
</span>
|
||||
{currentSection && showSection && (
|
||||
<span className='text-base-content/70 line-clamp-1 text-xs'>{currentSection}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
title={_('Close Search')}
|
||||
onClick={handleCloseSearch}
|
||||
className='btn btn-ghost h-8 min-h-8 w-8 p-0 hover:bg-transparent'
|
||||
>
|
||||
<IoMdCloseCircle size={iconSize16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar: Navigation buttons */}
|
||||
<div className='bg-base-200/95 pointer-events-auto flex items-center justify-between gap-6 rounded-xl px-4 py-0 shadow-lg backdrop-blur-sm'>
|
||||
<button
|
||||
title={_('Previous Result')}
|
||||
onClick={handlePreviousResult}
|
||||
disabled={!hasPreviousPage}
|
||||
className={clsx(
|
||||
'btn btn-ghost flex h-auto min-h-0 flex-1 flex-col items-center gap-0 p-1 hover:bg-transparent',
|
||||
!hasPreviousPage && 'opacity-40',
|
||||
)}
|
||||
>
|
||||
<HiArrowLongLeft size={iconSize18} className='text-base-content' />
|
||||
<span className='text-sm font-medium'>{_('Previous')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
title={_('Next Result')}
|
||||
onClick={handleNextResult}
|
||||
disabled={!hasNextPage}
|
||||
className={clsx(
|
||||
'btn btn-ghost flex h-auto min-h-0 flex-1 flex-col items-center gap-0 p-1 hover:bg-transparent',
|
||||
!hasNextPage && 'opacity-40',
|
||||
)}
|
||||
>
|
||||
<HiArrowLongRight size={iconSize18} className='text-base-content' />
|
||||
<span className='text-sm font-medium'>{_('Next')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<BookSearchResult[] | null>(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<{
|
||||
<SearchBar
|
||||
isVisible={isSearchBarVisible}
|
||||
bookKey={sideBarBookKey!}
|
||||
searchTerm={searchTerm}
|
||||
onSearchResultChange={setSearchResults}
|
||||
onHideSearchBar={handleHideSearchBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<TTSControlProps> = ({ 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) {
|
||||
|
||||
@@ -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<HTMLLIElement | null>(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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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<SidebarState>((set, get) => ({
|
||||
@@ -20,6 +30,10 @@ export const useSidebarStore = create<SidebarState>((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<SidebarState>((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 }),
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user