diff options
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 511 |
1 files changed, 418 insertions, 93 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 226fad977..279317f49 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,27 +1,32 @@ +import { Button, Colors, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; -import { Colors } from 'browndash-components'; +import { Slider, Tooltip } from '@mui/material'; +import axios from 'axios'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; -import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +import { AiOutlineSend } from 'react-icons/ai'; +import ReactLoading from 'react-loading'; +import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; +import { Upload } from '../../../server/SharedMediaTypes'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DocUtils } from '../../documents/DocUtils'; +import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { SnappingManager } from '../../util/SnappingManager'; -import { undoBatch } from '../../util/UndoManager'; +import { undoable, undoBatch } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; @@ -30,21 +35,26 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { PinDocView, PinProps } from '../PinFuncs'; +import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler'; +import { FireflyImageData, isFireflyImageData } from '../smartdraw/FireflyConstants'; +import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; +import { StickerPalette } from '../smartdraw/StickerPalette'; import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { RichTextField } from '../../../fields/RichTextField'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined }); - @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => { + private static set = action((open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => { this._instance.imageData = { open, rootDoc, source, addDoc }; - }; + }); constructor() { makeObservable(this); @@ -60,25 +70,38 @@ export class ImageEditorData { public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore } + +const API_URL = 'https://api.unsplash.com/search/photos'; @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + _ffref = React.createRef<CollectionFreeFormView>(); private _ignoreScroll = false; private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; private _overlayIconRef = React.createRef<HTMLDivElement>(); - private _marqueeref = React.createRef<MarqueeAnnotator>(); - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _regenerateIconRef = React.createRef<HTMLDivElement>(); + private _mainCont: HTMLDivElement | null = null; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>(); - @observable _curSuffix = ''; - @observable _error = ''; - @observable _isHovering = false; // flag to switch between primary and alternate images on hover - _ffref = React.createRef<CollectionFreeFormView>(); + imageRef: HTMLImageElement | null = null; // <video> ref + marqueeref = React.createRef<MarqueeAnnotator>(); + @observable Loading = false; // bcz: this should be migrated into StylProviderQuiz since it's not fundamental to the imageBox + + @observable private _searchInput = ''; + @observable private _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>(); + @observable private _curSuffix = ''; + @observable private _error = ''; + @observable private _isHovering = false; // flag to switch between primary and alternate images on hover + + // variables for AI Image Editor + @observable private _regenInput = ''; + @observable private _canInteract = true; + @observable private _regenerateLoading = false; + @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : []; constructor(props: FieldViewProps) { super(props); @@ -87,10 +110,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } protected createDropTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor const anchor = @@ -115,30 +138,30 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._disposers.sizer = reaction( () => ({ forceFull: this._props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: (this.ScreenToLocalBoxXf().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.layoutDoc._freeform_scale, 1), + scrSize: (NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.DocumentView?.().screenToLocalScale() ?? 1)) * this._props.PanelWidth(), selected: this._props.isSelected(), }), ({ forceFull, scrSize, selected }) => { - this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'; + this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 100 ? '_s' : scrSize < 400 ? '_m' : scrSize < 800 ? '_l' : '_o'; }, { fireImmediately: true, delay: 1000 } ); const { layoutDoc } = this; - // this._disposers.path = reaction( - // () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), - // ({ nativeSize, width }) => { - // if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { - // this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; - // } - // }, - // { fireImmediately: true } - // ); + this._disposers.path = reaction( + () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), + ({ nativeSize, width }) => { + if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) { + this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; + } + }, + { fireImmediately: true } + ); this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, sTop => { this._forcedScroll = true; - !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(sTop)); - this._mainCont.current?.scrollTo({ top: NumCast(sTop) }); + !this._ignoreScroll && this._mainCont && (this._mainCont.scrollTop = NumCast(sTop)); + this._mainCont?.scrollTo({ top: NumCast(sTop) }); this._forcedScroll = false; }, { fireImmediately: true } @@ -149,38 +172,74 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Object.values(this._disposers).forEach(disposer => disposer?.()); } - @undoBatch - drop = (e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData) { - let added: boolean | undefined; - const targetIsBullseye = (ele: HTMLElement): boolean => { - if (!ele) return false; - if (ele === this._overlayIconRef.current) return true; - return targetIsBullseye(ele.parentElement as HTMLElement); - }; - if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { - added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { - this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; - return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); - }, true); - } else if (de.altKey || !this.dataDoc[this.fieldKey]) { - const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutFieldKey(layoutDoc); - const targetDoc = layoutDoc[DocData]; - if (targetDoc[targetField] instanceof ImageField) { - added = true; - this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); - Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); - } - } - added === false && e.preventDefault(); - added !== undefined && e.stopPropagation(); - return added; + /** + * Find images from the unsplash api to add to flashcards. + */ + fetchImages = async () => { + try { + const { data } = await axios.get(`${API_URL}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`); + const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, { + _nativeWidth: Doc.NativeWidth(this.layoutDoc), + _nativeHeight: Doc.NativeHeight(this.layoutDoc), + x: NumCast(this.layoutDoc.x), + y: NumCast(this.layoutDoc.y), + onClick: FollowLinkScript(), + _width: 150, + _height: 150, + title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', + }); + this._props.addDocument?.(imageSnapshot); + } catch (error) { + console.log(error); } - return false; }; + handleSelection = async (selection: string) => { + this._searchInput = selection; + }; + + drop = undoable( + action((e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + let added: boolean | undefined; + const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => { + if (!ele) return false; + if (ele === dropTarget) return true; + return hitDropTarget(ele.parentElement as HTMLElement, dropTarget); + }; + if (de.metaKey || hitDropTarget(e.target as HTMLElement, this._overlayIconRef.current)) { + added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { + this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); + }, true); + } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { + this._regenerateLoading = true; + const drag = de.complete.docDragData.draggedDocuments.lastElement(); + const dragField = drag[Doc.LayoutFieldKey(drag)]; + const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); + const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); + DrawingFillHandler.drawingToImage(this.Document, 100, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false))); + added = false; + } else if (de.altKey || !this.dataDoc[this.fieldKey]) { + const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; + const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetDoc = layoutDoc[DocData]; + if (targetDoc[targetField] instanceof ImageField) { + added = true; + this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); + } + } + added === false && e.preventDefault(); + added !== undefined && e.stopPropagation(); + return added; + } + return false; + }), + 'image drop' + ); + @undoBatch resolution = () => { this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes; @@ -234,7 +293,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw; + const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh; cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); @@ -252,9 +311,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { croppingProto.data_nativeWidth = anchw; croppingProto.data_nativeHeight = anchh; croppingProto.freeform_scale = viewScale; - croppingProto.freeform_scale_min = viewScale; croppingProto.freeform_panX = anchx / viewScale; croppingProto.freeform_panY = anchy / viewScale; + croppingProto.freeform_scale_min = viewScale; croppingProto.freeform_panX_min = anchx / viewScale; croppingProto.freeform_panX_max = anchw / viewScale; croppingProto.freeform_panY_min = anchy / viewScale; @@ -270,6 +329,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return cropping; }; + docEditorView = action(() => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + if (field) { + ImageEditorData.Open = true; + ImageEditorData.Source = this.choosePath(field.url); + ImageEditorData.AddDoc = this._props.addDocument; + ImageEditorData.RootDoc = this.Document; + } + }); + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { @@ -277,21 +346,84 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); + funcs.push({ + description: 'GetImageText', + event: () => { + Networking.PostToServer('/queryFireflyImageText', { + file: (file => { + const ext = extname(file); + return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + }).then(text => alert(text)); + }, + icon: 'expand-arrows-alt', + }); + funcs.push({ + description: 'Expand Image', + event: () => { + Networking.PostToServer('/expandImage', { + prompt: 'sunny skies', + file: (file => { + const ext = extname(file); + return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + }).then(res => { + const info = res as Upload.ImageInformation; + const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); + DocUtils.assignImageInfo(info, img); + this._props.addDocTab(img, OpenWhere.addRight); + }); + }, + icon: 'expand-arrows-alt', + }); funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); + funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' }); + this.layoutDoc.ai && + funcs.push({ + description: 'Regenerate AI Image', + event: action(() => { + if (!SmartDrawHandler.Instance.ShowRegenerate && this.DocumentView) { + const [x, y] = this.DocumentView().screenToViewTransform().inverse().transformPoint(NumCast(this.Document.width), 0); + this._props.docViewPath().slice(-2)[0]?.ComponentView?.showSmartDraw?.(x, y, true); + } else { + SmartDrawHandler.Instance.hideRegenerate(); + } + }), + icon: 'pen-to-square', + }); funcs.push({ - description: 'Open Image Editor', - event: action(() => { - ImageEditorData.Open = true; - ImageEditorData.Source = this.choosePath(field.url); - ImageEditorData.AddDoc = this._props.addDocument; - ImageEditorData.RootDoc = this.Document; - }), - icon: 'pencil-alt', + 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' }); } }; + // updateIcon = () => new Promise<void>(res => res()); + updateIcon = (usePanelDimensions?: boolean) => { + const contentDiv = this._mainCont; + return !contentDiv + ? new Promise<void>(res => res()) + : UpdateIcon( + this.layoutDoc[Id] + '_icon_' + new Date().getTime(), + contentDiv, + usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + this._props.PanelWidth(), + this._props.PanelHeight(), + 0, + 1, + false, + '', + (iconFile, nativeWidth, nativeHeight) => { + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; + } + ); + }; + choosePath = (url: URL) => { if (!url?.href) return ''; const lower = url.href.toLowerCase(); @@ -306,12 +438,26 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @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); return { nativeWidth, nativeHeight, nativeOrientation }; } + private _sideBtnWidth = 35; + /** + * How much the content of the view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore + @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; return ( @@ -340,9 +486,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }) } style={{ - display: (this._props.isContentActive() !== false && SnappingManager.CanEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none', - width: 'min(10%, 25px)', - height: 'min(10%, 25px)', + display: this._props.isContentActive() && (SnappingManager.CanEmbed || this.dataDoc[this.fieldKey + '_alternates']) ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', color: usePath === undefined ? 'black' : 'white', }}> @@ -351,6 +498,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </Tooltip> ); } + @computed get regenerateImageIcon() { + return ( + <div + className="imageBox-regenerateDropTarget" + ref={this._regenerateIconRef} + onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} + style={{ + display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, + background: 'transparent', + // color: SettingsManager.userBackgroundColor, + }}> + {this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" color={SettingsManager.userColor} size="lg" />} + </div> + ); + } @computed get paths() { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc @@ -399,11 +564,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._isHovering = false; })} key={this.layoutDoc[Id]} - ref={this.createDropTarget} onPointerDown={this.marqueeDown}> <div className="imageBox-fader" style={{ opacity: backAlpha }}> <img alt="" + ref={action((r: HTMLImageElement | null) => (this.imageRef = r))} key="paths" src={srcpath} style={{ transform, transformOrigin, objectFit: 'fill', height: '100%' }} @@ -419,11 +584,163 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </div> )} </div> - {this.overlayImageIcon} </div> ); } + protected _btnWidth = 50; + protected _inputWidth = 50; + protected _sideBtnMaxPanelPct = 0.12; + @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined; + @observable private _fireflyRefStrength = 0; + + componentAIViewHistory = () => ( + <div className="imageBox-aiView-history"> + <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} /> + {this._prevImgs.map(img => ( + <div key={img.pathname}> + <img + className="imageBox-aiView-img" + src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))} + onClick={() => { + this.dataDoc[this.fieldKey] = new ImageField(img.pathname); + this.dataDoc.ai_firefly_prompt = img.prompt; + this.dataDoc.ai_firefly_seed = img.seed; + }} + /> + <span>{img.prompt}</span> + </div> + ))} + </div> + ); + + componentAIView = () => { + const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); + return ( + <div className="imageBox-aiView"> + <div className="imageBox-aiView-regenerate"> + <span className="imageBox-aiView-firefly">Firefly:</span> + <input + className="imageBox-aiView-input" + aria-label="Edit instructions input" + type="text" + value={this._regenInput} + onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} + placeholder={this._regenInput || StrCast(this.Document.title)} + /> + <div className="imageBox-aiView-strength"> + <span className="imageBox-aiView-similarity">Similarity</span> + <Slider + className="imageBox-aiView-slider" + sx={{ + '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, + '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor }, + '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, + }} + min={0} + max={100} + step={1} + size="small" + value={this._fireflyRefStrength} + onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))} + valueLabelDisplay="auto" + /> + </div> + <div className="imageBox-aiView-regenerate-createBtn"> + <Button + text="Create" + type={Type.SEC} + // style={{ alignSelf: 'flex-end' }} + icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + onClick={action(async () => { + this._regenerateLoading = true; + if (this._fireflyRefStrength) { + DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(action(() => (this._regenerateLoading = false))); + } else { + SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( + action(newImgs => { + const firstImg = newImgs[0]; + if (isFireflyImageData(firstImg)) { + const url = firstImg.pathname; + const imgField = new ImageField(url); + this._prevImgs.length === 0 && + this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); + this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url }); + this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); + this.dataDoc.ai_firefly_prompt = firstImg.prompt; + this.dataDoc[this.fieldKey] = imgField; + this._regenerateLoading = false; + this._regenInput = ''; + } + }) + ); + } + })} + /> + </div> + </div> + <div className="imageBox-aiView-options"> + <span className="imageBox-aiView-subtitle"> More: </span> + <Button + type={Type.TERT} + text="Get Text" + icon={<FontAwesomeIcon icon="font" />} + color={SettingsManager.userBackgroundColor} + iconPlacement="right" + onClick={() => { + Networking.PostToServer('/queryFireflyImageText', { + file: (file => { + const ext = extname(file); + return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + }).then(text => alert(text)); + }} + /> + <Button + type={Type.TERT} + text="Generative Fill" + icon={<FontAwesomeIcon icon="fill" />} + color={SettingsManager.userBackgroundColor} + iconPlacement="right" + onClick={action(() => { + ImageEditorData.Open = true; + ImageEditorData.Source = (field && this.choosePath(field.url)) || ''; + ImageEditorData.AddDoc = this._props.addDocument; + ImageEditorData.RootDoc = this.Document; + })} + /> + <Button + type={Type.TERT} + text="Expand" + icon={<FontAwesomeIcon icon="expand" />} + color={SettingsManager.userBackgroundColor} + iconPlacement="right" + onClick={() => { + Networking.PostToServer('/expandImage', { + prompt: 'sunny skies', + file: (file => { + const ext = extname(file); + return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + }).then(res => { + const info = res as Upload.ImageInformation; + const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); + DocUtils.assignImageInfo(info, img); + const genratedDocs = this.Document.generatedDocs + ? DocCast(this.Document.generatedDocs) + : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) }); + Doc.AddDocToList(genratedDocs, undefined, img); + this.Document[DocData].generatedDocs = genratedDocs; + if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight); + }); + }} + /> + </div> + </div> + ); + }; + @computed get annotationLayer() { TraceMobx(); return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />; @@ -432,19 +749,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { marqueeDown = (e: React.PointerEvent) => { if (!this.dataDoc[this.fieldKey]) { this.chooseImage(); - } else if ( - !e.altKey && - e.button === 0 && - NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && - this._props.isContentActive() && - ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) - ) { + } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { setupMoveUpEvents( this, e, action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -456,17 +767,22 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; - this._marqueeref.current?.onTerminateSelection(); + this._props.styleProvider?.(this.Document, this._props, StyleProp.AnchorMenuItems); + AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; + AnchorMenu.Instance.marqueeWidth = this.marqueeref.current?.Width ?? 0; + AnchorMenu.Instance.marqueeHeight = this.marqueeref.current?.Height ?? 0; + this.marqueeref.current?.onTerminateSelection(); this._props.select(false); }; focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); renderedPixelDimensions = async () => { - const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); + const res = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); + const { nativeWidth: width, nativeHeight: height } = res as { nativeWidth: number; nativeHeight: number }; return { width, height }; }; - savedAnnotations = () => this._savedAnnotations; + render() { TraceMobx(); const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; @@ -475,12 +791,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="imageBox" onContextMenu={this.specificContextMenu} - ref={this._mainCont} + ref={this.createDropTarget} onScroll={action(() => { if (!this._forcedScroll) { - if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) { + if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) { this._ignoreScroll = true; - this.layoutDoc._layout_scrollTop = this._mainCont.current?.scrollTop; + this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop; this._ignoreScroll = false; } } @@ -490,7 +806,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { height: this._props.PanelHeight() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : undefined, + overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden', }}> <CollectionFreeFormView ref={this._ffref} @@ -517,24 +833,33 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { addDocument={this.addDocument}> {this.content} </CollectionFreeFormView> + {this.Loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={50} width={50} color={'blue'} /> + </div> + ) : null} + {this.regenerateImageIcon} + {this.overlayImageIcon} {this.annotationLayer} - {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( + {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : ( <MarqueeAnnotator Document={this.Document} - ref={this._marqueeref} + ref={this.marqueeref} scrollTop={0} annotationLayerScrollTop={0} scaling={returnOne} annotationLayerScaling={this._props.NativeDimScaling} + screenTransform={this.DocumentView().screenToViewTransform} docView={this.DocumentView} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} - marqueeContainer={this._mainCont.current} + marqueeContainer={this._mainCont} highlightDragSrcColor="" anchorMenuCrop={this.crop} + // anchorMenuFlashcard={() => this.getImageDesc()} /> )} </div> @@ -568,5 +893,5 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Docs.Prototypes.TemplateMap.set(DocumentType.IMG, { layout: { view: ImageBox, dataField: 'data' }, - options: { acl: '', freeform: '', systemIcon: 'BsFileEarmarkImageFill' }, + options: { acl: '', freeform: '', _layout_nativeDimEditable: true, systemIcon: 'BsFileEarmarkImageFill' }, }); |