Add better zoom + controls
This commit is contained in:
parent
5edae0e8ae
commit
d78be686c1
3 changed files with 70 additions and 22 deletions
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue