diff options
-rw-r--r-- | src/client/documents/Documents.ts | 4 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.scss | 129 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 476 | ||||
-rw-r--r-- | src/client/views/nodes/AudioResizer.scss | 11 | ||||
-rw-r--r-- | src/client/views/nodes/AudioResizer.tsx | 48 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 2 | ||||
-rw-r--r-- | src/fields/documentSchemas.ts | 4 |
7 files changed, 627 insertions, 47 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index cd2792226..fb9f6fe46 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -191,6 +191,9 @@ export interface DocumentOptions { searchQuery?: string; // for queryBox filterQuery?: string; linearViewIsExpanded?: boolean; // is linear view expanded + isLabel?: boolean; // whether the document is a label or not (video / audio) + audioStart?: number; // the time frame where the audio should begin playing + audioEnd?: number; // the time frame where the audio should stop playing } class EmptyBox { @@ -560,6 +563,7 @@ export namespace Docs { viewDoc.author = Doc.CurrentUserEmail; viewDoc.type !== DocumentType.LINK && DocUtils.MakeLinkToActiveAudio(viewDoc); + console.log("audio link!"); return Doc.assign(viewDoc, delegateProps, true); } diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index e9420a072..7b0e50e60 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -46,6 +46,40 @@ width: 100%; height: 100%; position: relative; + + + } + + .recording { + margin-top: auto; + margin-bottom: auto; + width: 100%; + height: 100%; + position: relative; + padding-right: 5px; + display: flex; + background-color: red; + + .time { + position: relative; + height: 100%; + width: 100%; + font-size: 20; + text-align: center; + top: 5; + } + + .buttons { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 5px; + } + + .buttons:hover { + background-color: crimson; + } } .audiobox-controls { @@ -54,6 +88,7 @@ position: relative; display: flex; padding-left: 2px; + background: black; .audiobox-player { margin-top: auto; @@ -64,16 +99,28 @@ padding-right: 5px; display: flex; - .audiobox-playhead, - .audiobox-dictation { + .audiobox-playhead { position: relative; margin-top: auto; margin-bottom: auto; - width: 25px; + margin-right: 2px; + width: 30px; + height: 25px; padding: 2px; + border-radius: 50%; + background-color: dimgrey; + } + + .audiobox-playhead:hover { + background-color: white; } .audiobox-dictation { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 2px; align-items: center; display: inherit; background: dimgray; @@ -81,11 +128,12 @@ .audiobox-timeline { position: relative; - height: 100%; + height: 80%; width: 100%; background: white; border: gray solid 1px; border-radius: 3px; + z-index: 1000; .audiobox-current { width: 1px; @@ -104,7 +152,6 @@ background: gray; border-radius: 100%; opacity: 0.9; - background-color: transparent; box-shadow: black 2px 2px 1px; .linkAnchorBox-cont { @@ -142,11 +189,37 @@ .audiobox-marker-minicontainer { position: absolute; width: 10px; + height: 10px; + top: 2.5%; + background: gray; + border-radius: 50%; + box-shadow: black 2px 2px 1px; + overflow: auto; + cursor: pointer; + + .audiobox-marker { + position: relative; + height: 100%; + // height: calc(100% - 15px); + width: 100%; + //margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + } + + .audiobox-marker-container1, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; height: 90%; top: 2.5%; background: gray; border-radius: 5px; box-shadow: black 2px 2px 1px; + opacity: 0.3; .audiobox-marker { position: relative; @@ -157,6 +230,36 @@ .audio-marker:hover { border: orange 2px solid; } + + .resizer { + position: absolute; + right: 0; + cursor: ew-resize; + height: 100%; + width: 2px; + z-index: 100; + } + + .click { + position: relative; + height: 100%; + width: 100%; + z-index: 100; + } + + .left-resizer { + position: absolute; + left: 0; + cursor: ew-resize; + height: 100%; + width: 2px; + z-index: 100; + } + } + + .audiobox-marker-container1:hover, + .audiobox-marker-minicontainer:hover { + opacity: 1; } .audiobox-marker-minicontainer { @@ -170,6 +273,22 @@ } } } + + .current-time { + position: absolute; + font-size: 12; + top: calc(100% - 10px); + left: 30px; + color: white; + } + + .total-time { + position: absolute; + top: calc(100% - 10px); + font-size: 12; + right: 2px; + color: white; + } } } } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 5c921cea4..0cab0fc61 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -2,13 +2,13 @@ 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 { ViewBoxBaseComponent, 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 { 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"; @@ -18,9 +18,18 @@ 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"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +import { RichTextField } from "../../../fields/RichTextField"; +import { AudioResizer } from "./AudioResizer"; +import { List } from "../../../fields/List"; +import { LabelBox } from "./LabelBox"; +import { Transform } from "../../util/Transform"; +import { Scripting } from "../../util/Scripting"; +import { ColorBox } from "./ColorBox"; + // testing testing @@ -40,19 +49,40 @@ 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; + _start: number = 0; + _hold: boolean = false; + _left: boolean = false; + _amount: number = 1; + _markers: Array<any> = []; + + private _isPointerDown = false; + private _currMarker: any; + @observable private _dragging: boolean = false; + @observable private _duration = 0; + @observable private _rect: Array<any> = []; + + @observable private _paused: boolean = false; @observable private static _scrubTime = 0; + @observable private _repeat: 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 = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); }; @@ -60,12 +90,30 @@ 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); + 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 => { @@ -106,14 +154,23 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument } pause = action(() => { - this._ele!.pause(); - this.audioState = "paused"; + if (this._repeat) { + this.playFrom(0); + } else { + this._ele!.pause(); + this.audioState = "paused"; + } }); playFromTime = (absoluteTime: number) => { this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000); } - playFrom = (seekTimeInSeconds: number) => { + + @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) { @@ -122,9 +179,13 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument this.pause(); } } else if (seekTimeInSeconds <= this._ele.duration) { + console.log("playing"); this._ele.currentTime = seekTimeInSeconds; this._ele.play(); runInAction(() => this.audioState = "playing"); + if (endTime !== this.dataDoc.duration) { + play = setTimeout(() => this.pause(), (this._duration) * 1000); + } } else { this.pause(); } @@ -134,8 +195,13 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument 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; + } } } @@ -160,14 +226,16 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument 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.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") + " onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }) ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } 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); @@ -222,7 +290,226 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument </audio>; } + @action + onRepeat = (e: React.MouseEvent) => { + this._repeat = !this._repeat; + e.stopPropagation(); + } + + @action + recordPause = (e: React.MouseEvent) => { + this._pauseStart = new Date().getTime(); + this._paused = true; + this._recorder.pause(); + e.stopPropagation(); + + } + + @action + recordPlay = (e: React.MouseEvent) => { + this._pauseEnd = new Date().getTime(); + this._paused = false; + this._recorder.resume(); + e.stopPropagation(); + + } + + @computed get pauseTime() { + return (this._pauseEnd - this._pauseStart); + } + + @action + newMarker(marker: Doc) { + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(marker); + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([marker]); + } + } + + start(marker: number) { + console.log("start!"); + this._hold = true; + this._start = marker; + } + + @action + end(marker: number) { + console.log("end!"); + this._hold = false; + //this._markers.push(Docs.Create.LabelDocument({ isLabel: false, audioStart: this._start, audioEnd: marker, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document })) + let newMarker = Docs.Create.LabelDocument({ title: "", isLabel: false, audioStart: this._start, audioEnd: marker, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }); + + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(newMarker); // onClick: ScriptField.MakeScript(`playFrom(${NumCast(this._start)}, ${NumCast(marker)})`) + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([newMarker]); + } + + + this._start = 0; + this._amount++; + } + + onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = true; + console.log("click"); + this._currMarker = m; + const targetele = document.getElementById("timeline"); + targetele?.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); + } + + @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); + + const targetele = document.getElementById("timeline"); + targetele?.releasePointerCapture(e.pointerId); + + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + onPointerMove = async (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + console.log("drag"); + + if (!this._isPointerDown) { + return; + } + + const rect = await (e.target as any).getBoundingClientRect(); + + // if (e.target as HTMLElement === document.getElementById("timeline")) { + + let newTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + + this.changeMarker(this._currMarker, newTime); + // } + } + + @action + changeMarker = (m: any, time: any) => { + for (let i = 0; i < this.dataDoc[this.annotationKey].length; i++) { + if (this.isSame(this.dataDoc[this.annotationKey][i], m)) { + // this._left ? this._markers[i][0] = time : this._markers[i][1] = time; + this._left ? this.dataDoc[this.annotationKey][i].audioStart = time : this.dataDoc[this.annotationKey][i].audioEnd = time; + } + } + } + + isSame = (m1: any, m2: any) => { + if (m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd) { + return true; + } + return false; + } + + isOverlap = (m: any, i: number) => { + console.log("called"); + let counter = 0; + + if (i == 0) { + this._markers = []; + } + for (let i = 0; i < this._markers.length; i++) { + if ((m.audioEnd > this._markers[i].audioStart && m.audioStart < this._markers[i].audioEnd)) { + counter++; + console.log(counter); + } + } + + if (this.dataDoc.markerAmount < counter) { + this.dataDoc.markerAmount = counter; + } + + this._markers.push(m); + + return counter; + } + + // isOverlap = (m: any) => { + // if (this._markers.length < 1) { + // this._markers = new Array(Math.round(this.dataDoc.duration)).fill(0); + // } + // console.log(this._markers); + // let max = 0 + + // for (let i = Math.round(m.audioStart); i <= Math.round(m.audioEnd); i++) { + // this._markers[i] = this._markers[i] + 1; + // console.log(this._markers[i]); + + // if (this._markers[i] > max) { + // max = this._markers[i]; + // } + // } + + // console.log(max); + // if (this.dataDoc.markerAmount < max) { + // this.dataDoc.markerAmount = max; + // } + // return max + // } + + formatTime = (time: number) => { + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) - (hours * 60); + const seconds = time % 60; + + return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); + } + + @action + onHover = () => { + this._dragging = true; + } + + @action + onLeave = () => { + this._dragging = false; + } + // onMouseOver={this.onHover} onMouseLeave={this.onLeave} + + change = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + const rect = (e.target as any).getBoundingClientRect(); + + const wasPaused = this.audioState === "paused"; + this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + wasPaused && this.pause(); + console.log("double!"); + } + + rangeScript = () => AudioBox.RangeScript; + + labelScript = () => AudioBox.LabelScript; + + // see if time is encapsulated by comparing time on both sides (for moving onto a new row in the timeline for the markers) + check = (e: React.PointerEvent) => { + if (e.target as HTMLElement === document.getElementById("timeline")) { + return true; + } + } + render() { + //trace(); const interactive = this.active() ? "-interactive" : ""; return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}> {!this.path ? @@ -230,59 +517,164 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument <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> + {/* <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this.audioState === "recording" ? "lightgrey" : "black" }}> + {this.audioState === "recording" ? + <div className="recording" style={{}}> + 10:00 + <FontAwesomeIcon style={{ width: "100%" }} icon={"stop-circle"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + <FontAwesomeIcon style={{ width: "100%" }} icon={"pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> : "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">{this.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" onClick={this.layoutDoc.playOnSelect ? this.onPlay : undefined}> + <div className="background"> + </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" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> + {/* <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.layoutDoc.playOnSelect ? "darkgrey" : "" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-playhead" onClick={this.onRepeat}><FontAwesomeIcon style={{ width: "100%", background: this._repeat ? "darkgrey" : "" }} icon="redo-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> */} + <div className="audiobox-timeline" id="timeline" onClick={e => { e.stopPropagation(); e.preventDefault(); }} onDoubleClick={e => this.change} onPointerDown={e => { + e.stopPropagation(); + e.preventDefault(); if (e.button === 0 && !e.ctrlKey) { const rect = (e.target as any).getBoundingClientRect(); - const wasPaused = this.audioState === "paused"; - this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); - wasPaused && this.pause(); - e.stopPropagation(); + + 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(); + } } - }} > - {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 (e.button === 0 && e.altKey) { + + this.newMarker(Docs.Create.LabelDocument({ title: "", isLabel: true, audioStart: this._ele!.currentTime, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document })); } - 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}> + + 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); + this._hold ? this.end(this._ele!.currentTime) : this.start(this._ele!.currentTime); + } + }}> + {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => { + // let text = Docs.Create.TextDocument("hello", { title: "label", _showSidebar: false, _autoHeight: false }); + let rect; + (!m.isLabel) ? + (this.layoutDoc.hideMarkers) ? (null) : + rect = + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container1"} title={`${this.formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${this.formatTime(Math.round(NumCast(m.audioEnd)))}`} key={i} id={"audiobox-marker-container1"} 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 + 2) * 100}%`, top: `${this.isOverlap(m, i) * 1 / (this.dataDoc.markerAmount + 2) * 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={l} + Document={m} + pointerEvents={true} NativeHeight={returnZero} NativeWidth={returnZero} rootSelected={returnFalse} LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)} ContainingCollectionDoc={this.props.Document} dontRegisterView={true} + removeDocument={undefined} parentActive={returnTrue} + onClick={this.layoutDoc.playOnClick ? this.rangeScript : undefined} + ignoreAutoHeight={false} bringToFront={emptyFunction} - backgroundColor={returnTransparent} /> + scriptContext={this} /> + {/* <LabelBox {... this.props} Document={m} /> */} + {/* <div className="click" onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)) }}></div> */} + <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} + dontRegisterView={true} + removeDocument={undefined} + 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); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + linkTime = NumCast(l.anchor1_timecode); + } + + if (la1.audioStart) { + linkTime = NumCast(la1.audioStart); + } + + if (la2.audioStart) { + 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}%` }} onClick={e => e.stopPropagation()}> + {/* <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(); } }} /> + onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); this.pause(); 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}%` }} /> {this.audio} + + </div> + <div className="current-time"> + {this.formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))} + </div> + <div className="total-time"> + {this.formatTime(Math.round(NumCast(this.layoutDoc.duration)))} </div> </div> </div> } </div>; } -}
\ No newline at end of file +} + +// Scripting.addGlobal(function playFrom(audioDoc: Doc, start: number, end: number) { return audioDoc.playFrom(start, end); })
\ No newline at end of file diff --git a/src/client/views/nodes/AudioResizer.scss b/src/client/views/nodes/AudioResizer.scss new file mode 100644 index 000000000..892ad21e7 --- /dev/null +++ b/src/client/views/nodes/AudioResizer.scss @@ -0,0 +1,11 @@ +.resizer { + width: 0px; + height: 100%; + position: absolute; + right: 0px; + z-index: 999; + cursor: e-resize; + content: " "; + display: inline-block; + border-left: 20px solid transparent; +}
\ No newline at end of file diff --git a/src/client/views/nodes/AudioResizer.tsx b/src/client/views/nodes/AudioResizer.tsx new file mode 100644 index 000000000..f9ab8353f --- /dev/null +++ b/src/client/views/nodes/AudioResizer.tsx @@ -0,0 +1,48 @@ +import { observer } from "mobx-react" +import React = require("react"); +import "./AudioResizer.scss"; + +@observer +export class AudioResizer extends React.Component { + private _isPointerDown = false; + + onPointerDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = true; + console.log("click"); + + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + + onPointerUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + this._isPointerDown = false; + + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + onPointerMove = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + console.log("drag"); + + if (!this._isPointerDown) { + return; + } + + let resize = document.getElementById("resizer"); + if (resize) { + resize.style.right += e.movementX; + } + } + + render() { + return <div className="resizer" onPointerDown={this.onPointerDown}></div> + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b59875a7e..74634f837 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -100,6 +100,7 @@ export interface DocumentViewProps { layoutKey?: string; radialMenu?: String[]; display?: string; + scriptContext?: any; } @observer @@ -322,6 +323,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, + scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log); if (this.props.Document !== Doc.UserDoc()["dockedBtn-undo"] && this.props.Document !== Doc.UserDoc()["dockedBtn-redo"]) { diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index ddffb56c3..c77d80d76 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -19,6 +19,10 @@ export const documentSchema = createSchema({ currentTimecode: "number", // current play back time of a temporal document (video / audio) displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently + isLabel: "boolean", // whether the document is a label or not (video / audio) + audioStart: "number", // the time frame where the audio should begin playing + audioEnd: "number", // the time frame where the audio should stop playing + markers: listSpec(Doc), // list of markers for audio / video x: "number", // x coordinate when in a freeform view y: "number", // y coordinate when in a freeform view z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview |