This commit is contained in:
saqib mir
2026-06-09 10:50:03 +05:30
5 changed files with 121 additions and 11 deletions
+65 -1
View File
@@ -18,6 +18,7 @@ function App() {
// Settings // Settings
const [zoom, setZoom] = useState<number>(1.0); const [zoom, setZoom] = useState<number>(1.0);
const [activeTool, setActiveTool] = useState<string>('select'); const [activeTool, setActiveTool] = useState<string>('select');
const [highlightColor, setHighlightColor] = useState<string>('#ffeb3b');
const [currentPage, setCurrentPage] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(0);
// States // States
@@ -244,7 +245,65 @@ function App() {
} }
}; };
const handleRotateClick = async (newRotValue?: number) => { const handleSaveEdits = async () => {
if (!selectedDocId || !activeDoc || annotations.length === 0) return;
try {
setIsLoading(true);
const operations: any[] = annotations.map((anno) => {
if (anno.type === 'highlight') {
return {
id: anno.id,
type: 'highlight',
pageIndex: anno.pageIndex || 0,
data: {
quadPoints: [{
x1: anno.bbox.x, y1: anno.bbox.y,
x2: anno.bbox.x + anno.bbox.width, y2: anno.bbox.y,
x3: anno.bbox.x, y3: anno.bbox.y + anno.bbox.height,
x4: anno.bbox.x + anno.bbox.width, y4: anno.bbox.y + anno.bbox.height,
}],
color: anno.color || '#ffeb3b',
opacity: 0.5,
author: anno.author,
content: anno.content
}
};
} else if (anno.type === 'ink') {
return {
id: anno.id,
type: 'freehand',
pageIndex: anno.pageIndex || 0,
data: {
paths: anno.paths || [],
color: anno.color || '#3b82f6',
thickness: 2.0
}
};
}
return null;
}).filter(Boolean);
if (operations.length === 0) {
setIsLoading(false);
return;
}
const result = await gatewayService.applyEdits(selectedDocId, operations);
if (result.success) {
setAnnotations([]);
const docs = await gatewayService.listDocuments();
setDocuments(docs);
setSelectedDocId(result.newDocumentId);
}
} catch (err) {
console.error('Failed to save edits:', err);
alert('Failed to save edits. Make sure the gateway is connected.');
} finally {
setIsLoading(false);
}
};
const handleRotateClick = async () => {
if (!selectedDocId || !activeDoc) return; if (!selectedDocId || !activeDoc) return;
const pageIndex = currentPage; const pageIndex = currentPage;
try { try {
@@ -365,6 +424,8 @@ function App() {
onRotationChange={handleRotateClick} onRotationChange={handleRotateClick}
activeTool={activeTool} activeTool={activeTool}
onActiveToolChange={setActiveTool} onActiveToolChange={setActiveTool}
highlightColor={highlightColor}
onHighlightColorChange={setHighlightColor}
currentPage={currentPage} currentPage={currentPage}
totalPages={activeDoc?.totalPages || 1} totalPages={activeDoc?.totalPages || 1}
onUploadStart={handleUploadStart} onUploadStart={handleUploadStart}
@@ -374,6 +435,8 @@ function App() {
searchCurrentMatch={searchCurrentMatch} searchCurrentMatch={searchCurrentMatch}
onSearchNext={handleSearchNext} onSearchNext={handleSearchNext}
onSearchPrev={handleSearchPrev} onSearchPrev={handleSearchPrev}
onSaveEdits={handleSaveEdits}
hasAnnotations={annotations.length > 0}
/> />
<div className="flex-1 w-full flex overflow-hidden"> <div className="flex-1 w-full flex overflow-hidden">
@@ -412,6 +475,7 @@ function App() {
zoom={zoom} zoom={zoom}
pagesInfo={activeDoc.pages} pagesInfo={activeDoc.pages}
activeTool={activeTool} activeTool={activeTool}
highlightColor={highlightColor}
annotations={annotations} annotations={annotations}
searchQuery={searchQuery} searchQuery={searchQuery}
searchResults={searchResults} searchResults={searchResults}
+36
View File
@@ -8,6 +8,8 @@ interface ToolbarProps {
onRotationChange: (rot: number) => void; onRotationChange: (rot: number) => void;
activeTool: string; activeTool: string;
onActiveToolChange: (tool: string) => void; onActiveToolChange: (tool: string) => void;
highlightColor: string;
onHighlightColorChange: (color: string) => void;
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
onUploadStart?: (file: File) => void; onUploadStart?: (file: File) => void;
@@ -19,6 +21,8 @@ interface ToolbarProps {
searchCurrentMatch: number; searchCurrentMatch: number;
onSearchNext: () => void; onSearchNext: () => void;
onSearchPrev: () => void; onSearchPrev: () => void;
onSaveEdits?: () => void;
hasAnnotations?: boolean;
} }
export const Toolbar: React.FC<ToolbarProps> = ({ export const Toolbar: React.FC<ToolbarProps> = ({
@@ -28,6 +32,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
onRotationChange, onRotationChange,
activeTool, activeTool,
onActiveToolChange, onActiveToolChange,
highlightColor,
onHighlightColorChange,
currentPage, currentPage,
totalPages, totalPages,
onUploadStart, onUploadStart,
@@ -37,6 +43,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
searchCurrentMatch, searchCurrentMatch,
onSearchNext, onSearchNext,
onSearchPrev, onSearchPrev,
onSaveEdits,
hasAnnotations,
}) => { }) => {
const handleZoomPercentSelect = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleZoomPercentSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
@@ -159,6 +167,20 @@ export const Toolbar: React.FC<ToolbarProps> = ({
> >
Highlight Highlight
</button> </button>
{activeTool === 'highlight' && (
<div className="flex items-center gap-1 ml-1 pl-2 border-l border-slate-700/50">
{['#ffeb3b', '#4caf50', '#2196f3', '#e91e63', '#ff9800'].map((color) => (
<button
key={color}
className={`w-5 h-5 rounded-full border-2 transition-transform ${highlightColor === color ? 'border-white scale-110' : 'border-transparent hover:scale-105'}`}
style={{ backgroundColor: color }}
onClick={() => onHighlightColorChange(color)}
title={`Set color to ${color}`}
/>
))}
</div>
)}
<button <button
onClick={() => onActiveToolChange('draw')} onClick={() => onActiveToolChange('draw')}
@@ -194,6 +216,20 @@ export const Toolbar: React.FC<ToolbarProps> = ({
onPrev={onSearchPrev} onPrev={onSearchPrev}
/> />
{onSaveEdits && (
<button
onClick={onSaveEdits}
className="save-btn font-semibold bg-indigo-600 text-white px-3 py-1.5 rounded-md text-sm hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
disabled={!hasAnnotations}
title="Save Annotations to PDF"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
Save Edits
</button>
)}
<label className="upload-btn"> <label className="upload-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
+6 -1
View File
@@ -4,6 +4,7 @@ import type { Rect } from '../lib/coordinateMapping';
export interface Annotation { export interface Annotation {
id: string; id: string;
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink'; type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink';
pageIndex: number;
bbox: Rect; bbox: Rect;
color?: string; color?: string;
author: string; author: string;
@@ -33,7 +34,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
return ( return (
<div <div
className="annotation-layer" className="annotation-layer"
style={{ width: `${width}px`, height: `${height}px` }} style={{ width: `${width}px`, height: `${height}px`, pointerEvents: 'none' }}
> >
{annotations {annotations
.filter((anno) => anno.pageIndex === undefined || anno.pageIndex === pageIndex) .filter((anno) => anno.pageIndex === undefined || anno.pageIndex === pageIndex)
@@ -64,6 +65,10 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
top: `${scaledBbox.y}px`, top: `${scaledBbox.y}px`,
width: `${scaledBbox.width}px`, width: `${scaledBbox.width}px`,
height: `${scaledBbox.height}px`, height: `${scaledBbox.height}px`,
backgroundColor: anno.type === 'highlight' ? (anno.color || '#ffeb3b') : undefined,
opacity: anno.type === 'highlight' ? 0.4 : undefined,
mixBlendMode: anno.type === 'highlight' ? 'multiply' : undefined,
pointerEvents: 'auto',
}} }}
title={tooltipText} title={tooltipText}
> >
+3 -2
View File
@@ -34,7 +34,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
const handlePointerDown = (e: React.PointerEvent) => { const handlePointerDown = (e: React.PointerEvent) => {
if (activeTool !== 'draw') return; if (activeTool !== 'draw') return;
setIsDrawing(true); setIsDrawing(true);
e.target.setPointerCapture?.(e.pointerId); (e.target as Element).setPointerCapture?.(e.pointerId);
setCurrentPath([getCoordinates(e)]); setCurrentPath([getCoordinates(e)]);
}; };
@@ -46,7 +46,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
const handlePointerUp = (e: React.PointerEvent) => { const handlePointerUp = (e: React.PointerEvent) => {
if (!isDrawing || activeTool !== 'draw') return; if (!isDrawing || activeTool !== 'draw') return;
setIsDrawing(false); setIsDrawing(false);
e.target.releasePointerCapture?.(e.pointerId); (e.target as Element).releasePointerCapture?.(e.pointerId);
if (currentPath.length > 1) { if (currentPath.length > 1) {
// Calculate bounding box // Calculate bounding box
@@ -60,6 +60,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
const newAnno: Annotation = { const newAnno: Annotation = {
id: `anno_${Math.random().toString(36).substring(2, 11)}`, id: `anno_${Math.random().toString(36).substring(2, 11)}`,
type: 'ink', type: 'ink',
pageIndex,
bbox: { bbox: {
x: minX, x: minX,
y: minY, y: minY,
+11 -7
View File
@@ -17,6 +17,7 @@ interface PDFViewerProps {
zoom: number; zoom: number;
pagesInfo?: PageInfo[]; pagesInfo?: PageInfo[];
activeTool: string; activeTool: string;
highlightColor: string;
annotations: Annotation[]; annotations: Annotation[];
searchQuery?: string; searchQuery?: string;
searchResults?: SearchResult[]; searchResults?: SearchResult[];
@@ -46,6 +47,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
zoom, zoom,
pagesInfo, pagesInfo,
activeTool, activeTool,
highlightColor,
annotations, annotations,
searchQuery, searchQuery,
searchResults, searchResults,
@@ -201,10 +203,15 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
}; };
}, [visiblePages, documentId, zoom, renderedPages]); }, [visiblePages, documentId, zoom, renderedPages]);
const verifiedPages = useRef<Set<number>>(new Set());
useEffect(() => { useEffect(() => {
const verifyPageModel = async () => { const verifyPageModel = async () => {
if (visiblePages.length > 0 && documentId && !renderedPages[visiblePages[0].index + '_verified']) { if (visiblePages.length > 0 && documentId) {
const pageIndex = visiblePages[0].index; const pageIndex = visiblePages[0].index;
if (verifiedPages.current.has(pageIndex)) return;
verifiedPages.current.add(pageIndex);
try { try {
const model = await gatewayService.getPageModel(documentId, pageIndex); const model = await gatewayService.getPageModel(documentId, pageIndex);
console.log(`--- Verification for Page ${pageIndex} ---`); console.log(`--- Verification for Page ${pageIndex} ---`);
@@ -217,13 +224,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
}); });
}); });
}); });
setRenderedPages((prev) => {
const next = [...prev];
next[pageIndex + '_verified' as any] = 'true';
return next;
});
} catch (e) { } catch (e) {
// ignore or log verifiedPages.current.delete(pageIndex);
} }
} }
}; };
@@ -235,12 +237,14 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
const newAnno: Annotation = { const newAnno: Annotation = {
id: generateUniqueId(), id: generateUniqueId(),
type: 'highlight', type: 'highlight',
pageIndex,
bbox: { bbox: {
x: bbox.x / zoom, x: bbox.x / zoom,
y: bbox.y / zoom, y: bbox.y / zoom,
width: bbox.width / zoom, width: bbox.width / zoom,
height: bbox.height / zoom, height: bbox.height / zoom,
}, },
color: highlightColor,
author: 'Current User', author: 'Current User',
content: text, content: text,
pageIndex: pageIndex, pageIndex: pageIndex,