247 lines
6.8 KiB
TypeScript
247 lines
6.8 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Document, Page } from "react-pdf";
|
|
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
|
|
import "react-pdf/dist/esm/Page/TextLayer.css";
|
|
import ArrowCircleLeftIcon from "@mui/icons-material/ArrowCircleLeft";
|
|
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
|
|
import { Box, IconButton } from "@mui/material";
|
|
import type {
|
|
CustomTextRenderer,
|
|
OnGetTextSuccess,
|
|
} from "node_modules/react-pdf/dist/esm/shared/types";
|
|
import { socket } from "../socket";
|
|
import { API_HOST } from "../util/api";
|
|
import { highlightPattern } from "../util/highlighting";
|
|
|
|
interface PDFViewerProps {
|
|
pitchBookId: string;
|
|
currentPage?: number;
|
|
onPageChange?: (page: number) => void;
|
|
highlight: { text: string; page: number }[];
|
|
focusHighlight: { text: string; page: number };
|
|
}
|
|
|
|
export default function PDFViewer({
|
|
pitchBookId,
|
|
currentPage,
|
|
onPageChange,
|
|
highlight = [],
|
|
focusHighlight,
|
|
}: PDFViewerProps) {
|
|
const [numPages, setNumPages] = useState<number | null>(null);
|
|
const [pageNumber, setPageNumber] = useState(currentPage || 1);
|
|
const [containerWidth, setContainerWidth] = useState<number | null>(null);
|
|
const [pdfKey, setPdfKey] = useState(Date.now());
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [posHighlight, setPosHighlight] = useState<string[]>([]);
|
|
const [posHighlightFocus, setPosHighlightFocus] = useState<string[]>([]);
|
|
const [textContent, setTextContent] = useState<
|
|
{ posKey: string; text: string; i: number }[]
|
|
>([]);
|
|
|
|
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
|
setNumPages(numPages);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const updateWidth = () => {
|
|
if (containerRef.current) {
|
|
setContainerWidth(containerRef.current.offsetWidth);
|
|
}
|
|
};
|
|
|
|
updateWidth();
|
|
window.addEventListener("resize", updateWidth);
|
|
return () => window.removeEventListener("resize", updateWidth);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (currentPage && currentPage !== pageNumber) {
|
|
setPageNumber(currentPage);
|
|
}
|
|
}, [currentPage, pageNumber]);
|
|
|
|
useEffect(() => {
|
|
const handleProgress = (data: { id: number; progress: number }) => {
|
|
if (data.id.toString() === pitchBookId && data.progress === 50) {
|
|
setPdfKey(Date.now());
|
|
}
|
|
};
|
|
|
|
socket.on("progress", handleProgress);
|
|
|
|
return () => {
|
|
socket.off("progress", handleProgress);
|
|
};
|
|
}, [pitchBookId]);
|
|
|
|
const handlePageChange = (newPage: number) => {
|
|
setPageNumber(newPage);
|
|
onPageChange?.(newPage);
|
|
};
|
|
|
|
const textRenderer: CustomTextRenderer = useCallback(
|
|
(textItem) => {
|
|
return highlightPattern(
|
|
textItem.str,
|
|
`${textItem.width};${textItem.height};${textItem.transform}`,
|
|
posHighlight,
|
|
posHighlightFocus,
|
|
);
|
|
},
|
|
[posHighlight, posHighlightFocus],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const tmpPos: string[] = [];
|
|
const tmpPosHighlight: string[] = [];
|
|
|
|
if (textContent.length === 0) {
|
|
setPosHighlight([]);
|
|
setPosHighlightFocus([]);
|
|
return;
|
|
}
|
|
const findTextPositions = (searchText: string): number[] => {
|
|
const positions: number[] = [];
|
|
const normalizedSearch = searchText.toLowerCase().trim();
|
|
|
|
textContent.forEach((item, index) => {
|
|
if (item.text.toLowerCase().trim() === normalizedSearch) {
|
|
positions.push(index);
|
|
}
|
|
});
|
|
|
|
if (positions.length === 0) {
|
|
let cumulativeText = '';
|
|
const textBoundaries: { start: number; end: number; index: number }[] = [];
|
|
|
|
textContent.forEach((item, index) => {
|
|
const start = cumulativeText.length;
|
|
cumulativeText += item.text;
|
|
const end = cumulativeText.length;
|
|
textBoundaries.push({ start, end, index });
|
|
});
|
|
|
|
const lowerCumulative = cumulativeText.toLowerCase();
|
|
let searchIndex = lowerCumulative.indexOf(normalizedSearch);
|
|
|
|
while (searchIndex !== -1) {
|
|
const endIndex = searchIndex + normalizedSearch.length;
|
|
|
|
textBoundaries.forEach(boundary => {
|
|
if (
|
|
(boundary.start <= searchIndex && searchIndex < boundary.end) || // Search starts in this item
|
|
(boundary.start < endIndex && endIndex <= boundary.end) || // Search ends in this item
|
|
(searchIndex <= boundary.start && boundary.end <= endIndex) // This item is completely within search
|
|
) {
|
|
if (!positions.includes(boundary.index)) {
|
|
positions.push(boundary.index);
|
|
}
|
|
}
|
|
});
|
|
searchIndex = lowerCumulative.indexOf(normalizedSearch, searchIndex + 1);
|
|
}
|
|
}
|
|
return positions.sort((a, b) => a - b);
|
|
};
|
|
highlight
|
|
.filter(h => h.page === pageNumber)
|
|
.forEach(highlightItem => {
|
|
const positions = findTextPositions(highlightItem.text);
|
|
positions.forEach(pos => {
|
|
if (pos >= 0 && pos < textContent.length) {
|
|
tmpPos.push(textContent[pos].posKey);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (focusHighlight?.page === pageNumber && focusHighlight.text) {
|
|
const positions = findTextPositions(focusHighlight.text);
|
|
|
|
positions.forEach(pos => {
|
|
if (pos >= 0 && pos < textContent.length) {
|
|
tmpPosHighlight.push(textContent[pos].posKey);
|
|
}
|
|
});
|
|
}
|
|
|
|
setPosHighlight([...new Set(tmpPos)]);
|
|
setPosHighlightFocus([...new Set(tmpPosHighlight)]);
|
|
}, [highlight, focusHighlight, pageNumber, textContent]);
|
|
|
|
const onGetTextSuccess: OnGetTextSuccess = useCallback((fullText) => {
|
|
setTextContent(
|
|
fullText.items.map((e, i) => ({
|
|
posKey: `${"width" in e ? e.width : 0};${"height" in e ? e.height : 0};${"transform" in e ? e.transform : ""}`,
|
|
text: "str" in e ? e.str : "",
|
|
i,
|
|
})),
|
|
);
|
|
}, []);
|
|
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
width="100%"
|
|
height="auto"
|
|
>
|
|
<Box
|
|
ref={containerRef}
|
|
sx={{
|
|
width: "100%",
|
|
height: "auto",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Document
|
|
key={pdfKey}
|
|
file={`${API_HOST}/api/pitch_book/${pitchBookId}/download`}
|
|
onLoadSuccess={onDocumentLoadSuccess}
|
|
onLoadError={(error) =>
|
|
console.error("Es gab ein Fehler beim Laden des PDFs:", error)
|
|
}
|
|
onSourceError={(error) => console.error("Ungültige PDF:", error)}
|
|
>
|
|
{containerWidth && (
|
|
<Page
|
|
pageNumber={pageNumber}
|
|
width={containerWidth * 0.98}
|
|
customTextRenderer={textRenderer}
|
|
onGetTextSuccess={onGetTextSuccess}
|
|
/>
|
|
)}
|
|
</Document>
|
|
</Box>
|
|
<Box
|
|
mt={1}
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
gap={1}
|
|
p={1}
|
|
>
|
|
<IconButton
|
|
disabled={pageNumber <= 1}
|
|
onClick={() => handlePageChange(pageNumber - 1)}
|
|
>
|
|
<ArrowCircleLeftIcon fontSize="large" />
|
|
</IconButton>
|
|
<span>
|
|
{pageNumber} / {numPages}
|
|
</span>
|
|
<IconButton
|
|
disabled={pageNumber >= (numPages || 1)}
|
|
onClick={() => handlePageChange(pageNumber + 1)}
|
|
>
|
|
<ArrowCircleRightIcon fontSize="large" />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|