diff --git a/frontend/components/workspace/DocumentRenderer.tsx b/frontend/components/workspace/DocumentRenderer.tsx index 90cc452..a7d29a7 100644 --- a/frontend/components/workspace/DocumentRenderer.tsx +++ b/frontend/components/workspace/DocumentRenderer.tsx @@ -1,19 +1,26 @@ 'use client' -import React, { useEffect, useRef } from 'react' +import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline' +import React, { useEffect, useRef, useState, WheelEvent } from 'react' import { useProject } from '../../context/Project/provider' import loadImage from '../../useCases/loadImage' import processImageArea from '../../useCases/processImageArea' +import classNames from '../../utils/classNames' import LanguageSelect from './LanguageSelect' +const zoomStep = 0.05 +const maxZoomLevel = 4 + const DocumentRenderer = () => { - const { getSelectedDocument, requestAddArea } = useProject() + const { getSelectedDocument, requestAddArea, selectedAreaId, setSelectedAreaId } = useProject() const selectedDocument = getSelectedDocument() const areas = selectedDocument?.areas const documentCanvas = useRef(null) const areaCanvas = useRef(null) const drawingCanvas = useRef(null) + const [zoomLevel, setZoomLevel] = useState(1) + let downClickX = 0 let downClickY = 0 let isMouseDown = false @@ -43,14 +50,17 @@ const DocumentRenderer = () => { return } - applyCanvasSizes({ width: image.naturalWidth, height: image.naturalHeight }) + const width = image.naturalWidth * zoomLevel + const height = image.naturalHeight * zoomLevel + + applyCanvasSizes({ width, height }) const documentCanvasInstance = documentCanvas.current if (!documentCanvasInstance) return const context = documentCanvasInstance.getContext('2d') if (!context) return - context.drawImage(image, 0, 0, image.width, image.height) + context.drawImage(image, 0, 0, width, height) if (areas) applyAreasToCanvas() } @@ -58,7 +68,7 @@ const DocumentRenderer = () => { const applyAreasToCanvas = () => { const areaCanvasInstance = areaCanvas.current if (!areaCanvasInstance) return - const context = areaCanvasInstance.getContext('2d') + const context = areaCanvasInstance.getContext('2d')! if (!context) return context.clearRect(0, 0, areaCanvasInstance.width, areaCanvasInstance.height) @@ -66,14 +76,23 @@ const DocumentRenderer = () => { if (!areas || !areas.length) return areas.forEach(a => { - const width = a.endX - a.startX - const height = a.endY - a.startY - const x = a.startX - const y = a.startY - context.rect(x, y, width, height) - context.lineWidth = 2 - context.strokeStyle = '#dc8dec' + context.beginPath() + if (a.id !== selectedAreaId) { + context.setLineDash([4]) + context.lineWidth = 2 + context.strokeStyle = '#010101' + } else { + context.setLineDash([]) + context.lineWidth = 3 + context.strokeStyle = '#dc8dec' + } + const width = (a.endX - a.startX) * zoomLevel + const height = (a.endY - a.startY) * zoomLevel + const x = a.startX * zoomLevel + const y = a.startY * zoomLevel + context.roundRect(x, y, width, height, 4) context.stroke() + context.closePath() }) } @@ -95,25 +114,26 @@ const DocumentRenderer = () => { let startX: number, endX: number if (downClickX < mouseX) { - startX = downClickX - endX = mouseX + startX = Math.floor(downClickX / zoomLevel) + endX = Math.floor(mouseX / zoomLevel) } else { - startX = mouseX - endX = downClickX + startX = Math.floor(mouseX / zoomLevel) + endX = Math.floor(downClickX / zoomLevel) } let startY: number, endY: number if (downClickY < mouseY) { - startY = downClickY - endY = mouseY + startY = Math.floor(downClickY / zoomLevel) + endY = Math.floor(mouseY / zoomLevel) } else { - startY = mouseY - endY = downClickY + startY = Math.floor(mouseY / zoomLevel) + endY = Math.floor(downClickY / zoomLevel) } if (selectedDocument?.id) { const addedArea = await requestAddArea(selectedDocument.id, { startX, startY, endX, endY }) - processImageArea(selectedDocument.id, addedArea) + setSelectedAreaId(addedArea.id) + processImageArea(selectedDocument.id, addedArea.id) } const context = drawingCanvasInstance.getContext('2d') @@ -145,9 +165,16 @@ const DocumentRenderer = () => { } } + const handleWheelEvent = (e: WheelEvent) => { + if (!e.ctrlKey) return + + const shouldAttemptToZoomIn = (e.deltaY < 0) && zoomLevel < maxZoomLevel + if (shouldAttemptToZoomIn) setZoomLevel(zoomLevel + zoomStep) + else if (zoomLevel > (zoomStep * 2)) setZoomLevel(zoomLevel - zoomStep) + } + useEffect(() => { if (selectedDocument?.path) applyDocumentToCanvas(selectedDocument.path) - applyAreasToCanvas() }) return
@@ -155,26 +182,42 @@ const DocumentRenderer = () => {

{getSelectedDocument()?.name}

- +
+ +
+ + { setZoomLevel(e.currentTarget.valueAsNumber) }} + /> + +
+ +
-
+
-
+ } export default DocumentRenderer diff --git a/frontend/components/workspace/Sidebar.tsx b/frontend/components/workspace/Sidebar.tsx index e7aa5e3..03c136d 100644 --- a/frontend/components/workspace/Sidebar.tsx +++ b/frontend/components/workspace/Sidebar.tsx @@ -70,6 +70,7 @@ function Sidebar() { getSelectedDocument, getAreaById, requestUpdateArea, + requestDeleteAreaById, requestAddDocument, requestAddDocumentGroup, selectedAreaId, @@ -128,34 +129,24 @@ function Sidebar() { setIsEditAreaNameInputShowing(false) } - - // ________________ - const onAreaDragOver = (areaId: string) => { setDragOverAreaId(areaId) } const onAreaDragStart = (areaId: string) => { - // setDragStartAreaId(areaId) + setSelectedAreaId(areaId) } - + const onAreaDropEnd = (areaId: string) => { const areaDroppedOn = navigation.map(n => n.documents).flat().map(d => d.areas).flat().find(a => a.id === dragOverAreaId) if (!areaDroppedOn) return requestChangeAreaOrder(areaId, areaDroppedOn.order) setDragOverAreaId('') } - - - - - // ________________ - - - - - + const handleAreaDeleteButtonClick = (areaId: string) => { + requestDeleteAreaById(areaId) + } const onDocumentClickHandler = (itemId: string) => { setSelectedDocumentId(itemId) @@ -362,7 +353,7 @@ function Sidebar() { name="documentName" id="documentName" autoFocus - className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + className="h-8 w-[calc(100%-18px)] text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 inline-block rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" defaultValue={d.name} onBlur={onDocumentInputBlur} onKeyDown={(event) => { @@ -397,12 +388,12 @@ function Sidebar() { )}> {selectedDocumentId === d.id && isEditDocumentNameInputShowing - ? { @@ -434,7 +425,7 @@ function Sidebar() { id="areaName" autoFocus className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" - placeholder={a.name || `Area ${index + 1}`} + placeholder={a.name || `Area ${index}`} onBlur={onAreaInputBlur} onKeyDown={(event) => { onEnterHandler(event, @@ -442,23 +433,29 @@ function Sidebar() { }} ref={editAreaNameTextInput} /> - : onAreaClick(a.id)} - onDoubleClick={() => onAreaDoubleClick(a.id)} + :
onAreaDragOver(a.id)} onDragStart={() => onAreaDragStart(a.id)} onDragEnd={() => onAreaDropEnd(a.id)} - className={classNames('text-gray-300 hover:bg-gray-700 hover:text-white', - 'group w-full flex items-center pr-2 py-2 text-left font-medium pl-8 text-xs', - 'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none', - selectedAreaId === a.id ? 'underline' : '', - dragOverAreaId === a.id ? 'bg-gray-300 text-gray-700' : '' - )} - > - {a.name || `Area ${a.order}`} - + className={classNames('flex justify-between items-center cursor-pointer', + selectedAreaId === a.id ? 'bg-indigo-500 text-gray-200' : 'text-gray-300 hover:bg-gray-700 hover:text-white', + dragOverAreaId === a.id ? 'bg-gray-300 text-gray-700' : '', + selectedAreaId === a.id && dragOverAreaId === a.id ? 'bg-indigo-300' : '', + )}> + onAreaClick(a.id)} + onDoubleClick={() => onAreaDoubleClick(a.id)} + className={classNames('group w-full pr-2 py-2 text-left font-medium pl-8 text-xs', + 'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none', + )}> + {a.name || `Area ${a.order}`} + + handleAreaDeleteButtonClick(a.id)} /> +
} ))} diff --git a/frontend/context/Project/makeDefaultProject.ts b/frontend/context/Project/makeDefaultProject.ts index fe1b198..efc901b 100644 --- a/frontend/context/Project/makeDefaultProject.ts +++ b/frontend/context/Project/makeDefaultProject.ts @@ -13,6 +13,7 @@ const makeDefaultProject = (): ProjectContextType => ({ requestAddProcessedArea: (processesArea) => Promise.resolve(new ipc.ProcessedArea()), requestAddArea: (documentId, area) => Promise.resolve(new ipc.Area()), requestUpdateArea: (updatedArea) => Promise.resolve(new ipc.Area()), + requestDeleteAreaById: (areaId) => Promise.resolve(false), requestAddDocument: (groupId, documentName) => Promise.resolve(new ipc.Document()), requestAddDocumentGroup: (groupName: string) => Promise.resolve(new ipc.Group()), requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise.resolve(new ipc.UserMarkdown()), diff --git a/frontend/context/Project/provider.tsx b/frontend/context/Project/provider.tsx index 9066812..de20149 100644 --- a/frontend/context/Project/provider.tsx +++ b/frontend/context/Project/provider.tsx @@ -9,6 +9,7 @@ import { RequestChooseUserAvatar, RequestUpdateDocument, RequestChangeAreaOrder, + RequestDeleteAreaById, } from '../../wailsjs/wailsjs/go/ipc/Channel' import { ipc } from '../../wailsjs/wailsjs/go/models' import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps, UpdateDocumentRequest, UserProps } from './types' @@ -30,7 +31,6 @@ export function ProjectProvider({ children, projectProps }: Props) { const updateDocuments = async () => { GetDocuments().then(response => { - console.log(response) if (response.documents.length) setDocuments(response.documents) if (response.groups.length) setGroups(response.groups) Promise.resolve(response) @@ -66,6 +66,12 @@ export function ProjectProvider({ children, projectProps }: Props) { documents.map(d => d.areas).flat().find(a => a.id === areaId) ) + const requestDeleteAreaById = async (areaId: string): Promise => { + const wasSuccessfulDeletion = await RequestDeleteAreaById(areaId) + if (wasSuccessfulDeletion) updateDocuments() + return wasSuccessfulDeletion + } + const getSelectedDocument = () => documents.find(d => d.id === selectedDocumentId) const getProcessedAreasByDocumentId = async (documentId: string) => { @@ -132,7 +138,6 @@ export function ProjectProvider({ children, projectProps }: Props) { } const requestChangeAreaOrder = async (areaId: string, newOrder: number) => { - console.log('requestChangeAreaOrder') const response = await RequestChangeAreaOrder(areaId, newOrder) await updateDocuments() return response @@ -152,6 +157,7 @@ export function ProjectProvider({ children, projectProps }: Props) { requestAddDocument, requestAddDocumentGroup, requestUpdateArea, + requestDeleteAreaById, selectedAreaId, setSelectedAreaId, selectedDocumentId, diff --git a/frontend/context/Project/types.ts b/frontend/context/Project/types.ts index cb0c60d..6aa730b 100644 --- a/frontend/context/Project/types.ts +++ b/frontend/context/Project/types.ts @@ -43,6 +43,7 @@ export type ProjectContextType = { requestAddProcessedArea: (processedArea: ipc.ProcessedArea) => Promise requestAddArea: (documentId: string, area: AddAreaProps) => Promise requestUpdateArea: (area: AreaProps) => Promise + requestDeleteAreaById: (areaId: string) => Promise requestAddDocument: (groupId: string, documentName: string) => Promise requestAddDocumentGroup: (groupName: string) => Promise requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise diff --git a/frontend/useCases/processImageArea.ts b/frontend/useCases/processImageArea.ts index dc5f0b3..ced06d7 100644 --- a/frontend/useCases/processImageArea.ts +++ b/frontend/useCases/processImageArea.ts @@ -1,32 +1,35 @@ import { createScheduler, createWorker } from 'tesseract.js' -import { GetDocumentById, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel' +import { GetAreaById, GetDocumentById, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel' import { ipc } from '../wailsjs/wailsjs/go/models' import loadImage from './loadImage' -const processImageArea = async (documentId: string, area: ipc.Area) => { +const processImageArea = async (documentId: string, areaId: string) => { const foundDocument = await GetDocumentById(documentId) - if (!foundDocument.path || !foundDocument.areas?.length) return + const foundArea = await GetAreaById(areaId) + if (!foundDocument.path || !foundDocument.areas?.length || !foundArea.id) return + + const processLanguage = foundDocument.defaultLanguage.processCode const { path } = foundDocument const imageData = await loadImage(path) const scheduler = createScheduler() const worker = await createWorker() - await worker.loadLanguage('eng') // TODO: change this when multilangiage system is implementd - await worker.initialize('eng') // TODO: same here + await worker.loadLanguage(processLanguage) + await worker.initialize(processLanguage) scheduler.addWorker(worker) const result = await scheduler.addJob('recognize', imageData, { rectangle: { - left: area.startX, - top: area.startY, - width: area.endX - area.startX, - height: area.endY - area.startY, + left: foundArea.startX, + top: foundArea.startY, + width: foundArea.endX - foundArea.startX, + height: foundArea.endY - foundArea.startY, } }) const addProcessesAreaRequest = await RequestAddProcessedArea(new ipc.ProcessedArea({ - id: area.id, + id: foundArea.id, documentId, fullText: result.data.text, lines: result.data.lines.map((l: any) => new ipc.ProcessedLine({ diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts index 4f0f41e..6381d23 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts @@ -4,6 +4,8 @@ import {ipc} from '../models'; export function CreateNewProject(arg1:string):Promise; +export function GetAreaById(arg1:string):Promise; + export function GetCurrentSession():Promise; export function GetCurrentUser():Promise; @@ -30,6 +32,8 @@ export function RequestChangeAreaOrder(arg1:string,arg2:number):Promise; +export function RequestDeleteAreaById(arg1:string):Promise; + export function RequestUpdateArea(arg1:ipc.Area):Promise; export function RequestUpdateCurrentUser(arg1:ipc.User):Promise; diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.js b/frontend/wailsjs/wailsjs/go/ipc/Channel.js index 43a9d7d..348b272 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.js +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.js @@ -6,6 +6,10 @@ export function CreateNewProject(arg1) { return window['go']['ipc']['Channel']['CreateNewProject'](arg1); } +export function GetAreaById(arg1) { + return window['go']['ipc']['Channel']['GetAreaById'](arg1); +} + export function GetCurrentSession() { return window['go']['ipc']['Channel']['GetCurrentSession'](); } @@ -58,6 +62,10 @@ export function RequestChooseUserAvatar() { return window['go']['ipc']['Channel']['RequestChooseUserAvatar'](); } +export function RequestDeleteAreaById(arg1) { + return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1); +} + export function RequestUpdateArea(arg1) { return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1); } diff --git a/ipc/Documents.go b/ipc/Documents.go index f27051f..3304f31 100644 --- a/ipc/Documents.go +++ b/ipc/Documents.go @@ -32,12 +32,13 @@ func (c *Channel) GetDocumentById(id string) Document { }) } response := Document{ - Id: foundDocument.Id, - Name: foundDocument.Name, - GroupId: foundDocument.GroupId, - Path: foundDocument.Path, - ProjectId: foundDocument.ProjectId, - Areas: jsonAreas, + Id: foundDocument.Id, + Name: foundDocument.Name, + GroupId: foundDocument.GroupId, + Path: foundDocument.Path, + ProjectId: foundDocument.ProjectId, + Areas: jsonAreas, + DefaultLanguage: Language(foundDocument.DefaultLanguage), } return response } @@ -193,6 +194,32 @@ func (c *Channel) RequestAddDocumentGroup(name string) Group { return response } +func (c *Channel) GetAreaById(areaId string) Area { + foundDocument := document.GetDocumentCollection().GetDocumentByAreaId(areaId) + + if len(foundDocument.Areas) == 0 { + return Area{} + } + + var foundArea document.Area + for i, a := range foundDocument.Areas { + if a.Id == areaId { + foundArea = foundDocument.Areas[i] + } + } + + return Area{ + Id: foundArea.Id, + Name: foundArea.Name, + StartX: foundArea.StartX, + EndX: foundArea.EndX, + StartY: foundArea.StartY, + EndY: foundArea.EndY, + Order: foundArea.Order, + Language: Language(foundArea.Language), + } +} + func (c *Channel) RequestAddArea(documentId string, area Area) Area { foundDocument := document.GetDocumentCollection().GetDocumentById(documentId) @@ -233,7 +260,7 @@ func (c *Channel) RequestUpdateArea(updatedArea Area) Area { return Area{} } - areaToUpdate := documentOfArea.GetAreaById((updatedArea.Id)) + areaToUpdate := documentOfArea.GetAreaById(updatedArea.Id) if areaToUpdate.Id == "" { return Area{} @@ -259,6 +286,37 @@ func (c *Channel) RequestUpdateArea(updatedArea Area) Area { } } +func (c *Channel) RequestDeleteAreaById(areaId string) bool { + documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(areaId) + + if documentOfArea.Id == "" { + return false + } + + areaToDeleteIndex := -1 + + for i, a := range documentOfArea.Areas { + if a.Id == areaId { + areaToDeleteIndex = i + break + } + } + + if areaToDeleteIndex < 0 { + return false + } + + // func remove(s []int, i int) []int { + // s[i] = s[len(s)-1] + // return s[:len(s)-1] + // } + + documentOfArea.Areas[areaToDeleteIndex] = documentOfArea.Areas[len(documentOfArea.Areas)-1] + documentOfArea.Areas = documentOfArea.Areas[:len(documentOfArea.Areas)-1] + return true + +} + func (c *Channel) RequestUpdateDocument(updatedDocument Document) Document { documentToUpdate := document.GetDocumentCollection().GetDocumentById(updatedDocument.Id)