Compare commits

...

8 Commits

Author SHA1 Message Date
Yehoshua Sandler
806f4a28e4
Refactor Stage Context to redux (#7)
* feat: setup stage slice

* refact: replace useStage() with redux
2023-09-04 11:32:54 -05:00
Yehoshua Sandler
095c1ca8ec
Refactor notifications to redux (#6)
* feat: make new connections

refact: context groups | feat: area detection

and a bunch of small things. hate yourself for this massive commit

* refact: initial RTK setup for Notifications

* refact: removed Notification Context
2023-09-04 10:02:08 -05:00
Yehoshua Sandler
7dd6de064f
Refactor Context Groups & Area Detection (#4)
* feat: make new connections

* refact: context groups | feat: area detection

and a bunch of small things. hate yourself for this massive commit
2023-09-02 10:58:38 -05:00
Joshua Shoemaker
ee5ac6ea69 basic init readme 2023-07-24 08:01:25 -05:00
Joshua Shoemaker
f129a9cb13 feat: starting features of translation & contexts 2023-07-24 07:45:03 -05:00
Yehoshua Sandler
917662e9ba
feat: context menu & notification system (#3) 2023-07-01 12:31:25 -05:00
Yehoshua Sandler
1631271b93
Replace native canvas implementation with Konva library (#2)
* style: spelling

* refact: canvases replaced with konva

* refact: area text calculated by words

* refact: moved konva files out of test dir
2023-06-27 08:42:44 -05:00
Yehoshua Sandler
c49f8e4d07
refact: generalized back end structs (#1)
* refact: generalized back end structs

* refact: fixed front end type, removed dead code

* removed test image folder

* refact: removed dead structs
2023-05-26 19:23:35 -05:00
110 changed files with 3414 additions and 2057 deletions

View File

@ -3,7 +3,15 @@
"*.css": "tailwindcss"
},
"cSpell.words": [
"consts",
"headlessui",
"heroicons",
"konva",
"libretranslate",
"reduxjs",
"tailwindcss",
"Tesseract",
"Textualize",
"wailsjs"
]
}

View File

@ -1 +1,14 @@
# Textualize
Textualize is a desktop application designed to process your photos or scans of physical text documents, convert them into textual data and translate them.
Textualize comes with an interface to edit, modify, and manage your projects, making it a powerful tool to work on entire volumes as well as single page documents.
![image](/docs/assets/overviewScreenshot.png)
This project is still under development. Currently on English, classic Hebrew script, and Rashi script are available options for textualizing in the GUI.
Context linking for translations and translation output are still very much buggy and not accessible through the GUI
Built with Wails
Tested only on MacOS

View File

@ -5,6 +5,7 @@ import (
"fmt"
document "textualize/core/Document"
session "textualize/core/Session"
"textualize/entities"
storage "textualize/storage"
)
@ -26,10 +27,10 @@ func (a *App) Startup(ctx context.Context) {
a.Context = ctx
localUserData := storage.GetDriver().ReadUserData()
session.InitializeModule(session.Session{
User: session.User(localUserData),
User: entities.User(localUserData),
})
document.InitizeModule()
document.InitializeModule()
fmt.Println(localUserData)
}

View File

@ -1,22 +1,26 @@
package consts
type Language struct {
DisplayName string
ProcessCode string
TranslateCode string
}
import "textualize/entities"
func GetSuppportedLanguages() []Language {
return []Language{
func GetSupportedLanguages() []entities.Language {
return []entities.Language{
{
DisplayName: "English",
ProcessCode: "eng",
TranslateCode: "en",
IsBundledCustom: false,
},
{
DisplayName: "Hebrew",
DisplayName: "Hebrew - Classic",
ProcessCode: "heb",
TranslateCode: "he",
IsBundledCustom: false,
},
{
DisplayName: "Hebrew - Rashi",
ProcessCode: "heb_rashi",
TranslateCode: "he",
IsBundledCustom: true,
},
}
}

View File

@ -0,0 +1,147 @@
package contextGroup
import (
"fmt"
"textualize/entities"
)
type ContextGroupCollection struct {
Groups []entities.LinkedAreaList
}
var contextGroupCollectionInstance *ContextGroupCollection
func GetContextGroupCollection() *ContextGroupCollection {
if contextGroupCollectionInstance == nil {
contextGroupCollectionInstance = &ContextGroupCollection{}
}
return contextGroupCollectionInstance
}
func SetContextGroupCollection(collection ContextGroupCollection) *ContextGroupCollection {
contextGroupCollectionInstance = &collection
return contextGroupCollectionInstance
}
func SetContextGroupCollectionBySerialized(serialized []entities.SerializedLinkedProcessedArea) *ContextGroupCollection {
newInstance := ContextGroupCollection{}
newInstance.Groups = append(newInstance.Groups, entities.DeserializeLinkedAreaList(serialized))
SetContextGroupCollection(newInstance)
return &newInstance
}
func (collection *ContextGroupCollection) DoesGroupExistBetweenProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorAreaId)
descendantGroup, _ := collection.FindGroupByLinkedProcessedAreaId(descendantAreaId)
isAncestorInAnyInGroup := ancestorGroup != nil
isDescendantInAnyInGroup := descendantGroup != nil
areBothInAnyInGroup := isAncestorInAnyInGroup && isDescendantInAnyInGroup
areBothInSameGroup := false
if areBothInAnyInGroup {
areBothInSameGroup = ancestorGroup.Id == descendantGroup.Id
}
return areBothInSameGroup
}
func (collection *ContextGroupCollection) DisconnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
doesConnectionExist := collection.DoesGroupExistBetweenProcessedAreas(ancestorAreaId, descendantAreaId)
if !doesConnectionExist {
return false
}
ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorAreaId)
wasRemoved := false
for i, group := range collection.Groups {
if group.Id == ancestorGroup.Id {
collection.Groups = append(collection.Groups[:i], collection.Groups[i+1:]...)
wasRemoved = true
break
}
}
return wasRemoved
}
func (collection *ContextGroupCollection) FindGroupById(id string) (*entities.LinkedAreaList, error) {
found := false
var foundGroup *entities.LinkedAreaList = nil
for _, group := range collection.Groups {
if group.Id == id {
found = true
foundGroup = &group
}
}
if !found {
return nil, fmt.Errorf("ContextGroupCollection.FindGroupById: Group with id %s not found", id)
}
return foundGroup, nil
}
func (collection *ContextGroupCollection) FindGroupByLinkedProcessedAreaId(id string) (*entities.LinkedAreaList, error) {
found := false
var foundGroup *entities.LinkedAreaList = nil
for _, group := range collection.Groups {
for n := group.First(); n != nil && !found; n = n.GetNext() {
if n.Area.Id == id {
found = true
foundGroup = &group
}
}
}
if !found {
return nil, fmt.Errorf("ContextGroupCollection.FindGroupByLinkedProcessedAreaId: Group with LinkedProcessedArea.Id %s not found", id)
}
return foundGroup, nil
}
func (collection *ContextGroupCollection) ConnectProcessedAreas(ancestorNode entities.ProcessedArea, descendantNode entities.ProcessedArea) bool {
ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorNode.Id)
descendantGroup, _ := collection.FindGroupByLinkedProcessedAreaId(descendantNode.Id)
isAncestorInAnyInGroup := ancestorGroup != nil
isDescendantInAnyInGroup := descendantGroup != nil
isEitherInAnyInGroup := isAncestorInAnyInGroup || isDescendantInAnyInGroup
areBothInAnyInGroup := isAncestorInAnyInGroup && isDescendantInAnyInGroup
areBothInSameGroup := false
if areBothInAnyInGroup {
areBothInSameGroup = ancestorGroup.Id == descendantGroup.Id
}
if areBothInSameGroup {
return true
}
if !isEitherInAnyInGroup {
collection.createNewGroupAndConnectNodes(ancestorNode, descendantNode)
return true
}
if isAncestorInAnyInGroup && !isDescendantInAnyInGroup {
ancestorGroup.InsertAfter(ancestorNode.Id, descendantNode)
return true
}
if !isAncestorInAnyInGroup && isDescendantInAnyInGroup {
descendantGroup.InsertBefore(descendantNode.Id, ancestorNode)
return true
}
return false
}
func (collection *ContextGroupCollection) createNewGroupAndConnectNodes(ancestorNode entities.ProcessedArea, descendantNode entities.ProcessedArea) {
newGroup := entities.LinkedAreaList{
Id: ancestorNode.Id,
DocumentId: ancestorNode.DocumentId,
Head: &entities.LinkedProcessedArea{Area: ancestorNode},
Tail: &entities.LinkedProcessedArea{Area: descendantNode},
}
newGroup.Head.Next = newGroup.Tail
newGroup.Tail.Previous = newGroup.Head
collection.Groups = append(collection.Groups, newGroup)
}

View File

@ -1,36 +1,19 @@
package document
import (
consts "textualize/core/Consts"
"textualize/entities"
)
type Entity struct {
Id string
GroupId string
Name string
Path string
ProjectId string
Areas []Area
DefaultLanguage consts.Language
}
type Entity entities.Document
type Area struct {
Id string
Name string
StartX int
StartY int
EndX int
EndY int
Language consts.Language
Order int
}
type Area entities.Area
func (e *Entity) AddArea(a Area) {
func (e *Entity) AddArea(a entities.Area) {
e.Areas = append(e.Areas, a)
}
func (e *Entity) GetAreaById(areaId string) *Area {
var foundArea *Area
func (e *Entity) GetAreaById(areaId string) *entities.Area {
var foundArea *entities.Area
for index, a := range e.Areas {
if a.Id == areaId {

View File

@ -1,18 +1,10 @@
package document
type Group struct {
Id string
ParentId string
ProjectId string
Name string
Order int
}
import "textualize/entities"
type GroupCollection struct {
Id string
Groups []Group
ProjectId string
}
type Group entities.Group
type GroupCollection entities.GroupCollection
var groupCollectionInstance *GroupCollection
@ -29,12 +21,12 @@ func SetGroupCollection(collection GroupCollection) *GroupCollection {
return groupCollectionInstance
}
func (collection *GroupCollection) AddDocumentGroup(group Group) {
func (collection *GroupCollection) AddDocumentGroup(group entities.Group) {
collection.Groups = append(collection.Groups, group)
}
func (collection *GroupCollection) GetGroupById(groupId string) *Group {
var foundGroup *Group
func (collection *GroupCollection) GetGroupById(groupId string) *entities.Group {
var foundGroup *entities.Group
for index, g := range collection.Groups {
if g.Id == groupId {

View File

@ -1,6 +1,8 @@
package document
func InitizeModule() {
import "textualize/entities"
func InitializeModule() {
GetDocumentCollection()
GetGroupCollection()
}
@ -9,7 +11,7 @@ func createTestData() {
documentCollection := GetDocumentCollection()
documentGroupCollection := GetGroupCollection()
documentGroupCollection.AddDocumentGroup(Group{
documentGroupCollection.AddDocumentGroup(entities.Group{
Id: "XYZ",
Name: "Test Group One",
})

View File

@ -1,42 +1,9 @@
package document
type ProcessedBoundingBox struct {
X0 int32
Y0 int32
X1 int32
Y1 int32
}
type ProcessedSymbol struct {
Text string
Confidence float32
BoundingBox ProcessedBoundingBox
}
type ProcessedWord struct {
Id string
FullText string
Symbols []ProcessedSymbol
Confidence float32
Direction string
BoundingBox ProcessedBoundingBox
}
type ProcessedLine struct {
FullText string
Words []ProcessedWord
}
type ProcessedArea struct {
Id string
DocumentId string
FullText string
Order int
Lines []ProcessedLine
}
import "textualize/entities"
type ProcessedAreaCollection struct {
Areas []ProcessedArea
Areas []entities.ProcessedArea
}
var processedAreaCollectionInstnace *ProcessedAreaCollection
@ -52,12 +19,12 @@ func SetProcessedAreaCollection(collection ProcessedAreaCollection) {
processedAreaCollectionInstnace = &collection
}
func (collection *ProcessedAreaCollection) AddProcessedArea(area ProcessedArea) {
func (collection *ProcessedAreaCollection) AddProcessedArea(area entities.ProcessedArea) {
collection.Areas = append(collection.Areas, area)
}
func (collection *ProcessedAreaCollection) GetAreasByDocumentId(id string) []*ProcessedArea {
var foundAreas []*ProcessedArea
func (collection *ProcessedAreaCollection) GetAreasByDocumentId(id string) []*entities.ProcessedArea {
var foundAreas []*entities.ProcessedArea
for index, a := range collection.Areas {
if a.DocumentId == id {
@ -68,8 +35,8 @@ func (collection *ProcessedAreaCollection) GetAreasByDocumentId(id string) []*Pr
return foundAreas
}
func (collection *ProcessedAreaCollection) GetAreaById(areaId string) *ProcessedArea {
var foundArea *ProcessedArea
func (collection *ProcessedAreaCollection) GetAreaById(areaId string) *entities.ProcessedArea {
var foundArea *entities.ProcessedArea
for index, a := range collection.Areas {
if a.Id == areaId {

View File

@ -1,13 +1,9 @@
package document
type UserMarkdown struct {
Id string
DocumentId string
Value string
}
import "textualize/entities"
type UserMarkdownCollection struct {
Values []UserMarkdown
Values []entities.UserMarkdown
}
var userMarkdownCollection *UserMarkdownCollection
@ -24,8 +20,8 @@ func SetUserMarkdownCollection(collection UserMarkdownCollection) {
userMarkdownCollection = &collection
}
func (collection *UserMarkdownCollection) GetUserMarkdownByDocumentId(documentId string) *UserMarkdown {
var foundUserMarkdown *UserMarkdown
func (collection *UserMarkdownCollection) GetUserMarkdownByDocumentId(documentId string) *entities.UserMarkdown {
var foundUserMarkdown *entities.UserMarkdown
for index, m := range collection.Values {
if m.DocumentId == documentId {
@ -37,12 +33,12 @@ func (collection *UserMarkdownCollection) GetUserMarkdownByDocumentId(documentId
return foundUserMarkdown
}
func (collection *UserMarkdownCollection) AddUserMarkdown(userMarkdown UserMarkdown) UserMarkdown {
func (collection *UserMarkdownCollection) AddUserMarkdown(userMarkdown entities.UserMarkdown) entities.UserMarkdown {
collection.Values = append(collection.Values, userMarkdown)
return userMarkdown
}
func (collection *UserMarkdownCollection) UpdateUserMarkdown(userMarkdown UserMarkdown) UserMarkdown {
func (collection *UserMarkdownCollection) UpdateUserMarkdown(userMarkdown entities.UserMarkdown) entities.UserMarkdown {
currentUserMarkdown := collection.GetUserMarkdownByDocumentId(userMarkdown.DocumentId)
if currentUserMarkdown != nil {

View File

@ -1,8 +1,5 @@
package session
type Organization struct {
Id string
Name string
LogoPath string
Users []User
}
import "textualize/entities"
type Organization entities.Organization

View File

@ -1,18 +1,9 @@
package session
import (
consts "textualize/core/Consts"
"textualize/entities"
)
type Project struct {
Id string
OrganizationId string
Name string
Settings ProjectSettings
}
type Project entities.Project
type ProjectSettings struct {
DefaultProcessLanguage consts.Language
DefaultTranslateTargetLanguage consts.Language
IsHosted bool
}
type ProjectSettings entities.ProjectSettings

View File

@ -1,10 +1,8 @@
package session
type Session struct {
Project Project
Organization Organization
User User
}
import "textualize/entities"
type Session entities.Session
var sessionInstance *Session
@ -22,7 +20,7 @@ func InitializeModule(newSession Session) *Session {
return sessionInstance
}
func (s *Session) UpdateCurrentUser(updatedUser User) User {
s.User = User(updatedUser)
func (s *Session) UpdateCurrentUser(updatedUser entities.User) entities.User {
s.User = entities.User(updatedUser)
return s.User
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

173
entities/ContextGroup.go Normal file
View File

@ -0,0 +1,173 @@
package entities
import (
"errors"
"fmt"
)
type IndependentTranslatedWord struct {
Id string
ProcessedWordId string
Value string
}
type LinkedProcessedArea struct {
Area ProcessedArea
Previous *LinkedProcessedArea
Next *LinkedProcessedArea
}
type SerializedLinkedProcessedArea struct {
AreaId string `json:"areaId"`
PreviousId string `json:"previousId"`
NextId string `json:"nextId"`
}
type ContextGroupCollection struct {
Groups []LinkedAreaList
}
type LinkedAreaList struct {
Id string
DocumentId string
TranslationText string
Head *LinkedProcessedArea
Tail *LinkedProcessedArea
}
func (l *LinkedAreaList) First() *LinkedProcessedArea {
return l.Head
}
func (linkedProcessedWord *LinkedProcessedArea) GetNext() *LinkedProcessedArea {
return linkedProcessedWord.Next
}
func (linkedProcessedWord *LinkedProcessedArea) GetPrevious() *LinkedProcessedArea {
return linkedProcessedWord.Previous
}
// Create new node with value
func (l *LinkedAreaList) Push(processedArea ProcessedArea) *LinkedAreaList {
n := &LinkedProcessedArea{Area: processedArea}
if l.Head == nil {
l.Head = n // First node
} else {
l.Tail.Next = n // Add after prev last node
n.Previous = l.Tail // Link back to prev last node
}
l.Tail = n // reset tail to newly added node
return l
}
func (l *LinkedAreaList) Find(id string) *LinkedProcessedArea {
found := false
var ret *LinkedProcessedArea = nil
for n := l.First(); n != nil && !found; n = n.GetNext() {
if n.Area.Id == id {
found = true
ret = n
}
}
return ret
}
func (l *LinkedAreaList) Delete(id string) bool {
success := false
node2del := l.Find(id)
if node2del != nil {
fmt.Println("Delete - FOUND: ", id)
prev_node := node2del.Previous
next_node := node2del.Next
// Remove this node
prev_node.Next = node2del.Next
next_node.Previous = node2del.Previous
success = true
}
return success
}
var errEmpty = errors.New("ERROR - List is empty")
// Pop last item from list
func (l *LinkedAreaList) Pop() (processedArea ProcessedArea, err error) {
if l.Tail == nil {
err = errEmpty
} else {
processedArea = l.Tail.Area
l.Tail = l.Tail.Previous
if l.Tail == nil {
l.Head = nil
}
}
return processedArea, err
}
func (l *LinkedAreaList) InsertAfter(id string, processedArea ProcessedArea) bool {
found := false
for n := l.First(); n != nil && !found; n = n.GetNext() {
if n.Area.Id == id {
found = true
newNode := &LinkedProcessedArea{Area: processedArea}
newNode.Next = n.Next
newNode.Previous = n
n.Next = newNode
}
}
return found
}
func (l *LinkedAreaList) InsertBefore(id string, processedArea ProcessedArea) bool {
found := false
for n := l.First(); n != nil && !found; n = n.GetNext() {
if n.Area.Id == id {
found = true
newNode := &LinkedProcessedArea{Area: processedArea}
newNode.Next = n
newNode.Previous = n.Previous
n.Previous = newNode
}
}
return found
}
func (l *LinkedAreaList) Serialize() []SerializedLinkedProcessedArea {
var serialized []SerializedLinkedProcessedArea
for n := l.First(); n != nil; n = n.GetNext() {
areaId := n.Area.Id
previousId := ""
if n.Previous != nil {
previousId = n.Previous.Area.Id
}
nextId := ""
if n.Next != nil {
nextId = n.Next.Area.Id
}
serialized = append(serialized, SerializedLinkedProcessedArea{
AreaId: areaId,
PreviousId: previousId,
NextId: nextId,
})
}
return serialized
}
func DeserializeLinkedAreaList(serialized []SerializedLinkedProcessedArea) LinkedAreaList {
linkedAreaList := LinkedAreaList{}
for _, serializedLinkedProcessedArea := range serialized {
linkedAreaList.Push(ProcessedArea{
Id: serializedLinkedProcessedArea.AreaId,
})
}
for _, serializedLinkedProcessedArea := range serialized {
linkedProcessedArea := linkedAreaList.Find(serializedLinkedProcessedArea.AreaId)
if serializedLinkedProcessedArea.PreviousId != "" {
linkedProcessedArea.Previous = linkedAreaList.Find(serializedLinkedProcessedArea.PreviousId)
}
if serializedLinkedProcessedArea.NextId != "" {
linkedProcessedArea.Next = linkedAreaList.Find(serializedLinkedProcessedArea.NextId)
}
}
return linkedAreaList
}

View File

@ -1,4 +1,4 @@
package storage
package entities
type DocumentCollection struct {
Documents []Document `json:"documents"`
@ -23,5 +23,6 @@ type Area struct {
EndX int `json:"endX"`
EndY int `json:"endY"`
Language Language `json:"language"`
TranslateLanguage Language `json:"translateLanguage"`
Order int `json:"order"`
}

View File

@ -1,4 +1,4 @@
package storage
package entities
type Group struct {
Id string `json:"id"`

8
entities/Language.go Normal file
View File

@ -0,0 +1,8 @@
package entities
type Language struct {
DisplayName string `json:"displayName"`
ProcessCode string `json:"processCode"`
TranslateCode string `json:"translateCode"`
IsBundledCustom bool `json:"isBundledCustom"`
}

8
entities/Organization.go Normal file
View File

@ -0,0 +1,8 @@
package entities
type Organization struct {
Id string `json:"id"`
Name string `json:"name"`
LogoPath string `json:"logoPath"`
Users []User `json:"users"`
}

View File

@ -1,4 +1,4 @@
package storage
package entities
type ProcessedBoundingBox struct {
X0 int32 `json:"x0"`
@ -15,6 +15,7 @@ type ProcessedSymbol struct {
type ProcessedWord struct {
Id string `json:"id"`
AreaId string `json:"areaId"`
FullText string `json:"fullText"`
Symbols []ProcessedSymbol `json:"symbols"`
Confidence float32 `json:"confidence"`
@ -23,14 +24,12 @@ type ProcessedWord struct {
}
type ProcessedLine struct {
FullText string `json:"fullText"`
Words []ProcessedWord `json:"words"`
}
type ProcessedArea struct {
Id string `json:"id"`
DocumentId string `json:"documentId"`
FullText string `json:"fullText"`
Order int `json:"order"`
Lines []ProcessedLine `json:"lines"`
}

View File

@ -1,4 +1,4 @@
package storage
package entities
type Project struct {
Id string `json:"id"`

7
entities/Session.go Normal file
View File

@ -0,0 +1,7 @@
package entities
type Session struct {
Project Project `json:"project"`
Organization Organization `json:"organization"`
User User `json:"user"`
}

View File

@ -1,4 +1,4 @@
package storage
package entities
type User struct {
Id string `json:"id"`

11
entities/UserMarkdown.go Normal file
View File

@ -0,0 +1,11 @@
package entities
type UserMarkdown struct {
Id string `json:"id"`
DocumentId string `json:"documentId"`
Value string `json:"value"`
}
type UserMarkdownCollection struct {
Values []UserMarkdown `json:"values"`
}

View File

@ -0,0 +1,79 @@
'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 { RootState } from '../../redux/store'
type Props = {
isActive: boolean,
area: entities.Area,
}
type coordinates = { x: number, y: number }
const Area = (props: Props) => {
const { scale } = useSelector((state: RootState) => state.stage)
const { selectedAreaId, setSelectedAreaId } = useProject()
const shapeRef = React.useRef<Konva.Rect>(null)
const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false)
const [areaContextMenuPosition, setAreaContextMenuPosition] = useState<coordinates>()
const { area, isActive } = props
const a = area
const width = (a.endX - a.startX)
const height = (a.endY - a.startY)
const handleContextMenu = (e: KonvaEventObject<PointerEvent>) => {
e.evt.preventDefault()
const stage = e.currentTarget.getStage()
const pointerPosition = stage?.getRelativePointerPosition()
if (!pointerPosition) return
const x = pointerPosition.x + 4
const y = pointerPosition.y + 4
setAreaContextMenuPosition({ x, y })
setIsAreaContextMenuOpen(true)
}
const handleAreaClick = (areaId: string) => {
if (areaId === selectedAreaId) setSelectedAreaId('')
else setSelectedAreaId(areaId)
}
return <Group>
<Rect
ref={shapeRef}
id={a.id}
width={width}
height={height}
x={a.startX * scale}
y={a.startY * scale}
scale={{ x: scale, y: scale }}
strokeEnabled
stroke={isActive ? '#dc8dec' : '#1e1e1e'}
strokeWidth={1}
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
onClick={() => handleAreaClick(a.id)}
onContextMenu={handleContextMenu}
isArea
/>
{isAreaContextMenuOpen
? <AreaContextMenu
area={area}
x={areaContextMenuPosition?.x || 0}
y={areaContextMenuPosition?.y || 0}
scale={scale}
setIsAreaContextMenuOpen={setIsAreaContextMenuOpen} />
: <></>
}
</Group>
}
export default Area

View File

@ -1,69 +0,0 @@
'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

View File

@ -0,0 +1,195 @@
'use client'
import React, { useState } from 'react'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import { Html } from 'react-konva-utils'
import { ClipboardIcon, ArrowPathIcon, TrashIcon, LanguageIcon } from '@heroicons/react/24/outline'
import { getScaled, makeFormStyles, makeIconStyles } from './styles'
import { useProject } from '../../../context/Project/provider'
import asyncClick from '../../../utils/asyncClick'
import processImageArea from '../../../useCases/processImageArea'
import classNames from '../../../utils/classNames'
import LanguageSelect from '../../utils/LanguageSelect'
import { RequestTranslateArea } from '../../../wailsjs/wailsjs/go/ipc/Channel'
import { useDispatch } from 'react-redux'
import { pushNotification } from '../../../redux/features/notifications/notificationQueueSlice'
type Props = {
x: number,
y: number,
scale: number,
area: entities.Area,
setIsAreaContextMenuOpen: Function
}
const AreaContextMenu = (props: Props) => {
const dispatch = useDispatch()
const { getProcessedAreaById, requestDeleteAreaById, getSelectedDocument, requestUpdateArea } = useProject()
const [shouldShowProcessLanguageSelect, setShouldShowProcessLanguageSelect] = useState(false)
const { area, setIsAreaContextMenuOpen, scale, x, y } = props
const handleCopyButtonClick = async () => {
setIsAreaContextMenuOpen(false)
const processedArea = await getProcessedAreaById(area.id)
const wordsOfProcessedArea = processedArea?.lines.flatMap(l => l.words.map(w => w.fullText))
const fullText = wordsOfProcessedArea?.join(' ')
if (!fullText) {
dispatch(pushNotification({ message: 'No text found to copy.', level: 'warning' }))
return
}
try {
await navigator.clipboard.writeText(fullText)
dispatch(pushNotification({ message: 'Copied area to clipboard' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error copying area', level: 'error' }))
}
}
const handleDeleteButtonClick = async () => {
setIsAreaContextMenuOpen(false)
try {
const response = await requestDeleteAreaById(area.id)
if (!response) dispatch(pushNotification({ message: 'Could not delete area', level: 'warning' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error deleting area', level: 'error' }))
}
}
const handleReprocessButtonClick = async () => {
setIsAreaContextMenuOpen(false)
const documentId = getSelectedDocument()?.id
if (!documentId) {
dispatch(pushNotification({ message: 'Issue finding selected document', level: 'warning' }))
return
}
try {
dispatch(pushNotification({ message: 'Processing test of area' }))
const response = await processImageArea(documentId, area.id)
if (response) dispatch(pushNotification({ message: 'Area successfully processed' }))
else dispatch(pushNotification({ message: 'No text result from processing area', level: 'warning' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error processing area', level: 'error' }))
}
}
const handleTranslateArea = async () => {
setIsAreaContextMenuOpen(false)
try {
const wasSuccessful = await RequestTranslateArea(area.id)
if (wasSuccessful) dispatch(pushNotification({ message: 'Successfully translated area' }))
else dispatch(pushNotification({ message: 'Issue translating area', level: 'warning' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error translating area', level: 'error' }))
}
}
const handleProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
setIsAreaContextMenuOpen(false)
let successfullyUpdatedLanguageOnArea = false
try {
successfullyUpdatedLanguageOnArea = await requestUpdateArea({...area, ...{language: selectedLanguage}})
} catch (err) {
dispatch(pushNotification({ message: 'Error updating area language', level: 'error' }))
return
}
const selectedDocumentId = getSelectedDocument()?.id
if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) {
dispatch(pushNotification({ message: 'Did not successfully update area language', level: 'warning' }))
return
}
try {
await processImageArea(selectedDocumentId, area.id)
dispatch(pushNotification({ message: 'Finished processing area', level: 'info' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error processing area', level: 'error' }))
}
}
const handleOnBlur = (e: React.FocusEvent) => {
e.preventDefault()
if (e.relatedTarget === null) setIsAreaContextMenuOpen(false)
}
const baseMenuItemClassNames = 'flex items-center justify-between w-full px-3 py-1 flex-shrink-0 text-left cursor-pointer focus:outline-none'
return <Html>
<div style={makeFormStyles(x, y, scale)} tabIndex={1} onBlur={handleOnBlur}>
<div className={classNames(
'z-40 min-w-max py-1 rounded-lg shadow-sm outline-none font-light',
'bg-white border border-gray-200',)}
>
<button autoFocus tabIndex={2}
onClick={(e) => asyncClick(e, handleCopyButtonClick)} className={
classNames(baseMenuItemClassNames,
'focus:bg-neutral-100 hover:bg-slate-300',
)}>
<span className="mr-2">Copy Area</span>
<ClipboardIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
<button tabIndex={3}
onClick={(e) => asyncClick(e, handleReprocessButtonClick)} className={
classNames(baseMenuItemClassNames,
'focus:bg-neutral-100 hover:bg-slate-300',
)}>
<span className="mr-2">Reprocess Area</span>
<ArrowPathIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
<button tabIndex={3}
onClick={(e) => asyncClick(e, handleTranslateArea)} className={
classNames(baseMenuItemClassNames,
'focus:bg-neutral-100 hover:bg-slate-300',
)}>
<span className="mr-2">Translate Area</span>
<LanguageIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
<button tabIndex={4}
onClick={(e) => asyncClick(e, handleDeleteButtonClick)} className={
classNames(baseMenuItemClassNames,
'focus:bg-neutral-100 bg-red-100 text-gray-900 hover:text-gray-100 hover:bg-red-600',
)}>
<span className="mr-2">Delete Area</span>
<TrashIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
{shouldShowProcessLanguageSelect
? <LanguageSelect
defaultLanguage={area.language || getSelectedDocument()?.defaultLanguage}
styles={{ fontSize: `${getScaled(14, scale)}px` }}
onSelect={handleProcessLanguageSelect}
/>
: <button tabIndex={5}
onClick={(e) => setShouldShowProcessLanguageSelect(true)}
className={classNames(
baseMenuItemClassNames,
'focus:bg-neutral-100 hover:bg-slate-300',
)}>
<span className="mr-2">
{area.language?.displayName || getSelectedDocument()?.defaultLanguage.displayName}
</span>
<LanguageIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
}
</div>
</div>
</Html >
}
export default AreaContextMenu

View File

@ -0,0 +1,31 @@
import { DetailedHTMLProps, FormHTMLAttributes } from 'react'
const getScaled = (value: number, scale: number) => Math.floor(value / scale)
const makeFormStyles = (x: number, y: number, scale: number) => {
const shadowOffset = { x: getScaled(4, scale), y: getScaled(4, scale), color: 'rgba(50, 50, 50, 0.4)', blur: getScaled(20, scale) }
return {
position: 'absolute',
fontSize: `${getScaled(16, scale)}px`,
width: `${getScaled(224, scale)}px`,
left: `${x}px`,
top: `${y}px`,
boxShadow: `${shadowOffset.x}px ${shadowOffset.y}px ${shadowOffset.blur}px ${shadowOffset.color}`
} as DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
}
const makeIconStyles = (scale: number) => {
return {
width: `${getScaled(14, scale)}px`,
height: `${getScaled(14, scale)}px`
}
}
export {
makeFormStyles,
makeIconStyles,
getScaled,
}

View File

@ -1,42 +0,0 @@
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

View File

@ -0,0 +1,71 @@
'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 { 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 areas = getSelectedDocument()?.areas || []
const [editingWord, setEditingWord] = useState<entities.ProcessedWord | null>(null)
const [selectedProcessedArea, setSelectedProcessedArea] = useState<entities.ProcessedArea | null>(null)
useEffect(() => {
if (!selectedAreaId) return setSelectedProcessedArea(null)
else {
getProcessedAreaById(selectedAreaId).then(res => {
if (res) setSelectedProcessedArea(res)
}).catch(err => {
console.warn('getProcessedAreaById', err)
setSelectedProcessedArea(null)
})
}
}, [selectedAreaId])
const renderEditingWord = () => {
if (!editingWord) return
return <EditingWord
scale={scale}
editingWord={editingWord}
setEditingWord={setEditingWord}
/>
}
const renderProcessedWords = () => {
if (!selectedProcessedArea) return <></>
const words = selectedProcessedArea.lines.map(l => l.words).flat()
return words.map((w, index) => <ProcessedWord
key={index}
area={selectedProcessedArea}
word={w}
scale={scale}
setEditingWord={setEditingWord}
/>)
}
const renderAreas = (areas: entities.Area[]) => areas.map((a, index) => {
return <Area key={index} area={a} isActive={a.id === selectedAreaId} />
})
return <Group>
{renderAreas(areas)}
{areProcessedWordsVisible ? renderProcessedWords() : <></>}
{areProcessedWordsVisible ? renderEditingWord() : <></>}
</Group>
}
export default Areas

View File

@ -0,0 +1,118 @@
'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'
import { useProject } from '../../context/Project/provider'
import useImage from 'use-image'
import { RectangleCoordinates } from './types'
import DrawingArea from './DrawingArea'
import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds'
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 [documentImage] = useImage(getSelectedDocument()?.path || '')
const documentRef = useRef(null)
const [drawingAreaRect, setDrawingAreaRect] = useState<RectangleCoordinates | null>(null)
const documentWidth = documentImage?.naturalWidth || 0
const documentHeight = documentImage?.naturalHeight || 0
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
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
dispatch(setIsDrawingArea(true))
}
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
const currentPosition = e.currentTarget.getRelativePointerPosition()
if (isDrawingArea) return setDrawingAreaRect({
startX: downClickX,
startY: downClickY,
endX: currentPosition.x,
endY: currentPosition.y,
})
}
const handleMouseUp = (e: KonvaEventObject<MouseEvent>) => {
const stage = e.currentTarget
if (stage.isDragging()) stage.stopDrag()
else if (isDrawingArea) dispatch(setIsDrawingArea(false))
if (!drawingAreaRect) return
const normalizedDrawnRect = getNormalizedRectToBounds(drawingAreaRect, documentWidth, documentHeight, scale)
const selectedDocumentId = getSelectedDocument()!.id
processImageRect(selectedDocumentId, normalizedDrawnRect).then(async addedAreas => {
updateDocuments().then(response => {
if (!addedAreas.length) return
setSelectedAreaId(addedAreas[0].id)
})
})
setDrawingAreaRect(null)
}
const handleWheel = (e: KonvaEventObject<WheelEvent>) => {
if (!e.evt.ctrlKey) return
const wheelDelta = e.evt.deltaY
const shouldAttemptScaleUp = (wheelDelta < 0) && scale < maxScale
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}>
<Layer id='documentLayer'>
<Image alt='Document Image'
ref={documentRef}
image={documentImage}
width={documentWidth}
height={documentHeight}
scale={{ x: scale, y: scale }}
shadowEnabled
shadowColor='black'
shadowOpacity={0.3}
shadowBlur={documentWidth * 0.05}
listening={false}
/>
{(isDrawingArea && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
</Layer>
{areAreasVisible
? <Layer id='areaLayer'>
<Areas scale={scale} />
</Layer>
: <></>
}
{areAreasVisible && areLinkAreaContextsVisible
? <Layer id='contextConnections'>
<ContextConnections />
</Layer>
: <></>
}
</Stage>
}
export default CanvasStage

View File

@ -0,0 +1,72 @@
'use client'
import React from 'react'
import { useSelector } from 'react-redux'
import { Group, Line } from 'react-konva'
import { useProject } from '../../../context/Project/provider'
import { RootState } from '../../../redux/store'
const ConnectionLines = () => {
const { scale } = useSelector((state: RootState) => state.stage)
const { getSelectedDocument, contextGroups } = useProject()
const areas = getSelectedDocument()?.areas || []
const renderLines = () => {
if (!contextGroups?.length) return <></>
const linesAlreadyRendered = new Set<string>()
const lines = contextGroups.map((contextGroup) => {
const currentArea = areas.find(a => a.id === contextGroup.areaId)
const nextArea = areas.find(a => a.id === contextGroup.nextId)
if (!currentArea || !nextArea) return
if (linesAlreadyRendered.has(`${contextGroup.areaId}-${contextGroup.nextId}`)) return
if (linesAlreadyRendered.has(`${contextGroup.nextId}-${contextGroup.areaId}`)) return
const startingPoint = {
x: ((currentArea.startX + currentArea.endX) * scale) / 2,
y: currentArea.startY * scale
}
const startingTensionPoint = {
x: (startingPoint.x + (nextArea.startX * scale)) / 2,
y: startingPoint.y,
}
const endingPoint = {
x: ((nextArea.startX + nextArea.endX) * scale) / 2,
y: nextArea.endY * scale
}
const endingTensionPoint = {
x: (startingPoint.x + (nextArea.startX * scale)) / 2,
y: endingPoint.y,
}
linesAlreadyRendered.add(`${contextGroup.areaId}-${contextGroup.nextId}`)
linesAlreadyRendered.add(`${contextGroup.nextId}-${contextGroup.areaId}`)
return <Line
key={`${contextGroup.areaId}-${contextGroup.nextId}`}
points={[
...Object.values(startingPoint),
...Object.values(startingTensionPoint),
...Object.values(endingTensionPoint),
...Object.values(endingPoint),
]}
strokeEnabled
strokeWidth={2 * scale}
stroke='#dc8dec'
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
tension={0.2}
listening={false}
/>
})
return lines.filter(l => !!l)
}
return <Group>{renderLines()}</Group>
}
export default ConnectionLines

View File

@ -0,0 +1,109 @@
'use client'
import { Circle, Group } from 'react-konva'
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 dispatch = useDispatch()
const { scale, areLinkAreaContextsVisible, startingContextConnectionPoint } = useSelector((state: RootState) => state.stage)
const { requestConnectProcessedAreas } = useProject()
const handleContextAreaMouseDown = async (e: KonvaEventObject<MouseEvent>) => {
e.cancelBubble = true
const clickedConnectionPoint = {
isHead: e.currentTarget.attrs.isHead,
areaId: e.currentTarget.attrs.id
}
if (!startingContextConnectionPoint) return dispatch(setStartingContextConnectionPoint(clickedConnectionPoint))
if (clickedConnectionPoint.isHead === startingContextConnectionPoint.isHead
|| clickedConnectionPoint.areaId === startingContextConnectionPoint.areaId)
return dispatch(setStartingContextConnectionPoint(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)
} catch (err) {
console.warn('RequestConnectProcessedAreas', err)
}
}
const renderConnectingPointsForArea = (a: entities.Area) => {
if (!areLinkAreaContextsVisible) return <></>
const headConnector = <Circle
key={`head-${a.id}`}
id={a.id}
radius={8}
x={((a.startX + a.endX) * scale) / 2}
y={a.startY * scale}
strokeEnabled={false}
fill='#dc8dec'
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
onMouseDown={handleContextAreaMouseDown}
isHead
/>
const tailConnector = <Circle
key={`tail-${a.id}`}
id={a.id}
radius={10}
x={((a.startX + a.endX) * scale) / 2}
y={a.endY * scale}
strokeEnabled={false}
fill='#1e1e1e'
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
onMouseDown={handleContextAreaMouseDown}
isHead={false}
/>
let connectorsToRender = []
if (!startingContextConnectionPoint) connectorsToRender = [headConnector, tailConnector]
else if (startingContextConnectionPoint.isHead) connectorsToRender = [tailConnector]
else connectorsToRender = [headConnector]
if (startingContextConnectionPoint?.areaId === a.id) {
let y = (startingContextConnectionPoint.isHead ? a.startY : a.endY) * scale
connectorsToRender.push(<Circle
key={`active-${a.id}`}
id={a.id}
radius={10}
x={((a.startX + a.endX) * scale) / 2}
y={y}
strokeEnabled={false}
fill={startingContextConnectionPoint.isHead ? '#dc8dec' : '#1e1e1e'}
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
isHead={startingContextConnectionPoint.isHead}
onMouseDown={() => dispatch(setStartingContextConnectionPoint(null))}
/>)
}
return <Group key={`group-${a.id}`}>
{connectorsToRender}
</Group>
}
const renderAllConnectingPoints = () => props.areas.map(a => renderConnectingPointsForArea(a))
return <Group>
{renderAllConnectingPoints()}
</Group>
}
export default ConnectionPoints

View File

@ -0,0 +1,58 @@
'use client'
import React from 'react'
import { useSelector } from 'react-redux'
import { Line } from 'react-konva'
import { Coordinates } from '../types'
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 { getSelectedDocument } = useProject()
const areas = getSelectedDocument()?.areas || []
if (!startingContextConnectionPoint || !endDrawingPosition) return <></>
const { areaId, isHead } = startingContextConnectionPoint
const area = areas.find(a => a.id === areaId)
if (!area) return <></>
const startingPoint = {
x: ((area.startX + area.endX) * scale) / 2,
y: (isHead ? area.startY : area.endY) * scale
}
const startingTensionPoint = {
x: (startingPoint.x + endDrawingPosition.x) / 2,
y: startingPoint.y,
}
const endingTensionPoint = {
x: (startingPoint.x + endDrawingPosition.x) / 2,
y: endDrawingPosition.y,
}
return <Line
points={[
...Object.values(startingPoint),
...Object.values(startingTensionPoint),
...Object.values(endingTensionPoint),
...Object.values(endDrawingPosition),
]}
strokeEnabled
strokeWidth={2 * scale}
stroke='#dc8dec'
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
tension={0.2}
listening={false}
/>
}
export default CurrentDrawingConnection

View File

@ -0,0 +1,45 @@
'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 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 areas = getSelectedDocument()?.areas || []
const [endDrawingPosition, setEndDrawingPosition] = useState<Coordinates | null>(null)
const handleMouseMove = (e: MouseEvent) => {
if (!areLinkAreaContextsVisible || !startingContextConnectionPoint) return
setEndDrawingPosition(Konva.stages[0].getRelativePointerPosition())
}
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
})
useEffect(() => {
if (!startingContextConnectionPoint) setEndDrawingPosition(null)
}, [startingContextConnectionPoint])
if (!areLinkAreaContextsVisible) return <></>
return <Group>
<ConnectionPoints areas={areas} />
<ConnectionLines />
<CurrentDrawingConnection endDrawingPosition={endDrawingPosition} />
</Group>
}
export default ContextConnections

View File

@ -0,0 +1,30 @@
'use client'
import React from 'react'
import { Rect, } from 'react-konva'
type Props = {
rect: {
startX: number,
startY: number,
endX: number,
endY: number,
},
}
const DrawingArea = (props: Props) => {
const { rect } = props
const width = rect.endX - rect.startX
const height = rect.endY - rect.startY
return <Rect
width={width}
height={height}
x={rect.startX}
y={rect.startY}
strokeEnabled
stroke='#dc8dec'
strokeWidth={2}
strokeScaleEnabled={false}
/>
}
export default DrawingArea

View File

@ -1,82 +0,0 @@
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

View File

@ -0,0 +1,51 @@
import React from 'react'
import { Html } from 'react-konva-utils'
import { entities } from '../../wailsjs/wailsjs/go/models'
import { useProject } from '../../context/Project/provider'
import onEnterHandler from '../../utils/onEnterHandler'
type Props = {
scale: number,
editingWord: entities.ProcessedWord,
setEditingWord: Function,
}
const EditingWord = (props: Props) => {
const { requestUpdateProcessedWordById } = useProject()
const { scale, setEditingWord, editingWord } = props
const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => {
requestUpdateProcessedWordById(wordId, newWordValue).catch(console.error)
setEditingWord(null)
}
const { x0, x1, y0, y1 } = editingWord.boundingBox
const left = x0 * scale
const top = y1 * scale
const width = (x1 - x0) * scale
const height = (y1 - y0) * scale
return <Html>
<input
defaultValue={editingWord.fullText}
style={{
position: 'absolute',
left: `${left}px`,
top: `${top}px`,
textAlign: 'center',
display: 'block',
width: `${width}px`,
height: `${height}px`,
fontSize: `${Math.floor(24 * scale)}px`,
alignContent: 'center',
alignItems: 'center',
lineHeight: 0,
direction: 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'
}}
onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(editingWord.id, e.currentTarget.value))}
onBlur={(e) => handleWordCorrectionSubmit(editingWord.id, e.currentTarget.value)}
/>
</Html>
}
export default EditingWord

View File

@ -1,55 +0,0 @@
'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

View File

@ -0,0 +1,57 @@
'use client'
import React from 'react'
import { Group, Rect, Text } from 'react-konva'
import { entities } from '../../wailsjs/wailsjs/go/models'
type Props = {
area: entities.ProcessedArea,
word: entities.ProcessedWord,
scale: number,
setEditingWord: Function
}
const ProcessedWord = (props: Props) => {
const { area, scale, word, setEditingWord } = props
const { x0, x1, y0, y1 } = word.boundingBox
return <Group
id={word.id}
areaId={area.id}
isProcessedWord
onDblClick={() => setEditingWord(word)}>
<Rect
id={word.id}
areaId={area.id}
width={x1 - x0}
height={y1 - y0}
scale={{ x: scale, y: scale }}
x={x0 * scale}
y={y0 * scale}
strokeEnabled={false}
shadowForStrokeEnabled={false}
strokeScaleEnabled={false}
cornerRadius={4}
fill='rgb(80,80,80)'
opacity={0.4}
shadowColor='rgb(180,180,180)'
shadowBlur={10}
shadowOffset={{ x: 10, y: 10 }} />
<Text text={word.fullText}
width={x1 - x0}
height={y1 - y0}
scale={{ x: scale, y: scale }}
x={x0 * scale}
y={y0 * scale}
align='center'
verticalAlign='middle'
fontSize={36}
fontFamily='Calibri'
fill='white'
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
/>
</Group >
}
export default ProcessedWord

View File

@ -0,0 +1,29 @@
'use client'
import React from 'react'
import classNames from '../../../utils/classNames'
type Icon = (props: React.SVGProps<SVGSVGElement> & {
title?: string | undefined;
titleId?: string | undefined;
}) => JSX.Element
const ToolToggleButton = (props: { icon: Icon, hint: string, isActive: boolean, onClick?: React.MouseEventHandler<HTMLButtonElement> }) => {
return <div className="group flex relative">
<button className='pointer-events-auto p-2 bg-white rounded-md block mt-3 shadow-lg hover:bg-slate-100 aria-pressed:bg-indigo-400 aria-pressed:text-white'
aria-pressed={props.isActive}
onClick={props.onClick}>
<props.icon className='w-5 h-5' />
</button>
<div className={classNames(
'group-hover:opacity-100 transition-opacity0 p-1',
'absolute -translate-x-full opacity-0 m-4 mx-auto',
)}>
<div className={'bg-gray-800 p-1 text-xs text-gray-100 rounded-md'}>
{props.hint}
</div>
</div>
</div>
}
export default ToolToggleButton

View File

@ -0,0 +1,122 @@
'use client'
import React, { useEffect, useState } from 'react'
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 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 {
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>()
useEffect(() => {
setSelectedArea(selectedDocument?.areas.find(a => a.id == selectedAreaId))
}, [selectedAreaId, selectedDocument, selectedArea])
const handleAreaProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
if (!selectedArea) return
let successfullyUpdatedLanguageOnArea = false
try {
successfullyUpdatedLanguageOnArea = await requestUpdateArea({ ...selectedArea, ...{ language: selectedLanguage } })
} catch (err) {
dispatch(pushNotification({ message: 'Error updating area language', level: 'error' }))
return
}
const selectedDocumentId = getSelectedDocument()?.id
if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) {
dispatch(pushNotification({ message: 'Did not successfully update area language', level: 'warning' }))
return
}
try {
await processImageArea(selectedDocumentId, selectedArea.id)
await updateDocuments()
dispatch(pushNotification({ message: 'Finished processing area', level: 'info' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error processing area', level: 'error' }))
}
}
const handleDocumentProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
if (!selectedDocument) return
const currentDocument = selectedDocument
currentDocument.defaultLanguage = selectedLanguage
await requestUpdateDocument(currentDocument)
await updateDocuments()
}
const renderLanguageSelect = () => {
const defaultLanguage = selectedArea?.language.displayName ? selectedArea?.language : selectedDocument?.defaultLanguage
const onSelect = selectedArea ? handleAreaProcessLanguageSelect : handleDocumentProcessLanguageSelect
return <LanguageSelect
styles={{ fontSize: '16px', borderRadius: '2px' }}
defaultLanguage={defaultLanguage}
onSelect={onSelect}
/>
}
return <>
{/* Top buttons */}
<div className='absolute flex justify-between align-top top-2 p-2 drop-shadow-2xl pointer-events-none shadow-slate-100' style={{ width: 'calc(100% - 0.5rem)' }}>
<div className='align-top pointer-events-auto w-1/3'>
<h1 className="text-lg font-medium text-gray-900 block mr-2 drop-shadow-2xl shadow-slate-100 drop truncate">
{selectedArea?.name
? `${selectedDocument?.name} / ${selectedArea?.name}`
: selectedDocument?.name
}
</h1>
{ renderLanguageSelect() }
{/* <LanguageSelect styles={{ fontSize: '16px', borderRadius: '2px' }} defaultLanguage={selectedArea?.language.displayName ? selectedArea?.language : selectedDocument?.defaultLanguage} /> */}
</div>
<div className='flex mt-4 justify-evenly align-top pointer-events-auto'>
<MagnifyingGlassMinusIcon className='w-4 h-4' />
<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) => { dispatch(setScale(e.currentTarget.valueAsNumber)) }}
/>
<MagnifyingGlassPlusIcon className='w-4 h-4' />
</div>
</div>
{/* Right Buttons */}
<div className='absolute bottom-6 right-3 pointer-events-none'>
{areAreasVisible
? <>
<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={areAreasVisible} onClick={() => dispatch(setAreAreasVisible(!areAreasVisible))} />
</div>
</>
}
export default ToolingOverlay

View File

@ -1,186 +0,0 @@
'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

View File

@ -1,101 +0,0 @@
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

View File

@ -1,56 +1,34 @@
'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'
import dynamic from 'next/dynamic'
import React, { useEffect, useRef } from 'react'
import { useDispatch } from 'react-redux'
import ToolingOverlay from './ToolingOverlay'
import { setSize } from '../../redux/features/stage/stageSlice'
const zoomStep = 0.025
const maxZoomLevel = 4
const CanvasStage = dynamic(() => import('./CanvasStage'), { ssr: false })
const DocumentCanvas = () => {
const { getSelectedDocument } = useProject()
const selectedDocument = getSelectedDocument()
const dispatch = useDispatch()
const [zoomLevel, setZoomLevel] = useState(1)
const [size, setSize] = useState({ width: 0, height: 0 })
const { width, height } = size
const thisRef = useRef<HTMLDivElement>(null)
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')}>
const handleWindowResize = () => {
const width = thisRef?.current?.clientWidth || 0
const height = thisRef?.current?.clientHeight || 0
dispatch(setSize({ width, height }))
}
<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,
}} />
useEffect(() => {
handleWindowResize()
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
}, [thisRef?.current?.clientWidth, thisRef?.current?.clientHeight])
return <div ref={thisRef} className='relative' style={{ height: 'calc(100vh - 140px)' }}>
<div className='h-full overflow-hidden rounded-lg border-4 border-dashed border-gray-200'>
<CanvasStage />
<ToolingOverlay />
</div>
</div >
}

View File

@ -0,0 +1,21 @@
export type MouseCoordinates = {
startMouseX: number, startMouseY: number, endMouseX: number, endMouseY: number
}
export type RectangleCoordinates = {
startX: number, startY: number, endX: number, endY: number
}
export type Coordinates = { x: number, y: number }
export type AddAreaToStoreCallback = (startX: number, startY: number, endX: number, endY: number) => Promise<void>
export type SetZoomCallback = (newZoomLevel: number) => void
export type ZoomDetails = {
currentZoomLevel: number,
maxZoomLevel: number,
zoomStep: number
}
export type HoverOverAreaCallback = (areaId?: string) => void

View File

@ -0,0 +1,86 @@
import { Fragment, useEffect } from 'react'
import { Transition } from '@headlessui/react'
import { useDispatch, useSelector } from 'react-redux'
import { XMarkIcon, InformationCircleIcon, ExclamationTriangleIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline'
import { RootState } from '../../redux/store'
import { NotificationProps } from '../../redux/features/notifications/types'
import { dismissCurrentNotification } from '../../redux/features/notifications/notificationQueueSlice'
const renderIcon = (level: NotificationProps['level'] = 'info') => {
switch (level) {
default: return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
case 'info': return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
case 'warning': return <ExclamationTriangleIcon className='w-6 h-6 text-orange-400' />
case 'error': return <ExclamationCircleIcon className='w-6 h-6 text-red-600' />
}
}
const notificationTime = 3000
const Notification = () => {
const { currentNotification, queue } = useSelector((state: RootState) => state.notificationQueue)
const dispatch = useDispatch()
const handleOnClick = () => {
if (currentNotification?.onActionClickCallback) currentNotification?.onActionClickCallback()
if (currentNotification?.closeOnAction) dispatch(dismissCurrentNotification())
}
useEffect(() => {
if (queue.length) setTimeout(() => dispatch(dismissCurrentNotification()), notificationTime)
}, [currentNotification])
return <>
<div
aria-live="assertive"
className="pointer-events-none absolute block top-0 left-0 w-full h-full"
>
<div className="absolute items-center" style={{ bottom: '12px', right: '16px' }}>
<Transition
show={!!currentNotification}
as={Fragment}
enter="transform ease-out duration-1300 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<div className="flex items-center">
{renderIcon(currentNotification?.level)}
<div className="flex content-center flex-1 justify-between">
<p className="flex-1 text-sm font-medium text-gray-900 ml-2">{currentNotification?.message}</p>
{currentNotification?.actionButtonText ? <button
type="button"
className="ml-3 flex-shrink-0 rounded-md bg-white text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => handleOnClick()}
>
{currentNotification?.actionButtonText}
</button>
: <></>
}
</div>
<div className="ml-4 flex flex-shrink-0">
<button
type="button"
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => {
dispatch(dismissCurrentNotification())
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</>
}
export default Notification

View File

@ -7,7 +7,7 @@ import { useNavigation } from '../../context/Navigation/provider'
import { mainPages } from '../../context/Navigation/types'
import { useProject } from '../../context/Project/provider'
import { GetAllLocalProjects } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { entities } from '../../wailsjs/wailsjs/go/models'
import NewProjectModal from './NewProjectModal'
import ProjectListModal from './ProjectListModal'
@ -17,7 +17,7 @@ const MainProject = () => {
const [isProjectListModal, setIsProjectListModal] = useState(false)
const [canPopoverBeOpen, setCanPopoverBeOpen] = useState(true)
const [avalibleProjects, setAvalibleProjects] = useState<ipc.Project[]>([])
const [availableProjects, setAvailableProjects] = useState<entities.Project[]>([])
const { createNewProject, requestSelectProjectByName } = useProject()
const { setSelectedMainPage } = useNavigation()
@ -39,7 +39,7 @@ const MainProject = () => {
setCanPopoverBeOpen(false)
GetAllLocalProjects().then(response => {
console.log(response)
setAvalibleProjects(response)
setAvailableProjects(response)
setIsProjectListModal(true)
})
},
@ -73,7 +73,7 @@ const MainProject = () => {
{isNewProjectModalOpen ? <NewProjectModal onCreateNewProjectHandler={onCreateNewProjectHandler} /> : ''}
{isProjectListModal ? <ProjectListModal onSelectProjectHandler={onSelectProjectHandler} projects={avalibleProjects} /> : '' }
{isProjectListModal ? <ProjectListModal onSelectProjectHandler={onSelectProjectHandler} projects={availableProjects} /> : '' }
<div className="py-20 px-6 sm:px-6 sm:py-32 lg:px-8">
<div className="mx-auto max-w-2xl text-center">

View File

@ -1,6 +1,6 @@
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { entities } from '../../wailsjs/wailsjs/go/models'
type Props = { projects: ipc.Project[], onSelectProjectHandler: (projectName: string) => void }
type Props = { projects: entities.Project[], onSelectProjectHandler: (projectName: string) => void }
const ProjectListModal = (props: Props) => {
return (

View File

@ -0,0 +1,87 @@
import { Combobox } from '@headlessui/react'
import { LanguageIcon } from '@heroicons/react/20/solid'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline'
import { useEffect, useState } from 'react'
import classNames from '../../utils/classNames'
import getSupportedLanguages from '../../utils/getSupportedLanguages'
import { entities } from '../../wailsjs/wailsjs/go/models'
type Props = {
defaultLanguage?: entities.Language,
onSelect?: Function
styles?: Partial<React.CSSProperties>
}
const LanguageSelect = (props?: Props) => {
const [languages, setLanguages] = useState<entities.Language[]>([])
const [selectedLanguage, setSelectedLanguage] = useState<entities.Language | undefined>(props?.defaultLanguage)
const [query, setQuery] = useState('')
const filteredLanguages = query !== ''
? languages.filter(l => l.displayName.toLowerCase().includes(query.toLowerCase()))
: languages
useEffect(() => {
if (languages.length === 0) {
getSupportedLanguages().then(response => {
setLanguages(response)
})
}
})
useEffect(() => {
setSelectedLanguage(props?.defaultLanguage)
}, [props?.defaultLanguage])
const handleLanguageChange = (language: entities.Language) => {
if (props?.onSelect) props.onSelect(language)
setSelectedLanguage(language)
}
return <Combobox as="div" value={selectedLanguage} onChange={handleLanguageChange} className='block w-full'>
<div className="block relative">
<Combobox.Input
className="w-full border-none bg-white shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onChange={(event) => setQuery(event.target.value)}
displayValue={(language: entities.Language) => language?.displayName}
placeholder='Document Language'
style={props?.styles}
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
<LanguageIcon className="text-gray-400" style={props?.styles ? {width: props.styles.fontSize} : {}} />
<ChevronUpDownIcon className=" text-gray-400" aria-hidden="true" style={props?.styles ? {width: props.styles.fontSize} : {}} />
</Combobox.Button>
{filteredLanguages.length > 0 && (
<Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{filteredLanguages.map((l) => (
<Combobox.Option
style={props?.styles}
key={l.displayName}
value={l}
className={({ active }) => classNames(
'relative cursor-default select-none py-2 pl-3 pr-9',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)}>
{({ active, selected }) => <>
<span className={classNames('block truncate', selected && 'font-semibold')}>{l.displayName}</span>
{selected && (
<span className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}>
<CheckIcon aria-hidden="true" style={props?.styles ? {width: props.styles.fontSize} : {}} />
</span>
)}
</>
}
</Combobox.Option>
))}
</Combobox.Options>
)}
</div>
</Combobox>
}
export default LanguageSelect

View File

@ -5,17 +5,17 @@ import { useEffect, useState } from 'react'
import { useProject } from '../../context/Project/provider'
import classNames from '../../utils/classNames'
import getSupportedLanguages from '../../utils/getSupportedLanguages'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { entities } from '../../wailsjs/wailsjs/go/models'
type forAreaType = { shouldUpdateArea?: true, shouldUpdateDocument?: never }
type forDocumentType = { shouldUpdateDocument?: true, shouldUpdateArea?: never }
type Props = (forAreaType | forDocumentType) & { defaultLanguage?: ipc.Language }
type Props = (forAreaType | forDocumentType) & { defaultLanguage?: entities.Language }
const LanguageSelect = (props?: Props) => {
const { requestUpdateDocument, getSelectedDocument } = useProject()
const [languages, setLanguages] = useState<ipc.Language[]>([])
const [languages, setLanguages] = useState<entities.Language[]>([])
const [query, setQuery] = useState('')
const [selectedLanguage, setSelectedLanguage] = useState<ipc.Language | undefined>(props?.defaultLanguage)
const [selectedLanguage, setSelectedLanguage] = useState<entities.Language | undefined>(props?.defaultLanguage)
const filteredLanguages = query === ''
@ -47,7 +47,7 @@ const LanguageSelect = (props?: Props) => {
style={{'maxWidth': '240px', 'height': '30px'}}
className="rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
onChange={(event) => setQuery(event.target.value)}
displayValue={(language: ipc.Language) => language?.displayName}
displayValue={(language: entities.Language) => language?.displayName}
placeholder='Document Language'
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">

View File

@ -13,7 +13,9 @@ const MainWorkspace = () => {
const renderSelectedWorkSpace = () => {
if (selectedWorkspace === workspaces.TEXTEDITOR) return <TextEditor />
else return !selectedDocumentId ? <NoSelectedDocument /> : <DocumentCanvas />
else return !selectedDocumentId
? <NoSelectedDocument />
: <DocumentCanvas />
}
return <main className=" bg-gray-100 min-h-[calc(100vh-118px)] ml-64 overflow-y-scroll">

View File

@ -3,7 +3,7 @@
import React, { useRef } from 'react'
import { useProject } from '../../../context/Project/provider'
import classNames from '../../../utils/classNames'
import { ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { ArrowPathIcon, TrashIcon } from '@heroicons/react/24/outline'
import { SidebarArea } from './types'
import { useSidebar } from './provider'
import onEnterHandler from '../../../utils/onEnterHandler'
@ -15,13 +15,13 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
getAreaById,
requestUpdateArea,
setSelectedDocumentId,
setSelectedAreaId,
requestChangeAreaOrder,
requestDeleteAreaById,
selectedAreaId,
setSelectedAreaId,
} = useProject()
const {
selectedAreaId,
isEditAreaNameInputShowing,
setIsEditAreaNameInputShowing,
dragOverAreaId,
@ -30,7 +30,6 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
const editAreaNameTextInput = useRef<HTMLInputElement>(null)
const onConfirmAreaNameChangeHandler = async (areaDetails: { areaId: string, areaName: string }) => {
const { areaId, areaName } = areaDetails
@ -46,7 +45,8 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
const onAreaClick = (areaId: string) => {
setSelectedDocumentId(props.documentId)
setSelectedAreaId(areaId)
if (selectedAreaId !== areaId) setSelectedAreaId(areaId)
else setSelectedAreaId('')
}
const onAreaDoubleClick = (areaId: string) => {
@ -125,7 +125,7 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
aria-hidden="true"
onClick={handleReprocessAreaButtonClick}
/>
<XMarkIcon
<TrashIcon
className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
onClick={() => handleAreaDeleteButtonClick(props.area.id)} />
</div>

View File

@ -56,7 +56,7 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
return (
<li className='p-0 m-0' key={props.document.id}>
{!props.document.areas.length
{!props.document.areas?.length
?
<div
onClick={() => onDocumentClickHandler(props.document.id)}
@ -135,7 +135,7 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
props.document.id === selectedDocumentId
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'text-left font-medium text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 '
'text-left font-medium text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 inline-block'
)}
>
{props.document.name}
@ -143,58 +143,12 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
}
<XMarkIcon
className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
className='inline-block 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) => (
<AreaLineItem key={a.id} area={a} index={index} documentId={props.document.id} />
// <li key={a.id}>
// {selectedAreaId === a.id && isEditAreaNameInputShowing
// ? <input
// type="text"
// name="areaName"
// id="areaName"
// autoFocus
// className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
// placeholder={a.name || `Area ${index}`}
// onBlur={onAreaInputBlur}
// onKeyDown={(event) => {
// onEnterHandler(event,
// () => onConfirmAreaNameChangeHandler({ areaId: a.id, areaName: event.currentTarget.value }))
// }}
// ref={editAreaNameTextInput}
// />
// : <div
// draggable
// onDragOver={() => onAreaDragOver(a.id)}
// onDragStart={() => onAreaDragStart(a.id)}
// onDragEnd={() => onAreaDropEnd(a.id)}
// className={classNames('flex justify-between items-center cursor-pointer',
// selectedAreaId === a.id ? 'bg-indigo-500 text-gray-200' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
// dragOverAreaId === a.id ? 'bg-gray-300 text-gray-700' : '',
// selectedAreaId === a.id && dragOverAreaId === a.id ? 'bg-indigo-300' : '',
// )}>
// <a
// role='button'
// onClick={() => onAreaClick(a.id)}
// onDoubleClick={() => onAreaDoubleClick(a.id)}
// className={classNames('group w-full pr-2 py-2 text-left font-medium pl-8 text-xs',
// 'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none',
// )}>
// {a.name || `Area ${a.order}`}
// </a>
// <ArrowPathIcon
// className='w-6 h-5 mr-2 text-white hover:bg-white hover:text-gray-700 rounded-full p-0.5'
// aria-hidden="true"
// onClick={() => console.log('refresh')}
// />
// <XMarkIcon
// className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
// onClick={() => handleAreaDeleteButtonClick(a.id)} />
// </div>
// }
// </li>
))}
</ul>
</details>

View File

@ -1,17 +1,14 @@
'use client'
import { DocumentPlusIcon, PlusIcon, XMarkIcon } from '@heroicons/react/24/outline'
import React, { useRef, useState } from 'react'
import React, { useRef } from 'react'
import { useProject } from '../../../context/Project/provider'
import classNames from '../../../utils/classNames'
import onEnterHandler from '../../../utils/onEnterHandler'
import AddGroupInput from './AddGroupInput'
import DocumentLineItem from './DocumentLineItem'
import { useSidebar } from './provider'
import { SidebarGroup } from './types'
const GroupLineItem = (props: { group: SidebarGroup, dragOverGroupId?: string }) => {
const {
requestAddDocument,
@ -58,10 +55,6 @@ const GroupLineItem = (props: { group: SidebarGroup, dragOverGroupId?: string })
setDragOverGroupId(groupId)
}
const onGroupDragStart = (groupId: string) => {
setSelectedGroupId(groupId)
}
const onGroupDropEnd = (groupId: string) => {
if (!groupId || groupId == dragOverGroupId) return
@ -136,140 +129,6 @@ const GroupLineItem = (props: { group: SidebarGroup, dragOverGroupId?: string })
<ul>
{props.group.documents.map((d, index) => (
<DocumentLineItem key={d.id} document={d} index={index} groupId={props.group.id} />
// <li className='p-0 m-0' key={d.id}>
// {!d.areas.length
// ?
// <div
// onClick={() => onDocumentClickHandler(d.id)}
// onDoubleClick={() => onDocumentDoubleClickHandler(d.id)}
// className={classNames(
// d.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',
// index !== 0 ? 'rounded-t-md' : '',
// )}>
// {selectedDocumentId === d.id && isEditDocumentNameInputShowing
// ? <input
// type="text"
// name="documentName"
// id="documentName"
// autoFocus
// className="h-8 w-[calc(100%-18px)] text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 inline-block rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
// defaultValue={d.name}
// onBlur={onDocumentInputBlur}
// onKeyDown={(event) => {
// onEnterHandler(event,
// () => onConfirmDocumentNameChangeHandler(event.currentTarget.value))
// }}
// ref={editDocumentNameTextInput}
// />
// : <a
// role='button'
// className={classNames(
// d.id === selectedDocumentId
// ? 'bg-gray-900 text-white'
// : 'text-gray-300 hover:bg-gray-700 hover:text-white',
// 'text-left font-medium text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 '
// )}
// >
// {d.name}
// </a>
// }
// </div>
// : <details>
// <summary
// onClick={() => onDocumentClickHandler(d.id)}
// onDoubleClick={() => onDocumentDoubleClickHandler(d.id)}
// className={classNames(
// d.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',
// index !== 0 ? 'rounded-t-md' : '',
// )}>
// {selectedDocumentId === d.id && isEditDocumentNameInputShowing
// ? <input
// type="text"
// name="documentName"
// id="documentName"
// autoFocus
// className="h-8 w-[calc(100%-18px)] text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 inline-block rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
// defaultValue={d.name}
// onBlur={onDocumentInputBlur}
// onKeyDown={(event) => {
// onEnterHandler(event,
// () => onConfirmDocumentNameChangeHandler(event.currentTarget.value))
// }}
// ref={editDocumentNameTextInput}
// />
// : <a
// role='button'
// className={classNames(
// d.id === selectedDocumentId
// ? 'bg-gray-900 text-white'
// : 'text-gray-300 hover:bg-gray-700 hover:text-white',
// 'text-left font-medium text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 '
// )}
// >
// {d.name}
// </a>
// }
// </summary>
// <ul>
// {d.areas.map((a, index) => (
// <li key={a.id}>
// {selectedAreaId === a.id && isEditAreaNameInputShowing
// ? <input
// type="text"
// name="areaName"
// id="areaName"
// autoFocus
// className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
// placeholder={a.name || `Area ${index}`}
// onBlur={onAreaInputBlur}
// onKeyDown={(event) => {
// onEnterHandler(event,
// () => onConfirmAreaNameChangeHandler({ areaId: a.id, areaName: event.currentTarget.value }))
// }}
// ref={editAreaNameTextInput}
// />
// : <div
// draggable
// onDragOver={() => onAreaDragOver(a.id)}
// onDragStart={() => onAreaDragStart(a.id)}
// onDragEnd={() => onAreaDropEnd(a.id)}
// className={classNames('flex justify-between items-center cursor-pointer',
// selectedAreaId === a.id ? 'bg-indigo-500 text-gray-200' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
// dragOverAreaId === a.id ? 'bg-gray-300 text-gray-700' : '',
// selectedAreaId === a.id && dragOverAreaId === a.id ? 'bg-indigo-300' : '',
// )}>
// <a
// role='button'
// onClick={() => onAreaClick(a.id)}
// onDoubleClick={() => onAreaDoubleClick(a.id)}
// className={classNames('group w-full pr-2 py-2 text-left font-medium pl-8 text-xs',
// 'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none',
// )}>
// {a.name || `Area ${a.order}`}
// </a>
// <ArrowPathIcon
// className='w-6 h-5 mr-2 text-white hover:bg-white hover:text-gray-700 rounded-full p-0.5'
// aria-hidden="true"
// onClick={() => console.log('refresh')}
// />
// <XMarkIcon
// className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
// onClick={() => handleAreaDeleteButtonClick(a.id)} />
// </div>
// }
// </li>
// ))}
// </ul>
// </details>
// }
// </li>
))}
{renderAddNewDocument(props.group.id)}

View File

@ -16,6 +16,10 @@ const makeDefaultSidebar = (): SidebarContextType => ({
setIsAddNewGroupInputShowing: (_: boolean) => {},
isEditAreaNameInputShowing: false,
setIsEditAreaNameInputShowing: (_: boolean) => {},
dragOverGroupId: '',
setDragOverGroupId: (_: string) => {},
dragOverAreaId: '',
setDragOverAreaId: (_: string) => {},
})
export default makeDefaultSidebar

View File

@ -1,14 +1,14 @@
import { ipc } from '../../../wailsjs/wailsjs/go/models'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import { SidebarGroup } from './types'
const getNavigationProps = (documents: ipc.Document[], groups: ipc.Group[]) : SidebarGroup[] => {
const getNavigationProps = (documents: entities.Document[], groups: entities.Group[]) : SidebarGroup[] => {
const groupsWithDocuments = groups.map(g => {
const childrenDocuments = documents
.filter(d => d.groupId === g.id)
.map(d => ({
id: d.id,
name: d.name,
areas: d.areas.map(a => ({ id: a.id, name: a.name, order: a.order }))//.sort((a, b) => a.order - b.order)
areas: d.areas?.map(a => ({ id: a.id, name: a.name, order: a.order }))//.sort((a, b) => a.order - b.order)
}))
return {
@ -23,7 +23,7 @@ const getNavigationProps = (documents: ipc.Document[], groups: ipc.Group[]) : Si
.map(d => ({
id: d.id,
name: d.name,
areas: d.areas.map(a => ({ id: a.id, name: a.name, order: a.order }))//.sort((a, b) => a.order - b.order)
areas: d.areas?.map(a => ({ id: a.id, name: a.name, order: a.order }))//.sort((a, b) => a.order - b.order)
}))
return [

7
frontend/consts/index.ts Normal file
View File

@ -0,0 +1,7 @@
const colors = {
BRAND_PRIMARY: {
hex: '#dc8dec',
}
}
export { colors }

View File

@ -11,7 +11,7 @@ export function useNavigation() {
}
type Props = { children: ReactNode, navigationProps: NavigationProps }
export function NavigationProvidor({ children, navigationProps }: Props) {
export function NavigationProvider({ children, navigationProps }: Props) {
const [selectedWorkspace, setSelectedWorkspace] = useState<workspaces>(navigationProps.selectedWorkspace)
const [selectedMainPage, setSelectedMainPage] = useState<mainPages>(navigationProps.selectedMainPage)

View File

@ -1,10 +1,10 @@
import { saveDocuments } from '../../useCases/saveData'
import { GetProcessedAreasByDocumentId, RequestAddArea, RequestAddProcessedArea, RequestChangeAreaOrder, RequestDeleteAreaById, RequestUpdateArea } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { GetProcessedAreasByDocumentId, RequestAddArea, RequestAddProcessedArea, RequestChangeAreaOrder, RequestDeleteAreaById, RequestUpdateArea, RequestUpdateProcessedArea, } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { entities, ipc } from '../../wailsjs/wailsjs/go/models'
import { AddAreaProps, AreaProps } from './types'
type Dependencies = {
documents: ipc.Document[]
documents: entities.Document[]
updateDocuments: () => Promise<ipc.GetDocumentsResponse>
selectedDocumentId: string
}
@ -12,7 +12,7 @@ type Dependencies = {
const createAreaProviderMethods = (dependencies: Dependencies) => {
const { documents, updateDocuments, selectedDocumentId } = dependencies
const getAreaById = (areaId: string): ipc.Area | undefined => (
const getAreaById = (areaId: string): entities.Area | undefined => (
documents.map(d => d.areas).flat().find(a => a.id === areaId)
)
@ -29,7 +29,7 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
}
const getProcessedAreasByDocumentId = async (documentId: string) => {
let response: ipc.ProcessedArea[] = []
let response: entities.ProcessedArea[] = []
try {
response = await GetProcessedAreasByDocumentId(documentId)
} catch (err) {
@ -38,19 +38,20 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
return response
}
const requestAddArea = async (documentId: string, area: AddAreaProps): Promise<ipc.Area> => {
const response = await RequestAddArea(documentId, new ipc.Area(area))
const requestAddArea = async (documentId: string, area: AddAreaProps): Promise<entities.Area> => {
const response = await RequestAddArea(documentId, new entities.Area(area))
if (response.id) await updateDocuments()
saveDocuments()
return response
}
const requestUpdateArea = async (updatedArea: AreaProps): Promise<ipc.Area> => {
const response = await RequestUpdateArea(new ipc.Area(updatedArea))
const requestUpdateArea = async (updatedArea: AreaProps): Promise<boolean> => {
console.log('requestUpdateArea', updatedArea)
const wasSuccessful = await RequestUpdateArea(new entities.Area(updatedArea))
if (response.id) await updateDocuments()
if (wasSuccessful) await updateDocuments()
saveDocuments()
return response
return wasSuccessful
}
const requestDeleteAreaById = async (areaId: string): Promise<boolean> => {
@ -60,7 +61,9 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
return wasSuccessfulDeletion
}
const requestAddProcessedArea = async (processedArea: ipc.ProcessedArea) => await RequestAddProcessedArea(processedArea)
const requestAddProcessedArea = async (processedArea: entities.ProcessedArea) => await RequestAddProcessedArea(processedArea)
const requestUpdateProcessedArea = async (updatedProcessedArea: entities.ProcessedArea) => await RequestUpdateProcessedArea(updatedProcessedArea)
const requestChangeAreaOrder = async (areaId: string, newOrder: number) => {
const response = await RequestChangeAreaOrder(areaId, newOrder)
@ -76,6 +79,7 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
requestDeleteAreaById,
getProcessedAreasByDocumentId,
requestAddProcessedArea,
requestUpdateProcessedArea,
requestChangeAreaOrder,
getProcessedAreaById,
}

View File

@ -0,0 +1,38 @@
import { saveContextGroups } from '../../useCases/saveData'
import { RequestConnectProcessedAreas, GetSerializedContextGroups } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { entities } from '../../wailsjs/wailsjs/go/models'
type Dependencies = { updateDocuments: Function }
const createContextGroupProviderMethods = (dependencies?: Dependencies) => {
const requestConnectProcessedAreas = async (headId: string, tailId: string) => {
let wasSuccessful = false
try {
wasSuccessful = await RequestConnectProcessedAreas(headId, tailId)
await saveContextGroups()
} catch (err) {
console.error(err)
}
dependencies?.updateDocuments()
return wasSuccessful
}
const getSerializedContextGroups = async () => {
let response: entities.SerializedLinkedProcessedArea[] = []
try {
response = await GetSerializedContextGroups()
} catch (err) {
console.error(err)
}
return response
}
return {
requestConnectProcessedAreas,
getSerializedContextGroups,
}
}
export default createContextGroupProviderMethods

View File

@ -1,20 +1,20 @@
import { saveGroups } from '../../useCases/saveData'
import { RequestAddDocument, RequestAddDocumentGroup, RequestChangeGroupOrder, RequestDeleteDocumentAndChildren, RequestUpdateDocument, RequestUpdateProcessedWordById } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { ipc, entities } from '../../wailsjs/wailsjs/go/models'
import { UpdateDocumentRequest } from './types'
type Dependencies = {
selectedDocumentId: string
documents: ipc.Document[]
documents: entities.Document[]
saveDocuments: () => Promise<void>
updateDocuments: () => Promise<ipc.GetDocumentsResponse>
groups: ipc.Group[]
groups: entities.Group[]
}
const createDocumentProviderMethods = (dependencies: Dependencies) => {
const { selectedDocumentId, documents, saveDocuments, updateDocuments, groups } = dependencies
const getGroupById = (groupId: string): ipc.Group | undefined => (
const getGroupById = (groupId: string): entities.Group | undefined => (
groups.find(g => g.id === groupId)
)
@ -42,7 +42,7 @@ const createDocumentProviderMethods = (dependencies: Dependencies) => {
}
const requestUpdateDocument = async (documentProps: UpdateDocumentRequest) => {
const response = await RequestUpdateDocument(new ipc.Document(documentProps))
const response = await RequestUpdateDocument(new entities.Document(documentProps))
await updateDocuments()
saveDocuments()
return response

View File

@ -1,9 +1,9 @@
import { CreateNewProject, RequestChangeSessionProjectByName, RequestChooseUserAvatar, RequestUpdateCurrentUser } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { ipc, entities } from '../../wailsjs/wailsjs/go/models'
import { UserProps } from './types'
type Dependencies = {
updateSession: () => Promise<ipc.Session>
updateSession: () => Promise<entities.Session>
updateDocuments: () => Promise<ipc.GetDocumentsResponse>
}
@ -17,7 +17,7 @@ const createSessionProviderMethods = (dependencies: Dependencies) => {
}
const requestUpdateCurrentUser = async (userProps: UserProps) => {
const response = await RequestUpdateCurrentUser(new ipc.User(userProps))
const response = await RequestUpdateCurrentUser(new entities.User(userProps))
await updateSession()
return response
}

View File

@ -1,13 +1,13 @@
import { saveUserProcessedMarkdown } from '../../useCases/saveData'
import { GetUserMarkdownByDocumentId, RequestUpdateDocumentUserMarkdown } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { entities } from '../../wailsjs/wailsjs/go/models'
type Dependencies = {}
const createUserMarkdownProviderMethods = (dependencies?: Dependencies) => {
const requestUpdateDocumentUserMarkdown = async (documentId: string, markdown: string) => {
let response: ipc.UserMarkdown = new ipc.UserMarkdown()
let response = new entities.UserMarkdown()
try {
response = await RequestUpdateDocumentUserMarkdown(documentId, markdown)
await saveUserProcessedMarkdown()
@ -17,8 +17,8 @@ const createUserMarkdownProviderMethods = (dependencies?: Dependencies) => {
return response
}
const getUserMarkdownByDocumentId = async (documentId: string): Promise<ipc.UserMarkdown> => {
let response: ipc.UserMarkdown = new ipc.UserMarkdown({})
const getUserMarkdownByDocumentId = async (documentId: string): Promise<entities.UserMarkdown> => {
let response = new entities.UserMarkdown({})
try {
response = await GetUserMarkdownByDocumentId(documentId)
} catch (err) {

View File

@ -1,37 +1,42 @@
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { entities, ipc } from '../../wailsjs/wailsjs/go/models'
import { ProjectContextType, UserProps } from './types'
const makeDefaultProject = (): ProjectContextType => ({
id: '',
documents: [] as ipc.Document[],
groups: [] as ipc.Group[],
documents: [] as entities.Document[],
groups: [] as entities.Group[],
contextGroups: [] as entities.SerializedLinkedProcessedArea[],
selectedAreaId: '',
selectedDocumentId: '',
getSelectedDocument: () => new ipc.Document(),
getSelectedDocument: () => new entities.Document(),
getAreaById: (areaId) => undefined,
getProcessedAreasByDocumentId: (documentId) => Promise.resolve([new ipc.ProcessedArea()]),
requestAddProcessedArea: (processesArea) => Promise.resolve(new ipc.ProcessedArea()),
requestAddArea: (documentId, area) => Promise.resolve(new ipc.Area()),
requestUpdateArea: (updatedArea) => Promise.resolve(new ipc.Area()),
getProcessedAreasByDocumentId: (documentId) => Promise.resolve([new entities.ProcessedArea()]),
requestAddProcessedArea: (processesArea) => Promise.resolve(new entities.ProcessedArea()),
requestAddArea: (documentId, area) => Promise.resolve(new entities.Area()),
requestUpdateArea: (updatedArea) => Promise.resolve(false),
requestDeleteAreaById: (areaId) => Promise.resolve(false),
requestAddDocument: (groupId, documentName) => Promise.resolve(new ipc.Document()),
requestAddDocument: (groupId, documentName) => Promise.resolve(new entities.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),
requestAddDocumentGroup: (groupName: string) => Promise.resolve(new entities.Group()),
requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise.resolve(new entities.UserMarkdown()),
getUserMarkdownByDocumentId: (documentId) => Promise.resolve(new entities.UserMarkdown),
setSelectedAreaId: (id) => {},
setSelectedDocumentId: (id) => {},
currentSession: new ipc.Session(),
createNewProject: (name: string) => Promise.resolve(new ipc.Session()),
requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise.resolve(new ipc.User()),
currentSession: new entities.Session(),
createNewProject: (name: string) => Promise.resolve(new entities.Session()),
requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise.resolve(new entities.User()),
requestChooseUserAvatar: () => Promise.resolve(''),
requestUpdateDocument: ({}) => Promise.resolve(new ipc.Document),
requestChangeAreaOrder: (areaId: string, newOrder: number) => Promise.resolve(new ipc.Document()),
requestChangeGroupOrder: (groupId: string, newOrder: number) => Promise.resolve(new ipc.Group()),
requestUpdateDocument: ({}) => Promise.resolve(new entities.Document),
requestChangeAreaOrder: (areaId: string, newOrder: number) => Promise.resolve(new entities.Document()),
requestChangeGroupOrder: (groupId: string, newOrder: number) => Promise.resolve(new entities.Group()),
getGroupById: (groupId) => undefined,
requestSelectProjectByName: (projectName) => Promise.resolve(false),
requestUpdateProcessedWordById: (wordId, newTestValue) => Promise.resolve(false),
getProcessedAreaById: (areaId) => Promise.resolve(undefined),
requestUpdateProcessedArea: updatedProcessedArea => Promise.resolve(false),
requestConnectProcessedAreas: (headId, tailId) => Promise.resolve(false),
getSerializedContextGroups: () => Promise.resolve([]),
updateDocuments: () => Promise.resolve(new ipc.GetDocumentsResponse())
})
export default makeDefaultProject

View File

@ -2,7 +2,7 @@
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { GetCurrentSession, GetDocuments, } from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { entities } from '../../wailsjs/wailsjs/go/models'
import { ProjectContextType, ProjectProps } from './types'
import makeDefaultProject from './makeDefaultProject'
import { saveDocuments } from '../../useCases/saveData'
@ -10,6 +10,7 @@ import createAreaProviderMethods from './createAreaProviderMethods'
import createDocumentProviderMethods from './createDocumentMethods'
import createSessionProviderMethods from './createSessionProviderMethods'
import createUserMarkdownProviderMethods from './createUserMarkdownProviderMethods'
import createContextGroupProviderMethods from './createContextGroupProviderMethods'
const ProjectContext = createContext<ProjectContextType>(makeDefaultProject())
@ -19,17 +20,19 @@ export function useProject() {
type Props = { children: ReactNode, projectProps: ProjectProps }
export function ProjectProvider({ children, projectProps }: Props) {
const [documents, setDocuments] = useState<ipc.Document[]>(projectProps.documents)
const [groups, setGroups] = useState<ipc.Group[]>(projectProps.groups)
const [documents, setDocuments] = useState<entities.Document[]>(projectProps.documents)
const [groups, setGroups] = useState<entities.Group[]>(projectProps.groups)
const [contextGroups, setContextGroups] = useState<entities.SerializedLinkedProcessedArea[]>(projectProps.contextGroups)
const [selectedAreaId, setSelectedAreaId] = useState<string>('')
const [selectedDocumentId, setSelectedDocumentId] = useState<string>('')
const [currentSession, setCurrentSession] = useState<ipc.Session>(new ipc.Session())
const [currentSession, setCurrentSession] = useState<entities.Session>(new entities.Session())
const updateDocuments = async () => {
const response = await GetDocuments()
const { documents, groups } = response
const { documents, groups, contextGroups } = response
setDocuments(documents)
setGroups(groups)
setContextGroups(contextGroups)
return response
}
@ -43,6 +46,7 @@ export function ProjectProvider({ children, projectProps }: Props) {
const areaMethods = createAreaProviderMethods({ documents, updateDocuments, selectedDocumentId })
const sessionMethods = createSessionProviderMethods({ updateSession, updateDocuments })
const userMarkDownMethods = createUserMarkdownProviderMethods()
const contextGroupMethods = createContextGroupProviderMethods({ updateDocuments })
useEffect(() => {
@ -60,15 +64,18 @@ export function ProjectProvider({ children, projectProps }: Props) {
id: '',
documents,
groups,
contextGroups,
selectedAreaId,
setSelectedAreaId,
selectedDocumentId,
setSelectedDocumentId,
currentSession,
updateDocuments,
...areaMethods,
...documentMethods,
...sessionMethods,
...userMarkDownMethods,
...contextGroupMethods,
}
return <ProjectContext.Provider value={value}>

View File

@ -1,9 +1,10 @@
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { ipc, entities } from '../../wailsjs/wailsjs/go/models'
export type ProjectProps = {
id: string,
documents: ipc.Document[],
groups: ipc.Group[],
documents: entities.Document[],
groups: entities.Group[],
contextGroups: entities.SerializedLinkedProcessedArea[],
}
export type AddAreaProps = {
@ -32,36 +33,40 @@ export type UpdateDocumentRequest = {
groupId?: string,
name?: string,
path?: string,
areas?: ipc.Area[]
defaultLanguage?: ipc.Language
areas?: entities.Area[]
defaultLanguage?: entities.Language
}
export type ProjectContextType = {
getSelectedDocument: () => ipc.Document | undefined
getAreaById: (areaId: string) => ipc.Area | undefined
getProcessedAreasByDocumentId: (documentId: string) => Promise<ipc.ProcessedArea[]>
requestAddProcessedArea: (processedArea: ipc.ProcessedArea) => Promise<ipc.ProcessedArea>
requestAddArea: (documentId: string, area: AddAreaProps) => Promise<ipc.Area>
requestUpdateArea: (area: AreaProps) => Promise<ipc.Area>
getSelectedDocument: () => entities.Document | undefined
getAreaById: (areaId: string) => entities.Area | undefined
getProcessedAreasByDocumentId: (documentId: string) => Promise<entities.ProcessedArea[]>
requestAddProcessedArea: (processedArea: entities.ProcessedArea) => Promise<entities.ProcessedArea>
requestAddArea: (documentId: string, area: AddAreaProps) => Promise<entities.Area>
requestUpdateArea: (area: AreaProps) => Promise<boolean>
requestDeleteAreaById: (areaId: string) => Promise<boolean>
requestAddDocument: (groupId: string, documentName: string) => Promise<ipc.Document>
requestAddDocument: (groupId: string, documentName: string) => Promise<entities.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>
requestAddDocumentGroup: (groupName: string) => Promise<entities.Group>
requestUpdateDocumentUserMarkdown: (documentId: string, markdown: string) => Promise<entities.UserMarkdown>
getUserMarkdownByDocumentId: (documentId: string) => Promise<entities.UserMarkdown>
selectedAreaId: string
setSelectedAreaId: (id: string) => void
selectedDocumentId: string
setSelectedDocumentId: (id: string) => void
currentSession: ipc.Session
createNewProject: (name: string) => Promise<ipc.Session>
requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise<ipc.User>
currentSession: entities.Session
createNewProject: (name: string) => Promise<entities.Session>
requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise<entities.User>
requestChooseUserAvatar: () => Promise<string>
requestUpdateDocument: (request: UpdateDocumentRequest) => Promise<ipc.Document>
requestChangeAreaOrder: (areaId: string, newOrder: number) => Promise<ipc.Document>
requestChangeGroupOrder: (groupId: string, newOrder: number) => Promise<ipc.Group>
getGroupById: (groupId: string) => ipc.Group | undefined
requestUpdateDocument: (request: UpdateDocumentRequest) => Promise<entities.Document>
requestChangeAreaOrder: (areaId: string, newOrder: number) => Promise<entities.Document>
requestChangeGroupOrder: (groupId: string, newOrder: number) => Promise<entities.Group>
getGroupById: (groupId: string) => entities.Group | undefined
requestSelectProjectByName: (projectName: string) => Promise<boolean>
requestUpdateProcessedWordById: (wordId: string, newTextValue: string) => Promise<boolean>
getProcessedAreaById: (areaId: string) => Promise<ipc.ProcessedArea | undefined>
getProcessedAreaById: (areaId: string) => Promise<entities.ProcessedArea | undefined>
requestUpdateProcessedArea: (updatedProcessedArea: entities.ProcessedArea) => Promise<boolean>
requestConnectProcessedAreas: (headId: string, tailId: string) => Promise<boolean>
getSerializedContextGroups: () => Promise<entities.SerializedLinkedProcessedArea[]>
updateDocuments: () => Promise<ipc.GetDocumentsResponse>
} & ProjectProps

File diff suppressed because it is too large Load Diff

View File

@ -16,13 +16,19 @@
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"@monaco-editor/react": "^4.4.6",
"@reduxjs/toolkit": "^1.9.5",
"@tailwindcss/forms": "^0.5.3",
"next": "^13.1.1",
"konva": "^9.2.0",
"next": "^13.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-konva": "^18.2.9",
"react-konva-utils": "^1.0.4",
"react-markdown": "^8.0.5",
"react-redux": "^8.1.2",
"rehype-raw": "^6.1.1",
"tesseract.js": "^4.0.2",
"use-image": "^1.1.0",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@ -1 +1 @@
2415a78ef8f325df057b22f577cbbe50
bf8d6eeb2add78baa4092415a836f1ad

View File

@ -3,15 +3,16 @@
import { AppProps } from 'next/app'
import { ProjectProvider } from '../context/Project/provider'
import '../styles/globals.css'
import { ipc } from '../wailsjs/wailsjs/go/models'
import { entities } from '../wailsjs/wailsjs/go/models'
import '../styles/globals.css'
import { NavigationProvidor } from '../context/Navigation/provider'
import { NavigationProvider } from '../context/Navigation/provider'
import { mainPages, workspaces } from '../context/Navigation/types'
import { Providers } from '../redux/provider'
const initialProjectProps = {
id: '',
documents: [] as ipc.Document[],
groups: [] as ipc.Group[]
documents: [] as entities.Document[],
groups: [] as entities.Group[]
}
const initialNavigationProps = {
@ -21,10 +22,12 @@ const initialNavigationProps = {
export default function MainAppLayout({ Component, pageProps }: AppProps) {
return <div className='min-h-screen' >
<NavigationProvidor navigationProps={initialNavigationProps}>
<NavigationProvider navigationProps={initialNavigationProps}>
<ProjectProvider projectProps={initialProjectProps}>
<Providers>
<Component {...pageProps} />
</Providers>
</ProjectProvider>
</NavigationProvidor>
</NavigationProvider>
</div>
}

View File

@ -1,5 +1,4 @@
import { NextPage } from 'next'
import { useEffect, useState } from 'react'
import MainHead from '../components/head'
import MainProject from '../components/project/Main'
import User from '../components/settings/User'
@ -8,6 +7,7 @@ import Navigation from '../components/workspace/Navigation'
import { useNavigation } from '../context/Navigation/provider'
import { mainPages } from '../context/Navigation/types'
import { useProject } from '../context/Project/provider'
import Notification from '../components/Notifications'
const Home: NextPage = () => {
const { currentSession } = useProject()
@ -28,6 +28,7 @@ const Home: NextPage = () => {
return <>
<MainHead />
{renderSelectedMainPage()}
<Notification />
</>
}

Binary file not shown.

View File

@ -0,0 +1,44 @@
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { NotificationProps, NotificationQueueState } from './types'
const initialState: NotificationQueueState = {
currentNotification: undefined,
queue: [],
}
export const notificationQueueSlice = createSlice({
name: 'notifications',
initialState,
reducers: {
setNotifications: (state, action: PayloadAction<NotificationProps[]>) => {
state.queue = action.payload
},
setCurrentNotification: (state, action: PayloadAction<NotificationProps | undefined>) => {
state.currentNotification = action.payload
},
pushNotification: (state, action: PayloadAction<NotificationProps>) => {
let { queue } = state
const { payload: newNotification } = action
if (queue.length) queue.push(newNotification)
else {
queue.push(newNotification)
state.currentNotification = newNotification
}
},
dismissCurrentNotification: (state) => {
state.queue.shift()
state.currentNotification = state.queue[0] || undefined
}
}
})
export const {
setNotifications,
setCurrentNotification,
pushNotification,
dismissCurrentNotification
} = notificationQueueSlice.actions
export default notificationQueueSlice.reducer

View File

@ -0,0 +1,15 @@
export type NotificationLevel = 'info' | 'warning' | 'error'
export type NotificationProps = {
shouldShow?: boolean,
message: string,
actionButtonText?: string,
onActionClickCallback?: Function,
closeOnAction?: boolean,
level?: NotificationLevel,
}
export type NotificationQueueState = {
queue: NotificationProps[],
currentNotification?: NotificationProps
}

View File

@ -0,0 +1,66 @@
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { ContextConnectionPoint, StageState } from './types'
export const maxScale = 4
export const scaleStep = 0.01
const initialState: StageState = {
size: { width: 1, height: 1 },
scale: 1,
areAreasVisible: true,
areProcessedWordsVisible: true,
areTranslatedWordsVisible: false,
areLinkAreaContextsVisible: false,
isDrawingArea: false,
startingContextConnectionPoint: null,
}
export const stageSlice = createSlice({
name: 'stage',
initialState,
reducers: {
setSize: (state, action: PayloadAction<{width: number, height: number}>) => {
state.size = action.payload
},
setScale: (state, action: PayloadAction<number>) => {
let clampedScale = action.payload
if (clampedScale > maxScale) clampedScale = maxScale
else if (clampedScale < scaleStep) clampedScale = scaleStep
state.scale = clampedScale
},
setAreAreasVisible: (state, action: PayloadAction<boolean>) => {
state.areAreasVisible = action.payload
},
setAreProcessedWordsVisible: (state, action: PayloadAction<boolean>) => {
state.areProcessedWordsVisible = action.payload
},
setAreTranslatedWordsVisible: (state, action: PayloadAction<boolean>) => {
state.areTranslatedWordsVisible = action.payload
},
setAreLinkAreaContextsVisible: (state, action: PayloadAction<boolean>) => {
state.areLinkAreaContextsVisible = action.payload
},
setIsDrawingArea: (state, action: PayloadAction<boolean>) => {
state.isDrawingArea = action.payload
},
setStartingContextConnectionPoint: (state, action: PayloadAction<ContextConnectionPoint | null>) => {
state.startingContextConnectionPoint = action.payload
},
}
})
export const {
setSize,
setScale,
setAreAreasVisible,
setAreProcessedWordsVisible,
setAreTranslatedWordsVisible,
setAreLinkAreaContextsVisible,
setIsDrawingArea,
setStartingContextConnectionPoint,
} = stageSlice.actions
export default stageSlice.reducer

View File

@ -0,0 +1,15 @@
export type ContextConnectionPoint = {
isHead: boolean,
areaId: string,
}
export type StageState = {
size: { width: number, height: number },
scale: number,
areAreasVisible: boolean,
areProcessedWordsVisible: boolean,
areTranslatedWordsVisible: boolean,
areLinkAreaContextsVisible: boolean,
isDrawingArea: boolean,
startingContextConnectionPoint: ContextConnectionPoint | null
}

5
frontend/redux/hooks.ts Normal file
View File

@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@ -0,0 +1,8 @@
'use client'
import { store } from './store'
import { Provider } from 'react-redux'
export function Providers({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>
}

18
frontend/redux/store.ts Normal file
View File

@ -0,0 +1,18 @@
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/dist/query'
import notificationQueueSlice from './features/notifications/notificationQueueSlice'
import stageSlice from './features/stage/stageSlice'
export const store = configureStore({
reducer: {
notificationQueue: notificationQueueSlice,
stage: stageSlice,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
setupListeners(store.dispatch)

View File

@ -16,4 +16,7 @@ module.exports = {
},
},
},
colors: {
brandPrimary: '#dc8dec',
}
}

View File

@ -1,6 +1,6 @@
import { createScheduler, createWorker } from 'tesseract.js'
import { GetAreaById, GetDocumentById, RequestAddProcessedArea, RequestSaveProcessedTextCollection } from '../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../wailsjs/wailsjs/go/models'
import { createScheduler, createWorker, PSM } from 'tesseract.js'
import { GetAreaById, GetDocumentById, GetProcessedAreaById, RequestAddProcessedArea, RequestSaveProcessedTextCollection, RequestUpdateProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel'
import { entities } from '../wailsjs/wailsjs/go/models'
import loadImage from './loadImage'
import { saveProcessedText } from './saveData'
@ -9,15 +9,27 @@ const processImageArea = async (documentId: string, areaId: string) => {
const foundArea = await GetAreaById(areaId)
if (!foundDocument.path || !foundDocument.areas?.length || !foundArea.id) return
const processLanguage = foundDocument.defaultLanguage.processCode
console.log(foundArea)
const processLanguage = foundArea.language.processCode || foundDocument.defaultLanguage.processCode
if (!processLanguage) return console.error('No process language selected')
const { path } = foundDocument
const imageData = await loadImage(path)
let workerOptions: Partial<Tesseract.WorkerOptions> = {}
if (foundDocument.defaultLanguage.isBundledCustom) {
workerOptions = {
langPath: '/customLanguages',
gzip: false,
logger: m => console.log(m)
}
}
const worker = await createWorker(workerOptions)
const scheduler = createScheduler()
const worker = await createWorker()
await worker.loadLanguage(processLanguage)
await worker.initialize(processLanguage)
scheduler.addWorker(worker)
@ -31,27 +43,28 @@ const processImageArea = async (documentId: string, areaId: string) => {
}
})
const addProcessesAreaRequest = await RequestAddProcessedArea(new ipc.ProcessedArea({
const newProcessedArea = new entities.ProcessedArea({
id: foundArea.id,
documentId,
order: foundArea.order,
fullText: result.data.text,
lines: result.data.lines.map((l: any) => new ipc.ProcessedLine({
lines: result.data.lines.map((l: any) => new entities.ProcessedLine({
fullText: l.text,
words: l.words.map((w: any) => new ipc.ProcessedWord({
words: l.words.map((w: any) => new entities.ProcessedWord({
areaId: foundArea.id,
fullText: w.text,
direction: w.direction,
confidence: w.confidence,
boundingBox: new ipc.ProcessedBoundingBox({
boundingBox: new entities.ProcessedBoundingBox({
x0: w.bbox.x0,
y0: w.bbox.y0,
x1: w.bbox.x1,
y1: w.bbox.y1,
}),
symbols: w.symbols.map((s: any) => new ipc.ProcessedSymbol({
symbols: w.symbols.map((s: any) => new entities.ProcessedSymbol({
fullText: s.text,
confidence: s.confidence,
boundingBox: new ipc.ProcessedBoundingBox({
boundingBox: new entities.ProcessedBoundingBox({
x0: s.bbox.x0,
y0: s.bbox.y0,
x1: s.bbox.x1,
@ -60,11 +73,22 @@ const processImageArea = async (documentId: string, areaId: string) => {
}))
}))
}))
}))
})
console.log(newProcessedArea)
const existingProcessedArea = await GetProcessedAreaById(areaId)
let didSuccessfullyProcess: boolean // TODO: fix this: this no longer is truthful, returns true or false if there was not a JS error
try {
if (existingProcessedArea.id !== areaId) await RequestAddProcessedArea(newProcessedArea)
else await RequestUpdateProcessedArea(newProcessedArea)
saveProcessedText()
return addProcessesAreaRequest
didSuccessfullyProcess = true
} catch (err) {
didSuccessfullyProcess = false
}
return didSuccessfullyProcess
}
export default processImageArea

View File

@ -0,0 +1,103 @@
import { PSM, createScheduler, createWorker } from 'tesseract.js'
import { GetDocumentById, RequestAddArea, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel'
import loadImage from './loadImage'
import { entities } from '../wailsjs/wailsjs/go/models'
import { saveProcessedText } from './saveData'
type rect = {
startX: number,
endX: number,
startY: number,
endY: number,
}
const processImageRect = async (documentId: string, rectangle: rect): Promise<entities.ProcessedArea[]> => {
const foundDocument = await GetDocumentById(documentId)
const { path, defaultLanguage } = foundDocument
if (!path || !defaultLanguage) return []
const processLanguage = defaultLanguage.processCode
const imageData = await loadImage(path)
let workerOptions: Partial<Tesseract.WorkerOptions> = {}
if (foundDocument.defaultLanguage.isBundledCustom) {
workerOptions = {
langPath: '/customLanguages',
gzip: false,
// logger: m => console.log(m)
}
}
const worker = await createWorker(workerOptions)
await worker.loadLanguage(processLanguage)
await worker.initialize(processLanguage)
await worker.setParameters({
tessedit_pageseg_mode: PSM.AUTO_OSD,
})
const scheduler = createScheduler()
scheduler.addWorker(worker)
const result = await scheduler.addJob('recognize', imageData, {
rectangle: {
left: rectangle.startX,
top: rectangle.startY,
width: rectangle.endX - rectangle.startX,
height: rectangle.endY - rectangle.startY,
}
})
const addAreaRequests = result.data.paragraphs.map(async (p: any) => {
const defaultAreaName = p.lines[0]?.words[0]?.text || ''
const area = await RequestAddArea(
documentId,
new entities.Area({
name: defaultAreaName,
startX: p.bbox.x0,
endX: p.bbox.x1,
startY: p.bbox.y0,
endY: p.bbox.y1,
})
)
const processedArea = await RequestAddProcessedArea(new entities.ProcessedArea({
id: area.id,
documentId,
order: area.order,
fullText: p.text,
lines: p.lines.map((l: any) => new entities.ProcessedLine({
fullText: l.text,
words: l.words.map((w: any) => new entities.ProcessedWord({
areaId: area.id,
fullText: w.text,
direction: w.direction,
confidence: w.confidence,
boundingBox: new entities.ProcessedBoundingBox({
x0: w.bbox.x0,
y0: w.bbox.y0,
x1: w.bbox.x1,
y1: w.bbox.y1,
}),
symbols: w.symbols.map((s: any) => new entities.ProcessedSymbol({
fullText: s.text,
confidence: s.confidence,
boundingBox: new entities.ProcessedBoundingBox({
x0: s.bbox.x0,
y0: s.bbox.y0,
x1: s.bbox.x1,
y1: s.bbox.y1,
})
}))
}))
}))
}))
return processedArea
})
const addAreaResponses = await Promise.allSettled(addAreaRequests)
const areas = addAreaResponses.filter((val): val is PromiseFulfilledResult<entities.ProcessedArea> => val.status === 'fulfilled').map(val => val.value)
await saveProcessedText()
return areas
}
export default processImageRect

View File

@ -1,4 +1,7 @@
import { RequestSaveDocumentCollection, RequestSaveGroupCollection, RequestSaveLocalUserProcessedMarkdownCollection, RequestSaveProcessedTextCollection } from '../wailsjs/wailsjs/go/ipc/Channel'
import { RequestSaveDocumentCollection, RequestSaveGroupCollection,
RequestSaveLocalUserProcessedMarkdownCollection,
RequestSaveProcessedTextCollection, RequestSaveContextGroupCollection
} from '../wailsjs/wailsjs/go/ipc/Channel'
const saveDocuments = async () => {
try {
@ -36,9 +39,19 @@ const saveUserProcessedMarkdown = async () => {
}
}
const saveContextGroups = async () => {
try {
const sucessfulSave = await RequestSaveContextGroupCollection()
if (!sucessfulSave) console.error('Could not save ContextGroupCollection')
} catch (err) {
console.error('Could not save ContextGroupCollection: ', err)
}
}
export {
saveDocuments,
saveGroups,
saveProcessedText,
saveUserProcessedMarkdown,
saveContextGroups,
}

View File

@ -0,0 +1,6 @@
const asyncClick = (e: React.MouseEvent, callback: (e: React.MouseEvent) => Promise<void>) => {
e.preventDefault()
callback(e)
}
export default asyncClick

View File

@ -0,0 +1,30 @@
import { RectangleCoordinates } from '../components/DocumentCanvas/types'
const getNormalizedRectToBounds = (rect: RectangleCoordinates, width: number, height: number, scale: number = 1): RectangleCoordinates => {
let startX: number, endX: number
if (rect.startX < rect.endX) {
startX = Math.floor(rect.startX / scale)
endX = Math.floor(rect.endX / scale)
} else {
startX = Math.floor(rect.endX / scale)
endX = Math.floor(rect.startX / scale)
}
let startY: number, endY: number
if (rect.startY < rect.endY) {
startY = Math.floor(rect.startY / scale)
endY = Math.floor(rect.endY / scale)
} else {
startY = Math.floor(rect.endY / scale)
endY = Math.floor(rect.startY / scale)
}
if (startX < 0) startX = 0
if (startY < 0) startY = 0
if (endX > width) endX = width
if (endY > height) endY = height
return { startX, startY, endX, endY }
}
export default getNormalizedRectToBounds

View File

@ -1,7 +1,7 @@
import { GetSuppportedLanguages } from '../wailsjs/wailsjs/go/ipc/Channel'
import { GetSupportedLanguages } from '../wailsjs/wailsjs/go/ipc/Channel'
const getSupportedLanguages = async () => {
const response = await GetSuppportedLanguages()
const response = await GetSupportedLanguages()
return response
}

View File

@ -1,49 +1,62 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {entities} from '../models';
import {ipc} from '../models';
export function CreateNewProject(arg1:string):Promise<ipc.Session>;
export function CreateNewProject(arg1:string):Promise<entities.Session>;
export function GetAllLocalProjects():Promise<Array<ipc.Project>>;
export function GetAllLocalProjects():Promise<Array<entities.Project>>;
export function GetAreaById(arg1:string):Promise<ipc.Area>;
export function GetAreaById(arg1:string):Promise<entities.Area>;
export function GetCurrentSession():Promise<ipc.Session>;
export function GetCurrentSession():Promise<entities.Session>;
export function GetCurrentUser():Promise<ipc.User>;
export function GetCurrentUser():Promise<entities.User>;
export function GetDocumentById(arg1:string):Promise<ipc.Document>;
export function GetDocumentById(arg1:string):Promise<entities.Document>;
export function GetDocuments():Promise<ipc.GetDocumentsResponse>;
export function GetProcessedAreasByDocumentId(arg1:string):Promise<Array<ipc.ProcessedArea>>;
export function GetProcessedAreaById(arg1:string):Promise<entities.ProcessedArea>;
export function GetProjectByName(arg1:string):Promise<ipc.Project>;
export function GetProcessedAreasByDocumentId(arg1:string):Promise<Array<entities.ProcessedArea>>;
export function GetSuppportedLanguages():Promise<Array<ipc.Language>>;
export function GetProjectByName(arg1:string):Promise<entities.Project>;
export function GetUserMarkdownByDocumentId(arg1:string):Promise<ipc.UserMarkdown>;
export function GetSerializedContextGroups():Promise<Array<entities.SerializedLinkedProcessedArea>>;
export function RequestAddArea(arg1:string,arg2:ipc.Area):Promise<ipc.Area>;
export function GetSupportedLanguages():Promise<Array<entities.Language>>;
export function RequestAddDocument(arg1:string,arg2:string):Promise<ipc.Document>;
export function GetUserMarkdownByDocumentId(arg1:string):Promise<entities.UserMarkdown>;
export function RequestAddDocumentGroup(arg1:string):Promise<ipc.Group>;
export function RequestAddArea(arg1:string,arg2:entities.Area):Promise<entities.Area>;
export function RequestAddProcessedArea(arg1:ipc.ProcessedArea):Promise<ipc.ProcessedArea>;
export function RequestAddDocument(arg1:string,arg2:string):Promise<entities.Document>;
export function RequestChangeAreaOrder(arg1:string,arg2:number):Promise<ipc.Document>;
export function RequestAddDocumentGroup(arg1:string):Promise<entities.Group>;
export function RequestChangeGroupOrder(arg1:string,arg2:number):Promise<ipc.Group>;
export function RequestAddProcessedArea(arg1:entities.ProcessedArea):Promise<entities.ProcessedArea>;
export function RequestChangeAreaOrder(arg1:string,arg2:number):Promise<entities.Document>;
export function RequestChangeGroupOrder(arg1:string,arg2:number):Promise<entities.Group>;
export function RequestChangeSessionProjectByName(arg1:string):Promise<boolean>;
export function RequestChooseUserAvatar():Promise<string>;
export function RequestConnectProcessedAreas(arg1:string,arg2:string):Promise<boolean>;
export function RequestDeleteAreaById(arg1:string):Promise<boolean>;
export function RequestDeleteDocumentAndChildren(arg1:string):Promise<boolean>;
export function RequestDeleteProcessedAreaById(arg1:string):Promise<boolean>;
export function RequestDisconnectProcessedAreas(arg1:string,arg2:string):Promise<boolean>;
export function RequestSaveContextGroupCollection():Promise<boolean>;
export function RequestSaveDocumentCollection():Promise<boolean>;
export function RequestSaveGroupCollection():Promise<boolean>;
@ -52,12 +65,16 @@ export function RequestSaveLocalUserProcessedMarkdownCollection():Promise<boolea
export function RequestSaveProcessedTextCollection():Promise<boolean>;
export function RequestUpdateArea(arg1:ipc.Area):Promise<ipc.Area>;
export function RequestTranslateArea(arg1:string):Promise<boolean>;
export function RequestUpdateCurrentUser(arg1:ipc.User):Promise<ipc.User>;
export function RequestUpdateArea(arg1:entities.Area):Promise<boolean>;
export function RequestUpdateDocument(arg1:ipc.Document):Promise<ipc.Document>;
export function RequestUpdateCurrentUser(arg1:entities.User):Promise<entities.User>;
export function RequestUpdateDocumentUserMarkdown(arg1:string,arg2:string):Promise<ipc.UserMarkdown>;
export function RequestUpdateDocument(arg1:entities.Document):Promise<entities.Document>;
export function RequestUpdateDocumentUserMarkdown(arg1:string,arg2:string):Promise<entities.UserMarkdown>;
export function RequestUpdateProcessedArea(arg1:entities.ProcessedArea):Promise<boolean>;
export function RequestUpdateProcessedWordById(arg1:string,arg2:string):Promise<boolean>;

View File

@ -30,6 +30,10 @@ export function GetDocuments() {
return window['go']['ipc']['Channel']['GetDocuments']();
}
export function GetProcessedAreaById(arg1) {
return window['go']['ipc']['Channel']['GetProcessedAreaById'](arg1);
}
export function GetProcessedAreasByDocumentId(arg1) {
return window['go']['ipc']['Channel']['GetProcessedAreasByDocumentId'](arg1);
}
@ -38,8 +42,12 @@ export function GetProjectByName(arg1) {
return window['go']['ipc']['Channel']['GetProjectByName'](arg1);
}
export function GetSuppportedLanguages() {
return window['go']['ipc']['Channel']['GetSuppportedLanguages']();
export function GetSerializedContextGroups() {
return window['go']['ipc']['Channel']['GetSerializedContextGroups']();
}
export function GetSupportedLanguages() {
return window['go']['ipc']['Channel']['GetSupportedLanguages']();
}
export function GetUserMarkdownByDocumentId(arg1) {
@ -78,6 +86,10 @@ export function RequestChooseUserAvatar() {
return window['go']['ipc']['Channel']['RequestChooseUserAvatar']();
}
export function RequestConnectProcessedAreas(arg1, arg2) {
return window['go']['ipc']['Channel']['RequestConnectProcessedAreas'](arg1, arg2);
}
export function RequestDeleteAreaById(arg1) {
return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1);
}
@ -86,6 +98,18 @@ export function RequestDeleteDocumentAndChildren(arg1) {
return window['go']['ipc']['Channel']['RequestDeleteDocumentAndChildren'](arg1);
}
export function RequestDeleteProcessedAreaById(arg1) {
return window['go']['ipc']['Channel']['RequestDeleteProcessedAreaById'](arg1);
}
export function RequestDisconnectProcessedAreas(arg1, arg2) {
return window['go']['ipc']['Channel']['RequestDisconnectProcessedAreas'](arg1, arg2);
}
export function RequestSaveContextGroupCollection() {
return window['go']['ipc']['Channel']['RequestSaveContextGroupCollection']();
}
export function RequestSaveDocumentCollection() {
return window['go']['ipc']['Channel']['RequestSaveDocumentCollection']();
}
@ -102,6 +126,10 @@ export function RequestSaveProcessedTextCollection() {
return window['go']['ipc']['Channel']['RequestSaveProcessedTextCollection']();
}
export function RequestTranslateArea(arg1) {
return window['go']['ipc']['Channel']['RequestTranslateArea'](arg1);
}
export function RequestUpdateArea(arg1) {
return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1);
}
@ -118,6 +146,10 @@ export function RequestUpdateDocumentUserMarkdown(arg1, arg2) {
return window['go']['ipc']['Channel']['RequestUpdateDocumentUserMarkdown'](arg1, arg2);
}
export function RequestUpdateProcessedArea(arg1) {
return window['go']['ipc']['Channel']['RequestUpdateProcessedArea'](arg1);
}
export function RequestUpdateProcessedWordById(arg1, arg2) {
return window['go']['ipc']['Channel']['RequestUpdateProcessedWordById'](arg1, arg2);
}

View File

@ -1,9 +1,10 @@
export namespace ipc {
export namespace entities {
export class Language {
displayName: string;
processCode: string;
translateCode: string;
isBundledCustom: boolean;
static createFrom(source: any = {}) {
return new Language(source);
@ -14,6 +15,7 @@ export namespace ipc {
this.displayName = source["displayName"];
this.processCode = source["processCode"];
this.translateCode = source["translateCode"];
this.isBundledCustom = source["isBundledCustom"];
}
}
export class Area {
@ -24,6 +26,7 @@ export namespace ipc {
endX: number;
endY: number;
language: Language;
translateLanguage: Language;
order: number;
static createFrom(source: any = {}) {
@ -39,6 +42,7 @@ export namespace ipc {
this.endX = source["endX"];
this.endY = source["endY"];
this.language = this.convertValues(source["language"], Language);
this.translateLanguage = this.convertValues(source["translateLanguage"], Language);
this.order = source["order"];
}
@ -122,39 +126,6 @@ export namespace ipc {
this.order = source["order"];
}
}
export class GetDocumentsResponse {
documents: Document[];
groups: Group[];
static createFrom(source: any = {}) {
return new GetDocumentsResponse(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.documents = this.convertValues(source["documents"], Document);
this.groups = this.convertValues(source["groups"], Group);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class User {
id: string;
@ -270,6 +241,7 @@ export namespace ipc {
}
export class ProcessedWord {
id: string;
areaId: string;
fullText: string;
symbols: ProcessedSymbol[];
confidence: number;
@ -283,6 +255,7 @@ export namespace ipc {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.areaId = source["areaId"];
this.fullText = source["fullText"];
this.symbols = this.convertValues(source["symbols"], ProcessedSymbol);
this.confidence = source["confidence"];
@ -309,7 +282,6 @@ export namespace ipc {
}
}
export class ProcessedLine {
fullText: string;
words: ProcessedWord[];
static createFrom(source: any = {}) {
@ -318,7 +290,6 @@ export namespace ipc {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.fullText = source["fullText"];
this.words = this.convertValues(source["words"], ProcessedWord);
}
@ -343,7 +314,6 @@ export namespace ipc {
export class ProcessedArea {
id: string;
documentId: string;
fullText: string;
order: number;
lines: ProcessedLine[];
@ -355,7 +325,6 @@ export namespace ipc {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.documentId = source["documentId"];
this.fullText = source["fullText"];
this.order = source["order"];
this.lines = this.convertValues(source["lines"], ProcessedLine);
}
@ -453,6 +422,22 @@ export namespace ipc {
}
}
export class SerializedLinkedProcessedArea {
areaId: string;
previousId: string;
nextId: string;
static createFrom(source: any = {}) {
return new SerializedLinkedProcessedArea(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.areaId = source["areaId"];
this.previousId = source["previousId"];
this.nextId = source["nextId"];
}
}
export class Session {
project: Project;
organization: Organization;
@ -507,3 +492,42 @@ export namespace ipc {
}
export namespace ipc {
export class GetDocumentsResponse {
documents: entities.Document[];
groups: entities.Group[];
contextGroups: entities.SerializedLinkedProcessedArea[];
static createFrom(source: any = {}) {
return new GetDocumentsResponse(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.documents = this.convertValues(source["documents"], entities.Document);
this.groups = this.convertValues(source["groups"], entities.Group);
this.contextGroups = this.convertValues(source["contextGroups"], entities.SerializedLinkedProcessedArea);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}

View File

@ -225,3 +225,11 @@ export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;

View File

@ -37,11 +37,11 @@ export function LogFatal(message) {
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
EventsOnMultiple(eventName, callback, -1);
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
@ -49,7 +49,7 @@ export function EventsOff(eventName, ...additionalEventNames) {
}
export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1);
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
@ -192,3 +192,11 @@ export function Hide() {
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}

10
go.mod
View File

@ -6,13 +6,13 @@ go 1.18
require (
github.com/google/uuid v1.3.0
github.com/wailsapp/wails/v2 v2.3.1
github.com/snakesel/libretranslate v0.0.2
github.com/wailsapp/wails/v2 v2.5.1
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.9.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
@ -30,7 +30,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
)

21
go.sum
View File

@ -7,8 +7,6 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
@ -40,6 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw=
github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
github.com/snakesel/libretranslate v0.0.2 h1:6LG/UMMpGtoj3NXvlzsxZgQEH0Qsi62jCDd5Yq5ALL8=
github.com/snakesel/libretranslate v0.0.2/go.mod h1:B8F8Dda8RlkHRMzs/aw8DWj9HfyHSXpaJTFD391hEUI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
@ -52,15 +52,15 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.3.1 h1:ZJz+pyIBKyASkgO8JO31NuHO1gTTHmvwiHYHwei1CqM=
github.com/wailsapp/wails/v2 v2.3.1/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8=
github.com/wailsapp/wails/v2 v2.5.1 h1:mfG+2kWqQXYOwdgI43HEILjOZDXbk5woPYI3jP2b+js=
github.com/wailsapp/wails/v2 v2.5.1/go.mod h1:jbOZbcr/zm79PxXxAjP8UoVlDd9wLW3uDs+isIthDfs=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f h1:90Jq/vvGVDsqj8QqCynjFw9MCerDguSMODLYII416Y8=
golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -70,15 +70,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -1,5 +1,10 @@
package ipc
import (
document "textualize/core/Document"
"textualize/translate"
)
type Channel struct{}
var channelInstance *Channel
@ -11,3 +16,34 @@ func GetInstance() *Channel {
return channelInstance
}
func (c *Channel) RequestTranslateArea(areaId string) bool {
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(areaId)
area := documentOfArea.GetAreaById(areaId)
processedArea := document.GetProcessedAreaCollection().GetAreaById(area.Id)
var textToTranslate string
for _, line := range processedArea.Lines {
for _, word := range line.Words {
textToTranslate = textToTranslate + " " + word.FullText
}
}
var sourceLanguage string
if area.Language.TranslateCode != "" {
sourceLanguage = area.Language.TranslateCode
} else if documentOfArea.DefaultLanguage.TranslateCode != "" {
sourceLanguage = documentOfArea.DefaultLanguage.TranslateCode
} else {
return false
}
sourceLanguage = "he"
targetLanguage := "en"
translatedText := translate.Text(textToTranslate, sourceLanguage, targetLanguage)
if translatedText == "" {
return true
} else {
return false
}
}

68
ipc/ContextGroup.go Normal file
View File

@ -0,0 +1,68 @@
package ipc
import (
contextGroup "textualize/core/ContextGroup"
document "textualize/core/Document"
"textualize/entities"
"textualize/storage"
)
func (c *Channel) RequestDisconnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
contextGroupCollection := contextGroup.GetContextGroupCollection()
wasSuccessfulDisconnect := contextGroupCollection.DisconnectProcessedAreas(ancestorAreaId, descendantAreaId)
if wasSuccessfulDisconnect {
wasSuccessfulWrite := c.RequestSaveContextGroupCollection()
return wasSuccessfulWrite
}
return false
}
/*
If a connection already exists, then this method will default to disconnecting the two areas.
*/
func (c *Channel) RequestConnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
contextGroupCollection := contextGroup.GetContextGroupCollection()
doesContextGroupAlreadyExist := contextGroupCollection.DoesGroupExistBetweenProcessedAreas(ancestorAreaId, descendantAreaId)
if doesContextGroupAlreadyExist {
return c.RequestDisconnectProcessedAreas(ancestorAreaId, descendantAreaId)
}
processedAreaCollection := document.GetProcessedAreaCollection()
ancestorArea := processedAreaCollection.GetAreaById(ancestorAreaId)
descendantArea := processedAreaCollection.GetAreaById(descendantAreaId)
wasSuccessfulConnect := contextGroupCollection.ConnectProcessedAreas(*ancestorArea, *descendantArea)
if wasSuccessfulConnect {
wasSuccessfulWrite := c.RequestSaveContextGroupCollection()
return wasSuccessfulWrite
}
return false
}
func (c *Channel) GetSerializedContextGroups() []entities.SerializedLinkedProcessedArea {
contextGroupCollection := contextGroup.GetContextGroupCollection()
serializedContextGroups := make([]entities.SerializedLinkedProcessedArea, 0)
for _, group := range contextGroupCollection.Groups {
serializedContextGroups = append(serializedContextGroups, group.Serialize()...)
}
return serializedContextGroups
}
func (c *Channel) RequestSaveContextGroupCollection() bool {
contextGroupCollection := contextGroup.GetContextGroupCollection()
projectName := c.GetCurrentSession().Project.Name
serializedContextGroups := make([]entities.SerializedLinkedProcessedArea, 0)
for _, group := range contextGroupCollection.Groups {
serializedContextGroups = append(serializedContextGroups, group.Serialize()...)
}
successfulWrite := storage.GetDriver().WriteContextGroupCollection(serializedContextGroups, projectName)
return successfulWrite
}

View File

@ -1,113 +1,63 @@
package ipc
import (
"fmt"
"sort"
app "textualize/core/App"
consts "textualize/core/Consts"
document "textualize/core/Document"
session "textualize/core/Session"
"textualize/entities"
storage "textualize/storage"
storageEntity "textualize/storage/Entities"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type GetDocumentsResponse struct {
Documents []Document `json:"documents"`
Groups []Group `json:"groups"`
Documents []entities.Document `json:"documents"`
Groups []entities.Group `json:"groups"`
ContextGroups []entities.SerializedLinkedProcessedArea `json:"contextGroups"`
}
func (c *Channel) GetDocumentById(id string) Document {
func (c *Channel) GetDocumentById(id string) entities.Document {
foundDocument := document.GetDocumentCollection().GetDocumentById(id)
var jsonAreas []Area
for _, a := range foundDocument.Areas {
jsonAreas = append(jsonAreas, Area{
Id: a.Id,
Name: a.Name,
StartX: a.StartX,
StartY: a.StartY,
EndX: a.EndX,
EndY: a.EndY,
Order: a.Order,
Language: Language(a.Language),
})
}
response := Document{
Id: foundDocument.Id,
Name: foundDocument.Name,
GroupId: foundDocument.GroupId,
Path: foundDocument.Path,
ProjectId: foundDocument.ProjectId,
Areas: jsonAreas,
DefaultLanguage: Language(foundDocument.DefaultLanguage),
}
return response
return entities.Document(*foundDocument)
}
func (c *Channel) GetDocuments() GetDocumentsResponse {
documents := document.GetDocumentCollection().Documents
groups := document.GetGroupCollection().Groups
contextGroups := c.GetSerializedContextGroups()
response := GetDocumentsResponse{
Groups: make([]Group, 0),
Documents: make([]Document, 0),
Groups: make([]entities.Group, 0),
Documents: make([]entities.Document, 0),
ContextGroups: contextGroups,
}
for _, d := range documents {
jsonAreas := make([]Area, 0)
for _, a := range d.Areas {
jsonAreas = append(jsonAreas, Area{
Id: a.Id,
Name: a.Name,
StartX: a.StartX,
StartY: a.StartY,
EndX: a.EndX,
EndY: a.EndY,
Order: a.Order,
Language: Language(a.Language),
})
}
sort.Slice(jsonAreas, func(i, j int) bool {
return jsonAreas[i].Order < jsonAreas[j].Order
sortedAreas := d.Areas
sort.Slice(sortedAreas, func(i, j int) bool {
return sortedAreas[i].Order < sortedAreas[j].Order
})
jsonDocument := Document{
Id: d.Id,
GroupId: d.GroupId,
Name: d.Name,
Path: d.Path,
ProjectId: d.ProjectId,
Areas: jsonAreas,
DefaultLanguage: Language(d.DefaultLanguage),
}
jsonDocument := entities.Document(d)
d.Areas = sortedAreas
response.Documents = append(response.Documents, jsonDocument)
}
jsonGroups := make([]Group, 0)
for _, g := range groups {
jsonGroup := Group{
Id: g.Id,
ParentId: g.ParentId,
ProjectId: g.ProjectId,
Name: g.Name,
Order: g.Order,
}
jsonGroups = append(jsonGroups, jsonGroup)
}
sort.Slice(jsonGroups, func(i, j int) bool {
return jsonGroups[i].Order < jsonGroups[j].Order
if len(groups) > 0 {
sortedGroups := groups
sort.Slice(sortedGroups, func(i, j int) bool {
return sortedGroups[i].Order < sortedGroups[j].Order
})
response.Groups = jsonGroups
response.Groups = sortedGroups
}
return response
}
func (c *Channel) RequestAddDocument(groupId string, documentName string) Document {
func (c *Channel) RequestAddDocument(groupId string, documentName string) entities.Document {
filePath, err := runtime.OpenFileDialog(app.GetInstance().Context, runtime.OpenDialogOptions{
Title: "Select an Image",
Filters: []runtime.FileFilter{
@ -120,7 +70,7 @@ func (c *Channel) RequestAddDocument(groupId string, documentName string) Docume
if err != nil {
runtime.LogError(app.GetInstance().Context, err.Error())
return Document{}
return entities.Document{}
}
newDocument := document.Entity{
@ -133,15 +83,7 @@ func (c *Channel) RequestAddDocument(groupId string, documentName string) Docume
document.GetDocumentCollection().AddDocument(newDocument)
documentResponse := Document{
Id: newDocument.Id,
Name: newDocument.Name,
Path: newDocument.Path,
GroupId: newDocument.GroupId,
ProjectId: newDocument.ProjectId,
}
return documentResponse
return entities.Document(newDocument)
}
func (c *Channel) deleteDocumentById(documentId string) bool {
@ -164,11 +106,11 @@ func (c *Channel) deleteDocumentById(documentId string) bool {
return true
}
func (c *Channel) RequestUpdateDocumentUserMarkdown(documentId string, markdown string) UserMarkdown {
func (c *Channel) RequestUpdateDocumentUserMarkdown(documentId string, markdown string) entities.UserMarkdown {
markdownCollection := document.GetUserMarkdownCollection()
markdownToUpdate := markdownCollection.GetUserMarkdownByDocumentId(documentId)
newMarkdown := document.UserMarkdown{
newMarkdown := entities.UserMarkdown{
DocumentId: documentId,
Value: markdown,
}
@ -179,11 +121,7 @@ func (c *Channel) RequestUpdateDocumentUserMarkdown(documentId string, markdown
}
updatedMarkdown := markdownCollection.UpdateUserMarkdown(newMarkdown)
return UserMarkdown{
Id: updatedMarkdown.Id,
DocumentId: updatedMarkdown.DocumentId,
Value: updatedMarkdown.Value,
}
return entities.UserMarkdown(updatedMarkdown)
}
func (c *Channel) deleteDocumentUserMarkdown(documentId string) bool {
@ -206,26 +144,15 @@ func (c *Channel) deleteDocumentUserMarkdown(documentId string) bool {
return true
}
func (c *Channel) GetUserMarkdownByDocumentId(documentId string) UserMarkdown {
func (c *Channel) GetUserMarkdownByDocumentId(documentId string) entities.UserMarkdown {
foundUserMarkdown := document.GetUserMarkdownCollection().GetUserMarkdownByDocumentId((documentId))
response := UserMarkdown{}
if foundUserMarkdown != nil {
response = UserMarkdown{
Id: foundUserMarkdown.Id,
DocumentId: foundUserMarkdown.DocumentId,
Value: foundUserMarkdown.Value,
}
return entities.UserMarkdown(*foundUserMarkdown)
}
return response
}
func (c *Channel) RequestAddDocumentGroup(name string) Group {
func (c *Channel) RequestAddDocumentGroup(name string) entities.Group {
groupCollection := document.GetGroupCollection()
newGroup := document.Group{
newGroup := entities.Group{
Id: uuid.NewString(),
Name: name,
ProjectId: session.GetInstance().Project.Id,
@ -234,60 +161,41 @@ func (c *Channel) RequestAddDocumentGroup(name string) Group {
groupCollection.AddDocumentGroup(newGroup)
response := Group{
Id: newGroup.Id,
Name: newGroup.Name,
ParentId: newGroup.ParentId,
ProjectId: newGroup.ProjectId,
Order: newGroup.Order,
return newGroup
}
return response
}
func (c *Channel) RequestChangeGroupOrder(groupId string, newOrder int) Group {
func (c *Channel) RequestChangeGroupOrder(groupId string, newOrder int) entities.Group {
groupCollection := document.GetGroupCollection()
for _, g := range groupCollection.Groups {
if g.Id == groupId {
// document.GetGroupCollection().Groups[index].Order = newOrder
document.GetGroupCollection().GetGroupById(groupId).Order = newOrder
} else if g.Order >= newOrder {
// document.GetGroupCollection().Groups[index].Order = g.Order + 1
document.GetGroupCollection().GetGroupById(groupId).Order = g.Order + 1
}
}
return Group(*document.GetGroupCollection().GetGroupById(groupId))
return *document.GetGroupCollection().GetGroupById(groupId)
}
func (c *Channel) GetAreaById(areaId string) Area {
func (c *Channel) GetAreaById(areaId string) entities.Area {
foundDocument := document.GetDocumentCollection().GetDocumentByAreaId(areaId)
if len(foundDocument.Areas) == 0 {
return Area{}
return entities.Area{}
}
var foundArea document.Area
var foundArea entities.Area
for i, a := range foundDocument.Areas {
if a.Id == areaId {
foundArea = foundDocument.Areas[i]
}
}
return Area{
Id: foundArea.Id,
Name: foundArea.Name,
StartX: foundArea.StartX,
EndX: foundArea.EndX,
StartY: foundArea.StartY,
EndY: foundArea.EndY,
Order: foundArea.Order,
Language: Language(foundArea.Language),
}
return foundArea
}
func (c *Channel) RequestAddArea(documentId string, area Area) Area {
func (c *Channel) RequestAddArea(documentId string, area entities.Area) entities.Area {
foundDocument := document.GetDocumentCollection().GetDocumentById(documentId)
var id string
@ -302,7 +210,7 @@ func (c *Channel) RequestAddArea(documentId string, area Area) Area {
order = len(foundDocument.Areas)
}
newArea := document.Area{
newArea := entities.Area{
Id: id,
Name: area.Name,
StartX: area.StartX,
@ -310,47 +218,40 @@ func (c *Channel) RequestAddArea(documentId string, area Area) Area {
StartY: area.StartY,
EndY: area.EndY,
Order: order,
Language: consts.Language(area.Language),
Language: entities.Language(area.Language),
}
foundDocument.AddArea(newArea)
responseArea := area
responseArea.Id = id
return responseArea
return newArea
}
func (c *Channel) RequestUpdateArea(updatedArea Area) Area {
func (c *Channel) RequestUpdateArea(updatedArea entities.Area) bool {
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(updatedArea.Id)
if documentOfArea.Id == "" {
return Area{}
return false
}
areaToUpdate := documentOfArea.GetAreaById(updatedArea.Id)
if areaToUpdate.Id == "" {
return Area{}
return false
}
// TODO: add more prop changes when needed
if updatedArea.Name != "" {
areaToUpdate.Name = updatedArea.Name
}
if updatedArea.Order != areaToUpdate.Order {
areaToUpdate.Order = updatedArea.Order
}
return Area{
Id: areaToUpdate.Id,
Name: areaToUpdate.Name,
StartX: areaToUpdate.StartX,
StartY: areaToUpdate.StartY,
EndX: areaToUpdate.EndX,
EndY: areaToUpdate.EndY,
Order: areaToUpdate.Order,
Language: Language(areaToUpdate.Language),
if updatedArea.Language.ProcessCode != "" {
areaToUpdate.Language = updatedArea.Language
}
fmt.Println(areaToUpdate.Language)
fmt.Println(documentOfArea.GetAreaById(updatedArea.Id))
return true
}
func (c *Channel) RequestDeleteAreaById(areaId string) bool {
@ -379,11 +280,11 @@ func (c *Channel) RequestDeleteAreaById(areaId string) bool {
}
func (c *Channel) RequestUpdateDocument(updatedDocument Document) Document {
func (c *Channel) RequestUpdateDocument(updatedDocument entities.Document) entities.Document {
documentToUpdate := document.GetDocumentCollection().GetDocumentById(updatedDocument.Id)
if documentToUpdate == nil {
return Document{}
return entities.Document{}
}
if updatedDocument.Id != "" {
@ -399,20 +300,20 @@ func (c *Channel) RequestUpdateDocument(updatedDocument Document) Document {
documentToUpdate.Path = updatedDocument.Path
}
if updatedDocument.DefaultLanguage.DisplayName != "" {
documentToUpdate.DefaultLanguage = consts.Language(updatedDocument.DefaultLanguage)
documentToUpdate.DefaultLanguage = updatedDocument.DefaultLanguage
}
return updatedDocument
}
func (c *Channel) RequestChangeAreaOrder(areaId string, newOrder int) Document {
func (c *Channel) RequestChangeAreaOrder(areaId string, newOrder int) entities.Document {
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId((areaId))
if documentOfArea == nil {
return Document{}
return entities.Document{}
}
var foundArea document.Area
var foundArea entities.Area
for _, a := range documentOfArea.Areas {
if a.Id == areaId {
foundArea = a
@ -421,7 +322,7 @@ func (c *Channel) RequestChangeAreaOrder(areaId string, newOrder int) Document {
}
if foundArea.Id == "" {
return Document{}
return entities.Document{}
}
processedAreasCollection := document.GetProcessedAreaCollection()
@ -455,37 +356,18 @@ func (c *Channel) RequestSaveDocumentCollection() bool {
return false
}
var documentsToWrite []storageEntity.Document
for _, d := range documentCollection.Documents {
var areasToWrite []storageEntity.Area
for _, a := range d.Areas {
areasToWrite = append(areasToWrite, storageEntity.Area{
Id: a.Id,
Name: a.Name,
StartX: a.StartX,
StartY: a.StartY,
EndX: a.EndX,
EndY: a.EndY,
Language: storageEntity.Language(a.Language),
Order: a.Order,
})
documentCount := len(documentCollection.Documents)
writableDocuments := make([]entities.Document, documentCount)
for i := 0; i < documentCount; i++ {
writableDocuments[i] = entities.Document(documentCollection.Documents[i])
}
documentsToWrite = append(documentsToWrite, storageEntity.Document{
Id: d.Id,
GroupId: d.GroupId,
Name: d.Name,
Path: d.Path,
ProjectId: d.ProjectId,
Areas: areasToWrite,
DefaultLanguage: storageEntity.Language(d.DefaultLanguage),
})
}
successfulWrite := storage.GetDriver().WriteDocumentCollection(storageEntity.DocumentCollection{
Documents: documentsToWrite,
successfulWrite := storage.GetDriver().WriteDocumentCollection(
entities.DocumentCollection{
ProjectId: fullProject.Id,
}, projectName)
Documents: writableDocuments,
},
projectName)
return successfulWrite
}
@ -500,15 +382,16 @@ func (c *Channel) RequestSaveGroupCollection() bool {
return false
}
var groupsToWrite []storageEntity.Group
for _, g := range groupCollection.Groups {
groupsToWrite = append(groupsToWrite, storageEntity.Group(g))
groupCount := len(groupCollection.Groups)
writableGroups := make([]entities.Group, groupCount)
for i := 0; i < groupCount; i++ {
writableGroups[i] = entities.Group(groupCollection.Groups[i])
}
successfulWrite := storage.GetDriver().WriteGroupCollection(storageEntity.GroupCollection{
successfulWrite := storage.GetDriver().WriteGroupCollection(entities.GroupCollection{
Id: groupCollection.Id,
ProjectId: groupCollection.ProjectId,
Groups: groupsToWrite,
Groups: writableGroups,
}, projectName)
return successfulWrite
@ -518,53 +401,18 @@ func (c *Channel) RequestSaveProcessedTextCollection() bool {
processedAreaCollection := document.GetProcessedAreaCollection()
projectName := c.GetCurrentSession().Project.Name
areasToWrite := make([]storageEntity.ProcessedArea, 0)
for _, a := range processedAreaCollection.Areas {
linesOfAreaToWrite := make([]storageEntity.ProcessedLine, 0)
for _, l := range a.Lines {
wordsOfLineToWrite := make([]storageEntity.ProcessedWord, 0)
for _, w := range l.Words {
symbolsOfWordToWrite := make([]storageEntity.ProcessedSymbol, 0)
for _, s := range w.Symbols {
symbolsOfWordToWrite = append(symbolsOfWordToWrite, storageEntity.ProcessedSymbol{
Text: s.Text,
Confidence: s.Confidence,
BoundingBox: storageEntity.ProcessedBoundingBox(s.BoundingBox),
})
processedAreasCount := len(processedAreaCollection.Areas)
writableProcessedAreasAreas := make([]entities.ProcessedArea, processedAreasCount)
for i := 0; i < processedAreasCount; i++ {
writableProcessedAreasAreas[i] = entities.ProcessedArea(processedAreaCollection.Areas[i])
}
wordsOfLineToWrite = append(wordsOfLineToWrite, storageEntity.ProcessedWord{
Id: w.Id,
FullText: w.FullText,
Confidence: w.Confidence,
Direction: w.Direction,
BoundingBox: storageEntity.ProcessedBoundingBox(w.BoundingBox),
Symbols: symbolsOfWordToWrite,
})
}
linesOfAreaToWrite = append(linesOfAreaToWrite, storageEntity.ProcessedLine{
FullText: l.FullText,
Words: wordsOfLineToWrite,
})
}
areasToWrite = append(areasToWrite, storageEntity.ProcessedArea{
Id: a.Id,
DocumentId: a.DocumentId,
FullText: a.FullText,
Order: a.Order,
Lines: linesOfAreaToWrite,
})
}
processedAreaCollectionToWrite := storageEntity.ProcessedTextCollection{
Areas: areasToWrite,
}
successfulWrite := storage.GetDriver().WriteProcessedTextCollection(processedAreaCollectionToWrite, projectName)
successfulWrite := storage.GetDriver().WriteProcessedTextCollection(
entities.ProcessedTextCollection{
Areas: writableProcessedAreasAreas,
},
projectName,
)
return successfulWrite
}
@ -578,14 +426,18 @@ func (c *Channel) RequestSaveLocalUserProcessedMarkdownCollection() bool {
return false
}
var valuesToWrite []storageEntity.ProcessedUserMarkdown
for _, v := range userProcessedMarkdownCollection.Values {
valuesToWrite = append(valuesToWrite, storageEntity.ProcessedUserMarkdown(v))
groupCount := len(userProcessedMarkdownCollection.Values)
writableMarkdownValues := make([]entities.ProcessedUserMarkdown, groupCount)
for i := 0; i < groupCount; i++ {
writableMarkdownValues[i] = entities.ProcessedUserMarkdown(userProcessedMarkdownCollection.Values[i])
}
successfulWrite := storage.GetDriver().WriteProcessedUserMarkdownCollection(storageEntity.ProcessedUserMarkdownCollection{
Values: valuesToWrite,
}, projectName)
successfulWrite := storage.GetDriver().WriteProcessedUserMarkdownCollection(
entities.ProcessedUserMarkdownCollection{
Values: writableMarkdownValues,
},
projectName,
)
return successfulWrite
}

View File

@ -1,128 +0,0 @@
package ipc
type Document struct {
Id string `json:"id"`
GroupId string `json:"groupId"`
Name string `json:"name"`
Path string `json:"path"`
ProjectId string `json:"projectId"`
Areas []Area `json:"areas"`
DefaultLanguage Language `json:"defaultLanguage"`
}
type DocumentCollection struct {
Documents []Document `json:"documents"`
ProjectId string `json:"projectId"`
}
type Group struct {
Id string `json:"id"`
ParentId string `json:"parentId"`
ProjectId string `json:"projectId"`
Name string `json:"name"`
Order int `json:"order"`
}
type GroupCollection struct {
Id string `json:"id"`
Groups []Group `json:"groups"`
ProjectId string `json:"projectId"`
}
type Area struct {
Id string `json:"id"`
Name string `json:"name"`
StartX int `json:"startX"`
StartY int `json:"startY"`
EndX int `json:"endX"`
EndY int `json:"endY"`
Language Language `json:"language"`
Order int `json:"order"`
}
type ProcessedBoundingBox struct {
X0 int32 `json:"x0"`
Y0 int32 `json:"y0"`
X1 int32 `json:"x1"`
Y1 int32 `json:"y1"`
}
type ProcessedSymbol struct {
Text string `json:"text"`
Confidence float32 `json:"confidence"`
BoundingBox ProcessedBoundingBox `json:"boundingBox"`
}
type ProcessedWord struct {
Id string `json:"id"`
FullText string `json:"fullText"`
Symbols []ProcessedSymbol `json:"symbols"`
Confidence float32 `json:"confidence"`
Direction string `json:"direction"`
BoundingBox ProcessedBoundingBox `json:"boundingBox"`
}
type ProcessedLine struct {
FullText string `json:"fullText"`
Words []ProcessedWord `json:"words"`
}
type ProcessedArea struct {
Id string `json:"id"`
DocumentId string `json:"documentId"`
FullText string `json:"fullText"`
Order int `json:"order"`
Lines []ProcessedLine `json:"lines"`
}
type UserMarkdown struct {
Id string `json:"id"`
DocumentId string `json:"documentId"`
Value string `json:"value"`
}
type UserMarkdownCollection struct {
Values []UserMarkdown `json:"values"`
}
type User struct {
Id string `json:"id"`
LocalId string `json:"localId"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
AvatarPath string `json:"avatarPath"`
AuthToken string `json:"authToken"`
Email string `json:"email"`
}
type Organization struct {
Id string `json:"id"`
Name string `json:"name"`
LogoPath string `json:"logoPath"`
Users []User `json:"users"`
}
type Project struct {
Id string `json:"id"`
OrganizationId string `json:"organizationId"`
Name string `json:"name"`
Settings ProjectSettings `json:"settings"`
}
type ProjectSettings struct {
DefaultProcessLanguage Language `json:"defaultProcessLanguage"`
DefaultTranslateTargetLanguage Language `json:"defaultTranslateTargetLanguage"`
IsHosted bool `json:"isHosted"`
}
type Session struct {
Project Project `json:"project"`
Organization Organization `json:"organization"`
User User `json:"user"`
}
type Language struct {
DisplayName string `json:"displayName"`
ProcessCode string `json:"processCode"`
TranslateCode string `json:"translateCode"`
}

View File

@ -3,181 +3,93 @@ package ipc
import (
"sort"
document "textualize/core/Document"
"textualize/entities"
"github.com/google/uuid"
)
func serializeBoundingBox(bbox document.ProcessedBoundingBox) ProcessedBoundingBox {
return ProcessedBoundingBox{
X0: bbox.X0,
Y0: bbox.Y0,
X1: bbox.X1,
Y1: bbox.Y1,
func (c *Channel) GetProcessedAreaById(id string) entities.ProcessedArea {
foundArea := document.GetProcessedAreaCollection().GetAreaById(id)
if foundArea != nil {
return *foundArea
} else {
return entities.ProcessedArea{}
}
}
func serializeSymbol(symbol document.ProcessedSymbol) ProcessedSymbol {
return ProcessedSymbol{
Text: symbol.Text,
Confidence: symbol.Confidence,
BoundingBox: serializeBoundingBox(symbol.BoundingBox),
}
}
func serialzeWord(word document.ProcessedWord) ProcessedWord {
var symbols []ProcessedSymbol
for _, symbol := range word.Symbols {
symbols = append(symbols, serializeSymbol(symbol))
}
return ProcessedWord{
Id: word.Id,
FullText: word.FullText,
Symbols: symbols,
Confidence: word.Confidence,
Direction: word.Direction,
BoundingBox: serializeBoundingBox(word.BoundingBox),
}
}
func serializeLine(line document.ProcessedLine) ProcessedLine {
var words []ProcessedWord
for _, word := range line.Words {
words = append(words, serialzeWord((word)))
}
return ProcessedLine{
FullText: line.FullText,
Words: words,
}
}
func serializeProcessedArea(area document.ProcessedArea) ProcessedArea {
var lines []ProcessedLine
for _, line := range area.Lines {
lines = append(lines, serializeLine(line))
}
return ProcessedArea{
Id: area.Id,
DocumentId: area.DocumentId,
FullText: area.FullText,
Order: area.Order,
Lines: lines,
}
}
func (c *Channel) GetProcessedAreasByDocumentId(id string) []ProcessedArea {
func (c *Channel) GetProcessedAreasByDocumentId(id string) []entities.ProcessedArea {
areas := document.GetProcessedAreaCollection().GetAreasByDocumentId(id)
var response []ProcessedArea
for _, a := range areas {
response = append(response, serializeProcessedArea(*a))
areaCount := len(areas)
readableAreas := make([]entities.ProcessedArea, areaCount)
for i := 0; i < areaCount; i++ {
readableAreas[i] = entities.ProcessedArea(*areas[i])
}
sort.Slice(response, func(i, j int) bool {
return response[i].Order < response[j].Order
sortedAreas := readableAreas
sort.Slice(sortedAreas, func(i, j int) bool {
return sortedAreas[i].Order < sortedAreas[j].Order
})
return response
return sortedAreas
}
func deserializeBoundingBox(bbox ProcessedBoundingBox) document.ProcessedBoundingBox {
return document.ProcessedBoundingBox{
X0: bbox.X0,
Y0: bbox.Y0,
X1: bbox.X1,
Y1: bbox.Y1,
}
}
func (c *Channel) RequestAddProcessedArea(processedArea entities.ProcessedArea) entities.ProcessedArea {
func deserializeSymbol(symbol ProcessedSymbol) document.ProcessedSymbol {
return document.ProcessedSymbol{
Text: symbol.Text,
Confidence: symbol.Confidence,
BoundingBox: deserializeBoundingBox(symbol.BoundingBox),
}
}
func deserialzeWord(word ProcessedWord) document.ProcessedWord {
var symbols []document.ProcessedSymbol
for _, symbol := range word.Symbols {
symbols = append(symbols, deserializeSymbol(symbol))
}
var wordId string
for lineIndex, line := range processedArea.Lines {
for wordIndex, word := range line.Words {
if word.Id == "" {
wordId = uuid.NewString()
} else {
wordId = word.Id
processedArea.Lines[lineIndex].Words[wordIndex].Id = uuid.NewString()
}
return document.ProcessedWord{
Id: wordId,
FullText: word.FullText,
Symbols: symbols,
Confidence: word.Confidence,
Direction: word.Direction,
BoundingBox: deserializeBoundingBox(word.BoundingBox),
}
}
func deserializeLine(line ProcessedLine) document.ProcessedLine {
var words []document.ProcessedWord
for _, word := range line.Words {
words = append(words, deserialzeWord((word)))
document.GetProcessedAreaCollection().AddProcessedArea(processedArea)
return *document.GetProcessedAreaCollection().GetAreaById(processedArea.Id)
}
return document.ProcessedLine{
FullText: line.FullText,
Words: words,
func (c *Channel) RequestDeleteProcessedAreaById(id string) bool {
processedAreas := document.GetProcessedAreaCollection().Areas
areaToUpdate := document.GetProcessedAreaCollection().GetAreaById(id)
if areaToUpdate.Id == "" {
return false
}
areaToDeleteIndex := -1
for i, a := range processedAreas {
if a.Id == id {
areaToDeleteIndex = i
break
}
}
func deserializeProcessedArea(area ProcessedArea) document.ProcessedArea {
var lines []document.ProcessedLine
for _, line := range area.Lines {
lines = append(lines, deserializeLine(line))
if areaToDeleteIndex < 0 {
return false
}
return document.ProcessedArea{
Id: area.Id,
DocumentId: area.DocumentId,
Order: area.Order,
FullText: area.FullText,
Lines: lines,
}
processedAreas[areaToDeleteIndex] = processedAreas[len(processedAreas)-1]
// processedAreas = processedAreas[:len(processedAreas)-1]
return true
}
func (c *Channel) RequestAddProcessedArea(processedArea ProcessedArea) ProcessedArea {
// doesAreaAlreadyExist := false
// processedAreasOfDocuments := document.GetProcessedAreaCollection().GetAreasByDocumentId(processedArea.DocumentId)
func (c *Channel) RequestUpdateProcessedArea(updatedProcessedArea entities.ProcessedArea) bool {
if updatedProcessedArea.Id == "" {
return false
}
// for _, a := range processedAreasOfDocuments {
// if a.Order == processedArea.Order {
// doesAreaAlreadyExist = true
// break
successfulDelete := c.RequestDeleteProcessedAreaById(updatedProcessedArea.Id)
if !successfulDelete {
return false
}
addedProcessedArea := c.RequestAddProcessedArea(updatedProcessedArea)
return addedProcessedArea.Id != ""
// if addedProcessedArea.Id != "" {
// return false
// }
// }
deserializedProcessedArea := deserializeProcessedArea(processedArea)
// if doesAreaAlreadyExist {
// storedProcessedArea := document.GetProcessedAreaCollection().GetAreaById(processedArea.Id)
// if storedProcessedArea.Id != "" {
// storedProcessedArea = &deserializedProcessedArea
// }
// } else {
document.GetProcessedAreaCollection().AddProcessedArea((deserializedProcessedArea))
// }
return processedArea
// return true
}
func (c *Channel) RequestUpdateProcessedWordById(wordId string, newTextValue string) bool {
@ -214,7 +126,7 @@ func (c *Channel) RequestUpdateProcessedWordById(wordId string, newTextValue str
}
wordProps := areas[areaOfWordIndex].Lines[lineOfWordIndex].Words[foundWordIndex]
areas[areaOfWordIndex].Lines[lineOfWordIndex].Words[foundWordIndex] = document.ProcessedWord{
areas[areaOfWordIndex].Lines[lineOfWordIndex].Words[foundWordIndex] = entities.ProcessedWord{
Id: wordProps.Id,
Direction: wordProps.Direction,
FullText: newTextValue,

View File

@ -3,42 +3,43 @@ package ipc
import (
app "textualize/core/App"
consts "textualize/core/Consts"
contextGroup "textualize/core/ContextGroup"
document "textualize/core/Document"
session "textualize/core/Session"
"textualize/entities"
storage "textualize/storage"
storageEntity "textualize/storage/Entities"
"github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/google/uuid"
)
func (c *Channel) GetCurrentSession() Session {
func (c *Channel) GetCurrentSession() entities.Session {
currentSession := session.GetInstance()
var sessionUsers []User
var sessionUsers []entities.User
for _, u := range currentSession.Organization.Users {
sessionUsers = append(sessionUsers, User(u))
sessionUsers = append(sessionUsers, entities.User(u))
}
currentProject := currentSession.Project
currentDefaultProcessLanguage := Language(currentProject.Settings.DefaultProcessLanguage)
currentDefaultTranslateTargetLanguage := Language(currentProject.Settings.DefaultTranslateTargetLanguage)
project := Project{
currentDefaultProcessLanguage := entities.Language(currentProject.Settings.DefaultProcessLanguage)
currentDefaultTranslateTargetLanguage := entities.Language(currentProject.Settings.DefaultTranslateTargetLanguage)
project := entities.Project{
Id: currentProject.Id,
Name: currentProject.Name,
OrganizationId: currentProject.OrganizationId,
Settings: ProjectSettings{
Settings: entities.ProjectSettings{
DefaultProcessLanguage: currentDefaultProcessLanguage,
DefaultTranslateTargetLanguage: currentDefaultTranslateTargetLanguage,
IsHosted: currentProject.Settings.IsHosted,
},
}
return Session{
Project: Project(project),
User: User(currentSession.User),
Organization: Organization{
return entities.Session{
Project: project,
User: currentSession.User,
Organization: entities.Organization{
Id: currentSession.Organization.Id,
Name: currentSession.Project.Name,
LogoPath: currentSession.Organization.LogoPath,
@ -47,23 +48,23 @@ func (c *Channel) GetCurrentSession() Session {
}
}
func (c *Channel) CreateNewProject(name string) Session {
func (c *Channel) CreateNewProject(name string) entities.Session {
currentSession := session.GetInstance()
newProject := session.Project{
newProject := entities.Project{
Id: uuid.NewString(),
OrganizationId: currentSession.Project.OrganizationId,
Name: name,
}
successfulProjectWrite := storage.GetDriver().WriteProjectData(storageEntity.Project{
successfulProjectWrite := storage.GetDriver().WriteProjectData(entities.Project{
Id: newProject.Id,
OrganizationId: newProject.OrganizationId,
Name: newProject.Name,
})
if !successfulProjectWrite {
return Session{}
return entities.Session{}
}
currentSession.Project = newProject
@ -71,14 +72,14 @@ func (c *Channel) CreateNewProject(name string) Session {
return c.GetCurrentSession()
}
func (c *Channel) GetCurrentUser() User {
return User(session.GetInstance().User)
func (c *Channel) GetCurrentUser() entities.User {
return session.GetInstance().User
}
func (c *Channel) RequestUpdateCurrentUser(updatedUserRequest User) User {
func (c *Channel) RequestUpdateCurrentUser(updatedUserRequest entities.User) entities.User {
sessionInstance := session.GetInstance()
sessionUser := session.User(sessionInstance.User)
sessionUser := entities.User(sessionInstance.User)
if sessionUser.LocalId == "" {
sessionUser.LocalId = uuid.NewString()
@ -95,14 +96,14 @@ func (c *Channel) RequestUpdateCurrentUser(updatedUserRequest User) User {
sessionUser.AvatarPath = updatedUserRequest.AvatarPath
successfulUserWrite := storage.GetDriver().WriteUserData(storageEntity.User(sessionUser))
successfulUserWrite := storage.GetDriver().WriteUserData(sessionUser)
if !successfulUserWrite {
return User{}
return entities.User{}
}
sessionInstance.UpdateCurrentUser(sessionUser)
return User(sessionInstance.User)
return sessionInstance.User
}
func (c *Channel) RequestChooseUserAvatar() string {
@ -124,43 +125,14 @@ func (c *Channel) RequestChooseUserAvatar() string {
}
}
func (c *Channel) GetAllLocalProjects() []Project {
func (c *Channel) GetAllLocalProjects() []entities.Project {
readLocalProjects := storage.GetDriver().ReadAllProjects()
response := make([]Project, 0)
for _, p := range readLocalProjects {
response = append(response, Project{
Id: p.Id,
OrganizationId: p.OrganizationId,
Name: p.Name,
Settings: ProjectSettings{
DefaultProcessLanguage: Language(p.Settings.DefaultProcessLanguage),
DefaultTranslateTargetLanguage: Language(p.Settings.DefaultTranslateTargetLanguage),
IsHosted: p.Settings.IsHosted,
},
})
return readLocalProjects
}
return response
}
func (c *Channel) GetProjectByName(projectName string) Project {
func (c *Channel) GetProjectByName(projectName string) entities.Project {
foundProject := storage.GetDriver().ReadProjectDataByName(projectName)
if foundProject.Id == "" {
return Project{}
}
return Project{
Id: foundProject.Id,
Name: foundProject.Name,
OrganizationId: foundProject.OrganizationId,
Settings: ProjectSettings{
DefaultProcessLanguage: Language(foundProject.Settings.DefaultProcessLanguage),
DefaultTranslateTargetLanguage: Language(foundProject.Settings.DefaultTranslateTargetLanguage),
IsHosted: foundProject.Settings.IsHosted,
},
}
return foundProject
}
func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool {
@ -171,139 +143,63 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool {
return false
}
session.GetInstance().Project = session.Project{
Id: foundProject.Id,
Name: foundProject.Name,
OrganizationId: foundProject.OrganizationId,
Settings: session.ProjectSettings{
DefaultProcessLanguage: consts.Language(foundProject.Settings.DefaultProcessLanguage),
DefaultTranslateTargetLanguage: consts.Language(foundProject.Settings.DefaultTranslateTargetLanguage),
IsHosted: foundProject.Settings.IsHosted,
},
}
session.GetInstance().Project = foundProject
// Documents
localDocumentCollection := storageDriver.ReadDocumentCollection(projectName)
newDocuments := make([]document.Entity, 0)
for _, d := range localDocumentCollection.Documents {
newAreas := make([]document.Area, 0)
for _, a := range d.Areas {
newAreas = append(newAreas, document.Area{
Id: a.Id,
Name: a.Name,
StartX: a.StartX,
StartY: a.StartY,
EndX: a.EndX,
EndY: a.EndY,
Language: consts.Language(a.Language),
Order: a.Order,
})
documentCount := len(localDocumentCollection.Documents)
readableDocuments := make([]document.Entity, documentCount)
for i := 0; i < documentCount; i++ {
readableDocuments[i] = document.Entity(localDocumentCollection.Documents[i])
}
newDocuments = append(newDocuments, document.Entity{
Id: d.Id,
GroupId: d.GroupId,
Name: d.Name,
Path: d.Path,
ProjectId: d.ProjectId,
Areas: newAreas,
DefaultLanguage: consts.Language(d.DefaultLanguage),
})
}
newDocumentColllection := document.DocumentCollection{
Documents: newDocuments,
document.SetDocumentCollection(document.DocumentCollection{
Documents: readableDocuments,
ProjectId: foundProject.Id,
}
document.SetDocumentCollection(newDocumentColllection)
})
// Groups
localGroupsCollection := storageDriver.ReadGroupCollection(projectName)
newGroups := make([]document.Group, 0)
for _, g := range localGroupsCollection.Groups {
newGroups = append(newGroups, document.Group(g))
groupCount := len(localGroupsCollection.Groups)
readableGroups := make([]entities.Group, groupCount)
for i := 0; i < groupCount; i++ {
readableGroups[i] = entities.Group(localGroupsCollection.Groups[i])
}
newGroupCollection := document.GroupCollection{
document.SetGroupCollection(document.GroupCollection{
Id: localGroupsCollection.Id,
ProjectId: localGroupsCollection.ProjectId,
Groups: newGroups,
}
document.SetGroupCollection(newGroupCollection)
Groups: readableGroups,
})
// Context Groups
localSerializedContextGroups := storageDriver.ReadContextGroupCollection(projectName)
contextGroup.SetContextGroupCollectionBySerialized(localSerializedContextGroups)
// Processed Texts
localProcessedAreaCollection := storageDriver.ReadProcessedTextCollection(projectName)
newAreas := make([]document.ProcessedArea, 0)
for _, a := range localProcessedAreaCollection.Areas {
linesOfArea := make([]document.ProcessedLine, 0)
for _, l := range a.Lines {
wordsOfLine := make([]document.ProcessedWord, 0)
for _, w := range l.Words {
symbolsOfWord := make([]document.ProcessedSymbol, 0)
for _, s := range w.Symbols {
symbolsOfWord = append(symbolsOfWord, document.ProcessedSymbol{
Text: s.Text,
Confidence: s.Confidence,
BoundingBox: document.ProcessedBoundingBox(s.BoundingBox),
})
areaCount := len(localProcessedAreaCollection.Areas)
readableAreas := make([]entities.ProcessedArea, areaCount)
for i := 0; i < areaCount; i++ {
readableAreas[i] = entities.ProcessedArea(localProcessedAreaCollection.Areas[i])
}
wordsOfLine = append(wordsOfLine, document.ProcessedWord{
FullText: w.FullText,
Confidence: w.Confidence,
Direction: w.Direction,
BoundingBox: document.ProcessedBoundingBox(w.BoundingBox),
Symbols: symbolsOfWord,
})
}
linesOfArea = append(linesOfArea, document.ProcessedLine{
FullText: l.FullText,
Words: wordsOfLine,
})
}
newAreas = append(newAreas, document.ProcessedArea{
Id: a.Id,
DocumentId: a.DocumentId,
FullText: a.FullText,
Order: a.Order,
Lines: linesOfArea,
})
}
document.SetProcessedAreaCollection(document.ProcessedAreaCollection{
Areas: newAreas,
Areas: readableAreas,
})
// UserProcessedMarkdown
localUserProcessedMarkdown := storageDriver.ReadProcessedUserMarkdownCollection(projectName)
newUserProcessedMarkdown := make([]document.UserMarkdown, 0)
for _, v := range localUserProcessedMarkdown.Values {
newUserProcessedMarkdown = append(newUserProcessedMarkdown, document.UserMarkdown{
Id: v.Id,
DocumentId: v.DocumentId,
Value: v.Value,
})
userProcessedMarkdownCount := len(localUserProcessedMarkdown.Values)
readableUserProcessedMarkdown := make([]entities.UserMarkdown, userProcessedMarkdownCount)
for i := 0; i < userProcessedMarkdownCount; i++ {
readableUserProcessedMarkdown[i] = entities.UserMarkdown(localUserProcessedMarkdown.Values[i])
}
document.SetUserMarkdownCollection(document.UserMarkdownCollection{
Values: newUserProcessedMarkdown,
Values: readableUserProcessedMarkdown,
})
// End UserProcessedMarkdown
return session.GetInstance().Project.Id == foundProject.Id
}
func (c *Channel) GetSuppportedLanguages() []Language {
supportedLanguages := consts.GetSuppportedLanguages()
var response []Language
for _, l := range supportedLanguages {
response = append(response, Language(l))
}
return response
func (c *Channel) GetSupportedLanguages() []entities.Language {
supportedLanguages := consts.GetSupportedLanguages()
return supportedLanguages
}

Some files were not shown because too many files have changed in this diff Show More