aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/AudioBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/AudioBox.tsx')
-rw-r--r--src/client/views/nodes/AudioBox.tsx458
1 files changed, 188 insertions, 270 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1c2812e86..c3c876b75 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -3,32 +3,31 @@ 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";
import { documentSchema } from "../../../fields/documentSchemas";
-import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { createSchema, listSpec, makeInterface } from "../../../fields/Schema";
import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { Cast, DateCast, NumCast } from "../../../fields/Types";
+import { Cast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime, numberRange, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils";
+import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { DocumentManager } from "../../util/DocumentManager";
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 { StyleProp } from "../StyleProvider";
import "./AudioBox.scss";
-import { DocumentView, DocumentViewProps } from "./DocumentView";
+import { DocumentView } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
-import { LinkAnchorBox } from "./LinkAnchorBox";
import { LinkDocPreview } from "./LinkDocPreview";
declare class MediaRecorder {
@@ -45,15 +44,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
public static Enabled = false;
public static NUMBER_OF_BUCKETS = 100;
-
+ 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;
- // _linkPlayDisposer: IReactionDisposer | undefined;
- // _reactionDisposer: IReactionDisposer | undefined;
- // _scrubbingDisposer: IReactionDisposer | undefined;
- private _disposers: { [name: string]: IReactionDisposer } = {};
+ _disposers: { [name: string]: IReactionDisposer } = {};
_ele: HTMLAudioElement | null = null;
_recorder: any;
_recordStart = 0;
@@ -64,146 +63,100 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
_start: number = 0;
_hold: boolean = false;
_left: boolean = false;
- _first: boolean = false;
_dragging = false;
_play: any = null;
-
- _count: Array<any> = [];
_audioRef = React.createRef<HTMLDivElement>();
_timeline: Opt<HTMLDivElement>;
- _duration = 0;
_markerStart: number = 0;
- private _currMarker: any;
+ _currMarker: any;
- @observable _visible: boolean = false;
+ @observable static SelectingRegion: AudioBox | undefined = undefined;
+ @observable static _scrubTime = 0;
@observable _markerEnd: number = 0;
@observable _position: number = 0;
@observable _waveHeight: Opt<number> = this.layoutDoc._height;
- @observable private _paused: boolean = false;
- @observable private static _scrubTime = 0;
+ @observable _paused: boolean = false;
@computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
set audioState(value) { this.dataDoc.audioState = value; }
public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; });
@computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
@computed get audioDuration() { return NumCast(this.dataDoc.duration); }
- async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
+ @computed get markerDocs() { 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
constructor(props: Readonly<FieldViewProps>) {
super(props);
AudioBox.Instance = this;
// onClick play scripts
- AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!;
- AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!;
+ AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!;
+ AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!;
+ AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!;
+ AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!;
}
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- let linkTime = NumCast(l.anchor2_timecode);
+ const linkTime = NumCast(la2.audioStart, NumCast(la1.audioStart));
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
- linkTime = NumCast(l.anchor1_timecode);
}
return { la1, la2, linkTime };
}
componentWillUnmount() {
- this._disposers.reaction?.();
- this._disposers.linkPlay?.();
- this._disposers.scrubbing?.();
- this._disposers.audioStart?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ }
+
+ getAnchor = () => {
+ return this.createMarker(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined));
}
@action
componentDidMount() {
- if (!this.dataDoc.markerAmount) {
- this.dataDoc.markerAmount = 0;
- }
+ this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
this.audioState = this.path ? "paused" : undefined;
- this._disposers.linkPlay = reaction(() => this.layoutDoc.scrollToLinkID,
- scrollLinkId => {
- if (scrollLinkId) {
- DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
- const { linkTime } = this.getLinkData(l);
- setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250);
- });
- Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
- }
- }, { fireImmediately: true });
-
- // for play when link is selected
- this._disposers.reaction = reaction(() => SelectionManager.Views(),
- selected => {
- const sel = selected.length ? selected[0].props.Document : undefined;
- let link;
- sel && DocListCast(this.dataDoc.links).forEach(l => {
- if (l.anchor1 === sel || l.anchor2 === sel && !sel.audioStart) {
- link = this.playLink(sel);
- }
- });
- // for links created during recording
- if (!link) {
- this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime());
- this.layoutDoc.playOnSelect && this.recordingStart && !sel && this.pause();
- }
- });
+
this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime));
this._disposers.audioStart = reaction(
- () => this.Document._audioStart,
- (audioStart) => {
- if (audioStart !== undefined) {
- if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
- const delay = this._audioRef.current ? 0 : 250; // wait for mainCont and try again to play
- const startTime: number = NumCast(this.Document._audioStart);
- setTimeout(() => this._audioRef.current && this.playFrom(startTime), delay);
- setTimeout(() => { this.Document._currentTimecode = startTime; this.Document._audioStart = undefined; }, 10 + delay);
- }
- }
- },
+ () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? Cast(this.Document._audioStart, "number", null) : undefined,
+ audioStart => audioStart !== undefined && setTimeout(() => {
+ this._audioRef.current && this.playFrom(audioStart);
+ setTimeout(() => {
+ this.Document._currentTimecode = audioStart;
+ this.Document._audioStart = undefined;
+ }, 10);
+ }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play
{ fireImmediately: true }
);
this._disposers.audioStop = reaction(
- () => this.Document._audioStop,
- (audioStop) => {
- if (audioStop !== undefined) {
- if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
- const delay = this._audioRef.current ? 0 : 250; // wait for mainCont and try again to play
- setTimeout(() => this._audioRef.current && this.pause(), delay);
- setTimeout(() => { this.Document._audioStop = undefined; }, 10 + delay);
- }
- }
- },
+ () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._audioStop, "number", null) : undefined,
+ audioStop => audioStop !== undefined && setTimeout(() => {
+ 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 }
);
}
playLink = (doc: Doc) => {
- let link = false;
- !Doc.AreProtosEqual(doc, this.props.Document) && DocListCast(this.props.Document.links).forEach(l => {
- if (l.anchor1 === doc || l.anchor2 === doc) {
- const { la1, la2, linkTime } = this.getLinkData(l);
- let startTime = linkTime;
- if (la2.audioStart) startTime = NumCast(la2.audioStart);
- if (la1.audioStart) startTime = NumCast(la1.audioStart);
-
- let endTime;
- if (la1.audioEnd) endTime = NumCast(la1.audioEnd);
- if (la2.audioEnd) endTime = NumCast(la2.audioEnd);
-
- if (startTime) {
- link = true;
- this.layoutDoc.playOnSelect && this.recordingStart && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime));
- }
+ this.links.filter(l => l.anchor1 === doc || l.anchor2 === doc).forEach(l => {
+ const { la1, la2 } = this.getLinkData(l);
+ const startTime = NumCast(la1.audioStart, NumCast(la2.audioStart, null));
+ const endTime = NumCast(la1.audioEnd, NumCast(la2.audioEnd, null));
+ if (startTime !== undefined) {
+ this.layoutDoc.playOnSelect && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime));
}
});
-
- this.layoutDoc.playOnSelect && this.recordingStart && Doc.AreProtosEqual(doc, this.props.Document) && this.pause();
- return link;
+ doc.annotationOn === this.rootDoc && this.playFrom(NumCast(doc.audioStart), Cast(doc.audioEnd, "number", null));
}
// for updating the timecode
@@ -211,7 +164,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.duration = htmlEle.duration);
- DocListCast(this.dataDoc.links).map(l => {
+ this.links.map(l => {
const { la1, linkTime } = this.getLinkData(l);
if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) {
Doc.linkFollowHighlight(la1);
@@ -227,16 +180,31 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this.audioState = "paused";
});
- // play audio for documents created during recording
+ // play audio for documents created during recording
playFromTime = (absoluteTime: number) => {
this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000);
}
// play back the audio from time
@action
+ playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
+ DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false);
+ this.playFrom(seekTimeInSeconds, endTime);
+ }
+
+ // play back the audio from time
+ @action
+ clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
+ if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime);
+ else {
+ DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false);
+ this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds);
+ }
+ }
+ // play back the audio from time
+ @action
playFrom = (seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
clearTimeout(this._play);
- this._duration = endTime - seekTimeInSeconds;
if (Number.isNaN(this._ele?.duration)) {
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
} else if (this._ele && AudioBox.Enabled) {
@@ -251,7 +219,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._ele.play();
runInAction(() => this.audioState = "playing");
if (endTime !== this.audioDuration) {
- this._play = setTimeout(() => this.pause(), (this._duration) * 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();
@@ -276,7 +244,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
- DocUtils.ActiveRecordings.push(this.props.Document);
+ DocUtils.ActiveRecordings.push(this);
this._recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
@@ -295,19 +263,18 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
const funcs: ContextMenuProps[] = [];
funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" });
funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " range markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.layoutDoc.hideLabels ? "Don't hide" : "Hide") + " label markers", event: () => this.layoutDoc.hideLabels = !this.layoutDoc.hideLabels, icon: "expand-arrows-alt" });
funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" });
ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
- // stops the recording
+ // stops the recording
stopRecording = action(() => {
this._recorder.stop();
this._recorder = undefined;
this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
this.audioState = "paused";
this._stream?.getAudioTracks()[0].stop();
- const ind = DocUtils.ActiveRecordings.indexOf(this.props.Document);
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
});
@@ -371,7 +338,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._paused = true;
this._recorder.pause();
e.stopPropagation();
-
}
// continue the recording
@@ -381,52 +347,57 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._paused = false;
this._recorder.resume();
e.stopPropagation();
-
- }
-
- // return the total time paused to update the correct time
- @computed get pauseTime() {
- return this._pauseEnd - this._pauseStart;
}
// starting the drag event for marker resizing
@action
onPointerDownTimeline = (e: React.PointerEvent): void => {
- const rect = (e.target as any).getBoundingClientRect();
- const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration;
- this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x);
- setupMoveUpEvents(this, e,
- action((e: PointerEvent) => {
- this._visible = true;
- this._markerEnd = toTimeline(e.clientX - rect.x);
- if (this._markerEnd < this._markerStart) {
- const tmp = this._markerStart;
- this._markerStart = this._markerEnd;
- this._markerEnd = tmp;
+ 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.audioDuration;
+ wasPaused && this.pause();
+
+ const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration;
+ this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x);
+ AudioBox.SelectingRegion = this;
+ setupMoveUpEvents(this, e,
+ action(e => {
+ this._markerEnd = toTimeline(e.clientX - rect.x);
+ if (this._markerEnd < this._markerStart) {
+ const tmp = this._markerStart;
+ this._markerStart = this._markerEnd;
+ this._markerEnd = tmp;
+ }
+ return false;
+ }),
+ action((e, movement) => {
+ AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x));
+ AudioBox.SelectingRegion = undefined;
+ }),
+ e => {
+ this.props.select(false);
+ e.shiftKey && this.createMarker(this._ele!.currentTime);
}
- return false;
- }),
- action((e: PointerEvent, movement: number[]) => {
- (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x));
- this._visible = false;
- }),
- (e: PointerEvent) => e.shiftKey && this.createMarker(this._ele!.currentTime)
- );
+ , this.props.isSelected(true) || this._isChildActive);
+ }
}
@action
- createMarker(audioStart: number, audioEnd?: number) {
+ createMarker(audioStart?: number, audioEnd?: number) {
+ if (audioStart === undefined) return this.rootDoc;
const marker = Docs.Create.LabelDocument({
- title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: audioEnd === undefined,
+ title: ComputedField.MakeFunction(`"#" + formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any,
useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false,
+ isLabel: audioEnd === undefined,
_autoHeight: true, annotationOn: this.props.Document
});
- marker.data = ""; // clears out the label's text so that only its border will display
if (this.dataDoc[this.annotationKey]) {
this.dataDoc[this.annotationKey].push(marker);
} else {
this.dataDoc[this.annotationKey] = new List<Doc>([marker]);
}
+ return marker;
}
// starting the drag event for marker resizing
@@ -436,12 +407,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._timeline?.setPointerCapture(e.pointerId);
const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.audioDuration;
setupMoveUpEvents(this, e,
- (e: PointerEvent) => {
+ (e) => {
const rect = (e.target as any).getBoundingClientRect();
this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width));
return false;
},
- (e: PointerEvent) => {
+ (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);
@@ -452,7 +423,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// updates the marker with the new time
@action
changeMarker = (m: any, time: any) => {
- DocListCast(this.dataDoc[this.annotationKey]).filter(marker => this.isSame(marker, m)).forEach(marker =>
+ this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker =>
this._left ? marker.audioStart = time : marker.audioEnd = time);
}
@@ -461,47 +432,30 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd;
}
- // instantiates a new array of size 500 for marker layout
- markers = () => {
- const increment = this.audioDuration / 500;
- this._count = [];
- for (let i = 0; i < 500; i++) {
- this._count.push([increment * i, 0]);
- }
- }
-
// makes sure no markers overlaps each other by setting the correct position and width
- isOverlap = (m: any) => {
- if (this._first) {
- this._first = false;
- this.markers();
- }
+ getLevel = (m: any, placed: { audioStart: number, audioEnd: number, level: number }[]) => {
+ const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth;
+ const x1 = m.audioStart;
+ const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.audioDuration : m.audioEnd;
let max = 0;
-
- for (let i = 0; i < 500; i++) {
- if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) {
- this._count[i][1]++;
-
- if (this._count[i][1] > max) {
- max = this._count[i][1];
- }
+ const overlappedLevels = new Set(placed.map(p => {
+ const y1 = p.audioStart;
+ const y2 = p.audioEnd;
+ 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);
- for (let i = 0; i < 500; i++) {
- if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) {
- this._count[i][1] = max;
- }
- }
-
- if (this.dataDoc.markerAmount < max) {
- this.dataDoc.markerAmount = max;
- }
- return max - 1;
+ placed.push({ audioStart: x1, audioEnd: x2, level });
+ return level;
}
@computed get selectionContainer() {
- return <div className="audiobox-container" style={{
+ return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`,
width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%"
}} />;
@@ -515,7 +469,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
color={"darkblue"}
height={this._waveHeight}
barWidth={0.1}
- // pos={this.layoutDoc._currentTimecode} need to correctly resize parent to make this work (not very necessary for function)
pos={this.audioDuration}
duration={this.audioDuration}
peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined}
@@ -537,42 +490,55 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
);
}
- rangeScript = () => AudioBox.RangeScript;
- labelScript = () => AudioBox.LabelScript;
- static audioStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => {
- if (property === StyleProp.BackgroundColor) return "transparent";
- if (property === StyleProp.PointerEvents) return "none";
- }
- markerStyle = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string): any => {
- return property.startsWith("backgroundColor") ? "dimGrey" : this.props.styleProvider?.(doc, props, property);
- }
- render() {
- const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : "";
- this._first = true; // for indicating the first marker that is rendered
- const markerDoc = (mark: Doc, script: undefined | (() => ScriptField), x?: number, y?: number, width?: number, height?: number) => {
- return <DocumentView {...this.props}
+ rangeClickScript = () => AudioBox.RangeScript;
+ labelClickScript = () => AudioBox.LabelScript;
+ rangePlayScript = () => AudioBox.RangePlayScript;
+ labelPlayScript = () => AudioBox.LabelPlayScript;
+ renderInner = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+ const marker = observable({ view: undefined as any });
+ return {
+ marker, view: <DocumentView key="view" {...this.props} ref={action((r: DocumentView | null) => marker.view = r)}
Document={mark}
- PanelWidth={width ? () => width : this.props.PanelWidth}
- PanelHeight={height ? () => height : this.props.PanelHeight}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
focus={() => this.playLink(mark)}
- styleProvider={this.markerStyle}
- pointerEvents={"all"}
rootSelected={returnFalse}
LayoutTemplate={undefined}
ContainingCollectionDoc={this.props.Document}
removeDocument={this.removeDocument}
- ScreenToLocalTransform={x && y ? () => this.props.ScreenToLocalTransform().translate(-x, -y) : this.props.ScreenToLocalTransform}
- parentActive={returnTrue}
- onClick={this.layoutDoc.playOnClick ? script : undefined}
+ 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.playOnClick ? undefined : doublescript}
ignoreAutoHeight={false}
bringToFront={emptyFunction}
- scriptContext={this} />;
+ scriptContext={this} />
};
- const heightPercent = 80;
- const playheadWidth = 30;
- const timelineContentWidth = this.props.PanelWidth() - playheadWidth;
- const timelineContentHeight = (this.props.PanelHeight() * heightPercent / 100) * heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
- return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} style={{ pointerEvents: !interactive ? "none" : undefined }}>
+ });
+ renderMarker = 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.marker.view || !SelectionManager.IsSelected(inner.marker.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 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: { audioStart: number, audioEnd: number, level: number }[] = [];
+ const drawMarkers = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m }));
+ const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 1;
+ return <div className="audiobox-container"
+ onContextMenu={this.specificContextMenu}
+ onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined}
+ style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined }}>
{!this.path ?
<div className="audiobox-buttons">
<div className="audiobox-dictation" onClick={this.onFile}>
@@ -581,10 +547,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
{this.audioState === "recording" || this.audioState === "paused" ?
<div className="recording" onClick={e => e.stopPropagation()}>
<div className="buttons" onClick={this.recordClick}>
- <FontAwesomeIcon style={{ width: "100%" }} icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
</div>
<div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}>
- <FontAwesomeIcon style={{ width: "100%" }} icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ <FontAwesomeIcon icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
</div>
<div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div>
</div>
@@ -593,81 +559,33 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
RECORD
</button>}
</div> :
- <div className="audiobox-controls" >
+ <div className="audiobox-controls" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }} >
<div className="audiobox-dictation" />
- <div className="audiobox-player" style={{ height: `${heightPercent}%` }} >
- <div className="audiobox-playhead" style={{ width: 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: `${heightPercent}%` }} ref={this.timelineRef}
+ <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}%` }} ref={this.timelineRef}
onClick={e => { e.stopPropagation(); e.preventDefault(); }}
- onPointerDown={e => {
- if (e.button === 0 && !e.ctrlKey) {
- const rect = (e.target as any).getBoundingClientRect();
-
- if (e.target !== this._audioRef.current) {
- const wasPaused = this.audioState === "paused";
- this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration;
- wasPaused && this.pause();
- }
-
- this.onPointerDownTimeline(e);
- }
- }}>
+ onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}>
<div className="waveform">
{this.waveform}
</div>
- {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => {
- const isOverlap = this.isOverlap(m);
- return !m.isLabel ?
- this.layoutDoc.hideMarkers ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container1`} key={i}
- title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`}
- style={{
- left: `${NumCast(m.audioStart) / this.audioDuration * 100}%`,
- top: `${isOverlap * 1 / (this.dataDoc.markerAmount + 1) * 100}%`,
- width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / this.audioDuration * 100}%`,
- height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%`
- }}
- onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)); e.stopPropagation(); }} >
- <div className="left-resizer" onPointerDown={e => this.onPointerDown(e, m, true)}></div>
- {markerDoc(m, this.rangeScript,
- playheadWidth + NumCast(m.audioStart) / this.audioDuration * timelineContentWidth,
- .1 * this.props.PanelHeight() + isOverlap / (this.dataDoc.markerAmount + 1) * timelineContentHeight,
- timelineContentWidth * (NumCast(m.audioEnd) - NumCast(m.audioStart)) / this.audioDuration,
- timelineContentHeight / (this.dataDoc.markerAmount + 1))}
- <div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div>
- </div>
- :
- this.layoutDoc.hideLabels ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={i}
- style={{ left: `${NumCast(m.audioStart) / this.audioDuration * 100}%` }}>
- {markerDoc(m, this.labelScript)}
- </div>;
- })}
- {DocListCast(this.dataDoc.links).map((l, i) => {
- const { la1, la2, linkTime } = this.getLinkData(l);
- let startTime = linkTime;
- if (la2.audioStart && !la2.audioEnd) {
- startTime = NumCast(la2.audioStart);
- }
-
- return !linkTime ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={l[Id]} style={{ left: `${startTime / this.audioDuration * 100}%` }} onClick={e => e.stopPropagation()}>
- <DocumentView {...this.props}
- Document={l}
- rootSelected={returnFalse}
- ContainingCollectionDoc={this.props.Document}
- parentActive={returnTrue}
- bringToFront={emptyFunction}
- styleProvider={AudioBox.audioStyleProvider}
- LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
- />
- <div key={i} className={`audiobox-marker`} onPointerEnter={() => Doc.linkFollowHighlight(la1)}
- onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(startTime); e.stopPropagation(); e.preventDefault(); } }} />
+ {drawMarkers.map((d, i) => {
+ const m = d.marker;
+ const left = NumCast(m.audioStart) / this.audioDuration * timelineContentWidth;
+ const top = d.level / maxLevel * timelineContentHeight;
+ const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.audioDuration : NumCast(m.audioEnd) - NumCast(m.audioStart);
+ return this.layoutDoc.hideMarkers ? (null) :
+ <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={i}
+ style={{ left, top, width: `${timespan / this.audioDuration * 100}%`, height: `${1 / maxLevel * 100}%` }}
+ onClick={e => { this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} >
+ {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript,
+ left + AudioBox.playheadWidth,
+ (1 - AudioBox.heightPercent / 100) / 2 * this.props.PanelHeight() + top,
+ timelineContentWidth * timespan / this.audioDuration,
+ timelineContentHeight / maxLevel)}
</div>;
})}
- {this._visible ? this.selectionContainer : null}
-
+ {this.selectionContainer}
<div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} />
{this.audio}
</div>