diff options
Diffstat (limited to 'src/client/views/nodes/AudioBox.tsx')
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 458 |
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> |