Response viewer for PDF (#48)

This PR adds a response viewer for PDF files using `react-pdf`
This commit is contained in:
Gregory Schier
2024-06-10 08:57:08 -07:00
committed by GitHub
parent 456d3aaf52
commit 7cd8ac3b21
9 changed files with 750 additions and 29 deletions

View File

@@ -23,6 +23,7 @@ import { ResponseHeaders } from './ResponseHeaders';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
@@ -164,6 +165,8 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
<WebPageViewer response={activeResponse} />
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : contentType?.match(/pdf/) ? (
<PdfViewer response={activeResponse} />
) : (
<TextViewer
className="-mr-2" // Pull to the right

View File

@@ -0,0 +1,3 @@
.react-pdf__Document * {
@apply select-text;
}

View File

@@ -0,0 +1,61 @@
import useResizeObserver from '@react-hook/resize-observer';
import 'react-pdf/dist/Page/TextLayer.css';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import { convertFileSrc } from '@tauri-apps/api/core';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import React, { useRef, useState } from 'react';
import { Document, Page } from 'react-pdf';
import { useDebouncedState } from '../../hooks/useDebouncedState';
import type { HttpResponse } from '../../lib/models';
import './PdfViewer.css';
interface Props {
response: HttpResponse;
}
const options = {
cMapUrl: '/cmaps/',
standardFontDataUrl: '/standard_fonts/',
};
export function PdfViewer({ response }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useDebouncedState<number>(0, 100);
const [numPages, setNumPages] = useState<number>();
useResizeObserver(containerRef.current ?? null, (v) => {
setContainerWidth(v.contentRect.width);
});
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
setNumPages(nextNumPages);
};
if (response.bodyPath === null) {
return <div>Empty response body</div>;
}
const src = convertFileSrc(response.bodyPath);
return (
<div ref={containerRef} className="w-full h-full overflow-y-auto">
<Document
file={src}
options={options}
onLoadSuccess={onDocumentLoadSuccess}
externalLinkTarget="_blank"
externalLinkRel="noopener noreferrer"
>
{Array.from(new Array(numPages), (_, index) => (
<Page
className="mb-6 select-all"
renderTextLayer
renderAnnotationLayer
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth}
/>
))}
</Document>
</div>
);
}

View File

@@ -35,21 +35,14 @@ export function TextViewer({ response, pretty, className }: Props) {
);
const contentType = useContentTypeFromHeaders(response.headers);
const rawBody = useResponseBodyText(response) ?? '';
const rawBody = useResponseBodyText(response) ?? null;
const isSearching = filterText != null;
const formattedBody =
pretty && contentType?.includes('json')
? tryFormatJson(rawBody)
: pretty && contentType?.includes('xml')
? tryFormatXml(rawBody)
: rawBody;
const filteredResponse = useFilterResponse({
filter: debouncedFilterText ?? '',
responseId: response.id,
});
const body = isSearching && filterText?.length > 0 ? filteredResponse : formattedBody;
const toggleSearch = useCallback(() => {
if (isSearching) {
setFilterText(null);
@@ -104,6 +97,18 @@ export function TextViewer({ response, pretty, className }: Props) {
return result;
}, [canFilter, filterText, isJson, isSearching, response.id, setFilterText, toggleSearch]);
if (rawBody == null) {
return 'bad';
}
const formattedBody =
pretty && contentType?.includes('json')
? tryFormatJson(rawBody)
: pretty && contentType?.includes('xml')
? tryFormatXml(rawBody)
: rawBody;
const body = isSearching && filterText?.length > 0 ? filteredResponse : formattedBody;
return (
<Editor
readOnly