diff options
| author | mehekj <mehek.jethani@gmail.com> | 2021-08-25 21:34:40 -0400 |
|---|---|---|
| committer | mehekj <mehek.jethani@gmail.com> | 2021-08-25 21:34:40 -0400 |
| commit | 8beb8fa42ba5f84bb13b5877560fc92ad3613e88 (patch) | |
| tree | ca555ebe77f2a163b849a41416460572548b2b6d /src/client/views/collections/CollectionStackedTimeline.tsx | |
| parent | 8f210e4dd1c8b1328fc6f4cf0094acecbae0a2ef (diff) | |
basic audio trim complete
Diffstat (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 1550 |
1 files changed, 786 insertions, 764 deletions
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 230238ba8..51e05e278 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,11 +1,11 @@ import React = require("react"); import { - action, - computed, - IReactionDisposer, - observable, - reaction, - runInAction, + action, + computed, + IReactionDisposer, + observable, + reaction, + runInAction, } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; @@ -16,14 +16,14 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { - emptyFunction, - formatTime, - OmitKeys, - returnFalse, - returnOne, - setupMoveUpEvents, - StopEvent, - returnTrue, + emptyFunction, + formatTime, + OmitKeys, + returnFalse, + returnOne, + setupMoveUpEvents, + StopEvent, + returnTrue, } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { LinkManager } from "../../util/LinkManager"; @@ -35,798 +35,820 @@ import { AudioWaveform } from "../AudioWaveform"; import { CollectionSubView } from "../collections/CollectionSubView"; import { LightboxView } from "../LightboxView"; import { - DocAfterFocusFunc, - DocFocusFunc, - DocumentView, - DocumentViewProps, + DocAfterFocusFunc, + DocFocusFunc, + DocumentView, + DocumentViewProps, } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; +import { Colors } from "../global/globalEnums"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); export type CollectionStackedTimelineProps = { - duration: number; - Play: () => void; - Pause: () => void; - playLink: (linkDoc: Doc) => void; - playFrom: (seekTimeInSeconds: number, endTime?: number) => void; - playing: () => boolean; - setTime: (time: number) => void; - startTag: string; - endTag: string; - mediaPath: string; - dictationKey: string; - trimming: boolean; - trimBounds: { start: number; end: number }; + duration: number; + Play: () => void; + Pause: () => void; + playLink: (linkDoc: Doc) => void; + playFrom: (seekTimeInSeconds: number, endTime?: number) => void; + playing: () => boolean; + setTime: (time: number) => void; + startTag: string; + endTag: string; + mediaPath: string; + dictationKey: string; + trimming: boolean; + trimStart: number; + trimEnd: number; + trimDuration: number; + setStartTrim: (newStart: number) => void; + setEndTrim: (newEnd: number) => void; }; @observer export class CollectionStackedTimeline extends CollectionSubView< - PanZoomDocument, - CollectionStackedTimelineProps + PanZoomDocument, + CollectionStackedTimelineProps >(PanZoomDocument) { - @observable static SelectingRegion: - | CollectionStackedTimeline - | undefined = undefined; - static RangeScript: ScriptField; - static LabelScript: ScriptField; - static RangePlayScript: ScriptField; - static LabelPlayScript: ScriptField; - - private _timeline: HTMLDivElement | null = null; - private _markerStart: number = 0; - @observable _markerEnd: number = 0; - - get minLength() { - const rect = this._timeline?.getBoundingClientRect(); - if (rect) { - return 0.05 * this.duration; + @observable static SelectingRegion: CollectionStackedTimeline | undefined = + undefined; + static RangeScript: ScriptField; + static LabelScript: ScriptField; + static RangePlayScript: ScriptField; + static LabelPlayScript: ScriptField; + + private _timeline: HTMLDivElement | null = null; + private _markerStart: number = 0; + @observable _markerEnd: number = 0; + + get minLength() { + const rect = this._timeline?.getBoundingClientRect(); + if (rect) { + return 0.05 * this.duration; + } + return 0; } - return 0; - } - - get trimStart() { - return this.props.trimBounds.start; - } - - get trimEnd() { - return this.props.trimBounds.end; - } - - set trimStart(start: number) { - this.props.trimBounds.start = start; - } - - set trimEnd(end: number) { - this.props.trimBounds.end = end; - } - - get duration() { - return this.props.duration; - } - @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, 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", - })!; - } - - 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; - rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; - - // for creating key anchors with key events - @action - keyEvents = (e: KeyboardEvent) => { - 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; - } - } + + get trimStart() { + return this.props.trimStart; } - }; - - getLinkData(l: Doc) { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - 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; + + get trimEnd() { + return this.props.trimEnd; } - return { la1, la2, linkTime }; - } - - // starting the drag event for anchor resizing - @action - onPointerDownTimeline = (e: React.PointerEvent): void => { - const rect = this._timeline?.getBoundingClientRect(); - const clientX = e.clientX; - if (rect && this.props.isContentActive()) { - const wasPlaying = this.props.playing(); - if (wasPlaying) this.props.Pause(); - 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, 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; - } - if ( - !isClick && - CollectionStackedTimeline.SelectingRegion === this && - Math.abs(movement[0]) > 15 && - !this.props.trimming - ) { - 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 && - 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.isContentActive(), - undefined, - () => - !wasPlaying && - this.props.setTime(((clientX - rect.x) / rect.width) * this.duration) - ); + + get duration() { + return this.props.duration; + } + + @computed get currentTime() { + return NumCast(this.layoutDoc._currentTimecode); + } + @computed get selectionContainer() { + return CollectionStackedTimeline.SelectingRegion !== this ? null : ( + <div + className="collectionStackedTimeline-selector" + style={{ + left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration) * 100}%`, + width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration) * 100}%`, + }} + /> + ); } - }; - - @action - startTrimLeft = (e: React.PointerEvent): void => { - document.addEventListener("pointermove", this.dragTrimLeft); - document.addEventListener("pointerup", this.endTrimLeft); - }; - - @action - startTrimRight = (e: React.PointerEvent): void => { - document.addEventListener("pointermove", this.dragTrimRight); - document.addEventListener("pointerup", this.endTrimRight); - }; - - @action - dragTrimLeft = (e: MouseEvent) => { - const rect = this._timeline?.getBoundingClientRect(); - if (rect && this.props.isContentActive()) { - this.trimStart = Math.min( - Math.max( - this.trimStart + (e.movementX / rect.width) * this.duration, - 0 - ), - this.trimEnd - this.minLength - ); + + constructor(props: any) { + super(props); + // onClick play scripts + CollectionStackedTimeline.RangeScript = + CollectionStackedTimeline.RangeScript || + 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", + })!; } - }; - - @action - dragTrimRight = (e: MouseEvent) => { - const rect = this._timeline?.getBoundingClientRect(); - if (rect && this.props.isContentActive()) { - this.trimEnd = Math.max( - Math.min( - this.trimEnd + (e.movementX / rect.width) * this.duration, - this.duration - ), - this.trimStart + this.minLength - ); + + componentDidMount() { + document.addEventListener("keydown", this.keyEvents, true); } - }; - - endTrimLeft = () => { - document.removeEventListener("pointermove", this.dragTrimLeft); - document.removeEventListener("pointerup", this.endTrimLeft); - }; - - endTrimRight = () => { - document.removeEventListener("pointermove", this.dragTrimRight); - document.removeEventListener("pointerup", this.endTrimRight); - }; - - @undoBatch - @action - 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["${startTag}"]) + "-" + formatToTime(self["${endTag}"])` - ) as any, - useLinkSmallAnchor: true, - hideLinkButton: true, - annotationOn: rootDoc, - _timelineLabel: true, - }); - 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 { - dataDoc[fieldKey] = new List<Doc>([anchor]); + componentWillUnmount() { + document.removeEventListener("keydown", this.keyEvents, true); + if (CollectionStackedTimeline.SelectingRegion === this) + runInAction( + () => (CollectionStackedTimeline.SelectingRegion = undefined) + ); } - return anchor; - } - - @action - playOnClick = (anchorDoc: Doc, clientX: number) => { - const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; - const endTime = this.anchorEnd(anchorDoc); - 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.autoPlayAnchors && this.props.playing()) { - this.props.Pause(); - } else { - this.props.Play(); + + 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) => { + return Math.max( + this.trimStart, + Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart)) + } + + rangeClickScript = () => CollectionStackedTimeline.RangeScript; + rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; + + // for creating key anchors with key events + @action + keyEvents = (e: KeyboardEvent) => { + 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; + } + } + } + }; + + getLinkData(l: Doc) { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + 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; } - } else { - this.props.playFrom(seekTimeInSeconds, endTime); - } + return { la1, la2, linkTime }; } - return { select: true }; - }; - - @action - clickAnchor = (anchorDoc: Doc, clientX: number) => { - 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.autoPlayAnchors) this.props.Play(); - else if (!this.layoutDoc.autoPlayAnchors) { + + // starting the drag event for anchor resizing + @action + onPointerDownTimeline = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); - rect && - this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); - } - } else { - if (this.layoutDoc.autoPlayAnchors) - this.props.playFrom(seekTimeInSeconds, endTime); - else this.props.setTime(seekTimeInSeconds); + const clientX = e.clientX; + if (rect && this.props.isContentActive()) { + const wasPlaying = this.props.playing(); + if (wasPlaying) this.props.Pause(); + 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, 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; + } + if ( + !isClick && + CollectionStackedTimeline.SelectingRegion === this && + Math.abs(movement[0]) > 15 && + !this.props.trimming + ) { + 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 && + 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.isContentActive(), + undefined, + () => { + !wasPlaying && + (this.props.trimming && this.duration ? + this.props.setTime(((clientX - rect.x) / rect.width) * this.duration) + : + this.props.setTime(((clientX - rect.x) / rect.width) * this.props.trimDuration + this.trimStart) + ) + } + ); + } + + }; + + @action + trimLeft = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.movementX; + setupMoveUpEvents( + this, + e, + action((e, [], []) => { + if (rect && this.props.isContentActive()) { + this.props.setStartTrim(Math.min( + Math.max( + this.trimStart + (e.movementX / rect.width) * this.duration, + 0 + ), + this.trimEnd - this.minLength + )); + } + return false; + }), + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this.props.setStartTrim(0); + } + }) + ); + }; + + @action + trimRight = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.movementX; + setupMoveUpEvents( + this, + e, + action((e, [], []) => { + if (rect && this.props.isContentActive()) { + this.props.setEndTrim(Math.max( + Math.min( + this.trimEnd + (e.movementX / rect.width) * this.duration, + this.duration + ), + this.trimStart + this.minLength + )); + } + return false; + }), + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this.props.setEndTrim(this.duration); + } + }) + ); + }; + + @undoBatch + @action + 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["${startTag}"]) + "-" + formatToTime(self["${endTag}"])` + ) as any, + useLinkSmallAnchor: true, + hideLinkButton: true, + annotationOn: rootDoc, + _timelineLabel: true, + }); + 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 { + dataDoc[fieldKey] = new List<Doc>([anchor]); + } + return anchor; } - return { select: true }; - }; - - // 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.duration - ); - let max = 0; - const overlappedLevels = new Set( - placed.map((p) => { - const y1 = p.anchorStartTime; - const y2 = p.anchorEndTime; + + @action + playOnClick = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; + const endTime = this.anchorEnd(anchorDoc); + 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.autoPlayAnchors && this.props.playing()) { + this.props.Pause(); + } else { + this.props.Play(); + } + } else { + this.props.playFrom(seekTimeInSeconds, endTime); + } + } + return { select: true }; + }; + + @action + clickAnchor = (anchorDoc: Doc, clientX: number) => { + if (anchorDoc.isLinkButton) + LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; + const endTime = this.anchorEnd(anchorDoc); if ( - (x1 >= y1 && x1 <= y2) || - (x2 >= y1 && x2 <= y2) || - (y1 >= x1 && y1 <= x2) || - (y2 >= x1 && y2 <= x2) + seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && + endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4 ) { - max = Math.max(max, p.level); - return p.level; + if (this.props.playing()) this.props.Pause(); + 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.autoPlayAnchors) + this.props.playFrom(seekTimeInSeconds, endTime); + else { + this.props.setTime(seekTimeInSeconds); + } } - }) - ); - let level = max + 1; - for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); - - placed.push({ anchorStartTime: x1, anchorEndTime: x2, level }); - return level; - }; - - dictationHeightPercent = 50; - dictationHeight = () => - (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100; - timelineContentHeight = () => - (this.props.PanelHeight() * this.dictationHeightPercent) / 100; - dictationScreenToLocalTransform = () => - this.props - .ScreenToLocalTransform() - .translate(0, -this.timelineContentHeight()); - @computed get renderDictation() { - const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null); - return !dictation ? null : ( - <div - style={{ - position: "absolute", - height: "100%", - top: this.timelineContentHeight(), - background: "tan", - }} - > - <DocumentView - {...OmitKeys(this.props, [ - "NativeWidth", - "NativeHeight", - "setContentView", - ]).omit} - Document={dictation} - PanelHeight={this.dictationHeight} - isAnnotationOverlay={true} - isDocumentActive={returnFalse} - select={emptyFunction} - scaling={returnOne} - xMargin={25} - yMargin={10} - ScreenToLocalTransform={this.dictationScreenToLocalTransform} - whenChildContentsActiveChanged={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} - trimming={this.props.trimming} - /> - </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.isContentActive() || 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 : ( + return { select: true }; + }; + + // 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.duration + ); + let max = 0; + const overlappedLevels = new Set( + placed.map((p) => { + const y1 = p.anchorStartTime; + const y2 = p.anchorEndTime; + if ( + (x1 >= y1 && x1 <= y2) || + (x2 >= y1 && x2 <= y2) || + (y1 >= x1 && y1 <= x2) || + (y2 >= x1 && y2 <= x2) + ) { + max = Math.max(max, p.level); + return p.level; + } + }) + ); + let level = max + 1; + for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); + + placed.push({ anchorStartTime: x1, anchorEndTime: x2, level }); + return level; + }; + + dictationHeightPercent = 50; + dictationHeight = () => + (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100; + timelineContentHeight = () => + (this.props.PanelHeight() * this.dictationHeightPercent) / 100; + dictationScreenToLocalTransform = () => + this.props + .ScreenToLocalTransform() + .translate(0, -this.timelineContentHeight()); + @computed get renderDictation() { + const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null); + return !dictation ? 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(); - }} + style={{ + position: "absolute", + height: "100%", + top: this.timelineContentHeight(), + background: Colors.LIGHT_BLUE, + }} > - <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} - /> + <DocumentView + {...OmitKeys(this.props, [ + "NativeWidth", + "NativeHeight", + "setContentView", + ]).omit} + Document={dictation} + PanelHeight={this.dictationHeight} + isAnnotationOverlay={true} + isDocumentActive={returnFalse} + select={emptyFunction} + scaling={returnOne} + xMargin={25} + yMargin={10} + ScreenToLocalTransform={this.dictationScreenToLocalTransform} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1} + ></DocumentView> </div> - ); - })} - {!this.props.trimming && this.selectionContainer} - {this.renderAudioWaveform} - {this.renderDictation} - - <div - className="collectionStackedTimeline-current" - style={{ - left: this.props.trimming - ? `${ - (this.currentTime / - (NumCast(this.dataDoc.clipEnd) - - NumCast(this.dataDoc.clipStart))) * - 100 - }%` - : `${(this.currentTime / this.duration) * 100}%`, - }} - /> - - {this.props.trimming && ( - <div> - <div - className="collectionStackedTimeline-trim-shade" - style={{ width: `${(this.trimStart / this.duration) * 100}%` }} - ></div> + ); + } + @computed get renderAudioWaveform() { + return !this.props.mediaPath ? null : ( + <div className="collectionStackedTimeline-waveform"> + <AudioWaveform + duration={this.duration} + mediaPath={this.props.mediaPath} + layoutDoc={this.layoutDoc} + PanelHeight={this.timelineContentHeight} + trimming={this.props.trimming} + /> + </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.isContentActive() || this.props.isSelected(false); + return ( <div - className="collectionStackedTimeline-trim-controls" - style={{ - left: `${(this.trimStart / this.duration) * 100}%`, - width: `${ - ((this.trimEnd - this.trimStart) / this.duration) * 100 - }%`, - }} + className="collectionStackedTimeline" + ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)} + onClick={(e) => isActive && StopEvent(e)} + onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)} > - <div - className="collectionStackedTimeline-trim-handle" - onPointerDown={this.startTrimLeft} - ></div> - <div - className="collectionStackedTimeline-trim-handle" - onPointerDown={this.startTrimRight} - ></div> + {drawAnchors.map((d) => { + + const start = this.anchorStart(d.anchor); + const end = this.anchorEnd( + d.anchor, + start + (10 / timelineContentWidth) * this.duration + ); + const left = this.props.trimming ? + (start / this.duration) * timelineContentWidth + : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth; + const top = (d.level / maxLevel) * this.timelineContentHeight(); + const timespan = end - start; + const width = (timespan / this.props.trimDuration) * timelineContentWidth; + const height = this.timelineContentHeight() / maxLevel + return this.props.Document.hideAnchors ? null : ( + <div + className={"collectionStackedTimeline-marker-timeline"} + key={d.anchor[Id]} + style={{ + left, + top, + width: `${width}px`, + height: `${height}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={width} + height={height} + toTimeline={this.toTimeline} + layoutDoc={this.layoutDoc} + currentTimecode={this.currentTimecode} + _timeline={this._timeline} + stackedTimeline={this} + trimStart={this.trimStart} + trimEnd={this.trimEnd} + /> + </div> + ); + })} + {!this.props.trimming && this.selectionContainer} + {this.renderAudioWaveform} + {this.renderDictation} + + <div + className="collectionStackedTimeline-current" + style={{ + left: this.props.trimming + ? `${(this.currentTime / this.duration) * 100}%` + : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`, + }} + /> + + {this.props.trimming && ( + <> + <div + className="collectionStackedTimeline-trim-shade" + style={{ width: `${(this.trimStart / this.duration) * 100}%` }} + ></div> + + <div + className="collectionStackedTimeline-trim-controls" + style={{ + left: `${(this.trimStart / this.duration) * 100}%`, + width: `${((this.trimEnd - this.trimStart) / this.duration) * 100 + }%`, + }} + > + <div + className="collectionStackedTimeline-trim-handle" + onPointerDown={this.trimLeft} + ></div> + <div + className="collectionStackedTimeline-trim-handle" + onPointerDown={this.trimRight} + ></div> + </div> + + <div + className="collectionStackedTimeline-trim-shade" + style={{ + left: `${(this.trimEnd / this.duration) * 100}%`, + width: `${((this.duration - this.trimEnd) / this.duration) * 100 + }%`, + }} + ></div> + </> + )} </div> - - <div - className="collectionStackedTimeline-trim-shade" - style={{ - left: `${(this.trimEnd / this.duration) * 100}%`, - width: `${ - ((this.duration - this.trimEnd) / this.duration) * 100 - }%`, - }} - ></div> - </div> - )} - </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; - startTag: string; - endTag: string; - renderDepth: number; - layoutDoc: Doc; - ScreenToLocalTransform: () => Transform; - _timeline: HTMLDivElement | null; - focus: DocFocusFunc; - currentTimecode: () => number; - isSelected: (outsideReaction?: boolean) => boolean; - stackedTimeline: CollectionStackedTimeline; + 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; + startTag: string; + endTag: string; + renderDepth: number; + layoutDoc: Doc; + ScreenToLocalTransform: () => Transform; + _timeline: HTMLDivElement | null; + focus: DocFocusFunc; + currentTimecode: () => number; + isSelected: (outsideReaction?: boolean) => boolean; + stackedTimeline: CollectionStackedTimeline; + trimStart: number; + trimEnd: number; } @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 + _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 ( + !LightboxView.LightboxDoc && + // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. + // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video. + /*(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; + } ); - const isDictation = - dictationDoc && - DocListCast(this.props.mark.links).some( - (link) => - Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc - ); - if ( - !LightboxView.LightboxDoc && - // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. - // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video. - /*(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 + } + 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 ); - 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); - this.props.focus(doc, { willZoom, scale, afterFocus, docTransform }); - }; - return { - anchor, - view: ( - <DocumentView - key="view" - {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - ref={action((r: DocumentView | null) => (anchor.view = r))} - Document={mark} - DataDoc={undefined} - renderDepth={this.props.renderDepth + 1} - LayoutTemplate={undefined} - LayoutTemplateString={LabelBox.LayoutString("data")} - isDocumentActive={returnFalse} - PanelWidth={() => width} - PanelHeight={() => height} - ScreenToLocalTransform={() => - this.props.ScreenToLocalTransform().translate(-x, -y) - } - focus={focusFunc} - rootSelected={returnFalse} - onClick={script} - onDoubleClick={ - this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript - } - ignoreAutoHeight={false} - hideResizeHandles={true} - bringToFront={emptyFunction} - scriptContext={this.props.stackedTimeline} - /> - ), - }; - }); - 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, this.props.mark, true)} - /> - <div - key="right" - className="collectionStackedTimeline-resizer" - onPointerDown={(e) => - this.onAnchorDown(e, this.props.mark, false) - } - /> - </> - )} - </> - ); - } + + @action + computeTitle = () => { + const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart; + const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart; + return `#${formatTime(start)}-${formatTime(end)}` + } + + 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); + this.props.focus(doc, { willZoom, scale, afterFocus, docTransform }); + }; + return { + anchor, + view: ( + <DocumentView + key="view" + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + ref={action((r: DocumentView | null) => (anchor.view = r))} + Document={mark} + DataDoc={undefined} + renderDepth={this.props.renderDepth + 1} + LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())} + isDocumentActive={returnFalse} + PanelWidth={() => width} + PanelHeight={() => height} + ScreenToLocalTransform={() => + this.props.ScreenToLocalTransform().translate(-x, -y) + } + focus={focusFunc} + rootSelected={returnFalse} + onClick={script} + onDoubleClick={ + this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript + } + ignoreAutoHeight={false} + hideResizeHandles={true} + bringToFront={emptyFunction} + scriptContext={this.props.stackedTimeline} + /> + ), + }; + }); + + 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, this.props.mark, true)} + /> + <div + key="right" + className="collectionStackedTimeline-resizer" + onPointerDown={(e) => + this.onAnchorDown(e, this.props.mark, false) + } + /> + </> + )} + </> + ); + } } Scripting.addGlobal(function formatToTime(time: number): any { - return formatTime(time); + return formatTime(time); +}); +Scripting.addGlobal(function min(num1: number, num2: number): number { + return Math.min(num1, num2); }); +Scripting.addGlobal(function max(num1: number, num2: number): number { + return Math.max(num1, num2); +});
\ No newline at end of file |
