diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 1 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 10 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 71 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 165 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/TestBox.tsx | 467 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 1 |
8 files changed, 719 insertions, 2 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index de366763b..06d35038a 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -31,6 +31,7 @@ export enum DocumentType { COLOR = "color", // color picker (view of a color picker for a color string) YOUTUBE = "youtube", // youtube directory (view of you tube search results) DOCHOLDER = "docholder", // nested document (view of a document) + COMPARISON = "comparison", // before/after view with slider (view of 2 images) LINKDB = "linkdb", // database of links ??? why do we have this RECOMMENDATION = "recommendation", // view of a recommendation diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 1268c97b0..e8a0f8b1a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -48,6 +48,9 @@ import { ContextMenuProps } from "../views/ContextMenuItem"; import { ContextMenu } from "../views/ContextMenu"; import { LinkBox } from "../views/nodes/LinkBox"; import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; +import { ComparisonBox } from "../views/nodes/ComparisonBox"; +import CollectionMapView from "../views/collections/CollectionMapView"; +const requestImageSize = require('../util/request-image-size'); const path = require('path'); export interface DocumentOptions { @@ -281,6 +284,9 @@ export namespace Docs { [DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, }], + [DocumentType.COMPARISON, { + layout: { view: ComparisonBox, dataField: defaultDataKey }, + }], ]); // All document prototypes are initialized with at least these values @@ -525,6 +531,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options); } + export function ComparisonDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", options); + } + export function AudioDocument(url: string, options: DocumentOptions = {}) { const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options); Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'"); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8fb67c435..da60549ab 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faTerminal, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons'; +import { faColumns, faTerminal, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -102,6 +102,7 @@ export class MainView extends React.Component { } } + library.add(faColumns); library.add(faTerminal); library.add(faCalculator); library.add(faWindowMaximize); diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss new file mode 100644 index 000000000..5449cd145 --- /dev/null +++ b/src/client/views/nodes/ComparisonBox.scss @@ -0,0 +1,71 @@ +.comparisonBox { + pointer-events: all; + border-radius: inherit; + width: 100%; + height: 100%; + + .clip-div { + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + overflow: hidden; + z-index: 999; + + .beforeBox-cont { + height: 100%; + overflow: auto; + background-color: rgb(230, 230, 230); + } + + .slide-bar { + position: absolute; + width: 5px; + height: 100%; + top: 0; + right: 0; + background-color: white; + cursor: ew-resize; + } + } + + .afterBox-cont { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: 100%; + overflow: hidden; + background-color: rgb(200, 200, 200); + } + + .clear-button { + position: absolute; + top: 3px; + opacity: 0.8; + pointer-events: all; + cursor: pointer; + } + + .clear-button.before { + left: 3px; + } + + .clear-button.after { + right: 3px; + } + + .placeholder { + width: 50%; + height: 50%; + margin-top: 25%; + margin-left: 25%; + + .upload-icon { + width: 100%; + height: 100%; + opacity: 0.5; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx new file mode 100644 index 000000000..09590aff7 --- /dev/null +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -0,0 +1,165 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush, faTimes, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction, Lambda } from 'mobx'; +import { observer } from "mobx-react"; +import { Doc } from '../../../new_fields/Doc'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; +import { ComputedField } from '../../../new_fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { DragManager } from '../../util/DragManager'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ComparisonBox.scss"; +import React = require("react"); +import { ContentFittingDocumentView } from './ContentFittingDocumentView'; + +library.add(faImage, faEye as any, faPaintBrush, faBrain); +library.add(faFileAudio, faAsterisk); + +export const pageSchema = createSchema({ + beforeDoc: "string", + afterDoc: "string" +}); + +type ComparisonDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; +const ComparisonDocument = makeInterface(pageSchema, documentSchema); + +@observer +export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, ComparisonDocument>(ComparisonDocument) { + protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; + + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + + private _beforeDropDisposer?: DragManager.DragDropDisposer; + private _afterDropDisposer?: DragManager.DragDropDisposer; + + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { + if (ele) { + return DragManager.MakeDropTarget(ele, (event, dropEvent) => this.dropHandler(event, dropEvent, fieldKey)); + } + } + + private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; + if (droppedDocs?.length) { + this.props.Document[fieldKey] = Doc.MakeAlias(droppedDocs[0]); + } + } + + @action + private registerSliding = (e: React.PointerEvent<HTMLDivElement>) => { + e.stopPropagation(); + e.preventDefault(); + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + } + + private resizeUpdater: Lambda | undefined; + componentWillMount() { + this.props.Document.clipWidth = this.props.PanelWidth() / 2; + + //preserve before/after ratio during resizing + this.resizeUpdater = computed(() => this.props.PanelWidth()).observe(({ oldValue, newValue }) => + this.props.Document.clipWidth = NumCast(this.props.Document.clipWidth) / NumCast(oldValue) * newValue + ); + } + + componentWillUnmount() { + if (this.resizeUpdater) this.resizeUpdater(); + } + + private onPointerMove = ({ movementX }: PointerEvent) => { + const width = movementX * this.props.ScreenToLocalTransform().Scale + NumCast(this.props.Document.clipWidth); + if (width && width > 5 && width < this.props.PanelWidth()) { + this.props.Document.clipWidth = width; + } + } + + @action + private onPointerUp = () => { + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + clearBeforeDoc = (e: React.MouseEvent) => { + e.stopPropagation; + e.preventDefault; + delete this.props.Document.beforeDoc; + } + + clearAfterDoc = (e: React.MouseEvent) => { + e.stopPropagation; + e.preventDefault; + delete this.props.Document.afterDoc; + } + + get fieldKey() { + return this.props.fieldKey.startsWith("@") ? StrCast(this.props.Document[this.props.fieldKey]) : this.props.fieldKey; + } + + render() { + const beforeDoc = this.props.Document.beforeDoc as Doc; + const afterDoc = this.props.Document.afterDoc as Doc; + const clipWidth = this.props.Document.clipWidth as Number; + return ( + <div className="comparisonBox"> + {/* wraps around before image and slider bar */} + <div className="clip-div" style={{ width: clipWidth + "px" }}> + <div + className="beforeBox-cont" + key={this.props.Document[Id]} + ref={(ele) => { + this._beforeDropDisposer && this._beforeDropDisposer(); + this._beforeDropDisposer = this.createDropTarget(ele, "beforeDoc"); + }} + style={{ width: this.props.PanelWidth() }}> + { + beforeDoc ? + <> + <ContentFittingDocumentView {...this.props} + Document={beforeDoc} + getTransform={this.props.ScreenToLocalTransform} /> + <div className="clear-button before" onClick={(e) => this.clearBeforeDoc(e)}> + <FontAwesomeIcon className="clear-button before" icon={faTimes} size="sm" /> + </div> + </> + : + <div className="placeholder"> + <FontAwesomeIcon className="upload-icon" icon={faCloudUploadAlt} size="lg" /> + </div> + } + </div> + <div className="slide-bar" onPointerDown={e => this.registerSliding(e)} /> + </div> + <div + className="afterBox-cont" + key={this.props.Document[Id]} + ref={(ele) => { + this._afterDropDisposer && this._afterDropDisposer(); + this._afterDropDisposer = this.createDropTarget(ele, "afterDoc"); + }}> + { + afterDoc ? + <> + <ContentFittingDocumentView {...this.props} + Document={afterDoc} + getTransform={this.props.ScreenToLocalTransform} /> + <div className="clear-button after" onClick={(e) => this.clearAfterDoc(e)}> + <FontAwesomeIcon className="clear-button after" icon={faTimes} size="sm" /> + </div> + </> + : + <div className="placeholder"> + <FontAwesomeIcon className="upload-icon" icon={faCloudUploadAlt} size="lg" /> + </div> + } + </div> + </div >); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index a022f2e02..90d2d4936 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -30,6 +30,7 @@ import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { LinkAnchorBox } from "./LinkAnchorBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { ScreenshotBox } from "./ScreenshotBox"; +import { ComparisonBox } from "./ComparisonBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; @@ -187,7 +188,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, - RecommendationsBox, ScreenshotBox, HTMLtag + RecommendationsBox, ScreenshotBox, HTMLtag, ComparisonBox }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/TestBox.tsx b/src/client/views/nodes/TestBox.tsx new file mode 100644 index 000000000..de1640027 --- /dev/null +++ b/src/client/views/nodes/TestBox.tsx @@ -0,0 +1,467 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List'; +import { ObjectField } from '../../../new_fields/ObjectField'; +import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; +import { ComputedField } from '../../../new_fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { AudioField, ImageField } from '../../../new_fields/URLField'; +import { TraceMobx } from '../../../new_fields/util'; +import { emptyFunction, returnOne, Utils, returnZero } from '../../../Utils'; +import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; +import { Docs } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { DragManager } from '../../util/DragManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenu } from "../../views/ContextMenu"; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import FaceRectangles from './FaceRectangles'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ImageBox.scss"; +import React = require("react"); +const requestImageSize = require('../../util/request-image-size'); +const path = require('path'); +const { Howl } = require('howler'); + + +library.add(faImage, faEye as any, faPaintBrush, faBrain); +library.add(faFileAudio, faAsterisk); + + +export const pageSchema = createSchema({ + curPage: "number", + fitWidth: "boolean", + googlePhotosUrl: "string", + googlePhotosTags: "string" +}); + +interface Window { + MediaRecorder: MediaRecorder; +} + +declare class MediaRecorder { + // whatever MediaRecorder has + constructor(e: any); +} + +type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; +const ImageDocument = makeInterface(pageSchema, documentSchema); + +const uploadIcons = { + idle: "downarrow.png", + loading: "loading.gif", + success: "greencheck.png", + failure: "redx.png" +}; + +@observer +export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) { + protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); + private _dropDisposer?: DragManager.DragDropDisposer; + @observable private _audioState = 0; + @observable static _showControls: boolean; + @observable uploadIcon = uploadIcons.idle; + + protected createDropTarget = (ele: HTMLDivElement) => { + this._dropDisposer && this._dropDisposer(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + if (de.metaKey) { + de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => { + Doc.AddDocToList(this.dataDoc, this.fieldKey + "-alternates", drop); + e.stopPropagation(); + })); + } else if (de.altKey || !this.dataDoc[this.fieldKey]) { + const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; + const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetDoc = layoutDoc[DataSym]; + if (targetDoc[targetField] instanceof ImageField) { + this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); + this.dataDoc[this.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]); + this.dataDoc[this.fieldKey + "-nativeHeight"] = NumCast(targetDoc[targetField + "-nativeHeight"]); + e.stopPropagation(); + } + } + } + } + + recordAudioAnnotation = () => { + let gumStream: any; + let recorder: any; + const self = this; + navigator.mediaDevices.getUserMedia({ + audio: true + }).then(function (stream) { + gumStream = stream; + recorder = new MediaRecorder(stream); + recorder.ondataavailable = async function (e: any) { + const formData = new FormData(); + formData.append("file", e.data); + const res = await fetch(Utils.prepend("/uploadFormData"), { + method: 'POST', + body: formData + }); + const files = await res.json(); + const url = Utils.prepend(files[0].path); + // upload to server with known URL + const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); + audioDoc.treeViewExpandedView = "layout"; + const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc)); + if (audioAnnos === undefined) { + this.dataDoc[this.fieldKey + "-audioAnnotations"] = new List([audioDoc]); + } else { + audioAnnos.push(audioDoc); + } + }; + runInAction(() => self._audioState = 2); + recorder.start(); + setTimeout(() => { + recorder.stop(); + runInAction(() => self._audioState = 0); + gumStream.getAudioTracks()[0].stop(); + }, 5000); + }); + } + + @undoBatch + rotate = action(() => { + const nw = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); + const nh = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); + const w = this.layoutDoc._width; + const h = this.layoutDoc._height; + this.dataDoc[this.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.fieldKey + "-rotation"]) + 90) % 360; + this.dataDoc[this.fieldKey + "-nativeWidth"] = nh; + this.dataDoc[this.fieldKey + "-nativeHeight"] = nw; + this.layoutDoc._width = h; + this.layoutDoc._height = w; + }); + + specificContextMenu = (e: React.MouseEvent): void => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + if (field) { + const funcs: ContextMenuProps[] = []; + funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); + funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); + funcs.push({ + description: "Reset Native Dimensions", event: action(async () => { + const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); + const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); + if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) { + this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH; + this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight(); + } else { + this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth(); + this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW; + } + }), icon: "expand-arrows-alt" + }); + + const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); + const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; + modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); + modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); + !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); + + ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); + } + } + + extractFaces = () => { + const converter = (results: any) => { + return results.map((face: CognitiveServices.Image.Face) => Docs.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!); + }; + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter); + } + + generateMetadata = (threshold: Confidence = Confidence.Excellent) => { + const converter = (results: any) => { + const tagDoc = new Doc; + const tagsList = new List(); + results.tags.map((tag: Tag) => { + tagsList.push(tag.name); + const sanitized = tag.name.replace(" ", "_"); + tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); + }); + this.dataDoc[this.fieldKey + "-generatedTags"] = tagsList; + tagDoc.title = "Generated Tags Doc"; + tagDoc.confidence = threshold; + return tagDoc; + }; + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter); + } + + @computed private get url() { + const data = Cast(this.dataDoc[this.fieldKey], ImageField); + return data ? data.url.href : undefined; + } + + choosePath(url: URL) { + const lower = url.href.toLowerCase(); + if (url.protocol === "data") { + return url.href; + } else if (url.href.indexOf(window.location.origin) === -1) { + return Utils.CorsProxy(url.href); + } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) { + return url.href;//Why is this here + } + const ext = path.extname(url.href); + const suffix = this.props.renderDepth < 1 ? "_o" : this._curSuffix; + return url.href.replace(ext, suffix + ext); + } + + @observable _smallRetryCount = 1; + @observable _mediumRetryCount = 1; + @observable _largeRetryCount = 1; + @action retryPath = () => { + if (this._curSuffix === "_s") this._smallRetryCount++; + if (this._curSuffix === "_m") this._mediumRetryCount++; + if (this._curSuffix === "_l") this._largeRetryCount++; + } + @action onError = (error: any) => { + const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; + if (timeout < 5) { + setTimeout(this.retryPath, 500); + } else { + const original = StrCast(this.dataDoc[this.fieldKey + "-originalUrl"]); + if (error.type === "error" && original) { + this.dataDoc[this.fieldKey] = new ImageField(original); + } + } + } + _curSuffix = "_m"; + + resize = (imgPath: string) => { + const cachedNativeSize = { + width: NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]), + height: NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) + }; + const docAspect = this.layoutDoc[HeightSym]() / this.layoutDoc[WidthSym](); + const cachedAspect = cachedNativeSize.height / cachedNativeSize.width; + if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05) { + if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) { + requestImageSize(imgPath).then((inquiredSize: any) => { + const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180; + const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize; + const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; + setTimeout(action(() => { + if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { + this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; + this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = rotatedNativeSize.width; + this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = rotatedNativeSize.height; + } + }), 0); + }).catch((err: any) => console.log(err)); + } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) { + this.layoutDoc._width = this.layoutDoc[WidthSym]() || cachedNativeSize.width; + this.layoutDoc._height = this.layoutDoc[WidthSym]() * cachedAspect; + } + } else if (this.layoutDoc._nativeWidth !== cachedNativeSize.width || this.layoutDoc._nativeHeight !== cachedNativeSize.height) { + !(this.layoutDoc[StrCast(this.layoutDoc.layoutKey)] instanceof Doc) && setTimeout(() => { + if (!(this.layoutDoc[StrCast(this.layoutDoc.layoutKey)] instanceof Doc)) { + this.layoutDoc._nativeWidth = cachedNativeSize.width; + this.layoutDoc._nativeHeight = cachedNativeSize.height; + } + }, 0); + } + } + + @action + onPointerEnter = () => { + const self = this; + const audioAnnos = DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]); + if (audioAnnos && audioAnnos.length && this._audioState === 0) { + const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; + anno.data instanceof AudioField && new Howl({ + src: [anno.data.url.href], + format: ["mp3"], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => self._audioState = 0); + } + }); + this._audioState = 1; + } + } + + audioDown = () => this.recordAudioAnnotation(); + + considerGooglePhotosLink = () => { + const remoteUrl = this.dataDoc.googlePhotosUrl; + return !remoteUrl ? (null) : (<img + style={{ transform: `scale(${this.props.ContentScaling()})`, transformOrigin: "bottom right" }} + id={"google-photos"} + src={"/assets/google_photos.png"} + onClick={() => window.open(remoteUrl)} + />); + } + + considerGooglePhotosTags = () => { + const tags = this.dataDoc.googlePhotosTags; + return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />); + } + + @computed + private get considerDownloadIcon() { + const data = this.dataDoc[this.fieldKey]; + if (!(data instanceof ImageField)) { + return (null); + } + const primary = data.url.href; + if (primary.includes(window.location.origin)) { + return (null); + } + return ( + <img + id={"upload-icon"} + style={{ transform: `scale(${1 / this.props.ContentScaling()})`, transformOrigin: "bottom right" }} + src={`/assets/${this.uploadIcon}`} + onClick={async () => { + const { dataDoc } = this; + const { success, failure, idle, loading } = uploadIcons; + runInAction(() => this.uploadIcon = loading); + const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] }); + dataDoc[this.props.fieldKey + "-originalUrl"] = primary; + let succeeded = true; + let data: ImageField | undefined; + try { + data = new ImageField(Utils.prepend(accessPaths.agnostic.client)); + } catch { + succeeded = false; + } + runInAction(() => this.uploadIcon = succeeded ? success : failure); + setTimeout(action(() => { + this.uploadIcon = idle; + if (data) { + dataDoc[this.fieldKey] = data; + } + }), 2000); + }} + /> + ); + } + + @computed get nativeSize() { + const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; + const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], pw); + const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1); + return { nativeWidth, nativeHeight }; + } + + // this._curSuffix = ""; + // if (w > 20) { + // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; + // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; + // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; + @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.href).filter(url => url); // access the primary layout data of the alternate documents + const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; + return paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + } + + @computed get content() { + TraceMobx(); + + const srcpath = this.paths[0]; + const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; + const { nativeWidth, nativeHeight } = this.nativeSize; + const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); + const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; + const pwidth = this.props.PanelWidth(); + const pheight = this.props.PanelHeight(); + const shift = (rotation % 180) ? (pheight - pwidth) / aspect / 2 + (pheight - pwidth) / 2 : 0; + + this.resize(srcpath); + + return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget}> + <div className="imageBox-fader" > + <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + src={srcpath} + style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} + width={nativeWidth} + ref={this._imgRef} + onError={this.onError} /> + {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> + <img className="imageBox-fadeaway" + key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + src={fadepath} + style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})`, }} + width={nativeWidth} + ref={this._imgRef} + onError={this.onError} /></div>} + </div> + {!this.layoutDoc._showAudio ? (null) : + <div className="imageBox-audioBackground" + onPointerDown={this.audioDown} + onPointerEnter={this.onPointerEnter} + style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }} + > + <FontAwesomeIcon className="imageBox-audioFont" + style={{ color: [DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} + icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" /> + </div>} + {this.considerDownloadIcon} + {this.considerGooglePhotosLink()} + <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> + </div>; + } + + contentFunc = () => [this.content]; + render() { + TraceMobx(); + const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging"; + return (<div className={`imageBox${dragging}`} onContextMenu={this.specificContextMenu} + style={{ + transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, + width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, + height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, + pointerEvents: this.layoutDoc.isBackground ? "none" : undefined, + borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px` + }} > + <CollectionFreeFormView {...this.props} + forceScaling={true} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + NativeHeight={returnZero} + NativeWidth={returnZero} + annotationsKey={this.annotationKey} + isAnnotationOverlay={true} + focus={this.props.focus} + isSelected={this.props.isSelected} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + {this.contentFunc} + </CollectionFreeFormView> + </div >); + } +}
\ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 25775edef..1f0371aca 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -208,6 +208,7 @@ export class CurrentUserUtils { { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" }); } return [ + { title: "Drag a comparison box", label: "Comp", icon: "columns", ignoreClick: true, drag: 'Docs.Create.ComparisonDocument()' }, { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, { title: "Drag a web page", label: "Web", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("", { title: "New Webpage" })' }, { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' }, |