fix
This commit is contained in:
+65
-1
@@ -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}
|
||||
|
||||
@@ -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>) => {
|
||||
@@ -160,6 +168,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')}
|
||||
className={`tool-btn draw ${activeTool === 'draw' ? 'active' : ''}`}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user