aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/Main.tsx7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx19
-rw-r--r--src/client/views/nodes/DataViz.tsx21
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss (renamed from src/client/views/nodes/DataViz.scss)0
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx90
-rw-r--r--src/client/views/nodes/DataVizBox/DrawHelper.ts247
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.tsx159
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.scss22
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.scss105
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.tsx328
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx79
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss33
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx227
-rw-r--r--src/client/views/nodes/VideoBox.tsx33
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx2
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx389
19 files changed, 1370 insertions, 448 deletions
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 17841c055..31fa5b157 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,7 +8,8 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions";
import { Docs } from "../documents/Documents";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { LinkManager } from "../util/LinkManager";
-import { RecordingApi } from "../util/RecordingApi";
+import { ReplayMovements } from '../util/ReplayMovements';
+import { TrackMovements } from "../util/TrackMovements";
import { CollectionView } from "./collections/CollectionView";
import { MainView } from "./MainView";
@@ -37,6 +38,8 @@ AssignAllExtensions();
d.setTime(d.getTime() + (100 * 24 * 60 * 60 * 1000));
const expires = "expires=" + d.toUTCString();
document.cookie = `loadtime=${loading};${expires};path=/`;
- new RecordingApi;
+ new LinkManager();
+ new TrackMovements();
+ new ReplayMovements();
ReactDOM.render(<MainView />, document.getElementById('root'));
})(); \ 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 d33f0b5be..8444c9119 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -17,7 +17,6 @@ import { TraceMobx } from '../../../../fields/util';
import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils';
import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
-import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { DocumentType } from '../../../documents/DocumentTypes';
import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
@@ -25,10 +24,8 @@ import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { HistoryUtil } from '../../../util/History';
import { InteractionUtils } from '../../../util/InteractionUtils';
-import { LinkManager } from '../../../util/LinkManager';
import { RecordingApi } from '../../../util/RecordingApi';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
-import { SearchUtil } from '../../../util/SearchUtil';
import { SelectionManager } from '../../../util/SelectionManager';
import { ColorScheme } from '../../../util/SettingsManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -59,6 +56,8 @@ import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
import React = require('react');
import e = require('connect-flash');
+import { ReplayMovements } from '../../../util/ReplayMovements';
+
export type collectionFreeformViewProps = {
annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -1080,11 +1079,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Doc.UserDoc()?.presentationMode === 'recording' && RecordingApi.Instance.setRecordingFFView(this);
// TODO: make this based off the specific recording FFView
Doc.UserDoc()?.presentationMode === 'none' && RecordingApi.Instance.setPlayFFView(this);
- if (Doc.UserDoc()?.presentationMode === 'watching') {
- RecordingApi.Instance.pauseVideoAndMovements();
- Doc.UserDoc().presentationMode = 'none';
- // RecordingApi.Instance.pauseMovements()
- }
+
+ // TODO: zzz + michael to figure out this merge in case of strange behaviour
+ // if (Doc.UserDoc()?.presentationMode === 'watching') {
+ // RecordingApi.Instance.pauseVideoAndMovements();
+ // Doc.UserDoc().presentationMode = 'none';
+ // // RecordingApi.Instance.pauseMovements()
+ // }
+ // this is the easiest way to do this -> will talk with Bob about using mobx to do this to remove this line of code.
+ if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction();
if (!this.isAnnotationOverlay && clamp) {
// this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
deleted file mode 100644
index d9541dba0..000000000
--- a/src/client/views/nodes/DataViz.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { observer } from "mobx-react";
-import * as React from "react";
-import { ViewBoxBaseComponent } from '../DocComponent';
-import "./DataViz.scss";
-import { FieldView, FieldViewProps } from "./FieldView";
-
-@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
-
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
-
- render() {
- return (
- <div >
- <div>
- Hi
- </div>
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataViz.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index e69de29bb..e69de29bb 100644
--- a/src/client/views/nodes/DataViz.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
new file mode 100644
index 000000000..592723ee9
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -0,0 +1,90 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { StrCast } from "../../../../fields/Types";
+import { ViewBoxBaseComponent } from "../../DocComponent";
+import { FieldViewProps, FieldView } from "../FieldView";
+import "./DataVizBox.scss";
+import { HistogramBox } from "./HistogramBox";
+import { TableBox } from "./TableBox";
+
+enum DataVizView {
+ TABLE = "table",
+ HISTOGRAM= "histogram"
+}
+
+
+@observer
+export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}];
+
+ // TODO: nda - make this use enum values instead
+ // @observable private currView: DataVizView = DataVizView.TABLE;
+ @computed get currView() {
+ if (this.rootDoc._dataVizView) {
+ return StrCast(this.rootDoc._dataVizView);
+ } else {
+ return "table";
+ }
+ }
+
+ constructor(props: any) {
+ super(props);
+ if (!this.rootDoc._dataVizView) {
+ // TODO: nda - this might not always want to default to "table"
+ this.rootDoc._dataVizView = "table";
+ }
+ }
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+
+ @action
+ private createPairs() {
+ const xVals: number[] = [0, 1, 2, 3, 4, 5];
+ // const yVals: number[] = [10, 20, 30, 40, 50, 60];
+ const yVals: number[] = [1, 2, 3, 4, 5, 6];
+ let pairs: {
+ x: number,
+ y:number
+ }[] = [];
+ if (xVals.length != yVals.length) return pairs;
+ for (let i = 0; i < xVals.length; i++) {
+ pairs.push({x: xVals[i], y: yVals[i]});
+ }
+ this.pairs = pairs;
+ return pairs;
+ }
+
+ @computed get selectView() {
+ switch(this.currView) {
+ case "table":
+ return (<TableBox pairs={this.pairs} />)
+ case "histogram":
+ return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ }
+ }
+
+ @computed get pairVals() {
+ return this.createPairs();
+ }
+
+ componentDidMount() {
+ this.createPairs();
+ }
+
+ // handle changing the view using a button
+ @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.rootDoc._dataVizView = this.currView == "table" ? "histogram" : "table";
+ }
+
+ render() {
+ return (
+ <div className="dataViz">
+ <button onClick={(e) => this.changeViewHandler(e)}>Change View</button>
+ {this.selectView}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts
new file mode 100644
index 000000000..595cecebf
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DrawHelper.ts
@@ -0,0 +1,247 @@
+export class PIXIPoint {
+ public get x() { return this.coords[0]; }
+ public get y() { return this.coords[1]; }
+ public set x(value: number) { this.coords[0] = value; }
+ public set y(value: number) { this.coords[1] = value; }
+ public coords: number[] = [0, 0];
+ constructor(x: number, y: number) {
+ this.coords[0] = x;
+ this.coords[1] = y;
+ }
+}
+
+export class PIXIRectangle {
+ public x: number;
+ public y: number;
+ public width: number;
+ public height: number;
+ public get left() { return this.x; }
+ public get right() { return this.x + this.width; }
+ public get top() { return this.y; }
+ public get bottom() { return this.top + this.height; }
+ public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); }
+ constructor(x: number, y: number, width: number, height: number) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+}
+
+export class MathUtil {
+
+ public static EPSILON: number = 0.001;
+
+ public static Sign(value: number): number {
+ return value >= 0 ? 1 : -1;
+ }
+
+ public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x += p2.x;
+ p1.y += p2.y;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
+ }
+ }
+
+ public static Perp(p1: PIXIPoint): PIXIPoint {
+ return new PIXIPoint(-p1.y, p1.x);
+ }
+
+ public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x /= by;
+ p1.y /= by;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x / by, p1.y / by);
+ }
+ }
+
+ public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
+ if (inline) {
+ p1.x *= by;
+ p1.y *= by;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x * by, p1.y * by);
+ }
+ }
+
+ public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x -= p2.x;
+ p1.y -= p2.y;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
+ }
+ }
+
+ public static Area(rect: PIXIRectangle): number {
+ return rect.width * rect.height;
+ }
+
+ public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
+ // Return minimum distance between line segment vw and point p
+ var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt
+ if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case
+ // Consider the line extending the segment, parameterized as v + t (w - v).
+ // We find projection of point p onto the line.
+ // It falls where t = [(p-v) . (w-v)] / |w-v|^2
+ // We clamp t from [0,1] to handle points outside the segment vw.
+ var dot = MathUtil.Dot(
+ MathUtil.SubtractPoint(p, v),
+ MathUtil.SubtractPoint(w, v)) / l2;
+ var t = Math.max(0, Math.min(1, dot));
+ // Projection falls on the segment
+ var projection = MathUtil.AddPoint(v,
+ MathUtil.MultiplyConstant(
+ MathUtil.SubtractPoint(w, v), t));
+ return MathUtil.Dist(p, projection);
+ }
+
+ public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
+ var a1 = pe1.y - ps1.y;
+ var b1 = ps1.x - pe1.x;
+
+ var a2 = pe2.y - ps2.y;
+ var b2 = ps2.x - pe2.x;
+
+ var delta = a1 * b2 - a2 * b1;
+ if (delta === 0) {
+ return undefined;
+ }
+ var c2 = a2 * ps2.x + b2 * ps2.y;
+ var c1 = a1 * ps1.x + b1 * ps1.y;
+ var invdelta = 1 / delta;
+ return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
+ }
+
+ public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
+ if (p.x < rect.left - this.EPSILON) {
+ return false;
+ }
+ if (p.x > rect.right + this.EPSILON) {
+ return false;
+ }
+ if (p.y < rect.top - this.EPSILON) {
+ return false;
+ }
+ if (p.y > rect.bottom + this.EPSILON) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
+ var r1 = new PIXIPoint(rect.left, rect.top);
+ var r2 = new PIXIPoint(rect.right, rect.top);
+ var r3 = new PIXIPoint(rect.right, rect.bottom);
+ var r4 = new PIXIPoint(rect.left, rect.bottom);
+ var ret = new Array<PIXIPoint>();
+ var dist = this.Dist(lineFrom, lineTo);
+ var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ return ret;
+ }
+
+ public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
+ const left = Math.max(rect1.x, rect2.x);
+ const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
+ const top = Math.max(rect1.y, rect2.y);
+ const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
+ return new PIXIRectangle(left, top, right - left, bottom - top);
+ }
+
+ public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
+ return Math.sqrt(MathUtil.DistSquared(p1, p2));
+ }
+
+ public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
+ return p1.x * p2.x + p1.y * p2.y;
+ }
+
+ public static Normalize(p1: PIXIPoint) {
+ var d = this.Length(p1);
+ return new PIXIPoint(p1.x / d, p1.y / d);
+ }
+
+ public static Length(p1: PIXIPoint): number {
+ return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
+ }
+
+ public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
+ const a = p1.x - p2.x;
+ const b = p1.y - p2.y;
+ return (a * a + b * b);
+ }
+
+ public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
+ return !(r2.x > r1.x + r1.width ||
+ r2.x + r2.width < r1.x ||
+ r2.y > r1.y + r1.height ||
+ r2.y + r2.height < r1.y);
+ }
+
+ public static ArgMin(temp: number[]): number {
+ let index = 0;
+ let value = temp[0];
+ for (let i = 1; i < temp.length; i++) {
+ if (temp[i] < value) {
+ value = temp[i];
+ index = i;
+ }
+ }
+ return index;
+ }
+
+ public static ArgMax(temp: number[]): number {
+ let index = 0;
+ let value = temp[0];
+ for (let i = 1; i < temp.length; i++) {
+ if (temp[i] > value) {
+ value = temp[i];
+ index = i;
+ }
+ }
+ return index;
+ }
+
+ public static Combinations<T>(chars: T[]) {
+ let result = new Array<T>();
+ let f = (prefix: any, chars: any) => {
+ for (let i = 0; i < chars.length; i++) {
+ result.push(prefix.concat(chars[i]));
+ f(prefix.concat(chars[i]), chars.slice(i + 1));
+ }
+ };
+ f([], chars);
+ return result;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss
new file mode 100644
index 000000000..5aac9dc77
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/HistogramBox.scss
@@ -0,0 +1,18 @@
+// change the stroke color of line-svg class
+.svgLine {
+ position: absolute;
+ background: darkGray;
+ stroke: #000;
+ stroke-width: 1px;
+ width:100%;
+ height:100%;
+ opacity: 0.4;
+}
+
+.svgContainer {
+ position: absolute;
+ top:0;
+ left:0;
+ width:100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
new file mode 100644
index 000000000..00dc2ef46
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
@@ -0,0 +1,159 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc } from "../../../../fields/Doc";
+import { NumCast } from "../../../../fields/Types";
+import "./HistogramBox.scss";
+
+interface HistogramBoxProps {
+ rootDoc: Doc;
+ pairs: {
+ x: number,
+ y: number
+ }[]
+}
+
+
+export class HistogramBox extends React.Component<HistogramBoxProps> {
+
+ private origin = {x: 0.1 * this.width, y: 0.9 * this.height};
+
+ @computed get width() {
+ return NumCast(this.props.rootDoc.width);
+ }
+
+ @computed get height() {
+ return NumCast(this.props.rootDoc.height);
+ }
+
+ @computed get x() {
+ return NumCast(this.props.rootDoc.x);
+ }
+
+ @computed get y() {
+ return NumCast(this.props.rootDoc.y);
+ }
+
+ @computed get generatePoints() {
+ // evenly distribute points along the x axis
+ const xVals: number[] = this.props.pairs.map(p => p.x);
+ const yVals: number[] = this.props.pairs.map(p => p.y);
+
+ const xMin = Math.min(...xVals);
+ const xMax = Math.max(...xVals);
+ const yMin = Math.min(...yVals);
+ const yMax = Math.max(...yVals);
+
+ const xRange = xMax - xMin;
+ const yRange = yMax - yMin;
+
+ const xScale = this.width / xRange;
+ const yScale = this.height / yRange;
+
+ const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale;
+ const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale;
+
+ const points: {
+ x: number,
+ y: number
+ }[] = this.props.pairs.map(p => {
+ return {
+ x: (p.x * xScale + xOffset) + this.origin.x,
+ y: (p.y * yScale + yOffset)
+ }
+ });
+
+ return points;
+ }
+
+ @computed get generateGraphLine() {
+ const points = this.generatePoints;
+ // loop through points and create a line from each point to the next
+ let lines: {
+ x1: number,
+ y1: number,
+ x2: number,
+ y2: number
+ }[] = [];
+ for (let i = 0; i < points.length - 1; i++) {
+ lines.push({
+ x1: points[i].x,
+ y1: points[i].y,
+ x2: points[i + 1].x,
+ y2: points[i + 1].y
+ });
+ }
+ // generate array of svg with lines
+ let svgLines: JSX.Element[] = [];
+ for (let i = 0; i < lines.length; i++) {
+ svgLines.push(
+ <line
+ className="svgLine"
+ key={i}
+ x1={lines[i].x1}
+ y1={lines[i].y1}
+ x2={lines[i].x2}
+ y2={lines[i].y2}
+ stroke="black"
+ strokeWidth={2}
+ />
+ );
+ }
+
+ let res = [];
+ for (let i = 0; i < svgLines.length; i++) {
+ res.push(<svg className="svgContainer">{svgLines[i]}</svg>)
+ }
+ return res;
+ }
+
+ @computed get generateAxes() {
+
+ const xAxis = {
+ x1: 0.1 * this.width,
+ x2: 0.9 * this.width,
+ y1: 0.9 * this.height,
+ y2: 0.9 * this.height,
+ };
+
+ const yAxis = {
+ x1: 0.1 * this.width,
+ x2: 0.1 * this.width,
+ y1: 0.25 * this.height,
+ y2: 0.9 * this.height,
+ };
+
+
+ return (
+ [
+ (<svg className="svgContainer">
+ {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */}
+ <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/>
+
+ {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
+ </svg>),
+ (
+ <svg className="svgContainer">
+ <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} />
+ {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
+ </svg>)
+ ]
+ )
+ }
+
+
+ render() {
+ return (
+ <div>histogram box
+ {/* <svg className="svgContainer">
+ {this.generateSVGLine}
+ </svg> */}
+ {this.generateAxes[0]}
+ {this.generateAxes[1]}
+ {this.generateGraphLine.map(line => line)}
+ </div>
+ )
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.scss b/src/client/views/nodes/DataVizBox/TableBox.scss
new file mode 100644
index 000000000..1264d6a46
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TableBox.scss
@@ -0,0 +1,22 @@
+.table {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.table-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 5px;
+ border-bottom: 1px solid #ccc;
+}
+
+.table-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
new file mode 100644
index 000000000..dfa8262d8
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TableBox.tsx
@@ -0,0 +1,37 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+
+interface TableBoxProps {
+ pairs: {x: number, y:number}[]
+}
+
+
+export class TableBox extends React.Component<TableBoxProps> {
+
+
+
+ render() {
+ return (
+ <div className="table-container">
+ <table className="table">
+ <thead>
+ <tr className="table-row">
+ <th>x</th>
+ <th>y</th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.props.pairs.map(p => {
+ return (<tr className="table-row">
+ <td>{p.x}</td>
+ <td>{p.y}</td>
+ </tr>)
+ })}
+ </tbody>
+ </table>
+ </div>
+ )
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 371d85a32..96ac3e332 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,7 +19,7 @@ import { AudioBox } from "./AudioBox";
import { FontIconBox } from "./button/FontIconBox";
import { ColorBox } from "./ColorBox";
import { ComparisonBox } from "./ComparisonBox";
-import { DataVizBox } from "./DataViz";
+import { DataVizBox } from "./DataVizBox/DataVizBox";
import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { EquationBox } from "./EquationBox";
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index a493b0b89..28ad25ffa 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -1,11 +1,19 @@
.progressbar {
+ touch-action: none;
+ vertical-align: middle;
+ text-align: center;
+
+ align-items: center;
+ cursor: default;
+
+
position: absolute;
display: flex;
justify-content: flex-start;
- bottom: 10px;
- width: 80%;
- height: 5px;
+ bottom: 2px;
+ width: 99%;
+ height: 30px;
background-color: gray;
&.done {
@@ -23,4 +31,93 @@
z-index: 3;
pointer-events: none;
}
-} \ No newline at end of file
+}
+
+.progressbar-disabled {
+ cursor: not-allowed;
+}
+
+.progressbar-dragging {
+ cursor: grabbing;
+}
+
+// citation: https://codepen.io/_Master_/pen/PRdjmQ
+@keyframes blinker {
+ from {opacity: 1.0;}
+ to {opacity: 0.0;}
+}
+.blink {
+ text-decoration: blink;
+ animation-name: blinker;
+ animation-duration: 0.6s;
+ animation-iteration-count:infinite;
+ animation-timing-function:ease-in-out;
+ animation-direction: alternate;
+}
+
+.segment {
+ border: 3px solid black;
+ background-color: red;
+ margin: 1px;
+ padding: 0;
+ cursor: pointer;
+ transition-duration: .5s;
+ user-select: none;
+
+ vertical-align: middle;
+ text-align: center;
+}
+
+.segment-expanding {
+border-color: red;
+ background-color: white;
+ transition-duration: 0s;
+ opacity: .75;
+ pointer-events: none;
+}
+
+.segment-expanding:hover {
+ background-color: inherit;
+ cursor: not-allowed;
+}
+
+.segment-disabled {
+ pointer-events: none;
+ opacity: 0.5;
+ transition-duration: 0s;
+ /* Hide the text. */
+ text-indent: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.segment-hide {
+ background-color: inherit;
+ text-align: center;
+ vertical-align: middle;
+ user-select: none;
+}
+
+.segment:first-child {
+ margin-left: 2px;
+}
+.segment:last-child {
+ margin-right: 2px;
+}
+
+.segment:hover {
+ background-color: white;
+}
+
+.segment:hover, .segment-selected {
+ margin: 0px;
+ border: 4px solid red;
+ border-radius: 2px;
+}
+
+.segment-selected {
+ border: 4px solid #202020;
+ background-color: red;
+ opacity: .75;
+ cursor: grabbing;
+}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
index 82d5e1f04..1bb2b7c84 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -1,45 +1,301 @@
import * as React from 'react';
-import { useEffect } from "react"
+import { useEffect, useState, useCallback, useRef } from "react"
import "./ProgressBar.scss"
+import { MediaSegment } from './RecordingView';
interface ProgressBarProps {
- progress: number,
- marks: number[],
+ videos: MediaSegment[],
+ setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
+ orderVideos: boolean,
+ progress: number,
+ recording: boolean,
+ doUndo: boolean,
+ setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>,
+}
+
+interface SegmentBox {
+ endTime: number,
+ startTime: number,
+ order: number,
+}
+interface CurrentHover {
+ index: number,
+ minX: number,
+ maxX: number
}
export function ProgressBar(props: ProgressBarProps) {
+ const progressBarRef = useRef<HTMLDivElement | null>(null)
+
+ // the actual list of JSX elements rendered as segments
+ const [segments, setSegments] = useState<JSX.Element[]>([]);
+ // array for the order of video segments
+ const [ordered, setOrdered] = useState<SegmentBox[]>([]);
+
+ const [undoStack, setUndoStack] = useState<SegmentBox[]>([]);
+
+ // -1 if no segment is currently being dragged around; else, it is the id of that segment over
+ // NOTE: the id of a segment is its index in the ordered array
+ const [dragged, setDragged] = useState<number>(-1);
+
+ // length of the time removed from the video, in seconds*100
+ const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
+
+ // this holds the index of the videoc segment to be removed
+ const [removed, setRemoved] = useState<number>(-1);
+
+ // update the canUndo props based on undo stack
+ useEffect(() => props.setCanUndo?.(undoStack.length > 0), [undoStack.length]);
+
+ // useEffect for undo - brings back the most recently deleted segment
+ useEffect(() => handleUndo(), [props.doUndo])
+ const handleUndo = () => {
+ // get the last element from the undo if it exists
+ if (undoStack.length === 0) return;
+ // get and remove the last element from the undo stack
+ const last = undoStack.lastElement();
+ setUndoStack(prevUndo => prevUndo.slice(0, -1));
+
+ // update the removed time and place element back into ordered
+ setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime));
+ setOrdered(prevOrdered => [...prevOrdered, last]);
+ }
+
+ // useEffect for recording changes - changes style to disabled and adds the "expanding-segment"
+ useEffect(() => {
+ // get segments segment's html using it's id -> make them appeared disabled (or enabled)
+ segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
+ progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);
+
+ if (props.recording)
+ setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'fit-content' }}>{props.videos.length + 1}</div>]);
+ }, [props.recording])
+
+
+ // useEffect that updates the segmentsJSX, which is rendered
+ // only updated when ordered is updated or if the user is dragging around a segment
+ useEffect(() => {
+ const totalTime = props.progress * 1000 - totalRemovedTime;
+ const segmentsJSX = ordered.map((seg, i) =>
+ <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>);
+
+ setSegments(segmentsJSX)
+ }, [dragged, ordered]);
+
+ // useEffect for dragged - update the cursor to be grabbing while grabbing
+ useEffect(() => {
+ progressBarRef.current?.classList.toggle('progressbar-dragging', dragged !== -1);
+ }, [dragged]);
+
+ // to imporve performance, only want to update the CSS width, not re-render the whole JSXList
+ useEffect(() => {
+ if (!props.recording) return
+ const totalTime = props.progress * 1000 - totalRemovedTime;
+ let remainingTime = totalTime;
+ segments.forEach((seg, i) => {
+ // for the last segment, we need to set that directly
+ if (i === segments.length - 1) return;
+ // update remaining time
+ remainingTime -= (ordered[i].endTime - ordered[i].startTime);
+
+ // update the width for this segment
+ const htmlId = seg.props.id;
+ const segmentHtml = document.getElementById(htmlId);
+ if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
+ });
+
+ // update the width of the expanding segment using the remaining time
+ const segExapandHtml = document.getElementById('segment-expanding');
+ if (segExapandHtml)
+ segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`;
+ }, [props.progress]);
+
+ // useEffect for props.videos - update the ordered array when a new video is added
+ useEffect(() => {
+ // this useEffect fired when the videos are being rearragned to the order
+ // in this case, do nothing.
+ if (props.orderVideos) return;
+
+ const order = props.videos.length - 1;
+ // in this case, a new video is added -> push it onto ordered
+ if (order >= ordered.length) {
+ const { endTime, startTime } = props.videos.lastElement();
+ setOrdered(prevOrdered => {
+ return [...prevOrdered, { endTime, startTime, order }];
+ });
+ }
+
+ // in this case, a video is removed
+ else if (order < ordered.length) {
+ console.warn('warning: video removed from parent');
+ }
+ }, [props.videos]);
+
+ // useEffect for props.orderVideos - matched the order array with the videos array before the export
+ useEffect(() => props.setVideos(vids => ordered.map((seg) => vids[seg.order])), [props.orderVideos]);
+
+ // useEffect for removed - handles logic for removing a segment
+ useEffect(() => {
+ if (removed === -1) return;
+ // update total removed time
+ setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
+
+ // put the element on the undo stack
+ setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
+ // remove the segment from the array
+ setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
+ // reset to default/nullish state
+ setRemoved(-1);
+ }, [removed]);
+
+ // returns the new currentHover based on the new index
+ const updateCurrentHover = (segId: number): CurrentHover | null => {
+ // get the segId of the segment that will become the new bounding area
+ const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
+ if (rect == null) return null
+ return {
+ index: segId,
+ minX: rect.x,
+ maxX: rect.x + rect.width,
+ }
+ }
+
+ // pointerdown event for the progress bar
+ const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ // don't move the videobox element
+ e.stopPropagation();
+
+ // if recording, do nothing
+ if (props.recording) return;
+
+ // get the segment the user clicked on to be dragged
+ const clickedSegment = e.target as HTMLDivElement & EventTarget
+
+ // get the profess bar ro add event listeners
+ // don't do anything if null
+ const progressBar = progressBarRef.current
+ if (progressBar == null || clickedSegment.id === progressBar.id) return
+
+ // if holding shift key, let's remove that segment
+ if (e.shiftKey) {
+ const segId = parseInt(clickedSegment.id.split('-')[1]);
+ setRemoved(segId);
+ return
+ }
+
+ // if holding ctrl key and click, let's undo that segment #hiddenfeature lol
+ if (e.ctrlKey) {
+ handleUndo();
+ return;
+ }
+
+ // if we're here, the user is dragging a segment around
+ // let the progress bar capture all the pointer events until the user releases (pointerUp)
+ const ptrId = e.pointerId;
+ progressBar.setPointerCapture(ptrId)
+
+ const rect = clickedSegment.getBoundingClientRect()
+ // id for segment is like 'segment-1' or 'segment-10',
+ // so this works to get the id
+ const segId = parseInt(clickedSegment.id.split('-')[1])
+ // set the selected segment to be the one dragged
+ setDragged(segId)
+
+ // this is the logic for storing the lower X bound and upper X bound to know
+ // whether a swap is needed between two segments
+ let currentHover: CurrentHover = {
+ index: segId,
+ minX: rect.x,
+ maxX: rect.x + rect.width,
+ }
+
+ // create the floating segment that tracks the cursor
+ const detchedSegment = document.createElement("div")
+ initDeatchSegment(detchedSegment, rect);
+
+ const updateSegmentOrder = (event: PointerEvent): void => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
+ if (!progressBar.hasPointerCapture(ptrId)) {
+ placeSegmentandCleanup();
+ return;
+ }
+
+ followCursor(event, detchedSegment);
+
+ const curX = event.clientX;
+ // handle the left bound
+ if (curX < currentHover.minX && currentHover.index > 0) {
+ swapSegments(currentHover.index, currentHover.index - 1)
+ currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover
+ }
+ // handle the right bound
+ else if (curX > currentHover.maxX && currentHover.index < segments.length - 1) {
+ swapSegments(currentHover.index, currentHover.index + 1)
+ currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover
+ }
+ }
+
+ // handles when the user is done dragging the segment (pointerUp)
+ const placeSegmentandCleanup = (event?: PointerEvent): void => {
+ event?.stopPropagation();
+ event?.preventDefault();
+ // if they put the segment outside of the bounds, remove it
+ if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight))
+ setRemoved(currentHover.index);
+
+ // remove the update event listener for pointermove
+ progressBar.removeEventListener('pointermove', updateSegmentOrder);
+ // remove the floating segment from the DOM
+ detchedSegment.remove();
+ // dragged is -1 is equiv to nothing being dragged, so the normal state
+ // so this will place the segment in it's location and update the segment bar
+ setDragged(-1);
+ }
+
+ // event listeners that allow the user to drag and release the floating segment
+ progressBar.addEventListener('pointermove', updateSegmentOrder);
+ progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true });
+ }
+
+ const swapSegments = (oldIndex: number, newIndex: number) => {
+ if (newIndex == null) return;
+ setOrdered(prevOrdered => {
+ const temp = { ...prevOrdered[oldIndex] }
+ prevOrdered[oldIndex] = prevOrdered[newIndex]
+ prevOrdered[newIndex] = temp
+ return prevOrdered
+ });
+ // update visually where the segment is hovering over
+ setDragged(newIndex);
+ }
+
+ // functions for the floating segment that tracks the cursor while grabbing it
+ const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => {
+ dot.classList.add("segment-selected");
+ dot.style.transitionDuration = '0s';
+ dot.style.position = 'absolute';
+ dot.style.zIndex = '999';
+ dot.style.width = `${rect.width}px`;
+ dot.style.height = `${rect.height}px`;
+ dot.style.left = `${rect.x}px`;
+ dot.style.top = `${rect.y}px`;
+ dot.draggable = false;
+ document.body.append(dot);
+ }
+ const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => {
+ // event.stopPropagation()
+ const { width, height } = dot.getBoundingClientRect();
+ dot.style.left = `${event.clientX - width / 2}px`;
+ dot.style.top = `${event.clientY - height / 2}px`;
+ }
+
- // 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>
- )
+ return (
+ <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
+ {segments}
+ </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
index 10393624b..0ff7c4292 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -8,54 +8,51 @@ import { FieldView } from "../FieldView";
import { VideoBox } from "../VideoBox";
import { RecordingView } from './RecordingView';
import { DocumentType } from "../../../documents/DocumentTypes";
-import { RecordingApi } from "../../../util/RecordingApi";
-import { Doc, FieldsSym } from "../../../../fields/Doc";
+import { Presentation } from "../../../util/TrackMovements";
+import { Doc } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
@observer
export class RecordingBox extends ViewBoxBaseComponent() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
- private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
+ constructor(props: any) {
super(props);
- }
-
- componentDidMount() {
- console.log("set native width and height")
- Doc.SetNativeWidth(this.dataDoc, 1280);
- Doc.SetNativeHeight(this.dataDoc, 720);
- }
-
- @observable result: Upload.FileInformation | undefined = undefined
- @observable videoDuration: number | undefined = undefined
-
- @action
- setVideoDuration = (duration: number) => {
- this.videoDuration = duration
- }
-
- @action
- setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
- this.result = info
- this.dataDoc.type = DocumentType.VID;
- this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
-
- this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
- this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
- this.dataDoc[this.fieldKey + "-recorded"] = true;
- // stringify the presenation and store it
- if (trackScreen) {
- this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
- }
- }
-
- render() {
- return <div className="recordingBox" ref={this._ref}>
- {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={Doc.GetProto(this.rootDoc)[Id]} />}
- </div>;
- }
+ }
+
+ componentDidMount() {
+ Doc.SetNativeWidth(this.dataDoc, 1280);
+ Doc.SetNativeHeight(this.dataDoc, 720);
+ }
+
+ @observable result: Upload.AccessPathInfo | undefined = undefined
+ @observable videoDuration: number | undefined = undefined
+
+ @action
+ setVideoDuration = (duration: number) => {
+ this.videoDuration = duration
+ }
+
+ @action
+ setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
+ this.result = info
+ this.dataDoc.type = DocumentType.VID;
+ this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
+
+ this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
+ this.dataDoc[this.fieldKey + "-recorded"] = true;
+ // stringify the presentation and store it
+ presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
+ }
+
+ render() {
+ return <div className="recordingBox" ref={this._ref}>
+ {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
+ </div>;
+ }
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index 9b2f6d070..2e6f6bc26 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -18,13 +18,13 @@ button {
width: 100%;
// display: flex;
pointer-events: all;
- background-color: grey;
+ background-color: black;
}
.video-wrapper {
// max-width: 600px;
// max-width: 700px;
- position: relative;
+ // position: relative;
display: flex;
justify-content: center;
// overflow: hidden;
@@ -33,7 +33,7 @@ button {
}
.video-wrapper:hover .controls {
- bottom: 30px;
+ bottom: 34.5px;
transform: translateY(0%);
opacity: 100%;
}
@@ -43,8 +43,8 @@ button {
align-items: center;
justify-content: space-evenly;
position: absolute;
- padding: 14px;
- width: 100%;
+ // padding: 14px;
+ //width: 100%;
max-width: 500px;
// max-height: 20%;
flex-wrap: wrap;
@@ -56,7 +56,14 @@ button {
// transform: translateY(150%);
transition: all 0.3s ease-in-out;
// opacity: 0%;
- bottom: 30px;
+ bottom: 34.5px;
+ height: 60px;
+ right: 2px;
+ // bottom: -150px;
+}
+
+.controls:active {
+ bottom: 40px;
// bottom: -150px;
}
@@ -127,9 +134,8 @@ button {
.controls-inner-container {
display: flex;
flex-direction: row;
- justify-content: center;
- width: 100%;
-
+ align-content: center;
+ position: relative;
}
.record-button-wrapper {
@@ -180,14 +186,14 @@ button {
height: 100%;
display: flex;
flex-direction: row;
- align-items: center;
- position: absolute;
+ align-content: center;
+ position: relative;
top: 0;
bottom: 0;
&.video-edit-wrapper {
- right: 50% - 15;
+ // right: 50% - 15;
.track-screen {
font-weight: 200;
@@ -197,10 +203,11 @@ button {
&.track-screen-wrapper {
- right: 50% - 30;
+ // right: 50% - 30;
.track-screen {
font-weight: 200;
+ color: aqua;
}
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index b95335792..ec5917b9e 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -1,23 +1,24 @@
import * as React from 'react';
import "./RecordingView.scss";
-import { ReactElement, useCallback, useEffect, useRef, useState } from "react";
+import { 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';
+import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { Presentation, TrackMovements } from '../../../util/TrackMovements';
-import { RecordingApi } from '../../../util/RecordingApi';
-import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-
-interface MediaSegment {
+export interface MediaSegment {
videoChunks: any[],
- endTime: number
+ endTime: number,
+ startTime: number,
+ presentation?: Presentation,
}
interface IRecordingViewProps {
- setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
+ setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
setDuration: (seconds: number) => void
id: string
}
@@ -32,12 +33,18 @@ export function RecordingView(props: IRecordingViewProps) {
const [playing, setPlaying] = useState(false);
const [progress, setProgress] = useState(0);
+ // acts as a "refresh state" to tell progressBar when to undo
+ const [doUndo, setDoUndo] = useState(false);
+ // whether an undo can occur or not
+ const [canUndo, setCanUndo] = useState(false);
+
const [videos, setVideos] = useState<MediaSegment[]>([]);
+ const [orderVideos, setOrderVideos] = useState<boolean>(false);
const videoRecorder = useRef<MediaRecorder | null>(null);
const videoElementRef = useRef<HTMLVideoElement | null>(null);
- const [finished, setFinished] = useState<boolean>(false)
- const [trackScreen, setTrackScreen] = useState<boolean>(true)
+ const [finished, setFinished] = useState<boolean>(false);
+ const [trackScreen, setTrackScreen] = useState<boolean>(false);
@@ -45,53 +52,42 @@ export function RecordingView(props: IRecordingViewProps) {
video: {
width: 1280,
height: 720,
+
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
- }
+ };
useEffect(() => {
-
if (finished) {
- props.setDuration(recordingTimer * 100)
- let allVideoChunks: any = []
- videos.forEach((vid) => {
- console.log(vid.videoChunks)
- allVideoChunks = allVideoChunks.concat(vid.videoChunks)
- })
-
- const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
-
- Networking.UploadFilesToServer(videoFile)
- .then((data) => {
- const result = data[0].result
- if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
- props.setResult(result, trackScreen)
- } else {
- alert("video conversion failed");
- }
- })
-
- }
+ // make the total presentation that'll match the concatted video
+ let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+ // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
+ (async () => {
+ const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
- }, [finished])
+ // upload the segments to the server and get their server access paths
+ const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
+ .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
- useEffect(() => {
- // check if the browser supports media devices on first load
- if (!navigator.mediaDevices) {
- console.log('This browser does not support getUserMedia.')
+ // concat the segments together using post call
+ const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
+ !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+ })();
}
- console.log('This device has the correct media devices.')
- }, [])
+ }, [videos]);
+ // this will call upon the progress bar to edit videos to be in the correct order
useEffect(() => {
- // get access to the video element on every render
- videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
- })
+ finished && setOrderVideos(true);
+ }, [finished]);
+
+ // check if the browser supports media devices on first load
+ useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
useEffect(() => {
let interval: any = null;
@@ -103,121 +99,108 @@ export function RecordingView(props: IRecordingViewProps) {
clearInterval(interval);
}
return () => clearInterval(interval);
- }, [recording])
+ }, [recording]);
useEffect(() => {
setVideoProgressHelper(recordingTimer)
recordingTimerRef.current = recordingTimer;
- }, [recordingTimer])
+ }, [recordingTimer]);
const setVideoProgressHelper = (progress: number) => {
const newProgress = (progress / MAXTIME) * 100;
- setProgress(newProgress)
+ setProgress(newProgress);
}
+
const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
- const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
+ const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
- videoElementRef.current!.src = ""
- videoElementRef.current!.srcObject = stream
- videoElementRef.current!.muted = true
+ videoElementRef.current!.src = "";
+ videoElementRef.current!.srcObject = stream;
+ videoElementRef.current!.muted = true;
- return stream
+ return stream;
}
const record = async () => {
- const stream = await startShowingStream();
- videoRecorder.current = new MediaRecorder(stream)
+ // don't need to start a new stream every time we start recording a new segment
+ if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream());
// temporary chunks of video
- let videoChunks: any = []
+ let videoChunks: any = [];
videoRecorder.current.ondataavailable = (event: any) => {
- if (event.data.size > 0) {
- videoChunks.push(event.data)
- }
- }
+ if (event.data.size > 0) videoChunks.push(event.data);
+ };
videoRecorder.current.onstart = (event: any) => {
setRecording(true);
- trackScreen && RecordingApi.Instance.start();
- }
+ // start the recording api when the video recorder starts
+ trackScreen && TrackMovements.Instance.start();
+ };
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 }])
+ const nextVideo = {
+ videoChunks,
+ endTime: recordingTimerRef.current,
+ startTime: videos?.lastElement()?.endTime || 0
+ };
+
+ // depending on if a presenation exists, add it to the video
+ const presentation = TrackMovements.Instance.yieldPresentation();
+ setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
}
// reset the temporary chunks
- videoChunks = []
+ videoChunks = [];
setRecording(false);
- setFinished(true);
- trackScreen && RecordingApi.Instance.pause();
}
- // recording paused
- videoRecorder.current.onpause = (event: any) => {
- // append the current portion to the video pieces
- setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+ videoRecorder.current.start(200);
+ }
- // reset the temporary chunks
- videoChunks = []
- setRecording(false);
- trackScreen && RecordingApi.Instance.pause();
- }
- videoRecorder.current.onresume = async (event: any) => {
- await startShowingStream();
- setRecording(true);
- trackScreen && RecordingApi.Instance.resume();
- }
+ // if this is called, then we're done recording all the segments
+ const finish = (e: React.PointerEvent) => {
+ e.stopPropagation();
- videoRecorder.current.start(200)
- }
+ // call stop on the video recorder if active
+ videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+ // end the streams (audio/video) to remove recording icon
+ const stream = videoElementRef.current!.srcObject;
+ stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
- const stop = () => {
- if (videoRecorder.current) {
- if (videoRecorder.current.state !== "inactive") {
- videoRecorder.current.stop();
- // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
- }
- }
+ // finish/clear the recoringApi
+ TrackMovements.Instance.finish();
+
+ // this will call upon progessbar to update videos to be in the correct order
+ setFinished(true);
}
- const pause = () => {
- if (videoRecorder.current) {
- if (videoRecorder.current.state === "recording") {
- videoRecorder.current.pause();
- }
- }
+ const pause = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ // if recording, then this is just a new segment
+ videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
}
- const startOrResume = (e: React.PointerEvent) => {
- // the code to start or resume does not get triggered if we start dragging the button
+ const start = (e: React.PointerEvent) => {
setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
- if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
- record();
- } else if (videoRecorder.current.state === "paused") {
- videoRecorder.current.resume();
- }
+ // start recording if not already recording
+ if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
+
return true; // cancels propagation to documentView to avoid selecting it.
}, false, 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 undoPrevious = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ setDoUndo(prev => !prev);
}
- const handleOnTimeUpdate = () => {
- if (playing) {
- setVideoProgressHelper(videoElementRef.current!.currentTime)
- }
- };
+ const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
const millisecondToMinuteSecond = (milliseconds: number) => {
const toTwoDigit = (digit: number) => {
@@ -234,7 +217,8 @@ export function RecordingView(props: IRecordingViewProps) {
<video id={`video-${props.id}`}
autoPlay
muted
- onTimeUpdate={handleOnTimeUpdate}
+ onTimeUpdate={() => handleOnTimeUpdate()}
+ ref={videoElementRef}
/>
<div className="recording-sign">
<span className="dot" />
@@ -246,18 +230,18 @@ export function RecordingView(props: IRecordingViewProps) {
<div className="record-button-wrapper">
{recording ?
<button className="stop-button" onPointerDown={pause} /> :
- <button className="record-button" onPointerDown={startOrResume} />
+ <button className="record-button" onPointerDown={start} />
}
</div>
{!recording && (videos.length > 0 ?
<div className="options-wrapper video-edit-wrapper">
- {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
- <MdBackspace onClick={clearPrevious} />
- </IconContext.Provider> */}
+ <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
+ <MdBackspace onPointerDown={undoPrevious} />
+ </IconContext.Provider>
<IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
- <FaCheckCircle onClick={stop} />
+ <FaCheckCircle onPointerDown={finish} />
</IconContext.Provider>
</div>
@@ -271,12 +255,17 @@ export function RecordingView(props: IRecordingViewProps) {
</div>
- <ProgressBar
- progress={progress}
- marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
- // playSegment={playSegment}
- />
</div>
+
+ <ProgressBar
+ videos={videos}
+ setVideos={setVideos}
+ orderVideos={orderVideos}
+ progress={progress}
+ recording={recording}
+ doUndo={doUndo}
+ setCanUndo={setCanUndo}
+ />
</div>
</div>)
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1b891034f..e833c7e30 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -15,7 +15,6 @@ import { DocumentType } from "../../documents/DocumentTypes";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { DocumentManager } from "../../util/DocumentManager";
-import { RecordingApi } from "../../util/RecordingApi";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
import { undoBatch } from "../../util/UndoManager";
@@ -29,8 +28,10 @@ import { MarqueeAnnotator } from "../MarqueeAnnotator";
import { AnchorMenu } from "../pdf/AnchorMenu";
import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
-import { RecordingBox } from "./RecordingBox/RecordingBox";
import "./VideoBox.scss";
+import { Presentation } from "../../util/TrackMovements";
+import { RecordingBox } from "./RecordingBox";
+import { ReplayMovements } from "../../util/ReplayMovements";
const path = require('path');
/**
@@ -150,6 +151,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
this.player && this.setPlayheadTime(0);
document.addEventListener("keydown", this.keyEvents, true);
+
+ if (this.presentation) {
+ ReplayMovements.Instance.setVideoBox(this);
+ }
}
componentWillUnmount() {
@@ -157,6 +162,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.Pause();
Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
document.removeEventListener("keydown", this.keyEvents, true);
+
+ if (this.presentation) {
+ ReplayMovements.Instance.removeVideoBox();
+ }
}
// handles key events, when timeline scrubs fade controls
@@ -183,20 +192,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action public Play = (update: boolean = true) => {
if (this._playRegionTimer) return;
- // if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) {
- // console.log('VideoBox : Play : presentation mode', this._playing);
- // return;
- // }
-
- // if presentation isn't null, call followmovements on the recording api
- if (this.presentation) {
- // console.log("presentation isn't null")
- const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
- err && console.log(err)
- } else {
- // console.log("presentation is null")
- }
-
this._playing = true;
const eleTime = this.player?.currentTime || 0;
if (this.timeline) {
@@ -238,12 +233,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// pauses video
@action public Pause = (update: boolean = true) => {
- if (this.presentation) {
- console.log('VideoBox : Pause');
- const err = RecordingApi.Instance.pauseMovements();
- err && console.log(err);
- }
-
this._playing = false;
this.removeCurrentlyPlaying();
try {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 9f858539f..7045d6d5d 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -30,6 +30,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
import "./PresBox.scss";
import { PresEffect, PresMovement, PresStatus } from "./PresEnums";
import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
+import { PresElementBox } from ".";
export interface PinProps {
audioRange?: boolean;
@@ -221,6 +222,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// No more frames in current doc and next slide is defined, therefore move to next slide
nextSlide = (activeNext: Doc) => {
const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null);
+ console.info('nextSlide', activeNext.title, targetNext?.title);
let nextSelected = this.itemIndex + 1;
this.gotoDocument(nextSelected, this.activeItem);
for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 1a2f4b93f..cc7bd5189 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from "mobx"
import { observer } from "mobx-react";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from "../../../../fields/Types";
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils";
import { Docs, DocUtils } from "../../../documents/Documents";
import { DocumentType } from "../../../documents/DocumentTypes";
@@ -35,6 +35,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
_heightDisposer: IReactionDisposer | undefined;
@observable _dragging = false;
+ // Idea: this boolean will determine whether to automatically show the video when this preselement is selected.
+ // @observable static showVideo: boolean = false;
@computed get indexInPres() { return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, "data")]).indexOf(this.rootDoc); } // 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 [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31; } // 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 this.presBox.presStatus; }
@@ -260,12 +262,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
removeItem = action((e: React.MouseEvent) => {
- e.stopPropagation();
+ e.stopPropagation();
this.props.removeDocument?.(this.rootDoc);
if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
PresBox.Instance._selectedArray.delete(this.rootDoc);
}
- this.removeAllRecordingInOverlay()
+ this.removeAllRecordingInOverlay()
});
// set the value/title of the individual pres element
@@ -286,17 +288,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
updateView = (targetDoc: Doc, activeItem: Doc) => {
switch (targetDoc.type) {
- case DocumentType.PDF: case DocumentType.WEB: case DocumentType.RTF :
+ case DocumentType.PDF: case DocumentType.WEB: case DocumentType.RTF:
const scroll = targetDoc._scrollTop;
activeItem.presPinViewScroll = scroll;
- break;
+ break;
case DocumentType.VID: case DocumentType.AUDIO:
activeItem.presStartTime = targetDoc._currentTimecode;
- break;
- case DocumentType.COMPARISON :
+ break;
+ case DocumentType.COMPARISON:
const clipWidth = targetDoc._clipWidth;
activeItem.presPinClipWidth = clipWidth;
- break;
+ break;
default:
const x = targetDoc._panX;
const y = targetDoc._panY;
@@ -307,122 +309,147 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
- @computed get recordingIsInOverlay() {
- let isInOverlay = false
- DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
- if (doc.slides === this.rootDoc) {
- isInOverlay = true
- return
- }
- })
- return isInOverlay
- }
-
- removeAllRecordingInOverlay = () => {
- DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
- if (doc.slides === this.rootDoc) {
- Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
- }
- })
- }
-
- @undoBatch
- @action
- hideRecording = (e: React.MouseEvent) => {
- e.stopPropagation()
- this.removeAllRecordingInOverlay()
- }
-
- @undoBatch
- @action
- showRecording = (activeItem: Doc) => {
- this.removeAllRecordingInOverlay()
- if (activeItem.recording) {
- // if we already have an existing recording
- Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ @computed get recordingIsInOverlay() {
+ let isInOverlay = false
+ DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+ if (doc.slides === this.rootDoc) {
+ isInOverlay = true
+ // return
+ }
+ })
+ return isInOverlay
+ }
+
+ // a previously recorded video will have timecode defined
+ static videoIsRecorded = (activeItem: Doc) => {
+ const casted = Cast(activeItem.recording, Doc, null);
+ return casted && 'currentTimecode' in casted;
+ }
+ removeAllRecordingInOverlay = () => {
+ DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+ if (doc.slides === this.rootDoc) {
+ Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
}
- }
+ })
+ }
+
+ static removeEveryExistingRecordingInOverlay = () => {
+ // Remove every recording that already exists in overlay view
+ DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+ // if it's a recording video, don't remove from overlay (user can lose data)
+ if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return
+
+ if (doc.slides !== null) {
+ Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
+ }
+ })
+ }
+
+ @undoBatch
+ @action
+ hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => {
+ e.stopPropagation()
+ this.removeAllRecordingInOverlay()
+
+ // if (iconClick) PresElementBox.showVideo = false;
+ }
+
+ @undoBatch
+ @action
+ showRecording = (activeItem: Doc, iconClick: boolean = false) => {
+ // if (iconClick) PresElementBox.showVideo = true;
+ // if (!PresElementBox.showVideo) return;
+
+ // remove the overlays on switch *IF* not opened from the specific icon
+ if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay();
+
+
+ if (activeItem.recording) {
+ Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ }
+ }
- @undoBatch
- @action
- startRecording = (activeItem: Doc) => {
+ @undoBatch
+ @action
+ startRecording = (activeItem: Doc) => {
+ console.log('start recording', 'activeItem', activeItem)
+ if (PresElementBox.videoIsRecorded(activeItem)) {
+ // if we already have an existing recording
+ this.showRecording(activeItem, true);
+ // // if we already have an existing recording
+ // Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+
+ } else {
// Remove every recording that already exists in overlay view
- DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
- if (doc.slides !== null) {
- Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
- }
- })
+ // this is a design decision to clear to focus in on the recoding mode
+ PresElementBox.removeEveryExistingRecordingInOverlay();
+
+ // if we dont have any recording
+ const recording = Docs.Create.WebCamDocument("", {
+ _width: 384, _height: 216,
+ hideDocumentButtonBar: true,
+ hideDecorationTitle: true,
+ hideOpenButton: true,
+ // hideDeleteButton: true,
+ cloneFieldFilter: new List<string>(["system"])
+ });
+
+ // attach the recording to the slide, and attach the slide to the recording
+ recording.slides = activeItem
+ activeItem.recording = recording
+
+ // make recording box appear in the bottom right corner of the screen
+ recording.x = window.innerWidth - recording[WidthSym]() - 20;
+ recording.y = window.innerHeight - recording[HeightSym]() - 20;
+ Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, recording);
+ }
+ }
- if (activeItem.recording) {
- // if we already have an existing recording
- Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ @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;
+ }
- } else {
- // if we dont have any recording
- const recording = Docs.Create.WebCamDocument("", {
- _width: 400, _height: 200,
- // hideDocumentButtonBar: true,
- hideDecorationTitle: true,
- hideOpenButton: true,
- // hideDeleteButton: true,
- cloneFieldFilter: new List<string>(["system"])
- });
-
- // attach the recording to the slide, and attach the slide to the recording
- recording.slides = activeItem
- activeItem.recording = recording
-
- // make recording box appear in the bottom right corner of the screen
- recording.x = window.innerWidth - recording[WidthSym]() - 20;
- recording.y = window.innerHeight - recording[HeightSym]() - 20;
- Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, 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);
- this.showRecording(activeItem);
- }}
- 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 ?
+ @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);
+ this.showRecording(activeItem);
+ }}
+ 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}.`}
@@ -432,53 +459,53 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="presItem-number">
{`${this.indexInPres + 1}.`}
</div>} */}
- {/* <div className="presItem-number">
+ {/* <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' }}>
- <div>{`${this.indexInPres + 1}. `}</div>
- <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.recordingIsInOverlay ?
- <Tooltip title={<><div className="dash-tooltip">{"Hide Recording"}</div></>}>
- <div className="slideButton"
- onClick={this.hideRecording}
- style={{ fontWeight: 700 }}>
- <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip> :
- <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
- <div className="slideButton"
- onClick={() => this.startRecording(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></>}>
+ {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' }}>
+ <div>{`${this.indexInPres + 1}. `}</div>
+ <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.recordingIsInOverlay ?
+ <Tooltip title={<><div className="dash-tooltip">{"Hide Recording"}</div></>}>
+ <div className="slideButton"
+ onClick={(e) => this.hideRecording(e, true)}
+ style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip> :
+ <Tooltip title={<><div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? "Show" : "Start"} recording`}</div></>}>
+ <div className="slideButton"
+ onClick={(e) => { e.stopPropagation(); this.startRecording(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={{
@@ -493,22 +520,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
</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;
- }
+ <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;
+ }
} \ No newline at end of file