diff options
Diffstat (limited to 'src')
24 files changed, 1992 insertions, 1065 deletions
diff --git a/src/client/Network.ts b/src/client/Network.ts index bf2918734..1255e5ce0 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -4,46 +4,47 @@ import { Upload } from "../server/SharedMediaTypes"; export namespace Networking { - export async function FetchFromServer(relativeRoute: string) { - return (await fetch(relativeRoute)).text(); - } + export async function FetchFromServer(relativeRoute: string) { + return (await fetch(relativeRoute)).text(); + } - export async function PostToServer(relativeRoute: string, body?: any) { - const options = { - uri: Utils.prepend(relativeRoute), - method: "POST", - body, - json: true - }; - return requestPromise.post(options); - } + export async function PostToServer(relativeRoute: string, body?: any) { + const options = { + uri: Utils.prepend(relativeRoute), + method: "POST", + body, + json: true + }; + return requestPromise.post(options); + } - export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> { - const formData = new FormData(); - if (Array.isArray(files)) { - if (!files.length) { - return []; + export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> { + console.log(files) + const formData = new FormData(); + if (Array.isArray(files)) { + if (!files.length) { + return []; + } + files.forEach(file => formData.append(Utils.GenerateGuid(), file)); + } else { + formData.append(Utils.GenerateGuid(), files); } - files.forEach(file => formData.append(Utils.GenerateGuid(), file)); - } else { - formData.append(Utils.GenerateGuid(), files); - } - const parameters = { - method: 'POST', - body: formData - }; - const response = await fetch("/uploadFormData", parameters); - return response.json(); - } + const parameters = { + method: 'POST', + body: formData + }; + const response = await fetch("/uploadFormData", parameters); + return response.json(); + } - export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> { - const parameters = { - method: 'POST', - body: JSON.stringify({ videoId }), - json: true - }; - const response = await fetch("/uploadYoutubeVideo", parameters); - return response.json(); - } + export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> { + const parameters = { + method: 'POST', + body: JSON.stringify({ videoId }), + json: true + }; + const response = await fetch("/uploadYoutubeVideo", parameters); + return response.json(); + } }
\ No newline at end of file diff --git a/src/client/apis/recording/recordingApi.tsx b/src/client/apis/recording/recordingApi.tsx new file mode 100644 index 000000000..55714f03b --- /dev/null +++ b/src/client/apis/recording/recordingApi.tsx @@ -0,0 +1,151 @@ +import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm"; +import React, { useState } from "react"; + +export function RecordingApi() { + + type Movement = { + time: number, + panX: number, + panY: number, + } + + type Presentation = { + movements: Array<Movement> + meta: Object, + startDate: Date | null, + } + + const NULL_PRESENTATION = { + movements: [], + meta: {}, + startDate: null, + } + + const [currentPresentation, setCurrentPresenation] = useState<Presentation>(NULL_PRESENTATION) + const [isRecording, setIsRecording] = useState(false) + const [absoluteStart, setAbsoluteStart] = useState<number>(-1) + + const initAndStart = (view: CollectionFreeFormView, meta?: Object): Error | undefined => { + // check if already init a presentation + if (currentPresentation.startDate !== null) { + console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.') + return new Error('[recordingApi.ts] start()') + } + + // (1a) get start date for presenation + const startDate = new Date() + // (1b) set start timestamp to absolute timestamp + setAbsoluteStart(startDate.getTime()) + + // TODO: (2) assign meta content + + // (3) assign init values to currentPresenation + setCurrentPresenation({ ...currentPresentation, startDate }) + + // (4) set isRecording true to allow trackMovements + setIsRecording(true) + } + + const clear = (): Error | undefined => { + // TODO: maybe archive the data? + if (isRecording) { + console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() or finish() first') + return new Error('[recordingApi.ts] clear()') + } + // clear presenation data + setCurrentPresenation(NULL_PRESENTATION) + + // clear isRecording + setIsRecording(false) + + // clear absoluteStart + setAbsoluteStart(-1) + } + + const pause = (): Error | undefined => { + if (currentPresentation.startDate === null) { + console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first') + return new Error('[recordingApi.ts] pause(): no presenation') + } + // don't allow track movments + setIsRecording(false) + + // set relativeStart to the pausedTimestamp + const timestamp = new Date().getTime() + setAbsoluteStart(timestamp) + } + + const resume = () => { + if (currentPresentation.startDate === null) { + console.error('[recordingApi.ts] resume() failed: no presentation started. try calling init() first') + return new Error('[recordingApi.ts] resume()') + } + + const timestamp = new Date().getTime() + const startTimestamp = currentPresentation.startDate?.getTime() + if (!startTimestamp) { + console.error('[recordingApi.ts] resume() failed: no presentation data. try calling init() first') + return new Error('[recordingApi.ts] pause()') + } + + setAbsoluteStart(prevTime => { + // const relativeUnpause = timestamp - absoluteStart + // const timePaused = relativeUnpause - prevTime + // return timePaused + absoluteStart + const absoluteTimePaused = timestamp - prevTime + return absoluteTimePaused + }) + } + + const finish = (): Error | Presentation => { + if (currentPresentation.movements === null) { + console.error('[recordingApi.ts] finish() failed: no presentation data. try calling init() first') + return new Error('[recordingApi.ts] finish()') + } + + // make copy and clear this class's data + const returnCopy = { ...currentPresentation } + clear() + + // return the copy + return returnCopy + } + + const trackMovements = (panX: number, panY: number): Error | undefined => { + // ensure we are recording + if (!isRecording) { + console.error('[recordingApi.ts] pause() failed: recording is paused()') + return new Error('[recordingApi.ts] pause()') + } + + // get the relative time + const timestamp = new Date().getTime() + const relativeTime = timestamp - absoluteStart + + // make new movement struct + const movement: Movement = { time: relativeTime, panX, panY } + + // add that movement struct to the current presentation data + setCurrentPresenation(prevPres => { + const movements = [...prevPres.movements, movement] + return {...prevPres, movements} + }) + } + + // TOOD: need to pause all intervals if possible lol + // TODO: extract this into different class with pause and resume recording + const followMovements = (presentation: Presentation, docView: CollectionFreeFormView): void => { + const document = docView.Document + + const { movements } = presentation + movements.forEach(movement => { + const { panX, panY, time } = movement + // set the pan to what was stored + setTimeout(() => { + document._panX = panX; + document._panY = panY; + }, time) + }) + } + +}
\ No newline at end of file diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 161dff6e0..ca942a38a 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -9,6 +9,7 @@ export enum DocumentType { KVP = "kvp", VID = "video", AUDIO = "audio", + REC = "recording", PDF = "pdf", INK = "inks", SCREENSHOT = "screenshot", diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index bb60586eb..e50172af5 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -11,7 +11,7 @@ import { RichTextField } from "../../fields/RichTextField"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../fields/Types"; -import { AudioField, ImageField, MapField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; +import { AudioField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; import { SharingPermissions } from "../../fields/util"; import { Upload } from "../../server/SharedMediaTypes"; import { OmitKeys, Utils } from "../../Utils"; @@ -61,6 +61,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { MapBox } from "../views/nodes/MapBox/MapBox"; +import { RecordingBox } from "../views/nodes/RecordingBox/RecordingBox"; const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); class EmptyBox { @@ -401,6 +402,10 @@ export namespace Docs { layout: { view: AudioBox, dataField: defaultDataKey }, options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" } }], + [DocumentType.REC, { + layout: { view: VideoBox, dataField: defaultDataKey }, + options: { _height: 100, backgroundColor: "pink", links: "@links(self)" } + }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } @@ -468,7 +473,7 @@ export namespace Docs { options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: "@links(self)" }, }], [DocumentType.WEBCAM, { - layout: { view: DashWebRTCVideo, dataField: defaultDataKey }, + layout: { view: RecordingBox, dataField: defaultDataKey }, options: { links: "@links(self)" } }], [DocumentType.PRESELEMENT, { @@ -699,6 +704,10 @@ export namespace Docs { { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); } + export function RecordingDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.REC), "", options); + } + export function SearchDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List<Doc>([]), options); } @@ -1160,6 +1169,9 @@ export namespace DocUtils { } else if (field instanceof AudioField) { created = Docs.Create.AudioDocument((field).url.href, resolved); layout = AudioBox.LayoutString; + } else if (field instanceof RecordingField) { + created = Docs.Create.RecordingDocument((field).url.href, resolved); + layout = RecordingBox.LayoutString; } else if (field instanceof InkField) { created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved); layout = InkingStroke.LayoutString; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index af731ce9f..edb362a37 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -424,8 +424,8 @@ export class CurrentUserUtils { doc.emptyScreenshot = Docs.Create.ScreenshotDocument("empty screenshot", { _fitWidth: true, title: "empty screenshot", _width: 400, _height: 200, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyWall === undefined) { - doc.emptyWall = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); - (doc.emptyWall as Doc).videoWall = true; + doc.emptyWall = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); + (doc.emptyWall as Doc).recording = true; } if (doc.emptyAudio === undefined) { doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 411fc6d11..47b7ce0e7 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -211,6 +211,10 @@ export namespace DragManager { options?: DragOptions, dropEvent?: () => any ) { + // stop an 'accidental' on-click drag that may have occured if the user is in presenting mode + // note: dragData.dropAction is only undefined when the element itself being dragged without being selected + if (Doc.UserDoc()?.presentationMode === 'recording' && dragData.dropAction === undefined) return false; + const addAudioTag = (dropDoc: any) => { dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField); dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 15cd2c144..3799b6b13 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -29,6 +29,11 @@ left: calc(100% + 5px); z-index: 1; pointer-events: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 10px; } .mainView-snapLines { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index a67cb3014..609af568e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -540,6 +540,7 @@ export class MainView extends React.Component { searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> + {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{this.userDoc?.presentationMode}</div>: <></>} </div>; } @computed get snapLines() { diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index 555f4298d..d6a6e4301 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -1,49 +1,59 @@ +.overlayView { + position: absolute; + pointer-events: none; + top: 0; + width: 100vw; + height: 100vh; + /* background-color: pink; */ + z-index: 100000; +} + .overlayWindow-outerDiv { - border-radius: 5px; - overflow: hidden; - display: flex; - flex-direction: column; - top: 0; - left: 0; + border-radius: 5px; + overflow: hidden; + display: flex; + flex-direction: column; + top: 0; + left: 0; } .overlayWindow-outerDiv, .overlayView-wrapperDiv { - position: absolute; - z-index: 1; + position: absolute; + z-index: 1; } .overlayWindow-titleBar { - flex: 0 1 30px; - background: darkslategray; - color: whitesmoke; - text-align: center; - cursor: move; + flex: 0 1 30px; + background: darkslategray; + color: whitesmoke; + text-align: center; + cursor: move; } .overlayWindow-content { - flex: 1 1 auto; - display: flex; - flex-direction: column; + flex: 1 1 auto; + display: flex; + flex-direction: column; } .overlayWindow-closeButton { - float: right; - height: 30px; - width: 30px; + float: right; + height: 30px; + width: 30px; } .overlayWindow-resizeDragger { - background-color: rgb(0, 0, 0); - position: absolute; - right: 0px; - bottom: 0px; - width: 10px; - height: 10px; - cursor: nwse-resize; + background-color: rgb(0, 0, 0); + position: absolute; + right: 0px; + bottom: 0px; + width: 10px; + height: 10px; + cursor: nwse-resize; } .overlayView-doc { - z-index: 9002; //so that it appears above chroma - position: absolute; + z-index: 9002; //so that it appears above chroma + position: absolute; }
\ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 0f51cf9b2..806abb990 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -20,212 +20,213 @@ import { DefaultStyleProvider } from "./StyleProvider"; export type OverlayDisposer = () => void; export type OverlayElementOptions = { - x: number; - y: number; - width?: number; - height?: number; - title?: string; + x: number; + y: number; + width?: number; + height?: number; + title?: string; }; export interface OverlayWindowProps { - children: JSX.Element; - overlayOptions: OverlayElementOptions; - onClick: () => void; + children: JSX.Element; + overlayOptions: OverlayElementOptions; + onClick: () => void; } @observer export class OverlayWindow extends React.Component<OverlayWindowProps> { - @observable x: number; - @observable y: number; - @observable width: number; - @observable height: number; - constructor(props: OverlayWindowProps) { - super(props); - - const opts = props.overlayOptions; - this.x = opts.x; - this.y = opts.y; - this.width = opts.width || 200; - this.height = opts.height || 200; - } - - onPointerDown = (_: React.PointerEvent) => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - } - - onResizerPointerDown = (_: React.PointerEvent) => { - document.removeEventListener("pointermove", this.onResizerPointerMove); - document.removeEventListener("pointerup", this.onResizerPointerUp); - document.addEventListener("pointermove", this.onResizerPointerMove); - document.addEventListener("pointerup", this.onResizerPointerUp); - } - - @action - onPointerMove = (e: PointerEvent) => { - this.x += e.movementX; - this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0); - this.y += e.movementY; - this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0); - } - - @action - onResizerPointerMove = (e: PointerEvent) => { - this.width += e.movementX; - this.width = Math.max(this.width, 30); - this.height += e.movementY; - this.height = Math.max(this.height, 30); - } - - onPointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - - onResizerPointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onResizerPointerMove); - document.removeEventListener("pointerup", this.onResizerPointerUp); - } - - render() { - return ( - <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}> - <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} > - {this.props.overlayOptions.title || "Untitled"} - <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button> - </div> - <div className="overlayWindow-content"> - {this.props.children} - </div> - <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div> - </div> - ); - } + @observable x: number; + @observable y: number; + @observable width: number; + @observable height: number; + constructor(props: OverlayWindowProps) { + super(props); + + const opts = props.overlayOptions; + this.x = opts.x; + this.y = opts.y; + this.width = opts.width || 200; + this.height = opts.height || 200; + } + + onPointerDown = (_: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + } + + onResizerPointerDown = (_: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onResizerPointerMove); + document.removeEventListener("pointerup", this.onResizerPointerUp); + document.addEventListener("pointermove", this.onResizerPointerMove); + document.addEventListener("pointerup", this.onResizerPointerUp); + } + + @action + onPointerMove = (e: PointerEvent) => { + this.x += e.movementX; + this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0); + this.y += e.movementY; + this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0); + } + + @action + onResizerPointerMove = (e: PointerEvent) => { + this.width += e.movementX; + this.width = Math.max(this.width, 30); + this.height += e.movementY; + this.height = Math.max(this.height, 30); + } + + onPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + onResizerPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onResizerPointerMove); + document.removeEventListener("pointerup", this.onResizerPointerUp); + } + + render() { + return ( + <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}> + <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} > + {this.props.overlayOptions.title || "Untitled"} + <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button> + </div> + <div className="overlayWindow-content"> + {this.props.children} + </div> + <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div> + </div> + ); + } } @observer export class OverlayView extends React.Component { - public static Instance: OverlayView; - @observable.shallow - private _elements: JSX.Element[] = []; - - constructor(props: any) { - super(props); - if (!OverlayView.Instance) { - OverlayView.Instance = this; - } - } - - @action - addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer { - const remove = action(() => { - const index = this._elements.indexOf(ele); - if (index !== -1) this._elements.splice(index, 1); - }); - ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{ - transform: `translate(${options.x}px, ${options.y}px)`, - width: options.width, - height: options.height, - top: 0, - left: 0 - }}>{ele}</div>; - this._elements.push(ele); - return remove; - } - - @action - addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer { - const remove = action(() => { - const index = this._elements.indexOf(contents); - if (index !== -1) this._elements.splice(index, 1); - }); - contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>; - this._elements.push(contents); - return remove; - } - - - @computed get overlayDocs() { - return CurrentUserUtils.OverlayDocs?.map(d => { - let offsetx = 0, offsety = 0; - 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; - } - if (e.metaKey) { - const dragData = new DragManager.DocumentDragData([d]); - 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().myOverlayDocs, 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; + public static Instance: OverlayView; + @observable.shallow + private _elements: JSX.Element[] = []; + + constructor(props: any) { + super(props); + if (!OverlayView.Instance) { + OverlayView.Instance = this; + } + } + + @action + addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer { + const remove = action(() => { + const index = this._elements.indexOf(ele); + if (index !== -1) this._elements.splice(index, 1); }); - - const onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction); - offsetx = NumCast(d.x) - e.clientX; - offsety = NumCast(d.y) - e.clientY; - }; - return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}> - <DocumentView - Document={d} - rootSelected={returnTrue} - bringToFront={emptyFunction} - addDocument={undefined} - removeDocument={undefined} - PanelWidth={returnOne} - PanelHeight={returnOne} - ScreenToLocalTransform={Transform.Identity} - renderDepth={1} - isDocumentActive={returnTrue} - isContentActive={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - focus={DocUtils.DefaultFocus} - styleProvider={DefaultStyleProvider} - layerProvider={undefined} - docViewPath={returnEmptyDoclist} - addDocTab={returnFalse} - pinToPres={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - </div>; - }); - } - - public static ShowSpinner() { - return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 }); - } - - - render() { - return ( - <div className="overlayView" id="overlayView"> - <div> - {this._elements} - </div> - <CollectionFreeFormLinksView key="freeformLinks" /> - {this.overlayDocs} - </div> - ); - } + ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{ + transform: `translate(${options.x}px, ${options.y}px)`, + width: options.width, + height: options.height, + top: 0, + left: 0 + }}>{ele}</div>; + this._elements.push(ele); + return remove; + } + + @action + addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer { + const remove = action(() => { + const index = this._elements.indexOf(contents); + if (index !== -1) this._elements.splice(index, 1); + }); + contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>; + this._elements.push(contents); + return remove; + } + + + @computed get overlayDocs() { + return CurrentUserUtils.OverlayDocs?.map(d => { + let offsetx = 0, offsety = 0; + 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; + } + if (e.metaKey) { + const dragData = new DragManager.DocumentDragData([d]); + 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().myOverlayDocs, 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; + }; + + return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}> + <DocumentView + Document={d} + rootSelected={returnTrue} + bringToFront={emptyFunction} + addDocument={undefined} + removeDocument={undefined} + PanelWidth={returnOne} + PanelHeight={returnOne} + ScreenToLocalTransform={Transform.Identity} + renderDepth={1} + isDocumentActive={returnTrue} + isContentActive={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + focus={DocUtils.DefaultFocus} + styleProvider={DefaultStyleProvider} + layerProvider={undefined} + docViewPath={returnEmptyDoclist} + addDocTab={returnFalse} + pinToPres={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div>; + }); + } + + public static ShowSpinner() { + return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 }); + } + + + render() { + return ( + <div className="overlayView" id="overlayView"> + <div> + {this._elements} + </div> + <CollectionFreeFormLinksView key="freeformLinks" /> + {this.overlayDocs} + </div> + ); + } } // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) { - OverlayView.Instance.addWindow(<ScriptingRepl />, options); + OverlayView.Instance.addWindow(<ScriptingRepl />, options); });
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b2697cf08..6db2269c4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -50,6 +50,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import e = require("connect-flash"); export const panZoomSchema = createSchema({ _panX: "number", @@ -117,6 +118,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. + @observable storedMovements: object[] = []; // stores the movement if in recoding mode + @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); } @@ -956,8 +959,38 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } + + followMovements = (): void => { + // need the first for subtraction + let first = null; + + this.storedMovements.forEach(movement => { + if (first === null) first = movement.time; + + // set the pan to what was stored + setTimeout(() => { + this.Document._panX = movement.panX; + this.Document._panY = movement.panY; + }, movement.time - first) + }) + + // for now, clear the movements + this.storedMovements = [] + } + @action setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { + // if not presenting, just retrace the movements + if (Doc.UserDoc()?.presentationMode === 'watching') { + this.followMovements() + return; + } + + if (Doc.UserDoc()?.presentationMode === 'recording') { + // store as many movments as possible + this.storedMovements.push({time: new Date().getTime(), panX, panY}) + } + if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 005133eb0..da9b448ec 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -41,6 +41,7 @@ import { WebBox } from "./WebBox"; import React = require("react"); import XRegExp = require("xregexp"); import { MapBox } from "./MapBox/MapBox"; +import { RecordingBox } from "./RecordingBox"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -225,7 +226,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo components={{ FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox, + PDFBox, VideoBox, AudioBox, RecordingBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox, ScreenshotBox, HTMLtag, ComparisonBox diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 103c3624d..3f1a703bf 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1296,6 +1296,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; + const isOverlay: boolean = CurrentUserUtils.OverlayDocs.includes(this.props.Document); return (<div className="contentFittingDocumentView"> {!this.props.Document || !this.props.PanelWidth() ? (null) : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} @@ -1303,8 +1304,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { transition: this.props.dataTransition, position: this.props.Document.isInkMask ? "absolute" : undefined, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: (!this.props.ignoreAutoHeight && this.layoutDoc.autoHeight && this.layoutDoc.type === DocumentType.RTF) || isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : + width: isOverlay ? undefined : isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, + height: isOverlay ? undefined : (!this.props.ignoreAutoHeight && this.layoutDoc.autoHeight && this.layoutDoc.type === DocumentType.RTF) || isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 943b9f153..1fd665bad 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -6,6 +6,7 @@ import { Doc, Field, FieldResult } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { WebField } from "../../../fields/URLField"; import { DocumentViewSharedProps } from "./DocumentView"; +import { RecordingBox } from "./RecordingBox"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss new file mode 100644 index 000000000..a493b0b89 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -0,0 +1,26 @@ + +.progressbar { + position: absolute; + display: flex; + justify-content: flex-start; + bottom: 10px; + width: 80%; + height: 5px; + background-color: gray; + + &.done { + top: 0; + width: 0px; + height: 5px; + background-color: red; + z-index: 2; + } + + &.mark { + top: 0; + background-color: transparent; + border-right: 2px solid white; + z-index: 3; + pointer-events: none; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx new file mode 100644 index 000000000..82d5e1f04 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { useEffect } from "react" +import "./ProgressBar.scss" + +interface ProgressBarProps { + progress: number, + marks: number[], +} + +export function ProgressBar(props: ProgressBarProps) { + + // const handleClick = (e: React.MouseEvent) => { + // let progressbar = document.getElementById('progressbar')! + // let bounds = progressbar!.getBoundingClientRect(); + // let x = e.clientX - bounds.left; + // let percent = x / progressbar.clientWidth * 100 + + // for (let i = 0; i < props.marks.length; i++) { + // let start = i == 0 ? 0 : props.marks[i-1]; + // if (percent > start && percent < props.marks[i]) { + // props.playSegment(i) + // // console.log(i) + // // console.log(percent) + // // console.log(props.marks[i]) + // break + // } + // } + // } + + return ( + <div className="progressbar" id="progressbar"> + <div + className="progressbar done" + style={{ width: `${props.progress}%` }} + // onClick={handleClick} + ></div> + {props.marks.map((mark) => { + return <div + className="progressbar mark" + style={{ width: `${mark}%` }} + ></div> + })} + </div> + ) +}
\ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx new file mode 100644 index 000000000..6f9451925 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -0,0 +1,54 @@ +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { AudioField, VideoField } from "../../../../fields/URLField"; +import { Upload } from "../../../../server/SharedMediaTypes"; +import { ViewBoxBaseComponent } from "../../DocComponent"; +import { FieldView } from "../FieldView"; +import { VideoBox } from "../VideoBox"; +import { RecordingView } from './RecordingView'; +import { DocumentType } from "../../../documents/DocumentTypes"; + + +@observer +export class RecordingBox extends ViewBoxBaseComponent() { + + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); } + + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + + constructor(props: any) { + super(props); + } + + @observable result: Upload.FileInformation | undefined = undefined + @observable videoDuration: number | undefined = undefined + + @action + setVideoDuration = (duration: number) => { + this.videoDuration = duration + } + + @action + setResult = (info: Upload.FileInformation) => { + console.log("Setting result to " + info) + this.result = info + console.log(this.result.accessPaths.agnostic.client) + this.dataDoc.type = DocumentType.VID; + console.log(this.videoDuration) + this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration; + + this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; + this.layoutDoc._fitWidth = undefined; + this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); + } + + render() { + return <div className="recordingBox" ref={this._ref}> + {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} />} + {/* {!this.result ? <RecordingView setResult={this.setResult} /> : + <p>video box</p>} */} + </div>; + } +} diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss new file mode 100644 index 000000000..1fea231b7 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/RecordingView.scss @@ -0,0 +1,189 @@ +video { + flex: 100%; + width: 100%; + min-height: 400px; + height: auto; + display: block; + background-color: black; +} + +button { + margin: 0 .5rem +} + +.recording-container { + height: 100%; + width: 100%; + display: flex; + pointer-events: all; +} + +.video-wrapper { + max-width: 600px; + max-width: 700px; + position: relative; + display: flex; + justify-content: center; + overflow: hidden; + border-radius: 10px; +} + +.video-wrapper:hover .controls { + bottom: 30px; + transform: translateY(0%); + opacity: 100%; +} + +.controls { + display: flex; + align-items: center; + justify-content: space-evenly; + position: absolute; + padding: 14px; + width: 100%; + max-width: 500px; + max-height: 20%; + flex-wrap: wrap; + background: rgba(255, 255, 255, 0.25); + box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1); + backdrop-filter: blur(4px); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.18); + // transform: translateY(150%); + transition: all 0.3s ease-in-out; + // opacity: 0%; + bottom: 30px; + // bottom: -150px; +} + +.actions button { + background: none; + border: none; + outline: none; + cursor: pointer; +} + +.actions button i { + background-color: none; + color: white; + font-size: 30px; +} + + +.velocity { + appearance: none; + background: none; + color: white; + outline: none; + border: none; + text-align: center; + font-size: 16px; +} + +.mute-btn { + background: none; + border: none; + outline: none; + cursor: pointer; +} + +.mute-btn i { + background-color: none; + color: white; + font-size: 20px; +} + +.recording-sign { + height: 20px; + width: auto; + display: flex; + flex-direction: row; + position: absolute; + top: 10px; + right: 15px; + align-items: center; + justify-content: center; + + .timer { + font-size: 15px; + color: white; + margin: 0; + } + + .dot { + height: 15px; + width: 15px; + margin: 5px; + background-color: red; + border-radius: 50%; + display: inline-block; + } +} + +.controls-inner-container { + display: flex; + flex-direction: row; + justify-content: center; + width: 100%; + +} + +.record-button-wrapper { + width: 35px; + height: 35px; + font-size: 0; + background-color: grey; + border: 0px; + border-radius: 35px; + margin: 10px; + display: flex; + justify-content: center; + + .record-button { + background-color: red; + border: 0px; + border-radius: 50%; + height: 80%; + width: 80%; + align-self: center; + margin: 0; + + &:hover { + height: 85%; + width: 85%; + } + } + + .stop-button { + background-color: red; + border: 0px; + border-radius: 10%; + height: 70%; + width: 70%; + align-self: center; + margin: 0; + + + // &:hover { + // width: 40px; + // height: 40px + // } + } + +} + +.video-edit-wrapper { + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + position: absolute; + top: 0; + bottom: 0; + right: 50% - 15; + + .video-edit-buttons { + margin: 0 5px; + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx new file mode 100644 index 000000000..6aed07c60 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -0,0 +1,348 @@ +import * as React from 'react'; +import "./RecordingView.scss"; +import { ReactElement, useCallback, useEffect, useRef, useState } from "react"; +import { ProgressBar } from "./ProgressBar" +import { MdBackspace } from 'react-icons/md'; +import { FaCheckCircle } from 'react-icons/fa'; +import { IconContext } from "react-icons"; +import { Networking } from '../../../Network'; +import { Upload } from '../../../../server/SharedMediaTypes'; + + +enum RecordingStatus { + Recording, + Stopped, + Paused +} + +interface MediaSegment { + videoChunks: any[], + endTime: number +} + +interface IRecordingViewProps { + setResult: (info: Upload.FileInformation) => void + setDuration: (seconds: number) => void +} + +const MAXTIME = 1000; + +export function RecordingView(props: IRecordingViewProps) { + + const [recording, setRecording] = useState(false); + const recordingTimerRef = useRef<number>(0); + const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second + const [playing, setPlaying] = useState(false); + const [progress, setProgress] = useState(0); + const [speed, setSpeed] = useState(1); + const [muted, setMuted] = useState(false); + + const [videos, setVideos] = useState<MediaSegment[]>([]); + const [currentVid, setCurrentVid] = useState<number>(0); + const videoRecorder = useRef<MediaRecorder | null>(null); + const audioRecorder = useRef<MediaRecorder | null>(null); + const videoElementRef = useRef<HTMLVideoElement | null>(null); + + const [finished, setFinished] = useState<Boolean>(false) + + + + const DEFAULT_MEDIA_CONSTRAINTS = { + // video: true, + // audio: true + video: { + width: 1280, + height: 720, + }, + // audio: true, + // audio: { + // echoCancellation: true, + // noiseSuppression: true, + // sampleRate: 44100 + // } + } + + useEffect(() => { + + if (finished) { + props.setDuration(recordingTimer * 100) + let allVideoChunks: any = [] + console.log(videos) + videos.forEach((vid) => { + console.log(vid.videoChunks) + allVideoChunks = allVideoChunks.concat(vid.videoChunks) + }) + + console.log(allVideoChunks) + const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() }); + + // const uploadVideo = async () => { + // const [{ result }] = await Networking.UploadFilesToServer(videoFile); + // console.log("upload result", result); + // if (!(result instanceof Error)) { + // setResult(result) + // } + // } + + Networking.UploadFilesToServer(videoFile) + .then((data) => { + const result = data[0].result + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result) + } else { + alert("video conversion failed"); + } + }) + // uploadVideo() + + // this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000; + + + + // change to one recording box + } + + + }, [finished]) + + useEffect(() => { + // check if the browser supports media devices on first load + if (!navigator.mediaDevices) { + console.log('This browser does not support getUserMedia.') + } + console.log('This device has the correct media devices.') + }, []) + + useEffect(() => { + // get access to the video element on every render + videoElementRef.current = document.getElementById('video') as HTMLVideoElement; + }) + + useEffect(() => { + let interval: any = null; + if (recording) { + interval = setInterval(() => { + setRecordingTimer(unit => unit + 1); + }, 10); + } else if (!recording && recordingTimer !== 0) { + clearInterval(interval); + } + return () => clearInterval(interval); + }, [recording]) + + useEffect(() => { + setVideoProgressHelper(recordingTimer) + recordingTimerRef.current = recordingTimer; + }, [recordingTimer]) + + const setVideoProgressHelper = (progress: number) => { + const newProgress = (progress / MAXTIME) * 100; + setProgress(newProgress) + } + const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { + const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) + + videoElementRef.current!.src = "" + videoElementRef.current!.srcObject = stream + videoElementRef.current!.muted = true + + return stream + } + + const record = async () => { + const stream = await startShowingStream(); + videoRecorder.current = new MediaRecorder(stream) + // audioRecorder.current = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); + + // temporary chunks of video + let videoChunks: any = [] + // let audioChunks: any = [] + + videoRecorder.current.ondataavailable = (event: any) => { + if (event.data.size > 0) { + videoChunks.push(event.data) + } + } + + // audioRecorder.current.ondataavailable = (event: any) => { + // if (event.data.size > 0) { + // audioChunks.push(event.data) + // } + // } + + videoRecorder.current.onstart = (event: any) => { + setRecording(true); + } + + videoRecorder.current.onstop = () => { + // if we have a last portion + if (videoChunks.length > 1) { + // append the current portion to the video pieces + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + } + + // reset the temporary chunks + videoChunks = [] + setRecording(false); + setFinished(true); + } + + // recording paused + videoRecorder.current.onpause = (event: any) => { + // append the current portion to the video pieces + console.log(videoChunks) + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + + // reset the temporary chunks + videoChunks = [] + setRecording(false); + } + + videoRecorder.current.onresume = async (event: any) => { + console.log(event) + await startShowingStream(); + setRecording(true); + } + + videoRecorder.current.start(200) + } + + + const stop = () => { + if (videoRecorder.current) { + if (videoRecorder.current.state !== "inactive") { + videoRecorder.current.stop(); + // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) + } + } + } + + const pause = () => { + if (videoRecorder.current) { + if (videoRecorder.current.state === "recording") { + videoRecorder.current.pause(); + } + } + } + + const startOrResume = () => { + console.log('[RecordingView.tsx] startOrResume') + if (!videoRecorder.current || videoRecorder.current.state === "inactive") { + record(); + } else if (videoRecorder.current.state === "paused") { + videoRecorder.current.resume(); + } + } + + // const playSegment = (idx: number) => { + // console.log(idx) + // let currentChunks = videos[idx].chunks + // console.log(currentChunks) + + // const blob = new Blob(currentChunks, { + // type: 'video/webm' + // }) + // const blobUrl = URL.createObjectURL(blob) + // console.log(blobUrl) + + // videoElementRef.current!.srcObject = null + // videoElementRef.current!.src = blobUrl + // videoElementRef.current!.muted = false + // } + + const clearPrevious = () => { + const numVideos = videos.length + setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) + setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) + setVideos(videos.filter((_, idx) => idx !== numVideos - 1)); + } + + // const handleVideoProgress = (event: any) => { + // const manualChange = Number(event.target.value); + // videoElement!.currentTime = (videoElement!.duration / 100) * manualChange; + // setProgress(manualChange) + // }; + + // const handleVideoSpeed = (event: any) => { + // const newSpeed = Number(event.target.value); + // videoElement!.playbackRate = speed; + // setSpeed(newSpeed) + // }; + + const handleOnTimeUpdate = () => { + if (playing) { + setVideoProgressHelper(videoElementRef.current!.currentTime) + } + }; + + const millisecondToMinuteSecond = (milliseconds: number) => { + const toTwoDigit = (digit: number) => { + return String(digit).length == 1 ? "0" + digit : digit + } + const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); + return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); + } + + + + + useEffect(() => { + console.log(videos.map((elt) => elt.endTime / MAXTIME * 100)) + console.log(videos) + }, [videos]) + + return ( + <div className="recording-container"> + <div className="video-wrapper"> + <video id="video" + autoPlay + muted + onTimeUpdate={handleOnTimeUpdate} + /> + <div className="recording-sign"> + <span className="dot" /> + <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p> + </div> + <div className="controls"> + + <div className="controls-inner-container"> + <div className="record-button-wrapper"> + {recording ? + <button className="stop-button" onClick={pause} /> : + <button className="record-button" onClick={startOrResume} /> + } + </div> + {!recording && videos.length > 0 ? + + <div className="video-edit-wrapper"> + <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}> + <MdBackspace onClick={clearPrevious} /> + </IconContext.Provider> + <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}> + <FaCheckCircle onClick={stop} /> + </IconContext.Provider> + </div> + + : <></>} + + </div> + + <ProgressBar + progress={progress} + marks={videos.map((elt) => elt.endTime / MAXTIME * 100)} + // playSegment={playSegment} + /> + + {/* <button className="mute-btn" onClick={() => setMuted(!muted)}> + {!muted ? ( + <i className="bx bxs-volume-full"></i> + ) : ( + <i className="bx bxs-volume-mute"></i> + )} + </button> */} + </div> + + </div> + </div>) +}
\ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/index.ts b/src/client/views/nodes/RecordingBox/index.ts new file mode 100644 index 000000000..ff21eaed6 --- /dev/null +++ b/src/client/views/nodes/RecordingBox/index.ts @@ -0,0 +1,2 @@ +export * from './RecordingView' +export * from './RecordingBox'
\ No newline at end of file diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index dbb567d3a..0c7d7dc31 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -26,7 +26,7 @@ import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import "./ScreenshotBox.scss"; import { VideoBox } from "./VideoBox"; declare class MediaRecorder { - constructor(e: any, options?: any); // whatever MediaRecorder has + constructor(e: any, options?: any); // whatever MediaRecorder has } // interface VideoTileProps { @@ -107,225 +107,226 @@ declare class MediaRecorder { @observer export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } - private _audioRec: any; - private _videoRec: any; - @observable private _videoRef: HTMLVideoElement | null = null; - @observable _screenCapture = false; - @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } + private _audioRec: any; + private _videoRec: any; + @observable private _videoRef: HTMLVideoElement | null = null; + @observable _screenCapture = false; + @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - constructor(props: any) { - super(props); - if (this.rootDoc.videoWall) { - this.rootDoc.nativeWidth = undefined; - this.rootDoc.nativeHeight = undefined; - this.layoutDoc.popOff = 0; - this.layoutDoc.popOut = 1; - } else { - this.setupDictation(); - } - } - getAnchor = () => { - const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", - startTime, startTime === undefined ? undefined : startTime + 3) - || this.rootDoc; - } + constructor(props: any) { + super(props); + if (this.rootDoc.videoWall) { + this.rootDoc.nativeWidth = undefined; + this.rootDoc.nativeHeight = undefined; + this.layoutDoc.popOff = 0; + this.layoutDoc.popOut = 1; + } else { + this.setupDictation(); + } + } + getAnchor = () => { + const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", + startTime, startTime === undefined ? undefined : startTime + 3) + || this.rootDoc; + } - videoLoad = () => { - const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight; - const nativeWidth = Doc.NativeWidth(this.layoutDoc); - const nativeHeight = Doc.NativeHeight(this.layoutDoc); - if (!nativeWidth || !nativeHeight) { - if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); - Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); - this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; - } - } + videoLoad = () => { + const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight; + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); + if (!nativeWidth || !nativeHeight) { + if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); + Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); + this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; + } + } - componentDidMount() { - this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; - this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), - // ({ width, height }) => { - // if (this._camera) { - // const angle = -Math.abs(1 - width / height); - // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)]; - // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle)); - // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); - // (this._camera as any).updateProjectionMatrix(); - // } - // }); - } - componentWillUnmount() { - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); - } + componentDidMount() { + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; + this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), + // ({ width, height }) => { + // if (this._camera) { + // const angle = -Math.abs(1 - width / height); + // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)]; + // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle)); + // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); + // (this._camera as any).updateProjectionMatrix(); + // } + // }); + } + componentWillUnmount() { + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + } - specificContextMenu = (e: React.MouseEvent): void => { - const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; - ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); - } + specificContextMenu = (e: React.MouseEvent): void => { + const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; + ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); + } - @computed get content() { - if (this.rootDoc.videoWall) return (null); - return <video className={"videoBox-content"} key="video" - ref={r => { - this._videoRef = r; - setTimeout(() => { - if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) { - this.toggleRecording(); - } - }, 100); - }} - autoPlay={this._screenCapture} - style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} - onCanPlay={this.videoLoad} - controls={true} - onClick={e => e.preventDefault()}> - <source type="video/mp4" /> - Not supported. - </video>; - } + @computed get content() { + if (this.rootDoc.videoWall) return (null); + return <video className={"videoBox-content"} key="video" + ref={r => { + this._videoRef = r; + setTimeout(() => { + if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) { + this.toggleRecording(); + } + }, 100); + }} + autoPlay={this._screenCapture} + style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} + onCanPlay={this.videoLoad} + controls={true} + onClick={e => e.preventDefault()}> + <source type="video/mp4" /> + Not supported. + </video>; + } - // _numScreens = 5; - // _camera: Camera | undefined; - // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; - // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; - @computed get threed() { - // if (this.rootDoc.videoWall) { - // const screens: any[] = []; - // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; - // let count = 0; - // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( - // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />))); - // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => { - // this._camera = props.camera; - // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); - // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); - // }}> - // {/* <ambientLight />*/} - // <pointLight position={[10, 10, 10]} intensity={1} /> - // {screens} - // </ Canvas>; - // } - return (null); - } - toggleRecording = async () => { - if (!this._screenCapture) { - this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); - const aud_chunks: any = []; - this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); - this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); - if (!(result instanceof Error)) { - this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client); - } - }; - this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); - this._videoRec = new MediaRecorder(this._videoRef!.srcObject); - const vid_chunks: any = []; - this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); - this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); - this._videoRec.onstop = async (e: any) => { - const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); - const [{ result }] = await Networking.UploadFilesToServer(file); - this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000; - if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - this.dataDoc.type = DocumentType.VID; - this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); - this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; - this.layoutDoc._fitWidth = undefined; - this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); - } else alert("video conversion failed"); - }; - this._audioRec.start(); - this._videoRec.start(); - runInAction(() => { - this._screenCapture = true; - this.dataDoc.mediaState = "recording"; - }); - DocUtils.ActiveRecordings.push(this); - } else { - this._audioRec?.stop(); - this._videoRec?.stop(); - runInAction(() => { - this._screenCapture = false; - this.dataDoc.mediaState = "paused"; - }); - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + // _numScreens = 5; + // _camera: Camera | undefined; + // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; + // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; + @computed get threed() { + // if (this.rootDoc.videoWall) { + // const screens: any[] = []; + // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; + // let count = 0; + // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( + // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />))); + // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => { + // this._camera = props.camera; + // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); + // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); + // }}> + // {/* <ambientLight />*/} + // <pointLight position={[10, 10, 10]} intensity={1} /> + // {screens} + // </ Canvas>; + // } + return (null); + } + toggleRecording = async () => { + if (!this._screenCapture) { + this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); + const aud_chunks: any = []; + this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); + this._audioRec.onstop = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); + if (!(result instanceof Error)) { + this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client); + } + }; + this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + this._videoRec = new MediaRecorder(this._videoRef!.srcObject); + const vid_chunks: any = []; + this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); + this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); + this._videoRec.onstop = async (e: any) => { + console.log("screenshotbox: upload") + const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); + const [{ result }] = await Networking.UploadFilesToServer(file); + this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000; + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + this.dataDoc.type = DocumentType.VID; + this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; + this.layoutDoc._fitWidth = undefined; + this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); + } else alert("video conversion failed"); + }; + this._audioRec.start(); + this._videoRec.start(); + runInAction(() => { + this._screenCapture = true; + this.dataDoc.mediaState = "recording"; + }); + DocUtils.ActiveRecordings.push(this); + } else { + this._audioRec?.stop(); + this._videoRec?.stop(); + runInAction(() => { + this._screenCapture = false; + this.dataDoc.mediaState = "paused"; + }); + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); - CaptureManager.Instance.open(this.rootDoc); - } - } + CaptureManager.Instance.open(this.rootDoc); + } + } - setupDictation = () => { - if (this.dataDoc[this.fieldKey + "-dictation"]) return; - const dictationText = CurrentUserUtils.GetNewTextDoc("dictation", - NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, - NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - dictationText._autoHeight = false; - const dictationTextProto = Doc.GetProto(dictationText); - dictationTextProto.recordingSource = this.dataDoc; - dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); - this.dataDoc[this.fieldKey + "-dictation"] = dictationText; - } - contentFunc = () => [this.threed, this.content]; - videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth(); - formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); - render() { - TraceMobx(); - return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} > - <div className="videoBox-viewer" > - <div style={{ position: "relative", height: this.videoPanelHeight() }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - PanelHeight={this.videoPanelHeight} - PanelWidth={this.props.PanelWidth} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - isContentActive={returnFalse} - scaling={returnOne} - whenChildContentsActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - {this.contentFunc} - </CollectionFreeFormView></div> - <div style={{ position: "relative", height: this.formattedPanelHeight() }}> - {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : - <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - Document={this.dataDoc[this.fieldKey + "-dictation"]} - fieldKey={"text"} - PanelHeight={this.formattedPanelHeight} - isAnnotationOverlay={true} - select={emptyFunction} - isContentActive={emptyFunction} - scaling={returnOne} - xPadding={25} - yPadding={10} - whenChildContentsActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - </FormattedTextBox>} - </div> - </div> - {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons"> - <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > - <FontAwesomeIcon icon="file" size="lg" /> - </div> - </div>} - </div >; - } + setupDictation = () => { + if (this.dataDoc[this.fieldKey + "-dictation"]) return; + const dictationText = CurrentUserUtils.GetNewTextDoc("dictation", + NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, + NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + dictationText._autoHeight = false; + const dictationTextProto = Doc.GetProto(dictationText); + dictationTextProto.recordingSource = this.dataDoc; + dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); + dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); + this.dataDoc[this.fieldKey + "-dictation"] = dictationText; + } + contentFunc = () => [this.threed, this.content]; + videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); + render() { + TraceMobx(); + return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} > + <div className="videoBox-viewer" > + <div style={{ position: "relative", height: this.videoPanelHeight() }}> + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + PanelHeight={this.videoPanelHeight} + PanelWidth={this.props.PanelWidth} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + isContentActive={returnFalse} + scaling={returnOne} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + {this.contentFunc} + </CollectionFreeFormView></div> + <div style={{ position: "relative", height: this.formattedPanelHeight() }}> + {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : + <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + Document={this.dataDoc[this.fieldKey + "-dictation"]} + fieldKey={"text"} + PanelHeight={this.formattedPanelHeight} + isAnnotationOverlay={true} + select={emptyFunction} + isContentActive={emptyFunction} + scaling={returnOne} + xPadding={25} + yPadding={10} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + </FormattedTextBox>} + </div> + </div> + {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons"> + <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > + <FontAwesomeIcon icon="file" size="lg" /> + </div> + </div>} + </div >; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index a4ec559f5..92834fb07 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -6,7 +6,7 @@ import { DataSym, Doc, Opt } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { Cast, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils"; -import { DocUtils } from "../../../documents/Documents"; +import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { DocumentManager } from "../../../util/DocumentManager"; @@ -23,353 +23,392 @@ import { PresBox } from "./PresBox"; import "./PresElementBox.scss"; import { PresMovement } from "./PresEnums"; import React = require("react"); +import { List } from "../../../../fields/List"; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. */ @observer export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); } - _heightDisposer: IReactionDisposer | undefined; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); } + _heightDisposer: IReactionDisposer | undefined; - @observable _dragging = false; - // these fields are conditionally computed fields on the layout document that take this document as a parameter - @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) - @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up - @computed get presStatus() { return StrCast(this.lookupField("presStatus")); } - @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); } - @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); } - @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; } + @observable _dragging = false; + // these fields are conditionally computed fields on the layout document that take this document as a parameter + @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) + @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up + @computed get presStatus() { return StrCast(this.lookupField("presStatus")); } + @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); } + @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); } + @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; } - componentDidMount() { - this.layoutDoc.hideLinkButton = true; - this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], - params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true }); - } - componentWillUnmount() { - this._heightDisposer?.(); - } + @observable isShowingVideo = false; + @action setIsShowingVideo(shown: boolean) { + this.isShowingVideo = shown + } - /** - * Returns a local transformed coordinate array for given coordinates. - */ - ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; + componentDidMount() { + this.layoutDoc.hideLinkButton = true; + this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], + params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true }); + } + componentWillUnmount() { + this._heightDisposer?.(); + } - @action - presExpandDocumentClick = () => { - this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; - } + /** + * Returns a local transformed coordinate array for given coordinates. + */ + ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; - embedHeight = (): number => 97; - // embedWidth = () => this.props.PanelWidth(); - // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); - embedWidth = (): number => this.props.PanelWidth() - 35; - styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { - if (property === StyleProp.Opacity) return 1; - return this.props.styleProvider?.(doc, props, property); - } - /** - * The function that is responsible for rendering a preview or not for this - * presentation element. - */ - @computed get renderEmbeddedInline() { - return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) : - <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}> - <DocumentView - Document={this.targetDoc} - DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} - styleProvider={this.styleProvider} - layerProvider={this.props.layerProvider} - docViewPath={returnEmptyDoclist} - rootSelected={returnTrue} - addDocument={returnFalse} - removeDocument={returnFalse} - isContentActive={this.props.isContentActive} - addDocTab={returnFalse} - pinToPres={returnFalse} - PanelWidth={this.embedWidth} - PanelHeight={this.embedHeight} - ScreenToLocalTransform={Transform.Identity} - moveDocument={this.props.moveDocument!} - renderDepth={this.props.renderDepth + 1} - focus={DocUtils.DefaultFocus} - whenChildContentsActiveChanged={returnFalse} - bringToFront={returnFalse} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - hideLinkButton={true} - /> - <div className="presItem-embeddedMask" /> - </div>; - } + @action + presExpandDocumentClick = () => { + this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; + } - @computed get duration() { - let durationInS: number; - if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } - else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; - else durationInS = 2; - return "D: " + durationInS + "s"; - } + embedHeight = (): number => 97; + // embedWidth = () => this.props.PanelWidth(); + // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); + embedWidth = (): number => this.props.PanelWidth() - 35; + styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + if (property === StyleProp.Opacity) return 1; + return this.props.styleProvider?.(doc, props, property); + } + /** + * The function that is responsible for rendering a preview or not for this + * presentation element. + */ + @computed get renderEmbeddedInline() { + return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) : + <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}> + <DocumentView + Document={this.targetDoc} + DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} + styleProvider={this.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + rootSelected={returnTrue} + addDocument={returnFalse} + removeDocument={returnFalse} + isContentActive={this.props.isContentActive} + addDocTab={returnFalse} + pinToPres={returnFalse} + PanelWidth={this.embedWidth} + PanelHeight={this.embedHeight} + ScreenToLocalTransform={Transform.Identity} + moveDocument={this.props.moveDocument!} + renderDepth={this.props.renderDepth + 1} + focus={DocUtils.DefaultFocus} + whenChildContentsActiveChanged={returnFalse} + bringToFront={returnFalse} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + hideLinkButton={true} + /> + <div className="presItem-embeddedMask" /> + </div>; + } - @computed get transition() { - let transitionInS: number; - if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000; - else transitionInS = 0.5; - return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s"; - } + @computed get duration() { + let durationInS: number; + if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } + else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; + else durationInS = 2; + return "D: " + durationInS + "s"; + } - private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _titleRef: React.RefObject<EditableView> = React.createRef(); + @computed get transition() { + let transitionInS: number; + if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000; + else transitionInS = 0.5; + return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s"; + } + private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _titleRef: React.RefObject<EditableView> = React.createRef(); - @action - headerDown = (e: React.PointerEvent<HTMLDivElement>) => { - const element = e.target as any; - e.stopPropagation(); - e.preventDefault(); - if (element && !(e.ctrlKey || e.metaKey)) { - if (PresBox.Instance._selectedArray.has(this.rootDoc)) { - PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); - setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); - } else { - setupMoveUpEvents(this, e, ((e: PointerEvent) => { - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); - return this.startDrag(e); - }), emptyFunction, emptyFunction); + + @action + headerDown = (e: React.PointerEvent<HTMLDivElement>) => { + const element = e.target as any; + e.stopPropagation(); + e.preventDefault(); + if (element && !(e.ctrlKey || e.metaKey)) { + if (PresBox.Instance._selectedArray.has(this.rootDoc)) { + PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); + setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + } else { + setupMoveUpEvents(this, e, ((e: PointerEvent) => { + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false); + return this.startDrag(e); + }), emptyFunction, emptyFunction); + } } - } - } + } - headerUp = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - } + headerUp = (e: React.PointerEvent<HTMLDivElement>) => { + e.stopPropagation(); + e.preventDefault(); + } - /** - * Function to drag and drop the pres element to a diferent location - */ - startDrag = (e: PointerEvent) => { - const miniView: boolean = this.toolbarWidth <= 100; - const activeItem = this.rootDoc; - const dragArray = PresBox.Instance._dragArray; - const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray()); - const dragItem: HTMLElement[] = []; - if (dragArray.length === 1) { - const doc = dragArray[0]; - doc.className = miniView ? "presItem-miniSlide" : "presItem-slide"; - dragItem.push(doc); - } else if (dragArray.length >= 1) { - const doc = document.createElement('div'); - doc.className = "presItem-multiDrag"; - doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides"; - doc.style.position = 'absolute'; - doc.style.top = (e.clientY) + 'px'; - doc.style.left = (e.clientX - 50) + 'px'; - dragItem.push(doc); - } + /** + * Function to drag and drop the pres element to a diferent location + */ + startDrag = (e: PointerEvent) => { + const miniView: boolean = this.toolbarWidth <= 100; + const activeItem = this.rootDoc; + const dragArray = PresBox.Instance._dragArray; + const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray()); + const dragItem: HTMLElement[] = []; + if (dragArray.length === 1) { + const doc = dragArray[0]; + doc.className = miniView ? "presItem-miniSlide" : "presItem-slide"; + dragItem.push(doc); + } else if (dragArray.length >= 1) { + const doc = document.createElement('div'); + doc.className = "presItem-multiDrag"; + doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides"; + doc.style.position = 'absolute'; + doc.style.top = (e.clientY) + 'px'; + doc.style.left = (e.clientX - 50) + 'px'; + dragItem.push(doc); + } - // const dropEvent = () => runInAction(() => this._dragging = false); - if (activeItem) { - DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined); - // runInAction(() => this._dragging = true); - return true; - } - return false; - } + // const dropEvent = () => runInAction(() => this._dragging = false); + if (activeItem) { + DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined); + // runInAction(() => this._dragging = true); + return true; + } + return false; + } - onPointerOver = (e: any) => { - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); - } + onPointerOver = (e: any) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + } - onPointerMove = (e: PointerEvent) => { - const slide = this._itemRef.current!; - let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false; - for (const doc of DragManager.docsBeingDragged) { - if (!doc.presentationTargetDoc) dragIsPresItem = false; - } - if (slide && dragIsPresItem) { - const rect = slide.getBoundingClientRect(); - const y = e.clientY - rect.top; //y position within the element. - const height = slide.clientHeight; - const halfLine = height / 2; - if (y <= halfLine) { - slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; - slide.style.borderBottom = "0px"; - } else if (y > halfLine) { - slide.style.borderTop = "0px"; - slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; + onPointerMove = (e: PointerEvent) => { + const slide = this._itemRef.current!; + let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false; + for (const doc of DragManager.docsBeingDragged) { + if (!doc.presentationTargetDoc) dragIsPresItem = false; + } + if (slide && dragIsPresItem) { + const rect = slide.getBoundingClientRect(); + const y = e.clientY - rect.top; //y position within the element. + const height = slide.clientHeight; + const halfLine = height / 2; + if (y <= halfLine) { + slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; + slide.style.borderBottom = "0px"; + } else if (y > halfLine) { + slide.style.borderTop = "0px"; + slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; + } } - } - document.removeEventListener("pointermove", this.onPointerMove); - } + document.removeEventListener("pointermove", this.onPointerMove); + } - onPointerLeave = (e: any) => { - this._itemRef.current!.style.borderTop = "0px"; - this._itemRef.current!.style.borderBottom = "0px"; - document.removeEventListener("pointermove", this.onPointerMove); - } + onPointerLeave = (e: any) => { + this._itemRef.current!.style.borderTop = "0px"; + this._itemRef.current!.style.borderBottom = "0px"; + document.removeEventListener("pointermove", this.onPointerMove); + } - @action - toggleProperties = () => { - if (CurrentUserUtils.propertiesWidth < 5) { - action(() => (CurrentUserUtils.propertiesWidth = 250)); - } - } + @action + toggleProperties = () => { + if (CurrentUserUtils.propertiesWidth < 5) { + action(() => (CurrentUserUtils.propertiesWidth = 250)); + } + } - @undoBatch - removeItem = action((e: React.MouseEvent) => { - this.props.removeDocument?.(this.rootDoc); - if (PresBox.Instance._selectedArray.has(this.rootDoc)) { - PresBox.Instance._selectedArray.delete(this.rootDoc); - } - e.stopPropagation(); - }); + @undoBatch + removeItem = action((e: React.MouseEvent) => { + this.props.removeDocument?.(this.rootDoc); + if (PresBox.Instance._selectedArray.has(this.rootDoc)) { + PresBox.Instance._selectedArray.delete(this.rootDoc); + } + e.stopPropagation(); + }); - // set the value/title of the individual pres element - @undoBatch - @action - onSetValue = (value: string) => { - this.rootDoc.title = !value.trim().length ? "-untitled-" : value; - return true; - } + // set the value/title of the individual pres element + @undoBatch + @action + onSetValue = (value: string) => { + this.rootDoc.title = !value.trim().length ? "-untitled-" : value; + return true; + } - /** - * Method called for updating the view of the currently selected document - * - * @param targetDoc - * @param activeItem - */ - @undoBatch - @action - updateView = (targetDoc: Doc, activeItem: Doc) => { - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { - const scroll = targetDoc._scrollTop; - activeItem.presPinViewScroll = scroll; - } else if (targetDoc.type === DocumentType.VID) { - activeItem.presPinTimecode = targetDoc._currentTimecode; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const clipWidth = targetDoc._clipWidth; - activeItem.presPinClipWidth = clipWidth; - } else { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; - } - } + /** + * Method called for updating the view of the currently selected document + * + * @param targetDoc + * @param activeItem + */ + @undoBatch + @action + updateView = (targetDoc: Doc, activeItem: Doc) => { + if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { + const scroll = targetDoc._scrollTop; + activeItem.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeItem.presPinTimecode = targetDoc._currentTimecode; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const clipWidth = targetDoc._clipWidth; + activeItem.presPinClipWidth = clipWidth; + } else { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + } + } - @computed - get toolbarWidth(): number { - const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); - let width: number = NumCast(this.presBox._width); - if (presBoxDocView) width = presBoxDocView.props.PanelWidth(); - if (width === 0) width = 300; - return width; - } + @undoBatch + @action + startRecording = (targetDoc: Doc, activeItem: Doc) => { - @computed get mainItem() { - const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc); - const toolbarWidth: number = this.toolbarWidth; - const showMore: boolean = this.toolbarWidth >= 300; - const miniView: boolean = this.toolbarWidth <= 110; - const presBox: Doc = this.presBox; //presBox - const presBoxColor: string = StrCast(presBox._backgroundColor); - const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false; - const targetDoc: Doc = this.targetDoc; - const activeItem: Doc = this.rootDoc; - return ( - <div className={`presItem-container`} - key={this.props.Document[Id] + this.indexInPres} - ref={this._itemRef} - style={{ - backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent", - opacity: this._dragging ? 0.3 : 1 - }} - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); - }} - onDoubleClick={action(e => { - this.toggleProperties(); - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); - })} - onPointerOver={this.onPointerOver} - onPointerLeave={this.onPointerLeave} - onPointerDown={this.headerDown} - onPointerUp={this.headerUp} - > - {miniView ? - // when width is LESS than 110 px - <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}> - {`${this.indexInPres + 1}.`} - </div> - : - // when width is MORE than 110 px - <div className="presItem-number"> - {`${this.indexInPres + 1}.`} - </div>} - {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} - style={{ - backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), - boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined - }}> - <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> - <EditableView - ref={this._titleRef} - editing={!isSelected ? false : undefined} - contents={activeItem.title} - overflow={'ellipsis'} - GetValue={() => StrCast(activeItem.title)} - SetValue={this.onSetValue} - /> - </div> - <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> - <div className={"presItem-slideButtons"}> - <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}> - <div className="slideButton" - onClick={() => this.updateView(targetDoc, activeItem)} - style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div> - </Tooltip> - {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}> - <div className="slideButton" - onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp} - style={{ - zIndex: 1000 - this.indexInPres, - fontWeight: 700, - backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined, - height: activeItem.groupWithUp ? 53 : 18, - transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined - }}> - <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}> - <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} /> - </div> - </div> - </Tooltip>} - <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> - <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} /> - </div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div - className={"slideButton"} - onClick={this.removeItem}> - <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> - </div></Tooltip> - </div> - <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> - {this.renderEmbeddedInline} - </div>} - </div >); - } + // TODO: Remove everything that already exists + if (activeItem.recording) { + // if we already have an existing recording + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null)); + + } else { + // if we dont have any recording + console.log(targetDoc.title, activeItem.title); + const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); + activeItem.recording = recording + // TODO: Figure out exactly where we want the video to appear + const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + console.log("x: ", pt[0], "y: ", pt[1]); + recording.x = pt[0]; + recording.y = pt[1]; + console.log(Doc.UserDoc().myOverlayDocs) + // Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording); + } + + } + + @computed + get toolbarWidth(): number { + const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox); + let width: number = NumCast(this.presBox._width); + if (presBoxDocView) width = presBoxDocView.props.PanelWidth(); + if (width === 0) width = 300; + return width; + } + + @computed get mainItem() { + const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc); + const toolbarWidth: number = this.toolbarWidth; + const showMore: boolean = this.toolbarWidth >= 300; + const miniView: boolean = this.toolbarWidth <= 110; + const presBox: Doc = this.presBox; //presBox + const presBoxColor: string = StrCast(presBox._backgroundColor); + const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false; + const targetDoc: Doc = this.targetDoc; + const activeItem: Doc = this.rootDoc; + return ( + <div className={`presItem-container`} + key={this.props.Document[Id] + this.indexInPres} + ref={this._itemRef} + style={{ + backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent", + opacity: this._dragging ? 0.3 : 1 + }} + onClick={e => { + e.stopPropagation(); + e.preventDefault(); + PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + }} + onDoubleClick={action(e => { + this.toggleProperties(); + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + })} + onPointerOver={this.onPointerOver} + onPointerLeave={this.onPointerLeave} + onPointerDown={this.headerDown} + onPointerUp={this.headerUp} + > + {miniView ? + // when width is LESS than 110 px + <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}> + {`${this.indexInPres + 1}.`} + </div> + : + // when width is MORE than 110 px + <div className="presItem-number"> + {`${this.indexInPres + 1}.`} + </div>} + {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} + style={{ + backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined + }}> + <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> + <EditableView + ref={this._titleRef} + editing={!isSelected ? false : undefined} + contents={activeItem.title} + overflow={'ellipsis'} + GetValue={() => StrCast(activeItem.title)} + SetValue={this.onSetValue} + /> + </div> + <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> + <div className={"presItem-slideButtons"}> + <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}> + <div className="slideButton" + onClick={() => this.updateView(targetDoc, activeItem)} + style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}> + <div className="slideButton" + onClick={() => this.startRecording(targetDoc, activeItem)} + style={{ fontWeight: 700 }}> + <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}> + <div className="slideButton" + onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp} + style={{ + zIndex: 1000 - this.indexInPres, + fontWeight: 700, + backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined, + height: activeItem.groupWithUp ? 53 : 18, + transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined + }}> + <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}> + <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} /> + </div> + </div> + </Tooltip>} + <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> + <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div + className={"slideButton"} + onClick={this.removeItem}> + <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + </div> + <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> + {this.renderEmbeddedInline} + </div>} + </div >); + } - render() { - return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem; - } + render() { + return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem; + } }
\ No newline at end of file diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 1d4bbaed0..3e7542e46 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -53,6 +53,7 @@ export abstract class URLField extends ObjectField { export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg"; @scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { } +@scriptingGlobal @Deserializable("recording") export class RecordingField extends URLField { } @scriptingGlobal @Deserializable("image") export class ImageField extends URLField { } @scriptingGlobal @Deserializable("video") export class VideoField extends URLField { } @scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { } diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index bfa07d47a..5a19477dd 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -19,288 +19,288 @@ import { SolrManager } from "./SearchManager"; import { StringDecoder } from "string_decoder"; export enum Directory { - parsed_files = "parsed_files", - images = "images", - videos = "videos", - pdfs = "pdfs", - text = "text", - pdf_thumbnails = "pdf_thumbnails", - audio = "audio", + parsed_files = "parsed_files", + images = "images", + videos = "videos", + pdfs = "pdfs", + text = "text", + pdf_thumbnails = "pdf_thumbnails", + audio = "audio", } export function serverPathToFile(directory: Directory, filename: string) { - return normalize(`${filesDirectory}/${directory}/${filename}`); + return normalize(`${filesDirectory}/${directory}/${filename}`); } export function pathToDirectory(directory: Directory) { - return normalize(`${filesDirectory}/${directory}`); + return normalize(`${filesDirectory}/${directory}`); } export function clientPathToFile(directory: Directory, filename: string) { - return `/files/${directory}/${filename}`; + return `/files/${directory}/${filename}`; } export default class UploadManager extends ApiManager { - protected initialize(register: Registration): void { + protected initialize(register: Registration): void { - register({ - method: Method.POST, - subscription: "/uploadFormData", - secureHandler: async ({ req, res }) => { - const form = new formidable.IncomingForm(); - form.keepExtensions = true; - form.uploadDir = pathToDirectory(Directory.parsed_files); - return new Promise<void>(resolve => { - form.parse(req, async (_err, _fields, files) => { - const results: Upload.FileResponse[] = []; - for (const key in files) { - const f = files[key]; - if (!Array.isArray(f)) { - const result = await DashUploadUtils.upload(f); - result && !(result.result instanceof Error) && results.push(result); - } - } - _success(res, results); - resolve(); - }); - }); - } - }); + register({ + method: Method.POST, + subscription: "/uploadFormData", + secureHandler: async ({ req, res }) => { + const form = new formidable.IncomingForm(); + form.keepExtensions = true; + form.uploadDir = pathToDirectory(Directory.parsed_files); + return new Promise<void>(resolve => { + form.parse(req, async (_err, _fields, files) => { + const results: Upload.FileResponse[] = []; + for (const key in files) { + const f = files[key]; + if (!Array.isArray(f)) { + const result = await DashUploadUtils.upload(f); + result && !(result.result instanceof Error) && results.push(result); + } + } + _success(res, results); + resolve(); + }); + }); + } + }); - register({ - method: Method.POST, - subscription: "/uploadYoutubeVideo", - secureHandler: async ({ req, res }) => { - //req.readableBuffer.head.data - return new Promise<void>(async resolve => { - req.addListener("data", async (args) => { - console.log(args); - const payload = String.fromCharCode.apply(String, args); - const videoId = JSON.parse(payload).videoId; - const results: Upload.FileResponse[] = []; - const result = await DashUploadUtils.uploadYoutube(videoId); - result && !(result.result instanceof Error) && results.push(result); - _success(res, results); - resolve(); - }); - }); - } - }); + register({ + method: Method.POST, + subscription: "/uploadYoutubeVideo", + secureHandler: async ({ req, res }) => { + //req.readableBuffer.head.data + return new Promise<void>(async resolve => { + req.addListener("data", async (args) => { + console.log(args); + const payload = String.fromCharCode.apply(String, args); + const videoId = JSON.parse(payload).videoId; + const results: Upload.FileResponse[] = []; + const result = await DashUploadUtils.uploadYoutube(videoId); + result && !(result.result instanceof Error) && results.push(result); + _success(res, results); + resolve(); + }); + }); + } + }); - register({ - method: Method.POST, - subscription: new RouteSubscriber("youtubeScreenshot"), - secureHandler: async ({ req, res }) => { - const { id, timecode } = req.body; - const convert = (raw: string) => { - const number = Math.floor(Number(raw)); - const seconds = number % 60; - const minutes = (number - seconds) / 60; - return `${minutes}m${seconds}s`; - }; - const suffix = timecode ? `&t=${convert(timecode)}` : ``; - const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`; - const buffer = await captureYoutubeScreenshot(targetUrl); - if (!buffer) { - return res.send(); - } - const resolvedName = `youtube_capture_${id}_${suffix}.png`; - const resolvedPath = serverPathToFile(Directory.images, resolvedName); - return new Promise<void>(resolve => { - writeFile(resolvedPath, buffer, async error => { - if (error) { - return res.send(); + register({ + method: Method.POST, + subscription: new RouteSubscriber("youtubeScreenshot"), + secureHandler: async ({ req, res }) => { + const { id, timecode } = req.body; + const convert = (raw: string) => { + const number = Math.floor(Number(raw)); + const seconds = number % 60; + const minutes = (number - seconds) / 60; + return `${minutes}m${seconds}s`; + }; + const suffix = timecode ? `&t=${convert(timecode)}` : ``; + const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`; + const buffer = await captureYoutubeScreenshot(targetUrl); + if (!buffer) { + return res.send(); } - await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images)); - res.send({ - accessPaths: { - agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName) - } - } as Upload.FileInformation); - resolve(); - }); - }); - } - }); + const resolvedName = `youtube_capture_${id}_${suffix}.png`; + const resolvedPath = serverPathToFile(Directory.images, resolvedName); + return new Promise<void>(resolve => { + writeFile(resolvedPath, buffer, async error => { + if (error) { + return res.send(); + } + await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images)); + res.send({ + accessPaths: { + agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName) + } + } as Upload.FileInformation); + resolve(); + }); + }); + } + }); - register({ - method: Method.POST, - subscription: "/uploadRemoteImage", - secureHandler: async ({ req, res }) => { + register({ + method: Method.POST, + subscription: "/uploadRemoteImage", + secureHandler: async ({ req, res }) => { - const { sources } = req.body; - if (Array.isArray(sources)) { - const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); - return res.send(results); - } - res.send(); - } - }); + const { sources } = req.body; + if (Array.isArray(sources)) { + const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); + return res.send(results); + } + res.send(); + } + }); - register({ - method: Method.POST, - subscription: "/uploadDoc", - secureHandler: ({ req, res }) => { + register({ + method: Method.POST, + subscription: "/uploadDoc", + secureHandler: ({ req, res }) => { - const form = new formidable.IncomingForm(); - form.keepExtensions = true; - // let path = req.body.path; - const ids: { [id: string]: string } = {}; - let remap = true; - const getId = (id: string): string => { - if (!remap) return id; - if (id.endsWith("Proto")) return id; - if (id in ids) { - return ids[id]; - } else { - return ids[id] = v4(); - } - }; - const mapFn = (doc: any) => { - if (doc.id) { - doc.id = getId(doc.id); - } - for (const key in doc.fields) { - if (!doc.fields.hasOwnProperty(key)) { continue; } - const field = doc.fields[key]; - if (field === undefined || field === null) { continue; } + const form = new formidable.IncomingForm(); + form.keepExtensions = true; + // let path = req.body.path; + const ids: { [id: string]: string } = {}; + let remap = true; + const getId = (id: string): string => { + if (!remap) return id; + if (id.endsWith("Proto")) return id; + if (id in ids) { + return ids[id]; + } else { + return ids[id] = v4(); + } + }; + const mapFn = (doc: any) => { + if (doc.id) { + doc.id = getId(doc.id); + } + for (const key in doc.fields) { + if (!doc.fields.hasOwnProperty(key)) { continue; } + const field = doc.fields[key]; + if (field === undefined || field === null) { continue; } - if (field.__type === "Doc") { - mapFn(field); - } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") { - field.fieldId = getId(field.fieldId); - } else if (field.__type === "script" || field.__type === "computed") { - if (field.captures) { - field.captures.fieldId = getId(field.captures.fieldId); - } - } else if (field.__type === "list") { - mapFn(field); - } else if (typeof field === "string") { - const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g; - doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => { - return `${p1}${getId(p2)}"`; - }); - } else if (field.__type === "RichTextField") { - const re = /("href"\s*:\s*")(.*?)"/g; - field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => { - return `${p1}${getId(p2)}"`; - }); - } - } - }; - return new Promise<void>(resolve => { - form.parse(req, async (_err, fields, files) => { - remap = fields.remap !== "false"; - let id: string = ""; - try { - for (const name in files) { - const f = files[name]; - const path_2 = Array.isArray(f) ? "" : f.path; - const zip = new AdmZip(path_2); - zip.getEntries().forEach((entry: any) => { - if (!entry.entryName.startsWith("files/")) return; - let directory = dirname(entry.entryName) + "/"; - const extension = extname(entry.entryName); - const base = basename(entry.entryName).split(".")[0]; + if (field.__type === "Doc") { + mapFn(field); + } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") { + field.fieldId = getId(field.fieldId); + } else if (field.__type === "script" || field.__type === "computed") { + if (field.captures) { + field.captures.fieldId = getId(field.captures.fieldId); + } + } else if (field.__type === "list") { + mapFn(field); + } else if (typeof field === "string") { + const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g; + doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => { + return `${p1}${getId(p2)}"`; + }); + } else if (field.__type === "RichTextField") { + const re = /("href"\s*:\s*")(.*?)"/g; + field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => { + return `${p1}${getId(p2)}"`; + }); + } + } + }; + return new Promise<void>(resolve => { + form.parse(req, async (_err, fields, files) => { + remap = fields.remap !== "false"; + let id: string = ""; try { - zip.extractEntryTo(entry.entryName, publicDirectory, true, false); - directory = "/" + directory; + for (const name in files) { + const f = files[name]; + const path_2 = Array.isArray(f) ? "" : f.path; + const zip = new AdmZip(path_2); + zip.getEntries().forEach((entry: any) => { + if (!entry.entryName.startsWith("files/")) return; + let directory = dirname(entry.entryName) + "/"; + const extension = extname(entry.entryName); + const base = basename(entry.entryName).split(".")[0]; + try { + zip.extractEntryTo(entry.entryName, publicDirectory, true, false); + directory = "/" + directory; - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension)); - } catch (e) { - console.log(e); - } - }); - const json = zip.getEntry("doc.json"); - try { - const data = JSON.parse(json.getData().toString("utf8")); - const datadocs = data.docs; - id = getId(data.id); - const docs = Object.keys(datadocs).map(key => datadocs[key]); - docs.forEach(mapFn); - await Promise.all(docs.map((doc: any) => new Promise<void>(res => { - Database.Instance.replace(doc.id, doc, (err, r) => { - err && console.log(err); - res(); - }, true); - }))); - } catch (e) { console.log(e); } - unlink(path_2, () => { }); - } - SolrManager.update(); - res.send(JSON.stringify(id || "error")); - } catch (e) { console.log(e); } - resolve(); - }); - }); - } - }); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension)); + createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension)); + } catch (e) { + console.log(e); + } + }); + const json = zip.getEntry("doc.json"); + try { + const data = JSON.parse(json.getData().toString("utf8")); + const datadocs = data.docs; + id = getId(data.id); + const docs = Object.keys(datadocs).map(key => datadocs[key]); + docs.forEach(mapFn); + await Promise.all(docs.map((doc: any) => new Promise<void>(res => { + Database.Instance.replace(doc.id, doc, (err, r) => { + err && console.log(err); + res(); + }, true); + }))); + } catch (e) { console.log(e); } + unlink(path_2, () => { }); + } + SolrManager.update(); + res.send(JSON.stringify(id || "error")); + } catch (e) { console.log(e); } + resolve(); + }); + }); + } + }); - register({ - method: Method.POST, - subscription: "/inspectImage", - secureHandler: async ({ req, res }) => { + register({ + method: Method.POST, + subscription: "/inspectImage", + secureHandler: async ({ req, res }) => { - const { source } = req.body; - if (typeof source === "string") { - return res.send(await DashUploadUtils.InspectImage(source)); - } - res.send({}); - } - }); + const { source } = req.body; + if (typeof source === "string") { + return res.send(await DashUploadUtils.InspectImage(source)); + } + res.send({}); + } + }); - register({ - method: Method.POST, - subscription: "/uploadURI", - secureHandler: ({ req, res }) => { - const uri = req.body.uri; - const filename = req.body.name; - const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original; - if (!uri || !filename) { - res.status(401).send("incorrect parameters specified"); - return; - } - return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => { - const ext = extname(savedName).toLowerCase(); - const { pngs, jpgs } = AcceptableMedia; - const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [ - { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, - { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }, - { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" }, - ]; - let isImage = false; - if (pngs.includes(ext)) { - resizers.forEach(element => { - element.resizer = element.resizer.png(); - }); - isImage = true; - } else if (jpgs.includes(ext)) { - resizers.forEach(element => { - element.resizer = element.resizer.jpeg(); - }); - isImage = true; - } - if (isImage) { - resizers.forEach(resizer => { - const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext); - createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path)); - }); + register({ + method: Method.POST, + subscription: "/uploadURI", + secureHandler: ({ req, res }) => { + const uri = req.body.uri; + const filename = req.body.name; + const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original; + if (!uri || !filename) { + res.status(401).send("incorrect parameters specified"); + return; + } + return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => { + const ext = extname(savedName).toLowerCase(); + const { pngs, jpgs } = AcceptableMedia; + const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [ + { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, + { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }, + { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" }, + ]; + let isImage = false; + if (pngs.includes(ext)) { + resizers.forEach(element => { + element.resizer = element.resizer.png(); + }); + isImage = true; + } else if (jpgs.includes(ext)) { + resizers.forEach(element => { + element.resizer = element.resizer.jpeg(); + }); + isImage = true; + } + if (isImage) { + resizers.forEach(resizer => { + const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext); + createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path)); + }); - } - res.send(clientPathToFile(Directory.images, filename + ext)); - }); - } - }); + } + res.send(clientPathToFile(Directory.images, filename + ext)); + }); + } + }); - } + } } function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } /** * On success, returns a buffer containing the bytes of a screenshot @@ -309,25 +309,25 @@ function delay(ms: number) { * On failure, returns undefined. */ async function captureYoutubeScreenshot(targetUrl: string) { - // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); - // const page = await browser.newPage(); - // // await page.setViewport({ width: 1920, height: 1080 }); + // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + // const page = await browser.newPage(); + // // await page.setViewport({ width: 1920, height: 1080 }); - // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }); + // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }); - // const videoPlayer = await page.$('.html5-video-player'); - // videoPlayer && await page.focus("video"); - // await delay(7000); - // const ad = await page.$('.ytp-ad-skip-button-text'); - // await ad?.click(); - // await videoPlayer?.click(); - // await delay(1000); - // // hide youtube player controls. - // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none'); + // const videoPlayer = await page.$('.html5-video-player'); + // videoPlayer && await page.focus("video"); + // await delay(7000); + // const ad = await page.$('.ytp-ad-skip-button-text'); + // await ad?.click(); + // await videoPlayer?.click(); + // await delay(1000); + // // hide youtube player controls. + // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none'); - // const buffer = await videoPlayer?.screenshot({ encoding: "binary" }); - // await browser.close(); + // const buffer = await videoPlayer?.screenshot({ encoding: "binary" }); + // await browser.close(); - // return buffer; - return null; + // return buffer; + return null; }
\ No newline at end of file |