refact: broke out canvases
This commit is contained in:
parent
51c5141f99
commit
554cfa14ba
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -3,6 +3,7 @@
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"cSpell.words": [
|
||||
"heroicons"
|
||||
"heroicons",
|
||||
"wailsjs"
|
||||
]
|
||||
}
|
||||
69
frontend/components/DocumentCanvas/AreaCanvas.tsx
Normal file
69
frontend/components/DocumentCanvas/AreaCanvas.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useProject } from '../../context/Project/provider'
|
||||
|
||||
type Props = {
|
||||
width: number,
|
||||
height: number
|
||||
zoomLevel: number
|
||||
}
|
||||
|
||||
|
||||
const AreaCanvas = (props: Props) => {
|
||||
const { getSelectedDocument, selectedAreaId, } = useProject()
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const areas = getSelectedDocument()?.areas
|
||||
const { width, height, zoomLevel } = props
|
||||
|
||||
|
||||
const applyAreasToCanvas = (zoomLevel: number) => {
|
||||
if (!areas || !areas.length) return
|
||||
|
||||
const canvasContext = canvas.current!.getContext('2d')!
|
||||
|
||||
areas.forEach(a => {
|
||||
canvasContext.beginPath()
|
||||
if (a.id !== selectedAreaId) {
|
||||
canvasContext.setLineDash([4])
|
||||
canvasContext.lineWidth = 2
|
||||
canvasContext.strokeStyle = '#010101'
|
||||
} else {
|
||||
canvasContext.setLineDash([])
|
||||
canvasContext.lineWidth = 3
|
||||
canvasContext.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
|
||||
canvasContext.roundRect(x, y, width, height, 4)
|
||||
canvasContext.stroke()
|
||||
canvasContext.closePath()
|
||||
})
|
||||
}
|
||||
|
||||
const clearCanvas = () => {
|
||||
const canvasInstance = canvas.current!
|
||||
const context = canvasInstance.getContext('2d')!
|
||||
context.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
|
||||
}
|
||||
|
||||
const updateSize = (size: { width: number, height: number }) => {
|
||||
const canvasInstance = canvas.current!
|
||||
const { width, height } = size
|
||||
canvasInstance.width = width
|
||||
canvasInstance.height = height
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
clearCanvas()
|
||||
updateSize({ width, height })
|
||||
applyAreasToCanvas(zoomLevel)
|
||||
}, [width, height, zoomLevel, areas])
|
||||
|
||||
return <canvas className="absolute" ref={canvas} />
|
||||
}
|
||||
|
||||
export default AreaCanvas
|
||||
42
frontend/components/DocumentCanvas/AreaTextPreview.tsx
Normal file
42
frontend/components/DocumentCanvas/AreaTextPreview.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { ipc } from '../../wailsjs/wailsjs/go/models'
|
||||
import classNames from '../../utils/classNames'
|
||||
|
||||
type Props = {
|
||||
areas: ipc.Area[]
|
||||
processedArea?: ipc.ProcessedArea
|
||||
zoomLevel: number
|
||||
setWordToEdit: (props: { word: ipc.ProcessedWord, areaId: string }) => void
|
||||
}
|
||||
|
||||
const AreaTextPreview = ({ areas, processedArea, zoomLevel, setWordToEdit }: Props) => {
|
||||
if (!processedArea) return <></>
|
||||
|
||||
|
||||
return <div>
|
||||
{
|
||||
processedArea.lines?.map(l => l.words).flat().map((w, i) => {
|
||||
const width = Math.floor((w.boundingBox.x1 - w.boundingBox.x0) * zoomLevel) + 2
|
||||
const height = Math.floor((w.boundingBox.y1 - w.boundingBox.y0) * zoomLevel) + 2
|
||||
return <span
|
||||
key={i}
|
||||
dir={w.direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'}
|
||||
className={classNames('absolute text-center inline-block p-1 rounded-md shadow-zinc-900 shadow-2xl',
|
||||
'hover:bg-opacity-60 hover:bg-black hover:text-white',
|
||||
'bg-opacity-80 bg-slate-300 text-slate-500'
|
||||
)}
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
width,
|
||||
top: Math.floor(w.boundingBox.y0 * zoomLevel) + height,
|
||||
left: Math.floor(w.boundingBox.x0 * zoomLevel)
|
||||
}}
|
||||
onDoubleClick={() => setWordToEdit({ word: w, areaId: processedArea.id })}>
|
||||
{w.fullText}
|
||||
</span>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default AreaTextPreview
|
||||
82
frontend/components/DocumentCanvas/EditProcessedWord.tsx
Normal file
82
frontend/components/DocumentCanvas/EditProcessedWord.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { ipc } from '../../wailsjs/wailsjs/go/models'
|
||||
import classNames from '../../utils/classNames'
|
||||
import onEnterHandler from '../../utils/onEnterHandler'
|
||||
import { useProject } from '../../context/Project/provider'
|
||||
|
||||
type Props = {
|
||||
zoomLevel: number
|
||||
processedArea?: ipc.ProcessedArea
|
||||
wordToEdit?: ipc.ProcessedWord
|
||||
setWordToEdit: (props?: { word: ipc.ProcessedWord, areaId: string }) => void
|
||||
setHoveredProcessedArea: (area?: ipc.ProcessedArea) => void
|
||||
}
|
||||
|
||||
const EditProcessedWord = ({ setWordToEdit, zoomLevel, wordToEdit, processedArea, setHoveredProcessedArea }: Props) => {
|
||||
const {
|
||||
requestUpdateProcessedWordById,
|
||||
getProcessedAreaById,
|
||||
} = useProject()
|
||||
const editWordInput = useRef<HTMLInputElement>(null)
|
||||
|
||||
|
||||
if (!wordToEdit || !processedArea) return <></>
|
||||
|
||||
const width = Math.floor((wordToEdit.boundingBox.x1 - wordToEdit.boundingBox.x0) * zoomLevel) + 2
|
||||
const height = Math.floor(((wordToEdit.boundingBox.y1 - wordToEdit.boundingBox.y0) * zoomLevel) * 2) + 4
|
||||
|
||||
const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => {
|
||||
requestUpdateProcessedWordById(wordId, newWordValue)
|
||||
.then(res => {
|
||||
getProcessedAreaById(processedArea.id || '').then(response => {
|
||||
setHoveredProcessedArea(response)
|
||||
})
|
||||
})
|
||||
.catch(console.error)
|
||||
setWordToEdit(undefined)
|
||||
}
|
||||
|
||||
return <div
|
||||
dir={wordToEdit.direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'}
|
||||
className={classNames('absolute inline-block p-1 rounded-md',
|
||||
'bg-opacity-60 bg-black text-white',
|
||||
)}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
top: Math.floor(wordToEdit.boundingBox.y0 * zoomLevel) + (height / 2),
|
||||
left: Math.floor(wordToEdit.boundingBox.x0 * zoomLevel)
|
||||
}}
|
||||
onBlur={() => setWordToEdit(undefined)}
|
||||
>
|
||||
<div
|
||||
className={classNames('text-center align-middle block p-1 rounded-md shadow-zinc-900 shadow-2xl',
|
||||
'bg-opacity-60 bg-black text-white',
|
||||
)}
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
height: height / 2,
|
||||
}}>
|
||||
{wordToEdit.fullText}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='inline-block text-slate-900 p-0 m-0 w-full'
|
||||
autoFocus
|
||||
width={width}
|
||||
ref={editWordInput}
|
||||
placeholder={wordToEdit.fullText}
|
||||
defaultValue={wordToEdit.fullText}
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
height: height / 2,
|
||||
}}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={(e) => handleWordCorrectionSubmit(wordToEdit.id, e.currentTarget.value)}
|
||||
onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(wordToEdit.id, e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default EditProcessedWord
|
||||
55
frontend/components/DocumentCanvas/ImageCanvas.tsx
Normal file
55
frontend/components/DocumentCanvas/ImageCanvas.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import loadImage from '../../useCases/loadImage'
|
||||
|
||||
type Props = {
|
||||
zoomLevel: number,
|
||||
imagePath?: string,
|
||||
setSize: (size: { width: number, height: number }) => void
|
||||
}
|
||||
|
||||
const ImageCanvas = (props: Props) => {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const { imagePath, zoomLevel, setSize } = props
|
||||
|
||||
const applyImageToCanvas = async (path: string) => {
|
||||
const canvasContext = canvas.current!.getContext('2d')!
|
||||
|
||||
let image: HTMLImageElement
|
||||
try {
|
||||
image = await loadImage(path)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
|
||||
const width = image.naturalWidth * zoomLevel
|
||||
const height = image.naturalHeight * zoomLevel
|
||||
|
||||
updateSize({ width, height })
|
||||
|
||||
canvasContext.drawImage(image, 0, 0, width, height)
|
||||
}
|
||||
|
||||
const clearCanvas = () => {
|
||||
const canvasInstance = canvas.current!
|
||||
const context = canvasInstance.getContext('2d')!
|
||||
context.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
|
||||
}
|
||||
|
||||
const updateSize = (size: { width: number, height: number }) => {
|
||||
const canvasInstance = canvas.current!
|
||||
const { width, height } = size
|
||||
canvasInstance.width = width
|
||||
canvasInstance.height = height
|
||||
setSize(size)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (imagePath) applyImageToCanvas(imagePath)
|
||||
}, [imagePath, zoomLevel])
|
||||
|
||||
return <canvas className="absolute" ref={canvas} />
|
||||
}
|
||||
|
||||
export default ImageCanvas
|
||||
186
frontend/components/DocumentCanvas/UiCanvas.tsx
Normal file
186
frontend/components/DocumentCanvas/UiCanvas.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
'use client'
|
||||
|
||||
import React, { WheelEvent, useEffect, useRef, useState } from 'react'
|
||||
import { useProject } from '../../context/Project/provider'
|
||||
import { ipc } from '../../wailsjs/wailsjs/go/models'
|
||||
import createUiCanvasInteractions from './createUiCanvasInteractions'
|
||||
import processImageArea from '../../useCases/processImageArea'
|
||||
import AreaTextPreview from './AreaTextPreview'
|
||||
import EditProcessedWord from './EditProcessedWord'
|
||||
|
||||
type Props = {
|
||||
width: number,
|
||||
height: number
|
||||
zoomDetails: { currentZoomLevel: number, zoomStep: number, maxZoomLevel: number }
|
||||
setZoomLevel: (value: number) => void
|
||||
}
|
||||
|
||||
let interactions: ReturnType<typeof createUiCanvasInteractions> | null = null
|
||||
|
||||
let downClickX = 0
|
||||
let downClickY = 0
|
||||
let isDrawing = false
|
||||
|
||||
const UiCanvas = (props: Props) => {
|
||||
const {
|
||||
getSelectedDocument,
|
||||
getProcessedAreaById,
|
||||
requestAddArea,
|
||||
setSelectedAreaId,
|
||||
} = useProject()
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const [hoverOverAreaId, setHoverOverAreaId] = useState('')
|
||||
const [wordToEdit, setWordToEdit] = useState<{ word: ipc.ProcessedWord, areaId: string } | undefined>()
|
||||
const [hoveredProcessedArea, setHoveredProcessedArea] = useState<ipc.ProcessedArea | undefined>()
|
||||
|
||||
const areas = getSelectedDocument()?.areas || []
|
||||
const { width, height, zoomDetails, setZoomLevel } = props
|
||||
const { currentZoomLevel } = zoomDetails
|
||||
|
||||
const applyUiCanvasUpdates = () => {
|
||||
const canvasContext = canvas.current!.getContext('2d')!
|
||||
|
||||
if (!areas || !areas.length) return
|
||||
|
||||
const hoverArea = areas.find(a => a.id === hoverOverAreaId)
|
||||
if (!hoverArea) return
|
||||
|
||||
canvasContext.beginPath()
|
||||
canvasContext.setLineDash([])
|
||||
canvasContext.lineWidth = 6
|
||||
canvasContext.strokeStyle = '#dc8dec'
|
||||
const width = (hoverArea.endX - hoverArea.startX) * currentZoomLevel
|
||||
const height = (hoverArea.endY - hoverArea.startY) * currentZoomLevel
|
||||
const x = hoverArea.startX * currentZoomLevel
|
||||
const y = hoverArea.startY * currentZoomLevel
|
||||
canvasContext.roundRect(x, y, width, height, 4)
|
||||
canvasContext.stroke()
|
||||
canvasContext.closePath()
|
||||
}
|
||||
|
||||
const clearCanvas = () => {
|
||||
const canvasInstance = canvas.current!
|
||||
const context = canvasInstance.getContext('2d')!
|
||||
context.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.nativeEvent.shiftKey) {
|
||||
downClickX = e.nativeEvent.offsetX
|
||||
downClickY = e.nativeEvent.offsetY
|
||||
isDrawing = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (isDrawing) interactions?.onActivelyDrawArea({
|
||||
startX: downClickX,
|
||||
startY: downClickY,
|
||||
endX: e.nativeEvent.offsetX,
|
||||
endY: e.nativeEvent.offsetY,
|
||||
})
|
||||
else interactions?.onHoverOverArea(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
currentZoomLevel,
|
||||
areas,
|
||||
(areaId) => {
|
||||
if (areaId === hoverOverAreaId) return
|
||||
|
||||
setHoverOverAreaId(areaId || '')
|
||||
getProcessedAreaById(areaId || '').then(response => {
|
||||
setHoveredProcessedArea(response)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const handleMouseUp = async (e: React.MouseEvent) => {
|
||||
if (isDrawing) {
|
||||
const coordinates = {
|
||||
startMouseX: downClickX,
|
||||
startMouseY: downClickY,
|
||||
endMouseX: e.nativeEvent.offsetX,
|
||||
endMouseY: e.nativeEvent.offsetY,
|
||||
}
|
||||
interactions?.onFinishDrawArea(coordinates, currentZoomLevel,
|
||||
async (startX, startY, endX, endY) => {
|
||||
const canvasInstance = canvas.current
|
||||
if (!canvasInstance) return
|
||||
|
||||
const selectedDocumentId = getSelectedDocument()?.id
|
||||
if (selectedDocumentId) {
|
||||
const addedArea = await requestAddArea(selectedDocumentId, { startX, startY, endX, endY })
|
||||
setSelectedAreaId(addedArea.id)
|
||||
processImageArea(selectedDocumentId, addedArea.id)
|
||||
}
|
||||
|
||||
const context = canvasInstance.getContext('2d')
|
||||
context?.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
|
||||
isDrawing = false
|
||||
downClickX = 0
|
||||
downClickY = 0
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleWheelEvent = (e: WheelEvent<HTMLCanvasElement>) => {
|
||||
if (e.ctrlKey) interactions?.onZoom(e.deltaY, zoomDetails, setZoomLevel)
|
||||
}
|
||||
|
||||
const updateSize = (size: { width: number, height: number }) => {
|
||||
const canvasInstance = canvas.current!
|
||||
const { width, height } = size
|
||||
canvasInstance.width = width
|
||||
canvasInstance.height = height
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!interactions && canvas.current) {
|
||||
interactions = createUiCanvasInteractions(canvas.current)
|
||||
}
|
||||
}, [canvas.current])
|
||||
|
||||
useEffect(() => {
|
||||
clearCanvas()
|
||||
updateSize({ width, height })
|
||||
applyUiCanvasUpdates()
|
||||
}, [width, height, currentZoomLevel, areas])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
clearCanvas()
|
||||
applyUiCanvasUpdates()
|
||||
}, [hoverOverAreaId])
|
||||
|
||||
|
||||
return <>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={canvas}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onWheel={handleWheelEvent}
|
||||
/>
|
||||
<AreaTextPreview
|
||||
setWordToEdit={setWordToEdit}
|
||||
processedArea={hoveredProcessedArea}
|
||||
zoomLevel={currentZoomLevel}
|
||||
areas={areas}
|
||||
/>
|
||||
|
||||
<EditProcessedWord
|
||||
zoomLevel={currentZoomLevel}
|
||||
processedArea={hoveredProcessedArea}
|
||||
wordToEdit={wordToEdit?.word}
|
||||
setWordToEdit={setWordToEdit}
|
||||
setHoveredProcessedArea={setHoveredProcessedArea}
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
export default UiCanvas
|
||||
101
frontend/components/DocumentCanvas/createUiCanvasInteractions.ts
Normal file
101
frontend/components/DocumentCanvas/createUiCanvasInteractions.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import isInBounds from '../../utils/isInBounds'
|
||||
import { ipc } from '../../wailsjs/wailsjs/go/models'
|
||||
|
||||
|
||||
type MouseCoordinates = {
|
||||
startMouseX: number, startMouseY: number, endMouseX: number, endMouseY: number
|
||||
}
|
||||
|
||||
type RectangleCoordinates = {
|
||||
startX: number, startY: number, endX: number, endY: number
|
||||
}
|
||||
|
||||
type AddAreaToStoreCallback =
|
||||
(startX: number, startY: number, endX: number, endY: number)
|
||||
=> Promise<void>
|
||||
|
||||
type SetZoomCallback = (newZoomLevel: number) => void
|
||||
|
||||
type ZoomDetails = {
|
||||
currentZoomLevel: number,
|
||||
maxZoomLevel: number,
|
||||
zoomStep: number
|
||||
}
|
||||
|
||||
type HoverOverAreaCallback = (areaId?: string) => void
|
||||
|
||||
/**
|
||||
* @param uiCanvas
|
||||
* @returns Various methods to be called during events on the `UiCanvas`.
|
||||
* Dependencies must be injected, such as state change callbacks.
|
||||
*/
|
||||
const createUiCanvasInteractions = (uiCanvas: HTMLCanvasElement) => {
|
||||
const uiCanvasContext = uiCanvas.getContext('2d')!
|
||||
|
||||
return {
|
||||
onActivelyDrawArea: (coordinates: RectangleCoordinates) => {
|
||||
const { startX, startY, endX, endY } = coordinates
|
||||
|
||||
uiCanvasContext.clearRect(0, 0, uiCanvas.width, uiCanvas.height)
|
||||
uiCanvasContext.beginPath()
|
||||
const width = endX - startX
|
||||
const height = endY - startY
|
||||
uiCanvasContext.rect(startX, startY, width, height)
|
||||
uiCanvasContext.strokeStyle = '#000'
|
||||
uiCanvasContext.lineWidth = 2
|
||||
uiCanvasContext.stroke()
|
||||
},
|
||||
onFinishDrawArea: (coordinates: MouseCoordinates, zoomLevel: number, addAreaToStoreCallback: AddAreaToStoreCallback) => {
|
||||
let { startMouseX, endMouseX, startMouseY, endMouseY } = coordinates
|
||||
|
||||
let startX: number, endX: number
|
||||
if (startMouseX < endMouseX) {
|
||||
startX = Math.floor(startMouseX / zoomLevel)
|
||||
endX = Math.floor(endMouseX / zoomLevel)
|
||||
} else {
|
||||
startX = Math.floor(endMouseX / zoomLevel)
|
||||
endX = Math.floor(startMouseX / zoomLevel)
|
||||
}
|
||||
|
||||
let startY: number, endY: number
|
||||
if (startMouseY < endMouseY) {
|
||||
startY = Math.floor(startMouseY / zoomLevel)
|
||||
endY = Math.floor(endMouseY / zoomLevel)
|
||||
} else {
|
||||
startY = Math.floor(endMouseY / zoomLevel)
|
||||
endY = Math.floor(startMouseY / zoomLevel)
|
||||
}
|
||||
|
||||
addAreaToStoreCallback(startX, startY, endX, endY)
|
||||
},
|
||||
onZoom: (wheelDelta: number, zoomDetails: ZoomDetails, setZoomCallBack: SetZoomCallback) => {
|
||||
const { currentZoomLevel, maxZoomLevel, zoomStep } = zoomDetails
|
||||
|
||||
const shouldAttemptToZoomIn = (wheelDelta < 0) && currentZoomLevel < maxZoomLevel
|
||||
if (shouldAttemptToZoomIn) setZoomCallBack(currentZoomLevel + zoomStep)
|
||||
else if (currentZoomLevel > (zoomStep * 2)) setZoomCallBack(currentZoomLevel - zoomStep)
|
||||
},
|
||||
onHoverOverArea: (mouseX: number, mouseY: number, zoomLevel: number, areas: ipc.Area[], callback: HoverOverAreaCallback) => {
|
||||
if (!areas.length) return
|
||||
|
||||
const domRect = uiCanvas.getBoundingClientRect()
|
||||
const x = mouseX - domRect.left
|
||||
const y = mouseY - domRect.top
|
||||
const point = { x, y }
|
||||
|
||||
const areaContainingCoords = areas.find(a => {
|
||||
const bounds = {
|
||||
startX: a.startX,
|
||||
startY: a.startY,
|
||||
endX: a.endX,
|
||||
endY: a.endY
|
||||
}
|
||||
return isInBounds(point, bounds, zoomLevel)
|
||||
})
|
||||
|
||||
callback(areaContainingCoords?.id)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createUiCanvasInteractions
|
||||
58
frontend/components/DocumentCanvas/index.tsx
Normal file
58
frontend/components/DocumentCanvas/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useProject, } from '../../context/Project/provider'
|
||||
import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline'
|
||||
import classNames from '../../utils/classNames'
|
||||
import LanguageSelect from '../workspace/LanguageSelect'
|
||||
import ImageCanvas from './ImageCanvas'
|
||||
import AreaCanvas from './AreaCanvas'
|
||||
import UiCanvas from './UiCanvas'
|
||||
|
||||
const zoomStep = 0.025
|
||||
const maxZoomLevel = 4
|
||||
|
||||
const DocumentCanvas = () => {
|
||||
const { getSelectedDocument } = useProject()
|
||||
const selectedDocument = getSelectedDocument()
|
||||
|
||||
const [zoomLevel, setZoomLevel] = useState(1)
|
||||
const [size, setSize] = useState({ width: 0, height: 0 })
|
||||
const { width, height } = size
|
||||
|
||||
return <div className='relative'>
|
||||
<div className='flex justify-between align-top mb-2'>
|
||||
<div className='flex align-top'>
|
||||
<h1 className="text-xl font-semibold text-gray-900 inline-block mr-2">{selectedDocument?.name}</h1>
|
||||
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
|
||||
</div>
|
||||
<div className='flex justify-evenly items-center'>
|
||||
<MagnifyingGlassMinusIcon className='w-4 h-4' />
|
||||
<input
|
||||
id="zoomRange" type="range" min={zoomStep} max={maxZoomLevel} step={zoomStep}
|
||||
value={zoomLevel} className="w-[calc(100%-50px)] h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer p-0"
|
||||
onChange={(e) => { setZoomLevel(e.currentTarget.valueAsNumber) }}
|
||||
/>
|
||||
<MagnifyingGlassPlusIcon className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('relative mt-2 overflow-scroll',
|
||||
'w-[calc(100vw-320px)] h-[calc(100vh-174px)] border-4',
|
||||
'border-dashed border-gray-200')}>
|
||||
|
||||
<ImageCanvas imagePath={selectedDocument?.path} zoomLevel={zoomLevel} setSize={setSize} />
|
||||
<AreaCanvas width={width} height={height} zoomLevel={zoomLevel} />
|
||||
<UiCanvas
|
||||
width={width}
|
||||
height={height}
|
||||
setZoomLevel={setZoomLevel}
|
||||
zoomDetails={{
|
||||
currentZoomLevel: zoomLevel,
|
||||
maxZoomLevel: maxZoomLevel,
|
||||
zoomStep: zoomStep,
|
||||
}} />
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
|
||||
export default DocumentCanvas
|
||||
@ -1,412 +0,0 @@
|
||||
'use client'
|
||||
|
||||
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'
|
||||
import isInBounds from '../../utils/isInBounds'
|
||||
import { ipc } from '../../wailsjs/wailsjs/go/models'
|
||||
import onEnterHandler from '../../utils/onEnterHandler'
|
||||
|
||||
const zoomStep = 0.025
|
||||
const maxZoomLevel = 4
|
||||
|
||||
const DocumentRenderer = () => {
|
||||
const {
|
||||
getSelectedDocument,
|
||||
requestAddArea,
|
||||
selectedAreaId,
|
||||
setSelectedAreaId,
|
||||
getProcessedAreasByDocumentId,
|
||||
requestUpdateProcessedWordById
|
||||
} = useProject()
|
||||
const selectedDocument = getSelectedDocument()
|
||||
const areas = selectedDocument?.areas
|
||||
const documentCanvas = useRef<HTMLCanvasElement>(null)
|
||||
const areaCanvas = useRef<HTMLCanvasElement>(null)
|
||||
const uiCanvas = useRef<HTMLCanvasElement>(null)
|
||||
const drawingCanvas = useRef<HTMLCanvasElement>(null)
|
||||
const editWordInput = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [zoomLevel, setZoomLevel] = useState(1)
|
||||
const [hoverOverAreaId, setHoverOverAreaId] = useState('')
|
||||
const [hoveredProcessedArea, setHoveredProcessedArea] = useState<ipc.ProcessedArea | undefined>()
|
||||
const [wordToEdit, setWordToEdit] = useState<{ word: ipc.ProcessedWord, areaId: string } | undefined>()
|
||||
|
||||
let downClickX = 0
|
||||
let downClickY = 0
|
||||
let isDrawing = false
|
||||
|
||||
const applyCanvasSizes = (size: { width: number, height: number }) => {
|
||||
const documentCanvasInstance = documentCanvas.current
|
||||
if (!documentCanvasInstance) return
|
||||
documentCanvasInstance.width = size.width
|
||||
documentCanvasInstance.height = size.height
|
||||
|
||||
const areaCanvasInstance = areaCanvas.current
|
||||
if (!areaCanvasInstance) return
|
||||
areaCanvasInstance.width = size.width
|
||||
areaCanvasInstance.height = size.height
|
||||
|
||||
const uiCanvasInstance = uiCanvas.current
|
||||
if (!uiCanvasInstance) return
|
||||
uiCanvasInstance.width = size.width
|
||||
uiCanvasInstance.height = size.height
|
||||
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
drawingCanvasInstance.width = size.width
|
||||
drawingCanvasInstance.height = size.height
|
||||
}
|
||||
|
||||
const applyDocumentToCanvas = async (path: string) => {
|
||||
let image: HTMLImageElement
|
||||
try {
|
||||
image = await loadImage(path)
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
|
||||
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, width, height)
|
||||
|
||||
if (areas) applyAreasToCanvas()
|
||||
applyUiCanvasUpdates()
|
||||
}
|
||||
|
||||
const applyAreasToCanvas = () => {
|
||||
const areaCanvasInstance = areaCanvas.current
|
||||
if (!areaCanvasInstance) return
|
||||
const context = areaCanvasInstance.getContext('2d')!
|
||||
if (!context) return
|
||||
|
||||
context.clearRect(0, 0, areaCanvasInstance.width, areaCanvasInstance.height)
|
||||
|
||||
if (!areas || !areas.length) return
|
||||
|
||||
areas.forEach(a => {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
const applyUiCanvasUpdates = () => {
|
||||
const uiCanvasInstance = uiCanvas.current
|
||||
if (!uiCanvasInstance) return
|
||||
const context = uiCanvasInstance.getContext('2d')!
|
||||
if (!context) return
|
||||
|
||||
context.clearRect(0, 0, uiCanvasInstance.width, uiCanvasInstance.height)
|
||||
|
||||
if (!areas || !areas.length) return
|
||||
|
||||
const hoverArea = areas.find(a => a.id === hoverOverAreaId)
|
||||
if (!hoverArea) return
|
||||
|
||||
context.beginPath()
|
||||
context.setLineDash([])
|
||||
context.lineWidth = 6
|
||||
context.strokeStyle = '#dc8dec'
|
||||
const width = (hoverArea.endX - hoverArea.startX) * zoomLevel
|
||||
const height = (hoverArea.endY - hoverArea.startY) * zoomLevel
|
||||
const x = hoverArea.startX * zoomLevel
|
||||
const y = hoverArea.startY * zoomLevel
|
||||
context.roundRect(x, y, width, height, 4)
|
||||
context.stroke()
|
||||
context.closePath()
|
||||
}
|
||||
|
||||
const getProcessedAreaById = async (areaId: string) => {
|
||||
try {
|
||||
if (!selectedDocument || !selectedDocument.id || !areaId) return
|
||||
const processedAreas = await getProcessedAreasByDocumentId(selectedDocument.id)
|
||||
const foundProcessedArea = processedAreas.find(a => a.id === areaId)
|
||||
console.log(foundProcessedArea)
|
||||
return foundProcessedArea
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const handleHoverOverArea = (e: React.MouseEvent) => {
|
||||
const domRect = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - domRect.left
|
||||
const y = e.clientY - domRect.top
|
||||
const point = { x, y }
|
||||
|
||||
const areaContainingCoords = areas?.find(a => {
|
||||
const bounds = { startX: a.startX, startY: a.startY, endX: a.endX, endY: a.endY }
|
||||
return isInBounds(point, bounds, zoomLevel)
|
||||
})
|
||||
|
||||
if (areaContainingCoords?.id === hoverOverAreaId) return
|
||||
|
||||
setHoverOverAreaId(areaContainingCoords?.id || '')
|
||||
getProcessedAreaById(areaContainingCoords?.id || '').then(response => {
|
||||
setHoveredProcessedArea(response)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseDrawArea = (x: number, y: number) => {
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
const context = drawingCanvasInstance.getContext('2d')
|
||||
if (!context) return
|
||||
|
||||
context.clearRect(0, 0, drawingCanvasInstance.width, drawingCanvasInstance.height)
|
||||
context.beginPath()
|
||||
const width = x - downClickX
|
||||
const height = y - downClickY
|
||||
context.rect(downClickX, downClickY, width, height)
|
||||
context.strokeStyle = '#000'
|
||||
context.lineWidth = 2
|
||||
context.stroke()
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
console.log(e)
|
||||
if (e.nativeEvent.shiftKey) {
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
|
||||
downClickX = e.nativeEvent.offsetX
|
||||
downClickY = e.nativeEvent.offsetY
|
||||
isDrawing = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = async (e: React.MouseEvent) => {
|
||||
if (isDrawing) {
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
|
||||
const mouseX = e.nativeEvent.offsetX
|
||||
const mouseY = e.nativeEvent.offsetY
|
||||
|
||||
let startX: number, endX: number
|
||||
if (downClickX < mouseX) {
|
||||
startX = Math.floor(downClickX / zoomLevel)
|
||||
endX = Math.floor(mouseX / zoomLevel)
|
||||
} else {
|
||||
startX = Math.floor(mouseX / zoomLevel)
|
||||
endX = Math.floor(downClickX / zoomLevel)
|
||||
}
|
||||
|
||||
let startY: number, endY: number
|
||||
if (downClickY < mouseY) {
|
||||
startY = Math.floor(downClickY / zoomLevel)
|
||||
endY = Math.floor(mouseY / zoomLevel)
|
||||
} else {
|
||||
startY = Math.floor(mouseY / zoomLevel)
|
||||
endY = Math.floor(downClickY / zoomLevel)
|
||||
}
|
||||
|
||||
if (selectedDocument?.id) {
|
||||
const addedArea = await requestAddArea(selectedDocument.id, { startX, startY, endX, endY })
|
||||
setSelectedAreaId(addedArea.id)
|
||||
processImageArea(selectedDocument.id, addedArea.id)
|
||||
}
|
||||
|
||||
const context = drawingCanvasInstance.getContext('2d')
|
||||
context?.clearRect(0, 0, drawingCanvasInstance.width, drawingCanvasInstance.height)
|
||||
isDrawing = false
|
||||
downClickX = 0
|
||||
downClickY = 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
let mouseX = e.nativeEvent.offsetX
|
||||
let mouseY = e.nativeEvent.offsetY
|
||||
|
||||
if (isDrawing) handleMouseDrawArea(mouseX, mouseY)
|
||||
else handleHoverOverArea(e)
|
||||
}
|
||||
|
||||
const handleWheelEvent = (e: WheelEvent<HTMLDivElement>) => {
|
||||
if (!e.ctrlKey) return
|
||||
|
||||
const shouldAttemptToZoomIn = (e.deltaY < 0) && zoomLevel < maxZoomLevel
|
||||
if (shouldAttemptToZoomIn) setZoomLevel(zoomLevel + zoomStep)
|
||||
else if (zoomLevel > (zoomStep * 2)) setZoomLevel(zoomLevel - zoomStep)
|
||||
}
|
||||
|
||||
const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => {
|
||||
console.log(newWordValue)
|
||||
requestUpdateProcessedWordById(wordId, newWordValue)
|
||||
.then(res => {
|
||||
console.log('res', res)
|
||||
getProcessedAreaById(hoverOverAreaId || '').then(response => {
|
||||
setHoveredProcessedArea(response)
|
||||
})
|
||||
})
|
||||
.catch(console.error)
|
||||
setWordToEdit(undefined)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDocument?.path) applyDocumentToCanvas(selectedDocument.path)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
applyUiCanvasUpdates()
|
||||
}, [hoverOverAreaId])
|
||||
|
||||
const renderAreaPreview = () => {
|
||||
if (!areas || !areas.length || !hoveredProcessedArea) return <></>
|
||||
|
||||
const hoverArea = areas.find(a => a.id === hoverOverAreaId)
|
||||
if (!hoverArea) return <></>
|
||||
|
||||
|
||||
return <div>
|
||||
{
|
||||
hoveredProcessedArea.lines?.map(l => l.words).flat().map((w, i) => {
|
||||
const width = Math.floor((w.boundingBox.x1 - w.boundingBox.x0) * zoomLevel) + 2
|
||||
const height = Math.floor((w.boundingBox.y1 - w.boundingBox.y0) * zoomLevel) + 2
|
||||
return <span
|
||||
key={i}
|
||||
dir={w.direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'}
|
||||
className={classNames('absolute text-center inline-block p-1 rounded-md shadow-zinc-900 shadow-2xl',
|
||||
'hover:bg-opacity-60 hover:bg-black hover:text-white',
|
||||
'bg-opacity-80 bg-slate-300 text-slate-500'
|
||||
)}
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
width,
|
||||
top: Math.floor(w.boundingBox.y0 * zoomLevel) + height,
|
||||
left: Math.floor(w.boundingBox.x0 * zoomLevel)
|
||||
}}
|
||||
onDoubleClick={() => setWordToEdit({ word: w, areaId: hoverArea.id })}>
|
||||
{w.fullText}
|
||||
</span>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
const renderEditWord = () => {
|
||||
if (!wordToEdit) return <></>
|
||||
|
||||
const { word, areaId } = wordToEdit
|
||||
const width = Math.floor((word.boundingBox.x1 - word.boundingBox.x0) * zoomLevel) + 2
|
||||
const height = Math.floor(((word.boundingBox.y1 - word.boundingBox.y0) * zoomLevel) * 2) + 4
|
||||
return <div
|
||||
dir={word.direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'}
|
||||
className={classNames('absolute inline-block p-1 rounded-md',
|
||||
'bg-opacity-60 bg-black text-white',
|
||||
)}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
top: Math.floor(word.boundingBox.y0 * zoomLevel) + (height / 2),
|
||||
left: Math.floor(word.boundingBox.x0 * zoomLevel)
|
||||
}}
|
||||
onBlur={() => setWordToEdit(undefined)}
|
||||
>
|
||||
<div
|
||||
className={classNames('text-center align-middle block p-1 rounded-md shadow-zinc-900 shadow-2xl',
|
||||
'bg-opacity-60 bg-black text-white',
|
||||
)}
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
height: height / 2,
|
||||
}}>
|
||||
{word.fullText}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='inline-block text-slate-900 p-0 m-0 w-full'
|
||||
autoFocus
|
||||
width={width}
|
||||
ref={editWordInput}
|
||||
placeholder={word.fullText}
|
||||
defaultValue={word.fullText}
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
height: height / 2,
|
||||
}}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={(e) => handleWordCorrectionSubmit(word.id, e.currentTarget.value)}
|
||||
onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(word.id, e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
return <div className='relative'>
|
||||
<div className='flex justify-between align-top mb-2'>
|
||||
<div className='flex align-top'>
|
||||
<h1 className="text-xl font-semibold text-gray-900 inline-block mr-2">{getSelectedDocument()?.name}</h1>
|
||||
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
|
||||
</div>
|
||||
<div className='flex justify-evenly items-center'>
|
||||
<MagnifyingGlassMinusIcon className='w-4 h-4' />
|
||||
<input
|
||||
id="zoomRange" type="range" min={zoomStep} max={maxZoomLevel} step={zoomStep}
|
||||
value={zoomLevel} className="w-[calc(100%-50px)] h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer p-0"
|
||||
onChange={(e) => { setZoomLevel(e.currentTarget.valueAsNumber) }}
|
||||
/>
|
||||
<MagnifyingGlassPlusIcon className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onWheelCapture={handleWheelEvent}
|
||||
className={classNames('relative mt-2 overflow-scroll',
|
||||
'w-[calc(100vw-320px)] h-[calc(100vh-174px)] border-4',
|
||||
'border-dashed border-gray-200')}>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={documentCanvas}
|
||||
/>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={areaCanvas}
|
||||
/>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={uiCanvas}
|
||||
/>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={drawingCanvas}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
/>
|
||||
{renderAreaPreview()}
|
||||
{renderEditWord()}
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
|
||||
export default DocumentRenderer
|
||||
@ -3,7 +3,7 @@
|
||||
import { useNavigation } from '../../context/Navigation/provider'
|
||||
import { workspaces } from '../../context/Navigation/types'
|
||||
import { useProject } from '../../context/Project/provider'
|
||||
import DocumentRenderer from './DocumentRenderer'
|
||||
import DocumentCanvas from '../DocumentCanvas'
|
||||
import NoSelectedDocument from './NoSelectedDocument'
|
||||
import TextEditor from './TextEditor'
|
||||
|
||||
@ -13,7 +13,7 @@ const MainWorkspace = () => {
|
||||
|
||||
const renderSelectedWorkSpace = () => {
|
||||
if (selectedWorkspace === workspaces.TEXTEDITOR) return <TextEditor />
|
||||
else return !selectedDocumentId ? <NoSelectedDocument /> : <DocumentRenderer />
|
||||
else return !selectedDocumentId ? <NoSelectedDocument /> : <DocumentCanvas />
|
||||
}
|
||||
|
||||
return <main className=" bg-gray-100 min-h-[calc(100vh-118px)] ml-64 overflow-y-scroll">
|
||||
|
||||
@ -31,6 +31,7 @@ const makeDefaultProject = (): ProjectContextType => ({
|
||||
getGroupById: (groupId) => undefined,
|
||||
requestSelectProjectByName: (projectName) => Promise.resolve(false),
|
||||
requestUpdateProcessedWordById: (wordId, newTestValue) => Promise.resolve(false),
|
||||
getProcessedAreaById: (areaId) => Promise.resolve(undefined),
|
||||
})
|
||||
|
||||
export default makeDefaultProject
|
||||
|
||||
@ -189,6 +189,18 @@ export function ProjectProvider({ children, projectProps }: Props) {
|
||||
return successfulResponse
|
||||
}
|
||||
|
||||
const getProcessedAreaById = async (areaId: string) => {
|
||||
try {
|
||||
if (!selectedDocumentId || !areaId) return
|
||||
const processedAreas = await getProcessedAreasByDocumentId(selectedDocumentId)
|
||||
const foundProcessedArea = processedAreas.find(a => a.id === areaId)
|
||||
return foundProcessedArea
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!documents.length && !groups.length) updateDocuments()
|
||||
@ -231,6 +243,7 @@ export function ProjectProvider({ children, projectProps }: Props) {
|
||||
getGroupById,
|
||||
requestSelectProjectByName,
|
||||
requestUpdateProcessedWordById,
|
||||
getProcessedAreaById,
|
||||
}
|
||||
|
||||
return <ProjectContext.Provider value={value}>
|
||||
|
||||
@ -63,4 +63,5 @@ export type ProjectContextType = {
|
||||
getGroupById: (groupId: string) => ipc.Group | undefined
|
||||
requestSelectProjectByName: (projectName: string) => Promise<boolean>
|
||||
requestUpdateProcessedWordById: (wordId: string, newTextValue: string) => Promise<boolean>
|
||||
getProcessedAreaById: (areaId: string) => Promise<ipc.ProcessedArea | undefined>
|
||||
} & ProjectProps
|
||||
180
frontend/package-lock.json
generated
180
frontend/package-lock.json
generated
@ -32,6 +32,7 @@
|
||||
"postcss": "^8.4.19",
|
||||
"server-only": "^0.0.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typedoc": "^0.24.7",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
},
|
||||
@ -971,6 +972,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-sequence-parser": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz",
|
||||
"integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@ -3050,6 +3057,12 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
|
||||
@ -3148,6 +3161,24 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-definitions": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
|
||||
@ -4601,6 +4632,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz",
|
||||
"integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-sequence-parser": "^1.1.0",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"vscode-oniguruma": "^1.7.0",
|
||||
"vscode-textmate": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
@ -4984,6 +5027,51 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc": {
|
||||
"version": "0.24.7",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz",
|
||||
"integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^4.3.0",
|
||||
"minimatch": "^9.0.0",
|
||||
"shiki": "^0.14.1"
|
||||
},
|
||||
"bin": {
|
||||
"typedoc": "bin/typedoc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc/node_modules/minimatch": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
|
||||
@ -5204,6 +5292,18 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-oniguruma": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
|
||||
"integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vscode-textmate": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
|
||||
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wasm-feature-detect": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz",
|
||||
@ -5954,6 +6054,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
"ansi-sequence-parser": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz",
|
||||
"integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@ -7428,6 +7534,12 @@
|
||||
"minimist": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"jsx-ast-utils": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
|
||||
@ -7502,6 +7614,18 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
|
||||
"dev": true
|
||||
},
|
||||
"marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"dev": true
|
||||
},
|
||||
"mdast-util-definitions": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
|
||||
@ -8385,6 +8509,18 @@
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"shiki": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz",
|
||||
"integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-sequence-parser": "^1.1.0",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"vscode-oniguruma": "^1.7.0",
|
||||
"vscode-textmate": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
@ -8664,6 +8800,38 @@
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
|
||||
},
|
||||
"typedoc": {
|
||||
"version": "0.24.7",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz",
|
||||
"integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^4.3.0",
|
||||
"minimatch": "^9.0.0",
|
||||
"shiki": "^0.14.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.9.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
|
||||
@ -8809,6 +8977,18 @@
|
||||
"unist-util-stringify-position": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"vscode-oniguruma": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
|
||||
"integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
|
||||
"dev": true
|
||||
},
|
||||
"vscode-textmate": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
|
||||
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
|
||||
"dev": true
|
||||
},
|
||||
"wasm-feature-detect": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz",
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
"postcss": "^8.4.19",
|
||||
"server-only": "^0.0.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typedoc": "^0.24.7",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
e8bab26469d3f6b725d33cef9359048b
|
||||
2415a78ef8f325df057b22f577cbbe50
|
||||
Loading…
x
Reference in New Issue
Block a user