refact: replace useStage() with redux

This commit is contained in:
Joshua Shoemaker 2023-09-04 11:26:18 -05:00
parent 4c203d9bd0
commit d45465649f
14 changed files with 90 additions and 170 deletions

View File

@ -1,13 +1,14 @@
'use client'
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import Konva from 'konva'
import { Group, Rect } from 'react-konva'
import { KonvaEventObject } from 'konva/lib/Node'
import { entities } from '../../wailsjs/wailsjs/go/models'
import { useProject } from '../../context/Project/provider'
import AreaContextMenu from './AreaContextMenu'
import { useStage } from './context/provider'
import { RootState } from '../../redux/store'
type Props = {
isActive: boolean,
@ -17,8 +18,8 @@ type Props = {
type coordinates = { x: number, y: number }
const Area = (props: Props) => {
const { scale } = useSelector((state: RootState) => state.stage)
const { selectedAreaId, setSelectedAreaId } = useProject()
const { scale } = useStage()
const shapeRef = React.useRef<Konva.Rect>(null)
const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false)
const [areaContextMenuPosition, setAreaContextMenuPosition] = useState<coordinates>()

View File

@ -1,19 +1,20 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { Group } from 'react-konva'
import { useProject } from '../../context/Project/provider'
import { entities } from '../../wailsjs/wailsjs/go/models'
import Area from './Area'
import ProcessedWord from './ProcessedWord'
import EditingWord from './EditingWord'
import { useStage } from './context/provider'
import { RootState } from '../../redux/store'
type Props = { scale: number }
const Areas = ({ scale }: Props) => {
const { areProcessedWordsVisible } = useSelector((state: RootState) => state.stage)
const { getSelectedDocument, selectedAreaId, getProcessedAreaById } = useProject()
const { isProcessedWordsVisible } = useStage()
const areas = getSelectedDocument()?.areas || []
const [editingWord, setEditingWord] = useState<entities.ProcessedWord | null>(null)
const [selectedProcessedArea, setSelectedProcessedArea] = useState<entities.ProcessedArea | null>(null)
@ -59,8 +60,8 @@ const Areas = ({ scale }: Props) => {
return <Group>
{renderAreas(areas)}
{isProcessedWordsVisible ? renderProcessedWords() : <></>}
{isProcessedWordsVisible ? renderEditingWord() : <></>}
{areProcessedWordsVisible ? renderProcessedWords() : <></>}
{areProcessedWordsVisible ? renderEditingWord() : <></>}
</Group>
}

View File

@ -1,6 +1,7 @@
'use client'
import React, { useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Stage, Layer, Image, } from 'react-konva'
import { KonvaEventObject } from 'konva/lib/Node'
import Areas from './Areas'
@ -9,16 +10,25 @@ import useImage from 'use-image'
import { RectangleCoordinates } from './types'
import DrawingArea from './DrawingArea'
import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds'
import { useStage } from './context/provider'
import ContextConnections from './ContextConnections'
import processImageRect from '../../useCases/processImageRect'
import { RootState } from '../../redux/store'
import { maxScale, scaleStep, setIsDrawingArea, setScale, setStartingContextConnectionPoint } from '../../redux/features/stage/stageSlice'
let downClickX: number
let downClickY: number
const CanvasStage = () => {
const dispatch = useDispatch()
const {
scale, size,
isDrawingArea,
areAreasVisible,
areLinkAreaContextsVisible,
startingContextConnectionPoint
} = useSelector((state: RootState) => state.stage)
const { getSelectedDocument, updateDocuments, setSelectedAreaId } = useProject()
const { scale, scaleStep, maxScale, size, setScale, isAreasVisible, isLinkAreaContextsVisible, isDrawingArea, setIsDrawingArea, startingContextConnection, setStartingContextConnection } = useStage()
const [documentImage] = useImage(getSelectedDocument()?.path || '')
const documentRef = useRef(null)
const [drawingAreaRect, setDrawingAreaRect] = useState<RectangleCoordinates | null>(null)
@ -27,13 +37,13 @@ const CanvasStage = () => {
const documentHeight = documentImage?.naturalHeight || 0
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
if (startingContextConnection) return setStartingContextConnection(null) // TODO: handle if clicking o connect
if (startingContextConnectionPoint) return dispatch(setStartingContextConnectionPoint(null)) // TODO: handle if clicking o connect
if (!e.evt.shiftKey) return e.currentTarget.startDrag()
const position = e.currentTarget.getRelativePointerPosition()
downClickX = position.x
downClickY = position.y
setIsDrawingArea(true)
dispatch(setIsDrawingArea(true))
}
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
@ -49,7 +59,7 @@ const CanvasStage = () => {
const handleMouseUp = (e: KonvaEventObject<MouseEvent>) => {
const stage = e.currentTarget
if (stage.isDragging()) stage.stopDrag()
else if (isDrawingArea) setIsDrawingArea(false)
else if (isDrawingArea) dispatch(setIsDrawingArea(false))
if (!drawingAreaRect) return
@ -70,8 +80,8 @@ const CanvasStage = () => {
const wheelDelta = e.evt.deltaY
const shouldAttemptScaleUp = (wheelDelta < 0) && scale < maxScale
if (shouldAttemptScaleUp) setScale(scale + scaleStep)
else if (scale > (scaleStep * 2)) setScale(scale - scaleStep)
if (shouldAttemptScaleUp) dispatch(setScale(scale + scaleStep))
else if (scale > (scaleStep * 2)) dispatch(setScale(scale - scaleStep))
}
return <Stage width={size.width} height={size.height} scale={{ x: scale, y: scale }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onWheel={handleWheel}>
@ -90,13 +100,13 @@ const CanvasStage = () => {
/>
{(isDrawingArea && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
</Layer>
{isAreasVisible
{areAreasVisible
? <Layer id='areaLayer'>
<Areas scale={scale} />
</Layer>
: <></>
}
{isAreasVisible && isLinkAreaContextsVisible
{areAreasVisible && areLinkAreaContextsVisible
? <Layer id='contextConnections'>
<ContextConnections />
</Layer>

View File

@ -1,17 +1,17 @@
'use client'
import React from 'react'
import { useSelector } from 'react-redux'
import { Group, Line } from 'react-konva'
import { useProject } from '../../../context/Project/provider'
import { useStage } from '../context/provider'
import { RootState } from '../../../redux/store'
const ConnectionLines = () => {
const { scale } = useStage()
const { scale } = useSelector((state: RootState) => state.stage)
const { getSelectedDocument, contextGroups } = useProject()
const areas = getSelectedDocument()?.areas || []
const renderLines = () => {
console.log('contextGroups', contextGroups)
if (!contextGroups?.length) return <></>
const linesAlreadyRendered = new Set<string>()

View File

@ -1,14 +1,18 @@
'use client'
import { Circle, Group } from 'react-konva'
import { useStage } from '../context/provider'
import { useDispatch, useSelector } from 'react-redux'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import { KonvaEventObject } from 'konva/lib/Node'
import { useProject } from '../../../context/Project/provider'
import { RootState } from '../../../redux/store'
import { setStartingContextConnectionPoint } from '../../../redux/features/stage/stageSlice'
type Props = { areas: entities.Area[] }
const ConnectionPoints = (props: Props) => {
const { isLinkAreaContextsVisible, scale, startingContextConnection, setStartingContextConnection } = useStage()
const dispatch = useDispatch()
const { scale, areLinkAreaContextsVisible, startingContextConnectionPoint } = useSelector((state: RootState) => state.stage)
const { requestConnectProcessedAreas } = useProject()
const handleContextAreaMouseDown = async (e: KonvaEventObject<MouseEvent>) => {
@ -18,15 +22,15 @@ const ConnectionPoints = (props: Props) => {
areaId: e.currentTarget.attrs.id
}
if (!startingContextConnection) return setStartingContextConnection(clickedConnectionPoint)
if (!startingContextConnectionPoint) return dispatch(setStartingContextConnectionPoint(clickedConnectionPoint))
if (clickedConnectionPoint.isHead === startingContextConnection.isHead
|| clickedConnectionPoint.areaId === startingContextConnection.areaId)
return setStartingContextConnection(null)
if (clickedConnectionPoint.isHead === startingContextConnectionPoint.isHead
|| clickedConnectionPoint.areaId === startingContextConnectionPoint.areaId)
return dispatch(setStartingContextConnectionPoint(null))
const headId = startingContextConnection.isHead ? startingContextConnection.areaId : clickedConnectionPoint.areaId
const tailId = !startingContextConnection.isHead ? startingContextConnection.areaId : clickedConnectionPoint.areaId
setStartingContextConnection(null)
const headId = startingContextConnectionPoint.isHead ? startingContextConnectionPoint.areaId : clickedConnectionPoint.areaId
const tailId = !startingContextConnectionPoint.isHead ? startingContextConnectionPoint.areaId : clickedConnectionPoint.areaId
dispatch(setStartingContextConnectionPoint(null))
try {
await requestConnectProcessedAreas(headId, tailId)
@ -36,7 +40,7 @@ const ConnectionPoints = (props: Props) => {
}
const renderConnectingPointsForArea = (a: entities.Area) => {
if (!isLinkAreaContextsVisible) return <></>
if (!areLinkAreaContextsVisible) return <></>
const headConnector = <Circle
key={`head-${a.id}`}
@ -68,12 +72,12 @@ const ConnectionPoints = (props: Props) => {
let connectorsToRender = []
if (!startingContextConnection) connectorsToRender = [headConnector, tailConnector]
else if (startingContextConnection.isHead) connectorsToRender = [tailConnector]
if (!startingContextConnectionPoint) connectorsToRender = [headConnector, tailConnector]
else if (startingContextConnectionPoint.isHead) connectorsToRender = [tailConnector]
else connectorsToRender = [headConnector]
if (startingContextConnection?.areaId === a.id) {
let y = (startingContextConnection.isHead ? a.startY : a.endY) * scale
if (startingContextConnectionPoint?.areaId === a.id) {
let y = (startingContextConnectionPoint.isHead ? a.startY : a.endY) * scale
connectorsToRender.push(<Circle
key={`active-${a.id}`}
id={a.id}
@ -81,11 +85,11 @@ const ConnectionPoints = (props: Props) => {
x={((a.startX + a.endX) * scale) / 2}
y={y}
strokeEnabled={false}
fill={startingContextConnection.isHead ? '#dc8dec' : '#1e1e1e'}
fill={startingContextConnectionPoint.isHead ? '#dc8dec' : '#1e1e1e'}
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
isHead={startingContextConnection.isHead}
onMouseDown={() => setStartingContextConnection(null)}
isHead={startingContextConnectionPoint.isHead}
onMouseDown={() => dispatch(setStartingContextConnectionPoint(null))}
/>)
}

View File

@ -1,23 +1,24 @@
'use client'
import React from 'react'
import { useSelector } from 'react-redux'
import { Line } from 'react-konva'
import { Coordinates } from '../types'
import { useStage } from '../context/provider'
import { useProject } from '../../../context/Project/provider'
import { RootState } from '../../../redux/store'
type CurrentDrawingConnectionProps = {
endDrawingPosition: Coordinates | null
}
const CurrentDrawingConnection = (props: CurrentDrawingConnectionProps) => {
const { scale, startingContextConnectionPoint } = useSelector((state: RootState) => state.stage)
const { endDrawingPosition } = props
const { startingContextConnection, scale } = useStage()
const { getSelectedDocument } = useProject()
const areas = getSelectedDocument()?.areas || []
if (!startingContextConnection || !endDrawingPosition) return <></>
if (!startingContextConnectionPoint || !endDrawingPosition) return <></>
const { areaId, isHead } = startingContextConnection
const { areaId, isHead } = startingContextConnectionPoint
const area = areas.find(a => a.id === areaId)
if (!area) return <></>

View File

@ -1,24 +1,26 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { Group } from 'react-konva'
import { useStage } from '../context/provider'
import { useProject } from '../../../context/Project/provider'
import Konva from 'konva'
import { Coordinates } from '../types'
import CurrentDrawingConnection from './CurrentDrawingConnection'
import ConnectionPoints from './ConnectionPoints'
import ConnectionLines from './ConnectionLines'
import { RootState } from '../../../redux/store'
const ContextConnections = () => {
const { startingContextConnectionPoint, areLinkAreaContextsVisible } = useSelector((state: RootState) => state.stage)
const { getSelectedDocument } = useProject()
const { isLinkAreaContextsVisible, startingContextConnection, scale } = useStage()
const areas = getSelectedDocument()?.areas || []
const [endDrawingPosition, setEndDrawingPosition] = useState<Coordinates | null>(null)
const handleMouseMove = (e: MouseEvent) => {
if (!isLinkAreaContextsVisible || !startingContextConnection) return
if (!areLinkAreaContextsVisible || !startingContextConnectionPoint) return
setEndDrawingPosition(Konva.stages[0].getRelativePointerPosition())
}
@ -28,10 +30,10 @@ const ContextConnections = () => {
})
useEffect(() => {
if (!startingContextConnection) setEndDrawingPosition(null)
}, [startingContextConnection])
if (!startingContextConnectionPoint) setEndDrawingPosition(null)
}, [startingContextConnectionPoint])
if (!isLinkAreaContextsVisible) return <></>
if (!areLinkAreaContextsVisible) return <></>
return <Group>
<ConnectionPoints areas={areas} />

View File

@ -1,27 +1,29 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { DocumentTextIcon, LanguageIcon, LinkIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, SquaresPlusIcon } from '@heroicons/react/24/outline'
import { useProject } from '../../../context/Project/provider'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import LanguageSelect from '../../utils/LanguageSelect'
import { useStage } from '../context/provider'
import ToolToggleButton from './ToolToggleButton'
import processImageArea from '../../../useCases/processImageArea'
import { pushNotification } from '../../../redux/features/notifications/notificationQueueSlice'
import { RootState } from '../../../redux/store'
import { maxScale, scaleStep, setAreAreasVisible, setAreLinkAreaContextsVisible, setAreProcessedWordsVisible, setAreTranslatedWordsVisible, setScale } from '../../../redux/features/stage/stageSlice'
const ToolingOverlay = () => {
const dispatch = useDispatch()
const { getSelectedDocument, selectedAreaId, requestUpdateArea, requestUpdateDocument, updateDocuments } = useProject()
const {
scale, scaleStep, maxScale, setScale,
isLinkAreaContextsVisible, setIsLinkAreaContextsVisible,
isAreasVisible, setIsAreasVisible,
isProcessedWordsVisible, setIsProcessedWordsVisible,
isTranslatedWordsVisible, setIsTranslatedWordsVisible,
} = useStage()
scale,
areAreasVisible,
areLinkAreaContextsVisible,
areProcessedWordsVisible,
areTranslatedWordsVisible,
} = useSelector((state: RootState) => state.stage)
const { getSelectedDocument, selectedAreaId, requestUpdateArea, requestUpdateDocument, updateDocuments } = useProject()
const selectedDocument = getSelectedDocument()
const [selectedArea, setSelectedArea] = useState<entities.Area | undefined>()
@ -95,7 +97,7 @@ const ToolingOverlay = () => {
<input
id="zoomRange" type="range" min={scaleStep} max={maxScale} step={scaleStep}
value={scale} className="w-[calc(100%-50px)] h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer p-0"
onChange={(e) => { setScale(e.currentTarget.valueAsNumber) }}
onChange={(e) => { dispatch(setScale(e.currentTarget.valueAsNumber)) }}
/>
<MagnifyingGlassPlusIcon className='w-4 h-4' />
</div>
@ -103,16 +105,16 @@ const ToolingOverlay = () => {
{/* Right Buttons */}
<div className='absolute bottom-6 right-3 pointer-events-none'>
{isAreasVisible
{areAreasVisible
? <>
<ToolToggleButton icon={LinkIcon} hint='Link Area Contexts' isActive={isLinkAreaContextsVisible} onClick={() => setIsLinkAreaContextsVisible(!isLinkAreaContextsVisible)} />
<ToolToggleButton icon={LanguageIcon} hint='Toggle Translations' isActive={isTranslatedWordsVisible} onClick={() => setIsTranslatedWordsVisible(!isTranslatedWordsVisible)} />
<ToolToggleButton icon={DocumentTextIcon} hint='Toggle Processed' isActive={isProcessedWordsVisible} onClick={() => setIsProcessedWordsVisible(!isProcessedWordsVisible)} />
<ToolToggleButton icon={LinkIcon} hint='Link Area Contexts' isActive={areLinkAreaContextsVisible} onClick={() => dispatch(setAreLinkAreaContextsVisible(!areLinkAreaContextsVisible))} />
<ToolToggleButton icon={LanguageIcon} hint='Toggle Translations' isActive={areTranslatedWordsVisible} onClick={() => dispatch(setAreTranslatedWordsVisible(!areTranslatedWordsVisible))} />
<ToolToggleButton icon={DocumentTextIcon} hint='Toggle Processed' isActive={areProcessedWordsVisible} onClick={() => dispatch(setAreProcessedWordsVisible(!areProcessedWordsVisible))} />
</>
: <></>
}
<ToolToggleButton icon={SquaresPlusIcon} hint='Toggle Areas' isActive={isAreasVisible} onClick={() => setIsAreasVisible(!isAreasVisible)} />
<ToolToggleButton icon={SquaresPlusIcon} hint='Toggle Areas' isActive={areAreasVisible} onClick={() => dispatch(setAreAreasVisible(!areAreasVisible))} />
</div>
</>
}

View File

@ -1,24 +0,0 @@
import { StageContextType } from './types'
const makeDefaultStage = (): StageContextType => ({
scale: 1,
maxScale: 4,
scaleStep: 0.01,
setScale: (_) => {},
isAreasVisible: true,
setIsAreasVisible: (_) => {},
isProcessedWordsVisible: true,
setIsProcessedWordsVisible: (_) => {},
isTranslatedWordsVisible: true,
setIsTranslatedWordsVisible: (_) => {},
isLinkAreaContextsVisible: false,
setIsLinkAreaContextsVisible: (_) => {},
size: { width: 1, height: 1 },
setSize: (_) => {},
isDrawingArea: false,
setIsDrawingArea: (_) => {},
startingContextConnection: null,
setStartingContextConnection: (_) => {},
})
export default makeDefaultStage

View File

@ -1,51 +0,0 @@
'use client'
import { createContext, useContext, useState, ReactNode, } from 'react'
import makeDefaultStage from './makeDefaultStage'
import { StageContextType, StartingContextConnection } from './types'
const StageContext = createContext<StageContextType>(makeDefaultStage())
export function useStage() {
return useContext(StageContext)
}
const maxScale = 4
const scaleStep = 0.01
type Props = { children: ReactNode }
export function StageProvider({ children }: Props) {
const [size, setSize] = useState({width: 1, height: 1})
const [scale, setScale] = useState(1)
const [isAreasVisible, setIsAreasVisible] = useState(true)
const [isProcessedWordsVisible, setIsProcessedWordsVisible] = useState(true)
const [isTranslatedWordsVisible, setIsTranslatedWordsVisible] = useState(true)
const [isLinkAreaContextsVisible, setIsLinkAreaContextsVisible] = useState(false)
const [isDrawingArea, setIsDrawingArea] = useState(false)
const [startingContextConnection, setStartingContextConnection] = useState<StartingContextConnection | null>(null)
const value = {
scale,
maxScale,
scaleStep,
setScale,
isAreasVisible,
setIsAreasVisible,
isProcessedWordsVisible,
setIsProcessedWordsVisible,
isTranslatedWordsVisible,
setIsTranslatedWordsVisible,
isLinkAreaContextsVisible,
setIsLinkAreaContextsVisible,
size,
setSize,
isDrawingArea,
setIsDrawingArea,
startingContextConnection,
setStartingContextConnection,
}
return <StageContext.Provider value={value}>
{ children }
</StageContext.Provider>
}

View File

@ -1,25 +0,0 @@
export type StartingContextConnection = {
isHead: boolean,
areaId: string,
}
export type StageContextType = {
scale: number,
maxScale: number,
scaleStep: number,
setScale: (value: number) => void,
isAreasVisible: boolean,
setIsAreasVisible: (value: boolean) => void,
isProcessedWordsVisible: boolean,
setIsProcessedWordsVisible: (value: boolean) => void,
isTranslatedWordsVisible: boolean,
setIsTranslatedWordsVisible: (value: boolean) => void,
isLinkAreaContextsVisible: boolean,
setIsLinkAreaContextsVisible: (value: boolean) => void,
size: { width: number, height: number }
setSize: (size: {width: number, height: number}) => void,
isDrawingArea: boolean,
setIsDrawingArea: (value: boolean) => void,
startingContextConnection: StartingContextConnection | null,
setStartingContextConnection: (value: StartingContextConnection | null) => void,
}

View File

@ -2,19 +2,21 @@
import dynamic from 'next/dynamic'
import React, { useEffect, useRef } from 'react'
import { useDispatch } from 'react-redux'
import ToolingOverlay from './ToolingOverlay'
import { useStage } from './context/provider'
import { setSize } from '../../redux/features/stage/stageSlice'
const CanvasStage = dynamic(() => import('./CanvasStage'), { ssr: false })
const DocumentCanvas = () => {
const { setSize } = useStage()
const dispatch = useDispatch()
const thisRef = useRef<HTMLDivElement>(null)
const handleWindowResize = () => {
const width = thisRef?.current?.clientWidth || 0
const height = thisRef?.current?.clientHeight || 0
setSize({ width, height })
dispatch(setSize({ width, height }))
}
useEffect(() => {

View File

@ -4,7 +4,6 @@ import { useNavigation } from '../../context/Navigation/provider'
import { workspaces } from '../../context/Navigation/types'
import { useProject } from '../../context/Project/provider'
import DocumentCanvas from '../DocumentCanvas'
import { StageProvider } from '../DocumentCanvas/context/provider'
import NoSelectedDocument from './NoSelectedDocument'
import TextEditor from './TextEditor'
@ -16,9 +15,7 @@ const MainWorkspace = () => {
if (selectedWorkspace === workspaces.TEXTEDITOR) return <TextEditor />
else return !selectedDocumentId
? <NoSelectedDocument />
: <StageProvider>
<DocumentCanvas />
</StageProvider>
: <DocumentCanvas />
}
return <main className=" bg-gray-100 min-h-[calc(100vh-118px)] ml-64 overflow-y-scroll">

View File

@ -2,8 +2,8 @@ import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { ContextConnectionPoint, StageState } from './types'
const maxScale = 4
const scaleStep = 0.01
export const maxScale = 4
export const scaleStep = 0.01
const initialState: StageState = {
size: { width: 1, height: 1 },
@ -46,7 +46,7 @@ export const stageSlice = createSlice({
setIsDrawingArea: (state, action: PayloadAction<boolean>) => {
state.isDrawingArea = action.payload
},
setStartingContextConnectionPoint: (state, action: PayloadAction<ContextConnectionPoint>) => {
setStartingContextConnectionPoint: (state, action: PayloadAction<ContextConnectionPoint | null>) => {
state.startingContextConnectionPoint = action.payload
},
}