diff options
| author | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-03-30 11:00:02 -0400 |
|---|---|---|
| committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-03-30 11:00:02 -0400 |
| commit | 479dff344ff2cf92ace9c68c3ce6d03e6e6dce22 (patch) | |
| tree | 85db5d0f69dc8afaae4c5ea5e6d1492d831fc7f1 /src/client/views/collections/CollectionStackedTimeline.tsx | |
| parent | 4df769e20b9588fea61b602ec67ca2208fc3d747 (diff) | |
| parent | 47f4f4ce91bd7deacaa04526418341d1f6006404 (diff) | |
merging
Diffstat (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 247 |
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 |
