@@ -869,5 +869,8 @@
|
||||
"Search results for '{{term}}'": "نتائج البحث عن «{{term}}»",
|
||||
"Close Search": "إغلاق البحث",
|
||||
"Previous Result": "النتيجة السابقة",
|
||||
"Next Result": "النتيجة التالية"
|
||||
"Next Result": "النتيجة التالية",
|
||||
"Bookmarks": "الإشارات المرجعية",
|
||||
"Annotations": "التعليقات التوضيحية",
|
||||
"Show Results": "عرض النتائج"
|
||||
}
|
||||
|
||||
@@ -829,5 +829,8 @@
|
||||
"Search results for '{{term}}'": "'{{term}}' এর ফলাফল",
|
||||
"Close Search": "অনুসন্ধান বন্ধ করুন",
|
||||
"Previous Result": "আগের ফলাফল",
|
||||
"Next Result": "পরবর্তী ফলাফল"
|
||||
"Next Result": "পরবর্তী ফলাফল",
|
||||
"Bookmarks": "বুকমার্ক",
|
||||
"Annotations": "টীকা",
|
||||
"Show Results": "ফলাফল দেখুন"
|
||||
}
|
||||
|
||||
@@ -819,5 +819,8 @@
|
||||
"Search results for '{{term}}'": "'{{term}}'ཡི་འབྲས་བུ།",
|
||||
"Close Search": "འཚོལ་བཤེར་སྒོ་རྒྱག",
|
||||
"Previous Result": "སྔོན་མའི་འབྲས་བུ།",
|
||||
"Next Result": "རྗེས་མའི་འབྲས་བུ།"
|
||||
"Next Result": "རྗེས་མའི་འབྲས་བུ།",
|
||||
"Bookmarks": "དཔེ་རྟགས།",
|
||||
"Annotations": "མཆན།",
|
||||
"Show Results": "འབྲས་བུ་སྟོན།"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -829,5 +829,8 @@
|
||||
"Search results for '{{term}}'": "Αποτελέσματα για '{{term}}'",
|
||||
"Close Search": "Κλείσιμο αναζήτησης",
|
||||
"Previous Result": "Προηγούμενο αποτέλεσμα",
|
||||
"Next Result": "Επόμενο αποτέλεσμα"
|
||||
"Next Result": "Επόμενο αποτέλεσμα",
|
||||
"Bookmarks": "Σελιδοδείκτες",
|
||||
"Annotations": "Σχολιασμοί",
|
||||
"Show Results": "Εμφάνιση αποτελεσμάτων"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -829,5 +829,8 @@
|
||||
"Search results for '{{term}}'": "نتایج برای «{{term}}»",
|
||||
"Close Search": "بستن جستجو",
|
||||
"Previous Result": "نتیجه قبلی",
|
||||
"Next Result": "نتیجه بعدی"
|
||||
"Next Result": "نتیجه بعدی",
|
||||
"Bookmarks": "نشانکها",
|
||||
"Annotations": "یادداشتها",
|
||||
"Show Results": "نمایش نتایج"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -829,5 +829,8 @@
|
||||
"Search results for '{{term}}'": "'{{term}}' के परिणाम",
|
||||
"Close Search": "खोज बंद करें",
|
||||
"Previous Result": "पिछला परिणाम",
|
||||
"Next Result": "अगला परिणाम"
|
||||
"Next Result": "अगला परिणाम",
|
||||
"Bookmarks": "बुकमार्क",
|
||||
"Annotations": "टिप्पणियाँ",
|
||||
"Show Results": "परिणाम दिखाएं"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -819,5 +819,8 @@
|
||||
"Search results for '{{term}}'": "「{{term}}」を含む結果",
|
||||
"Close Search": "検索を閉じる",
|
||||
"Previous Result": "前の結果",
|
||||
"Next Result": "次の結果"
|
||||
"Next Result": "次の結果",
|
||||
"Bookmarks": "ブックマーク",
|
||||
"Annotations": "注釈",
|
||||
"Show Results": "結果を表示"
|
||||
}
|
||||
|
||||
@@ -819,5 +819,8 @@
|
||||
"Search results for '{{term}}'": "'{{term}}' 검색 결과",
|
||||
"Close Search": "검색 닫기",
|
||||
"Previous Result": "이전 결과",
|
||||
"Next Result": "다음 결과"
|
||||
"Next Result": "다음 결과",
|
||||
"Bookmarks": "북마크",
|
||||
"Annotations": "주석",
|
||||
"Show Results": "결과 표시"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -849,5 +849,8 @@
|
||||
"Search results for '{{term}}'": "Результаты для «{{term}}»",
|
||||
"Close Search": "Закрыть поиск",
|
||||
"Previous Result": "Предыдущий результат",
|
||||
"Next Result": "Следующий результат"
|
||||
"Next Result": "Следующий результат",
|
||||
"Bookmarks": "Закладки",
|
||||
"Annotations": "Аннотации",
|
||||
"Show Results": "Показать результаты"
|
||||
}
|
||||
|
||||
@@ -829,5 +829,8 @@
|
||||
"Search results for '{{term}}'": "'{{term}}' සඳහා ප්රතිඵල",
|
||||
"Close Search": "සෙවුම වසන්න",
|
||||
"Previous Result": "පෙර ප්රතිඵලය",
|
||||
"Next Result": "ඊළඟ ප්රතිඵලය"
|
||||
"Next Result": "ඊළඟ ප්රතිඵලය",
|
||||
"Bookmarks": "පොත් සලකුණු",
|
||||
"Annotations": "අනුසටහන්",
|
||||
"Show Results": "ප්රතිඵල පෙන්වන්න"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -829,5 +829,8 @@
|
||||
"Search results for '{{term}}'": "'{{term}}' க்கான முடிவுகள்",
|
||||
"Close Search": "தேடலை மூடு",
|
||||
"Previous Result": "முந்தைய முடிவு",
|
||||
"Next Result": "அடுத்த முடிவு"
|
||||
"Next Result": "அடுத்த முடிவு",
|
||||
"Bookmarks": "புக்மார்க்குகள்",
|
||||
"Annotations": "சிறுகுறிப்புகள்",
|
||||
"Show Results": "முடிவுகளைக் காட்டு"
|
||||
}
|
||||
|
||||
@@ -819,5 +819,8 @@
|
||||
"Search results for '{{term}}'": "ผลลัพธ์สำหรับ '{{term}}'",
|
||||
"Close Search": "ปิดการค้นหา",
|
||||
"Previous Result": "ผลลัพธ์ก่อนหน้า",
|
||||
"Next Result": "ผลลัพธ์ถัดไป"
|
||||
"Next Result": "ผลลัพธ์ถัดไป",
|
||||
"Bookmarks": "บุ๊กมาร์ก",
|
||||
"Annotations": "คำอธิบายประกอบ",
|
||||
"Show Results": "แสดงผลลัพธ์"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -849,5 +849,8 @@
|
||||
"Search results for '{{term}}'": "Результати для «{{term}}»",
|
||||
"Close Search": "Закрити пошук",
|
||||
"Previous Result": "Попередній результат",
|
||||
"Next Result": "Наступний результат"
|
||||
"Next Result": "Наступний результат",
|
||||
"Bookmarks": "Закладки",
|
||||
"Annotations": "Анотації",
|
||||
"Show Results": "Показати результати"
|
||||
}
|
||||
|
||||
@@ -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ả"
|
||||
}
|
||||
|
||||
@@ -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": "显示结果"
|
||||
}
|
||||
|
||||
@@ -819,5 +819,8 @@
|
||||
"Search results for '{{term}}'": "包含「{{term}}」的結果",
|
||||
"Close Search": "關閉搜尋",
|
||||
"Previous Result": "上一個結果",
|
||||
"Next Result": "下一個結果"
|
||||
"Next Result": "下一個結果",
|
||||
"Bookmarks": "書籤",
|
||||
"Annotations": "註解",
|
||||
"Show Results": "顯示結果"
|
||||
}
|
||||
|
||||
@@ -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<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
)}
|
||||
<Annotator bookKey={bookKey} />
|
||||
<SearchResultsNav bookKey={bookKey} gridInsets={gridInsets} />
|
||||
<BooknotesNav bookKey={bookKey} gridInsets={gridInsets} toc={bookDoc.toc || []} />
|
||||
<FootnotePopup bookKey={bookKey} bookDoc={bookDoc} />
|
||||
<FooterBar
|
||||
bookKey={bookKey}
|
||||
|
||||
@@ -62,7 +62,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
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 =
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -42,7 +42,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
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),
|
||||
|
||||
@@ -20,9 +20,10 @@ import TextEditor, { TextEditorRef } from '@/components/TextEditor';
|
||||
interface BooknoteItemProps {
|
||||
bookKey: string;
|
||||
item: BookNote;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, onClick }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
@@ -43,6 +44,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
event.preventDefault();
|
||||
eventDispatcher.dispatch('navigate', { bookKey, cfi });
|
||||
|
||||
onClick?.();
|
||||
getView(bookKey)?.goTo(cfi);
|
||||
if (note) {
|
||||
setNotebookVisible(true);
|
||||
|
||||
@@ -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 (
|
||||
<div className='rounded pt-2'>
|
||||
<ul role='tree' className='px-2'>
|
||||
@@ -47,7 +57,12 @@ const BooknoteView: React.FC<{
|
||||
<h3 className='content font-size-base line-clamp-1 font-normal'>{group.label}</h3>
|
||||
<ul>
|
||||
{group.booknotes.map((item, index) => (
|
||||
<BooknoteItem key={`${index}-${item.cfi}`} bookKey={bookKey} item={item} />
|
||||
<BooknoteItem
|
||||
key={`${index}-${item.cfi}`}
|
||||
bookKey={bookKey}
|
||||
item={item}
|
||||
onClick={handleBrowseBookNotes}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -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<BooknotesNavProps> = ({ 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 (
|
||||
<ContentNavBar
|
||||
bookKey={bookKey}
|
||||
gridInsets={gridInsets}
|
||||
title={getShowResultsTitle()}
|
||||
section={currentSection}
|
||||
hasPrevious={hasPreviousPage}
|
||||
hasNext={hasNextPage}
|
||||
previousTitle={_('Previous')}
|
||||
nextTitle={_('Next')}
|
||||
showResultsTitle={getShowResultsTitle()}
|
||||
closeTitle={_('Close')}
|
||||
onShowResults={handleShowResults}
|
||||
onClose={handleClose}
|
||||
onPrevious={handlePrevious}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BooknotesNav;
|
||||
@@ -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<ContentNavBarProps> = ({
|
||||
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 (
|
||||
<div
|
||||
className='results-nav-bar 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: Info */}
|
||||
<div className='bg-base-200 pointer-events-auto flex items-center justify-between rounded-xl px-4 py-1 shadow-lg sm:gap-6'>
|
||||
{showListButton && onShowResults ? (
|
||||
<button
|
||||
title={showResultsTitle || _('Show Results')}
|
||||
onClick={onShowResults}
|
||||
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='w-8' />
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col items-center px-2'>
|
||||
<span className='line-clamp-1 text-sm font-medium'>{title}</span>
|
||||
{section && showSection && (
|
||||
<span className='text-base-content/70 line-clamp-1 text-xs'>{section}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
title={closeTitle || _('Close')}
|
||||
onClick={onClose}
|
||||
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 pointer-events-auto flex items-center justify-between gap-6 rounded-xl px-4 py-0 shadow-lg'>
|
||||
<button
|
||||
title={previousTitle || _('Previous')}
|
||||
onClick={onPrevious}
|
||||
disabled={!hasPrevious}
|
||||
className={clsx(
|
||||
'btn btn-ghost flex h-auto min-h-0 flex-1 flex-col items-center gap-0 p-1 hover:bg-transparent',
|
||||
!hasPrevious && 'opacity-40 disabled:bg-transparent',
|
||||
)}
|
||||
>
|
||||
<HiArrowLongLeft size={iconSize18} className='text-base-content' />
|
||||
<span className='text-sm font-medium'>{previousLabel || _('Previous')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
title={nextTitle || _('Next')}
|
||||
onClick={onNext}
|
||||
disabled={!hasNext}
|
||||
className={clsx(
|
||||
'btn btn-ghost flex h-auto min-h-0 flex-1 flex-col items-center gap-0 p-1 hover:bg-transparent',
|
||||
!hasNext && 'opacity-40 disabled:bg-transparent',
|
||||
)}
|
||||
>
|
||||
<HiArrowLongRight size={iconSize18} className='text-base-content' />
|
||||
<span className='text-sm font-medium'>{nextLabel || _('Next')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentNavBar;
|
||||
@@ -32,7 +32,9 @@ const SearchBar: React.FC<SearchBarProps> = ({ 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<HTMLInputElement>(null);
|
||||
const inputFocusedRef = useRef(false);
|
||||
@@ -50,7 +52,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ 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<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setSearchTerm(value);
|
||||
setSearchTerm(bookKey, value);
|
||||
handleSearchTermChange(value);
|
||||
queuedSearchTerm.current = value;
|
||||
};
|
||||
@@ -125,7 +127,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ 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<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
||||
}
|
||||
} else {
|
||||
results.push(result);
|
||||
setSearchResults([...results]);
|
||||
setSearchResults(bookKey, [...results]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +159,9 @@ const SearchBar: React.FC<SearchBarProps> = ({ 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(
|
||||
|
||||
@@ -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<SearchResultsNavProps> = ({ 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 (
|
||||
<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>
|
||||
<ContentNavBar
|
||||
bookKey={bookKey}
|
||||
gridInsets={gridInsets}
|
||||
title={_("Search results for '{{term}}'", { term: searchTerm })}
|
||||
section={currentSection}
|
||||
hasPrevious={hasPreviousPage}
|
||||
hasNext={hasNextPage}
|
||||
previousTitle={_('Previous Result')}
|
||||
nextTitle={_('Next Result')}
|
||||
showResultsTitle={_('Show Search Results')}
|
||||
closeTitle={_('Close Search')}
|
||||
onShowResults={handleShowResults}
|
||||
onClose={handleCloseSearch}
|
||||
onPrevious={handlePreviousResult}
|
||||
onNext={handleNextResult}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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<string, SearchNavState>;
|
||||
booknotesNavStates: Record<string, BooknotesNavState>;
|
||||
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<SidebarState>((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<SidebarState>((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 },
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user