aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/CollectionStackedTimeline.tsx
diff options
context:
space:
mode:
authorusodhi <61431818+usodhi@users.noreply.github.com>2021-03-30 11:00:02 -0400
committerusodhi <61431818+usodhi@users.noreply.github.com>2021-03-30 11:00:02 -0400
commit479dff344ff2cf92ace9c68c3ce6d03e6e6dce22 (patch)
tree85db5d0f69dc8afaae4c5ea5e6d1492d831fc7f1 /src/client/views/collections/CollectionStackedTimeline.tsx
parent4df769e20b9588fea61b602ec67ca2208fc3d747 (diff)
parent47f4f4ce91bd7deacaa04526418341d1f6006404 (diff)
merging
Diffstat (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx')
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx247
1 files changed, 171 insertions, 76 deletions
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 16a1c02f7..c0cebf021 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,23 +1,26 @@
import React = require("react");
-import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
-import { Doc, Opt, DocListCast } from "../../../fields/Doc";
+import { Doc, DocListCast } from "../../../fields/Doc";
import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec, makeInterface } from "../../../fields/Schema";
import { ComputedField, ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent, returnTrue } from "../../../Utils";
import { Docs } from "../../documents/Documents";
+import { LinkManager } from "../../util/LinkManager";
import { Scripting } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
+import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
+import { AudioWaveform } from "../AudioWaveform";
import { CollectionSubView } from "../collections/CollectionSubView";
-import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView";
+import { LightboxView } from "../LightboxView";
+import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView";
import { LabelBox } from "../nodes/LabelBox";
import "./CollectionStackedTimeline.scss";
-import { Transform } from "../../util/Transform";
type PanZoomDocument = makeInterface<[]>;
const PanZoomDocument = makeInterface();
@@ -32,6 +35,7 @@ export type CollectionStackedTimelineProps = {
isChildActive: () => boolean;
startTag: string;
endTag: string;
+ mediaPath: string;
};
@observer
@@ -59,9 +63,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
super(props);
// onClick play scripts
CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
- CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
- CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
}
componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); }
@@ -77,9 +79,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
}
toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration));
rangeClickScript = () => CollectionStackedTimeline.RangeScript;
- labelClickScript = () => CollectionStackedTimeline.LabelScript;
rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
- labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript;
// for creating key anchors with key events
@action
@@ -173,14 +173,14 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
@action
playOnClick = (anchorDoc: Doc, clientX: number) => {
- const seekTimeInSeconds = this.anchorStart(anchorDoc);
+ const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
const endTime = this.anchorEnd(anchorDoc);
- if (this.layoutDoc.autoPlay) {
+ if (this.layoutDoc.autoPlayAnchors) {
if (this.props.playing()) this.props.Pause();
else this.props.playFrom(seekTimeInSeconds, endTime);
} else {
if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) {
- if (!this.layoutDoc.autoPlay && this.props.playing()) {
+ if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
this.props.Pause();
} else {
this.props.Play();
@@ -194,45 +194,24 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
@action
clickAnchor = (anchorDoc: Doc, clientX: number) => {
- const seekTimeInSeconds = this.anchorStart(anchorDoc);
+ if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
+ const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
const endTime = this.anchorEnd(anchorDoc);
if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
if (this.props.playing()) this.props.Pause();
- else if (this.layoutDoc.autoPlay) this.props.Play();
- else if (!this.layoutDoc.autoPlay) {
+ else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
+ else if (!this.layoutDoc.autoPlayAnchors) {
const rect = this._timeline?.getBoundingClientRect();
rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
}
} else {
- if (this.layoutDoc.autoPlay) this.props.playFrom(seekTimeInSeconds, endTime);
+ if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime);
else this.props.setTime(seekTimeInSeconds);
}
return { select: true };
}
- // starting the drag event for anchor resizing
- onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
- this._timeline?.setPointerCapture(e.pointerId);
- const newTime = (e: PointerEvent) => {
- const rect = (e.target as any).getBoundingClientRect();
- return this.toTimeline(e.clientX - rect.x, rect.width);
- };
- const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
- const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
- if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
- else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
- return false;
- };
- setupMoveUpEvents(this, e,
- (e) => changeAnchor(anchor, left, newTime(e)),
- (e) => {
- this.props.setTime(newTime(e));
- this._timeline?.releasePointerCapture(e.pointerId);
- },
- emptyFunction);
- }
-
// makes sure no anchors overlaps each other by setting the correct position and width
getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => {
const timelineContentWidth = this.props.PanelWidth();
@@ -255,7 +234,154 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
return level;
}
- renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+ dictationHeight = () => this.props.PanelHeight() / 3;
+ timelineContentHeight = () => this.props.PanelHeight() * 2 / 3;
+ dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight());
+ @computed get renderDictation() {
+ const dictation = Cast(this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")], Doc, null);
+ return !dictation ? (null) : <div style={{ position: "absolute", height: this.dictationHeight(), top: this.timelineContentHeight(), background: "tan" }}>
+ <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ Document={dictation}
+ PanelHeight={this.dictationHeight}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ scaling={returnOne}
+ xMargin={25}
+ yMargin={10}
+ ScreenToLocalTransform={this.dictationScreenToLocalTransform}
+ whenActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ renderDepth={this.props.renderDepth + 1}>
+ </DocumentView>
+ </div>;
+ }
+ @computed get renderAudioWaveform() {
+ return !this.props.mediaPath ? (null) :
+ <div className="collectionStackedTimeline-waveform" >
+ <AudioWaveform
+ duration={this.duration}
+ mediaPath={this.props.mediaPath}
+ dataDoc={this.dataDoc}
+ PanelHeight={this.timelineContentHeight} />
+ </div>;
+ }
+ currentTimecode = () => this.currentTime;
+ render() {
+ const timelineContentWidth = this.props.PanelWidth();
+ const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
+ const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
+ const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
+ const isActive = this.props.isChildActive() || this.props.isSelected(false);
+ return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline}
+ onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}>
+ {drawAnchors.map(d => {
+ const start = this.anchorStart(d.anchor);
+ const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
+ const left = start / this.duration * timelineContentWidth;
+ const top = d.level / maxLevel * this.timelineContentHeight();
+ const timespan = end - start;
+ return this.props.Document.hideAnchors ? (null) :
+ <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
+ style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${this.timelineContentHeight() / maxLevel}px` }}
+ onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
+ <StackedTimelineAnchor {...this.props}
+ mark={d.anchor}
+ rangeClickScript={this.rangeClickScript}
+ rangePlayScript={this.rangePlayScript}
+ left={left}
+ top={top}
+ width={timelineContentWidth * timespan / this.duration}
+ height={this.timelineContentHeight() / maxLevel}
+ toTimeline={this.toTimeline}
+ layoutDoc={this.layoutDoc}
+ currentTimecode={this.currentTimecode}
+ _timeline={this._timeline}
+ stackedTimeline={this}
+ />
+ </div>;
+ })}
+ {this.selectionContainer}
+ {this.renderAudioWaveform}
+ {this.renderDictation}
+
+ <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
+ </div>;
+ }
+}
+
+interface StackedTimelineAnchorProps {
+ mark: Doc;
+ rangeClickScript: () => ScriptField;
+ rangePlayScript: () => ScriptField;
+ left: number;
+ top: number;
+ width: number;
+ height: number;
+ toTimeline: (screen_delta: number, width: number) => number;
+ playLink: (linkDoc: Doc) => void;
+ setTime: (time: number) => void;
+ isChildActive: () => boolean;
+ startTag: string;
+ endTag: string;
+ renderDepth: number;
+ layoutDoc: Doc;
+ ScreenToLocalTransform: () => Transform;
+ _timeline: HTMLDivElement | null;
+ focus: DocFocusFunc;
+ currentTimecode: () => number;
+ isSelected: (outsideReaction?: boolean) => boolean;
+ stackedTimeline: CollectionStackedTimeline;
+}
+@observer
+class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
+ _lastTimecode: number;
+ _disposer: IReactionDisposer | undefined;
+ constructor(props: any) {
+ super(props);
+ this._lastTimecode = this.props.currentTimecode();
+ }
+ componentDidMount() {
+ this._disposer = reaction(() => this.props.currentTimecode(),
+ (time) => {
+ const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null);
+ const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
+ if ((isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc)) && DocListCast(this.props.mark.links).length &&
+ time > NumCast(this.props.mark[this.props.startTag]) &&
+ time < NumCast(this.props.mark[this.props.endTag]) &&
+ this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) {
+ LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true);
+ }
+ this._lastTimecode = time;
+ });
+ }
+ componentWillUnmount() {
+ this._disposer?.();
+ }
+ // starting the drag event for anchor resizing
+ onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
+ this.props._timeline?.setPointerCapture(e.pointerId);
+ const newTime = (e: PointerEvent) => {
+ const rect = (e.target as any).getBoundingClientRect();
+ return this.props.toTimeline(e.clientX - rect.x, rect.width);
+ };
+ const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
+ const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
+ if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
+ else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
+ return false;
+ };
+ setupMoveUpEvents(this, e,
+ (e) => changeAnchor(anchor, left, newTime(e)),
+ (e) => {
+ this.props.setTime(newTime(e));
+ this.props._timeline?.releasePointerCapture(e.pointerId);
+ },
+ emptyFunction);
+ }
+ renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
const anchor = observable({ view: undefined as any });
const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => {
this.props.playLink(mark);
@@ -276,54 +402,23 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
parentActive={out => this.props.isSelected(out) || this.props.isChildActive()}
rootSelected={returnFalse}
onClick={script}
- onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript}
+ onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
ignoreAutoHeight={false}
hideResizeHandles={true}
bringToFront={emptyFunction}
- scriptContext={this} />
+ scriptContext={this.props.stackedTimeline} />
};
});
- renderAnchor = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
- const inner = this.renderInner(mark, script, doublescript, x, y, width, height);
+ render() {
+ const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height);
return <>
{inner.view}
{!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
<>
- <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, mark, true)} />
- <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, mark, false)} />
+ <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} />
+ <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} />
</>}
</>;
- });
-
- render() {
- const timelineContentWidth = this.props.PanelWidth();
- const timelineContentHeight = this.props.PanelHeight();
- const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
- const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
- const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- const isActive = this.props.isChildActive() || this.props.isSelected(false);
- return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline}
- onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}>
- {drawAnchors.map(d => {
- const start = this.anchorStart(d.anchor);
- const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
- const left = start / this.duration * timelineContentWidth;
- const top = d.level / maxLevel * timelineContentHeight;
- const timespan = end - start;
- return this.props.Document.hideAnchors ? (null) :
- <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
- style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${timelineContentHeight / maxLevel}px` }}
- onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
- {this.renderAnchor(d.anchor, this.rangeClickScript, this.rangePlayScript,
- left,
- top,
- timelineContentWidth * timespan / this.duration,
- timelineContentHeight / maxLevel)}
- </div>;
- })}
- {this.selectionContainer}
- <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
- </div>;
}
}
Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file