diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/DataVizBox/components/TableBox.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/DiagramBox.scss | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/DiagramBox.tsx | 19 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 21 | ||||
| -rw-r--r-- | src/client/views/nodes/FontIconBox/FontIconBox.tsx | 8 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 47 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 30 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 16 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/RichTextMenu.tsx | 13 | ||||
| -rw-r--r-- | src/client/views/nodes/generativeFill/GenerativeFill.tsx | 104 | ||||
| -rw-r--r-- | src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx | 34 | ||||
| -rw-r--r-- | src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts | 16 |
14 files changed, 227 insertions, 102 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 8ce318c2b..6cc773da0 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -18,7 +18,6 @@ import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss index 323638bff..8a7863c14 100644 --- a/src/client/views/nodes/DiagramBox.scss +++ b/src/client/views/nodes/DiagramBox.scss @@ -1,5 +1,3 @@ -$searchbarHeight: 50px; - .DIYNodeBox { width: 100%; height: 100%; @@ -23,7 +21,6 @@ $searchbarHeight: 50px; justify-content: center; align-items: center; width: 100%; - height: $searchbarHeight; padding: 10px; input[type='text'] { @@ -42,7 +39,6 @@ $searchbarHeight: 50px; justify-content: center; align-items: center; width: 100%; - height: calc(100% - $searchbarHeight); .diagramBox { flex: 1; display: flex; diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx index 36deb2d8d..d6c9bb013 100644 --- a/src/client/views/nodes/DiagramBox.tsx +++ b/src/client/views/nodes/DiagramBox.tsx @@ -18,7 +18,9 @@ import { InkingStroke } from '../InkingStroke'; import './DiagramBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; - +/** + * this is a class for the diagram box doc type that can be found in the tools section of the side bar + */ @observer export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -59,14 +61,21 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { fireImmediately: true } ); } + /** + * helper method for renderMermaidAsync + * @param str string containing the mermaid code + * @returns + */ renderMermaid = (str: string) => { try { return mermaid.render('graph' + Date.now(), str); - } catch (error) { + } catch { return { svg: '', bindFunctions: undefined }; } }; - + /** + * will update the div containing the mermaid diagram to render the new mermaidCode + */ renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => { try { const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); @@ -97,7 +106,9 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { res ); }, 'set mermaid code'); - + /** + * will generate mermaid code with GPT based on what the user requested + */ generateMermaidCode = action(() => { this._generating = true; const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c4104f216..4a249838b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -16,7 +16,7 @@ import { List } from '../../../fields/List'; import { PrefetchProxy } from '../../../fields/Proxy'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, ImageCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { AudioAnnoState } from '../../../server/SharedMediaTypes'; @@ -27,7 +27,7 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes' import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; -import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter'; +import { MakeTemplate, makeUserTemplateButtonOrImage } from '../../util/DropConverter'; import { UPDATE_SERVER_CACHE } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SearchUtil } from '../../util/SearchUtil'; @@ -1089,10 +1089,21 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { * Pins a Doc to the current presentation trail. (see TabDocView for implementation) */ public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void; + + /** + * Renders an image of a Doc into the Doc's icon field, then returns a promise for the image value + * @param doc Doc to snapshot + * @returns promise of icon ImageField + */ + public static GetDocImage(doc: Doc) { + return DocumentView.getDocumentView(doc) + ?.ComponentView?.updateIcon?.() + .then(() => ImageCast(DocCast(doc).icon)); + } /** * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event). Used by GestureOverlay to determine the doc a gesture should apply to. */ - public static DownDocView: DocumentView | undefined; + public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to. public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore private _htmlOverlayEffect: Opt<Doc>; @@ -1338,7 +1349,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { tempDoc = view.Document; MakeTemplate(tempDoc); Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); - Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc)); + Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc); } else { tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]); @@ -1588,7 +1599,7 @@ export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.acti export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore -export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth); } // prettier-ignore +export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index cb0c4d188..feaf84b7b 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; @@ -262,9 +261,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const tooltip: string = StrCast(this.Document.toolTip); const script = ScriptCast(this.Document.onClick)?.script; - const toggleStatus = script?.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result as boolean; + const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string; const items = DocListCast(this.dataDoc.data); const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); return ( @@ -272,11 +272,11 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { tooltip={`Toggle ${tooltip}`} type={Type.PRIM} color={color} + background={background === SnappingManager.userBackgroundColor ? undefined : background} multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} - isToggle={script ? true : false} + isToggle={false} toggleStatus={toggleStatus} - //background={SnappingManager.userBackgroundColor} label={this.label} items={items.map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />, diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index fd95ee888..226fad977 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -494,7 +494,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }}> <CollectionFreeFormView ref={this._ffref} - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index cb0b0d71f..7ef431885 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types'; +import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -175,28 +175,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { updateIcon = () => { // currently we render pdf icons as text labels const docViewContent = this.DocumentView?.().ContentDiv; - const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime(); - this._pdfViewer?._mainCont.current && - docViewContent && - UpdateIcon( - filename, - docViewContent, - NumCast(this.layoutDoc._width), - NumCast(this.layoutDoc._height), - this._props.PanelWidth(), - this._props.PanelHeight(), - NumCast(this.layoutDoc._layout_scrollTop), - NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1), - true, - this.layoutDoc[Id] + '-icon', - (iconFile: string, nativeWidth: number, nativeHeight: number) => { - setTimeout(() => { - this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc.icon_nativeWidth = nativeWidth; - this.dataDoc.icon_nativeHeight = nativeHeight; - }, 500); - } - ); + const filename = this.layoutDoc[Id] + '_icon_' + new Date().getTime(); + return !(this._pdfViewer?._mainCont.current && docViewContent) + ? new Promise<void>(res => res()) + : UpdateIcon( + filename, + docViewContent, + NumCast(this.layoutDoc._width), + NumCast(this.layoutDoc._height), + this._props.PanelWidth(), + this._props.PanelHeight(), + NumCast(this.layoutDoc._layout_scrollTop), + NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1), + true, + this.layoutDoc[Id] + '_icon_' + new Date().getTime(), + (iconFile: string, nativeWidth: number, nativeHeight: number) => { + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; + } + ); }; componentWillUnmount() { @@ -540,7 +538,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} @@ -556,7 +553,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ) : ( <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this._props.select(false), true)}> <ComponentTag - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} // override setContentView to do nothing NativeWidth={this.sidebarNativeWidthFunc} @@ -610,7 +606,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { top: 0, }}> <PDFViewer - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} pdfBox={this} sidebarAddDoc={this.sidebarAddDocument} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 4933869a7..d653b27d7 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -298,18 +298,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, ''); const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_')); const filename = basename(encodedFilename); - ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY)); + return ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => { + if (returnedFilename) (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY); + }); } + return new Promise<void>(res => res()); }; - updateIcon = () => { - const makeIcon = (returnedfilename: string) => { + updateIcon = () => + this.Snapshot(undefined, undefined, (returnedfilename: string) => { this.dataDoc.icon = new ImageField(returnedfilename); this.dataDoc.icon_nativeWidth = NumCast(this.layoutDoc._width); this.dataDoc.icon_nativeHeight = NumCast(this.layoutDoc._height); - }; - this.Snapshot(undefined, undefined, makeIcon); - }; + }); // creates link for snapshot createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => { @@ -459,7 +460,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const url = field.url.href; const subitems: ContextMenuProps[] = []; subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' }); - subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' }); + subitems.push({ description: 'Take Snapshot', event: () => this.Snapshot(), icon: 'expand-arrows-alt' }); this.Document.type === DocumentType.SCREENSHOT && subitems.push({ description: 'Screen Capture', diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 1fd73c226..a5788d02a 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -44,7 +44,6 @@ import { LinkInfo } from './LinkDocPreview'; import { OpenWhere } from './OpenWhere'; import './WebBox.scss'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const { CreateImage } = require('./WebBoxRenderer'); @observer @@ -145,38 +144,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; updateIcon = async () => { - if (!this._iframe) return; + if (!this._iframe) return new Promise<void>(res => res()); const scrollTop = NumCast(this.layoutDoc._layout_scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth(); let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); if (!htmlString) { - htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text(); + htmlString = await fetch(ClientUtils.CorsProxy(this.webField!.href)).then(response => response.text()); } this.layoutDoc.thumb = undefined; this.Document.thumbLockout = true; // lock to prevent multiple thumb updates. - CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) + return (CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) as Promise<string>) .then((dataUrl: string) => { if (dataUrl.includes('<!DOCTYPE')) { console.log('BAD DATA IN THUMB CREATION'); return; } - ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => - setTimeout( - action(() => { - this.Document.thumbLockout = false; - this.layoutDoc.thumb = new ImageField(returnedfilename); - this.layoutDoc.thumbScrollTop = scrollTop; - this.layoutDoc.thumbNativeWidth = nativeWidth; - this.layoutDoc.thumbNativeHeight = nativeHeight; - }), - 500 - ) - ); + return ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '_icon_' + new Date().getTime(), true, this.layoutDoc[Id] + '_icon_').then(returnedfilename => { + this.Document.thumbLockout = false; + this.layoutDoc.thumb = new ImageField(returnedfilename); + this.layoutDoc.thumbScrollTop = scrollTop; + this.layoutDoc.thumbNativeWidth = nativeWidth; + this.layoutDoc.thumbNativeHeight = nativeHeight; + }); }) - .catch((error: object) => { - console.error('oops, something went wrong!', error); - }); + .catch((error: object) => console.error('oops, something went wrong!', error)); }; componentDidMount() { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index dcdac6775..730c57794 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -265,6 +265,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); + AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => { + const container = DocCast(this.Document.embedContainer); + const docView = DocumentView.getDocumentView?.(container); + docView?.ComponentView?._props.addDocument?.(drawing); + drawing.x = NumCast(this.Document.x) + NumCast(this.Document.width); + drawing.y = NumCast(this.Document.y); + }; + AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? ''); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); @@ -648,7 +656,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB textContent += node.child(i).textContent; i++; } - // eslint-disable-next-line no-cond-assign while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) { const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1)); ret.push(sel); @@ -707,7 +714,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } - // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @@ -1124,7 +1130,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { - // eslint-disable-next-line no-use-before-define const examinedNode = findAnchorNode(node, editor); if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); @@ -1278,7 +1283,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB action(selected => { this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { - // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { @@ -1705,7 +1709,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/); if (match) { this.dataDoc.title_custom = true; - // eslint-disable-next-line prefer-destructuring this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1)); } @@ -1787,7 +1790,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (children && !SnappingManager.IsDragging) { - // eslint-disable-next-line no-use-before-define const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { @@ -1874,7 +1876,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} @@ -1894,7 +1895,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ) : ( <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}> <ComponentTag - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} ref={this._sidebarTagRef} setContentView={emptyFunction} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 738f6d699..88e2e4248 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, toggleMark, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType } from 'prosemirror-model'; @@ -68,10 +68,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); - RichTextMenu._instance.menu = this; - this.updateMenu(undefined, undefined, props, this.layoutDoc); - this._canFade = false; - this.Pinned = true; + runInAction(() => { + RichTextMenu._instance.menu = this; + this.updateMenu(undefined, undefined, props, this.layoutDoc); + this._canFade = false; + this.Pinned = true; + }); } @computed get noAutoLink() { @@ -695,7 +697,6 @@ interface RichTextMenuPluginProps { editorProps: FormattedTextBoxProps; } export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> { - // eslint-disable-next-line react/no-unused-class-component-methods update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc); } diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 6d8ba9222..261eb4bb4 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -21,19 +21,16 @@ import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { ImageEditorData } from '../ImageBox'; import { OpenWhereMod } from '../OpenWhere'; import './GenerativeFill.scss'; -import Buttons from './GenerativeFillButtons'; -import { BrushHandler } from './generativeFillUtils/BrushHandler'; +import { EditButtons, CutButtons } from './GenerativeFillButtons'; +import { BrushHandler, BrushType } from './generativeFillUtils/BrushHandler'; import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; import { PointerHandler } from './generativeFillUtils/PointerHandler'; import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; import { DocumentView } from '../DocumentView'; - -// enum BrushStyle { -// ADD, -// SUBTRACT, -// MARQUEE, -// } +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ImageField } from '../../../../fields/URLField'; +import { resolve } from 'url'; interface GenerativeFillProps { imageEditorOpen: boolean; @@ -82,6 +79,9 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD const parentDoc = useRef<Doc | null>(null); const childrenDocs = useRef<Doc[]>([]); + // constants for image cutting + const cutPts = useRef<Point[]>([]); + // Undo and Redo const handleUndo = () => { const ctx = ImageUtility.getCanvasContext(canvasRef); @@ -161,7 +161,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD x: currPoint.x - e.movementX / canvasScale, y: currPoint.y - e.movementY / canvasScale, }; - BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor /* , brushStyle === BrushStyle.SUBTRACT */); + const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, BrushType.CUT); + cutPts.current.push(...pts); }; drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove); @@ -278,7 +279,6 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD const maskBlob = await ImageUtility.canvasToBlob(canvasMask); const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg); const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2); - // const res = await ImageUtility.mockGetEdit(img.src); // create first image if (!newCollectionRef.current) { @@ -334,6 +334,68 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD setLoading(false); }; + const cutImage = async () => { + const img = currImg.current; + const canvas = canvasRef.current; + if (!canvas || !img) return; + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx) return; + ctx.globalCompositeOperation = 'source-over'; + setLoading(true); + setEdited(true); + // get the original image + const canvasOriginalImg = ImageUtility.getCanvasImg(img); + if (!canvasOriginalImg) return; + // draw the image onto the canvas + ctx.drawImage(img, 0, 0); + // get the mask which i assume is the thing the user draws on + // const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg); + // if (!canvasMask) return; + // canvasMask.width = canvas.width; + // canvasMask.height = canvas.height; + // now put the user's path around the mask + if (cutPts.current.length) { + ctx.beginPath(); + ctx.moveTo(cutPts.current[0].x, cutPts.current[0].y); // later check edge case where cutPts is empty + for (let i = 0; i < cutPts.current.length; i++) { + ctx.lineTo(cutPts.current[i].x, cutPts.current[i].y); + } + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + // ctx.clip(); + } + const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl + if (!newCollectionRef.current) { + if (!isNewCollection && imageRootDoc) { + // if the parent hasn't been set yet + if (!parentDoc.current) parentDoc.current = imageRootDoc; + } else { + if (!(originalImg.current && imageRootDoc)) return; + // create new collection and add it to the view + newCollectionRef.current = Docs.Create.FreeformDocument([], { + x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX, + y: NumCast(imageRootDoc.y), + _width: newCollectionSize, + _height: newCollectionSize, + title: 'Image edit collection', + }); + DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' }); + // opening new tab + CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right); + } + } + const image = new Image(); + image.src = url; + await createNewImgDoc(image, true); + // add the doc to the main freeform + // eslint-disable-next-line no-use-before-define + setLoading(false); + cutPts.current.length = 0; + }; + // adjusts all the img positions to be aligned const adjustImgPositions = () => { if (!parentDoc.current) return; @@ -439,6 +501,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}> <div className="generativeFillControls"> <h1>Image Editor</h1> + {/* <IconButton text="Cut out" icon={<FontAwesomeIcon icon="scissors" />} /> */} <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}> <FormControlLabel control={ @@ -455,7 +518,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD labelPlacement="end" sx={{ whiteSpace: 'nowrap' }} /> - <Buttons getEdit={getEdit} loading={loading} onReset={handleReset} /> + <EditButtons onClick={getEdit} loading={loading} onReset={handleReset} /> + <CutButtons onClick={cutImage} loading={loading} onReset={handleReset} /> <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} /> </div> </div> @@ -526,6 +590,24 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD }} /> </div> + <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}> + <Slider + sx={{ + '& input[type="range"]': { + WebkitAppearance: 'slider-vertical', + }, + }} + orientation="vertical" + min={1} + max={500} + defaultValue={150} + size="small" + valueLabelDisplay="auto" + onChange={(e: any, val: any) => { + setCursorData(prev => ({ ...prev, width: val as number })); + }} + /> + </div> </div> {/* Edits thumbnails */} <div className="editsBox"> diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx index d1f68ee0e..fe22b273d 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx @@ -6,12 +6,12 @@ import { AiOutlineInfo } from 'react-icons/ai'; import { activeColor } from './generativeFillUtils/generativeFillConstants'; interface ButtonContainerProps { - getEdit: () => Promise<void>; + onClick: () => Promise<void>; loading: boolean; onReset: () => void; } -function Buttons({ loading, getEdit, onReset }: ButtonContainerProps) { +export function EditButtons({ loading, onClick: getEdit, onReset }: ButtonContainerProps) { return ( <div className="generativeFillBtnContainer"> <Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} /> @@ -41,4 +41,32 @@ function Buttons({ loading, getEdit, onReset }: ButtonContainerProps) { ); } -export default Buttons; +export function CutButtons({ loading, onClick: cutImage, onReset }: ButtonContainerProps) { + return ( + <div className="generativeFillBtnContainer"> + <Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} /> + {loading ? ( + <Button + text="CUT IMAGE" + type={Type.TERT} + color={activeColor} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} + iconPlacement="right" + onClick={() => { + if (!loading) cutImage(); + }} + /> + ) : ( + <Button + text="CUT IMAGE" + type={Type.TERT} + color={activeColor} + onClick={() => { + if (!loading) cutImage(); + }} + /> + )} + <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size="16px" />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} /> + </div> + ); +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts index 16d529d93..8a66d7347 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts @@ -1,6 +1,12 @@ import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers'; import { eraserColor } from './generativeFillConstants'; import { Point } from './generativeFillInterfaces'; +import { points } from '@turf/turf'; + +export enum BrushType { + GEN_FILL, + CUT, +} export class BrushHandler { static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => { @@ -14,12 +20,16 @@ export class BrushHandler { ctx.closePath(); }; - static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => { + static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, brushType: BrushType) => { const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint); - + const pts: Point[] = []; for (let i = 0; i < dist; i += 5) { const s = i / dist; - BrushHandler.brushCircleOverlay(startPoint.x * (1 - s) + endPoint.x * s, startPoint.y * (1 - s) + endPoint.y * s, brushRadius, ctx, fillColor /* , erase */); + const x = startPoint.x * (1 - s) + endPoint.x * s; + const y = startPoint.y * (1 - s) + endPoint.y * s; + pts.push({ x: startPoint.x, y: startPoint.y }); + BrushHandler.brushCircleOverlay(x, y, brushRadius, ctx, fillColor); } + return pts; }; } |
