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
const [zoom, setZoom] = useState<number>(1.0);
const [activeTool, setActiveTool] = useState<string>('select');
const [highlightColor, setHighlightColor] = useState<string>('#ffeb3b');
const [currentPage, setCurrentPage] = useState<number>(0);
// 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;
const pageIndex = currentPage;
try {
@@ -365,6 +424,8 @@ function App() {
onRotationChange={handleRotateClick}
activeTool={activeTool}
onActiveToolChange={setActiveTool}
highlightColor={highlightColor}
onHighlightColorChange={setHighlightColor}
currentPage={currentPage}
totalPages={activeDoc?.totalPages || 1}
onUploadStart={handleUploadStart}
@@ -374,6 +435,8 @@ function App() {
searchCurrentMatch={searchCurrentMatch}
onSearchNext={handleSearchNext}
onSearchPrev={handleSearchPrev}
onSaveEdits={handleSaveEdits}
hasAnnotations={annotations.length > 0}
/>
<div className="flex-1 w-full flex overflow-hidden">
@@ -412,6 +475,7 @@ function App() {
zoom={zoom}
pagesInfo={activeDoc.pages}
activeTool={activeTool}
highlightColor={highlightColor}
annotations={annotations}
searchQuery={searchQuery}
searchResults={searchResults}
+36
View File
@@ -8,6 +8,8 @@ interface ToolbarProps {
onRotationChange: (rot: number) => void;
activeTool: string;
onActiveToolChange: (tool: string) => void;
highlightColor: string;
onHighlightColorChange: (color: string) => void;
currentPage: number;
totalPages: number;
onUploadStart?: (file: File) => void;
@@ -19,6 +21,8 @@ interface ToolbarProps {
searchCurrentMatch: number;
onSearchNext: () => void;
onSearchPrev: () => void;
onSaveEdits?: () => void;
hasAnnotations?: boolean;
}
export const Toolbar: React.FC<ToolbarProps> = ({
@@ -28,6 +32,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
onRotationChange,
activeTool,
onActiveToolChange,
highlightColor,
onHighlightColorChange,
currentPage,
totalPages,
onUploadStart,
@@ -37,6 +43,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
searchCurrentMatch,
onSearchNext,
onSearchPrev,
onSaveEdits,
hasAnnotations,
}) => {
const handleZoomPercentSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
@@ -159,6 +167,20 @@ export const Toolbar: React.FC<ToolbarProps> = ({
>
Highlight
</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
onClick={() => onActiveToolChange('draw')}
@@ -194,6 +216,20 @@ export const Toolbar: React.FC<ToolbarProps> = ({
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">
<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" />
+6 -1
View File
@@ -4,6 +4,7 @@ import type { Rect } from '../lib/coordinateMapping';
export interface Annotation {
id: string;
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink';
pageIndex: number;
bbox: Rect;
color?: string;
author: string;
@@ -33,7 +34,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
return (
<div
className="annotation-layer"
style={{ width: `${width}px`, height: `${height}px` }}
style={{ width: `${width}px`, height: `${height}px`, pointerEvents: 'none' }}
>
{annotations
.filter((anno) => anno.pageIndex === undefined || anno.pageIndex === pageIndex)
@@ -64,6 +65,10 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
top: `${scaledBbox.y}px`,
width: `${scaledBbox.width}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}
>
+3 -2
View File
@@ -34,7 +34,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
const handlePointerDown = (e: React.PointerEvent) => {
if (activeTool !== 'draw') return;
setIsDrawing(true);
e.target.setPointerCapture?.(e.pointerId);
(e.target as Element).setPointerCapture?.(e.pointerId);
setCurrentPath([getCoordinates(e)]);
};
@@ -46,7 +46,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
const handlePointerUp = (e: React.PointerEvent) => {
if (!isDrawing || activeTool !== 'draw') return;
setIsDrawing(false);
e.target.releasePointerCapture?.(e.pointerId);
(e.target as Element).releasePointerCapture?.(e.pointerId);
if (currentPath.length > 1) {
// Calculate bounding box
@@ -60,6 +60,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
const newAnno: Annotation = {
id: `anno_${Math.random().toString(36).substring(2, 11)}`,
type: 'ink',
pageIndex,
bbox: {
x: minX,
y: minY,
+11 -7
View File
@@ -17,6 +17,7 @@ interface PDFViewerProps {
zoom: number;
pagesInfo?: PageInfo[];
activeTool: string;
highlightColor: string;
annotations: Annotation[];
searchQuery?: string;
searchResults?: SearchResult[];
@@ -46,6 +47,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
zoom,
pagesInfo,
activeTool,
highlightColor,
annotations,
searchQuery,
searchResults,
@@ -201,10 +203,15 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
};
}, [visiblePages, documentId, zoom, renderedPages]);
const verifiedPages = useRef<Set<number>>(new Set());
useEffect(() => {
const verifyPageModel = async () => {
if (visiblePages.length > 0 && documentId && !renderedPages[visiblePages[0].index + '_verified']) {
if (visiblePages.length > 0 && documentId) {
const pageIndex = visiblePages[0].index;
if (verifiedPages.current.has(pageIndex)) return;
verifiedPages.current.add(pageIndex);
try {
const model = await gatewayService.getPageModel(documentId, 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) {
// ignore or log
verifiedPages.current.delete(pageIndex);
}
}
};
@@ -235,12 +237,14 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
const newAnno: Annotation = {
id: generateUniqueId(),
type: 'highlight',
pageIndex,
bbox: {
x: bbox.x / zoom,
y: bbox.y / zoom,
width: bbox.width / zoom,
height: bbox.height / zoom,
},
color: highlightColor,
author: 'Current User',
content: text,
pageIndex: pageIndex,