diff options
| author | geireann <geireann.lindfield@gmail.com> | 2021-10-14 15:01:19 -0400 |
|---|---|---|
| committer | geireann <geireann.lindfield@gmail.com> | 2021-10-14 15:01:19 -0400 |
| commit | 5bbd1b35d2c3855eae8405e26deb0c6679cc7c26 (patch) | |
| tree | c9d999f36b078d7fd8f55a74c94ce495c9fa8d4e /src/client/views/collections/CollectionStackedTimeline.tsx | |
| parent | be4fd2492ad706f30af28f33133a4df0e8049e12 (diff) | |
| parent | ed68bbec549dedeb89bcb584151b097863b52d0d (diff) | |
Merge branch 'master' into schema-view-En-Hua
Diffstat (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 776 |
1 files changed, 614 insertions, 162 deletions
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index a2c95df6e..89da6692a 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,5 +1,12 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +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"; @@ -8,7 +15,16 @@ 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 { + 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"; @@ -18,9 +34,18 @@ 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 { + DocAfterFocusFunc, + DocFocusFunc, + DocumentView, + DocumentViewProps, +} from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; +import { Colors } from "../global/globalEnums"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { DragManager } from "../../util/DragManager"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -36,11 +61,21 @@ export type CollectionStackedTimelineProps = { 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) { - @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; +export class CollectionStackedTimeline extends CollectionSubView< + PanZoomDocument, + CollectionStackedTimelineProps +>(PanZoomDocument) { + @observable static SelectingRegion: CollectionStackedTimeline | undefined = + undefined; static RangeScript: ScriptField; static LabelScript: ScriptField; static RangePlayScript: ScriptField; @@ -50,48 +85,111 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument private _markerStart: number = 0; @observable _markerEnd: number = 0; - get duration() { return this.props.duration; } - @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); } + get minLength() { + const rect = this._timeline?.getBoundingClientRect(); + if (rect) { + return 0.05 * this.duration; + } + return 0; + } + + get trimStart() { + return this.props.trimStart; + } + + get trimEnd() { + return this.props.trimEnd; + } + + 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}%` - }} />; + 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}%`, + }} + /> + ); } 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" })!; + 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); } + componentDidMount() { + document.addEventListener("keydown", this.keyEvents, true); + } componentWillUnmount() { document.removeEventListener("keydown", this.keyEvents, true); - if (CollectionStackedTimeline.SelectingRegion === this) runInAction(() => CollectionStackedTimeline.SelectingRegion = undefined); + if (CollectionStackedTimeline.SelectingRegion === this) { + runInAction( + () => (CollectionStackedTimeline.SelectingRegion = undefined) + ); + } } - anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])); + 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); + 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)); + 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)) { + 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.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this.currentTime + ); CollectionStackedTimeline.SelectingRegion = undefined; } } @@ -101,7 +199,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument 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])); + 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; @@ -118,10 +219,18 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument 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); + 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); @@ -134,32 +243,148 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument this._markerStart = this._markerEnd; this._markerEnd = tmp; } - 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); + if ( + !isClick && + CollectionStackedTimeline.SelectingRegion === this && + Math.abs(movement[0]) > 15 && + !this.props.trimming + ) { + const anchor = CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this._markerStart, + this._markerEnd + ); + setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false)); } - (!isClick || !wasSelecting) && (CollectionStackedTimeline.SelectingRegion = undefined); + (!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); + 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)); + 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); + } + }) + ); + } + + @action + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { + if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; + if (!super.onInternalDrop(e, de)) return false; + + + // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view + + return true; + } + + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, 0); + return false; } @undoBatch @action - static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number) { + static createAnchor( + rootDoc: Doc, + dataDoc: Doc, + fieldKey: string, + startTag: string, + endTag: string, + anchorStartTime?: number, + anchorEndTime?: number, + docAnchor?: Doc + ) { if (anchorStartTime === undefined) return rootDoc; - const anchor = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any, + const anchor = docAnchor ?? Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction( + `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])` + ) as any, + _stayInCollection: true, useLinkSmallAnchor: true, hideLinkButton: true, annotationOn: rootDoc, - _timelineLabel: true + _timelineLabel: true, }); Doc.GetProto(anchor)[startTag] = anchorStartTime; Doc.GetProto(anchor)[endTag] = anchorEndTime; @@ -179,7 +404,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument 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 ( + seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && + endTime > NumCast(this.layoutDoc._currentTimecode) + ) { if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) { this.props.Pause(); } else { @@ -194,39 +422,60 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument @action clickAnchor = (anchorDoc: Doc, clientX: number) => { - if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + 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 ( + 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) { const rect = this._timeline?.getBoundingClientRect(); - rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); + 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); + if (this.layoutDoc.autoPlayAnchors) { + this.props.playFrom(seekTimeInSeconds, endTime); + } + else { + this.props.setTime(seekTimeInSeconds); + } } 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 }[]) => { + 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); + 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; - } - })); + 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); @@ -235,82 +484,185 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument } 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()); + 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>; + return !dictation ? null : ( + <div + style={{ + position: "absolute", + height: "100%", + top: this.timelineContentHeight(), + background: Colors.LIGHT_BLUE, + }} + > + <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" > + return !this.props.mediaPath ? null : ( + <div className="collectionStackedTimeline-waveform"> <AudioWaveform duration={this.duration} mediaPath={this.props.mediaPath} - dataDoc={this.dataDoc} - PanelHeight={this.timelineContentHeight} /> - </div>; + 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 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) : - <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>; + const isActive = + this.props.isContentActive() || this.props.isSelected(false); + return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}> + <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 = 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>); } } @@ -335,6 +687,8 @@ interface StackedTimelineAnchorProps { currentTimecode: () => number; isSelected: (outsideReaction?: boolean) => boolean; stackedTimeline: CollectionStackedTimeline; + trimStart: number; + trimEnd: number; } @observer class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> { @@ -345,22 +699,41 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this._lastTimecode = this.props.currentTimecode(); } componentDidMount() { - this._disposer = reaction(() => this.props.currentTimecode(), + 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 + 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 && + 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 < 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?.(); @@ -373,57 +746,136 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> 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; + 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, + setupMoveUpEvents( + this, + e, (e) => changeAnchor(anchor, left, newTime(e)), (e) => { this.props.setTime(newTime(e)); this.props._timeline?.releasePointerCapture(e.pointerId); }, - emptyFunction); + emptyFunction + ); } - renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + + @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) => { + 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} /> + 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)} /> - </>} - </>; + 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); });
\ No newline at end of file +Scripting.addGlobal(function formatToTime(time: number): any { + 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 |
