diff options
Diffstat (limited to 'src/client/views/nodes/AudioBox.tsx')
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 599 |
1 files changed, 537 insertions, 62 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 2396e6973..eba1046b2 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -2,31 +2,30 @@ import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; import { observer } from "mobx-react"; import "./AudioBox.scss"; -import { Cast, DateCast, NumCast } from "../../../fields/Types"; +import { Cast, DateCast, NumCast, FieldValue, ScriptCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { ViewBoxBaseComponent } from "../DocComponent"; +import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { makeInterface, createSchema } from "../../../fields/Schema"; import { documentSchema } from "../../../fields/documentSchemas"; -import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero } from "../../../Utils"; -import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx"; +import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero, formatTime } from "../../../Utils"; +import { runInAction, observable, reaction, IReactionDisposer, computed, action, trace, toJS } from "mobx"; import { DateField } from "../../../fields/DateField"; import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCast } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { ContextMenuProps } from "../ContextMenuItem"; import { ContextMenu } from "../ContextMenu"; import { Id } from "../../../fields/FieldSymbols"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DocumentView } from "./DocumentView"; import { Docs, DocUtils } from "../../documents/Documents"; -import { ComputedField } from "../../../fields/ScriptField"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Networking } from "../../Network"; import { LinkAnchorBox } from "./LinkAnchorBox"; - -// testing testing - -interface Window { - MediaRecorder: MediaRecorder; -} +import { List } from "../../../fields/List"; +import { Scripting } from "../../util/Scripting"; +import Waveform from "react-audio-waveform"; +import axios from "axios"; +const _global = (window /* browser */ || global /* node */) as any; declare class MediaRecorder { // whatever MediaRecorder has @@ -40,21 +39,42 @@ type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>; const AudioDocument = makeInterface(documentSchema, audioSchema); @observer -export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument>(AudioDocument) { +export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } public static Enabled = false; + static Instance: AudioBox; + static RangeScript: ScriptField; + static LabelScript: ScriptField; + _linkPlayDisposer: IReactionDisposer | undefined; _reactionDisposer: IReactionDisposer | undefined; _scrubbingDisposer: IReactionDisposer | undefined; _ele: HTMLAudioElement | null = null; _recorder: any; _recordStart = 0; + _pauseStart = 0; + _pauseEnd = 0; + _pausedTime = 0; _stream: MediaStream | undefined; - constructor(props: any) { - super(props); - AudioBox.Instance = this; - } + _start: number = 0; + _hold: boolean = false; + _left: boolean = false; + _markers: Array<any> = []; + _first: boolean = false; + _dragging = false; + + _count: Array<any> = []; + _timeline: Opt<HTMLDivElement>; + _duration = 0; + + private _isPointerDown = false; + private _currMarker: any; + + @observable _position: number = 0; + @observable _buckets: Array<number> = new Array<number>(); + @observable private _height: number = NumCast(this.layoutDoc._height); + @observable private _paused: boolean = false; @observable private static _scrubTime = 0; @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); } set audioState(value) { this.dataDoc.audioState = value; } @@ -62,12 +82,29 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); } + constructor(props: Readonly<FieldViewProps>) { + super(props); + + // onClick play script + if (!AudioBox.RangeScript) { + AudioBox.RangeScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!; + } + + if (!AudioBox.LabelScript) { + AudioBox.LabelScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!; + } + } + componentWillUnmount() { this._reactionDisposer?.(); this._linkPlayDisposer?.(); this._scrubbingDisposer?.(); } componentDidMount() { + if (!this.dataDoc.markerAmount) { + this.dataDoc.markerAmount = 0; + } + runInAction(() => this.audioState = this.path ? "paused" : undefined); this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { @@ -79,15 +116,59 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); } }, { fireImmediately: true }); + + // for play when link is selected this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), selected => { const sel = selected.length ? selected[0].props.Document : undefined; - 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(); + 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)); + } + } + }); + } + + // 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._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); } + // for updating the timecode timecodeChanged = () => { const htmlEle = this._ele; if (this.audioState !== "recording" && htmlEle) { @@ -107,15 +188,23 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument } } + // pause play back pause = action(() => { this._ele!.pause(); this.audioState = "paused"; }); + // play audio for documents created during recording playFromTime = (absoluteTime: number) => { this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000); } - playFrom = (seekTimeInSeconds: number) => { + + // play back the audio from time + @action + playFrom = (seekTimeInSeconds: number, endTime: number = this.dataDoc.duration) => { + let play; + clearTimeout(play); + this._duration = endTime - seekTimeInSeconds; if (this._ele && AudioBox.Enabled) { if (seekTimeInSeconds < 0) { if (seekTimeInSeconds > -1) { @@ -127,20 +216,29 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument this._ele.currentTime = seekTimeInSeconds; this._ele.play(); runInAction(() => this.audioState = "playing"); + if (endTime !== this.dataDoc.duration) { + play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration + } } else { this.pause(); } } } - + // update the recording time updateRecordTime = () => { if (this.audioState === "recording") { - setTimeout(this.updateRecordTime, 30); - this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart) / 1000; + if (this._paused) { + setTimeout(this.updateRecordTime, 30); + this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; + } else { + setTimeout(this.updateRecordTime, 30); + this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + } } } + // starts recording recordAudioAnnotation = async () => { this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); @@ -156,26 +254,31 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument runInAction(() => this.audioState = "recording"); setTimeout(this.updateRecordTime, 0); this._recorder.start(); - setTimeout(() => this._recorder && this.stopRecording(), 60 * 1000); // stop after an hour + setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour } + // context menu specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - + 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") + " markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.hideLabels ? "Don't hide" : "Hide") + " labels", 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 stopRecording = action(() => { this._recorder.stop(); this._recorder = undefined; - this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000; + 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); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); }); + // button for starting and stopping the recording recordClick = (e: React.MouseEvent) => { if (e.button === 0 && !e.ctrlKey) { this._recorder ? this.stopRecording() : this.recordAudioAnnotation(); @@ -183,14 +286,13 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument } } + // for play button onPlay = (e: any) => { this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); e.stopPropagation(); } - onStop = (e: any) => { - this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect; - e.stopPropagation(); - } + + // creates a text document for dictation onFile = (e: any) => { const newDoc = Docs.Create.TextDocument("", { title: "", _chromeStatus: "disabled", @@ -204,18 +306,21 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument e.stopPropagation(); } + // ref for updating time setRef = (e: HTMLAudioElement | null) => { e?.addEventListener("timeupdate", this.timecodeChanged); e?.addEventListener("ended", this.pause); this._ele = e; } + // returns the path of the audio file @computed get path() { const field = Cast(this.props.Document[this.props.fieldKey], AudioField); const path = (field instanceof AudioField) ? field.url.href : ""; return path === nullAudio ? "" : path; } + // returns the html audio element @computed get audio() { const interactive = this.active() ? "-interactive" : ""; return <audio ref={this.setRef} className={`audiobox-control${interactive}`}> @@ -224,33 +329,390 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument </audio>; } + // pause the time during recording phase + @action + recordPause = (e: React.MouseEvent) => { + this._pauseStart = new Date().getTime(); + this._paused = true; + this._recorder.pause(); + e.stopPropagation(); + + } + + // continue the recording + @action + recordPlay = (e: React.MouseEvent) => { + this._pauseEnd = new Date().getTime(); + 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); + } + + // creates a new label + @action + newMarker(marker: Doc) { + marker.data = ""; + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(marker); + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([marker]); + } + } + + // the starting time of the marker + start(startingPoint: number) { + this._hold = true; + this._start = startingPoint; + } + + // creates a new marker + @action + end(marker: number) { + this._hold = false; + const newMarker = Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: false, useLinkSmallAnchor: true, hideLinkButton: true, audioStart: this._start, audioEnd: marker, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }); + newMarker.data = ""; + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(newMarker); + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([newMarker]); + } + + this._start = 0; + } + + // starting the drag event for marker resizing + onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = true; + this._currMarker = m; + this._timeline?.setPointerCapture(e.pointerId); + this._left = left; + + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + // ending the drag event for marker resizing + @action + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = false; + this._dragging = false; + + const rect = (e.target as any).getBoundingClientRect(); + this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + + this._timeline?.releasePointerCapture(e.pointerId); + + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + // resizes the marker while dragging + onPointerMove = async (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!this._isPointerDown) { + return; + } + + const rect = await (e.target as any).getBoundingClientRect(); + + const newTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + + this.changeMarker(this._currMarker, newTime); + } + + // updates the marker with the new time + @action + changeMarker = (m: any, time: any) => { + DocListCast(this.dataDoc[this.annotationKey]).forEach((marker: Doc) => { + if (this.isSame(marker, m)) { + this._left ? marker.audioStart = time : marker.audioEnd = time; + } + }); + } + + // checks if the two markers are the same with start and end time + isSame = (m1: any, m2: any) => { + if (m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd) { + return true; + } + return false; + } + + // instantiates a new array of size 500 for marker layout + markers = () => { + const increment = NumCast(this.layoutDoc.duration) / 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(); + } + 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]; + } + } + } + + 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; + } + + // returns the audio waveform + @computed get waveform() { + return <Waveform + color={"darkblue"} + height={this._height} + barWidth={0.1} + // pos={this.layoutDoc.currentTimecode} + pos={this.dataDoc.duration} + duration={this.dataDoc.duration} + peaks={this._buckets.length === 100 ? this._buckets : undefined} + progressColor={"blue"} />; + } + + // decodes the audio file into peaks for generating the waveform + @action + buckets = async () => { + const audioCtx = new (window.AudioContext)(); + + axios({ url: this.path, responseType: "arraybuffer" }) + .then(response => { + const audioData = response.data; + + audioCtx.decodeAudioData(audioData, action(buffer => { + const decodedAudioData = buffer.getChannelData(0); + const NUMBER_OF_BUCKETS = 100; + const bucketDataSize = Math.floor(decodedAudioData.length / NUMBER_OF_BUCKETS); + + for (let i = 0; i < NUMBER_OF_BUCKETS; i++) { + const startingPoint = i * bucketDataSize; + const endingPoint = i * bucketDataSize + bucketDataSize; + let max = 0; + for (let j = startingPoint; j < endingPoint; j++) { + if (decodedAudioData[j] > max) { + max = decodedAudioData[j]; + } + } + const size = Math.abs(max); + this._buckets.push(size / 2); + } + + })); + }); + } + + // Returns the peaks of the audio waveform + @computed get peaks() { + return this.buckets(); + } + + // for updating the width and height of the waveform with timeline ref + timelineRef = (timeline: HTMLDivElement) => { + const observer = new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this.update(entry.contentRect.width, entry.contentRect.height); + this._position = entry.contentRect.width; + } + })); + timeline && observer.observe(timeline); + + this._timeline = timeline; + } + + // update the width and height of the audio waveform + @action + update = (width: number, height: number) => { + if (height) { + this._height = 0.8 * NumCast(this.layoutDoc._height); + const canvas2 = document.getElementsByTagName("canvas")[0]; + if (canvas2) { + const oldWidth = canvas2.width; + const oldHeight = canvas2.height; + canvas2.style.height = `${this._height}`; + canvas2.style.width = `${width}`; + + const ratio1 = oldWidth / window.innerWidth; + const ratio2 = oldHeight / window.innerHeight; + const context = canvas2.getContext('2d'); + if (context) { + context.scale(ratio1, ratio2); + } + } + + const canvas1 = document.getElementsByTagName("canvas")[1]; + if (canvas1) { + const oldWidth = canvas1.width; + const oldHeight = canvas1.height; + canvas1.style.height = `${this._height}`; + canvas1.style.width = `${width}`; + + const ratio1 = oldWidth / window.innerWidth; + const ratio2 = oldHeight / window.innerHeight; + const context = canvas1.getContext('2d'); + if (context) { + context.scale(ratio1, ratio2); + } + + const parent = canvas1.parentElement; + if (parent) { + parent.style.width = `${width}`; + parent.style.height = `${this._height}`; + } + } + } + } + + rangeScript = () => AudioBox.RangeScript; + + labelScript = () => AudioBox.LabelScript; + + // for indicating the first marker that is rendered + reset = () => this._first = true; + render() { const interactive = this.active() ? "-interactive" : ""; + this.reset(); + this.path && this._buckets.length !== 100 ? this.peaks : null; // render waveform if audio is done recording return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : 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: this.audioState === "recording" ? "red" : "black" }}> - {this.audioState === "recording" ? "STOP" : "RECORD"} - </button> + {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-player" onClick={this.onPlay}> - <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.layoutDoc.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-timeline" onClick={e => e.stopPropagation()} + <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" ref={this.timelineRef} 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(); - const wasPaused = this.audioState === "paused"; + + if (e.target as HTMLElement !== document.getElementById("current")) { + const wasPaused = this.audioState === "paused"; + this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + wasPaused && this.pause(); + } + } + if (e.button === 0 && e.altKey) { + this.newMarker(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 })); + } + + if (e.button === 0 && e.shiftKey) { + const rect = (e.target as any).getBoundingClientRect(); this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); - wasPaused && this.pause(); - e.stopPropagation(); + this._hold ? this.end(this._ele!.currentTime) : this.start(this._ele!.currentTime); } - }} > + }}> + <div className="waveform" id="waveform" style={{ height: `${100}%`, width: "100%", bottom: "0px" }}> + {this.waveform} + </div> + {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => { + let rect; + (!m.isLabel) ? + (this.layoutDoc.hideMarkers) ? (null) : + rect = + <div key={i} id={"audiobox-marker-container1"} className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container1"} + title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`} + style={{ + left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%`, + width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / NumCast(this.dataDoc.duration, 1) * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%`, + top: `${this.isOverlap(m) * 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> + <DocumentView {...this.props} + Document={m} + pointerEvents={true} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + LayoutTemplate={undefined} + ContainingCollectionDoc={this.props.Document} + removeDocument={this.removeDocument} + parentActive={returnTrue} + onClick={this.layoutDoc.playOnClick ? this.rangeScript : undefined} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + <div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div> + </div> + : + (this.layoutDoc.hideLabels) ? (null) : + rect = + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={i} style={{ left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%` }}> + <DocumentView {...this.props} + Document={m} + pointerEvents={true} + NativeHeight={returnZero} + NativeWidth={returnZero} + rootSelected={returnFalse} + LayoutTemplate={undefined} + ContainingCollectionDoc={this.props.Document} + removeDocument={this.removeDocument} + parentActive={returnTrue} + onClick={this.layoutDoc.playOnClick ? this.labelScript : undefined} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + </div>; + return rect; + })} {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); @@ -259,32 +721,45 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument la2 = l.anchor1 as Doc; linkTime = NumCast(l.anchor1_timecode); } + + if (la2.audioStart && !la2.audioEnd) { + linkTime = NumCast(la2.audioStart); + } + return !linkTime ? (null) : - <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> - <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> - <DocumentView {...this.props} - Document={l} - NativeHeight={returnZero} - NativeWidth={returnZero} - rootSelected={returnFalse} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)} - ContainingCollectionDoc={this.props.Document} - dontRegisterView={true} - parentActive={returnTrue} - bringToFront={emptyFunction} - backgroundColor={returnTransparent} /> - </div> - <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} - onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); wasPaused && this.pause(); e.stopPropagation(); } }} /> + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 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) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); e.stopPropagation(); e.preventDefault(); } }} /> </div>; })} - <div className="audiobox-current" style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> + <div className="audiobox-current" id="current" onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 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(NumCast(this.dataDoc.duration)))} + </div> </div> </div> } </div>; } -}
\ No newline at end of file +} +Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file |