diff options
| author | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-02-01 18:42:38 -0500 |
|---|---|---|
| committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-02-01 18:42:38 -0500 |
| commit | b160a00f80c4855e190d75f97482b5b019e77437 (patch) | |
| tree | 4fd150d45a5c49b9d5572edcb663956c0b29dd60 /src/client/views/collections/CollectionStackedTimeline.tsx | |
| parent | 888fb3b3933e7aa48e9ac3abe85536328fcca336 (diff) | |
| parent | 41bb365dd4f787aec2262dcb07508e0de3837e10 (diff) | |
more merge
Diffstat (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 293 |
1 files changed, 147 insertions, 146 deletions
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 1775250fa..d4eb66fcc 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,17 +1,18 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable } from "mobx"; +import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, Opt } from "../../../fields/Doc"; +import { Doc, Opt, 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 } from "../../../Utils"; +import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; import { CollectionSubView } from "../collections/CollectionSubView"; import { DocumentView } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; @@ -28,69 +29,80 @@ export type CollectionStackedTimelineProps = { playing: () => boolean; setTime: (time: number) => void; isChildActive: () => boolean; + startTag: string; + endTag: string; + fieldKeySuffix?: string; }; @observer export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) { + @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; static RangeScript: ScriptField; static LabelScript: ScriptField; static RangePlayScript: ScriptField; static LabelPlayScript: ScriptField; - _disposers: { [name: string]: IReactionDisposer } = {}; - _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name - _ele: HTMLAudioElement | null = null; - _start: number = 0; - _left: boolean = false; - _dragging = false; - _play: any = null; - _audioRef = React.createRef<HTMLDivElement>(); - _timeline: Opt<HTMLDivElement>; - _markerStart: number = 0; - _currAnchor: Opt<Doc>; - - @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; + private _timeline: HTMLDivElement | null = null; + private _markerStart: number = 0; @observable _markerEnd: number = 0; - @observable _position: number = 0; - @computed get anchorDocs() { return this.childDocs; } - @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); } + + get duration() { return this.props.duration; } + @computed get anchorDocs() { return this.props.fieldKeySuffix ? this.childDocs.concat(...DocListCast(this.rootDoc[this.props.fieldKey + this.props.fieldKeySuffix])) : this.childDocs; } + @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); } + @computed get selectionContainer() { + return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="collectionStackedTimeline-selector" style={{ + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`, + width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%` + }} />; + } constructor(props: any) { super(props); // onClick play scripts - CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; - CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; - CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; - CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; + 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); } + componentWillUnmount() { + document.removeEventListener("keydown", this.keyEvents, true); + if (CollectionStackedTimeline.SelectingRegion === this) runInAction(() => CollectionStackedTimeline.SelectingRegion = undefined); + } + + anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])); + anchorEnd = (anchor: Doc, val: any = null) => { + const endVal = NumCast(anchor[this.props.endTag], val); + return NumCast(anchor._timecodeToHide, endVal === undefined ? null : endVal); } + 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 keyEvents = (e: KeyboardEvent) => { - if (e.target instanceof HTMLInputElement) return; - if (!this.props.playing()) return; // can't create if video is not playing - switch (e.key) { - case "x": // currently set to x, but can be a different key - const currTime = this.currentTime; - if (this._start) { - this._markerStart = currTime; - // this._start = false; - // this._visible = true; - } else { - this.createAnchor(this._markerStart, currTime); - // this._start = true; - // this._visible = false; - } + if (!(e.target instanceof HTMLInputElement) && this.props.isSelected(true)) { + switch (e.key) { + case " ": + if (!CollectionStackedTimeline.SelectingRegion) { + this._markerStart = this._markerEnd = this.currentTime; + CollectionStackedTimeline.SelectingRegion = this; + } else { + CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime); + CollectionStackedTimeline.SelectingRegion = undefined; + } + } } } - anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))); - anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))); - getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime)); + const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag])); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -98,132 +110,134 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument return { la1, la2, linkTime }; } - // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { - this._timeline = timeline; - } - - // updates the anchor with the new time - @action - changeAnchor = (anchor: Opt<Doc>, time: number) => { - anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time); - } - - // checks if the two anchors are the same with start and end time - isSame = (m1: any, m2: any) => { - return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2); - } - - @computed get selectionContainer() { - return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ - left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.props.duration * 100}%`, - width: `${Math.abs(this._markerStart - this._markerEnd) / this.props.duration * 100}%`, height: "100%", top: "0%" - }} />; - } - // starting the drag event for anchor resizing @action onPointerDownTimeline = (e: React.PointerEvent): void => { - const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); - if (rect && e.target !== this._audioRef.current && this.props.active()) { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.clientX; + if (rect && this.props.active()) { const wasPlaying = this.props.playing(); if (wasPlaying) this.props.Pause(); - else if (!this._doubleTime) { - this._doubleTime = setTimeout(() => { - this._doubleTime = undefined; - this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration); - }, 300); - } - this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); - CollectionStackedTimeline.SelectingRegion = this; + const wasSelecting = CollectionStackedTimeline.SelectingRegion === this; setupMoveUpEvents(this, e, action(e => { + if (!wasSelecting && CollectionStackedTimeline.SelectingRegion !== this) { + this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width); + CollectionStackedTimeline.SelectingRegion = this; + } this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); return false; }), - action((e, movement) => { + action((e, movement, isClick) => { this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); if (this._markerEnd < this._markerStart) { const tmp = this._markerStart; this._markerStart = this._markerEnd; this._markerEnd = tmp; } - CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); - CollectionStackedTimeline.SelectingRegion = undefined; + if (!isClick && CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15)) { + CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, + this._markerStart, this._markerEnd); + } + (!isClick || !wasSelecting) && (CollectionStackedTimeline.SelectingRegion = undefined); }), (e, doubleTap) => { this.props.select(false); - e.shiftKey && this.createAnchor(this.currentTime); + e.shiftKey && CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime); !wasPlaying && doubleTap && this.props.Play(); - } - , this.props.isSelected(true) || this.props.isChildActive()); + }, + this.props.isSelected(true) || this.props.isChildActive(), undefined, + () => !wasPlaying && this.props.setTime((clientX - rect.x) / rect.width * this.duration)); } } + @undoBatch @action - createAnchor(anchorStartTime?: number, anchorEndTime?: number) { - if (anchorStartTime === undefined) return this.props.Document; + static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number) { + if (anchorStartTime === undefined) return rootDoc; const anchor = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, + title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any, useLinkSmallAnchor: true, hideLinkButton: true, - anchorStartTime, - anchorEndTime, - annotationOn: this.props.Document + annotationOn: rootDoc }); - if (Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []).push(anchor); + Doc.GetProto(anchor)[startTag] = anchorStartTime; + Doc.GetProto(anchor)[endTag] = anchorEndTime; + if (Cast(dataDoc[fieldKey], listSpec(Doc), null) !== undefined) { + Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor); } else { - this.dataDoc[this.props.fieldKey] = new List<Doc>([anchor]); + dataDoc[fieldKey] = new List<Doc>([anchor]); } return anchor; } - // play back the audio from time @action - playOnClick = (anchorDoc: Doc) => { - this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration)); + playOnClick = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc); + const endTime = this.anchorEnd(anchorDoc); + if (this.layoutDoc.autoPlay) { + 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()) { + this.props.Pause(); + } else { + this.props.Play(); + } + } else { + this.props.playFrom(seekTimeInSeconds, endTime); + } + } return { select: true }; } - // play back the audio from time @action - clickAnchor = (anchorDoc: Doc) => { - if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc); - this.props.setTime(this.anchorStart(anchorDoc)); + clickAnchor = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc); + 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) { + 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); + else this.props.setTime(seekTimeInSeconds); + } return { select: true }; } - toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.duration)); + // starting the drag event for anchor resizing - onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => { - this._currAnchor = m; - this._left = left; + 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) => { - const rect = (e.target as any).getBoundingClientRect(); - this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width)); - return false; - }, - (e) => { - const rect = (e.target as any).getBoundingClientRect(); - this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width)); + this.props.setTime(newTime(e)); this._timeline?.releasePointerCapture(e.pointerId); }, emptyFunction); } - rangeClickScript = () => CollectionStackedTimeline.RangeScript; - labelClickScript = () => CollectionStackedTimeline.LabelScript; - rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; - labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript; - // 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(); const x1 = this.anchorStart(m); - const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration); + const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); let max = 0; const overlappedLevels = new Set(placed.map(p => { const y1 = p.anchorStartTime; @@ -248,21 +262,19 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument ref={action((r: DocumentView | null) => anchor.view = r)} Document={mark} DataDoc={undefined} - PanelWidth={() => width} - PanelHeight={() => height} renderDepth={this.props.renderDepth + 1} - focus={() => this.props.playLink(mark)} - rootSelected={returnFalse} LayoutTemplate={undefined} LayoutTemplateString={LabelBox.LayoutString("data")} - ContainingCollectionDoc={this.props.Document} - removeDocument={this.props.removeDocument} + PanelWidth={() => width} + PanelHeight={() => height} ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} - parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()} - whenActiveChanged={this.props.whenActiveChanged} + focus={() => this.props.playLink(mark)} + parentActive={out => this.props.isSelected(out) || this.props.isChildActive()} + rootSelected={returnFalse} onClick={script} onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} ignoreAutoHeight={false} + hideResizeHandles={true} bringToFront={emptyFunction} scriptContext={this} /> }; @@ -273,8 +285,8 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument {inner.view} {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : <> - <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} /> - <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} /> + <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)} /> </>} </>; }); @@ -285,39 +297,28 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - return <div className="audiobox-timeline" style={{ height: "100%", width: "100%" }} ref={this.timelineRef} - onClick={e => { - if (this.props.isChildActive() || this.props.isSelected(false)) { - e.stopPropagation(); e.preventDefault(); - } - }} - onPointerDown={e => { - if (this.props.isChildActive() || this.props.isSelected(false)) { - e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e); - } - }}> + 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 m = d.anchor; - const start = this.anchorStart(m); - const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.props.duration); - const left = start / this.props.duration * timelineContentWidth; + 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={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]} - style={{ left, top, width: `${timespan / this.props.duration * 100}%`, height: `${1 / maxLevel * 100}%` }} - onClick={e => { this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > - {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, + <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.props.duration, + timelineContentWidth * timespan / this.duration, timelineContentHeight / maxLevel)} </div>; })} {this.selectionContainer} - <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} - style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }} - /> + <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} /> </div>; } } |
