From 4c768162e0436115a05b9c8b0e4d837d626d45ba Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 30 Oct 2024 18:54:52 -0400 Subject: reworked how context menu buttons for ink and text work. added disableMixBlend for making transparent docs not use 'multiply'. --- src/fields/Doc.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/fields/Doc.ts') diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 6ec195910..aef7bf330 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -14,7 +14,7 @@ import { Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width } from './DocSymbols'; // prettier-ignore import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -import { InkTool } from './InkField'; +import { InkEraserTool, InkInkTool, InkTool } from './InkField'; import { List } from './List'; import { ObjectField, serverOpType } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; @@ -253,6 +253,10 @@ export class Doc extends RefField { public static set ActivePage(val) { Doc.UserDoc().activePage = val; } // prettier-ignore public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; } // prettier-ignore public static set ActiveTool(tool:InkTool){ Doc.UserDoc().activeTool = tool; } // prettier-ignore + public static get ActiveInk(): InkInkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkInkTool; } // prettier-ignore + public static set ActiveInk(tool:InkInkTool){ Doc.UserDoc().activeInkTool = tool; } // prettier-ignore + public static get ActiveEraser(): InkEraserTool { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.None) as InkEraserTool; } // prettier-ignore + public static set ActiveEraser(tool:InkEraserTool){ Doc.UserDoc().activeEraserTool = tool; } // prettier-ignore public static get ActivePresentation() { return DocCast(Doc.ActiveDashboard?.activePresentation) as Opt; } // prettier-ignore public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore -- cgit v1.2.3-70-g09d2 From b5944e87f9d4f3149161de4de0d76db486461c76 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Wed, 30 Oct 2024 19:38:18 -0400 Subject: integrated image stickers with palette --- src/client/documents/Documents.ts | 2 +- src/client/util/CurrentUserUtils.ts | 4 +- src/client/views/DocumentDecorations.tsx | 4 +- src/client/views/LightboxView.tsx | 8 +- src/client/views/Main.tsx | 4 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 8 +- src/client/views/nodes/ImageBox.tsx | 8 +- src/client/views/nodes/imageEditor/ImageEditor.tsx | 2 +- src/client/views/smartdraw/AnnotationPalette.scss | 56 ---- src/client/views/smartdraw/AnnotationPalette.tsx | 361 -------------------- src/client/views/smartdraw/SmartDrawHandler.tsx | 2 +- src/client/views/smartdraw/StickerPalette.scss | 56 ++++ src/client/views/smartdraw/StickerPalette.tsx | 362 +++++++++++++++++++++ src/fields/Doc.ts | 2 +- 14 files changed, 443 insertions(+), 436 deletions(-) delete mode 100644 src/client/views/smartdraw/AnnotationPalette.scss delete mode 100644 src/client/views/smartdraw/AnnotationPalette.tsx create mode 100644 src/client/views/smartdraw/StickerPalette.scss create mode 100644 src/client/views/smartdraw/StickerPalette.tsx (limited to 'src/fields/Doc.ts') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5f2a592ae..b79e8930b 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1044,7 +1044,7 @@ export namespace Docs { } export function AnnoPaletteDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyAnnos]), { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyStickers]), { ...(options || {}) }); } export function DockDocument(documents: Array, config: string, options: DocumentOptions, id?: string) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 274bc79be..bb3d232fb 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -160,9 +160,9 @@ export class CurrentUserUtils { return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } - static setupAnnoPalette(doc: Doc, field="myAnnos") { + static setupAnnoPalette(doc: Doc, field="myStickers") { const reqdOpts:DocumentOptions = { - title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", hidden: false, _chromeHidden: true, + title: "Stickers", _xMargin: 0, _layout_showTitle: "title", hidden: false, _chromeHidden: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 62f2de776..24ebca751 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -232,9 +232,9 @@ export class DocumentDecorations extends ObservableReactComponent { }[] = []; private _savedState: LightboxSavedState = {}; private _history: { doc: Doc; target?: Doc }[] = []; - private _annoPaletteView: AnnotationPalette | null = null; + private _annoPaletteView: StickerPalette | null = null; @observable private _future: Doc[] = []; @observable private _layoutTemplate: Opt = undefined; @observable private _layoutTemplateString: Opt = undefined; @@ -317,7 +317,7 @@ export class LightboxView extends ObservableReactComponent { - {this._showPalette && (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />} + {this._showPalette && (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />} {this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)} {this.renderNavBtn( this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]), @@ -331,7 +331,7 @@ export class LightboxView extends ObservableReactComponent { {toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)} {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)} - {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)} + {toggleBtn('lightboxView-paletteBtn', 'toggle sticker palette', this._showPalette === true, 'palette', '', this.togglePalette)} {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)} {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 2a59f6dc3..dda543470 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -64,7 +64,7 @@ import { ImportElementBox } from './nodes/importBox/ImportElementBox'; import { PresBox, PresElementBox } from './nodes/trails'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; -import { AnnotationPalette } from './smartdraw/AnnotationPalette'; +import { StickerPalette } from './smartdraw/StickerPalette'; dotenv.config(); @@ -117,7 +117,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; KeyValueBox.Init(); PresBox.Init(TabDocView.AllTabDocs); DocumentContentsView.Init(KeyValueBox.LayoutString(), { - AnnotationPalette, + StickerPalette: StickerPalette, FormattedTextBox, ImageBox, FontIconBox, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index d2bc8f2c2..8d73601d1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -42,7 +42,7 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere'; import { PinDocView, PinProps } from '../../PinFuncs'; -import { AnnotationPalette } from '../../smartdraw/AnnotationPalette'; +import { StickerPalette } from '../../smartdraw/StickerPalette'; import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { StyleProp } from '../../StyleProp'; import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; @@ -1992,9 +1992,9 @@ export class CollectionFreeFormView extends CollectionSubView await AnnotationPalette.addToPalette(this.Document), 'save to palette')), - icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down', + description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', + event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); this._props.renderDepth && optionItems.push({ diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0dfc0ec28..f98ff6d6e 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -32,6 +32,7 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { PinDocView, PinProps } from '../PinFuncs'; +import { StickerPalette } from '../smartdraw/StickerPalette'; import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; @@ -320,6 +321,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }), icon: 'pencil-alt', }); + funcs.push({ + description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', + event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', + }); ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -338,7 +344,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { @computed get nativeSize() { TraceMobx(); - if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0} + if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index e6d651ee9..9642dcc31 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -435,7 +435,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc const newImgDoc = await createNewImgDoc(finalImg, firstDoc); if (newImgDoc) { const docData = newImgDoc[DocData]; - docData.backgroundColor = 'transparent'; + // docData.backgroundColor = 'transparent'; if (firstDoc) setIsFirstDoc(false); setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]); } diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss deleted file mode 100644 index 4f11e8afc..000000000 --- a/src/client/views/smartdraw/AnnotationPalette.scss +++ /dev/null @@ -1,56 +0,0 @@ -.annotation-palette { - display: flex; - flex-direction: column; - align-items: center; - position: absolute; - right: 14px; - top: 50px; - border-radius: 5px; - margin: auto; -} - -.palette-create { - display: flex; - flex-direction: row; - width: 170px; - - .palette-create-input { - color: black; - width: 170px; - } -} - -.palette-create-options { - display: flex; - flex-direction: row; - justify-content: space-between; - width: 170px; - margin-top: 5px; - - .palette-color { - display: flex; - flex-direction: column; - align-items: center; - width: 40px; - } - - .palette-detail, - .palette-size { - display: flex; - flex-direction: column; - align-items: center; - width: 60px; - } -} - -.palette-buttons { - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.palette-save-reset { - display: flex; - flex-direction: row; -} diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx deleted file mode 100644 index dc0c01d36..000000000 --- a/src/client/views/smartdraw/AnnotationPalette.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Slider, Switch } from '@mui/material'; -import { Button } from 'browndash-components'; -import { action, makeObservable, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { AiOutlineSend } from 'react-icons/ai'; -import ReactLoading from 'react-loading'; -import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; -import { ImageCast, NumCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { Docs } from '../../documents/Documents'; -import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; -import { SettingsManager } from '../../util/SettingsManager'; -import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; -import { ObservableReactComponent } from '../ObservableReactComponent'; -import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; -import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; -import { FieldView } from '../nodes/FieldView'; -import './AnnotationPalette.scss'; -import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; - -interface AnnotationPaletteProps { - Document: Doc; -} - -/** - * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette - * is to offer an easy way for users to save then drag and drop repeated annotations onto a document. - * These annotations can be of any annotation type and operate similarly to user templates. - * - * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can - * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing - * to choose from. These drawings can then be saved to the palette as annotations. - */ -@observer -export class AnnotationPalette extends ObservableReactComponent { - @observable private _paletteMode: 'create' | 'view' = 'view'; - @observable private _userInput: string = ''; - @observable private _isLoading: boolean = false; - @observable private _canInteract: boolean = true; - @observable private _showRegenerate: boolean = false; - @observable private _docView: DocumentView | null = null; - @observable private _docCarouselView: DocumentView | null = null; - @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; - private _gptRes: string[] = []; - - constructor(props: AnnotationPaletteProps) { - super(props); - makeObservable(this); - } - - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(AnnotationPalette, fieldKey); - } - - Contains = (view: DocumentView) => { - return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); - }; - - return170 = () => 170; - - @action - handleKeyPress = async (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - await this.generateDrawings(); - } - }; - - @action - setPaletteMode = (mode: 'create' | 'view') => { - this._paletteMode = mode; - }; - - @action - setUserInput = (input: string) => { - if (!this._isLoading) this._userInput = input; - }; - - @action - setDetail = (detail: number) => { - if (this._canInteract) this._opts.complexity = detail; - }; - - @action - setColor = (autoColor: boolean) => { - if (this._canInteract) this._opts.autoColor = autoColor; - }; - - @action - setSize = (size: number) => { - if (this._canInteract) this._opts.size = size; - }; - - @action - resetPalette = (changePaletteMode: boolean) => { - if (changePaletteMode) this.setPaletteMode('view'); - this.setUserInput(''); - this.setDetail(5); - this.setColor(true); - this.setSize(200); - this._showRegenerate = false; - this._canInteract = true; - this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; - this._gptRes = []; - this._props.Document[DocData].data = undefined; - }; - - /** - * Adds a doc to the annotation palette. Gets a snapshot of the document to use as a preview in the palette. When this - * preview is dragged onto a parent document, a copy of that document is added as an annotation. - */ - public static addToPalette = async (doc: Doc) => { - if (!doc.savedAsAnno) { - const docView = DocumentView.getDocumentView(doc); - await docView?.ComponentView?.updateIcon?.(true); - const { clone } = await Doc.MakeClone(doc); - clone.title = doc.title; - const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; - Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image)); - doc.savedAsAnno = true; - } - }; - - public static getIcon(group: Doc) { - const docView = DocumentView.getDocumentView(group); - if (docView) { - docView.ComponentView?.updateIcon?.(true); - return new Promise(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); - } - return undefined; - } - - /** - * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from - * the annotation palette. - */ - @undoBatch - generateDrawings = action(async () => { - this._isLoading = true; - this._props.Document[DocData].data = undefined; - for (let i = 0; i < 3; i++) { - try { - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - this._canInteract = false; - if (this._showRegenerate) { - await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput); - } else { - await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor); - } - } catch (e) { - console.log('Error generating drawing', e); - } - } - this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); - this._userInput = ''; - this._isLoading = false; - this._showRegenerate = true; - }); - - @action - addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => { - this._gptRes.push(gptRes); - drawing[DocData].freeform_fitContentsToBox = true; - Doc.AddDocToList(this._props.Document, 'data', drawing); - }; - - /** - * Saves the currently showing, newly generated drawing to the annotation palette and sets the metadata. - * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user - * presses the "save drawing" button. - */ - saveDrawing = async () => { - const cIndex = NumCast(this._props.Document.carousel_index); - const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; - const docData = focusedDrawing[DocData]; - docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text; - docData.drawingInput = this._opts.text; - docData.drawingComplexity = this._opts.complexity; - docData.drawingColored = this._opts.autoColor; - docData.drawingSize = this._opts.size; - docData.drawingData = this._gptRes[cIndex]; - focusedDrawing.width = this._opts.size; - docData.x = this._opts.x; - docData.y = this._opts.y; - await AnnotationPalette.addToPalette(focusedDrawing); - this.resetPalette(true); - }; - - render() { - return ( -
e.stopPropagation()}> - {this._paletteMode === 'view' && ( - <> - (this._docView = r)} - Document={Doc.MyAnnos} - addDocument={undefined} - addDocTab={DocumentViewInternal.addDocTabFunc} - pinToPres={DocumentView.PinDoc} - containerViewPath={returnEmptyDocViewList} - styleProvider={DefaultStyleProvider} - removeDocument={returnFalse} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={this.return170} - PanelHeight={this.return170} - renderDepth={0} - isContentActive={returnTrue} - focus={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> -
-
-
- Color - this.setColor(!this._opts.autoColor)} - /> -
-
- Detail - { - this.setDetail(val as number); - }} - valueLabelDisplay="auto" - /> -
-
- Size - { - this.setSize(val as number); - }} - valueLabelDisplay="auto" - /> -
-
- (this._docCarouselView = r)} - Document={this._props.Document} - addDocument={undefined} - addDocTab={DocumentViewInternal.addDocTabFunc} - pinToPres={DocumentView.PinDoc} - containerViewPath={returnEmptyDocViewList} - styleProvider={DefaultStyleProvider} - removeDocument={returnFalse} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={this.return170} - PanelHeight={this.return170} - renderDepth={1} - isContentActive={returnTrue} - focus={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> -
-
- - - )} - - ); - } -} - -Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { - layout: { view: AnnotationPalette, dataField: 'data' }, - options: { acl: '' }, -}); diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index b4635673c..0078dfc76 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -83,7 +83,7 @@ export class SmartDrawHandler extends ObservableReactComponent { /** * AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e. CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added - or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.) + or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.) */ public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction; public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction; diff --git a/src/client/views/smartdraw/StickerPalette.scss b/src/client/views/smartdraw/StickerPalette.scss new file mode 100644 index 000000000..ca99410cf --- /dev/null +++ b/src/client/views/smartdraw/StickerPalette.scss @@ -0,0 +1,56 @@ +.sticker-palette { + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + right: 14px; + top: 50px; + border-radius: 5px; + margin: auto; +} + +.palette-create { + display: flex; + flex-direction: row; + width: 170px; + + .palette-create-input { + color: black; + width: 170px; + } +} + +.palette-create-options { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 170px; + margin-top: 5px; + + .palette-color { + display: flex; + flex-direction: column; + align-items: center; + width: 40px; + } + + .palette-detail, + .palette-size { + display: flex; + flex-direction: column; + align-items: center; + width: 60px; + } +} + +.palette-buttons { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.palette-save-reset { + display: flex; + flex-direction: row; +} diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx new file mode 100644 index 000000000..468d0fd13 --- /dev/null +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -0,0 +1,362 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Slider, Switch } from '@mui/material'; +import { Button } from 'browndash-components'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { AiOutlineSend } from 'react-icons/ai'; +import ReactLoading from 'react-loading'; +import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; +import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { ImageCast, NumCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; +import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; +import { SettingsManager } from '../../util/SettingsManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; +import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { FieldView } from '../nodes/FieldView'; +import './StickerPalette.scss'; +import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; + +interface StickerPaletteProps { + Document: Doc; +} + +/** + * The StickerPalette can be toggled in the lightbox view of a document. The goal of the palette + * is to offer an easy way for users to create stickers and drag and drop them onto a document. + * These stickers can technically be of any document type and operate similarly to user templates. + * However, the palette is designed to be geared toward ink stickers and image stickers. + * + * On the "add" side of the palette, there is a way to create a drawing sticker with GPT. Users can + * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing + * to choose from. These drawings can then be saved to the palette as stickers. + */ +@observer +export class StickerPalette extends ObservableReactComponent { + @observable private _paletteMode: 'create' | 'view' = 'view'; + @observable private _userInput: string = ''; + @observable private _isLoading: boolean = false; + @observable private _canInteract: boolean = true; + @observable private _showRegenerate: boolean = false; + @observable private _docView: DocumentView | null = null; + @observable private _docCarouselView: DocumentView | null = null; + @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + private _gptRes: string[] = []; + + constructor(props: StickerPaletteProps) { + super(props); + makeObservable(this); + } + + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(StickerPalette, fieldKey); + } + + Contains = (view: DocumentView) => { + return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); + }; + + return170 = () => 170; + + @action + handleKeyPress = async (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + await this.generateDrawings(); + } + }; + + @action + setPaletteMode = (mode: 'create' | 'view') => { + this._paletteMode = mode; + }; + + @action + setUserInput = (input: string) => { + if (!this._isLoading) this._userInput = input; + }; + + @action + setDetail = (detail: number) => { + if (this._canInteract) this._opts.complexity = detail; + }; + + @action + setColor = (autoColor: boolean) => { + if (this._canInteract) this._opts.autoColor = autoColor; + }; + + @action + setSize = (size: number) => { + if (this._canInteract) this._opts.size = size; + }; + + @action + resetPalette = (changePaletteMode: boolean) => { + if (changePaletteMode) this.setPaletteMode('view'); + this.setUserInput(''); + this.setDetail(5); + this.setColor(true); + this.setSize(200); + this._showRegenerate = false; + this._canInteract = true; + this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + this._gptRes = []; + this._props.Document[DocData].data = undefined; + }; + + /** + * Adds a doc to the sticker palette. Gets a snapshot of the document to use as a preview in the palette. When this + * preview is dragged onto a parent document, a copy of that document is added as a sticker. + */ + public static addToPalette = async (doc: Doc) => { + if (!doc.savedAsSticker) { + const docView = DocumentView.getDocumentView(doc); + await docView?.ComponentView?.updateIcon?.(true); + const { clone } = await Doc.MakeClone(doc); + clone.title = doc.title; + const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; + Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image)); + doc.savedAsSticker = true; + } + }; + + public static getIcon(group: Doc) { + const docView = DocumentView.getDocumentView(group); + if (docView) { + docView.ComponentView?.updateIcon?.(true); + return new Promise(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + + /** + * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from + * the sticker palette. + */ + @undoBatch + generateDrawings = action(async () => { + this._isLoading = true; + this._props.Document[DocData].data = undefined; + for (let i = 0; i < 3; i++) { + try { + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + this._canInteract = false; + if (this._showRegenerate) { + await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput); + } else { + await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor); + } + } catch (e) { + console.log('Error generating drawing', e); + } + } + this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); + this._userInput = ''; + this._isLoading = false; + this._showRegenerate = true; + }); + + @action + addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => { + this._gptRes.push(gptRes); + drawing[DocData].freeform_fitContentsToBox = true; + Doc.AddDocToList(this._props.Document, 'data', drawing); + }; + + /** + * Saves the currently showing, newly generated drawing to the sticker palette and sets the metadata. + * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user + * presses the "save drawing" button. + */ + saveDrawing = async () => { + const cIndex = NumCast(this._props.Document.carousel_index); + const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; + const docData = focusedDrawing[DocData]; + docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text; + docData.drawingInput = this._opts.text; + docData.drawingComplexity = this._opts.complexity; + docData.drawingColored = this._opts.autoColor; + docData.drawingSize = this._opts.size; + docData.drawingData = this._gptRes[cIndex]; + focusedDrawing.width = this._opts.size; + docData.x = this._opts.x; + docData.y = this._opts.y; + await StickerPalette.addToPalette(focusedDrawing); + this.resetPalette(true); + }; + + render() { + return ( +
e.stopPropagation()}> + {this._paletteMode === 'view' && ( + <> + (this._docView = r)} + Document={Doc.MyStickers} + addDocument={undefined} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={DocumentView.PinDoc} + containerViewPath={returnEmptyDocViewList} + styleProvider={DefaultStyleProvider} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return170} + PanelHeight={this.return170} + renderDepth={0} + isContentActive={returnTrue} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> +
+
+
+ Color + this.setColor(!this._opts.autoColor)} + /> +
+
+ Detail + { + this.setDetail(val as number); + }} + valueLabelDisplay="auto" + /> +
+
+ Size + { + this.setSize(val as number); + }} + valueLabelDisplay="auto" + /> +
+
+ (this._docCarouselView = r)} + Document={this._props.Document} + addDocument={undefined} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={DocumentView.PinDoc} + containerViewPath={returnEmptyDocViewList} + styleProvider={DefaultStyleProvider} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return170} + PanelHeight={this.return170} + renderDepth={1} + isContentActive={returnTrue} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> +
+
+ + + )} + + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { + layout: { view: StickerPalette, dataField: 'data' }, + options: { acl: '' }, +}); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 81241f9fe..c77c2a77d 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -236,7 +236,7 @@ export class Doc extends RefField { public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore - public static get MyAnnos() { return DocCast(Doc.UserDoc().myAnnos); } // prettier-ignore + public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore public static get MyLightboxDrawings() { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore -- cgit v1.2.3-70-g09d2