aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/AudioBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/AudioBox.tsx')
-rw-r--r--src/client/views/nodes/AudioBox.tsx185
1 files changed, 106 insertions, 79 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index c79828470..bfc15cea8 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -6,7 +6,7 @@ import {
IReactionDisposer,
observable,
reaction,
- runInAction,
+ runInAction
} from "mobx";
import { observer } from "mobx-react";
import { DateField } from "../../../fields/DateField";
@@ -16,23 +16,25 @@ import { makeInterface } from "../../../fields/Schema";
import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, setupMoveUpEvents, returnFalse } from "../../../Utils";
import { DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { DragManager } from "../../util/DragManager";
import { SnappingManager } from "../../util/SnappingManager";
import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import {
ViewBoxAnnotatableComponent,
- ViewBoxAnnotatableProps,
+ ViewBoxAnnotatableProps
} from "../DocComponent";
+import { Colors } from "../global/globalEnums";
import "./AudioBox.scss";
import { FieldView, FieldViewProps } from "./FieldView";
import { LinkDocPreview } from "./LinkDocPreview";
-import { faLessThan } from "@fortawesome/free-solid-svg-icons";
-import { Colors } from "../global/globalEnums";
+import e = require("connect-flash");
+import { undoBatch } from "../../util/UndoManager";
declare class MediaRecorder {
constructor(e: any); // whatever MediaRecorder has
@@ -46,13 +48,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
ViewBoxAnnotatableProps & FieldViewProps,
AudioDocument
>(AudioDocument) {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(AudioBox, fieldKey);
- }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
public static Enabled = false;
static playheadWidth = 40; // width of playhead
static heightPercent = 75; // height of timeline in percent of height of audioBox.
static Instance: AudioBox;
+ static ScopeAll = 2;
+ static ScopeClip = 1;
+ static ScopeNone = 0;
_disposers: { [name: string]: IReactionDisposer } = {};
_ele: HTMLAudioElement | null = null;
@@ -72,10 +75,20 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
@observable _position: number = 0;
@observable _waveHeight: Opt<number> = this.layoutDoc._height;
@observable _paused: boolean = false;
- @observable _trimming: boolean = false;
- @observable _trimStart: number = NumCast(this.layoutDoc.clipStart) ? NumCast(this.layoutDoc.clipStart) : 0;
- @observable _trimEnd: number = NumCast(this.layoutDoc.clipEnd) ? NumCast(this.layoutDoc.clipEnd)
- : this.duration;
+ @observable _trimming: number = AudioBox.ScopeNone;
+ @observable _trimStart: number = NumCast(this.layoutDoc.clipStart);
+ @observable _trimEnd: number | undefined = Cast(this.layoutDoc.clipEnd, "number");
+ @computed get clipStart() { return this._trimming === AudioBox.ScopeAll ? 0 : NumCast(this.layoutDoc.clipStart); }
+ @computed get clipDuration() {
+ return this._trimming === AudioBox.ScopeAll ? NumCast(this.dataDoc[`${this.fieldKey}-duration`]) :
+ NumCast(this.layoutDoc.clipEnd, this.clipStart + NumCast(this.dataDoc[`${this.fieldKey}-duration`])) - this.clipStart;
+ }
+ @computed get clipEnd() { return this.clipStart + this.clipDuration; }
+ @computed get trimStart() { return this._trimming !== AudioBox.ScopeNone ? this._trimStart : NumCast(this.layoutDoc.clipStart); }
+ @computed get trimDuration() { return this.trimEnd - this.trimStart; }
+ @computed get trimEnd() {
+ return this._trimming !== AudioBox.ScopeNone && this._trimEnd !== undefined ? this._trimEnd : NumCast(this.layoutDoc.clipEnd, this.clipDuration);
+ }
@computed get mediaState():
| undefined
@@ -83,7 +96,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
| "recording"
| "paused"
| "playing" {
- return this.dataDoc.mediaState as
+ return this.layoutDoc.mediaState as
| undefined
| "pendingRecording"
| "recording"
@@ -91,7 +104,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
| "playing";
}
set mediaState(value) {
- this.dataDoc.mediaState = value;
+ this.layoutDoc.mediaState = value;
}
public static SetScrubTime = action((timeInMillisFrom1970: number) => {
AudioBox._scrubTime = 0;
@@ -103,12 +116,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
DateField
)?.date.getTime();
}
- @computed get duration() {
+ @computed get rawDuration() {
return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
}
- @computed get trimDuration() {
- return this._trimming && this._trimEnd ? this.duration : this._trimEnd - this._trimStart;
- }
@computed get anchorDocs() {
return DocListCast(this.dataDoc[this.annotationKey]);
}
@@ -125,13 +135,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
super(props);
AudioBox.Instance = this;
-
- if (this.duration === undefined) {
- runInAction(
- () =>
- (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
- );
- }
}
getLinkData(l: Doc) {
@@ -166,20 +169,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
}
componentWillUnmount() {
+ this.dropDisposer?.();
Object.values(this._disposers).forEach((disposer) => disposer?.());
const ind = DocUtils.ActiveRecordings.indexOf(this);
ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
}
+ private dropDisposer?: DragManager.DragDropDisposer;
@action
componentDidMount() {
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.mediaState = this.path ? "paused" : undefined;
- this.layoutDoc.clipStart = this.layoutDoc.clipStart ? this.layoutDoc.clipStart : 0;
- this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? this.layoutDoc.clipEnd : this.duration ? this.duration : undefined;
-
this.path && this.setAnchorTime(NumCast(this.layoutDoc.clipStart));
this.path && this.timecodeChanged();
@@ -220,13 +222,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
timecodeChanged = () => {
const htmlEle = this._ele;
if (this.mediaState !== "recording" && htmlEle) {
- htmlEle.duration &&
- htmlEle.duration !== Infinity &&
- runInAction(
- () => (this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration)
- );
- this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? Math.min(this.duration, NumCast(this.layoutDoc.clipEnd)) : this.duration;
- this._trimEnd = this._trimEnd ? Math.min(this.duration, this._trimEnd) : this.duration;
this.links
.map((l) => this.getLinkData(l))
.forEach(({ la1, la2, linkTime }) => {
@@ -256,7 +251,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
// play back the audio from time
@action
- playFrom = (seekTimeInSeconds: number, endTime: number = this._trimEnd, fullPlay: boolean = false) => {
+ playFrom = (seekTimeInSeconds: number, endTime: number = this.trimEnd, fullPlay: boolean = false) => {
clearTimeout(this._play);
if (Number.isNaN(this._ele?.duration)) {
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
@@ -267,13 +262,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
} else {
this.Pause();
}
- } else if (this._trimStart <= endTime && seekTimeInSeconds <= this._trimEnd) {
- const start = Math.max(this._trimStart, seekTimeInSeconds);
- const end = Math.min(this._trimEnd, endTime);
+ } else if (this.trimStart <= endTime && seekTimeInSeconds <= this.trimEnd) {
+ const start = Math.max(this.trimStart, seekTimeInSeconds);
+ const end = Math.min(this.trimEnd, endTime);
this._ele.currentTime = start;
this._ele.play();
runInAction(() => (this.mediaState = "playing"));
- if (endTime !== this.duration) {
+ if (endTime !== this.clipDuration) {
this._play = setTimeout(
() => {
this._ended = fullPlay ? true : this._ended;
@@ -313,6 +308,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
this.props.Document[this.props.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
+ if (this._trimEnd === undefined) this._trimEnd = this.clipDuration;
}
};
this._recordStart = new Date().getTime();
@@ -362,9 +358,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
this.dataDoc[this.fieldKey + "-duration"] =
(new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
this.mediaState = "paused";
- this._trimEnd = this.duration;
+ this._trimEnd = this.clipDuration;
this.layoutDoc.clipStart = 0;
- this.layoutDoc.clipEnd = this.duration;
+ this.layoutDoc.clipEnd = this.clipDuration;
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
@@ -381,15 +377,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
// for play button
Play = (e?: any) => {
let start;
- if (this._ended || this._ele!.currentTime === this.duration) {
- start = this._trimStart;
+ if (this._ended || this._ele!.currentTime === this.clipDuration) {
+ start = NumCast(this.layoutDoc.clipStart);
this._ended = false;
}
else {
start = this._ele!.currentTime;
}
- this.playFrom(start, this._trimEnd, true);
+ this.playFrom(start, this.trimEnd, true);
e?.stopPropagation?.();
}
@@ -431,7 +427,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
// returns the html audio element
@computed get audio() {
- return <audio ref={this.setRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+ return <audio ref={this.setRef}
+ onLoadedData={action(e => {
+ const duration = this._ele?.duration;
+ if (duration && duration !== Infinity) {
+ runInAction(
+ () => this.dataDoc[this.fieldKey + "-duration"] = duration
+ );
+ }
+ })}
+ className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
<source src={this.path} type="audio/mpeg" />
Not supported.
</audio>;
@@ -488,27 +493,24 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
// shows trim controls
@action
- startTrim = () => {
- if (!this.duration) {
- this.timecodeChanged();
- }
+ startTrim = (scope: number) => {
if (this.mediaState === "playing") {
this.Pause();
}
- this._trimming = true;
+ this._trimming = scope;
}
// hides trim controls and displays new clip
- @action
- finishTrim = () => {
+ @undoBatch
+ finishTrim = action(() => {
if (this.mediaState === "playing") {
this.Pause();
}
- this.layoutDoc.clipStart = this._trimStart;
- this.layoutDoc.clipEnd = this._trimEnd;
- this._trimming = false;
- this.setAnchorTime(Math.max(Math.min(this._trimEnd, this._ele!.currentTime), this._trimStart));
- }
+ this.layoutDoc.clipStart = this.trimStart;
+ this.layoutDoc.clipEnd = this.trimEnd;
+ this.setAnchorTime(Math.max(Math.min(this.trimEnd, this._ele!.currentTime), this.trimStart));
+ this._trimming = AudioBox.ScopeNone;
+ });
@action
setStartTrim = (newStart: number) => {
@@ -541,11 +543,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
this.heightPercent) /
100 // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
+ trimEndFunc = () => this.trimEnd;
+ trimStartFunc = () => this.trimStart;
+ trimDurationFunc = () => this.trimDuration;
@computed get renderTimeline() {
return (
<CollectionStackedTimeline
ref={this._stackedTimeline}
- {...this.props}
+ {...OmitKeys(this.props, ["CollectionFreeFormDocumentView"]).omit}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + "-dictation"}
mediaPath={this.path}
@@ -555,13 +560,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
focus={DocUtils.DefaultFocus}
bringToFront={emptyFunction}
CollectionView={undefined}
- duration={this.duration}
playFrom={this.playFrom}
setTime={this.setAnchorTime}
playing={this.playing}
- whenChildContentsActiveChanged={
- this.timelineWhenChildContentsActiveChanged
- }
+ whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
moveDocument={this.moveDocument}
addDocument={this.addDocument}
removeDocument={this.removeDocument}
@@ -573,15 +575,35 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
playLink={this.playLink}
PanelWidth={this.timelineWidth}
PanelHeight={this.timelineHeight}
- trimming={this._trimming}
- trimStart={this._trimStart}
- trimEnd={this._trimEnd}
- trimDuration={this.trimDuration}
+ rawDuration={this.rawDuration}
+
+ // this edits the entire waveform when trimming is activated
+ clipStart={this._trimming === AudioBox.ScopeAll ? 0 : this.clipStart}
+ clipEnd={this._trimming === AudioBox.ScopeAll ? this.rawDuration : this.clipEnd}
+ clipDuration={this._trimming === AudioBox.ScopeAll ? this.rawDuration : this.clipDuration}
+ // this edits just the current waveform clip when trimming is activated
+ // clipStart={this.clipStart}
+ // clipEnd={this.clipEnd}
+ // clipDuration={this.duration}
+
+ trimming={this._trimming !== AudioBox.ScopeNone}
+ trimStart={this.trimStartFunc}
+ trimEnd={this.trimEndFunc}
+ trimDuration={this.trimDurationFunc}
setStartTrim={this.setStartTrim}
setEndTrim={this.setEndTrim}
/>
);
}
+ onClipPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
+ if (doubleTap) {
+ this.startTrim(AudioBox.ScopeAll);
+ } else {
+ this._trimming !== AudioBox.ScopeNone ? this.finishTrim() : this.startTrim(AudioBox.ScopeClip);
+ }
+ }));
+ }
render() {
const interactive =
@@ -590,6 +612,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
: "";
return (
<div
+ ref={r => {
+ if (r && this._stackedTimeline.current) {
+ this.dropDisposer?.();
+ this.dropDisposer = DragManager.MakeDropTarget(r,
+ (e, de) => {
+ const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ de.complete.docDragData && this._stackedTimeline.current!.internalDocDrop(e, de, de.complete.docDragData, xp);
+ }
+ , this.layoutDoc, undefined);
+ }
+ }}
className="audiobox-container"
onContextMenu={this.specificContextMenu}
onClick={
@@ -606,9 +639,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
<div className="audiobox-buttons">
<div className="audiobox-dictation" onClick={this.onFile}>
<FontAwesomeIcon
- style={{
- width: "30px"
- }}
+ style={{ width: "30px" }}
icon="file-alt"
size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
/>
@@ -674,11 +705,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
</div>
<div
className="audiobox-buttons"
- title={this._trimming ? "finish" : "trim"}
- onClick={this._trimming ? this.finishTrim : this.startTrim}
+ title={this._trimming !== AudioBox.ScopeNone ? "finish" : "trim"}
+ onPointerDown={this.onClipPointerDown}
>
<FontAwesomeIcon
- icon={this._trimming ? "check" : "cut"}
+ icon={this._trimming !== AudioBox.ScopeNone ? "check" : "cut"}
size={"1x"}
/>
</div>
@@ -696,14 +727,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
</div>
{this.audio}
<div className="audioBox-current-time">
- {this._trimming ?
- formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))
- : formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this._trimStart)))}
+ {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.clipStart)))}
</div>
<div className="audioBox-total-time">
- {this._trimming || !this._trimEnd ?
- formatTime(Math.round(NumCast(this.duration)))
- : formatTime(Math.round(NumCast(this.trimDuration)))}
+ {formatTime(Math.round(NumCast(this.clipDuration)))}
</div>
</div>
</div>