aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/AudioWaveform.tsx120
-rw-r--r--src/client/views/DocumentDecorations.tsx4
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx97
-rw-r--r--src/client/views/nodes/AudioBox.scss121
-rw-r--r--src/client/views/nodes/AudioBox.tsx185
-rw-r--r--src/client/views/nodes/DocumentView.tsx5
-rw-r--r--src/client/views/nodes/LabelBox.tsx4
-rw-r--r--src/client/views/nodes/VideoBox.tsx28
8 files changed, 331 insertions, 233 deletions
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 8f3b7c2cd..f7b117130 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import axios from "axios";
-import { action, computed } from "mobx";
+import { action, computed, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
import Waveform from "react-audio-waveform";
import { Doc } from "../../fields/Doc";
@@ -12,114 +12,88 @@ import "./AudioWaveform.scss";
import { Colors } from "./global/globalEnums";
export interface AudioWaveformProps {
- duration: number;
+ duration: number; // length of media clip
+ rawDuration: number; // length of underlying media data
mediaPath: string;
layoutDoc: Doc;
trimming: boolean;
+ clipStart: number;
+ clipEnd: number;
PanelHeight: () => number;
}
@observer
export class AudioWaveform extends React.Component<AudioWaveformProps> {
public static NUMBER_OF_BUCKETS = 100;
+ _disposer: IReactionDisposer | undefined;
@computed get _waveHeight() {
return Math.max(50, this.props.PanelHeight());
}
+
+ @computed get clipStart() { return this.props.clipStart; }
+ @computed get clipEnd() { return this.props.clipEnd; }
+ audioBucketField = (start: number, end: number) => { return "audioBuckets/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_"); }
+ @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)], listSpec("number"), []); }
+ componentWillUnmount() {
+ this._disposer?.();
+ }
componentDidMount() {
- const audioBuckets = Cast(
- this.props.layoutDoc.audioBuckets,
- listSpec("number"),
- []
- );
- if (!audioBuckets.length) {
- this.props.layoutDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
- setTimeout(this.createWaveformBuckets);
- }
+ this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd) }),
+ ({ clipStart, clipEnd, fieldKey }) => {
+ if (!this.props.layoutDoc[fieldKey]) {
+ // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time.
+ this.props.layoutDoc[fieldKey] = new List<number>(numberRange(AudioWaveform.NUMBER_OF_BUCKETS));
+ setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd));
+ }
+ }, { fireImmediately: true });
+
}
// decodes the audio file into peaks for generating the waveform
- createWaveformBuckets = async () => {
+ createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number) => {
axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then(
(response) => {
const context = new window.AudioContext();
context.decodeAudioData(
response.data,
action((buffer) => {
- const decodedAudioData = buffer.getChannelData(0);
+ const rawDecodedAudioData = buffer.getChannelData(0);
+ const startInd = clipStart / this.props.rawDuration;
+ const endInd = clipEnd / this.props.rawDuration;
+ const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length));
const bucketDataSize = Math.floor(
decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS
);
const brange = Array.from(Array(bucketDataSize));
- this.props.layoutDoc.audioBuckets = new List<number>(
- numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map(
- (i: number) =>
- brange.reduce(
- (p, x, j) =>
- Math.abs(
- Math.max(p, decodedAudioData[i * bucketDataSize + j])
- ),
- 0
- ) / 2
- )
+ const bucketList = numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map(
+ (i: number) =>
+ brange.reduce(
+ (p, x, j) =>
+ Math.abs(
+ Math.max(p, decodedAudioData[i * bucketDataSize + j])
+ ),
+ 0
+ ) / 2
);
+ this.props.layoutDoc[fieldKey] = new List<number>(bucketList);
})
);
}
);
}
-
- @action
- createTrimBuckets = () => {
- const audioBuckets = Cast(
- this.props.layoutDoc.audioBuckets,
- listSpec("number"),
- []
- );
-
- const start = Math.floor(
- (NumCast(this.props.layoutDoc.clipStart) / this.props.duration) * 100
- );
- const end = Math.floor(
- (NumCast(this.props.layoutDoc.clipEnd) / this.props.duration) * 100
- );
- return audioBuckets.slice(start, end);
- }
-
render() {
- const audioBuckets = Cast(
- this.props.layoutDoc.audioBuckets,
- listSpec("number"),
- []
- );
-
return (
<div className="audioWaveform">
- {this.props.trimming || !this.props.layoutDoc.clipEnd ? (
- <Waveform
- color={Colors.MEDIUM_BLUE}
- height={this._waveHeight}
- barWidth={0.1}
- pos={this.props.duration}
- duration={this.props.duration}
- peaks={
- audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS
- ? audioBuckets
- : undefined
- }
- progressColor={Colors.MEDIUM_BLUE}
- />
- ) : (
- <Waveform
- color={Colors.MEDIUM_BLUE}
- height={this._waveHeight}
- barWidth={0.1}
- pos={this.props.duration}
- duration={this.props.duration}
- peaks={this.createTrimBuckets()}
- progressColor={Colors.MEDIUM_BLUE}
- />
- )}
+ <Waveform
+ color={Colors.MEDIUM_BLUE}
+ height={this._waveHeight}
+ barWidth={0.1}
+ pos={this.props.duration}
+ duration={this.props.duration}
+ peaks={this.audioBuckets}
+ progressColor={Colors.MEDIUM_BLUE}
+ />
</div>
);
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e9a54d6a5..c4f6625fc 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -429,8 +429,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton);
const canDelete = SelectionManager.Views().some(docView => {
const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
- return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) &&
- (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin);
+ //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) &&
+ return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin);
});
const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
<Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 89da6692a..7859d3c3f 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -30,7 +30,7 @@ import { LinkManager } from "../../util/LinkManager";
import { Scripting } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { AudioWaveform } from "../AudioWaveform";
import { CollectionSubView } from "../collections/CollectionSubView";
import { LightboxView } from "../LightboxView";
@@ -50,7 +50,6 @@ import { DragManager } from "../../util/DragManager";
type PanZoomDocument = makeInterface<[]>;
const PanZoomDocument = makeInterface();
export type CollectionStackedTimelineProps = {
- duration: number;
Play: () => void;
Pause: () => void;
playLink: (linkDoc: Doc) => void;
@@ -61,10 +60,14 @@ export type CollectionStackedTimelineProps = {
endTag: string;
mediaPath: string;
dictationKey: string;
+ rawDuration: number;
trimming: boolean;
- trimStart: number;
- trimEnd: number;
- trimDuration: number;
+ clipStart: number;
+ clipEnd: number;
+ clipDuration: number;
+ trimStart: () => number;
+ trimEnd: () => number;
+ trimDuration: () => number;
setStartTrim: (newStart: number) => void;
setEndTrim: (newEnd: number) => void;
};
@@ -88,21 +91,21 @@ export class CollectionStackedTimeline extends CollectionSubView<
get minLength() {
const rect = this._timeline?.getBoundingClientRect();
if (rect) {
- return 0.05 * this.duration;
+ return 0.05 * this.clipDuration;
}
return 0;
}
get trimStart() {
- return this.props.trimStart;
+ return this.props.trimStart();
}
get trimEnd() {
- return this.props.trimEnd;
+ return this.props.trimEnd();
}
- get duration() {
- return this.props.duration;
+ get clipDuration() {
+ return this.props.clipDuration;
}
@computed get currentTime() {
@@ -113,8 +116,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
<div
className="collectionStackedTimeline-selector"
style={{
- left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration) * 100}%`,
- width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration) * 100}%`,
+ left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration()) * 100}%`,
+ width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration()) * 100}%`,
}}
/>
);
@@ -162,8 +165,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
}
toTimeline = (screen_delta: number, width: number) => {
return Math.max(
- this.trimStart,
- Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart));
+ this.props.clipStart,
+ Math.min(this.props.clipEnd, (screen_delta / width) * this.props.clipDuration + this.props.clipStart));
}
rangeClickScript = () => CollectionStackedTimeline.RangeScript;
@@ -279,12 +282,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
this.props.isSelected(true) || this.props.isContentActive(),
undefined,
() => {
- !wasPlaying &&
- (this.props.trimming && this.duration ?
- this.props.setTime(((clientX - rect.x) / rect.width) * this.duration)
- :
- this.props.setTime(((clientX - rect.x) / rect.width) * this.props.trimDuration + this.trimStart)
- );
+ !wasPlaying && this.props.setTime(((clientX - rect.x) / rect.width) * this.clipDuration + this.props.clipStart);
}
);
}
@@ -302,7 +300,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
if (rect && this.props.isContentActive()) {
this.props.setStartTrim(Math.min(
Math.max(
- this.trimStart + (e.movementX / rect.width) * this.duration,
+ this.trimStart + (e.movementX / rect.width) * this.clipDuration,
0
),
this.trimEnd - this.minLength
@@ -330,8 +328,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
if (rect && this.props.isContentActive()) {
this.props.setEndTrim(Math.max(
Math.min(
- this.trimEnd + (e.movementX / rect.width) * this.duration,
- this.duration
+ this.trimEnd + (e.movementX / rect.width) * this.clipDuration,
+ this.props.clipStart + this.clipDuration
),
this.trimStart + this.minLength
));
@@ -341,7 +339,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
emptyFunction,
action((e, doubleTap) => {
if (doubleTap) {
- this.props.setEndTrim(this.duration);
+ this.props.setEndTrim(this.clipDuration);
}
})
);
@@ -354,6 +352,14 @@ export class CollectionStackedTimeline extends CollectionSubView<
// determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view
+ const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ const x = localPt[0] - docDragData.offset[0];
+ const timelineContentWidth = this.props.PanelWidth();
+ for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
+ const d = Doc.GetProto(docDragData.droppedDocuments[i]);
+ d._timecodeToHide = x / timelineContentWidth * this.props.trimDuration() + NumCast(d._timecodeToHide) - NumCast(d._timecodeToShow);
+ d._timecodeToShow = x / timelineContentWidth * this.props.trimDuration();
+ }
return true;
}
@@ -458,7 +464,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
const x1 = this.anchorStart(m);
const x2 = this.anchorEnd(
m,
- x1 + (10 / timelineContentWidth) * this.duration
+ x1 + (10 / timelineContentWidth) * this.clipDuration
);
let max = 0;
const overlappedLevels = new Set(
@@ -532,9 +538,12 @@ export class CollectionStackedTimeline extends CollectionSubView<
return !this.props.mediaPath ? null : (
<div className="collectionStackedTimeline-waveform">
<AudioWaveform
- duration={this.duration}
+ rawDuration={this.props.rawDuration}
+ duration={this.clipDuration}
mediaPath={this.props.mediaPath}
layoutDoc={this.layoutDoc}
+ clipStart={this.props.clipStart}
+ clipEnd={this.props.clipEnd}
PanelHeight={this.timelineContentHeight}
trimming={this.props.trimming}
/>
@@ -569,15 +578,13 @@ export class CollectionStackedTimeline extends CollectionSubView<
const start = this.anchorStart(d.anchor);
const end = this.anchorEnd(
d.anchor,
- start + (10 / timelineContentWidth) * this.duration
+ start + (10 / timelineContentWidth) * this.clipDuration
);
- const left = this.props.trimming ?
- (start / this.duration) * timelineContentWidth
- : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth;
- const top = (d.level / maxLevel) * this.timelineContentHeight();
+ const left = Math.max((start - this.props.clipStart) / this.clipDuration * timelineContentWidth, 0);
+ const top = (d.level / maxLevel) * this.timelineContentHeight() + 15;
const timespan = end - start;
- const width = (timespan / this.props.trimDuration) * timelineContentWidth;
- const height = this.timelineContentHeight() / maxLevel;
+ const width = (timespan / this.clipDuration) * timelineContentWidth;
+ const height = (this.timelineContentHeight()) / maxLevel;
return this.props.Document.hideAnchors ? null : (
<div
className={"collectionStackedTimeline-marker-timeline"}
@@ -620,9 +627,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
<div
className="collectionStackedTimeline-current"
style={{
- left: this.props.trimming
- ? `${(this.currentTime / this.duration) * 100}%`
- : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`,
+ left: `${((this.currentTime - this.props.clipStart) / this.clipDuration) * 100}%`,
}}
/>
@@ -630,15 +635,14 @@ export class CollectionStackedTimeline extends CollectionSubView<
<>
<div
className="collectionStackedTimeline-trim-shade"
- style={{ width: `${(this.trimStart / this.duration) * 100}%` }}
+ style={{ width: `${((this.trimStart - this.props.clipStart) / this.clipDuration) * 100}%` }}
></div>
<div
className="collectionStackedTimeline-trim-controls"
style={{
- left: `${(this.trimStart / this.duration) * 100}%`,
- width: `${((this.trimEnd - this.trimStart) / this.duration) * 100
- }%`,
+ left: `${((this.trimStart - this.props.clipStart) / this.clipDuration) * 100}%`,
+ width: `${((this.trimEnd - this.trimStart) / this.clipDuration) * 100}%`,
}}
>
<div
@@ -654,9 +658,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
<div
className="collectionStackedTimeline-trim-shade"
style={{
- left: `${(this.trimEnd / this.duration) * 100}%`,
- width: `${((this.duration - this.trimEnd) / this.duration) * 100
- }%`,
+ left: `${((this.trimEnd - this.props.clipStart) / this.clipDuration) * 100}%`,
+ width: `${((this.props.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`,
}}
></div>
</>
@@ -763,13 +766,19 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
}
return false;
};
+ var undo: UndoManager.Batch | undefined;
+
setupMoveUpEvents(
this,
e,
- (e) => changeAnchor(anchor, left, newTime(e)),
+ (e) => {
+ if (!undo) undo = UndoManager.StartBatch("drag anchor");
+ return changeAnchor(anchor, left, newTime(e))
+ },
(e) => {
this.props.setTime(newTime(e));
this.props._timeline?.releasePointerCapture(e.pointerId);
+ undo?.end();
},
emptyFunction
);
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index a6494e540..6adda4730 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -3,13 +3,100 @@
.audiobox-container,
.audiobox-container-interactive {
+ width: 100%;
+ height: 100%;
+ position: inherit;
+ display: flex;
+ position: relative;
+ cursor: default;
+
+ .audiobox-buttons {
+ display: flex;
+ width: 100%;
+ align-items: center;
+
+ .audiobox-dictation {
+ position: relative;
+ width: 30px;
+ height: 100%;
+ align-items: center;
+ display: inherit;
+ background: $medium-gray;
+ left: 0px;
+ color: $dark-gray;
+ &:hover {
+ color: $black;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .audiobox-control,
+ .audiobox-control-interactive {
+ top: 0;
+ max-height: 32px;
+ width: 100%;
+ display: inline-block;
+ pointer-events: none;
+ }
+
+ .audiobox-control-interactive {
+ pointer-events: all;
+ }
+
+ .audiobox-record-interactive,
+ .audiobox-record {
+ pointer-events: all;
+ cursor: pointer;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ color: white;
+ font-weight: bold;
+ }
+
+ .audiobox-record {
+ pointer-events: none;
+ }
+
+ .recording {
+ margin-top: auto;
+ margin-bottom: auto;
width: 100%;
height: 100%;
- position: inherit;
- display: flex;
position: relative;
- cursor: default;
+ padding-right: 5px;
+ display: flex;
+ background-color: $medium-blue;
+
+ .time {
+ position: relative;
+ width: 100%;
+ font-size: $large-header;
+ text-align: center;
+ }
+
+ .recording-buttons {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ color: $dark-gray;
+ &:hover {
+ color: $black;
+ }
+ }
+ .time, .recording-buttons {
+ display: flex;
+ align-items: center;
+ padding: 5px;
+ }
+ }
.audiobox-buttons {
display: flex;
width: 100%;
@@ -46,26 +133,6 @@
pointer-events: all;
}
- .audiobox-record-interactive,
- .audiobox-record {
- pointer-events: all;
- cursor: pointer;
- width: 100%;
- height: 100%;
- position: relative;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- gap: 10px;
- color: white;
- font-weight: bold;
- }
-
- .audiobox-record {
- pointer-events: none;
- }
-
.recording {
margin-top: auto;
margin-bottom: auto;
@@ -195,6 +262,12 @@
.audioBox-total-time {
right: 2px;
}
+
+ .audiobox-zoom {
+ bottom: 0;
+ left: 30px;
+ width: 70px;
+ }
}
}
}
@@ -219,4 +292,4 @@
.audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-buttons {
width: 70px;
}
-} \ No newline at end of file
+}
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>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 246d9f68d..ca68ee875 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -735,9 +735,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
!zorders && cm.addItem({ description: "ZOrder...", subitems: zorderItems, icon: "compass" });
- onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
+ !Doc.UserDoc().noviceMode && onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
!Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" });
- onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" });
+ this.props.CollectionFreeFormDocumentView && onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" });
if (!this.Document.annotationOn) {
const options = cm.findByDescription("Options...");
@@ -787,6 +787,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" });
}
+ !more && moreItems.length && cm.addItem({ description: "More...", subitems: moreItems, icon: "compass" });
const help = cm.findByDescription("Help...");
const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : [];
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index db1ae0537..468a2e585 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -50,14 +50,14 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; }
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({
+ !Doc.UserDoc().noviceMode && funcs.push({
description: "Clear Script Params", event: () => {
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
params?.map(p => this.paramsDoc[p] = undefined);
}, icon: "trash"
});
- ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });
+ funcs.length && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });
}
@undoBatch
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e3ba638b3..8b33842ff 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -9,7 +9,7 @@ import { InkTool } from "../../../fields/InkField";
import { makeInterface } from "../../../fields/Schema";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { AudioField, nullAudio, VideoField } from "../../../fields/URLField";
-import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils, returnFalse } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils, returnFalse, returnZero } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -61,9 +61,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable _playing = 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;
+
@computed get links() { return DocListCast(this.dataDoc.links); }
@computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); }
@computed get duration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ @computed get trimDuration() {
+ return this._trimming && this._trimEnd ? this.duration : this._trimEnd - this._trimStart;
+ }
private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; }
public get player(): HTMLVideoElement | null { return this._videoRef; }
@@ -202,6 +210,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
updateTimecode = () => {
this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+ 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;
try {
this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
} catch (e) {
@@ -516,6 +526,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+ trimEndFunc = () => this.duration;
@computed get renderTimeline() {
return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
<CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
@@ -527,7 +538,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
endTag={"_timecodeToHide" /* videoEnd */}
bringToFront={emptyFunction}
CollectionView={undefined}
- duration={this.duration}
playFrom={this.playFrom}
setTime={this.setAnchorTime}
playing={this.playing}
@@ -539,12 +549,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
Pause={this.Pause}
playLink={this.playLink}
PanelHeight={this.timelineHeight}
+ rawDuration={this.duration}
+ clipDuration={this.duration}
+ clipStart={0}
+ clipEnd={this.duration}
trimming={false}
- trimStart={0}
- trimEnd={this.duration}
- trimDuration={this.duration}
- setStartTrim={() => { }}
- setEndTrim={() => { }}
+ trimStart={returnZero}
+ trimEnd={this.trimEndFunc}
+ trimDuration={this.trimEndFunc}
+ setStartTrim={emptyFunction}
+ setEndTrim={emptyFunction}
/>
</div>;
}