From 6dc3e095a06174ffd86eef825554656278e4fffd Mon Sep 17 00:00:00 2001 From: Enes Salman Date: Tue, 20 Jan 2026 04:42:00 +0300 Subject: [PATCH] feat: implement pdf zoom functionality --- app/file-browser.tsx | 184 ++++++++++++++++++++++++++++-------- app/file/[...path]/page.tsx | 122 ++++++++++++++++++++---- 2 files changed, 250 insertions(+), 56 deletions(-) diff --git a/app/file-browser.tsx b/app/file-browser.tsx index ce07420..04f8a7f 100644 --- a/app/file-browser.tsx +++ b/app/file-browser.tsx @@ -130,6 +130,123 @@ function getCelebritiesForPage(filePath: string, pageNumber: number): { name: st return celebrities.sort((a, b) => b.confidence - a.confidence).filter(celeb => celeb.confidence > 99); } +// Zoom levels for PDF pages +const ZOOM_LEVELS = [0.5, 0.75, 1, 1.25, 1.5, 2] as const; + +function ZoomablePage({ + src, + alt, + pageNumber, + showPageNumber, + children +}: { + src: string; + alt: string; + pageNumber: number; + showPageNumber: boolean; + children?: React.ReactNode; +}) { + const [zoomIndex, setZoomIndex] = useState(2); // Default 100% + const zoom = ZOOM_LEVELS[zoomIndex]; + const zoomPercent = Math.round(zoom * 100); + + const zoomIn = () => setZoomIndex((prev) => Math.min(prev + 1, ZOOM_LEVELS.length - 1)); + const zoomOut = () => setZoomIndex((prev) => Math.max(prev - 1, 0)); + const resetZoom = () => setZoomIndex(2); + + const canZoomIn = zoomIndex < ZOOM_LEVELS.length - 1; + const canZoomOut = zoomIndex > 0; + const isDefaultZoom = zoomIndex === 2; + + // Keyboard support for this specific page when focused or hovered could be tricky with multiple pages. + // We'll stick to button controls for now in the list view to avoid conflicts. + + return ( +
+
+ {/* Controls bar */} +
+ {/* Page number */} + {showPageNumber && ( +
+ Page {pageNumber} +
+ )} + + {/* Zoom controls */} +
+ + + + {zoomPercent}% + + + + + {!isDefaultZoom && ( + + )} +
+
+ + {/* Image container with zoom */} +
1 ? "max-h-[80vh]" : ""}`}> +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {alt} +
+
+
+ {children} +
+ ); +} + // Track in-progress prefetch operations to avoid duplicates const prefetchingSet = new Set(); @@ -560,45 +677,38 @@ function FileModal({ {pages.map((dataUrl, index) => { const pageCelebrities = getCelebritiesForPage(filePath, index + 1); return ( -
e.stopPropagation()}> -
- {pages.length > 1 && ( -
- Page {index + 1} -
- )} - {/* eslint-disable-next-line @next/next/no-img-element */} - {`Page -
- {pageCelebrities.length > 0 && ( -
-
-
- - - +
e.stopPropagation()}> + 1} + > + {pageCelebrities.length > 0 && ( +
+
+
+ + + +
+

Detected in this image:

-

Detected in this image:

-
-
- {pageCelebrities.map((celeb, idx) => ( - - {celeb.name} - ({Math.round(celeb.confidence)}%) - - ))} +
+ {pageCelebrities.map((celeb, idx) => ( + + {celeb.name} + ({Math.round(celeb.confidence)}%) + + ))} +
+
- -
- )} + )} +
); })} diff --git a/app/file/[...path]/page.tsx b/app/file/[...path]/page.tsx index 8b005cc..5f8abb2 100644 --- a/app/file/[...path]/page.tsx +++ b/app/file/[...path]/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { use, useEffect, useState, useRef, useCallback, useMemo } from "react"; -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; +import { CelebrityDisclaimer } from "@/components/celebrity-disclaimer"; import { getPdfPages, setPdfPages } from "@/lib/cache"; -import { useFiles } from "@/lib/files-context"; import { CELEBRITY_DATA } from "@/lib/celebrity-data"; -import { CelebrityDisclaimer } from "@/components/celebrity-disclaimer"; +import { useFiles } from "@/lib/files-context"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { use, useCallback, useEffect, useMemo, useRef, useState } from "react"; const WORKER_URL = process.env.NODE_ENV === "development" ? "http://localhost:8787" @@ -89,33 +89,117 @@ function getCelebritiesForPage(filePath: string, pageNumber: number): { name: st return celebrities.sort((a, b) => b.confidence - a.confidence).filter(celeb => celeb.confidence > 99); } -// Component to display a page with its celebrity info +// Zoom levels for PDF pages +const ZOOM_LEVELS = [0.5, 0.75, 1, 1.25, 1.5, 2] as const; + +// Component to display a page with its celebrity info and zoom controls function PageWithCelebrities({ dataUrl, pageNumber, - filePath + filePath, + showPageNumber = true, }: { dataUrl: string; pageNumber: number; filePath: string; + showPageNumber?: boolean; }) { + const [zoomIndex, setZoomIndex] = useState(2); // Default 100% + const zoom = ZOOM_LEVELS[zoomIndex]; + const zoomPercent = Math.round(zoom * 100); + + const zoomIn = () => setZoomIndex((prev) => Math.min(prev + 1, ZOOM_LEVELS.length - 1)); + const zoomOut = () => setZoomIndex((prev) => Math.max(prev - 1, 0)); + const resetZoom = () => setZoomIndex(2); + + const canZoomIn = zoomIndex < ZOOM_LEVELS.length - 1; + const canZoomOut = zoomIndex > 0; + const isDefaultZoom = zoomIndex === 2; + const celebrities = useMemo(() => getCelebritiesForPage(filePath, pageNumber), [filePath, pageNumber]); return (
- {/* Page number badge */} -
- Page {pageNumber} + {/* Controls bar */} +
+ {/* Page number */} + {showPageNumber && ( +
+ Page {pageNumber} +
+ )} + + {/* Zoom controls */} +
+ + + + {zoomPercent}% + + + + + {!isDefaultZoom && ( + + )} +
+
+ + {/* Image container with zoom */} +
1 ? "max-h-[80vh]" : ""}`}> +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`Page +
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`Page
+ {celebrities.length > 0 && (
@@ -127,8 +211,7 @@ function PageWithCelebrities({

Detected in this image:

- {celebrities - .map((celeb, idx) => ( + {celebrities.map((celeb, idx) => (