import React = require("react"); import { action, computed, IReactionDisposer, observable, reaction, runInAction, } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; 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, 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 { LightboxView } from "../LightboxView"; import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps, } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; 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 }; }; @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; 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; } 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 : (
); } 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; } } } }; 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; } 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) ); } }; @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 ); } }; @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 ); } }; 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