diff options
Diffstat (limited to 'src')
39 files changed, 372 insertions, 314 deletions
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 b4c11a81b..7f5b62f22 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -110,6 +110,7 @@ export interface DocumentOptions { _backgroundColor?: string | ScriptField; // background color for each template layout doc ( overrides backgroundColor ) color?: string; // foreground color data doc _color?: string; // foreground color for each template layout doc (overrides color) + _clipWidth?: number; // percent transition from before to after in comparisonBox caption?: RichTextField; ignoreClick?: boolean; lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged @@ -125,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; @@ -559,7 +563,7 @@ export namespace Docs { } export function ComparisonDocument(options: DocumentOptions = { title: "Comparison Box" }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", { targetDropAction: "alias", ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", { _clipWidth: 50, _backgroundColor: "gray", targetDropAction: "alias", ...options }); } export function AudioDocument(url: string, options: DocumentOptions = {}) { 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 df75cc291..958ba0ce3 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -5,7 +5,7 @@ import { faQuestionCircle, faArrowLeft, faArrowRight, 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 + faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; @@ -118,7 +118,7 @@ export class MainView extends React.Component { faQuestionCircle, faArrowLeft, faArrowRight, 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, faTrashAlt, faAngleRight, faBell, - faThumbtack, faTree, faTv, faUndoAlt, faVideo); + faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -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/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 1e3bf11de..cc7a9f5ac 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -240,13 +240,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr toggle: this.toggleVisibility, color: this.color }; + const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled'); + const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; return this.collapsed ? (null) : <div style={{ position: "relative" }}> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + {showChrome ? <div className="collectionStackingView-addDocumentButton" style={{ - width: style.columnWidth / style.numGroupColumns, - padding: NumCast(this.props.parent.layoutDoc._yPadding) + //width: style.columnWidth / style.numGroupColumns, + padding: `${NumCast(this.props.parent.layoutDoc._yPadding, this.props.parent.yMargin)}px 0px 0px 0px` }}> <EditableView {...newEditableViewProps} /> </div> : null @@ -254,7 +256,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr <div className={`collectionStackingView-masonryGrid`} ref={this._contRef} style={{ - padding: `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px`, + padding: stackPad, width: this.props.parent.NodeWidth, gridGap: this.props.parent.gridGap, gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""), diff --git a/src/client/views/collections/CollectionPileView.scss b/src/client/views/collections/CollectionPileView.scss index ac874b663..48d07e42b 100644 --- a/src/client/views/collections/CollectionPileView.scss +++ b/src/client/views/collections/CollectionPileView.scss @@ -5,4 +5,7 @@ height: 100%; width: 100%; overflow: visible; + .collectionPileView-innards { + width:100%; + } } diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index e3bcf2a21..020a87b2e 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,18 +1,17 @@ import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { HeightSym, Opt, WidthSym, Doc } from "../../../fields/Doc"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, NumCast, StrCast } from "../../../fields/Types"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionPileView.scss"; import React = require("react"); import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; -import { UndoManager } from "../../util/UndoManager"; +import { UndoManager, undoBatch } from "../../util/UndoManager"; import { SnappingManager } from "../../util/SnappingManager"; +import { DragManager } from "../../util/DragManager"; @observer export class CollectionPileView extends CollectionSubView(doc => doc) { @@ -20,10 +19,12 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { _doubleTap: boolean | undefined = false; _originalChrome: string = ""; @observable _contentsActive = true; - @observable _layoutEngine = "pass"; @observable _collapsed: boolean = false; @observable _childClickedScript: Opt<ScriptField>; componentDidMount() { + if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { + this.Document._layoutEngine = "pass"; + } this._originalChrome = StrCast(this.layoutDoc._chromeStatus); this.layoutDoc._chromeStatus = "disabled"; this.layoutDoc.hideFilterView = true; @@ -33,49 +34,54 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { this.layoutDoc._chromeStatus = this._originalChrome; } - layoutEngine = () => this._layoutEngine; + layoutEngine = () => StrCast(this.Document._layoutEngine); @computed get contents() { - return <div className="collectionPileView-innards" style={{ - width: "100%", - pointerEvents: this.layoutEngine() !== "pass" && (this.props.active() || this.layoutEngine() === "starburst") ? undefined : "none" - }} > + return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} > <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} /> </div>; } - - specificMenu = (e: React.MouseEvent) => { - const layoutItems: ContextMenuProps[] = []; - const doc = this.props.Document; - - ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); - } - toggleStarburst = action(() => { - if (this._layoutEngine === 'starburst') { + if (this.layoutEngine() === 'starburst') { const defaultSize = 110; this.layoutDoc._overflow = undefined; + this.childDocs.forEach(d => Doc.iconify(d)); this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2; this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize); this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize); - this._layoutEngine = 'pass'; + Doc.pileup(this.childDocs); + this.layoutDoc._panX = 0; + this.layoutDoc._panY = -10; + this.props.Document._layoutEngine = 'pass'; } else { const defaultSize = 25; this.layoutDoc._overflow = 'visible'; !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 500); !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5); - if (this._layoutEngine === 'pass') { + if (this.layoutEngine() === 'pass') { this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2; this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym](); this.layoutDoc._starburstPileHeight = this.layoutDoc[HeightSym](); } + this.layoutDoc._panX = this.layoutDoc._panY = 0; this.layoutDoc._width = this.layoutDoc._height = defaultSize; - this._layoutEngine = 'starburst'; + this.props.Document._layoutEngine = 'starburst'; } }); + @undoBatch + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (super.onInternalDrop(e, de)) { + if (de.complete.docDragData) { + Doc.pileup(this.childDocs); + } + } + return true; + } + _undoBatch: UndoManager.Batch | undefined; pointerDown = (e: React.PointerEvent) => { let dist = 0; @@ -107,20 +113,17 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } onClick = (e: React.MouseEvent) => { - if (e.button === 0 && (this._doubleTap || this.layoutEngine() === "starburst")) { + if (e.button === 0 && this._doubleTap) { SelectionManager.DeselectAll(); this.toggleStarburst(); e.stopPropagation(); } - // else if (this.layoutEngine() === "pass") { - // runInAction(() => this._contentsActive = false); - // setTimeout(action(() => this._contentsActive = true), 300); - // } } render() { - return <div className={"collectionPileView"} onContextMenu={this.specificMenu} onClick={this.onClick} onPointerDown={this.pointerDown} + return <div className={"collectionPileView"} onClick={this.onClick} onPointerDown={this.pointerDown} + ref={this.createDashEventsTarget} style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> {this.contents} </div>; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 53acc15c3..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); @@ -451,7 +452,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (generatedDocuments.length) { const set = generatedDocuments.length > 1 && generatedDocuments.map(d => Doc.iconify(d)); if (set) { - addDocument(Doc.pileup(generatedDocuments, options.x!, options.y!)); + addDocument(Doc.pileup(generatedDocuments, options.x!, options.y!)!); } else { generatedDocuments.forEach(addDocument); } 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 0b61aad8a..0639d74a8 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,8 +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) { - CollectionFreeFormDocumentView.setValues(this.Document.currentTimecode, d, x + NumCast(d.x) - dropX, y + NumCast(d.y) - dropY, Cast(d.opacity, "number", null)); + 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 +1007,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: "" }; } @@ -1363,13 +1391,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> - {this.isAnnotationOverlay || !this.props.isSelected() ? (null) : + {this.isAnnotationOverlay || !this.props.isSelected() || this.props.Document._viewType === CollectionViewType.Pile ? (null) : <> <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 57f484214..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,18 +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) { - docs.forEach(doc => { - doc["x-indexed"] = new List<number>(numberRange(timecode).map(i => undefined) as any as number[]); - doc["y-indexed"] = new List<number>(numberRange(timecode).map(i => undefined) as any as number[]); - doc["opacity-indexed"] = new List<number>(numberRange(timecode).map(i => 0)); - (doc["x-indexed"] as any).push(NumCast(doc.x)); - (doc["y-indexed"] as any).push(NumCast(doc.y)); - (doc["opacity-indexed"] as any).push(NumCast(doc.opacity, 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"); + 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[]); + 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"] = 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.scss b/src/client/views/nodes/ComparisonBox.scss index 3d48d96e2..7849c9976 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -2,7 +2,6 @@ border-radius: inherit; width: 100%; height: 100%; - background-color: grey; position: absolute; z-index: 0; pointer-events: none; @@ -17,7 +16,6 @@ .beforeBox-cont { height: 100%; overflow: hidden; - background-color: rgb(240, 240, 240); } } @@ -26,7 +24,7 @@ height: 100%; width: 3px; display: inline-block; - background: gray; + background: white; cursor: ew-resize; .slide-handle { position: absolute; @@ -48,7 +46,6 @@ height: 100%; width: 100%; overflow: hidden; - background-color: lightgray; } .clear-button { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 7a4d40db1..77e07ec0c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,13 +1,10 @@ -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 { action, computed, observable, runInAction, Lambda, IReactionDisposer } from 'mobx'; import { observer } from "mobx-react"; -import { Doc } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { createSchema, makeInterface } from '../../../fields/Schema'; -import { NumCast, Cast } from '../../../fields/Types'; +import { NumCast, Cast, StrCast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; @@ -17,9 +14,7 @@ import { ContentFittingDocumentView } from './ContentFittingDocumentView'; import { undoBatch } from '../../util/UndoManager'; import { setupMoveUpEvents, emptyFunction } from '../../../Utils'; import { SnappingManager } from '../../util/SnappingManager'; - -library.add(faImage, faEye as any, faPaintBrush, faBrain); -library.add(faFileAudio, faAsterisk); +import { DocumentViewProps } from './DocumentView'; export const comparisonSchema = createSchema({}); @@ -28,17 +23,16 @@ const ComparisonDocument = makeInterface(comparisonSchema, 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); } + protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; + private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; - private _beforeDropDisposer?: DragManager.DragDropDisposer; - private _afterDropDisposer?: DragManager.DragDropDisposer; - private resizeUpdater: Lambda | undefined; + @observable _animating = ""; - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { + this._disposers[disposerId]?.(); if (ele) { - return DragManager.MakeDropTarget(ele, (event, dropEvent) => this.dropHandler(event, dropEvent, fieldKey), this.layoutDoc); + this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc); } } @@ -48,36 +42,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; if (droppedDocs?.length) { this.dataDoc[fieldKey] = droppedDocs[0]; - droppedDocs[0].isBackgound = true; } } - componentWillMount() { - this.dataDoc.clipWidth = this.props.PanelWidth() / 2; - - //preserve before/after ratio during resizing - this.resizeUpdater = computed(() => this.props.PanelWidth()).observe(({ oldValue, newValue }) => - this.dataDoc.clipWidth = NumCast(this.dataDoc.clipWidth) / (oldValue || 0) * newValue - ); - } - - componentWillUnmount() { - this.resizeUpdater?.(); - } - private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { - this._animating = true; - this.dataDoc.clipWidth = targetWidth; - setTimeout(action(() => this._animating = false), 1000); + this._animating = "all 1s"; + this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); + setTimeout(action(() => this._animating = ""), 1000); }), false); } @action private onPointerMove = ({ movementX }: PointerEvent) => { - const width = movementX * this.props.ScreenToLocalTransform().Scale + NumCast(this.dataDoc.clipWidth); + const width = movementX * this.props.ScreenToLocalTransform().Scale + NumCast(this.layoutDoc._clipWidth) / 100 * this.props.PanelWidth(); if (width && width > 5 && width < this.props.PanelWidth()) { - this.dataDoc.clipWidth = width; + this.layoutDoc._clipWidth = width * 100 / this.props.PanelWidth(); } return false; } @@ -85,66 +65,43 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C @undoBatch clearDoc = (e: React.MouseEvent, fieldKey: string) => { e.stopPropagation; - e.preventDefault; delete this.dataDoc[fieldKey]; } - @observable _animating = false; render() { - const beforeDoc = Cast(this.dataDoc.beforeDoc, Doc, null); - const afterDoc = Cast(this.dataDoc.afterDoc, Doc, null); - const clipWidth = NumCast(this.dataDoc.clipWidth); + const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%"; + const childProps: DocumentViewProps = { ...this.props, pointerEvents: false, parentActive: this.props.active }; + 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>; + }; + const displayDoc = (which: string) => { + const whichDoc = Cast(this.dataDoc[`${which}Doc`], Doc, null); + return whichDoc ? <> + <ContentFittingDocumentView {...childProps} Document={whichDoc} /> + {clearButton(which)} + </> : // placeholder image if doc is missing + <div className="placeholder"> + <FontAwesomeIcon className="upload-icon" icon={"cloud-upload-alt"} size="lg" /> + </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" : ""}`}> - <div className="afterBox-cont" key={"after"} onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() - 5)} - ref={(ele) => { - this._afterDropDisposer?.(); - this._afterDropDisposer = this.createDropTarget(ele, "afterDoc"); - }}> - {afterDoc ? <> - <ContentFittingDocumentView {...this.props} - Document={afterDoc} - pointerEvents={false} - parentActive={this.props.active} - /> - <div className="clear-button after" onClick={e => this.clearDoc(e, "afterDoc")}> - <FontAwesomeIcon className="clear-button after" icon={faTimes} size="sm" /> - </div> - </> : - <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon={faCloudUploadAlt} size="lg" /> - </div>} - </div> - <div className="clip-div" onPointerDown={e => this.registerSliding(e, 5)} style={{ width: clipWidth + "px", transition: this._animating ? "all 1s" : undefined }}> - {/* wraps around before image and slider bar */} - <div - className="beforeBox-cont" - key={"before"} - ref={(ele) => { - this._beforeDropDisposer?.(); - this._beforeDropDisposer = this.createDropTarget(ele, "beforeDoc"); - }} - style={{ width: this.props.PanelWidth() }}> - { - beforeDoc ? - <> - <ContentFittingDocumentView {...this.props} - Document={beforeDoc} - pointerEvents={false} - parentActive={this.props.active} /> - <div className="clear-button before" onClick={e => this.clearDoc(e, "beforeDoc")}> - <FontAwesomeIcon className="clear-button before" icon={faTimes} size="sm" /> - </div> - </> - : - <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon={faCloudUploadAlt} size="lg" /> - </div> - } - </div> + {displayBox("after", 1, this.props.PanelWidth() - 5)} + <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> + {displayBox("before", 0, 5)} </div> - <div className="slide-bar" style={{ left: `calc(${NumCast(this.dataDoc.clipWidth) * 100 / this.props.PanelWidth()}% - 0.5px)` }}> + <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }}> <div className="slide-handle" /> </div> </div >); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 993cabc36..00d19752f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1111,6 +1111,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }), 400); }); + renderLock() { + return (this.Document.isBackground !== undefined || this.isSelected(false)) && + ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) && + this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? + <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> + <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> + </div> + : (null); + } + render() { if (!(this.props.Document instanceof Doc)) return (null); const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); @@ -1161,11 +1171,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <div className="documentView-contentBlocker" /> </> : this.innards} - {(this.Document.isBackground !== undefined || this.isSelected(false)) && (this.Document.type === DocumentType.COL || this.Document.type === DocumentType.IMG) && this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? - <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> - <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> - </div> - : (null)} + {this.renderLock()} </div>; { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; } } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 77abfef1d..6913dfbc7 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -266,7 +266,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) { requestImageSize(imgPath).then(action((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 rotatedNativeSize = { width: inquiredSize.width, height: inquiredSize.height }; + if (inquiredSize.orientation === 6 || rotation === 90 || rotation === 270) { + rotatedNativeSize.width = inquiredSize.height; + rotatedNativeSize.height = inquiredSize.width; + } const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; 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/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 2f7d23021..0e3e7f91e 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -154,15 +154,12 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const originalDoc = layoutDoc.rootDocument || layoutDoc; if (originalDoc instanceof Doc) { const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Docs.Create.TextDocument("", { - layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, - layoutKey, - _singleLine: BoolCast(originalDoc._singleLine), - x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) - }); + const newDoc = Doc.MakeCopy(originalDoc, true); + newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; } + Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); } @@ -178,15 +175,12 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const originalDoc = layoutDoc.rootDocument || layoutDoc; if (force || props.Document._singleLine) { const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Docs.Create.TextDocument("", { - layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, - layoutKey, - _singleLine: BoolCast(originalDoc._singleLine), - x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) - }); + const newDoc = Doc.MakeCopy(originalDoc, true); + newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; } + Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); return true; diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index a2a6882b9..475fef5b2 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -100,12 +100,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); this.rootDoc.presProgressivize = !this.rootDoc.presProgressivize; const rootTarget = Cast(this.rootDoc.presentationTargetDoc, Doc, null); - if (this.rootDoc.presProgressivize && !rootTarget?.lastTimecode) { - const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]); - rootTarget.currentTimecode = 0; - CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, this.presBox); - docs.forEach((d, i) => numberRange(docs.length - i).forEach(f => Cast(d["opacity-indexed"], listSpec("number"), [])[f + i] = 1)); - rootTarget.lastTimecode = docs.length - 1; + const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]); + if (this.rootDoc.presProgressivize) { + rootTarget.currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); + rootTarget.lastFrame = docs.length - 1; } } @@ -216,7 +215,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc <strong className="presElementBox-name"> {`${this.indexInPres + 1}. ${this.targetDoc?.title}`} </strong> - <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument?.(this.rootDoc)}>X</button> + <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => { + this.props.removeDocument?.(this.rootDoc); + e.stopPropagation(); + }}>X</button> <br /> </>} <div className="presElementBox-buttons"> 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/Doc.ts b/src/fields/Doc.ts index 9f38f4369..1ea686cbb 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1028,28 +1028,32 @@ export namespace Doc { if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", ""); } - export function pileup(selected: Doc[], x: number, y: number) { - const newCollection = Docs.Create.PileDocument(selected, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true }); + export function pileup(docList: Doc[], x?: number, y?: number) { let w = 0, h = 0; - selected.forEach((d, i) => { - Doc.iconify(d); - w = Math.max(d[WidthSym](), w); - h = Math.max(d[HeightSym](), h); - }); - h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away. so this is a fallback hack to try to do something reasonable - selected.forEach((d, i) => { - d.x = Math.cos(Math.PI * 2 * i / selected.length) * 10 - w / 2; - d.y = Math.sin(Math.PI * 2 * i / selected.length) * 10 - h / 2; - d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + runInAction(() => { + docList.forEach(d => { + Doc.iconify(d); + w = Math.max(d[WidthSym](), w); + h = Math.max(d[HeightSym](), h); + }); + h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away. so this is a fallback hack to try to do something reasonable + docList.forEach((d, i) => { + d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2; + d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2; + d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + }); }); - newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; - newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; - newCollection._width = newCollection._height = 110; - //newCollection.borderRounding = "40px"; - newCollection._jitterRotation = 10; - newCollection._backgroundColor = "gray"; - newCollection._overflow = "visible"; - return newCollection; + if (x !== undefined && y !== undefined) { + const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true }); + newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; + newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; + newCollection._width = newCollection._height = 110; + //newCollection.borderRounding = "40px"; + newCollection._jitterRotation = 10; + newCollection._backgroundColor = "gray"; + newCollection._overflow = "visible"; + return newCollection; + } } 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) { |