aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-01-26 16:47:29 -0500
committerbobzel <zzzman@gmail.com>2021-01-26 16:47:29 -0500
commitdc919272a21ebdd21cae5986db135d893c42fe2d (patch)
tree77720ffcff9d335585d350e7cb1d0eb6044b5b0c
parent20add09510fb02b144d421910a56d3f3896b1f90 (diff)
refactored out timeline code from video and audio into StackedTimeline component.
-rw-r--r--src/client/views/GlobalKeyHandler.ts15
-rw-r--r--src/client/views/nodes/AudioBox.scss6
-rw-r--r--src/client/views/nodes/AudioBox.tsx308
-rw-r--r--src/client/views/nodes/StackedTimeline.scss374
-rw-r--r--src/client/views/nodes/StackedTimeline.tsx335
-rw-r--r--src/client/views/nodes/VideoBox.tsx411
6 files changed, 833 insertions, 616 deletions
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 342cdcd95..a07ba0a77 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,3 +1,4 @@
+import { random } from "lodash";
import { action, observable } from "mobx";
import { DateField } from "../../fields/DateField";
import { Doc, DocListCast } from "../../fields/Doc";
@@ -14,6 +15,7 @@ import { DragManager } from "../util/DragManager";
import { GroupManager } from "../util/GroupManager";
import { SelectionManager } from "../util/SelectionManager";
import { SharingManager } from "../util/SharingManager";
+import { SnappingManager } from "../util/SnappingManager";
import { undoBatch, UndoManager } from "../util/UndoManager";
import { CollectionDockingView } from "./collections/CollectionDockingView";
import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
@@ -22,13 +24,9 @@ import { DocumentDecorations } from "./DocumentDecorations";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { MainView } from "./MainView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
+import { StackedTimeline } from "./nodes/StackedTimeline";
import { AnchorMenu } from "./pdf/AnchorMenu";
-import { SnappingManager } from "../util/SnappingManager";
import { SearchBox } from "./search/SearchBox";
-import { random } from "lodash";
-import { DocumentView } from "./nodes/DocumentView";
-import { AudioBox } from "./nodes/AudioBox";
-import { VideoBox } from "./nodes/VideoBox";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -123,11 +121,8 @@ export class KeyManager {
DragManager.AbortDrag();
} else if (CollectionDockingView.Instance.HasFullScreen) {
CollectionDockingView.Instance.CloseFullScreen();
- } else if (VideoBox.SelectingRegion) {
- VideoBox.SelectingRegion = undefined;
- doDeselect = false;
- } else if (AudioBox.SelectingRegion) {
- AudioBox.SelectingRegion = undefined;
+ } else if (StackedTimeline.SelectingRegion) {
+ StackedTimeline.SelectingRegion = undefined;
doDeselect = false;
} else {
doDeselect = !ContextMenu.Instance.closeMenu();
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 4a1cee721..4a3bbf8d8 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -341,7 +341,7 @@
.audiobox-marker {
position: relative;
- height: calc(100% - 8px);
+ height: 100%;
margin-top: 8px;
}
}
@@ -350,14 +350,14 @@
.current-time {
position: absolute;
font-size: 8;
- top: calc(100% - 8px);
+ top: 100%;
left: 30px;
color: white;
}
.total-time {
position: absolute;
- top: calc(100% - 8px);
+ top: 100%;
font-size: 8;
right: 2px;
color: white;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 0faac44a7..b6f4af8a3 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -3,7 +3,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { computedFn } from "mobx-utils";
import Waveform from "react-audio-waveform";
import { DateField } from "../../../fields/DateField";
import { Doc, DocListCast, Opt } from "../../../fields/Doc";
@@ -13,24 +12,19 @@ import { createSchema, listSpec, makeInterface } from "../../../fields/Schema";
import { ComputedField, ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils, OmitKeys } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { formatTime, numberRange, Utils } from "../../../Utils";
+import { DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { Scripting } from "../../util/Scripting";
-import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
-import { DocumentView } from "./DocumentView";
+import "./AudioBox.scss";
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
import { LinkDocPreview } from "./LinkDocPreview";
-import "./AudioBox.scss";
-import { Id } from "../../../fields/FieldSymbols";
-import { LabelBox } from "./LabelBox";
-
+import { StackedTimeline } from "./StackedTimeline";
declare class MediaRecorder {
// whatever MediaRecorder has
constructor(e: any);
@@ -48,13 +42,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
static playheadWidth = 30; // width of playhead
static heightPercent = 80; // height of timeline in percent of height of audioBox.
static Instance: AudioBox;
- static RangeScript: ScriptField;
- static LabelScript: ScriptField;
- static RangePlayScript: ScriptField;
- static LabelPlayScript: ScriptField;
_disposers: { [name: string]: IReactionDisposer } = {};
_ele: HTMLAudioElement | null = null;
+ _audioRef = React.createRef<HTMLDivElement>();
+ _stackedTimeline = React.createRef<StackedTimeline>();
_recorder: any;
_recordStart = 0;
_pauseStart = 0;
@@ -62,16 +54,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
_pausedTime = 0;
_stream: MediaStream | undefined;
_start: number = 0;
- _hold: boolean = false;
- _left: boolean = false;
- _dragging = false;
_play: any = null;
- _audioRef = React.createRef<HTMLDivElement>();
- _timeline: Opt<HTMLDivElement>;
- _markerStart: number = 0;
- _currAnchor: Opt<Doc>;
- @observable static SelectingRegion: AudioBox | undefined = undefined;
@observable static _scrubTime = 0;
@observable _markerEnd: number = 0;
@observable _position: number = 0;
@@ -85,27 +69,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
@computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); }
@computed get links() { return DocListCast(this.dataDoc.links); }
@computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time
+ @computed get heightPercent() { return AudioBox.heightPercent; }
constructor(props: Readonly<FieldViewProps>) {
super(props);
AudioBox.Instance = this;
if (this.duration === undefined) {
- runInAction(() => {
- this.Document[this.fieldKey + "-duration"] = this.Document.duration;
- })
+ runInAction(() => this.Document[this.fieldKey + "-duration"] = this.Document.duration);
}
-
- // onClick play scripts
- AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
- AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
- AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
- AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
}
- anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor.audioStart))
- anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor.audioEnd, defaultVal))
-
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
@@ -118,7 +92,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
getAnchor = () => {
- return this.createAnchor(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined));
+ return this._stackedTimeline.current?.createAnchor(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc;
}
componentWillUnmount() {
@@ -150,7 +124,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._disposers.audioStop = reaction(
() => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._audioStop, "number", null) : undefined,
audioStop => audioStop !== undefined && setTimeout(() => {
- this._audioRef.current && this.pause();
+ this._audioRef.current && this.Pause();
setTimeout(() => this.Document._audioStop = undefined, 10);
}, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play
{ fireImmediately: true }
@@ -162,8 +136,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
const htmlEle = this._ele;
if (this.audioState !== "recording" && htmlEle) {
htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration);
- this.links.map(l => {
- const { la1, linkTime } = this.getLinkData(l);
+ this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => {
if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) {
Doc.linkFollowHighlight(la1);
}
@@ -173,7 +146,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
// pause play back
- pause = action(() => {
+ Pause = action(() => {
this._ele!.pause();
this.audioState = "paused";
});
@@ -185,20 +158,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// play back the audio from time
@action
- playOnClick = (anchorDoc: Doc) => {
- this.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.duration));
- return true;
- }
-
- // play back the audio from time
- @action
- clickAnchor = (anchorDoc: Doc) => {
- if (this.layoutDoc.autoPlay) return this.playOnClick(anchorDoc);
- this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = this.anchorStart(anchorDoc));
- return true;
- }
- // play back the audio from time
- @action
playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
clearTimeout(this._play);
if (Number.isNaN(this._ele?.duration)) {
@@ -208,17 +167,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
if (seekTimeInSeconds > -1) {
setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
} else {
- this.pause();
+ this.Pause();
}
} else if (seekTimeInSeconds <= this._ele.duration) {
this._ele.currentTime = seekTimeInSeconds;
this._ele.play();
runInAction(() => this.audioState = "playing");
if (endTime !== this.duration) {
- this._play = setTimeout(() => this.pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration
+ this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration
}
} else {
- this.pause();
+ this.Pause();
}
}
}
@@ -283,9 +242,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
// for play button
- onPlay = (e: any) => {
+ Play = (e?: any) => {
this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
- e.stopPropagation();
+ e?.stopPropagation?.();
}
// creates a text document for dictation
@@ -302,15 +261,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// ref for updating time
setRef = (e: HTMLAudioElement | null) => {
e?.addEventListener("timeupdate", this.timecodeChanged);
- e?.addEventListener("ended", this.pause);
+ e?.addEventListener("ended", this.Pause);
this._ele = e;
}
- // ref for timeline
- timelineRef = (timeline: HTMLDivElement) => {
- this._timeline = timeline;
- }
-
// returns the path of the audio file
@computed get path() {
const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
@@ -345,120 +299,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
e.stopPropagation();
}
- // 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.active()) {
- const wasPaused = this.audioState === "paused";
- this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.duration;
- wasPaused && this.pause();
-
- const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.duration;
- this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x);
- AudioBox.SelectingRegion = this;
- setupMoveUpEvents(this, e,
- action(e => {
- this._markerEnd = toTimeline(e.clientX - rect.x);
- return false;
- }),
- action((e, movement) => {
- this._markerEnd = toTimeline(e.clientX - rect.x);
- if (this._markerEnd < this._markerStart) {
- const tmp = this._markerStart;
- this._markerStart = this._markerEnd;
- this._markerEnd = tmp;
- }
- AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd);
- AudioBox.SelectingRegion = undefined;
- }),
- e => {
- this.props.select(false);
- e.shiftKey && this.createAnchor(this._ele!.currentTime);
- }
- , this.props.isSelected(true) || this._isChildActive);
- }
- }
-
- @action
- createAnchor(anchorStartTime?: number, anchorEndTime?: number) {
- if (anchorStartTime === undefined) return this.rootDoc;
- 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 (this.dataDoc[this.annotationKey]) {
- this.dataDoc[this.annotationKey].push(anchor);
- } else {
- this.dataDoc[this.annotationKey] = new List<Doc>([anchor]);
- }
- return anchor;
- }
-
- // 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);
- const toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration));
- setupMoveUpEvents(this, e,
- (e) => {
- const rect = (e.target as any).getBoundingClientRect();
- this.changeAnchor(this._currAnchor, toTimeline(e.clientX - rect.x, rect.width));
- return false;
- },
- (e) => {
- const rect = (e.target as any).getBoundingClientRect();
- this._ele!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x, rect.width);
- this._timeline?.releasePointerCapture(e.pointerId);
- },
- emptyFunction);
- }
-
- // updates the anchor with the new time
- @action
- changeAnchor = (anchor: Opt<Doc>, time: number) => {
- anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time);
- }
-
- // checks if the two anchors are the same with start and end time
- isSame = (m1: any, m2: any) => {
- return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2);
- }
-
- // 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() - AudioBox.playheadWidth;
- 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;
- }
-
- @computed get selectionContainer() {
- return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
- left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`,
- width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%`, height: "100%", top: "0%"
- }} />;
- }
-
// returns the audio waveform
@computed get waveform() {
const audioBuckets = Cast(this.dataDoc.audioBuckets, listSpec("number"), []);
@@ -488,15 +328,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
);
}
- rangeClickScript = () => AudioBox.RangeScript;
- labelClickScript = () => AudioBox.LabelScript;
- rangePlayScript = () => AudioBox.RangePlayScript;
- labelPlayScript = () => AudioBox.LabelPlayScript;
-
+ playing = () => { return this.audioState === "playing"; }
playLink = (link: Doc) => {
if (link.annotationOn === this.rootDoc) {
- if (this.layoutDoc.playOnSelect) this.playFrom(this.anchorStart(link), this.anchorEnd(link));
- else this._ele!.currentTime = this.layoutDoc._currentTimecode = this.anchorStart(link);
+ 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);
@@ -509,50 +345,34 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
});
}
- renderInner = computedFn(function (this: AudioBox, 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: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => anchor.view = r)}
- Document={mark}
- DataDoc={undefined}
- PanelWidth={() => width}
- PanelHeight={() => height}
- renderDepth={this.props.renderDepth + 1}
- focus={() => this.playLink(mark)}
- rootSelected={returnFalse}
- LayoutTemplate={undefined}
- LayoutTemplateString={LabelBox.LayoutString("data")}
- ContainingCollectionDoc={this.props.Document}
- removeDocument={this.removeDocument}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)}
- parentActive={(out) => this.props.isSelected(out) || this._isChildActive}
- whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
- onClick={script}
- onDoubleClick={this.layoutDoc.autoPlay ? undefined : doublescript}
- ignoreAutoHeight={false}
- bringToFront={emptyFunction}
- scriptContext={this} />
- };
- });
- renderAnchor = computedFn(function (this: AudioBox, 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) :
- <>
- <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} />
- <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} />
- </>}
- </>;
- });
+ @computed get renderTimeline() {
+ return <StackedTimeline ref={this._stackedTimeline}
+ Document={this.props.Document}
+ dataDoc={this.dataDoc}
+ anchorProps={this.props}
+ renderDepth={this.props.renderDepth + 1}
+ annotationKey={this.annotationKey}
+ duration={this.duration}
+ playFrom={this.playFrom}
+ setTime={(time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time}
+ playing={this.playing}
+ select={this.props.select}
+ isSelected={this.props.isSelected}
+ whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(0, -(100 - this.heightPercent) / 200 * this.props.PanelHeight())}
+ isChildActive={() => this._isChildActive}
+ Play={this.Play}
+ Pause={this.Pause}
+ active={this.active}
+ playLink={this.playLink}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={() => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100}// panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
+ />;
+ }
render() {
const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : "";
- const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth;
- const timelineContentHeight = (this.props.PanelHeight() * AudioBox.heightPercent / 100) * AudioBox.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
- const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
- const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
- const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
return <div className="audiobox-container"
onContextMenu={this.specificContextMenu}
onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined}
@@ -580,38 +400,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
<div className="audiobox-controls" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }} >
<div className="audiobox-dictation" />
<div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} >
- <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.onPlay}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div>
- <div className="audiobox-timeline" style={{ height: `${AudioBox.heightPercent}%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
- <div className="waveform" >
+ <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div>
+ <div className="audiobox-timeline" style={{ height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
+ <div className="waveform">
{this.waveform}
</div>
</div>
- <div className="audiobox-timeline" style={{ height: `${AudioBox.heightPercent}%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)` }} ref={this.timelineRef}
- onClick={e => { e.stopPropagation(); e.preventDefault(); }}
- onPointerDown={e => 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.duration);
- const left = start / this.duration * timelineContentWidth;
- const top = d.level / maxLevel * timelineContentHeight;
- const timespan = end - start;
- return this.layoutDoc.hideAnchors ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]}
- style={{ left, top, width: `${timespan / this.duration * 100}%`, height: `${1 / maxLevel * 100}%` }}
- onClick={e => { this.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} >
- {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript,
- left + AudioBox.playheadWidth,
- (1 - AudioBox.heightPercent / 100) / 2 * this.props.PanelHeight() + top,
- timelineContentWidth * timespan / this.duration,
- timelineContentHeight / maxLevel)}
- </div>;
- })}
- {this.selectionContainer}
- <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
- style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.duration * 100}%`, pointerEvents: "none" }}
- />
- </div>
+ {this.renderTimeline}
{this.audio}
<div className="current-time">
{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
@@ -624,5 +419,4 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
</div>;
}
-}
-Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/client/views/nodes/StackedTimeline.scss b/src/client/views/nodes/StackedTimeline.scss
new file mode 100644
index 000000000..da7310794
--- /dev/null
+++ b/src/client/views/nodes/StackedTimeline.scss
@@ -0,0 +1,374 @@
+.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
new file mode 100644
index 000000000..808ca982d
--- /dev/null
+++ b/src/client/views/nodes/StackedTimeline.tsx
@@ -0,0 +1,335 @@
+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<StackedTimelineProps> {
+ static RangeScript: ScriptField;
+ static LabelScript: ScriptField;
+ static RangePlayScript: ScriptField;
+ static LabelPlayScript: ScriptField;
+
+ _disposers: { [name: string]: IReactionDisposer } = {};
+ _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name
+ _ele: HTMLAudioElement | null = null;
+ _start: number = 0;
+ _left: boolean = false;
+ _dragging = false;
+ _play: any = null;
+ _audioRef = React.createRef<HTMLDivElement>();
+ _timeline: Opt<HTMLDivElement>;
+ _markerStart: number = 0;
+ _currAnchor: Opt<Doc>;
+
+ @observable static SelectingRegion: 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<StackedTimelineProps>) {
+ 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<Doc>, time: number) => {
+ anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time);
+ }
+
+ // checks if the two anchors are the same with start and end time
+ isSame = (m1: any, m2: any) => {
+ return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2);
+ }
+
+ @computed get selectionContainer() {
+ return StackedTimeline.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
+ left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.props.duration * 100}%`,
+ width: `${Math.abs(this._markerStart - this._markerEnd) / this.props.duration * 100}%`, height: "100%", top: "0%"
+ }} />;
+ }
+
+ // starting the drag event for anchor resizing
+ @action
+ onPointerDownTimeline = (e: React.PointerEvent): void => {
+ const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect();
+ if (rect && e.target !== this._audioRef.current && this.props.active()) {
+ const 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<Doc>([anchor]);
+ }
+ return anchor;
+ }
+
+ // play back the audio from time
+ @action
+ playOnClick = (anchorDoc: Doc) => {
+ this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration));
+ 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: <DocumentView key="view" {...OmitKeys(this.props.anchorProps, ["NativeWidth", "NativeHeight"]).omit}
+ ref={action((r: DocumentView | null) => anchor.view = r)}
+ Document={mark}
+ DataDoc={undefined}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
+ renderDepth={this.props.renderDepth + 1}
+ focus={() => this.props.playLink(mark)}
+ rootSelected={returnFalse}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LabelBox.LayoutString("data")}
+ ContainingCollectionDoc={this.props.Document}
+ removeDocument={this.props.removeDocument}
+ 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) :
+ <>
+ <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} />
+ <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} />
+ </>}
+ </>;
+ });
+
+ 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 <div className="audiobox-timeline" style={{ height: "100%", width: "100%" }} ref={this.timelineRef}
+ onClick={e => {
+ if (this.props.isChildActive() || this.props.isSelected(false)) {
+ e.stopPropagation(); e.preventDefault();
+ }
+ }}
+ onPointerDown={e => {
+ if (this.props.isChildActive() || this.props.isSelected(false)) {
+ e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e);
+ }
+ }}>
+ {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) :
+ <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]}
+ style={{ left, top, width: `${timespan / this.props.duration * 100}%`, height: `${1 / maxLevel * 100}%` }}
+ onClick={e => { this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} >
+ {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript,
+ left,
+ top,
+ timelineContentWidth * timespan / this.props.duration,
+ timelineContentHeight / maxLevel)}
+ </div>;
+ })}
+ {this.selectionContainer}
+ <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
+ style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }}
+ />
+ </div>
+ }
+}
+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 988dd47d8..c6b9661a2 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,35 +3,32 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
-import { Doc, Opt, DocListCast } from "../../../fields/Doc";
+import { Dictionary } from "typescript-collections";
+import { Doc, DocListCast } from "../../../fields/Doc";
+import { documentSchema } from "../../../fields/documentSchemas";
import { InkTool } from "../../../fields/InkField";
+import { List } from "../../../fields/List";
import { createSchema, makeInterface } from "../../../fields/Schema";
-import { Cast, StrCast, NumCast } from "../../../fields/Types";
+import { ComputedField } from "../../../fields/ScriptField";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { VideoField } from "../../../fields/URLField";
-import { Utils, emptyFunction, returnOne, returnZero, OmitKeys, setupMoveUpEvents, returnFalse, returnTrue, formatTime } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnOne, Utils, setupMoveUpEvents } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
+import { Networking } from "../../Network";
+import { SelectionManager } from "../../util/SelectionManager";
+import { SnappingManager } from "../../util/SnappingManager";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
+import { MarqueeAnnotator } from "../MarqueeAnnotator";
+import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
-import "./VideoBox.scss";
-import { documentSchema } from "../../../fields/documentSchemas";
-import { Networking } from "../../Network";
-import { SnappingManager } from "../../util/SnappingManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { List } from "../../../fields/List";
-import { DocumentView } from "./DocumentView";
-import { LinkDocPreview } from "./LinkDocPreview";
import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
-import { StyleProp } from "../StyleProvider";
-import { computedFn } from "mobx-utils";
-import { Dictionary } from "typescript-collections";
-import { MarqueeAnnotator } from "../MarqueeAnnotator";
-import { Id } from "../../../fields/FieldSymbols";
-import { LabelBox } from "./LabelBox";
+import { LinkDocPreview } from "./LinkDocPreview";
+import { StackedTimeline } from "./StackedTimeline";
+import "./VideoBox.scss";
const path = require('path');
export const timeSchema = createSchema({
@@ -45,35 +42,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
static _youtubeIframeCounter: number = 0;
static Instance: VideoBox;
- static RangeScript: ScriptField;
- static LabelScript: ScriptField;
- static RangePlayScript: ScriptField;
- static LabelPlayScript: ScriptField;
static heightPercent = 60; // height of timeline in percent of height of videoBox.
private _disposers: { [name: string]: IReactionDisposer } = {};
- private _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name
private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null;
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
- private _isResetClick = 0;
+ private _stackedTimeline = React.createRef<StackedTimeline>();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
- private _play: any = null;
- private _timeline: Opt<HTMLDivElement>;
- private _audioRef = React.createRef<HTMLDivElement>();
- private _markerStart: number = 0;
- private _left: boolean = false;
- private _duration = 0;
- private _start: boolean = true;
- private _currAnchor: Doc | undefined;
+ private _playRegionTimer: any = null;
+ private _playRegionDuration = 0;
@observable static _showControls: boolean;
- @observable static SelectingRegion: VideoBox | undefined = undefined;
@observable _marqueeing: number[] | undefined;
@observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
@observable _screenCapture = false;
@observable _visible: boolean = false;
- @observable _markerEnd: number = 0;
@observable _forceCreateYouTubeIFrame = false;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@@ -88,19 +72,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
constructor(props: Readonly<FieldViewProps>) {
super(props);
VideoBox.Instance = this;
-
- // onClick play scripts
- VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { this: Doc.name, clientX: "number", scriptContext: "any" })!;
- VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { this: Doc.name, clientX: "number", scriptContext: "any" })!;
- VideoBox.RangePlayScript = VideoBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!;
- VideoBox.LabelPlayScript = VideoBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!;
}
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.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null));
+ return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc;
}
choosePath(url: string) {
@@ -115,6 +93,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration;
}
+ static keyEventsWrapper = (e: KeyboardEvent) => {
+ VideoBox.Instance._stackedTimeline.current?.keyEvents(e);
+ }
+
@action public Play = (update: boolean = true) => {
document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true);
document.addEventListener("keydown", VideoBox.keyEventsWrapper, true);
@@ -272,9 +254,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
componentWillUnmount() {
this.Pause();
- this._disposers.reactionDisposer?.();
- this._disposers.youtubeReactionDisposer?.();
- this._disposers.videoStart?.();
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true);
}
@@ -405,11 +385,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
<div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} >
<FontAwesomeIcon icon="camera" size="lg" />
</div>,
- <div className="timeline-button" key="timeline-button" onPointerDown={this.toggleTimeline} style={{
- transform: `scale(${this.scaling()})`,
- right: this.scaling() * 10 - 10,
- bottom: this.scaling() * 10 - 10
- }}>
+ <div className="timeline-button" key="timeline-button" onPointerDown={action(e => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow)}
+ style={{
+ transform: `scale(${this.scaling()})`,
+ right: this.scaling() * 10 - 10,
+ bottom: this.scaling() * 10 - 10
+ }}>
<FontAwesomeIcon icon={this.layoutDoc._timelineShow ? "eye-slash" : "eye"} style={{ width: "100%" }} />
</div>,
VideoBox._showControls ? (null) : [
@@ -424,7 +405,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
]]);
}
-
onPlayDown = () => this._playing ? this.Pause() : this.Play();
onFullDown = (e: React.PointerEvent) => {
@@ -440,24 +420,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
onResetDown = (e: React.PointerEvent) => {
- this.Pause();
- e.stopPropagation();
- this._isResetClick = 0;
- document.addEventListener("pointermove", this.onResetMove, true);
- document.addEventListener("pointerup", this.onResetUp, true);
- }
-
- onResetMove = (e: PointerEvent) => {
- this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
- this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
- e.stopImmediatePropagation();
- }
-
- @action
- onResetUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onResetMove, true);
- document.removeEventListener("pointerup", this.onResetUp, true);
- this._isResetClick < 10 && (this.layoutDoc._currentTimecode = 0);
+ setupMoveUpEvents(this, e, (e: PointerEvent) => {
+ this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
+ e.stopImmediatePropagation();
+ return false;
+ }, emptyFunction,
+ (e: PointerEvent) => this.layoutDoc._currentTimecode = 0);
}
@computed get youtubeContent() {
@@ -482,8 +450,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
// play back the video from time
@action
playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
- clearTimeout(this._play);
- this._duration = endTime - seekTimeInSeconds;
+ clearTimeout(this._playRegionTimer);
+ this._playRegionDuration = endTime - seekTimeInSeconds;
if (Number.isNaN(this.player?.duration)) {
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
} else if (this.player) {
@@ -498,7 +466,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
this.player.play();
runInAction(() => this._playing = true);
if (endTime !== this.duration) {
- this._play = setTimeout(() => this.Pause(), (this._duration) * 1000); // use setTimeout to play a specific duration
+ this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration
}
} else {
this.Pause();
@@ -506,150 +474,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
}
- @action
- toggleTimeline = (e: React.PointerEvent) => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow
-
- // ref for timeline
- timelineRef = (timeline: HTMLDivElement) => this._timeline = timeline
-
- // starting the drag event creating a range mark
- @action
- onPointerDownTimeline = (e: React.PointerEvent): void => {
- const rect = this._timeline?.getBoundingClientRect();
- if (rect && e.target !== this._audioRef.current && this.active()) {
- const wasPlaying = this._playing;
- if (this._playing) this.Pause();
- else if (!this._doubleTime) {
- this._doubleTime = setTimeout(() => {
- this._doubleTime = undefined;
- this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.duration;
- }, 300);
- }
-
- this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
- VideoBox.SelectingRegion = this;
- setupMoveUpEvents(this, e,
- action(e => {
- this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
- return false;
- }),
- action((e, movement) => {
- if (this._markerEnd < this._markerStart) {
- const tmp = this._markerStart;
- this._markerStart = this._markerEnd;
- this._markerEnd = tmp;
- }
- VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd);
- VideoBox.SelectingRegion = undefined;
- }),
- (e, doubleTap) => {
- this.props.select(false);
- e.shiftKey && this.createAnchor(this.player!.currentTime);
- !wasPlaying && doubleTap && this.Play();
- }
- , this.props.isSelected(true) || this._isChildActive);
- }
- }
-
- @action
- createAnchor(anchorStartTime: number, anchorEndTime?: number) {
- const anchor = Docs.Create.LabelDocument({
- title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any,
- useLinkSmallAnchor: true, // bcz: note this also flags that the annotation is not on the video itself, just the timeline
- hideLinkButton: true,
- anchorStartTime,
- anchorEndTime,
- annotationOn: this.props.Document
- });
- if (this.dataDoc[this.annotationKey + "-timeline"]) {
- this.dataDoc[this.annotationKey + "-timeline"].push(anchor);
- } else {
- this.dataDoc[this.annotationKey + "-timeline"] = new List<Doc>([anchor]);
- }
- return anchor;
- }
-
- @action
- playOnClick = (anchorDoc: Doc, clientX: number) => {
- const seekTimeInSeconds = this.anchorStart(anchorDoc);
- const endTime = this.anchorEnd(anchorDoc);
- if (this.layoutDoc.autoPlay) {
- if (this._playing) this.Pause();
- else this.playFrom(seekTimeInSeconds, endTime);
- } else {
- if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) {
- if (!this.layoutDoc.autoPlay && this._playing) {
- this.Pause();
- } else {
- this.Play();
- }
- } else {
- this.playFrom(seekTimeInSeconds, endTime);
- }
- }
- return { select: true };
- }
-
- @action
- clickAnchor = (anchorDoc: Doc, clientX: number) => {
- const seekTimeInSeconds = this.anchorStart(anchorDoc);
- const endTime = this.anchorEnd(anchorDoc);
- if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
- if (this._playing) this.Pause();
- else if (this.layoutDoc.autoPlay) this.Play();
- else if (!this.layoutDoc.autoPlay) {
- const rect = this._timeline?.getBoundingClientRect();
- rect && this.Seek(this.toTimeline(clientX - rect.x, rect.width));
- }
- } else {
- if (this.layoutDoc.autoPlay) this.playFrom(seekTimeInSeconds, endTime);
- else this.Seek(seekTimeInSeconds);
- }
- return { select: true };
- }
-
- toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.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: PointerEvent) => {
- const rect = (e.target as any).getBoundingClientRect();
- this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width));
- return false;
- },
- (e: PointerEvent) => {
- const rect = (e.target as any).getBoundingClientRect();
- this.player!.currentTime = this.layoutDoc._currentTimecode = this.toTimeline(e.clientX - rect.x, rect.width);
- this._timeline?.releasePointerCapture(e.pointerId);
- },
- emptyFunction);
- }
-
- // makes sure no anchors overlaps each other by setting the correct position and width
- getLevel = (m: any, placed: { anchorStartTime: number, videoEnd: 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.videoEnd;
- 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, videoEnd: x2, level });
- return level;
- }
-
playLink = (doc: Doc) => {
const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow));
const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null));
@@ -658,145 +482,40 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
else this.Seek(startTime);
}
}
- // renders the anchors as a document
- renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) {
- const anchor = observable({ view: undefined as any });
- return {
- anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => anchor.view = r)}
- Document={mark}
- DataDoc={undefined}
- PanelWidth={() => width}
- PanelHeight={() => height}
- renderDepth={this.props.renderDepth + 1}
- focus={() => this.playLink(mark)}
- rootSelected={returnFalse}
- LayoutTemplate={undefined}
- LayoutTemplateString={LabelBox.LayoutString("data")}
- ContainingCollectionDoc={this.props.Document}
- removeDocument={(doc: Doc | Doc[]) => this.removeDocument(doc, annotationKey)}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(-x, -y)}
- parentActive={(out) => this.props.isSelected(out) || this._isChildActive}
- whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
- onClick={script}
- onDoubleClick={doublescript}
- ignoreAutoHeight={false}
- bringToFront={emptyFunction}
- scriptContext={this} />
- };
- });
-
- renderAnchor = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) {
- const inner = this.renderInner(mark, script, doublescript, x, y, width, height, annotationKey);
- return <>
- {inner.view}
- {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
- <>
- <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} />
- <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} />
- </>}
- </>;
- });
// returns the timeline
@computed get renderTimeline() {
- const timelineContentWidth = this.props.PanelWidth();
- const timelineContentHeight = this.props.PanelHeight() * (100 - this.heightPercent) / 100;
- const overlaps: { anchorStartTime: number, videoEnd: number, level: number }[] = [];
- const drawAnchors: { level: number, anchor: Doc }[] = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
- const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- return !this.layoutDoc._timelineShow ? (null) :
- <div className="audiobox-timeline" ref={this.timelineRef} style={{ height: `${100 - this.heightPercent}%` }}
- onClick={e => {
- if (this._isChildActive || this.props.isSelected()) {
- e.stopPropagation(); e.preventDefault();
- }
- }}
- onPointerDown={e => {
- if (this._isChildActive || this.props.isSelected()) {
- 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.duration);
- const left = start / this.duration * timelineContentWidth;
- const top = d.level / maxLevel * timelineContentHeight;
- const timespan = end - start;
- return this.layoutDoc.hideAnchors ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]}
- style={{ left, top, width: `${timespan / this.duration * 100}%`, height: `${1 / maxLevel * 100}%` }}
- onClick={e => { this.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} >
- {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript,
- left,
- top + (this.props.PanelHeight() - timelineContentHeight),
- timelineContentWidth * timespan / this.duration,
- timelineContentHeight / maxLevel, this.annotationKey + (m.anchorStartTime !== undefined ? "-timeline" : ""))}
- </div>;
- })}
- {this.selectionContainer}
- <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
- style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.duration * 100}%`, pointerEvents: "none" }}
- />
- </div>;
- }
-
- // updates the anchor with the new time
- @action
- changeAnchor = (anchor: Opt<Doc>, time: number) => {
- if (anchor) {
- const timelineOnly = Cast(anchor.anchorStartTime, "number", null) !== undefined;
- if (timelineOnly) this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time;
- else this._left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
- }
- }
-
- // checks if the two anchors are the same with start and end time
- isSame = (m1: any, m2: any) => {
- return m1.anchorStartTime === m2.anchorStartTime && m1.anchorEndTime === m2.anchorEndTime && m1._timecodeToShow === m2._timecodeToShow && m1._timecodeToHide === m2._timecodeToHide;
- }
-
- // returns the blue container when dragging
- @computed get selectionContainer() {
- return VideoBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
- left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`,
- width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%`, height: "100%", top: "0%"
- }} />;
- }
-
- static keyEventsWrapper = (e: KeyboardEvent) => {
- VideoBox.Instance.keyEvents(e);
- }
-
- // for creating key anchors with key events
- @action
- keyEvents = (e: KeyboardEvent) => {
- if (e.target instanceof HTMLInputElement) return;
- if (!this._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.player!.currentTime;
- if (this._start) {
- this._markerStart = this.player!.currentTime;
- this._start = false;
- this._visible = true;
- } else {
- this.createAnchor(this._markerStart, currTime);
- this._start = true;
- this._visible = false;
- }
- }
+ return <div style={{ width: "100%", height: `${100 - this.heightPercent}%`, position: "absolute" }}>
+ <StackedTimeline ref={this._stackedTimeline}
+ Document={this.props.Document}
+ dataDoc={this.dataDoc}
+ anchorProps={this.props}
+ renderDepth={this.props.renderDepth + 1}
+ annotationKey={this.annotationKey + "-timeline"}
+ duration={this.duration}
+ playFrom={this.playFrom}
+ setTime={(time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time}
+ playing={() => this._playing}
+ select={this.props.select}
+ isSelected={this.props.isSelected}
+ whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight())}
+ isChildActive={() => this._isChildActive}
+ Play={this.Play}
+ Pause={this.Pause}
+ active={this.active}
+ playLink={this.playLink}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={() => this.props.PanelHeight() * (100 - this.heightPercent) / 100}
+ />
+ </div>;
}
- rangeClickScript = () => VideoBox.RangeScript;
- labelClickScript = () => VideoBox.LabelScript;
- rangePlayScript = () => VideoBox.RangePlayScript;
- labelPlayScript = () => VideoBox.LabelPlayScript;
-
contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
@computed get annotationLayer() {
- return <div className="imageBox-annotationLayer" style={{ height: "100%" }} ref={this._annotationLayer} />;
+ return <div className="imageBox-annotationLayer" style={{ height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
}
marqueeDown = action((e: React.PointerEvent) => {