fix
This commit is contained in:
+65
-1
@@ -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}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user