From eb2e88ef810eed9c1d31b3b2fdc3ba848f067c53 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 26 Jan 2021 20:12:55 -0500 Subject: made StackedTimeline a collectionview and renamed CollectionStackedTimeline. Now timeline anchors will observe filters. --- src/client/views/GlobalKeyHandler.ts | 6 +- .../collections/CollectionStackedTimeline.scss | 373 ++++++++++++++++++++ .../collections/CollectionStackedTimeline.tsx | 324 ++++++++++++++++++ src/client/views/collections/CollectionView.tsx | 3 +- src/client/views/nodes/AudioBox.tsx | 49 ++- src/client/views/nodes/StackedTimeline.scss | 374 --------------------- src/client/views/nodes/StackedTimeline.tsx | 335 ------------------ src/client/views/nodes/VideoBox.tsx | 27 +- 8 files changed, 752 insertions(+), 739 deletions(-) create mode 100644 src/client/views/collections/CollectionStackedTimeline.scss create mode 100644 src/client/views/collections/CollectionStackedTimeline.tsx delete mode 100644 src/client/views/nodes/StackedTimeline.scss delete mode 100644 src/client/views/nodes/StackedTimeline.tsx (limited to 'src') diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index a07ba0a77..e56ba38dd 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -24,7 +24,7 @@ import { DocumentDecorations } from "./DocumentDecorations"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { MainView } from "./MainView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; -import { StackedTimeline } from "./nodes/StackedTimeline"; +import { CollectionStackedTimeline } from "./collections/CollectionStackedTimeline"; import { AnchorMenu } from "./pdf/AnchorMenu"; import { SearchBox } from "./search/SearchBox"; @@ -121,8 +121,8 @@ export class KeyManager { DragManager.AbortDrag(); } else if (CollectionDockingView.Instance.HasFullScreen) { CollectionDockingView.Instance.CloseFullScreen(); - } else if (StackedTimeline.SelectingRegion) { - StackedTimeline.SelectingRegion = undefined; + } else if (CollectionStackedTimeline.SelectingRegion) { + CollectionStackedTimeline.SelectingRegion = undefined; doDeselect = false; } else { doDeselect = !ContextMenu.Instance.closeMenu(); diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss new file mode 100644 index 000000000..1bb5bc720 --- /dev/null +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -0,0 +1,373 @@ +.audiobox-container, +.audiobox-container-interactive { + width: 100%; + height: 100%; + position: inherit; + display: flex; + position: relative; + cursor: default; + + .audiobox-inner { + width:100%; + height: 100%; + } + + .audiobox-buttons { + display: flex; + width: 100%; + align-items: center; + height: 100%; + + .audiobox-dictation { + position: relative; + width: 30px; + height: 100%; + align-items: center; + display: inherit; + background: dimgray; + left: 0px; + } + + .audiobox-dictation:hover { + color: white; + cursor: pointer; + } + } + + .audiobox-handle { + width: 20px; + height: 100%; + display: inline-block; + } + + .audiobox-control, + .audiobox-control-interactive { + top: 0; + max-height: 32px; + width: 100%; + display: inline-block; + pointer-events: none; + } + + .audiobox-control-interactive { + pointer-events: all; + } + + .audiobox-record { + pointer-events: all; + width: 100%; + height: 100%; + position: relative; + pointer-events: none; + } + + .audiobox-record-interactive { + pointer-events: all; + width: 100%; + height: 100%; + position: relative; + + + } + + .recording { + margin-top: auto; + margin-bottom: auto; + width: 100%; + height: 100%; + position: relative; + padding-right: 5px; + display: flex; + background-color: red; + + .time { + position: relative; + height: 100%; + width: 100%; + font-size: 20; + text-align: center; + top: 5; + } + + .buttons { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 5px; + } + + .buttons:hover { + background-color: crimson; + } + } + + .audiobox-controls { + width: 100%; + height: 100%; + position: relative; + display: flex; + padding-left: 2px; + background: black; + + .audiobox-dictation { + position: absolute; + width: 30px; + height: 100%; + align-items: center; + display: inherit; + background: dimgray; + left: 0px; + } + + .audiobox-player { + margin-top: auto; + margin-bottom: auto; + width: 100%; + position: relative; + padding-right: 5px; + display: flex; + + .audiobox-playhead { + position: relative; + margin-top: auto; + margin-bottom: auto; + margin-right: 2px; + height: 25px; + padding: 2px; + border-radius: 50%; + background-color: black; + color: white; + } + + .audiobox-playhead:hover { + // background-color: black; + // border-radius: 5px; + background-color: grey; + color: lightgrey; + } + + .audiobox-dictation { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 2px; + align-items: center; + display: inherit; + background: dimgray; + } + + .audiobox-timeline { + position: absolute; + width: 100%; + border: gray solid 1px; + border-radius: 3px; + z-index: 1000; + overflow: hidden; + + .audiobox-container { + position: absolute; + width: 10px; + top: 2.5%; + height: 0px; + background: lightblue; + border-radius: 5px; + // box-shadow: black 2px 2px 1px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: darkblue; + border-width: 1px; + } + + .audiobox-current { + width: 1px; + height: 100%; + background-color: red; + position: absolute; + top: 0px; + } + + .waveform { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + z-index: -1000; + bottom: 0; + pointer-events: none; + div { + height: 100% !important; + width: 100% !important; + } + canvas { + height: 100% !important; + width: 100% !important; + } + } + + .audiobox-linker, + .audiobox-linker-mini { + position: absolute; + width: 15px; + min-height: 10px; + height: 15px; + margin-left: -2.55px; + background: gray; + border-radius: 100%; + opacity: 0.9; + box-shadow: black 2px 2px 1px; + + .linkAnchorBox-cont { + position: relative !important; + height: 100% !important; + width: 100% !important; + left: unset !important; + top: unset !important; + } + } + + .audiobox-linker-mini { + width: 8px; + min-height: 8px; + height: 8px; + box-shadow: black 1px 1px 1px; + margin-left: -1; + margin-top: -2; + + .linkAnchorBox-cont { + position: relative !important; + height: 100% !important; + width: 100% !important; + left: unset !important; + top: unset !important; + } + } + + .audiobox-linker:hover, + .audiobox-linker-mini:hover { + opacity: 1; + } + + .audiobox-marker-container, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 10px; + top: 2.5%; + background: gray; + border-radius: 50%; + box-shadow: black 2px 2px 1px; + overflow: visible; + cursor: pointer; + + .audiobox-marker { + position: relative; + height: 100%; + // height: calc(100% - 15px); + width: 100%; + //margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + } + + .audiobox-marker-timeline, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 90%; + top: 2.5%; + border-radius: 5px; + + .left-resizer { + background: dimgrey; + } + .resizer { + background: dimgrey; + } + + .audiobox-marker { + position: relative; + height: calc(100% - 15px); + margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + + .resizer { + position: absolute; + top: 0; + right: 0; + pointer-events: all; + cursor: ew-resize; + height: 100%; + width: 10px; + z-index: 100; + } + + .click { + position: relative; + height: 100%; + width: 100%; + z-index: 100; + } + + .left-resizer { + position: absolute; + left: 0; + top : 0; + pointer-events: all; + cursor: ew-resize; + height: 100%; + width: 10px; + z-index: 100; + } + } + + .audiobox-marker-timeline:hover, + .audiobox-marker-minicontainer:hover { + opacity: 0.8; + } + + .audiobox-marker-minicontainer { + width: 5px; + border-radius: 1px; + + .audiobox-marker { + position: relative; + height: calc(100% - 8px); + margin-top: 8px; + } + } + } + } + } +} + +@media only screen and (max-device-width: 480px) { + .audiobox-dictation { + font-size: 5em; + display: flex; + width: 100; + justify-content: center; + flex-direction: column; + align-items: center; + } + + .audiobox-container .audiobox-record, + .audiobox-container-interactive .audiobox-record { + font-size: 3em; + } + + .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, + .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, + .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead { + width: 70px; + } +} \ No newline at end of file diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx new file mode 100644 index 000000000..1775250fa --- /dev/null +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -0,0 +1,324 @@ +import React = require("react"); +import { action, computed, IReactionDisposer, observable } from "mobx"; +import { observer } from "mobx-react"; +import { computedFn } from "mobx-utils"; +import { Doc, Opt } 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 { Docs } from "../../documents/Documents"; +import { Scripting } from "../../util/Scripting"; +import { SelectionManager } from "../../util/SelectionManager"; +import { CollectionSubView } from "../collections/CollectionSubView"; +import { DocumentView } 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; + isChildActive: () => boolean; +}; + +@observer +export class CollectionStackedTimeline extends CollectionSubView(PanZoomDocument) { + 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(); + _timeline: Opt; + _markerStart: number = 0; + _currAnchor: Opt; + + @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; + @observable _markerEnd: number = 0; + @observable _position: number = 0; + @computed get anchorDocs() { return this.childDocs; } + @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); } + + 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" })!; + } + + // 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; + } + } + } + + 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)); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + } + return { la1, la2, linkTime }; + } + + // ref for timeline + timelineRef = (timeline: HTMLDivElement) => { + this._timeline = timeline; + } + + // updates the anchor with the new time + @action + changeAnchor = (anchor: Opt, 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) :
; + } + + // 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 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; + setupMoveUpEvents(this, e, + action(e => { + this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); + return false; + }), + action((e, movement) => { + 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; + }), + (e, doubleTap) => { + this.props.select(false); + e.shiftKey && this.createAnchor(this.currentTime); + !wasPlaying && doubleTap && this.props.Play(); + } + , this.props.isSelected(true) || this.props.isChildActive()); + } + } + + @action + createAnchor(anchorStartTime?: number, anchorEndTime?: number) { + if (anchorStartTime === undefined) return this.props.Document; + const anchor = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, + useLinkSmallAnchor: true, + hideLinkButton: true, + anchorStartTime, + anchorEndTime, + annotationOn: this.props.Document + }); + if (Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []).push(anchor); + } else { + this.dataDoc[this.props.fieldKey] = new List([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)); + 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)); + 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; + this._timeline?.setPointerCapture(e.pointerId); + setupMoveUpEvents(this, 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._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); + 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; + } + + renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + const anchor = observable({ view: undefined as any }); + return { + anchor, view: 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} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} + parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()} + whenActiveChanged={this.props.whenActiveChanged} + onClick={script} + onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + }; + }); + renderAnchor = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + const inner = this.renderInner(mark, script, doublescript, x, y, width, height); + return <> + {inner.view} + {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : + <> +
this.onPointerDown(e, mark, true)} /> +
this.onPointerDown(e, mark, false)} /> + } + ; + }); + + render() { + const timelineContentWidth = this.props.PanelWidth(); + const timelineContentHeight = this.props.PanelHeight(); + 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
{ + 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); + } + }}> + {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 top = d.level / maxLevel * timelineContentHeight; + const timespan = end - start; + return this.props.Document.hideAnchors ? (null) : +
{ this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > + {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, + left, + top, + timelineContentWidth * timespan / this.props.duration, + timelineContentHeight / maxLevel)} +
; + })} + {this.selectionContainer} +
{ e.stopPropagation(); e.preventDefault(); }} + style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }} + /> +
; + } +} +Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 6b9b1a3c0..03d8606d7 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -59,7 +59,8 @@ export enum CollectionViewType { //Staff = "staff", Map = "map", Grid = "grid", - Pile = "pileup" + Pile = "pileup", + StackedTimeline = "stacked timeline" } export interface CollectionViewProps extends FieldViewProps { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index f509bfd64..c8bec74fb 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -9,14 +9,15 @@ import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { List } from "../../../fields/List"; import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { formatTime, numberRange, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SnappingManager } from "../../util/SnappingManager"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -24,7 +25,6 @@ import "./AudioBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; -import { StackedTimeline } from "./StackedTimeline"; declare class MediaRecorder { // whatever MediaRecorder has constructor(e: any); @@ -46,7 +46,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent(); - _stackedTimeline = React.createRef(); + _stackedTimeline = React.createRef(); _recorder: any; _recordStart = 0; _pauseStart = 0; @@ -327,30 +327,43 @@ export class AudioBox extends ViewBoxAnnotatableComponent { return this.audioState === "playing"; } + playing = () => this.audioState === "playing"; playLink = (link: Doc) => { if (link.annotationOn === this.rootDoc) { if (this.layoutDoc.playOnSelect) this.playFrom(this._stackedTimeline.current?.anchorStart(link) || 0, this._stackedTimeline.current?.anchorEnd(link)); else this._ele!.currentTime = this.layoutDoc._currentTimecode = (this._stackedTimeline.current?.anchorStart(link) || 0); } - else this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { - const { la1, la2 } = this.getLinkData(l); - const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null)); - const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null)); - if (startTime !== undefined) { - if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); - else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; - } - }); + else { + this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { + const { la1, la2 } = this.getLinkData(l); + const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null)); + const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null)); + if (startTime !== undefined) { + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; + } + }); + } } @computed get renderTimeline() { - return this._ele!.currentTime = this.layoutDoc._currentTimecode = time} diff --git a/src/client/views/nodes/StackedTimeline.scss b/src/client/views/nodes/StackedTimeline.scss deleted file mode 100644 index da7310794..000000000 --- a/src/client/views/nodes/StackedTimeline.scss +++ /dev/null @@ -1,374 +0,0 @@ -.audiobox-container, -.audiobox-container-interactive { - width: 100%; - height: 100%; - position: inherit; - display: flex; - position: relative; - cursor: default; - - .audiobox-inner { - width:100%; - height: 100%; - } - - .audiobox-buttons { - display: flex; - width: 100%; - align-items: center; - height: 100%; - - .audiobox-dictation { - position: relative; - width: 30px; - height: 100%; - align-items: center; - display: inherit; - background: dimgray; - left: 0px; - } - - .audiobox-dictation:hover { - color: white; - cursor: pointer; - } - } - - .audiobox-handle { - width: 20px; - height: 100%; - display: inline-block; - } - - .audiobox-control, - .audiobox-control-interactive { - top: 0; - max-height: 32px; - width: 100%; - display: inline-block; - pointer-events: none; - } - - .audiobox-control-interactive { - pointer-events: all; - } - - .audiobox-record { - pointer-events: all; - width: 100%; - height: 100%; - position: relative; - pointer-events: none; - } - - .audiobox-record-interactive { - pointer-events: all; - width: 100%; - height: 100%; - position: relative; - - - } - - .recording { - margin-top: auto; - margin-bottom: auto; - width: 100%; - height: 100%; - position: relative; - padding-right: 5px; - display: flex; - background-color: red; - - .time { - position: relative; - height: 100%; - width: 100%; - font-size: 20; - text-align: center; - top: 5; - } - - .buttons { - position: relative; - margin-top: auto; - margin-bottom: auto; - width: 25px; - padding: 5px; - } - - .buttons:hover { - background-color: crimson; - } - } - - .audiobox-controls { - width: 100%; - height: 100%; - position: relative; - display: flex; - padding-left: 2px; - background: black; - - .audiobox-dictation { - position: absolute; - width: 30px; - height: 100%; - align-items: center; - display: inherit; - background: dimgray; - left: 0px; - } - - .audiobox-player { - margin-top: auto; - margin-bottom: auto; - width: 100%; - position: relative; - padding-right: 5px; - display: flex; - - .audiobox-playhead { - position: relative; - margin-top: auto; - margin-bottom: auto; - margin-right: 2px; - height: 25px; - padding: 2px; - border-radius: 50%; - background-color: black; - color: white; - } - - .audiobox-playhead:hover { - // background-color: black; - // border-radius: 5px; - background-color: grey; - color: lightgrey; - } - - .audiobox-dictation { - position: relative; - margin-top: auto; - margin-bottom: auto; - width: 25px; - padding: 2px; - align-items: center; - display: inherit; - background: dimgray; - } - - .audiobox-timeline { - position: absolute; - width: 100%; - border: gray solid 1px; - border-radius: 3px; - z-index: 1000; - overflow: hidden; - - .audiobox-container { - position: absolute; - width: 10px; - top: 2.5%; - height: 0px; - background: lightblue; - border-radius: 5px; - // box-shadow: black 2px 2px 1px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: darkblue; - border-width: 1px; - } - - .audiobox-current { - width: 1px; - height: 100%; - background-color: red; - position: absolute; - top: 0px; - } - - .waveform { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - z-index: -1000; - bottom: 0; - pointer-events: none; - div { - height: 100% !important; - width: 100% !important; - } - canvas { - height: 100% !important; - width: 100% !important; - } - } - - .audiobox-linker, - .audiobox-linker-mini { - position: absolute; - width: 15px; - min-height: 10px; - height: 15px; - margin-left: -2.55px; - background: gray; - border-radius: 100%; - opacity: 0.9; - box-shadow: black 2px 2px 1px; - - .linkAnchorBox-cont { - position: relative !important; - height: 100% !important; - width: 100% !important; - left: unset !important; - top: unset !important; - } - } - - .audiobox-linker-mini { - width: 8px; - min-height: 8px; - height: 8px; - box-shadow: black 1px 1px 1px; - margin-left: -1; - margin-top: -2; - - .linkAnchorBox-cont { - position: relative !important; - height: 100% !important; - width: 100% !important; - left: unset !important; - top: unset !important; - } - } - - .audiobox-linker:hover, - .audiobox-linker-mini:hover { - opacity: 1; - } - - .audiobox-marker-container, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 10px; - top: 2.5%; - background: gray; - border-radius: 50%; - box-shadow: black 2px 2px 1px; - overflow: visible; - cursor: pointer; - - .audiobox-marker { - position: relative; - height: 100%; - // height: calc(100% - 15px); - width: 100%; - //margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - } - - .audiobox-marker-timeline, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 90%; - top: 2.5%; - border-radius: 5px; - - .left-resizer { - background: dimgrey; - } - .resizer { - background: dimgrey; - } - - .audiobox-marker { - position: relative; - height: calc(100% - 15px); - margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - - .resizer { - position: absolute; - top: 0; - right: 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - - .click { - position: relative; - height: 100%; - width: 100%; - z-index: 100; - } - - .left-resizer { - position: absolute; - left: 0; - top : 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - } - - .audiobox-marker-timeline:hover, - .audiobox-marker-minicontainer:hover { - opacity: 0.8; - } - - .audiobox-marker-minicontainer { - width: 5px; - border-radius: 1px; - - .audiobox-marker { - position: relative; - height: calc(100% - 8px); - margin-top: 8px; - } - } - } - } - } -} - - -@media only screen and (max-device-width: 480px) { - .audiobox-dictation { - font-size: 5em; - display: flex; - width: 100; - justify-content: center; - flex-direction: column; - align-items: center; - } - - .audiobox-container .audiobox-record, - .audiobox-container-interactive .audiobox-record { - font-size: 3em; - } - - .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, - .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, - .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead { - width: 70px; - } -} \ No newline at end of file diff --git a/src/client/views/nodes/StackedTimeline.tsx b/src/client/views/nodes/StackedTimeline.tsx deleted file mode 100644 index 808ca982d..000000000 --- a/src/client/views/nodes/StackedTimeline.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import React = require("react"); -import { action, computed, IReactionDisposer, observable } from "mobx"; -import { observer } from "mobx-react"; -import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { Scripting } from "../../util/Scripting"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Transform } from "../../util/Transform"; -import "./StackedTimeline.scss"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; -import { LabelBox } from "./LabelBox"; - -export interface StackedTimelineProps { - Document: Doc; - dataDoc: Doc; - anchorProps: DocumentViewProps; - renderDepth: number; - annotationKey: string; - duration: number; - Play: () => void; - Pause: () => void; - playLink: (linkDoc: Doc) => void; - playFrom: (seekTimeInSeconds: number, endTime?: number) => void; - playing: () => boolean; - setTime: (time: number) => void; - select: (ctrlKey: boolean) => void; - isSelected: (outsideReaction: boolean) => boolean; - whenActiveChanged: (isActive: boolean) => void; - removeDocument: (doc: Doc | Doc[]) => boolean; - ScreenToLocalTransform: () => Transform; - isChildActive: () => boolean; - active: () => boolean; - PanelWidth: () => number; - PanelHeight: () => number; -} - -@observer -export class StackedTimeline extends React.Component { - 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(); - _timeline: Opt; - _markerStart: number = 0; - _currAnchor: Opt; - - @observable static SelectingRegion: StackedTimeline | undefined = undefined; - @observable _markerEnd: number = 0; - @observable _position: number = 0; - @computed get anchorDocs() { return DocListCast(this.props.dataDoc[this.props.annotationKey]); } - @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); } - - constructor(props: Readonly) { - super(props); - // onClick play scripts - StackedTimeline.RangeScript = StackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; - StackedTimeline.LabelScript = StackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; - StackedTimeline.RangePlayScript = StackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; - StackedTimeline.LabelPlayScript = StackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; - } - - // 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; - } - } - } - - 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)); - if (Doc.AreProtosEqual(la1, this.props.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - } - return { la1, la2, linkTime }; - } - - // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { - this._timeline = timeline; - } - - // updates the anchor with the new time - @action - changeAnchor = (anchor: Opt, 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 StackedTimeline.SelectingRegion !== this ? (null) :
; - } - - // 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 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); - StackedTimeline.SelectingRegion = this; - setupMoveUpEvents(this, e, - action(e => { - this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); - return false; - }), - action((e, movement) => { - 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; - } - StackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); - StackedTimeline.SelectingRegion = undefined; - }), - (e, doubleTap) => { - this.props.select(false); - e.shiftKey && this.createAnchor(this.currentTime); - !wasPlaying && doubleTap && this.props.Play(); - } - , this.props.isSelected(true) || this.props.isChildActive()); - } - } - - @action - createAnchor(anchorStartTime?: number, anchorEndTime?: number) { - if (anchorStartTime === undefined) return this.props.Document; - const anchor = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, - useLinkSmallAnchor: true, - hideLinkButton: true, - anchorStartTime, - anchorEndTime, - annotationOn: this.props.Document - }); - if (Cast(this.props.dataDoc[this.props.annotationKey], listSpec(Doc), null) !== undefined) { - Cast(this.props.dataDoc[this.props.annotationKey], listSpec(Doc), []).push(anchor); - } else { - this.props.dataDoc[this.props.annotationKey] = new List([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)); - 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)); - 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; - this._timeline?.setPointerCapture(e.pointerId); - setupMoveUpEvents(this, 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._timeline?.releasePointerCapture(e.pointerId); - }, - emptyFunction); - } - - rangeClickScript = () => StackedTimeline.RangeScript; - labelClickScript = () => StackedTimeline.LabelScript; - rangePlayScript = () => StackedTimeline.RangePlayScript; - labelPlayScript = () => StackedTimeline.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); - 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; - } - - renderInner = computedFn(function (this: StackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const anchor = observable({ view: undefined as any }); - return { - anchor, view: 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} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} - parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()} - whenActiveChanged={this.props.whenActiveChanged} - onClick={script} - onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} - ignoreAutoHeight={false} - bringToFront={emptyFunction} - scriptContext={this} /> - }; - }); - renderAnchor = computedFn(function (this: StackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const inner = this.renderInner(mark, script, doublescript, x, y, width, height); - return <> - {inner.view} - {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : - <> -
this.onPointerDown(e, mark, true)} /> -
this.onPointerDown(e, mark, false)} /> - } - ; - }); - - render() { - const timelineContentWidth = this.props.PanelWidth(); - const timelineContentHeight = this.props.PanelHeight(); - 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
{ - 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); - } - }}> - {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 top = d.level / maxLevel * timelineContentHeight; - const timespan = end - start; - return this.props.Document.hideAnchors ? (null) : -
{ this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > - {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, - left, - top, - timelineContentWidth * timespan / this.props.duration, - timelineContentHeight / maxLevel)} -
; - })} - {this.selectionContainer} -
{ e.stopPropagation(); e.preventDefault(); }} - style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }} - /> -
- } -} -Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index f89f54309..bfac7dc1c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -16,6 +16,7 @@ import { Networking } from "../../Network"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -25,7 +26,6 @@ import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; -import { StackedTimeline } from "./StackedTimeline"; import "./VideoBox.scss"; const path = require('path'); @@ -46,7 +46,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent(); + private _stackedTimeline = React.createRef(); private _mainCont: React.RefObject = React.createRef(); private _annotationLayer: React.RefObject = React.createRef(); private _playRegionTimer: any = null; @@ -72,8 +72,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent 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))) + 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))); getAnchor = () => { return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc; @@ -484,12 +484,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent - this.player!.currentTime = this.layoutDoc._currentTimecode = time} -- cgit v1.2.3-70-g09d2