diff options
33 files changed, 284 insertions, 181 deletions
diff --git a/package-lock.json b/package-lock.json index 2a9e33a28..f675beb94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2800,7 +2800,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2818,11 +2819,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2835,15 +2838,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", @@ -2946,7 +2952,8 @@ }, "inherits": { "version": "2.0.4", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2956,6 +2963,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2968,17 +2976,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" @@ -2995,6 +3006,7 @@ "mkdirp": { "version": "0.5.3", "bundled": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -3050,7 +3062,8 @@ }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -3075,7 +3088,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3085,6 +3099,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3153,7 +3168,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3183,6 +3199,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", @@ -3200,6 +3217,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3238,11 +3256,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.1.1", - "bundled": true + "bundled": true, + "optional": true } } } @@ -9436,7 +9456,7 @@ }, "chownr": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "resolved": false, "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "ci-info": { @@ -9742,7 +9762,7 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "resolved": false, "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "defaults": { @@ -10241,7 +10261,7 @@ }, "glob": { "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "resolved": false, "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", @@ -10329,7 +10349,7 @@ }, "hosted-git-info": { "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "resolved": false, "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, "http-cache-semantics": { @@ -10465,7 +10485,7 @@ }, "is-ci": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "resolved": false, "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "requires": { "ci-info": "^1.5.0" @@ -10541,7 +10561,7 @@ }, "is-retry-allowed": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "resolved": false, "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" }, "is-stream": { @@ -11050,7 +11070,7 @@ }, "mkdirp": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "resolved": false, "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", "requires": { "minimist": "^1.2.5" @@ -11058,7 +11078,7 @@ "dependencies": { "minimist": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "resolved": false, "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } @@ -11110,7 +11130,7 @@ }, "node-gyp": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz", + "resolved": false, "integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==", "requires": { "env-paths": "^2.2.0", @@ -11224,7 +11244,7 @@ }, "npm-packlist": { "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "resolved": false, "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", "requires": { "ignore-walk": "^3.0.1", @@ -11244,7 +11264,7 @@ }, "npm-profile": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/npm-profile/-/npm-profile-4.0.4.tgz", + "resolved": false, "integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==", "requires": { "aproba": "^1.1.2 || 2", @@ -11254,7 +11274,7 @@ }, "npm-registry-fetch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz", + "resolved": false, "integrity": "sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw==", "requires": { "JSONStream": "^1.3.4", @@ -11689,7 +11709,7 @@ }, "rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "resolved": false, "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { "deep-extend": "^0.6.0", @@ -11700,7 +11720,7 @@ "dependencies": { "minimist": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "resolved": false, "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } @@ -11759,7 +11779,7 @@ }, "readable-stream": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "resolved": false, "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { "inherits": "^2.0.3", @@ -11780,7 +11800,7 @@ }, "registry-auth-token": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "resolved": false, "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", "requires": { "rc": "^1.1.6", @@ -11844,7 +11864,7 @@ }, "rimraf": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "resolved": false, "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { "glob": "^7.1.3" @@ -12143,7 +12163,7 @@ }, "string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "resolved": false, "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { "safe-buffer": "~5.2.0" @@ -12151,7 +12171,7 @@ "dependencies": { "safe-buffer": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "resolved": false, "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" } } @@ -12463,7 +12483,7 @@ }, "widest-line": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "resolved": false, "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", "requires": { "string-width": "^2.1.1" diff --git a/src/Utils.ts b/src/Utils.ts index bcb215804..ef5002bec 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -43,7 +43,7 @@ export namespace Utils { } /** - * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the + * A convenience method. Prepends the full path (i.e. http://localhost:<port>) to the * requested extension * @param extension the specified sub-path to append to the window origin */ diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 69f8f9b33..9b1912fb6 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -126,6 +126,9 @@ export interface DocumentOptions { curPage?: number; currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) + currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) + lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) + activeFrame?: number; // the active frame of a document in a frame base collection borderRounding?: string; boxShadow?: string; dontRegisterChildViews?: boolean; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 2db5cd3ba..a35a8869c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -121,7 +121,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV dragComplete: dropEv => { const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop if (this.view0 && linkDoc) { - Doc.GetProto(linkDoc).linkRelationship = "hyperlink"; + !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 17c001971..6878658a8 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -5,12 +5,16 @@ import * as ReactDOM from 'react-dom'; import * as React from 'react'; import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; +import { Networking } from "../Network"; AssignAllExtensions(); +export let resolvedPorts: { server: number, socket: number }; + (async () => { const info = await CurrentUserUtils.loadCurrentUser(); - DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email); + resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts")); + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email); await Docs.Prototypes.initialize(); if (info.id !== "__guest__") { // a guest will not have an id registered diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b7a75dfb9..358de2333 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -139,10 +139,7 @@ export class MainView extends React.Component { initEventListeners = () => { window.addEventListener("drop", (e) => { e.preventDefault(); }, false); // drop event handler - window.addEventListener("dragover", (e) => { - console.log("MDRAG"); - e.preventDefault(); - }, false); // drag event handler + window.addEventListener("dragover", (e) => { e.preventDefault(); }, false); // drag event handler // click interactions for the context menu document.addEventListener("pointerdown", this.globalPointerDown); document.addEventListener("pointerup", this.globalPointerUp); @@ -246,7 +243,6 @@ export class MainView extends React.Component { onDrop = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); - console.log("Drop"); } @action diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index bfa44fe47..cfa869fb2 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -3,14 +3,17 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; -import { NumCast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils } from "../../Utils"; +import { NumCast, Cast } from "../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents } from "../../Utils"; import { Transform } from "../util/Transform"; import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView"; import { DocumentView } from "./nodes/DocumentView"; import './OverlayView.scss'; import { Scripting } from "../util/Scripting"; import { ScriptingRepl } from './ScriptingRepl'; +import { DragManager } from "../util/DragManager"; +import { listSpec } from "../../fields/Schema"; +import { List } from "../../fields/List"; export type OverlayDisposer = () => void; @@ -139,46 +142,51 @@ export class OverlayView extends React.Component { return remove; } + @computed get overlayDocs() { const userDocOverlays = Doc.UserDoc().myOverlayDocuments; if (!userDocOverlays) { - return (null); + return null; } return userDocOverlays instanceof Doc && DocListCast(userDocOverlays.data).map(d => { setTimeout(() => d.inOverlay = true, 0); let offsetx = 0, offsety = 0; - const onPointerMove = action((e: PointerEvent) => { + const dref = React.createRef<HTMLDivElement>(); + const onPointerMove = action((e: PointerEvent, down: number[]) => { if (e.buttons === 1) { d.x = e.clientX + offsetx; d.y = e.clientY + offsety; - e.stopPropagation(); - e.preventDefault(); } - }); - const onPointerUp = action((e: PointerEvent) => { - document.removeEventListener("pointermove", onPointerMove); - document.removeEventListener("pointerup", onPointerUp); - e.stopPropagation(); - e.preventDefault(); + if (e.metaKey) { + const dragData = new DragManager.DocumentDragData([d]); + d.removeDropProperties = new List<string>(["inOverlay"]); + dragData.offset = [-offsetx, -offsety]; + dragData.dropAction = "move"; + dragData.removeDocument = (doc: Doc | Doc[]) => { + const docs = (doc instanceof Doc) ? [doc] : doc; + docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocuments, Doc, null), "data", d)); + return true; + }; + dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { + return dragData.removeDocument!(doc) ? addDocument(doc) : false; + }; + DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]); + return true; + } + return false; }); const onPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction); offsetx = NumCast(d.x) - e.clientX; offsety = NumCast(d.y) - e.clientY; - e.stopPropagation(); - e.preventDefault(); - document.addEventListener("pointermove", onPointerMove); - document.addEventListener("pointerup", onPointerUp); }; - return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)` }}> + return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}> <DocumentView Document={d} LibraryPath={emptyPath} ChromeHeight={returnZero} rootSelected={returnTrue} - // isSelected={returnFalse} - // select={emptyFunction} - // layoutKey={"layout"} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a6b0d03b8..423eb1d90 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -218,7 +218,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); if (docDragData) { let added = false; - if (docDragData.dropAction || docDragData.userDropAction) { + const dropaction = docDragData.dropAction || docDragData.userDropAction; + if (dropaction && dropaction !== "move") { added = this.addDocument(docDragData.droppedDocuments); } else if (docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 7a84fcde1..5478a1c4a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -51,12 +51,21 @@ } .backKeyframe { right:45; + svg { + display:block; + margin:auto; + } } .numKeyframe { right:25; + text-align:center; } .fwdKeyframe { right:5; + svg { + display:block; + margin:auto; + } } .collectionfreeformview-placeholder { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 1611b6935..4840bb7e7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -18,7 +18,7 @@ import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { aggregateBounds, intersectRect, returnOne, Utils, returnZero, returnFalse, numberRange } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; -import { Docs } from "../../../documents/Documents"; +import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; @@ -56,6 +56,7 @@ export const panZoomSchema = createSchema({ scale: "number", currentTimecode: "number", displayTimecode: "number", + currentFrame: "number", arrangeScript: ScriptField, arrangeInit: ScriptField, useClusters: "boolean", @@ -126,44 +127,62 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); } - addDocument = (newBox: Doc | Doc[]) => { - if (this.Document.currentTimecode !== undefined && !this.props.isAnnotationOverlay) { - CollectionFreeFormDocumentView.setupKeyframes((newBox instanceof Doc) ? [newBox] : newBox, this.Document.currentTimecode, this.props.Document); - } - + addDocument = action((newBox: Doc | Doc[]) => { + let retVal = false; if (newBox instanceof Doc) { - const added = this.props.addDocument(newBox); - added && this.bringToFront(newBox); - added && this.updateCluster(newBox); - return added; + retVal = this.props.addDocument(newBox); + retVal && this.bringToFront(newBox); + retVal && this.updateCluster(newBox); } else { - return this.props.addDocument(newBox); + retVal = this.props.addDocument(newBox); // bcz: deal with clusters } - } + if (retVal) { + const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox; + for (let i = 0; i < newBoxes.length; i++) { + const newBox = newBoxes[i]; + if (newBox.activeFrame !== undefined) { + const x = newBox.x; + const y = newBox.y; + delete newBox["x-indexed"]; + delete newBox["y-indexed"]; + delete newBox["opacity-indexed"]; + delete newBox.x; + delete newBox.y; + delete newBox.activeFrame; + newBox.x = x; + newBox.y = y; + } + } + if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { + CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentFrame); + } + } + return retVal; + }) @undoBatch @action nextKeyframe = (): void => { - const currentTimecode = this.Document.currentTimecode; - if (currentTimecode === undefined) { - this.Document.currentTimecode = 0; - CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0, this.props.Document); + const currentFrame = this.Document.currentFrame; + if (currentFrame === undefined) { + this.Document.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } - CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentTimecode || 0); - this.Document.currentTimecode = Math.max(0, (currentTimecode || 0) + 1); - this.Document.lastTimecode = Math.max(NumCast(this.Document.currentTimecode), NumCast(this.Document.lastTimecode)); + CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); + this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1); + this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame)); } @undoBatch @action prevKeyframe = (): void => { - const currentTimecode = this.Document.currentTimecode; - if (currentTimecode === undefined) { - this.Document.currentTimecode = 0; - CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0, this.props.Document); + const currentFrame = this.Document.currentFrame; + if (currentFrame === undefined) { + this.Document.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); - this.Document.currentTimecode = Math.max(0, (currentTimecode || 0) - 1); + this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1); } private selectDocuments = (docs: Doc[]) => { @@ -191,6 +210,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const [xp, yp] = xf.transformPoint(de.x, de.y); const [xpo, ypo] = xfo.transformPoint(de.x, de.y); const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); + if (!this.isAnnotationOverlay && de.complete.linkDragData && de.complete.linkDragData.linkSourceDocument !== this.props.Document) { + const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); + this.props.addDocument(source); + (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceDocument }, + "doc annotation")); // TODODO this is where in text links get passed + e.stopPropagation(); + return true; + } if (super.onInternalDrop(e, de)) { if (de.complete.docDragData) { if (de.complete.docDragData.droppedDocuments.length) { @@ -206,9 +233,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P for (let i = 0; i < droppedDocs.length; i++) { const d = droppedDocs[i]; const layoutDoc = Doc.Layout(d); - if (this.Document.currentTimecode !== undefined && !this.props.isAnnotationOverlay) { - const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.displayTimecode, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document.currentTimecode, d, x + vals.x - dropX, y + vals.y - dropY, vals.opacity); + if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { + const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); + CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropX, y + vals.y - dropY, vals.opacity); } else { d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; @@ -979,12 +1006,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return { x: 0, y: 0, transition: "transform 1s", ...result, pair: params.pair, replica: "" }; } const layoutDoc = Doc.Layout(params.pair.layout); - const { x, y, opacity } = this.Document.currentTimecode === undefined ? params.pair.layout : - CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentTimecode || 0); + const { x, y, opacity } = this.Document.currentFrame === undefined ? params.pair.layout : + CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentFrame || 0); const { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), - transition: StrCast(layoutDoc.transition), opacity: Cast(opacity, "number", null), + transition: StrCast(layoutDoc.transition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: "" }; } @@ -1368,8 +1395,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P <div key="back" className="backKeyframe" onClick={this.prevKeyframe}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> - <div key="num" className="numKeyframe" > - {NumCast(this.props.Document.currentTimecode)} + <div key="num" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.Document.editing = !this.Document.editing)} > + {NumCast(this.Document.currentFrame)} </div> <div key="fwd" className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 0244dfc56..ed70ac9e8 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -334,9 +334,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque _LODdisable: true, title: "a nested collection", }); - // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); - // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; - // this.marqueeInkDelete(inkData); + selected.forEach(d => d.context = newCollection); this.hideMarquee(); return newCollection; } @@ -347,8 +345,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque SelectionManager.DeselectAll(); selected.forEach(d => this.props.removeDocument(d)); const newCollection = Doc.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); - this.props.addDocument(newCollection); - this.props.selectDocuments([newCollection], []); + this.props.addDocument(newCollection!); + this.props.selectDocuments([newCollection!], []); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a4120f958..682aed8f5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -69,19 +69,24 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } public static getValues(doc: Doc, time: number) { + const timecode = Math.round(time); return ({ - x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= time && x !== undefined) || p === undefined ? x : p, undefined as any as number), - y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= time && y !== undefined) || p === undefined ? y : p, undefined as any as number), - opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= time || p === undefined ? o : p, undefined as any as number), + x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), + y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), + opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), }); } - public static setValues(timecode: number, d: Doc, x?: number, y?: number, opacity?: number) { + public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) { + const timecode = Math.round(time); + Cast(d["x-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = x as any as number; + Cast(d["y-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = y as any as number; Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number; - Cast(d["y-indexed"], listSpec("number"), null)[timecode] = y as any as number; + Cast(d["y-indexed"], listSpec("number"), [])[timecode] = y as any as number; Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number; } - public static updateKeyframe(docs: Doc[], timecode: number) { + public static updateKeyframe(docs: Doc[], time: number) { + const timecode = Math.round(time); docs.forEach(doc => { const xindexed = Cast(doc['x-indexed'], listSpec("number"), null); const yindexed = Cast(doc['y-indexed'], listSpec("number"), null); @@ -99,19 +104,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010); } - public static setupKeyframes(docs: Doc[], timecode: number, collection: Doc) { + public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { docs.forEach((doc, i) => { + const curTimecode = progressivize ? i : timecode; const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - xlist[Math.max(i - 1)] = xlist[timecode + 1] = NumCast(doc.x); - ylist[Math.max(i - 1)] = ylist[timecode + 1] = NumCast(doc.y); + const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < i ? 0 : 1)); + xlist[Math.max(curTimecode - 1, 0)] = xlist[curTimecode] = NumCast(doc.x); + ylist[Math.max(curTimecode - 1, 0)] = ylist[curTimecode] = NumCast(doc.y); doc["x-indexed"] = xlist; doc["y-indexed"] = ylist; - doc["opacity-indexed"] = new List<number>(numberRange(timecode).map(i => 1)); - doc.displayTimecode = ComputedField.MakeFunction("collection ? collection.currentTimecode : 0", {}, { collection }); - doc.x = ComputedField.MakeInterpolated("x", "displayTimecode"); - doc.y = ComputedField.MakeInterpolated("y", "displayTimecode"); - doc.opacity = ComputedField.MakeInterpolated("opacity", "displayTimecode"); + doc["opacity-indexed"] = olist; + doc.activeFrame = ComputedField.MakeFunction("self.context ? (self.context.currentFrame||0) : 0"); + doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); + doc.y = ComputedField.MakeInterpolated("y", "activeFrame"); + doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame"); }); } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index f79fe6e78..77e07ec0c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -74,8 +74,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C const clearButton = (which: string) => { return <div className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} onClick={e => this.clearDoc(e, `${which}Doc`)}> <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" /> - </div> - } + </div>; + }; const displayDoc = (which: string) => { const whichDoc = Cast(this.dataDoc[`${which}Doc`], Doc, null); return whichDoc ? <> @@ -84,15 +84,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C </> : // placeholder image if doc is missing <div className="placeholder"> <FontAwesomeIcon className="upload-icon" icon={"cloud-upload-alt"} size="lg" /> - </div> - } + </div>; + }; const displayBox = (which: string, index: number, cover: number) => { return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, `${which}Doc`, index)} > {displayDoc(which)} </div>; - } + }; return ( <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}`}> diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 956d6556b..3cbe3e494 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -99,9 +99,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <div className="keyValuePair-td-key-container"> <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { - props.Document[props.fieldKey] = undefined; + delete props.Document[props.fieldKey]; } - else props.Document.proto![props.fieldKey] = undefined; + else delete props.Document.proto![props.fieldKey]; })}> X </button> diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index aeb77a894..05306c29f 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -59,10 +59,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> next = () => { this.updateCurrentPresentation(); const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); - const lastFrame = Cast(presTargetDoc.lastTimecode, "number", null); - const curFrame = NumCast(presTargetDoc.currentTimecode); + const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); + const curFrame = NumCast(presTargetDoc.currentFrame); if (lastFrame !== undefined && curFrame < lastFrame) { - presTargetDoc.currentTimecode = curFrame + 1; + presTargetDoc.currentFrame = curFrame + 1; } else if (this.childDocs[this.itemIndex + 1] !== undefined) { let nextSelected = this.itemIndex + 1; @@ -199,8 +199,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); - if (presTargetDoc.lastTimecode !== undefined) { - presTargetDoc.currentTimecode = 0; + if (presTargetDoc.lastFrame !== undefined) { + presTargetDoc.currentFrame = 0; } if (!this.layoutDoc.presStatus) { diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 5d4af2d77..29e3c008a 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -6,7 +6,7 @@ import { action, computed, IReactionDisposer, observable, runInAction } from "mo import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface } from "../../../fields/Schema"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { Cast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils"; @@ -18,6 +18,8 @@ import { ViewBoxBaseComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; +import { Doc, WidthSym, HeightSym } from "../../../fields/Doc"; +import { OverlayView } from "../OverlayView"; const path = require('path'); type ScreenshotDocument = makeInterface<[typeof documentSchema]>; @@ -72,7 +74,14 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _width: 150, _height: height / width * 150, title: "--screenshot--" }); - this.props.addDocument?.(imageSummary); + if (!this.props.addDocument || this.props.addDocument === returnFalse) { + const spt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + imageSummary.x = spt[0]; + imageSummary.y = spt[1]; + Cast(Cast(Doc.UserDoc().myOverlayDocuments, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); + } else { + this.props.addDocument?.(imageSummary); + } } }, 500); }); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index f80cea941..a91d4dfd9 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -106,7 +106,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } // else it's an HTMLfield } else if (field?.url) { const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); - this.dataDoc.text = htmlToText.fromString(result.content); + if (result) { + this.dataDoc.text = htmlToText.fromString(result.content); + } } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index b8fbe3420..fc131cd38 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -869,7 +869,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true }); + const link = view.state.schema.mark(view.state.schema.marks.link, { href: Utils.prepend(`/doc/${linkId}`), location: "onRight", title: title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 526a3dbf4..475fef5b2 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -101,12 +101,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc this.rootDoc.presProgressivize = !this.rootDoc.presProgressivize; const rootTarget = Cast(this.rootDoc.presentationTargetDoc, Doc, null); const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]); - if (this.rootDoc.presProgressivize && !rootTarget?.lastTimecode) { - rootTarget.currentTimecode = 0; - CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, this.presBox); - rootTarget.lastTimecode = docs.length - 1; + if (this.rootDoc.presProgressivize) { + rootTarget.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); + rootTarget.lastFrame = docs.length - 1; } - docs.forEach((d, i) => i && numberRange(i).forEach(f => Cast(d["opacity-indexed"], listSpec("number"), [])[f] = 0)); } /** diff --git a/src/client/views/webcam/WebCamLogic.js b/src/client/views/webcam/WebCamLogic.js index c847b8656..a8a2f5fa4 100644 --- a/src/client/views/webcam/WebCamLogic.js +++ b/src/client/views/webcam/WebCamLogic.js @@ -1,5 +1,8 @@ 'use strict'; import io from "socket.io-client"; +import { + resolvedPorts +} from "../Main"; var socket; var isChannelReady = false; @@ -29,7 +32,7 @@ export function initialize(roomName, handlerUI) { room = roomName; - socket = io.connect(`${window.location.protocol}//${window.location.hostname}:${4321}`); + socket = io.connect(`${window.location.protocol}//${window.location.hostname}:${resolvedPorts.socket}`); if (room !== '') { socket.emit('create or join', room); @@ -104,9 +107,9 @@ export function initialize(roomName, handlerUI) { navigator.mediaDevices.getUserMedia({ - audio: true, - video: true - }) + audio: true, + video: true + }) .then(gotStream) .catch(function (e) { alert('getUserMedia() error: ' + e.name); diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index d541c8009..be53c0b9b 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -7,6 +7,7 @@ import { makeInterface } from '../fields/Schema'; import { ObjectField } from '../fields/ObjectField'; import { RefField } from '../fields/RefField'; import { DocServer } from '../client/DocServer'; +import { resolvedPorts } from '../client/views/Main'; @observer class Repl extends React.Component { @@ -61,6 +62,6 @@ class Repl extends React.Component { } (async function () { - DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl"); + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "repl"); ReactDOM.render(<Repl />, document.getElementById("root")); })();
\ No newline at end of file diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index ddddee3be..0ca067ed3 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -14,6 +14,7 @@ import { RichTextField } from '../fields/RichTextField'; import { DateField } from '../fields/DateField'; import { ScriptField } from '../fields/ScriptField'; import CursorField from '../fields/CursorField'; +import { resolvedPorts } from '../client/views/Main'; DateField; URLField; @@ -182,7 +183,7 @@ class Viewer extends React.Component { } (async function () { - await DocServer.init(window.location.protocol, window.location.hostname, 4321, "viewer"); + await DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "viewer"); ReactDOM.render(( <div style={{ position: "absolute", width: "100%", height: "100%" }}> <Viewer /> diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 503c60790..5192af407 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -65,14 +65,14 @@ export class ScriptField extends ObjectField { @serializable(autoObject()) private captures?: ProxyField<Doc>; - constructor(script: CompiledScript, setterscript?: CompileResult) { + constructor(script: CompiledScript, setterscript?: CompiledScript) { super(); if (script?.options.capturedVariables) { const doc = Doc.assign(new Doc, script.options.capturedVariables); this.captures = new ProxyField(doc); } - this.setterscript = setterscript?.compiled ? setterscript : undefined; + this.setterscript = setterscript; this.script = script; } @@ -98,10 +98,10 @@ export class ScriptField extends ObjectField { // } [Copy](): ObjectField { - return new ScriptField(this.script); + return new ScriptField(this.script, this.setterscript); } toString() { - return `${this.script.originalScript}`; + return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } [ToScriptString]() { @@ -141,22 +141,21 @@ export class ComputedField extends ScriptField { [Copy](): ObjectField { - return new ComputedField(this.script); + return new ComputedField(this.script, this.setterscript); } public static MakeScript(script: string, params: object = {}) { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }, setterScript?: string) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); - const setCompiled = setterScript ? ScriptField.CompileScript(setterScript, params, true, capturedVariables) : undefined; - return compiled.compiled ? new ComputedField(compiled, setCompiled?.compiled ? setCompiled : undefined) : undefined; + return compiled.compiled ? new ComputedField(compiled) : undefined; } public static MakeInterpolated(fieldKey: string, interpolatorKey: string) { const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); const setField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}] = value`, { value: "any" }, true, {}); - return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + return getField.compiled && setField.compiled ? new ComputedField(getField, setField) : undefined; } } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 142f7e079..32f1b6e6c 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -12,7 +12,10 @@ export const documentSchema = createSchema({ links: listSpec(Doc), // computed (readonly) list of links associated with this document // "Location" properties in a very general sense - currentTimecode: "number", // current play back time of a temporal document (video / audio) + currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide) + lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) + activeFrame: "number", // the active frame of a frame based animated document + urrentTimecode: "number", // current play back time of a temporal document (video / audio) displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently x: "number", // x coordinate when in a freeform view @@ -77,7 +80,7 @@ export const documentSchema = createSchema({ isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee) lockedPosition: "boolean", // whether the document can be moved (dragged) _lockedTransform: "boolean",// whether a freeformview can pan/zoom - + // drag drop properties dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias", "copy", "move") diff --git a/src/fields/util.ts b/src/fields/util.ts index a287b0210..024c0f80e 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -116,7 +116,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an return true; } } - if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript) { + if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; } return _setter(target, prop, value, receiver); diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 231870531..b15042f9f 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -13,9 +13,7 @@ import { observable } from 'mobx'; import { Utils } from '../Utils'; import MobileInterface from './MobileInterface'; import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; - - - +import { resolvedPorts } from '../client/views/Main'; // const onPointerDown = (e: React.TouchEvent) => { // let imgInput = document.getElementById("input_image_file"); @@ -106,10 +104,10 @@ class Uploader extends React.Component { } -// DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload"); +// DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "image upload"); (async () => { const info = await CurrentUserUtils.loadCurrentUser(); - DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + "mobile"); + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email + "mobile"); await Docs.Prototypes.initialize(); if (info.id !== "__guest__") { // a guest will not have an id registered diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts index 01d2dfcad..c5f3ca717 100644 --- a/src/server/ApiManagers/DownloadManager.ts +++ b/src/server/ApiManagers/DownloadManager.ts @@ -246,7 +246,7 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera if (typeof result === "string") { let path: string; let matches: RegExpExecArray | null; - if ((matches = /\:1050\/files\/images\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) { + if ((matches = /\:\d+\/files\/images\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) { // image already exists on our server path = serverPathToFile(Directory.images, matches[1]); } else { diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index c887aa4e6..2bf4c1956 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -15,7 +15,9 @@ const parse = require('pdf-parse'); import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager'; import { red } from 'colors'; import { Stream } from 'stream'; +import { resolvedPorts } from './server_Initialization'; const requestImageSize = require("../client/util/request-image-size"); +import { resolvedServerUrl } from "./server_Initialization"; export enum SizeSuffix { Small = "_s", @@ -184,7 +186,7 @@ export namespace DashUploadUtils { if (error !== null) { return error; } - source = `http://localhost:1050${clientPathToFile(Directory.images, resolved)}`; + source = `${resolvedServerUrl}${clientPathToFile(Directory.images, resolved)}`; } let resolvedUrl: string; /** @@ -194,14 +196,14 @@ export namespace DashUploadUtils { * basename subtree (i.e. /images/<some_guid>.<ext>) and put it on the end of the server's url. * * This can always be localhost, regardless of whether this is on the server or not, since we (the server, not the client) - * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:1050 refers to the same thing - * as the full dash-release.eastus.cloudapp.azure.com:1050. + * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:<port> refers to the same thing + * as the full dash-release.eastus.cloudapp.azure.com:<port>. */ const matches = isLocal().exec(source); if (matches === null) { resolvedUrl = source; } else { - resolvedUrl = `http://localhost:1050/${matches[1].split("\\").join("/")}`; + resolvedUrl = `${resolvedServerUrl}/${matches[1].split("\\").join("/")}`; } // See header comments: not all image files have exif data (I believe only JPG is the only format that can have it) const exifData = await parseExifData(resolvedUrl); diff --git a/src/server/index.ts b/src/server/index.ts index ff74cfb33..590affd06 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { Database } from './database'; import { DashUploadUtils } from './DashUploadUtils'; import RouteSubscriber from './RouteSubscriber'; -import initializeServer from './server_Initialization'; +import initializeServer, { resolvedPorts } from './server_Initialization'; import RouteManager, { Method, _success, _permission_denied, _error, _invalid, PublicHandler } from './RouteManager'; import * as qs from 'query-string'; import UtilManager from './ApiManagers/UtilManager'; @@ -95,6 +95,11 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: secureHandler: ({ res }) => res.send(true) }); + addSupervisedRoute({ + method: Method.GET, + subscription: "/resolvedPorts", + secureHandler: ({ res }) => res.send(resolvedPorts) + }); const serve: PublicHandler = ({ req, res }) => { const detector = new mobileDetect(req.headers['user-agent'] || ""); diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 91a3cb6bf..7178add93 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -1,4 +1,5 @@ import { Database } from "./database"; +import { resolvedPorts } from "./server_Initialization"; //npx ts-node src/server/remapUrl.ts @@ -34,7 +35,7 @@ async function update() { if (url.href.includes("localhost") && url.href.includes("Bill")) { dynfield = true; - update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:1050${url.pathname}` }; + update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:${resolvedPorts.server}${url.pathname}` }; } } } diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index d370385b2..744d4547b 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -20,8 +20,8 @@ import * as fs from 'fs'; import * as request from 'request'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory } from '.'; -import { logPort, pathFromRoot, } from './ActionUtilities'; -import { blue, yellow, red } from 'colors'; +import { logPort } from './ActionUtilities'; +import { blue, yellow } from 'colors'; import * as cors from "cors"; import { createServer, Server as HttpsServer } from "https"; import { Server as HttpServer } from "http"; @@ -32,6 +32,9 @@ import { SSL } from './apis/google/CredentialsLoader'; export type RouteSetter = (server: RouteManager) => void; export let disconnect: Function; +export let resolvedPorts: { server: number, socket: number } = { server: 1050, socket: 4321 }; +export let resolvedServerUrl: string; + export default async function InitializeServer(routeSetter: RouteSetter) { const app = buildWithMiddleware(express()); @@ -55,17 +58,19 @@ export default async function InitializeServer(routeSetter: RouteSetter) { registerRelativePath(app); let server: HttpServer | HttpsServer; - const { serverPort } = process.env; - const resolved = isRelease && serverPort ? Number(serverPort) : 1050; + const { serverPort, serverName } = process.env; + isRelease && serverPort && (resolvedPorts.server = Number(serverPort)); await new Promise<void>(resolve => server = isRelease ? - createServer(SSL.Credentials, app).listen(resolved, resolve) : - app.listen(resolved, resolve) + createServer(SSL.Credentials, app).listen(resolvedPorts.server, resolve) : + app.listen(resolvedPorts.server, resolve) ); - logPort("server", resolved); + logPort("server", resolvedPorts.server); + + resolvedServerUrl = `${isRelease && serverName ? `https://${serverName}.com` : "http://localhost"}:${resolvedPorts.server}`; // initialize the web socket (bidirectional communication: if a user changes // a field on one client, that change must be broadcast to all other clients) - WebSocket.initialize(isRelease, app); + await WebSocket.initialize(isRelease, app); disconnect = async () => new Promise<Error>(resolve => server.close(resolve)); return isRelease; @@ -174,11 +179,11 @@ function registerRelativePath(server: express.Express) { server.use("*", (req, res) => { const relativeUrl = req.originalUrl; if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here. - const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:1050/corsProxy/https://en.wikipedia.org/wiki/Engelbart) - const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:1050/corsProxy/ ) + const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart) + const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ ) const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart) const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org) - const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:1050/corsProxy/https://en.wikipedia.org/<somethingelse>) + const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>) res.redirect(redirectedProxiedUrl); } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl)); diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 21e772e83..d55c2e198 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -16,6 +16,7 @@ import executeImport from "../scraping/buxton/final/BuxtonImporter"; import { DocumentsCollection } from "./IDatabase"; import { createServer, Server } from "https"; import * as express from "express"; +import { resolvedPorts } from './server_Initialization'; export namespace WebSocket { @@ -23,21 +24,21 @@ export namespace WebSocket { const clients: { [key: string]: Client } = {}; export const socketMap = new Map<SocketIO.Socket, string>(); export let disconnect: Function; - const defaultPort = 4321; export async function initialize(isRelease: boolean, app: express.Express) { let io: sio.Server; - let resolved: number; if (isRelease) { const { socketPort } = process.env; - resolved = socketPort ? Number(socketPort) : defaultPort; + if (socketPort) { + resolvedPorts.socket = Number(socketPort); + } let socketEndpoint: Server; - await new Promise<void>(resolve => socketEndpoint = createServer(SSL.Credentials, app).listen(resolved, resolve)); + await new Promise<void>(resolve => socketEndpoint = createServer(SSL.Credentials, app).listen(resolvedPorts.socket, resolve)); io = sio(socketEndpoint!, SSL.Credentials as any); } else { - io = sio().listen(resolved = defaultPort); + io = sio().listen(resolvedPorts.socket); } - logPort("websocket", resolved); + logPort("websocket", resolvedPorts.socket); console.log(); io.on("connection", function (socket: Socket) { diff --git a/test/test.ts b/test/test.ts index 5fc156b46..9dcd273af 100644 --- a/test/test.ts +++ b/test/test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import 'mocha'; const { JSDOM } = require('jsdom'); const dom = new JSDOM("", { - url: "http://localhost:1050" + url: `http://localhost:${resolvedPorts.server}` }); (global as any).window = dom.window; @@ -12,6 +12,7 @@ import { Doc } from '../src/fields/Doc'; import { Cast } from '../src/fields/Types'; import { createSchema, makeInterface, defaultSpec } from '../src/fields/Schema'; import { ImageField } from '../src/fields/URLField'; +import { resolvedPorts } from '../src/client/views/Main'; describe("Document", () => { it('should hold fields', () => { const key = "Test"; |