Add better zoom + controls

This commit is contained in:
Richard Roberson 2025-01-22 13:34:48 -07:00
parent 5edae0e8ae
commit d78be686c1
3 changed files with 70 additions and 22 deletions

View file

@ -25,6 +25,7 @@ export default function PDFViewerPage() {
const [document, setDocument] = useState<{ name: string; data: Blob } | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [zoomLevel, setZoomLevel] = useState<number>(100);
useEffect(() => {
async function loadDocument() {
@ -46,6 +47,9 @@ export default function PDFViewerPage() {
loadDocument();
}, [id, getDocument]);
const handleZoomIn = () => setZoomLevel(prev => Math.min(prev + 10, 200));
const handleZoomOut = () => setZoomLevel(prev => Math.max(prev - 10, 50));
if (error) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
@ -71,20 +75,39 @@ export default function PDFViewerPage() {
<>
<TTSPlayer />
<div className="p-2 pb-2 border-b border-offbase">
<div className="flex items-center justify-between">
<Link
href="/"
onClick={() => {
setText('');
stop();
}}
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
>
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Documents
</Link>
<div className="flex flex-wrap items-center justify-between">
<div className="flex items-center gap-4">
<Link
href="/"
onClick={() => {
setText('');
stop();
}}
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
>
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Documents
</Link>
<div className="bg-offbase px-2 py-0.5 rounded-full flex items-center gap-2">
<button
onClick={handleZoomOut}
className="text-xs hover:text-accent transition-colors"
aria-label="Zoom out"
>
</button>
<span className="text-xs">{zoomLevel}%</span>
<button
onClick={handleZoomIn}
className="text-xs hover:text-accent transition-colors"
aria-label="Zoom in"
>
</button>
</div>
</div>
<h1 className="mr-2 text-md font-semibold text-foreground">
{isLoading ? 'Loading...' : document?.name}
</h1>
@ -95,7 +118,7 @@ export default function PDFViewerPage() {
<PDFSkeleton />
</div>
) : (
<PDFViewer pdfData={document?.data} />
<PDFViewer pdfData={document?.data} zoomLevel={zoomLevel} />
)}
</>
);

View file

@ -11,10 +11,12 @@ import { usePDF } from '@/contexts/PDFContext';
interface PDFViewerProps {
pdfData: Blob | undefined;
zoomLevel: number;
}
export function PDFViewer({ pdfData }: PDFViewerProps) {
export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
const [numPages, setNumPages] = useState<number>();
const [containerWidth, setContainerWidth] = useState<number>(0);
const { setText, currentSentence, stopAndPlayFromIndex, isProcessing } = useTTS();
const [pdfText, setPdfText] = useState('');
const [pdfDataUrl, setPdfDataUrl] = useState<string>();
@ -143,6 +145,29 @@ export function PDFViewer({ pdfData }: PDFViewerProps) {
};
}, [pdfText, currentSentence, highlightPattern, clearHighlights]);
// Add scale calculation function
const calculateScale = (pageWidth: number = 595) => { // 595 is default PDF width in points
const margin = 24; // 24px padding on each side
const targetWidth = containerWidth - margin;
const baseScale = targetWidth / pageWidth;
return baseScale * (zoomLevel / 100);
};
// Add resize observer effect
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver(entries => {
const width = entries[0]?.contentRect.width;
if (width) {
setContainerWidth(width);
}
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumPages(numPages);
}
@ -150,7 +175,7 @@ export function PDFViewer({ pdfData }: PDFViewerProps) {
return (
<div
ref={containerRef}
className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)]"
className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6"
style={{ WebkitTapHighlightColor: 'transparent' }}
>
{loadingError ? (
@ -161,7 +186,7 @@ export function PDFViewer({ pdfData }: PDFViewerProps) {
noData={<PDFSkeleton />}
file={pdfDataUrl}
onLoadSuccess={onDocumentLoadSuccess}
className="flex flex-col items-center"
className="flex flex-col items-center m-0"
>
{Array.from(
new Array(numPages),
@ -178,7 +203,7 @@ export function PDFViewer({ pdfData }: PDFViewerProps) {
renderAnnotationLayer={true}
renderTextLayer={true}
className="shadow-lg"
scale={1.2}
scale={calculateScale()}
/>
</div>
</div>

View file

@ -131,7 +131,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
}, [audioContext]);
// Text preprocessing function to clean and normalize text
const preprocessText = (text: string): string => {
const preprocessSentenceForAudio = (text: string): string => {
return text
// Replace URLs with descriptive text including domain
.replace(/\S*(?:https?:\/\/|www\.)([^\/\s]+)(?:\/\S*)?/gi, '- (link to $1) -')
@ -147,7 +147,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const splitIntoSentences = (text: string): string[] => {
// Preprocess the text before splitting into sentences
const cleanedText = preprocessText(text);
const cleanedText = preprocessSentenceForAudio(text);
const doc = nlp(cleanedText);
return doc.sentences().out('array') as string[];
};
@ -173,7 +173,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
try {
// Only set processing if we need to fetch from API
const cleanedSentence = preprocessText(sentence);
const cleanedSentence = preprocessSentenceForAudio(sentence);
if (!audioCacheRef.current.has(cleanedSentence)) {
setIsProcessing(true);
}