feat: area hover text preview
This commit is contained in:
parent
2963dac8a6
commit
1f8ffc01b1
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,5 +1,8 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
},
|
||||
"cSpell.words": [
|
||||
"heroicons"
|
||||
]
|
||||
}
|
||||
@ -7,19 +7,24 @@ 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'
|
||||
|
||||
const zoomStep = 0.025
|
||||
const maxZoomLevel = 4
|
||||
|
||||
const DocumentRenderer = () => {
|
||||
const { getSelectedDocument, requestAddArea, selectedAreaId, setSelectedAreaId } = useProject()
|
||||
const { getSelectedDocument, requestAddArea, selectedAreaId, setSelectedAreaId, getProcessedAreasByDocumentId } = 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 [zoomLevel, setZoomLevel] = useState(1)
|
||||
const [hoverOverAreaId, setHoverOverAreaId] = useState('')
|
||||
const [hoveredProcessedArea, setHoveredProcessedArea] = useState<ipc.ProcessedArea | undefined>()
|
||||
|
||||
let downClickX = 0
|
||||
let downClickY = 0
|
||||
@ -36,6 +41,11 @@ const DocumentRenderer = () => {
|
||||
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
|
||||
@ -63,6 +73,7 @@ const DocumentRenderer = () => {
|
||||
context.drawImage(image, 0, 0, width, height)
|
||||
|
||||
if (areas) applyAreasToCanvas()
|
||||
applyUiCanvasUpdates()
|
||||
}
|
||||
|
||||
const applyAreasToCanvas = () => {
|
||||
@ -96,6 +107,59 @@ const DocumentRenderer = () => {
|
||||
})
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!selectedDocument || !selectedDocument.id || !areaId) return
|
||||
const processedAreas = await getProcessedAreasByDocumentId(selectedDocument.id)
|
||||
const foundProcessedArea = processedAreas.find(a => a.id === areaId)
|
||||
return foundProcessedArea
|
||||
}
|
||||
|
||||
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 => {
|
||||
console.log(response)
|
||||
setHoveredProcessedArea(response)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
@ -144,13 +208,13 @@ const DocumentRenderer = () => {
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
|
||||
let mouseX = e.nativeEvent.offsetX
|
||||
let mouseY = e.nativeEvent.offsetY
|
||||
|
||||
if (isMouseDown) {
|
||||
if (!isMouseDown) handleHoverOverArea(e)
|
||||
else {
|
||||
const drawingCanvasInstance = drawingCanvas.current
|
||||
if (!drawingCanvasInstance) return
|
||||
const context = drawingCanvasInstance.getContext('2d')
|
||||
if (!context) return
|
||||
|
||||
@ -177,6 +241,39 @@ const DocumentRenderer = () => {
|
||||
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='absolute text-center inline-block p-1 bg-opacity-60 bg-black text-white rounded-md shadow-zinc-900 shadow-2xl'
|
||||
style={{
|
||||
fontSize: `${3.4 * zoomLevel}vmin`,
|
||||
width,
|
||||
top: Math.floor(w.boundingBox.y0 * zoomLevel) + height,
|
||||
left: Math.floor(w.boundingBox.x0 * zoomLevel)
|
||||
}}>
|
||||
{w.fullText}
|
||||
</span>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className='relative'>
|
||||
<div className='flex justify-between align-top mb-2'>
|
||||
<div className='flex align-top'>
|
||||
@ -196,7 +293,7 @@ const DocumentRenderer = () => {
|
||||
<div
|
||||
onWheelCapture={handleWheelEvent}
|
||||
className={classNames('relative mt-2 overflow-scroll',
|
||||
'w-[calc(100vw-320px)] h-[calc(100vh-240px)] border-4',
|
||||
'w-[calc(100vw-320px)] h-[calc(100vh-174px)] border-4',
|
||||
'border-dashed border-gray-200')}>
|
||||
<canvas
|
||||
className="absolute"
|
||||
@ -206,6 +303,10 @@ const DocumentRenderer = () => {
|
||||
className="absolute"
|
||||
ref={areaCanvas}
|
||||
/>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={uiCanvas}
|
||||
/>
|
||||
<canvas
|
||||
className="absolute"
|
||||
ref={drawingCanvas}
|
||||
@ -213,6 +314,7 @@ const DocumentRenderer = () => {
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
/>
|
||||
{renderAreaPreview()}
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { useProject } from '../../../context/Project/provider'
|
||||
import classNames from '../../../utils/classNames'
|
||||
import { ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useRef } from 'react'
|
||||
import { useProject } from '../../../context/Project/provider'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import classNames from '../../../utils/classNames'
|
||||
import onEnterHandler from '../../../utils/onEnterHandler'
|
||||
import AreaLineItem from './AreaLineItem'
|
||||
@ -11,7 +12,8 @@ import { SidebarDocument } from './types'
|
||||
const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, index: number }) => {
|
||||
const {
|
||||
getSelectedDocument,
|
||||
requestUpdateDocument
|
||||
requestUpdateDocument,
|
||||
requestDeleteDocumentById,
|
||||
} = useProject()
|
||||
|
||||
const {
|
||||
@ -81,7 +83,8 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
|
||||
}}
|
||||
ref={editDocumentNameTextInput}
|
||||
/>
|
||||
: <a
|
||||
: <span className='flex justify-between items-center'>
|
||||
<a
|
||||
role='button'
|
||||
className={classNames(
|
||||
props.document.id === selectedDocumentId
|
||||
@ -92,6 +95,11 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
|
||||
>
|
||||
{props.document.name}
|
||||
</a>
|
||||
|
||||
<XMarkIcon
|
||||
className='w-5 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
|
||||
onClick={() => requestDeleteDocumentById(props.document.id)} />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
: <details>
|
||||
@ -133,6 +141,10 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
|
||||
{props.document.name}
|
||||
</a>
|
||||
}
|
||||
|
||||
<XMarkIcon
|
||||
className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
|
||||
onClick={() => requestDeleteDocumentById(props.document.id)} />
|
||||
</summary>
|
||||
<ul>
|
||||
{props.document.areas.map((a, index) => (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useProject } from '../../../context/Project/provider'
|
||||
import AddGroupInput from './AddGroupInput'
|
||||
import GroupLineItem from './GroupLineItem'
|
||||
|
||||
@ -16,7 +16,7 @@ loader.config({
|
||||
})
|
||||
|
||||
let editorInteractions: ReturnType<typeof createDiffEditorInteractions>
|
||||
const editorHeightOffset = 234
|
||||
const editorHeightOffset = 174
|
||||
|
||||
const fontSizeStep = 1
|
||||
const maxFontSize = 36
|
||||
|
||||
@ -15,6 +15,7 @@ const makeDefaultProject = (): ProjectContextType => ({
|
||||
requestUpdateArea: (updatedArea) => Promise.resolve(new ipc.Area()),
|
||||
requestDeleteAreaById: (areaId) => Promise.resolve(false),
|
||||
requestAddDocument: (groupId, documentName) => Promise.resolve(new ipc.Document()),
|
||||
requestDeleteDocumentById: (documentId) => Promise.resolve(false),
|
||||
requestAddDocumentGroup: (groupName: string) => Promise.resolve(new ipc.Group()),
|
||||
requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise.resolve(new ipc.UserMarkdown()),
|
||||
getUserMarkdownByDocumentId: (documentId) => Promise.resolve(new ipc.UserMarkdown),
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
RequestDeleteAreaById,
|
||||
RequestChangeGroupOrder,
|
||||
RequestChangeSessionProjectByName,
|
||||
RequestDeleteDocumentAndChildren,
|
||||
} from '../../wailsjs/wailsjs/go/ipc/Channel'
|
||||
import { ipc } from '../../wailsjs/wailsjs/go/models'
|
||||
import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps, UpdateDocumentRequest, UserProps } from './types'
|
||||
@ -35,8 +36,8 @@ 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)
|
||||
setDocuments(response.documents)
|
||||
setGroups(response.groups)
|
||||
Promise.resolve(response)
|
||||
})
|
||||
}
|
||||
@ -48,6 +49,13 @@ export function ProjectProvider({ children, projectProps }: Props) {
|
||||
return response
|
||||
}
|
||||
|
||||
const requestDeleteDocumentById = async (documentId: string): Promise<boolean> => {
|
||||
const wasSuccessfulDeletion = await RequestDeleteDocumentAndChildren(documentId)
|
||||
updateDocuments()
|
||||
saveDocuments()
|
||||
return wasSuccessfulDeletion
|
||||
}
|
||||
|
||||
const requestAddDocumentGroup = async (groupName: string) => {
|
||||
const response = await RequestAddDocumentGroup(groupName)
|
||||
if (response.id) await updateDocuments()
|
||||
@ -195,6 +203,7 @@ export function ProjectProvider({ children, projectProps }: Props) {
|
||||
getAreaById,
|
||||
requestAddArea,
|
||||
requestAddDocument,
|
||||
requestDeleteDocumentById,
|
||||
requestAddDocumentGroup,
|
||||
requestUpdateArea,
|
||||
requestDeleteAreaById,
|
||||
|
||||
@ -45,6 +45,7 @@ export type ProjectContextType = {
|
||||
requestUpdateArea: (area: AreaProps) => Promise<ipc.Area>
|
||||
requestDeleteAreaById: (areaId: string) => Promise<boolean>
|
||||
requestAddDocument: (groupId: string, documentName: string) => Promise<ipc.Document>
|
||||
requestDeleteDocumentById: (documentId: string) => Promise<boolean>
|
||||
requestAddDocumentGroup: (groupName: string) => Promise<ipc.Group>
|
||||
requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise<ipc.UserMarkdown>
|
||||
getUserMarkdownByDocumentId: (documentId: string) => Promise<ipc.UserMarkdown>
|
||||
|
||||
13
frontend/utils/isInBounds.ts
Normal file
13
frontend/utils/isInBounds.ts
Normal file
@ -0,0 +1,13 @@
|
||||
type Point = { x: number, y: number }
|
||||
type Bounds = { startX: number, startY: number, endX: number, endY: number }
|
||||
|
||||
const isInBounds = (point: Point, bounds: Bounds, rectOffsetMultiplier: number = 1) => {
|
||||
const { x, y } = point
|
||||
const { startX, startY, endX, endY } = bounds
|
||||
|
||||
const isInBoundsX = (x >= startX * rectOffsetMultiplier) && x <= endX * rectOffsetMultiplier
|
||||
const isInBoundsY = (y >= startY * rectOffsetMultiplier) && y <= endY * rectOffsetMultiplier
|
||||
return isInBoundsX && isInBoundsY
|
||||
}
|
||||
|
||||
export default isInBounds
|
||||
2
frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts
vendored
2
frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts
vendored
@ -42,6 +42,8 @@ export function RequestChooseUserAvatar():Promise<string>;
|
||||
|
||||
export function RequestDeleteAreaById(arg1:string):Promise<boolean>;
|
||||
|
||||
export function RequestDeleteDocumentAndChildren(arg1:string):Promise<boolean>;
|
||||
|
||||
export function RequestSaveDocumentCollection():Promise<boolean>;
|
||||
|
||||
export function RequestSaveGroupCollection():Promise<boolean>;
|
||||
|
||||
@ -82,6 +82,10 @@ export function RequestDeleteAreaById(arg1) {
|
||||
return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1);
|
||||
}
|
||||
|
||||
export function RequestDeleteDocumentAndChildren(arg1) {
|
||||
return window['go']['ipc']['Channel']['RequestDeleteDocumentAndChildren'](arg1);
|
||||
}
|
||||
|
||||
export function RequestSaveDocumentCollection() {
|
||||
return window['go']['ipc']['Channel']['RequestSaveDocumentCollection']();
|
||||
}
|
||||
|
||||
@ -144,6 +144,26 @@ func (c *Channel) RequestAddDocument(groupId string, documentName string) Docume
|
||||
return documentResponse
|
||||
}
|
||||
|
||||
func (c *Channel) deleteDocumentById(documentId string) bool {
|
||||
collection := document.GetDocumentCollection()
|
||||
|
||||
documentToDeleteIndex := -1
|
||||
for i, d := range collection.Documents {
|
||||
if d.Id == documentId {
|
||||
documentToDeleteIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if documentToDeleteIndex < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
collection.Documents[documentToDeleteIndex] = collection.Documents[len(collection.Documents)-1]
|
||||
collection.Documents = collection.Documents[:len(collection.Documents)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Channel) RequestUpdateDocumentUserMarkdown(documentId string, markdown string) UserMarkdown {
|
||||
markdownCollection := document.GetUserMarkdownCollection()
|
||||
markdownToUpdate := markdownCollection.GetUserMarkdownByDocumentId(documentId)
|
||||
@ -166,6 +186,26 @@ func (c *Channel) RequestUpdateDocumentUserMarkdown(documentId string, markdown
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) deleteDocumentUserMarkdown(documentId string) bool {
|
||||
collection := document.GetUserMarkdownCollection()
|
||||
|
||||
markdownToDeleteIndex := -1
|
||||
for i, d := range collection.Values {
|
||||
if d.DocumentId == documentId {
|
||||
markdownToDeleteIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if markdownToDeleteIndex < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
collection.Values[markdownToDeleteIndex] = collection.Values[len(collection.Values)-1]
|
||||
collection.Values = collection.Values[:len(collection.Values)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Channel) GetUserMarkdownByDocumentId(documentId string) UserMarkdown {
|
||||
foundUserMarkdown := document.GetUserMarkdownCollection().GetUserMarkdownByDocumentId((documentId))
|
||||
|
||||
@ -548,3 +588,19 @@ func (c *Channel) RequestSaveLocalUserProcessedMarkdownCollection() bool {
|
||||
|
||||
return successfulWrite
|
||||
}
|
||||
|
||||
func (c *Channel) RequestDeleteDocumentAndChildren(documentId string) bool {
|
||||
success := true
|
||||
|
||||
deletedDocument := c.deleteDocumentById(documentId)
|
||||
if !deletedDocument {
|
||||
success = false
|
||||
}
|
||||
|
||||
deletedUserMarkDown := c.deleteDocumentUserMarkdown(documentId)
|
||||
if !deletedUserMarkDown {
|
||||
success = false
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
@ -146,25 +146,26 @@ func deserializeProcessedArea(area ProcessedArea) document.ProcessedArea {
|
||||
}
|
||||
|
||||
func (c *Channel) RequestAddProcessedArea(processedArea ProcessedArea) ProcessedArea {
|
||||
doesAreaAlreadyExist := false
|
||||
processedAreasOfDocument := document.GetProcessedAreaCollection().GetAreasByDocumentId(processedArea.DocumentId)
|
||||
for _, a := range processedAreasOfDocument {
|
||||
if a.Order == processedArea.Order {
|
||||
doesAreaAlreadyExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// doesAreaAlreadyExist := false
|
||||
// processedAreasOfDocuments := document.GetProcessedAreaCollection().GetAreasByDocumentId(processedArea.DocumentId)
|
||||
|
||||
// for _, a := range processedAreasOfDocuments {
|
||||
// if a.Order == processedArea.Order {
|
||||
// doesAreaAlreadyExist = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
deserializedProcessedArea := deserializeProcessedArea(processedArea)
|
||||
|
||||
if doesAreaAlreadyExist {
|
||||
storedProcessedArea := document.GetProcessedAreaCollection().GetAreaById(processedArea.Id)
|
||||
if storedProcessedArea.Id != "" {
|
||||
storedProcessedArea = &deserializedProcessedArea
|
||||
}
|
||||
} else {
|
||||
// if doesAreaAlreadyExist {
|
||||
// storedProcessedArea := document.GetProcessedAreaCollection().GetAreaById(processedArea.Id)
|
||||
// if storedProcessedArea.Id != "" {
|
||||
// storedProcessedArea = &deserializedProcessedArea
|
||||
// }
|
||||
// } else {
|
||||
document.GetProcessedAreaCollection().AddProcessedArea((deserializedProcessedArea))
|
||||
}
|
||||
// }
|
||||
|
||||
return processedArea
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user