diff options
author | bobzel <zzzman@gmail.com> | 2025-02-10 19:07:20 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-02-10 19:07:20 -0500 |
commit | c9686eaebffb3547b7e0f20aec64754627af76ce (patch) | |
tree | 7ebf1c38323a8d7af554ba564acf95cfe79b7709 /src/client/views/nodes/ImageBox.tsx | |
parent | b72d018698ad1d2e713f0fcbef392d23bf1cf545 (diff) | |
parent | e93ca53af693fa1ec2186ca9417af122bb5e8e09 (diff) |
updated from master
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 262 |
1 files changed, 245 insertions, 17 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0dfc0ec28..caefbf542 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,19 +1,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; +import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; -import { Colors } from 'browndash-components'; +import { Colors, Button, Type, Size } from '@dash/components'; 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 ReactLoading from 'react-loading'; -import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +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 { emptyFunction } from '../../../Utils'; @@ -32,21 +32,28 @@ 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'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { Upload } from '../../../server/SharedMediaTypes'; +import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; +import { SettingsManager } from '../../util/SettingsManager'; +import { AiOutlineSend } from 'react-icons/ai'; +import { FireflyImageData } from '../smartdraw/FireflyConstants'; +import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler'; 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); @@ -88,6 +95,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @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); makeObservable(this); @@ -266,7 +279,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); @@ -284,9 +297,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; @@ -309,6 +322,35 @@ 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((info: 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', @@ -320,10 +362,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }), icon: 'pencil-alt', }); + this.layoutDoc.ai && + funcs.push({ + description: 'Regenerate AI Image', + event: action(e => { + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(e?.x || 0, e?.y || 0) : SmartDrawHandler.Instance.hideRegenerate(); + }), + icon: 'pen-to-square', + }); + 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' }); } }; + // updateIcon = () => new Promise<void>(res => res()); + updateIcon = (usePanelDimensions?: boolean) => { + const contentDiv = this._mainCont.current; + 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(); @@ -338,7 +417,7 @@ 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); @@ -457,6 +536,160 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); } + 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 => { + if (newImgs[0]) { + const url = newImgs[0].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: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url }); + this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); + this.dataDoc.ai_firefly_prompt = newImgs[0].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((info: 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} />; @@ -465,13 +698,7 @@ 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, @@ -502,8 +729,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); return { width, height }; }; - savedAnnotations = () => this._savedAnnotations; + render() { TraceMobx(); const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; @@ -568,6 +795,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { annotationLayerScrollTop={0} scaling={returnOne} annotationLayerScaling={this._props.NativeDimScaling} + screenTransform={this.DocumentView().screenToViewTransform} docView={this.DocumentView} addDocument={this.addDocument} finishMarquee={this.finishMarquee} |