diff options
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 101 |
1 files changed, 70 insertions, 31 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index e2b0ee7df..a79dda74e 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,20 +1,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; +import { Colors } from 'browndash-components'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; +import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -23,29 +26,39 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; import { OpenWhere } from './DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; +import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; +export class ImageEditorData { + 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>) => (this._instance.imageData = { open, rootDoc, source, addDoc }); + + constructor() { + makeObservable(this); + ImageEditorData._instance = this; + } + + public static get Open() { return ImageEditorData.imageData.open; } // prettier-ignore + public static get Source() { return ImageEditorData.imageData.source; } // prettier-ignore + public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore + public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore + public static set Open(open: boolean) { ImageEditorData.set(open, this.imageData.rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore + public static set Source(source: string) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, source, this.imageData.addDoc); } // prettier-ignore + public static set RootDoc(rootDoc: Opt<Doc>) { ImageEditorData.set(this.imageData.open, rootDoc, this.imageData.source, this.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 +} @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } - - @observable public static imageRootDoc: Doc | undefined = undefined; - @observable public static imageEditorOpen: boolean = false; - @observable public static imageEditorSource: string = ''; - @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined = undefined; - @action public static setImageEditorOpen(open: boolean) { - ImageBox.imageEditorOpen = open; - } - @action public static setImageEditorSource(source: string) { - ImageBox.imageEditorSource = source; - } private _ignoreScroll = false; private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; @@ -134,7 +147,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl 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); + 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]; @@ -159,8 +172,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @undoBatch setNativeSize = action(() => { - const scaling = (this.DocumentView?.().screenToViewTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1); - const nscale = NumCast(this._props.PanelWidth()) / scaling; + const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1); const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; @@ -243,10 +255,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl funcs.push({ description: 'Open Image Editor', event: action(() => { - ImageBox.setImageEditorOpen(true); - ImageBox.setImageEditorSource(this.choosePath(field.url)); - ImageBox.addDoc = this._props.addDocument; - ImageBox.imageRootDoc = this.Document; + ImageEditorData.Open = true; + ImageEditorData.Source = this.choosePath(field.url); + ImageEditorData.AddDoc = this._props.addDocument; + ImageEditorData.RootDoc = this.Document; }), icon: 'pencil-alt', }); @@ -259,7 +271,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -297,7 +309,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -311,13 +323,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get paths() { const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts - .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) - .filter(url => url) - .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const alts = this.dataDoc[this.fieldKey + '_alternates'] as any as List<Doc>; // retrieve alternate documents that may be rendered as alternate images + const defaultUrl = new URL(Utils.prepend('/assets/unknown-file-icon-hi.png')); + const altpaths = + alts + ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl)) + .filter(url => url) + .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; - return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')]; + return paths.length ? paths : [defaultUrl.href]; } @observable _error = ''; @@ -326,7 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get content() { TraceMobx(); - const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)); + const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) ?? Colors.WHITE); const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1; const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); @@ -370,6 +384,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { + if (!this.dataDoc[this.fieldKey]) return this.chooseImage(); 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)) { setupMoveUpEvents( this, @@ -468,4 +483,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl </div> ); } + + public chooseImage = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.accept = 'image/*'; + input.onchange = async _e => { + const file = input.files?.[0]; + if (file) { + const disposer = OverlayView.ShowSpinner(); + const [{ result }] = await Networking.UploadFilesToServer({ file }); + if (result instanceof Error) { + alert('Error uploading files - possibly due to unsupported file types'); + } else { + this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); + !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc); + } + disposer(); + } else { + console.log('No file selected'); + } + }; + input.click(); + }; } |