aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json44
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts10
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/nodes/ComparisonBox.scss71
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx165
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/TestBox.tsx467
-rw-r--r--src/server/authentication/models/current_user_utils.ts1
9 files changed, 751 insertions, 14 deletions
diff --git a/package-lock.json b/package-lock.json
index 176d1f27e..ea60d2876 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5551,7 +5551,8 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -5569,7 +5570,8 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"bindings": {
"version": "1.5.0",
@@ -5583,6 +5585,7 @@
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5595,15 +5598,18 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5706,7 +5712,8 @@
},
"inherits": {
"version": "2.0.4",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -5716,6 +5723,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5728,17 +5736,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -5755,6 +5766,7 @@
"mkdirp": {
"version": "0.5.3",
"bundled": true,
+ "optional": true,
"requires": {
"minimist": "^1.2.5"
}
@@ -5810,7 +5822,8 @@
},
"npm-normalize-package-bin": {
"version": "1.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"npm-packlist": {
"version": "1.4.8",
@@ -5835,7 +5848,8 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -5845,6 +5859,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -5913,7 +5928,8 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5943,6 +5959,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5960,6 +5977,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -5998,11 +6016,13 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"yallist": {
"version": "3.1.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
}
}
},
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" })' },