diff options
Diffstat (limited to 'src/client/views/nodes/AudioBox.tsx')
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 365 |
1 files changed, 174 insertions, 191 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index bc89cb6f9..289388320 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -25,6 +25,7 @@ import { List } from "../../../fields/List"; import { Scripting } from "../../util/Scripting"; import Waveform from "react-audio-waveform"; import axios from "axios"; +import { SnappingManager } from "../../util/SnappingManager"; const _global = (window /* browser */ || global /* node */) as any; declare class MediaRecorder { @@ -85,14 +86,21 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD constructor(props: Readonly<FieldViewProps>) { super(props); - // onClick play script - if (!AudioBox.RangeScript) { - AudioBox.RangeScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!; - } + // 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" })!; + } - if (!AudioBox.LabelScript) { - AudioBox.LabelScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!; + getLinkData(l: Doc) { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + let linkTime = NumCast(l.anchor2_timecode); + 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() { @@ -110,7 +118,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD scrollLinkId => { if (scrollLinkId) { DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => { - const linkTime = Doc.AreProtosEqual(l.anchor1 as Doc, this.dataDoc) ? NumCast(l.anchor1_timecode) : NumCast(l.anchor2_timecode); + const { linkTime } = this.getLinkData(l); setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250); }); Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); @@ -121,34 +129,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), selected => { const sel = selected.length ? selected[0].props.Document : undefined; - let link; - if (sel) { - // for determining if the link is created after recording (since it will use linkTime rather than creation date) - DocListCast(this.dataDoc.links).map((l, i) => { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - if (la1 === sel || la2 === sel) { // if the selected document is linked to this audio - let linkTime = NumCast(l.anchor2_timecode); - let endTime; - if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1_timecode); - } - if (la2.audioStart) linkTime = NumCast(la2.audioStart); - if (la1.audioStart) linkTime = NumCast(la1.audioStart); - - if (la1.audioEnd) endTime = NumCast(la1.audioEnd); - if (la2.audioEnd) endTime = NumCast(la2.audioEnd); - - if (linkTime) { - link = true; - this.layoutDoc.playOnSelect && this.recordingStart && sel && !Doc.AreProtosEqual(sel, this.props.Document) && (endTime ? this.playFrom(linkTime, endTime) : this.playFrom(linkTime)); - } - } - }); - } - + const link = sel && DocListCast(this.dataDoc.links).forEach(l => (l.anchor1 === sel || l.anchor2 === sel) && this.playLink(sel), false); // 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()); @@ -158,18 +139,35 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); } + 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.recordingStart && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); + } + } + }); + return link; + } + // for updating the timecode timecodeChanged = () => { 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 => { - let la1 = l.anchor1 as Doc; - let linkTime = NumCast(l.anchor2_timecode); - if (Doc.AreProtosEqual(la1, this.dataDoc)) { - linkTime = NumCast(l.anchor1_timecode); - la1 = l.anchor2 as Doc; - } + const { la1, linkTime } = this.getLinkData(l); if (linkTime > NumCast(this.layoutDoc.currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); } @@ -195,7 +193,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD let play; clearTimeout(play); this._duration = endTime - seekTimeInSeconds; - if (this._ele && AudioBox.Enabled) { + if (Number.isNaN(this._ele?.duration)) { + setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); + } else if (this._ele && AudioBox.Enabled) { if (seekTimeInSeconds < 0) { if (seekTimeInSeconds > -1) { setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000); @@ -349,47 +349,33 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD 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; - } - return false; - }), - action((e: PointerEvent, movement: number[]) => { - if (Math.abs(movement[0]) > 15) { - this.createNewMarker(this._markerStart, 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; } + return false; + }), + action((e: PointerEvent, movement: number[]) => { + (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x)); this._visible = false; }), - emptyFunction); - } - // returns the selection container - @computed get container() { - return <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%" - }}></div>; + (e: PointerEvent) => e.shiftKey && this.createMarker(this._ele!.currentTime) + ); } - // creates a new marker @action - createNewMarker(audioStart: number, audioEnd: number) { - const newMarker = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: false, + createMarker(audioStart: number, audioEnd?: number) { + const marker = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: audioEnd === undefined, useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }); - this.addMark(newMarker); - } - - // adds an annotation marker or label - @action - addMark(marker: Doc) { - marker.data = ""; + 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 { @@ -403,10 +389,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._left = left; const rect = (e.target as any).getBoundingClientRect(); const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration; - setupMoveUpEvents(this, e, () => { - this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x)); - return false; - }, + setupMoveUpEvents(this, e, + () => { + this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x)); + return false; + }, () => this._ele!.currentTime = this.layoutDoc.currentTimecode = toTimeline(e.clientX - rect.x), emptyFunction); } @@ -462,6 +449,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD return max - 1; } + @computed get selectionContainer() { + return <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%" + }} />; + } + // returns the audio waveform @computed get waveform() { return <Waveform @@ -515,12 +509,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD labelScript = () => AudioBox.LabelScript; render() { - const interactive = this.active() ? "-interactive" : ""; + const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : ""; this._first = true; // for indicating the first marker that is rendered this.path && this._buckets.length !== 100 ? this.peaks : null; // render waveform if audio is done recording const markerDoc = (mark: Doc, script: undefined | (() => ScriptField)) => { return <DocumentView {...this.props} Document={mark} + focus={() => this.playLink(mark)} pointerEvents={true} NativeHeight={returnZero} NativeWidth={returnZero} @@ -534,125 +529,113 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD bringToFront={emptyFunction} scriptContext={this} />; }; - return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined} - style={{ pointerEvents: !interactive ? "none" : undefined }}> - {!this.path ? - <div className="audiobox-buttons"> - <div className="audiobox-dictation" onClick={this.onFile}> - <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> - </div> - {this.audioState === "recording" ? - <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"} /> - </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"} /> - </div> - <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}</div> + return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}> + <div className="audiobox-inner" style={{ pointerEvents: !interactive ? "none" : undefined }}> + {!this.path ? + <div className="audiobox-buttons"> + <div className="audiobox-dictation" onClick={this.onFile}> + <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> - : - <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}> - RECORD + {this.audioState === "recording" ? + <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"} /> + </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"} /> + </div> + <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}</div> + </div> + : + <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}> + RECORD </button>} - </div> : - <div className="audiobox-controls" > - <div className="audiobox-dictation"></div> - <div className="audiobox-player" > - <div className="audiobox-playhead" 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" onClick={e => { e.stopPropagation(); e.preventDefault(); }} - onPointerDown={e => { - e.stopPropagation(); - e.preventDefault(); - 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(); + </div> : + <div className="audiobox-controls" > + <div className="audiobox-dictation"></div> + <div className="audiobox-player" > + <div className="audiobox-playhead" 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" 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); + } + }}> + <div className="waveform"> + {this.waveform} + </div> + {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => + (!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: `${this.isOverlap(m) * 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)} + <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); } - this.onPointerDownTimeline(e); - } - if (e.button === 0 && e.shiftKey) { - this.addMark(Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, isLabel: true, audioStart: this._ele!.currentTime, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document })); - } - }}> - <div className="waveform"> - {this.waveform} + 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} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + ContainingCollectionDoc={this.props.Document} + parentActive={returnTrue} + bringToFront={emptyFunction} + backgroundColor={returnTransparent} + ContentScaling={returnOne} + forcedBackgroundColor={returnTransparent} + pointerEvents={false} + 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(); } }} /> + </div>; + })} + {this._visible ? this.selectionContainer : null} + + <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> + <div className="current-time"> + {formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))} + </div> + <div className="total-time"> + {formatTime(Math.round(this.audioDuration))} </div> - {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => - (!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: `${this.isOverlap(m) * 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)} - <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) => { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - let linkTime = NumCast(l.anchor2_timecode); - if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1_timecode); - } - - if (la2.audioStart && !la2.audioEnd) { - linkTime = NumCast(la2.audioStart); - } - - return !linkTime ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={l[Id]} style={{ left: `${linkTime / this.audioDuration * 100}%` }} onClick={e => e.stopPropagation()}> - <DocumentView {...this.props} - Document={l} - NativeHeight={returnZero} - NativeWidth={returnZero} - rootSelected={returnFalse} - ContainingCollectionDoc={this.props.Document} - parentActive={returnTrue} - bringToFront={emptyFunction} - backgroundColor={returnTransparent} - ContentScaling={returnOne} - forcedBackgroundColor={returnTransparent} - pointerEvents={false} - 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(linkTime); e.stopPropagation(); e.preventDefault(); } }} /> - </div>; - })} - {this._visible ? this.container : null} - - <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> - <div className="current-time"> - {formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))} - </div> - <div className="total-time"> - {formatTime(Math.round(this.audioDuration))} </div> </div> - </div> - } + }</div> </div>; } } |