From c23d9ba83c87511a626bfe8d1da5dd981e311262 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 23 Jan 2020 18:24:38 -0500
Subject: got rid of extension docs.  changed layout-specific keys to start
 with "_" which flags them to be written to the current layout document

---
 src/client/views/nodes/AudioBox.tsx | 101 +++++++++++++++++-------------------
 1 file changed, 49 insertions(+), 52 deletions(-)

(limited to 'src/client/views/nodes/AudioBox.tsx')

diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 95c765e8a..8ede79edc 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -68,7 +68,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
         this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
-            const start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+            const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]);
             start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
         });
     }
@@ -128,13 +128,12 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     recordAudioAnnotation = () => {
         let gumStream: any;
         const self = this;
-        const extensionDoc = this.extensionDoc;
-        extensionDoc && navigator.mediaDevices.getUserMedia({
+        navigator.mediaDevices.getUserMedia({
             audio: true
         }).then(function (stream) {
             gumStream = stream;
             self._recorder = new MediaRecorder(stream);
-            extensionDoc.recordingStart = new DateField(new Date());
+            self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date());
             AudioBox.ActiveRecordings.push(self.props.Document);
             self._recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
@@ -213,55 +212,53 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     render() {
         const interactive = this.active() ? "-interactive" : "";
-        return (!this.extensionDoc ? (null) :
-            <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
-                onClick={!this.path ? this.recordClick : undefined}>
-                <div className="audiobox-handle"></div>
-                {!this.path ?
-                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
-                        {this._audioState === "recording" ? "STOP" : "RECORD"}
-                    </button> :
-                    <div className="audiobox-controls">
-                        <div className="audiobox-player" onClick={this.onPlay}>
-                            <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
-                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
-                            <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
-                                onPointerDown={e => {
-                                    if (e.button === 0 && !e.ctrlKey) {
-                                        const rect = (e.target as any).getBoundingClientRect();
-                                        this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
-                                        this.pause();
-                                        e.stopPropagation();
-                                    }
-                                }} >
-                                {DocListCast(this.dataDoc.links).map((l, i) => {
-                                    let la1 = l.anchor1 as Doc;
-                                    let la2 = l.anchor2 as Doc;
-                                    let linkTime = NumCast(l.anchor2Timecode);
-                                    if (Doc.AreProtosEqual(la1, this.dataDoc)) {
-                                        la1 = l.anchor2 as Doc;
-                                        la2 = l.anchor1 as Doc;
-                                        linkTime = NumCast(l.anchor1Timecode);
-                                    }
-                                    return !linkTime ? (null) :
-                                        <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
-                                            <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
-                                                <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
-                                                    parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
-                                                    backgroundColor={returnTransparent} />
-                                            </div>
-                                            <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
-                                                onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
-                                                onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
-                                        </div>;
-                                })}
-                                <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
-                                {this.audio}
-                            </div>
+        return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
+            onClick={!this.path ? this.recordClick : undefined}>
+            <div className="audiobox-handle"></div>
+            {!this.path ?
+                <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+                    {this._audioState === "recording" ? "STOP" : "RECORD"}
+                </button> :
+                <div className="audiobox-controls">
+                    <div className="audiobox-player" onClick={this.onPlay}>
+                        <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                        <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                        <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
+                            onPointerDown={e => {
+                                if (e.button === 0 && !e.ctrlKey) {
+                                    const rect = (e.target as any).getBoundingClientRect();
+                                    this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+                                    this.pause();
+                                    e.stopPropagation();
+                                }
+                            }} >
+                            {DocListCast(this.dataDoc.links).map((l, i) => {
+                                let la1 = l.anchor1 as Doc;
+                                let la2 = l.anchor2 as Doc;
+                                let linkTime = NumCast(l.anchor2Timecode);
+                                if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+                                    la1 = l.anchor2 as Doc;
+                                    la2 = l.anchor1 as Doc;
+                                    linkTime = NumCast(l.anchor1Timecode);
+                                }
+                                return !linkTime ? (null) :
+                                    <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+                                        <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
+                                            <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+                                                parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+                                                backgroundColor={returnTransparent} />
+                                        </div>
+                                        <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+                                            onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
+                                            onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+                                    </div>;
+                            })}
+                            <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+                            {this.audio}
                         </div>
                     </div>
-                }
-            </div>
-        );
+                </div>
+            }
+        </div>;
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From c5d618538d4fd9669476dc7eb66ddd45783e5fa6 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 05:50:15 -0500
Subject: nativewidth and nativeheight extension key fix and implemented UI to
 manually download a remotely hosted image

---
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 ++--
 src/client/util/Import & Export/ImageUtils.ts      |  4 ++--
 src/client/views/DocComponent.tsx                  |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/nodes/AudioBox.tsx                |  2 +-
 src/client/views/nodes/ImageBox.scss               | 24 ++++++++++++++-----
 src/client/views/nodes/ImageBox.tsx                | 28 +++++++++++++++++++++-
 src/mobile/ImageUpload.tsx                         |  2 +-
 src/server/ApiManagers/UploadManager.ts            | 14 ++++++++++-
 src/server/DashUploadUtils.ts                      | 12 +++-------
 10 files changed, 69 insertions(+), 25 deletions(-)

(limited to 'src/client/views/nodes/AudioBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c7c94abed..071015193 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -116,13 +116,13 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
+            collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData)));
             runInAction(() => this.completed += batch.length);
         });
 
         await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
             const path = Utils.prepend(clientAccessPath);
-            const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
+            const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 2f4db0e17..ff909cc6b 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -22,8 +22,8 @@ export namespace ImageUtils {
         } = await Networking.PostToServer("/inspectImage", { source });
         document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
         const proto = Doc.GetProto(document);
-        proto.nativeWidth = nativeWidth;
-        proto.nativeHeight = nativeHeight;
+        proto["data-nativeWidth"] = nativeWidth;
+        proto["data-nativeHeight"] = nativeHeight;
         proto.contentSize = contentSize;
         return data !== undefined;
     };
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d98301cf6..6f3eb6808 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -61,7 +61,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
 
         _annotationKey: string = "annotations";
         public set annotationKey(val: string) { this._annotationKey = val; }
-        public get annotationKey() { return this._annotationKey }
+        public get annotationKey() { return this._annotationKey; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b96b86929..80fcc33aa 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -324,7 +324,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
 
                     formData.append('file', file);
                     const dropFileName = file ? file.name : "-empty-";
-                    promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
+                    promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
                         results.map(action((result: any) => {
                             const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result;
                             const full = { ...options, _width: 300, title: dropFileName };
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8ede79edc..62a479b2a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -138,7 +138,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             self._recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend("/upload"), {
+                const res = await fetch(Utils.prepend("/uploadFormData"), {
                     method: 'POST',
                     body: formData
                 });
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index cf5d999a7..43f4a0ba9 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,10 +1,12 @@
-.imageBox, .imageBox-dragging{
+.imageBox,
+.imageBox-dragging {
     pointer-events: all;
     border-radius: inherit;
-    width:100%;
-    height:100%;
+    width: 100%;
+    height: 100%;
     position: absolute;
     transform-origin: top left;
+
     .imageBox-fader {
         pointer-events: all;
     }
@@ -16,6 +18,14 @@
     }
 }
 
+#upload-icon {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    width: 20px;
+    height: 20px;
+}
+
 .imageBox-cont {
     padding: 0vw;
     position: absolute;
@@ -25,7 +35,8 @@
     max-width: 100%;
     max-height: 100%;
     pointer-events: none;
-    background:transparent;
+    background: transparent;
+
     img {
         height: auto;
         width: 100%;
@@ -55,9 +66,10 @@
     padding: 3px;
     background: white;
     cursor: pointer;
-    opacity:0.15;
+    opacity: 0.15;
 }
-#google-photos:hover{
+
+#google-photos:hover {
     opacity: 1;
 }
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8db66d367..3d15bcb15 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -29,6 +29,7 @@ import { TraceMobx } from '../../../new_fields/util';
 import { SelectionManager } from '../../util/SelectionManager';
 import { cache } from 'sharp';
 import { ObjectField } from '../../../new_fields/ObjectField';
+import { Networking } from '../../Network';
 const requestImageSize = require('../../util/request-image-size');
 const path = require('path');
 const { Howl } = require('howler');
@@ -104,7 +105,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend("/upload"), {
+                const res = await fetch(Utils.prepend("/uploadFormData"), {
                     method: 'POST',
                     body: formData
                 });
@@ -287,6 +288,30 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
     }
 
+    @computed
+    private get considerDownloadIcon() {
+        const data = this.dataDoc[this.props.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"}
+                src={"/assets/downarrow.png"}
+                onClick={async () => {
+                    const { dataDoc } = this;
+                    const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
+                    dataDoc.originalUrl = primary;
+                    dataDoc[this.props.fieldKey] = new ImageField(Utils.prepend(clientAccessPath));
+                }}
+            />
+        );
+    }
+
     @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.props.fieldKey + "-nativeWidth"], pw);
@@ -347,6 +372,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
                     icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
             </div>
+            {this.considerDownloadIcon}
             {this.considerGooglePhotosLink()}
             <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
         </div>;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index dab4de110..1583e3d5d 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -45,7 +45,7 @@ class Uploader extends React.Component {
                     const formData = new FormData();
                     formData.append("file", files[0]);
 
-                    const upload = window.location.origin + "/upload";
+                    const upload = window.location.origin + "/uploadFormData";
                     this.status = "uploading image";
                     const res = await fetch(upload, {
                         method: 'POST',
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 583eaa59b..a92b613b7 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -40,7 +40,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: "/upload",
+            subscription: "/uploadFormData",
             secureHandler: async ({ req, res }) => {
                 const form = new formidable.IncomingForm();
                 form.uploadDir = pathToDirectory(Directory.parsed_files);
@@ -59,6 +59,18 @@ export default class UploadManager extends ApiManager {
             }
         });
 
+        register({
+            method: Method.POST,
+            subscription: "/uploadRemoteImage",
+            secureHandler: async ({ req, res }) => {
+                const { sources } = req.body;
+                if (Array.isArray(sources)) {
+                    return res.send(await Promise.all(sources.map(url => DashUploadUtils.UploadImage(url))));
+                }
+                res.send();
+            }
+        });
+
         register({
             method: Method.POST,
             subscription: "/uploadDoc",
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index b826b4882..cb7104757 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -108,13 +108,7 @@ export namespace DashUploadUtils {
         return MoveParsedFile(absolutePath, Directory.pdfs);
     }
 
-    const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
-    const sanitizeExtension = (source: string) => {
-        let extension = path.extname(source);
-        extension = extension.toLowerCase();
-        extension = extension.split("?")[0];
-        return extension;
-    };
+    const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
 
     /**
      * Uploads an image specified by the @param source to Dash's /public/files/
@@ -209,8 +203,8 @@ export namespace DashUploadUtils {
 
     export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
         const { requestable, source, ...remaining } = metadata;
-        const resolved = filename || generate(prefix, requestable);
-        const extension = format || sanitizeExtension(requestable || resolved);
+        const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
+        const resolved = filename || generate(prefix, extension);
         const information: ImageUploadInformation = {
             clientAccessPath: clientPathToFile(Directory.images, resolved),
             serverAccessPaths: {},
-- 
cgit v1.2.3-70-g09d2

