From 1f8ffc01b178b33c96bb2e804b8ad265142c01bf Mon Sep 17 00:00:00 2001 From: Joshua Shoemaker Date: Sat, 20 May 2023 19:41:00 -0500 Subject: [PATCH] feat: area hover text preview --- .vscode/settings.json | 5 +- .../components/workspace/DocumentRenderer.tsx | 116 ++++++++- .../workspace/Sidebar/AreaLineItem.tsx | 2 +- .../workspace/Sidebar/DocumentLineItem.tsx | 234 +++++++++--------- .../components/workspace/Sidebar/Sidebar.tsx | 2 +- frontend/components/workspace/TextEditor.tsx | 2 +- .../context/Project/makeDefaultProject.ts | 1 + frontend/context/Project/provider.tsx | 13 +- frontend/context/Project/types.ts | 1 + frontend/utils/isInBounds.ts | 13 + frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts | 2 + frontend/wailsjs/wailsjs/go/ipc/Channel.js | 4 + ipc/Documents.go | 56 +++++ ipc/ProcessedDocument.go | 33 +-- 14 files changed, 344 insertions(+), 140 deletions(-) create mode 100644 frontend/utils/isInBounds.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index dea696c..2a15253 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "files.associations": { "*.css": "tailwindcss" - } + }, + "cSpell.words": [ + "heroicons" + ] } \ No newline at end of file diff --git a/frontend/components/workspace/DocumentRenderer.tsx b/frontend/components/workspace/DocumentRenderer.tsx index 54c25b7..adf8fd6 100644 --- a/frontend/components/workspace/DocumentRenderer.tsx +++ b/frontend/components/workspace/DocumentRenderer.tsx @@ -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(null) const areaCanvas = useRef(null) + const uiCanvas = useRef(null) const drawingCanvas = useRef(null) const [zoomLevel, setZoomLevel] = useState(1) + const [hoverOverAreaId, setHoverOverAreaId] = useState('') + const [hoveredProcessedArea, setHoveredProcessedArea] = useState() 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
+ { + 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 + {w.fullText} + + }) + } +
+ } + return
@@ -196,16 +293,20 @@ const DocumentRenderer = () => {
+ { onMouseUp={handleMouseUp} onMouseMove={handleMouseMove} /> + {renderAreaPreview()}
} diff --git a/frontend/components/workspace/Sidebar/AreaLineItem.tsx b/frontend/components/workspace/Sidebar/AreaLineItem.tsx index 2721468..b2ab129 100644 --- a/frontend/components/workspace/Sidebar/AreaLineItem.tsx +++ b/frontend/components/workspace/Sidebar/AreaLineItem.tsx @@ -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' diff --git a/frontend/components/workspace/Sidebar/DocumentLineItem.tsx b/frontend/components/workspace/Sidebar/DocumentLineItem.tsx index 97c4208..cc0e25a 100644 --- a/frontend/components/workspace/Sidebar/DocumentLineItem.tsx +++ b/frontend/components/workspace/Sidebar/DocumentLineItem.tsx @@ -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 { @@ -54,57 +56,17 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i return (
  • - {!props.document.areas.length - ? -
    onDocumentClickHandler(props.document.id)} - onDoubleClick={() => onDocumentDoubleClickHandler(props.document.id)} - className={classNames( - props.document.id === selectedDocumentId - ? 'bg-gray-900 text-white' - : 'text-gray-300 hover:bg-gray-700 hover:text-white', - 'group items-center py-2 text-base font-medium rounded-b-md pl-10', - props.index !== 0 ? 'rounded-t-md' : '', - )}> - {selectedDocumentId === props.document.id && isEditDocumentNameInputShowing - ? { - onEnterHandler(event, - () => onConfirmDocumentNameChangeHandler(event.currentTarget.value)) - }} - ref={editDocumentNameTextInput} - /> - : - {props.document.name} - - } -
    - :
    - onDocumentClickHandler(props.document.id)} onDoubleClick={() => onDocumentDoubleClickHandler(props.document.id)} className={classNames( props.document.id === selectedDocumentId ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white', - 'group items-center py-2 text-base font-medium rounded-b-md pl-6', + 'group items-center py-2 text-base font-medium rounded-b-md pl-10', props.index !== 0 ? 'rounded-t-md' : '', - )}> {selectedDocumentId === props.document.id && isEditDocumentNameInputShowing ? - : - {props.document.name} - + : + + {props.document.name} + + + requestDeleteDocumentById(props.document.id)} /> + } - - -
    - } -
  • +
    + :
    + onDocumentClickHandler(props.document.id)} + onDoubleClick={() => onDocumentDoubleClickHandler(props.document.id)} + className={classNames( + props.document.id === selectedDocumentId + ? 'bg-gray-900 text-white' + : 'text-gray-300 hover:bg-gray-700 hover:text-white', + 'group items-center py-2 text-base font-medium rounded-b-md pl-6', + props.index !== 0 ? 'rounded-t-md' : '', + + )}> + {selectedDocumentId === props.document.id && isEditDocumentNameInputShowing + ? { + onEnterHandler(event, + () => onConfirmDocumentNameChangeHandler(event.currentTarget.value)) + }} + ref={editDocumentNameTextInput} + /> + : + {props.document.name} + + } + + requestDeleteDocumentById(props.document.id)} /> + + +
    + } + ) } diff --git a/frontend/components/workspace/Sidebar/Sidebar.tsx b/frontend/components/workspace/Sidebar/Sidebar.tsx index 02676bb..3a5ec46 100644 --- a/frontend/components/workspace/Sidebar/Sidebar.tsx +++ b/frontend/components/workspace/Sidebar/Sidebar.tsx @@ -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' diff --git a/frontend/components/workspace/TextEditor.tsx b/frontend/components/workspace/TextEditor.tsx index 57e0871..b30c562 100644 --- a/frontend/components/workspace/TextEditor.tsx +++ b/frontend/components/workspace/TextEditor.tsx @@ -16,7 +16,7 @@ loader.config({ }) let editorInteractions: ReturnType -const editorHeightOffset = 234 +const editorHeightOffset = 174 const fontSizeStep = 1 const maxFontSize = 36 diff --git a/frontend/context/Project/makeDefaultProject.ts b/frontend/context/Project/makeDefaultProject.ts index 54af595..8d9b7cc 100644 --- a/frontend/context/Project/makeDefaultProject.ts +++ b/frontend/context/Project/makeDefaultProject.ts @@ -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), diff --git a/frontend/context/Project/provider.tsx b/frontend/context/Project/provider.tsx index 5719928..fd6f972 100644 --- a/frontend/context/Project/provider.tsx +++ b/frontend/context/Project/provider.tsx @@ -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 => { + 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, diff --git a/frontend/context/Project/types.ts b/frontend/context/Project/types.ts index 8b3e11b..194e45c 100644 --- a/frontend/context/Project/types.ts +++ b/frontend/context/Project/types.ts @@ -45,6 +45,7 @@ export type ProjectContextType = { requestUpdateArea: (area: AreaProps) => Promise requestDeleteAreaById: (areaId: string) => Promise requestAddDocument: (groupId: string, documentName: string) => Promise + requestDeleteDocumentById: (documentId: string) => Promise requestAddDocumentGroup: (groupName: string) => Promise requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise getUserMarkdownByDocumentId: (documentId: string) => Promise diff --git a/frontend/utils/isInBounds.ts b/frontend/utils/isInBounds.ts new file mode 100644 index 0000000..b060570 --- /dev/null +++ b/frontend/utils/isInBounds.ts @@ -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 diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts index 8e0d8b7..72c5042 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts @@ -42,6 +42,8 @@ export function RequestChooseUserAvatar():Promise; export function RequestDeleteAreaById(arg1:string):Promise; +export function RequestDeleteDocumentAndChildren(arg1:string):Promise; + export function RequestSaveDocumentCollection():Promise; export function RequestSaveGroupCollection():Promise; diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.js b/frontend/wailsjs/wailsjs/go/ipc/Channel.js index a5a5926..b9ee10c 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.js +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.js @@ -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'](); } diff --git a/ipc/Documents.go b/ipc/Documents.go index f8bbafd..e881ff1 100644 --- a/ipc/Documents.go +++ b/ipc/Documents.go @@ -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 +} diff --git a/ipc/ProcessedDocument.go b/ipc/ProcessedDocument.go index c9628a3..d506af3 100644 --- a/ipc/ProcessedDocument.go +++ b/ipc/ProcessedDocument.go @@ -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 { - document.GetProcessedAreaCollection().AddProcessedArea((deserializedProcessedArea)) - } + // if doesAreaAlreadyExist { + // storedProcessedArea := document.GetProcessedAreaCollection().GetAreaById(processedArea.Id) + // if storedProcessedArea.Id != "" { + // storedProcessedArea = &deserializedProcessedArea + // } + // } else { + document.GetProcessedAreaCollection().AddProcessedArea((deserializedProcessedArea)) + // } return processedArea }