Archived
109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18n-lite";
|
|
import { useAnnotations } from "@/context/AnnotationsContext";
|
|
import type { MarkerEntry } from "@/types";
|
|
import {
|
|
CollapsibleMarkerPassage,
|
|
CollapsibleMarkerThought,
|
|
} from "@/components/annotations/CollapsibleMarkerText";
|
|
import { MarkerPassageActions } from "@/components/annotations/MarkerPassageActions";
|
|
|
|
interface MarkerThreadsViewProps {
|
|
ebookIdFilter?: string;
|
|
onGoToPassage: (ebookId: number, epubCfi: string) => void;
|
|
}
|
|
|
|
export function MarkerThreadsView({
|
|
ebookIdFilter,
|
|
onGoToPassage,
|
|
}: MarkerThreadsViewProps) {
|
|
const { t } = useTranslation();
|
|
const { markersByBook, loadBookmarks, removeBookmark, state } = useAnnotations();
|
|
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
|
|
|
|
useEffect(() => {
|
|
void loadBookmarks(ebookIdFilter);
|
|
}, [loadBookmarks, ebookIdFilter]);
|
|
|
|
const groups = useMemo(() => {
|
|
if (!ebookIdFilter) return markersByBook;
|
|
const id = Number(ebookIdFilter);
|
|
return markersByBook.filter((g) => g.ebookId === id);
|
|
}, [markersByBook, ebookIdFilter]);
|
|
|
|
if (state.bookmarksLoading) {
|
|
return <div className="annotations-loading">{t("annotations.loading")}</div>;
|
|
}
|
|
|
|
if (groups.length === 0) {
|
|
return (
|
|
<div className="annotations-empty">
|
|
<p>{t("annotations.empty")}</p>
|
|
<p style={{ fontSize: 14, color: "#6b7280", marginTop: 8 }}>{t("annotations.selectTextHint")}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const toggleBook = (ebookId: number) => {
|
|
setCollapsed((prev) => ({ ...prev, [ebookId]: !prev[ebookId] }));
|
|
};
|
|
|
|
return (
|
|
<div className="marker-books-list">
|
|
{groups.map((group) => {
|
|
const isCollapsed = collapsed[group.ebookId] ?? false;
|
|
return (
|
|
<section key={group.ebookId} className="marker-book-group">
|
|
<button
|
|
type="button"
|
|
className="marker-book-header"
|
|
onClick={() => toggleBook(group.ebookId)}
|
|
>
|
|
<span className="marker-book-title">{group.ebookTitle}</span>
|
|
<span className="marker-book-count">
|
|
{t("annotations.markerCount", { count: String(group.markers.length) })}
|
|
</span>
|
|
<span className="marker-book-chevron">{isCollapsed ? "▸" : "▾"}</span>
|
|
</button>
|
|
{!isCollapsed && (
|
|
<ul className="marker-thread-list">
|
|
{group.markers.map((m: MarkerEntry) => (
|
|
<li key={m.id} className="marker-thread">
|
|
<div className="marker-thread-meta">
|
|
{m.chapter_title ? (
|
|
<span className="marker-thread-chapter">{m.chapter_title}</span>
|
|
) : (
|
|
<span className="marker-thread-chapter">
|
|
{t("annotations.chapterIndex", { index: String(m.chapter_index + 1) })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{m.location_text && (
|
|
<CollapsibleMarkerPassage
|
|
text={m.location_text}
|
|
highlightColor={m.highlight_color}
|
|
/>
|
|
)}
|
|
{m.content ? (
|
|
<CollapsibleMarkerThought
|
|
text={m.content}
|
|
className="marker-thread-thought marker-thread-reply"
|
|
/>
|
|
) : (
|
|
<span className="annotation-kind-badge bookmark-badge">{t("annotations.bookmarkOnly")}</span>
|
|
)}
|
|
<MarkerPassageActions
|
|
onGoToPassage={() => onGoToPassage(m.ebook_id, m.epub_cfi)}
|
|
onDelete={() => removeBookmark(m.id)}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|