aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgeireann <geireann.lindfield@gmail.com>2021-08-25 21:42:55 -0400
committergeireann <geireann.lindfield@gmail.com>2021-08-25 21:42:55 -0400
commitd5ee7f7436aaf3a50275379ff986cb3546d6f6a0 (patch)
tree8f78096042c66ae4b4f2de00de151eee521dc5cf
parent685c2c2e34a56619ba07052d7298149bc218e6e5 (diff)
parent8beb8fa42ba5f84bb13b5877560fc92ad3613e88 (diff)
Merge branch 'temporalmedia-mehek' into menu_updates_geireann
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/util/CurrentUserUtils.ts2
-rw-r--r--src/client/views/AudioWaveform.tsx121
-rw-r--r--src/client/views/DocumentDecorations.tsx11
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss142
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx757
-rw-r--r--src/client/views/nodes/AudioBox.scss338
-rw-r--r--src/client/views/nodes/AudioBox.tsx645
-rw-r--r--src/client/views/nodes/DocumentView.tsx1
-rw-r--r--src/client/views/nodes/LabelBox.tsx21
10 files changed, 1457 insertions, 583 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ee3a38a0a..8ef31362c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -377,7 +377,7 @@ export namespace Docs {
}],
[DocumentType.AUDIO, {
layout: { view: AudioBox, dataField: defaultDataKey },
- options: { _height: 35, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any }
+ options: { _height: 100, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any }
}],
[DocumentType.PDF, {
layout: { view: PDFBox, dataField: defaultDataKey },
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fbe216ef1..cd2803373 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -480,7 +480,7 @@ export class CurrentUserUtils {
(doc.emptyWall as Doc).videoWall = true;
}
if (doc.emptyAudio === undefined) {
- doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptyNote === undefined) {
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 7ff9c1071..f519f9ef0 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -6,56 +6,121 @@ import Waveform from "react-audio-waveform";
import { Doc } from "../../fields/Doc";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
+import { Cast, NumCast } from "../../fields/Types";
import { numberRange } from "../../Utils";
import "./AudioWaveform.scss";
+import { Colors } from "./global/globalEnums";
export interface AudioWaveformProps {
duration: number;
mediaPath: string;
- dataDoc: Doc;
+ layoutDoc: Doc;
+ trimming: boolean;
PanelHeight: () => number;
}
@observer
export class AudioWaveform extends React.Component<AudioWaveformProps> {
public static NUMBER_OF_BUCKETS = 100;
- @computed get _waveHeight() { return Math.max(50, this.props.PanelHeight()); }
+ @computed get _waveHeight() {
+ return Math.max(50, this.props.PanelHeight());
+ }
componentDidMount() {
- const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []);
+ const audioBuckets = Cast(
+ this.props.layoutDoc.audioBuckets,
+ listSpec("number"),
+ []
+ );
if (!audioBuckets.length) {
- this.props.dataDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
+ this.props.layoutDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
setTimeout(this.createWaveformBuckets);
}
}
+
// decodes the audio file into peaks for generating the waveform
createWaveformBuckets = async () => {
- axios({ url: this.props.mediaPath, responseType: "arraybuffer" })
- .then(response => {
+ axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then(
+ (response) => {
const context = new window.AudioContext();
- context.decodeAudioData(response.data,
- action(buffer => {
+ context.decodeAudioData(
+ response.data,
+ action((buffer) => {
const decodedAudioData = buffer.getChannelData(0);
- const bucketDataSize = Math.floor(decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS);
+
+ const bucketDataSize = Math.floor(
+ decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS
+ );
const brange = Array.from(Array(bucketDataSize));
- this.props.dataDoc.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));
- }));
- });
- }
+ 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
+ )
+ );
+ })
+ );
+ }
+ );
+ };
+
+ @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.dataDoc.audioBuckets, listSpec("number"), []);
- return <div className="audioWaveform">
- <Waveform
- color={"darkblue"}
- height={this._waveHeight}
- barWidth={0.1}
- pos={this.props.duration}
- duration={this.props.duration}
- peaks={audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS ? audioBuckets : undefined}
- progressColor={"blue"} />
- </div>;
+ 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}
+ />
+ )}
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 118d2e7c7..e454be618 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -391,10 +391,10 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
.forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth)
- ({
- X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width,
- Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height
- })));
+ ({
+ X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width,
+ Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height
+ })));
Doc.SetNativeWidth(doc, undefined);
Doc.SetNativeHeight(doc, undefined);
});
@@ -404,6 +404,9 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
get selectionTitle(): string {
if (SelectionManager.Views().length === 1) {
const selected = SelectionManager.Views()[0];
+ if (selected.ComponentView?.getTitle?.()) {
+ return selected.ComponentView.getTitle();
+ }
if (this._titleControlString.startsWith("=")) {
return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || "";
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index e456c0664..59c21210a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -1,70 +1,94 @@
+@import "../global/globalCssVariables.scss";
+
.collectionStackedTimeline {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 1000;
+ overflow: hidden;
+ top: 0px;
+
+ .collectionStackedTimeline-trim-shade {
position: absolute;
- width: 100%;
height: 100%;
- border: gray solid 1px;
- border-radius: 3px;
- z-index: 1000;
- overflow: hidden;
- top: 0px;
+ background-color: $dark-gray;
+ opacity: 0.3;
+ }
- .collectionStackedTimeline-selector {
- position: absolute;
- width: 10px;
- top: 2.5%;
- height: 95%;
- background: lightblue;
- border-radius: 5px;
- opacity: 0.3;
- z-index: 500;
- border-style: solid;
- border-color: darkblue;
- border-width: 1px;
- }
+ .collectionStackedTimeline-trim-controls {
+ height: 100%;
+ position: absolute;
+ box-sizing: border-box;
+ border: 2px solid $medium-blue;
+ display: flex;
+ justify-content: space-between;
+ max-width: 100%;
- .collectionStackedTimeline-current {
- width: 1px;
- height: 100%;
- background-color: red;
- position: absolute;
- top: 0px;
- pointer-events: none;
+ .collectionStackedTimeline-trim-handle {
+ background-color: $medium-blue;
+ height: 100%;
+ width: 5px;
+ cursor: ew-resize;
}
+ }
- .collectionStackedTimeline-marker-timeline {
- position: absolute;
- top: 2.5%;
- height: 95%;
- border-radius: 4px;
- &:hover {
- opacity: 1;
- }
+ .collectionStackedTimeline-selector {
+ position: absolute;
+ width: 10px;
+ top: 2.5%;
+ height: 95%;
+ background: $light-blue;
+ border-radius: 3px;
+ opacity: 0.3;
+ z-index: 500;
+ border-style: solid;
+ border-color: $medium-blue;
+ border-width: 1px;
+ }
- .collectionStackedTimeline-left-resizer,
- .collectionStackedTimeline-resizer {
- background: dimgrey;
- position: absolute;
- top: 0;
- height: 100%;
- width: 10px;
- pointer-events: all;
- cursor: ew-resize;
- z-index: 100;
- }
- .collectionStackedTimeline-resizer {
- right: 0;
- }
- .collectionStackedTimeline-left-resizer {
- left: 0;
- }
+ .collectionStackedTimeline-current {
+ width: 1px;
+ height: 100%;
+ background-color: $pink;
+ position: absolute;
+ top: 0px;
+ pointer-events: none;
+ }
+
+ .collectionStackedTimeline-marker-timeline {
+ position: absolute;
+ top: 2.5%;
+ height: 95%;
+ border-radius: 4px;
+ &:hover {
+ opacity: 1;
}
- .collectionStackedTimeline-waveform {
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0;
- left: 0;
- pointer-events: none;
+ .collectionStackedTimeline-left-resizer,
+ .collectionStackedTimeline-resizer {
+ background: $medium-gray;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ width: 10px;
+ pointer-events: all;
+ cursor: ew-resize;
+ z-index: 100;
+ }
+ .collectionStackedTimeline-resizer {
+ right: 0;
}
-} \ No newline at end of file
+ .collectionStackedTimeline-left-resizer {
+ left: 0;
+ }
+ }
+
+ .collectionStackedTimeline-waveform {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ }
+}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index a2c95df6e..51e05e278 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,5 +1,12 @@
import React = require("react");
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import {
+ action,
+ computed,
+ IReactionDisposer,
+ observable,
+ reaction,
+ runInAction,
+} from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { Doc, DocListCast } from "../../../fields/Doc";
@@ -8,7 +15,16 @@ import { List } from "../../../fields/List";
import { listSpec, makeInterface } from "../../../fields/Schema";
import { ComputedField, ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent, returnTrue } from "../../../Utils";
+import {
+ emptyFunction,
+ formatTime,
+ OmitKeys,
+ returnFalse,
+ returnOne,
+ setupMoveUpEvents,
+ StopEvent,
+ returnTrue,
+} from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { LinkManager } from "../../util/LinkManager";
import { Scripting } from "../../util/Scripting";
@@ -18,9 +34,15 @@ import { undoBatch } from "../../util/UndoManager";
import { AudioWaveform } from "../AudioWaveform";
import { CollectionSubView } from "../collections/CollectionSubView";
import { LightboxView } from "../LightboxView";
-import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView";
+import {
+ DocAfterFocusFunc,
+ DocFocusFunc,
+ DocumentView,
+ DocumentViewProps,
+} from "../nodes/DocumentView";
import { LabelBox } from "../nodes/LabelBox";
import "./CollectionStackedTimeline.scss";
+import { Colors } from "../global/globalEnums";
type PanZoomDocument = makeInterface<[]>;
const PanZoomDocument = makeInterface();
@@ -36,11 +58,21 @@ export type CollectionStackedTimelineProps = {
endTag: string;
mediaPath: string;
dictationKey: string;
+ trimming: boolean;
+ trimStart: number;
+ trimEnd: number;
+ trimDuration: number;
+ setStartTrim: (newStart: number) => void;
+ setEndTrim: (newEnd: number) => void;
};
@observer
-export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) {
- @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
+export class CollectionStackedTimeline extends CollectionSubView<
+ PanZoomDocument,
+ CollectionStackedTimelineProps
+>(PanZoomDocument) {
+ @observable static SelectingRegion: CollectionStackedTimeline | undefined =
+ undefined;
static RangeScript: ScriptField;
static LabelScript: ScriptField;
static RangePlayScript: ScriptField;
@@ -50,58 +82,123 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
private _markerStart: number = 0;
@observable _markerEnd: number = 0;
- get duration() { return this.props.duration; }
- @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }
+ get minLength() {
+ const rect = this._timeline?.getBoundingClientRect();
+ if (rect) {
+ return 0.05 * this.duration;
+ }
+ return 0;
+ }
+
+ get trimStart() {
+ return this.props.trimStart;
+ }
+
+ get trimEnd() {
+ return this.props.trimEnd;
+ }
+
+ get duration() {
+ return this.props.duration;
+ }
+
+ @computed get currentTime() {
+ return NumCast(this.layoutDoc._currentTimecode);
+ }
@computed get selectionContainer() {
- return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="collectionStackedTimeline-selector" style={{
- left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`,
- width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%`
- }} />;
+ return CollectionStackedTimeline.SelectingRegion !== this ? null : (
+ <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}%`,
+ }}
+ />
+ );
}
constructor(props: any) {
super(props);
// onClick play scripts
- CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
- CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
+ CollectionStackedTimeline.RangeScript =
+ CollectionStackedTimeline.RangeScript ||
+ ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, {
+ self: Doc.name,
+ scriptContext: "any",
+ clientX: "number",
+ })!;
+ CollectionStackedTimeline.RangePlayScript =
+ CollectionStackedTimeline.RangePlayScript ||
+ ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, {
+ self: Doc.name,
+ scriptContext: "any",
+ clientX: "number",
+ })!;
}
- componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); }
+ componentDidMount() {
+ document.addEventListener("keydown", this.keyEvents, true);
+ }
componentWillUnmount() {
document.removeEventListener("keydown", this.keyEvents, true);
- if (CollectionStackedTimeline.SelectingRegion === this) runInAction(() => CollectionStackedTimeline.SelectingRegion = undefined);
+ if (CollectionStackedTimeline.SelectingRegion === this)
+ runInAction(
+ () => (CollectionStackedTimeline.SelectingRegion = undefined)
+ );
}
- anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
+ anchorStart = (anchor: Doc) =>
+ NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
anchorEnd = (anchor: Doc, val: any = null) => {
const endVal = NumCast(anchor[this.props.endTag], val);
- return NumCast(anchor._timecodeToHide, endVal === undefined ? null : endVal);
+ return NumCast(
+ anchor._timecodeToHide,
+ endVal === undefined ? null : endVal
+ );
+ };
+ toTimeline = (screen_delta: number, width: number) => {
+ return Math.max(
+ this.trimStart,
+ Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart))
}
- toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration));
+
rangeClickScript = () => CollectionStackedTimeline.RangeScript;
rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
// for creating key anchors with key events
@action
keyEvents = (e: KeyboardEvent) => {
- if (!(e.target instanceof HTMLInputElement) && this.props.isSelected(true)) {
+ if (
+ !(e.target instanceof HTMLInputElement) &&
+ this.props.isSelected(true)
+ ) {
switch (e.key) {
case " ":
if (!CollectionStackedTimeline.SelectingRegion) {
this._markerStart = this._markerEnd = this.currentTime;
CollectionStackedTimeline.SelectingRegion = this;
} else {
- CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime);
+ CollectionStackedTimeline.createAnchor(
+ this.rootDoc,
+ this.dataDoc,
+ this.props.fieldKey,
+ this.props.startTag,
+ this.props.endTag,
+ this.currentTime
+ );
CollectionStackedTimeline.SelectingRegion = undefined;
}
}
}
- }
+ };
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag]));
+ const linkTime = NumCast(
+ la2[this.props.startTag],
+ NumCast(la1[this.props.startTag])
+ );
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
@@ -118,10 +215,18 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
const wasPlaying = this.props.playing();
if (wasPlaying) this.props.Pause();
const wasSelecting = CollectionStackedTimeline.SelectingRegion === this;
- setupMoveUpEvents(this, e,
- action(e => {
- if (!wasSelecting && CollectionStackedTimeline.SelectingRegion !== this) {
- this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e) => {
+ if (
+ !wasSelecting &&
+ CollectionStackedTimeline.SelectingRegion !== this
+ ) {
+ this._markerStart = this._markerEnd = this.toTimeline(
+ clientX - rect.x,
+ rect.width
+ );
CollectionStackedTimeline.SelectingRegion = this;
}
this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
@@ -134,32 +239,129 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
this._markerStart = this._markerEnd;
this._markerEnd = tmp;
}
- if (!isClick && CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15)) {
- CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag,
- this._markerStart, this._markerEnd);
+ if (
+ !isClick &&
+ CollectionStackedTimeline.SelectingRegion === this &&
+ Math.abs(movement[0]) > 15 &&
+ !this.props.trimming
+ ) {
+ CollectionStackedTimeline.createAnchor(
+ this.rootDoc,
+ this.dataDoc,
+ this.props.fieldKey,
+ this.props.startTag,
+ this.props.endTag,
+ this._markerStart,
+ this._markerEnd
+ );
}
- (!isClick || !wasSelecting) && (CollectionStackedTimeline.SelectingRegion = undefined);
+ (!isClick || !wasSelecting) &&
+ (CollectionStackedTimeline.SelectingRegion = undefined);
}),
(e, doubleTap) => {
this.props.select(false);
- e.shiftKey && CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime);
+ e.shiftKey &&
+ CollectionStackedTimeline.createAnchor(
+ this.rootDoc,
+ this.dataDoc,
+ this.props.fieldKey,
+ this.props.startTag,
+ this.props.endTag,
+ this.currentTime
+ );
!wasPlaying && doubleTap && this.props.Play();
},
- this.props.isSelected(true) || this.props.isContentActive(), undefined,
- () => !wasPlaying && this.props.setTime((clientX - rect.x) / rect.width * this.duration));
+ 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)
+ )
+ }
+ );
}
- }
+
+ };
+
+ @action
+ trimLeft = (e: React.PointerEvent): void => {
+ const rect = this._timeline?.getBoundingClientRect();
+ const clientX = e.movementX;
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e, [], []) => {
+ if (rect && this.props.isContentActive()) {
+ this.props.setStartTrim(Math.min(
+ Math.max(
+ this.trimStart + (e.movementX / rect.width) * this.duration,
+ 0
+ ),
+ this.trimEnd - this.minLength
+ ));
+ }
+ return false;
+ }),
+ emptyFunction,
+ action((e, doubleTap) => {
+ if (doubleTap) {
+ this.props.setStartTrim(0);
+ }
+ })
+ );
+ };
+
+ @action
+ trimRight = (e: React.PointerEvent): void => {
+ const rect = this._timeline?.getBoundingClientRect();
+ const clientX = e.movementX;
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e, [], []) => {
+ if (rect && this.props.isContentActive()) {
+ this.props.setEndTrim(Math.max(
+ Math.min(
+ this.trimEnd + (e.movementX / rect.width) * this.duration,
+ this.duration
+ ),
+ this.trimStart + this.minLength
+ ));
+ }
+ return false;
+ }),
+ emptyFunction,
+ action((e, doubleTap) => {
+ if (doubleTap) {
+ this.props.setEndTrim(this.duration);
+ }
+ })
+ );
+ };
@undoBatch
@action
- static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number) {
+ static createAnchor(
+ rootDoc: Doc,
+ dataDoc: Doc,
+ fieldKey: string,
+ startTag: string,
+ endTag: string,
+ anchorStartTime?: number,
+ anchorEndTime?: number
+ ) {
if (anchorStartTime === undefined) return rootDoc;
const anchor = Docs.Create.LabelDocument({
- title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any,
+ title: ComputedField.MakeFunction(
+ `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`
+ ) as any,
useLinkSmallAnchor: true,
hideLinkButton: true,
annotationOn: rootDoc,
- _timelineLabel: true
+ _timelineLabel: true,
});
Doc.GetProto(anchor)[startTag] = anchorStartTime;
Doc.GetProto(anchor)[endTag] = anchorEndTime;
@@ -179,7 +381,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
if (this.props.playing()) this.props.Pause();
else this.props.playFrom(seekTimeInSeconds, endTime);
} else {
- if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) {
+ if (
+ seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) &&
+ endTime > NumCast(this.layoutDoc._currentTimecode)
+ ) {
if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
this.props.Pause();
} else {
@@ -190,127 +395,249 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
}
}
return { select: true };
- }
+ };
@action
clickAnchor = (anchorDoc: Doc, clientX: number) => {
- if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
+ if (anchorDoc.isLinkButton)
+ LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
const endTime = this.anchorEnd(anchorDoc);
- if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
+ if (
+ seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 &&
+ endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4
+ ) {
if (this.props.playing()) this.props.Pause();
else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
else if (!this.layoutDoc.autoPlayAnchors) {
const rect = this._timeline?.getBoundingClientRect();
- rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
+ rect &&
+ this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
}
} else {
- if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime);
- else this.props.setTime(seekTimeInSeconds);
+ if (this.layoutDoc.autoPlayAnchors)
+ this.props.playFrom(seekTimeInSeconds, endTime);
+ else {
+ this.props.setTime(seekTimeInSeconds);
+ }
}
return { select: true };
- }
-
+ };
// makes sure no anchors overlaps each other by setting the correct position and width
- getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => {
+ getLevel = (
+ m: Doc,
+ placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
+ ) => {
const timelineContentWidth = this.props.PanelWidth();
const x1 = this.anchorStart(m);
- const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration);
+ const x2 = this.anchorEnd(
+ m,
+ x1 + (10 / timelineContentWidth) * this.duration
+ );
let max = 0;
- const overlappedLevels = new Set(placed.map(p => {
- const y1 = p.anchorStartTime;
- const y2 = p.anchorEndTime;
- if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) ||
- (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) {
- max = Math.max(max, p.level);
- return p.level;
- }
- }));
+ const overlappedLevels = new Set(
+ placed.map((p) => {
+ const y1 = p.anchorStartTime;
+ const y2 = p.anchorEndTime;
+ if (
+ (x1 >= y1 && x1 <= y2) ||
+ (x2 >= y1 && x2 <= y2) ||
+ (y1 >= x1 && y1 <= x2) ||
+ (y2 >= x1 && y2 <= x2)
+ ) {
+ max = Math.max(max, p.level);
+ return p.level;
+ }
+ })
+ );
let level = max + 1;
for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
return level;
- }
+ };
dictationHeightPercent = 50;
- dictationHeight = () => this.props.PanelHeight() * (100 - this.dictationHeightPercent) / 100;
- timelineContentHeight = () => this.props.PanelHeight() * this.dictationHeightPercent / 100;
- dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight());
+ dictationHeight = () =>
+ (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
+ timelineContentHeight = () =>
+ (this.props.PanelHeight() * this.dictationHeightPercent) / 100;
+ dictationScreenToLocalTransform = () =>
+ this.props
+ .ScreenToLocalTransform()
+ .translate(0, -this.timelineContentHeight());
@computed get renderDictation() {
const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
- return !dictation ? (null) : <div style={{ position: "absolute", height: "100%", top: this.timelineContentHeight(), background: "tan" }}>
- <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- Document={dictation}
- PanelHeight={this.dictationHeight}
- isAnnotationOverlay={true}
- isDocumentActive={returnFalse}
- select={emptyFunction}
- scaling={returnOne}
- xMargin={25}
- yMargin={10}
- ScreenToLocalTransform={this.dictationScreenToLocalTransform}
- whenChildContentsActiveChanged={emptyFunction}
- removeDocument={returnFalse}
- moveDocument={returnFalse}
- addDocument={returnFalse}
- CollectionView={undefined}
- renderDepth={this.props.renderDepth + 1}>
- </DocumentView>
- </div>;
+ return !dictation ? null : (
+ <div
+ style={{
+ position: "absolute",
+ height: "100%",
+ top: this.timelineContentHeight(),
+ background: Colors.LIGHT_BLUE,
+ }}
+ >
+ <DocumentView
+ {...OmitKeys(this.props, [
+ "NativeWidth",
+ "NativeHeight",
+ "setContentView",
+ ]).omit}
+ Document={dictation}
+ PanelHeight={this.dictationHeight}
+ isAnnotationOverlay={true}
+ isDocumentActive={returnFalse}
+ select={emptyFunction}
+ scaling={returnOne}
+ xMargin={25}
+ yMargin={10}
+ ScreenToLocalTransform={this.dictationScreenToLocalTransform}
+ whenChildContentsActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ></DocumentView>
+ </div>
+ );
}
@computed get renderAudioWaveform() {
- return !this.props.mediaPath ? (null) :
- <div className="collectionStackedTimeline-waveform" >
+ return !this.props.mediaPath ? null : (
+ <div className="collectionStackedTimeline-waveform">
<AudioWaveform
duration={this.duration}
mediaPath={this.props.mediaPath}
- dataDoc={this.dataDoc}
- PanelHeight={this.timelineContentHeight} />
- </div>;
+ layoutDoc={this.layoutDoc}
+ PanelHeight={this.timelineContentHeight}
+ trimming={this.props.trimming}
+ />
+ </div>
+ );
}
+
currentTimecode = () => this.currentTime;
render() {
const timelineContentWidth = this.props.PanelWidth();
- const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
- const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
+ const overlaps: {
+ anchorStartTime: number;
+ anchorEndTime: number;
+ level: number;
+ }[] = [];
+ const drawAnchors = this.childDocs.map((anchor) => ({
+ level: this.getLevel(anchor, overlaps),
+ anchor,
+ }));
const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- const isActive = this.props.isContentActive() || this.props.isSelected(false);
- return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline}
- onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}>
- {drawAnchors.map(d => {
- const start = this.anchorStart(d.anchor);
- const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
- const left = start / this.duration * timelineContentWidth;
- const top = d.level / maxLevel * this.timelineContentHeight();
- const timespan = end - start;
- return this.props.Document.hideAnchors ? (null) :
- <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
- style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${this.timelineContentHeight() / maxLevel}px` }}
- onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
- <StackedTimelineAnchor {...this.props}
- mark={d.anchor}
- rangeClickScript={this.rangeClickScript}
- rangePlayScript={this.rangePlayScript}
- left={left}
- top={top}
- width={timelineContentWidth * timespan / this.duration}
- height={this.timelineContentHeight() / maxLevel}
- toTimeline={this.toTimeline}
- layoutDoc={this.layoutDoc}
- currentTimecode={this.currentTimecode}
- _timeline={this._timeline}
- stackedTimeline={this}
- />
- </div>;
- })}
- {this.selectionContainer}
- {this.renderAudioWaveform}
- {this.renderDictation}
-
- <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
- </div>;
+ const isActive =
+ this.props.isContentActive() || this.props.isSelected(false);
+ return (
+ <div
+ className="collectionStackedTimeline"
+ ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
+ onClick={(e) => isActive && StopEvent(e)}
+ onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)}
+ >
+ {drawAnchors.map((d) => {
+
+ const start = this.anchorStart(d.anchor);
+ const end = this.anchorEnd(
+ d.anchor,
+ start + (10 / timelineContentWidth) * this.duration
+ );
+ const left = this.props.trimming ?
+ (start / this.duration) * timelineContentWidth
+ : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth;
+ const top = (d.level / maxLevel) * this.timelineContentHeight();
+ const timespan = end - start;
+ const width = (timespan / this.props.trimDuration) * timelineContentWidth;
+ const height = this.timelineContentHeight() / maxLevel
+ return this.props.Document.hideAnchors ? null : (
+ <div
+ className={"collectionStackedTimeline-marker-timeline"}
+ key={d.anchor[Id]}
+ style={{
+ left,
+ top,
+ width: `${width}px`,
+ height: `${height}px`,
+ }}
+ onClick={(e) => {
+ this.props.playFrom(start, this.anchorEnd(d.anchor));
+ e.stopPropagation();
+ }}
+ >
+ <StackedTimelineAnchor
+ {...this.props}
+ mark={d.anchor}
+ rangeClickScript={this.rangeClickScript}
+ rangePlayScript={this.rangePlayScript}
+ left={left}
+ top={top}
+ width={width}
+ height={height}
+ toTimeline={this.toTimeline}
+ layoutDoc={this.layoutDoc}
+ currentTimecode={this.currentTimecode}
+ _timeline={this._timeline}
+ stackedTimeline={this}
+ trimStart={this.trimStart}
+ trimEnd={this.trimEnd}
+ />
+ </div>
+ );
+ })}
+ {!this.props.trimming && this.selectionContainer}
+ {this.renderAudioWaveform}
+ {this.renderDictation}
+
+ <div
+ className="collectionStackedTimeline-current"
+ style={{
+ left: this.props.trimming
+ ? `${(this.currentTime / this.duration) * 100}%`
+ : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`,
+ }}
+ />
+
+ {this.props.trimming && (
+ <>
+ <div
+ className="collectionStackedTimeline-trim-shade"
+ style={{ width: `${(this.trimStart / this.duration) * 100}%` }}
+ ></div>
+
+ <div
+ className="collectionStackedTimeline-trim-controls"
+ style={{
+ left: `${(this.trimStart / this.duration) * 100}%`,
+ width: `${((this.trimEnd - this.trimStart) / this.duration) * 100
+ }%`,
+ }}
+ >
+ <div
+ className="collectionStackedTimeline-trim-handle"
+ onPointerDown={this.trimLeft}
+ ></div>
+ <div
+ className="collectionStackedTimeline-trim-handle"
+ onPointerDown={this.trimRight}
+ ></div>
+ </div>
+
+ <div
+ className="collectionStackedTimeline-trim-shade"
+ style={{
+ left: `${(this.trimEnd / this.duration) * 100}%`,
+ width: `${((this.duration - this.trimEnd) / this.duration) * 100
+ }%`,
+ }}
+ ></div>
+ </>
+ )}
+ </div>
+ );
}
}
@@ -335,6 +662,8 @@ interface StackedTimelineAnchorProps {
currentTimecode: () => number;
isSelected: (outsideReaction?: boolean) => boolean;
stackedTimeline: CollectionStackedTimeline;
+ trimStart: number;
+ trimEnd: number;
}
@observer
class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
@@ -345,22 +674,41 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
this._lastTimecode = this.props.currentTimecode();
}
componentDidMount() {
- this._disposer = reaction(() => this.props.currentTimecode(),
+ this._disposer = reaction(
+ () => this.props.currentTimecode(),
(time) => {
- const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null);
- const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
- if (!LightboxView.LightboxDoc
+ const dictationDoc = Cast(
+ this.props.layoutDoc["data-dictation"],
+ Doc,
+ null
+ );
+ const isDictation =
+ dictationDoc &&
+ DocListCast(this.props.mark.links).some(
+ (link) =>
+ Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc
+ );
+ if (
+ !LightboxView.LightboxDoc &&
// bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront.
// for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
/*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
- && DocListCast(this.props.mark.links).length &&
+ DocListCast(this.props.mark.links).length &&
time > NumCast(this.props.mark[this.props.startTag]) &&
time < NumCast(this.props.mark[this.props.endTag]) &&
- this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) {
- LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true);
+ this._lastTimecode < NumCast(this.props.mark[this.props.startTag])
+ ) {
+ LinkManager.FollowLink(
+ undefined,
+ this.props.mark,
+ this.props as any as DocumentViewProps,
+ false,
+ true
+ );
}
this._lastTimecode = time;
- });
+ }
+ );
}
componentWillUnmount() {
this._disposer?.();
@@ -373,57 +721,134 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
return this.props.toTimeline(e.clientX - rect.x, rect.width);
};
const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
- const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
- if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
- else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
+ const timelineOnly =
+ Cast(anchor[this.props.startTag], "number", null) !== undefined;
+ if (timelineOnly)
+ Doc.SetInPlace(
+ anchor,
+ left ? this.props.startTag : this.props.endTag,
+ time,
+ true
+ );
+ else
+ left
+ ? (anchor._timecodeToShow = time)
+ : (anchor._timecodeToHide = time);
return false;
};
- setupMoveUpEvents(this, e,
+ setupMoveUpEvents(
+ this,
+ e,
(e) => changeAnchor(anchor, left, newTime(e)),
(e) => {
this.props.setTime(newTime(e));
this.props._timeline?.releasePointerCapture(e.pointerId);
},
- emptyFunction);
+ emptyFunction
+ );
+ };
+
+ @action
+ computeTitle = () => {
+ const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart;
+ const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart;
+ return `#${formatTime(start)}-${formatTime(end)}`
}
- renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+
+ renderInner = computedFn(function (
+ this: StackedTimelineAnchor,
+ mark: Doc,
+ script: undefined | (() => ScriptField),
+ doublescript: undefined | (() => ScriptField),
+ x: number,
+ y: number,
+ width: number,
+ height: number
+ ) {
const anchor = observable({ view: undefined as any });
- const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => {
+ const focusFunc = (
+ doc: Doc,
+ willZoom?: boolean,
+ scale?: number,
+ afterFocus?: DocAfterFocusFunc,
+ docTransform?: Transform
+ ) => {
this.props.playLink(mark);
this.props.focus(doc, { willZoom, scale, afterFocus, docTransform });
};
return {
- anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- ref={action((r: DocumentView | null) => anchor.view = r)}
- Document={mark}
- DataDoc={undefined}
- renderDepth={this.props.renderDepth + 1}
- LayoutTemplate={undefined}
- LayoutTemplateString={LabelBox.LayoutString("data")}
- isDocumentActive={returnFalse}
- PanelWidth={() => width}
- PanelHeight={() => height}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)}
- focus={focusFunc}
- rootSelected={returnFalse}
- onClick={script}
- onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
- ignoreAutoHeight={false}
- hideResizeHandles={true}
- bringToFront={emptyFunction}
- scriptContext={this.props.stackedTimeline} />
+ anchor,
+ view: (
+ <DocumentView
+ key="view"
+ {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ ref={action((r: DocumentView | null) => (anchor.view = r))}
+ Document={mark}
+ DataDoc={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())}
+ isDocumentActive={returnFalse}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
+ ScreenToLocalTransform={() =>
+ this.props.ScreenToLocalTransform().translate(-x, -y)
+ }
+ focus={focusFunc}
+ rootSelected={returnFalse}
+ onClick={script}
+ onDoubleClick={
+ this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript
+ }
+ ignoreAutoHeight={false}
+ hideResizeHandles={true}
+ bringToFront={emptyFunction}
+ scriptContext={this.props.stackedTimeline}
+ />
+ ),
};
});
+
render() {
- const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height);
- return <>
- {inner.view}
- {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
- <>
- <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} />
- <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} />
- </>}
- </>;
+ const inner = this.renderInner(
+ this.props.mark,
+ this.props.rangeClickScript,
+ this.props.rangePlayScript,
+ this.props.left,
+ this.props.top,
+ this.props.width,
+ this.props.height
+ );
+ return (
+ <>
+ {inner.view}
+ {!inner.anchor.view ||
+ !SelectionManager.IsSelected(inner.anchor.view) ? null : (
+ <>
+ <div
+ key="left"
+ className="collectionStackedTimeline-left-resizer"
+ onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)}
+ />
+ <div
+ key="right"
+ className="collectionStackedTimeline-resizer"
+ onPointerDown={(e) =>
+ this.onAnchorDown(e, this.props.mark, false)
+ }
+ />
+ </>
+ )}
+ </>
+ );
}
}
-Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file
+Scripting.addGlobal(function formatToTime(time: number): any {
+ return formatTime(time);
+});
+Scripting.addGlobal(function min(num1: number, num2: number): number {
+ return Math.min(num1, num2);
+});
+Scripting.addGlobal(function max(num1: number, num2: number): number {
+ return Math.max(num1, num2);
+}); \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 3fcb024df..ac2b19fd6 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,188 +1,204 @@
+@import "../global/globalCssVariables.scss";
+
+
.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;
height: 100%;
- position: inherit;
- display: flex;
- position: relative;
- cursor: default;
- .audiobox-buttons {
- display: flex;
- width: 100%;
- align-items: center;
- height: 100%;
-
- .audiobox-dictation {
- position: relative;
- width: 30px;
- height: 100%;
- align-items: center;
- display: inherit;
- background: dimgray;
- left: 0px;
- &:hover {
- color: white;
- cursor: pointer;
- }
- }
+ .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,
+ .audiobox-control-interactive {
+ top: 0;
+ max-height: 32px;
+ width: 100%;
+ display: inline-block;
+ pointer-events: none;
+ }
+
+ .audiobox-control-interactive {
+ pointer-events: all;
+ }
- .audiobox-control-interactive {
- pointer-events: all;
+ .audiobox-record-interactive,
+ .audiobox-record {
+ pointer-events: all;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ }
+
+ .audiobox-record {
+ pointer-events: none;
+ }
+
+ .recording {
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+ background-color: $medium-blue;
+
+ .time {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ font-size: $large-header;
+ text-align: center;
+ top: 5;
}
- .audiobox-record-interactive,
- .audiobox-record {
- pointer-events: all;
- width: 100%;
- height: 100%;
- position: relative;
+ .buttons {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ width: 25px;
+ padding: 5px;
+ color: $dark-gray;
+ &:hover {
+ color: $black;
+ }
}
+ }
- .audiobox-record {
- pointer-events: none;
+ .audiobox-controls {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ background: $dark-gray;
+
+ .audiobox-dictation {
+ position: absolute;
+ width: 40px;
+ height: 100%;
+ align-items: center;
+ display: inherit;
+ background: $medium-gray;
+ left: 0px;
}
- .recording {
+ .audiobox-player {
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .audiobox-buttons {
+ position: relative;
margin-top: auto;
margin-bottom: auto;
- width: 100%;
- height: 100%;
- position: relative;
- padding-right: 5px;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background-color: $dark-gray;
+ color: $white;
display: flex;
- background-color: red;
-
- .time {
- position: relative;
- height: 100%;
- width: 100%;
- font-size: 20;
- text-align: center;
- top: 5;
+ align-items: center;
+ justify-content: center;
+ left: 5px;
+ &:hover {
+ background-color: $black;
}
- .buttons {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 25px;
- padding: 5px;
- &:hover{
- background-color: crimson;
- }
+ svg {
+ width: 100%;
+ position: absolute;
+ border-width: "thin";
+ border-color: "white";
}
- }
+ }
- .audiobox-controls {
- width: 100%;
- height: 100%;
+ .audiobox-dictation {
position: relative;
- display: flex;
- padding-left: 2px;
- background: black;
-
- .audiobox-dictation {
- position: absolute;
- width: 30px;
- height: 100%;
- align-items: center;
- display: inherit;
- background: dimgray;
- left: 0px;
- }
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ align-items: center;
+ display: inherit;
+ background: $medium-gray;
+ }
- .audiobox-player {
- margin-top: auto;
- margin-bottom: auto;
- width: 100%;
- position: relative;
- padding-right: 5px;
- display: flex;
-
- .audiobox-playhead {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- margin-right: 2px;
- height: 25px;
- padding: 2px;
- border-radius: 50%;
- background-color: black;
- color: white;
- &:hover {
- background-color: grey;
- color: lightgrey;
- }
- }
-
- .audiobox-dictation {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 25px;
- padding: 2px;
- align-items: center;
- display: inherit;
- background: dimgray;
- }
-
- .audiobox-timeline {
- position: absolute;
- width: 100%;
- border: gray solid 1px;
- border-radius: 3px;
- z-index: 1000;
- overflow: hidden;
- }
-
- .audioBox-total-time,
- .audioBox-current-time {
- position: absolute;
- font-size: 8;
- top: 100%;
- color: white;
- }
- .audioBox-current-time {
- left: 30px;
- }
-
- .audioBox-total-time {
- right: 2px;
- }
- }
+ .audiobox-timeline {
+ position: absolute;
+ width: 100%;
+ z-index: 1000;
+ overflow: hidden;
+ border-right: 5px solid black;
+ }
+
+ .audioBox-total-time,
+ .audioBox-current-time {
+ position: absolute;
+ font-size: $small-text;
+ top: 100%;
+ color: $white;
+ }
+ .audioBox-current-time {
+ left: 42px;
+ }
+
+ .audioBox-total-time {
+ right: 2px;
+ }
}
+ }
}
-
@media only screen and (max-device-width: 480px) {
- .audiobox-dictation {
- font-size: 5em;
- display: flex;
- width: 100;
- justify-content: center;
- flex-direction: column;
- align-items: center;
- }
-
- .audiobox-container .audiobox-record,
- .audiobox-container-interactive .audiobox-record {
- font-size: 3em;
- }
-
- .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
- .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
- .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead {
- width: 70px;
- }
-} \ No newline at end of file
+ .audiobox-dictation {
+ font-size: 5em;
+ display: flex;
+ width: 100;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .audiobox-container .audiobox-record,
+ .audiobox-container-interactive .audiobox-record {
+ font-size: 3em;
+ }
+
+ .audiobox-container .audiobox-controls .audiobox-player .audiobox-buttons,
+ .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
+ .audiobox-container-interactive
+ .audiobox-controls
+ .audiobox-player
+ .audiobox-buttons {
+ width: 70px;
+ }
+}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 60c655958..a3f03fc4b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,6 +1,13 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import {
+ action,
+ computed,
+ IReactionDisposer,
+ observable,
+ reaction,
+ runInAction,
+} from "mobx";
import { observer } from "mobx-react";
import { DateField } from "../../../fields/DateField";
import { Doc, DocListCast, Opt } from "../../../fields/Doc";
@@ -17,23 +24,34 @@ import { SnappingManager } from "../../util/SnappingManager";
import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
+import {
+ ViewBoxAnnotatableComponent,
+ ViewBoxAnnotatableProps,
+} from "../DocComponent";
import "./AudioBox.scss";
-import { FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps } from "./FieldView";
import { LinkDocPreview } from "./LinkDocPreview";
+import { faLessThan } from "@fortawesome/free-solid-svg-icons";
+import { Colors } from "../global/globalEnums";
+
declare class MediaRecorder {
- constructor(e: any); // whatever MediaRecorder has
+ constructor(e: any); // whatever MediaRecorder has
}
type AudioDocument = makeInterface<[typeof documentSchema]>;
const AudioDocument = makeInterface(documentSchema);
@observer
-export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, AudioDocument>(AudioDocument) {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
+export class AudioBox extends ViewBoxAnnotatableComponent<
+ ViewBoxAnnotatableProps & FieldViewProps,
+ AudioDocument
+>(AudioDocument) {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(AudioBox, fieldKey);
+ }
public static Enabled = false;
- static playheadWidth = 30; // width of playhead
- static heightPercent = 80; // height of timeline in percent of height of audioBox.
+ static playheadWidth = 40; // width of playhead
+ static heightPercent = 75; // height of timeline in percent of height of audioBox.
static Instance: AudioBox;
_disposers: { [name: string]: IReactionDisposer } = {};
@@ -47,35 +65,82 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_stream: MediaStream | undefined;
_start: number = 0;
_play: any = null;
+ _ended: boolean = false;
@observable static _scrubTime = 0;
@observable _markerEnd: number = 0;
@observable _position: number = 0;
@observable _waveHeight: Opt<number> = this.layoutDoc._height;
@observable _paused: boolean = false;
- @computed get mediaState(): undefined | "pendingRecording" | "recording" | "paused" | "playing" { return this.dataDoc.mediaState as (undefined | "pendingRecording" | "recording" | "paused" | "playing"); }
- set mediaState(value) { this.dataDoc.mediaState = value; }
- public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; });
- @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
- @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); }
- @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); }
- @computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time
- @computed get heightPercent() { return AudioBox.heightPercent; }
+ @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 mediaState():
+ | undefined
+ | "pendingRecording"
+ | "recording"
+ | "paused"
+ | "playing" {
+ return this.dataDoc.mediaState as
+ | undefined
+ | "pendingRecording"
+ | "recording"
+ | "paused"
+ | "playing";
+ }
+ set mediaState(value) {
+ this.dataDoc.mediaState = value;
+ }
+ public static SetScrubTime = action((timeInMillisFrom1970: number) => {
+ AudioBox._scrubTime = 0;
+ AudioBox._scrubTime = timeInMillisFrom1970;
+ });
+ @computed get recordingStart() {
+ return Cast(
+ this.dataDoc[this.props.fieldKey + "-recordingStart"],
+ DateField
+ )?.date.getTime();
+ }
+ @computed get duration() {
+ 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]);
+ }
+ @computed get links() {
+ return DocListCast(this.dataDoc.links);
+ }
+ @computed get pauseTime() {
+ return this._pauseEnd - this._pauseStart;
+ } // total time paused to update the correct time
+ @computed get heightPercent() {
+ return AudioBox.heightPercent;
+ }
constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
super(props);
AudioBox.Instance = this;
if (this.duration === undefined) {
- runInAction(() => this.Document[this.fieldKey + "-duration"] = this.Document.duration);
+ runInAction(
+ () =>
+ (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
+ );
}
}
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- const linkTime = this._stackedTimeline.current?.anchorStart(la2) || this._stackedTimeline.current?.anchorStart(la1) || 0;
+ const linkTime =
+ this._stackedTimeline.current?.anchorStart(la2) ||
+ this._stackedTimeline.current?.anchorStart(la1) ||
+ 0;
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
@@ -84,16 +149,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
getAnchor = () => {
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey,
- "_timecodeToShow" /* audioStart */, "_timecodeToHide" /* audioEnd */, this._ele?.currentTime ||
- Cast(this.props.Document._currentTimecode, "number", null) || (this.mediaState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined))
- || this.rootDoc;
- }
+ return (
+ CollectionStackedTimeline.createAnchor(
+ this.rootDoc,
+ this.dataDoc,
+ this.annotationKey,
+ "_timecodeToShow" /* audioStart */,
+ "_timecodeToHide" /* audioEnd */,
+ this._ele?.currentTime ||
+ Cast(this.props.Document._currentTimecode, "number", null) ||
+ (this.mediaState === "recording"
+ ? (Date.now() - (this.recordingStart || 0)) / 1000
+ : undefined)
+ ) || this.rootDoc
+ );
+ };
componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
+ Object.values(this._disposers).forEach((disposer) => disposer?.());
const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
}
@action
@@ -102,41 +177,70 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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();
+
this._disposers.triggerAudio = reaction(
- () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined,
- start => start !== undefined && setTimeout(() => {
- this.playFrom(start);
+ () =>
+ !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1
+ ? NumCast(this.Document._triggerAudio, null)
+ : undefined,
+ (start) =>
+ start !== undefined &&
setTimeout(() => {
- this.Document._currentTimecode = start;
- this.Document._triggerAudio = undefined;
- }, 10);
- }), // wait for mainCont and try again to play
+ this.playFrom(start);
+ setTimeout(() => {
+ this.Document._currentTimecode = start;
+ this.Document._triggerAudio = undefined;
+ }, 10);
+ }), // wait for mainCont and try again to play
{ fireImmediately: true }
);
this._disposers.audioStop = reaction(
- () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo ? Cast(this.Document._audioStop, "number", null) : undefined,
- audioStop => audioStop !== undefined && setTimeout(() => {
- this.Pause();
- setTimeout(() => this.Document._audioStop = undefined, 10);
- }), // wait for mainCont and try again to play
+ () =>
+ this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
+ ? Cast(this.Document._audioStop, "number", null)
+ : undefined,
+ (audioStop) =>
+ audioStop !== undefined &&
+ setTimeout(() => {
+ this.Pause();
+ setTimeout(() => (this.Document._audioStop = undefined), 10);
+ }), // wait for mainCont and try again to play
{ fireImmediately: true }
);
}
// for updating the timecode
+ @action
timecodeChanged = () => {
const htmlEle = this._ele;
if (this.mediaState !== "recording" && htmlEle) {
- htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration);
- this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => {
- if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) {
- Doc.linkFollowHighlight(la1);
- }
- });
+ 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 }) => {
+ if (
+ linkTime > NumCast(this.layoutDoc._currentTimecode) &&
+ linkTime < htmlEle.currentTime
+ ) {
+ Doc.linkFollowHighlight(la1);
+ }
+ });
this.layoutDoc._currentTimecode = htmlEle.currentTime;
+
}
- }
+ };
// pause play back
Pause = action(() => {
@@ -146,12 +250,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// play audio for documents created during recording
playFromTime = (absoluteTime: number) => {
- this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000);
- }
+ this.recordingStart &&
+ this.playFrom((absoluteTime - this.recordingStart) / 1000);
+ };
// play back the audio from time
@action
- playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
+ 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);
@@ -162,18 +267,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
} else {
this.Pause();
}
- } else if (seekTimeInSeconds <= this._ele.duration) {
- this._ele.currentTime = seekTimeInSeconds;
+ } 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");
+ runInAction(() => (this.mediaState = "playing"));
if (endTime !== this.duration) {
- this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration
+ this._play = setTimeout(
+ () => {
+ this._ended = fullPlay ? true : this._ended;
+ this.Pause()
+ },
+ (end - start) * 1000
+ ); // use setTimeout to play a specific duration
}
} else {
this.Pause();
}
}
- }
+ };
// update the recording time
updateRecordTime = () => {
@@ -182,16 +295,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (this._paused) {
this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
} else {
- this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ this.layoutDoc._currentTimecode =
+ (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
}
}
- }
+ };
// starts recording
recordAudioAnnotation = async () => {
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
- this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+ this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(
+ new Date()
+ );
DocUtils.ActiveRecordings.push(this);
this._recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
@@ -200,30 +316,58 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
};
this._recordStart = new Date().getTime();
- runInAction(() => this.mediaState = "recording");
+ runInAction(() => (this.mediaState = "recording"));
setTimeout(this.updateRecordTime, 0);
this._recorder.start();
setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
- }
+ };
// context menu
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
- ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
- }
+ funcs.push({
+ description:
+ (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors",
+ event: () => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
+ icon: "expand-arrows-alt",
+ });
+ funcs.push({
+ description:
+ (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") +
+ " play when link is selected",
+ event: () =>
+ (this.layoutDoc.dontAutoPlayFollowedLinks =
+ !this.layoutDoc.dontAutoPlayFollowedLinks),
+ icon: "expand-arrows-alt",
+ });
+ funcs.push({
+ description:
+ (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") +
+ " anchors onClick",
+ event: () =>
+ (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
+ icon: "expand-arrows-alt",
+ });
+ ContextMenu.Instance?.addItem({
+ description: "Options...",
+ subitems: funcs,
+ icon: "asterisk",
+ });
+ };
// stops the recording
stopRecording = action(() => {
this._recorder.stop();
this._recorder = undefined;
- this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ this.dataDoc[this.fieldKey + "-duration"] =
+ (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
this.mediaState = "paused";
+ this._trimEnd = this.duration;
+ this.layoutDoc.clipStart = 0;
+ this.layoutDoc.clipEnd = this.duration;
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
});
// button for starting and stopping the recording
@@ -232,45 +376,71 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
e.stopPropagation();
}
- }
+ };
// for play button
Play = (e?: any) => {
- this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+ let start;
+ if (this._ended || this._ele!.currentTime == this.duration) {
+ start = this._trimStart;
+ this._ended = false;
+ }
+ else {
+ start = this._ele!.currentTime;
+ }
+
+ this.playFrom(start, this._trimEnd, true);
e?.stopPropagation?.();
- }
+ };
// creates a text document for dictation
onFile = (e: any) => {
- const newDoc = CurrentUserUtils.GetNewTextDoc("", NumCast(this.props.Document.x), NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10,
- NumCast(this.props.Document._width), 2 * NumCast(this.props.Document._height));
+ const newDoc = CurrentUserUtils.GetNewTextDoc(
+ "",
+ NumCast(this.props.Document.x),
+ NumCast(this.props.Document.y) +
+ NumCast(this.props.Document._height) +
+ 10,
+ NumCast(this.props.Document._width),
+ 2 * NumCast(this.props.Document._height)
+ );
Doc.GetProto(newDoc).recordingSource = this.dataDoc;
- Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
- Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
+ Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(
+ `self.recordingSource["${this.props.fieldKey}-recordingStart"]`
+ );
+ Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(
+ "self.recordingSource.mediaState"
+ );
this.props.addDocument?.(newDoc);
e.stopPropagation();
- }
+ };
// ref for updating time
setRef = (e: HTMLAudioElement | null) => {
e?.addEventListener("timeupdate", this.timecodeChanged);
e?.addEventListener("ended", this.Pause);
this._ele = e;
- }
+ };
// returns the path of the audio file
@computed get path() {
const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
- const path = (field instanceof AudioField) ? field.url.href : "";
+ const path = field instanceof AudioField ? field.url.href : "";
return path === nullAudio ? "" : path;
}
// returns the html audio element
@computed get audio() {
- return <audio ref={this.setRef} className={`audiobox-control${this.isContentActive() ? "-interactive" : ""}`}>
- <source src={this.path} type="audio/mpeg" />
- Not supported.
- </audio>;
+ return (
+ <audio
+ ref={this.setRef}
+ className={`audiobox-control${this.isContentActive() ? "-interactive" : ""
+ }`}
+ >
+ <source src={this.path} type="audio/mpeg" />
+ Not supported.
+ </audio>
+ );
}
// pause the time during recording phase
@@ -280,7 +450,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._paused = true;
this._recorder.pause();
e.stopPropagation();
- }
+ };
// continue the recording
@action
@@ -289,104 +459,259 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._paused = false;
this._recorder.resume();
e.stopPropagation();
- }
+ };
playing = () => this.mediaState === "playing";
playLink = (link: Doc) => {
const stack = this._stackedTimeline.current;
if (link.annotationOn === this.rootDoc) {
- if (!this.layoutDoc.dontAutoPlayFollowedLinks) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
- else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0);
+ if (!this.layoutDoc.dontAutoPlayFollowedLinks)
+ this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
+ else
+ this._ele!.currentTime = this.layoutDoc._currentTimecode =
+ stack?.anchorStart(link) || 0;
+ } else {
+ this.links
+ .filter((l) => l.anchor1 === link || l.anchor2 === link)
+ .forEach((l) => {
+ const { la1, la2 } = this.getLinkData(l);
+ const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
+ const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
+ if (startTime !== undefined) {
+ if (!this.layoutDoc.dontAutoPlayFollowedLinks)
+ endTime
+ ? this.playFrom(startTime, endTime)
+ : this.playFrom(startTime);
+ else
+ this._ele!.currentTime = this.layoutDoc._currentTimecode =
+ startTime;
+ }
+ });
}
- else {
- this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => {
- const { la1, la2 } = this.getLinkData(l);
- const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
- const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
- if (startTime !== undefined) {
- if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
- else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
- }
- });
+ };
+
+ // shows trim controls
+ @action
+ startTrim = () => {
+ if (!this.duration) {
+ this.timecodeChanged();
+ }
+ if (this.mediaState === "playing") {
+ this.Pause();
+ }
+ this._trimming = true;
+ };
+
+ // hides trim controls and displays new clip
+ @action
+ finishTrim = () => {
+ 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));
+ };
+
+ @action
+ setStartTrim = (newStart: number) => {
+ this._trimStart = newStart;
+ }
+
+ @action
+ setEndTrim = (newEnd: number) => {
+ this._trimEnd = newEnd;
}
isActiveChild = () => this._isAnyChildContentActive;
- timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged(runInAction(() => this._isAnyChildContentActive = isActive));
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-AudioBox.playheadWidth, -(100 - this.heightPercent) / 200 * this.props.PanelHeight());
- setAnchorTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time;
- timelineHeight = () => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
+ timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
+ this.props.whenChildContentsActiveChanged(
+ runInAction(() => (this._isAnyChildContentActive = isActive))
+ );
+ timelineScreenToLocal = () =>
+ this.props
+ .ScreenToLocalTransform()
+ .translate(
+ -AudioBox.playheadWidth,
+ (-(100 - this.heightPercent) / 200) * this.props.PanelHeight()
+ );
+ setAnchorTime = (time: number) => {
+ (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
+ }
+
+ timelineHeight = () =>
+ (((this.props.PanelHeight() * this.heightPercent) / 100) *
+ this.heightPercent) /
+ 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
@computed get renderTimeline() {
- return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
- fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + "-dictation"}
- mediaPath={this.path}
- renderDepth={this.props.renderDepth + 1}
- startTag={"_timecodeToShow" /* audioStart */}
- endTag={"_timecodeToHide" /* audioEnd */}
- focus={DocUtils.DefaultFocus}
- bringToFront={emptyFunction}
- CollectionView={undefined}
- duration={this.duration}
- playFrom={this.playFrom}
- setTime={this.setAnchorTime}
- playing={this.playing}
- whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- ScreenToLocalTransform={this.timelineScreenToLocal}
- Play={this.Play}
- Pause={this.Pause}
- isContentActive={this.isContentActive}
- playLink={this.playLink}
- PanelWidth={this.timelineWidth}
- PanelHeight={this.timelineHeight}
- />;
+ return (
+ <CollectionStackedTimeline
+ ref={this._stackedTimeline}
+ {...this.props}
+ fieldKey={this.annotationKey}
+ dictationKey={this.fieldKey + "-dictation"}
+ mediaPath={this.path}
+ renderDepth={this.props.renderDepth + 1}
+ startTag={"_timecodeToShow" /* audioStart */}
+ endTag={"_timecodeToHide" /* audioEnd */}
+ focus={DocUtils.DefaultFocus}
+ bringToFront={emptyFunction}
+ CollectionView={undefined}
+ duration={this.duration}
+ playFrom={this.playFrom}
+ setTime={this.setAnchorTime}
+ playing={this.playing}
+ whenChildContentsActiveChanged={
+ this.timelineWhenChildContentsActiveChanged
+ }
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.timelineScreenToLocal}
+ Play={this.Play}
+ Pause={this.Pause}
+ isContentActive={this.isContentActive}
+ playLink={this.playLink}
+ PanelWidth={this.timelineWidth}
+ PanelHeight={this.timelineHeight}
+ trimming={this._trimming}
+ trimStart={this._trimStart}
+ trimEnd={this._trimEnd}
+ trimDuration={this.trimDuration}
+ setStartTrim={this.setStartTrim}
+ setEndTrim={this.setEndTrim}
+ />
+ );
}
render() {
- const interactive = SnappingManager.GetIsDragging() || this.isContentActive() ? "-interactive" : "";
- return <div className="audiobox-container"
- onContextMenu={this.specificContextMenu}
- onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined}
- style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined }}>
- {!this.path ?
- <div className="audiobox-buttons">
- <div className="audiobox-dictation" onClick={this.onFile}>
- <FontAwesomeIcon style={{ width: "30px", background: !this.layoutDoc.dontAutoPlayFollowedLinks ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ const interactive =
+ SnappingManager.GetIsDragging() || this.isContentActive()
+ ? "-interactive"
+ : "";
+ return (
+ <div
+ className="audiobox-container"
+ onContextMenu={this.specificContextMenu}
+ onClick={
+ !this.path && !this._recorder ? this.recordAudioAnnotation : undefined
+ }
+ style={{
+ pointerEvents:
+ this.props.layerProvider?.(this.layoutDoc) === false
+ ? "none"
+ : undefined,
+ }}
+ >
+ {!this.path ? (
+ <div className="audiobox-buttons">
+ <div className="audiobox-dictation" onClick={this.onFile}>
+ <FontAwesomeIcon
+ style={{
+ width: "30px",
+ background: !this.layoutDoc.dontAutoPlayFollowedLinks
+ ? Colors.LIGHT_BLUE
+ : "rgba(0,0,0,0)",
+ }}
+ icon="file-alt"
+ size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+ />
+ </div>
+ {this.mediaState === "recording" || this.mediaState === "paused" ? (
+ <div className="recording" onClick={(e) => e.stopPropagation()}>
+ <div className="recording-buttons" onClick={this.recordClick}>
+ <FontAwesomeIcon
+ icon={"stop"}
+ size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+ />
+ </div>
+ <div
+ className="recording-buttons"
+ onClick={this._paused ? this.recordPlay : this.recordPause}
+ >
+ <FontAwesomeIcon
+ icon={this._paused ? "play" : "pause"}
+ size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+ />
+ </div>
+ <div className="time">
+ {formatTime(
+ Math.round(NumCast(this.layoutDoc._currentTimecode))
+ )}
+ </div>
+ </div>
+ ) : (
+ <button
+ className={`audiobox-record${interactive}`}
+ style={{ backgroundColor: Colors.DARK_GRAY }}
+ >
+ RECORD
+ </button>
+ )}
</div>
- {this.mediaState === "recording" || this.mediaState === "paused" ?
- <div className="recording" onClick={e => e.stopPropagation()}>
- <div className="buttons" onClick={this.recordClick}>
- <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ ) : (
+ <div
+ className="audiobox-controls"
+ style={{
+ pointerEvents:
+ this._isAnyChildContentActive || this.isContentActive()
+ ? "all"
+ : "none",
+ }}
+ >
+ <div className="audiobox-dictation" />
+ <div
+ className="audiobox-player"
+ style={{ height: `${AudioBox.heightPercent}%` }}
+ >
+ <div
+ className="audiobox-buttons"
+ title={this.mediaState === "paused" ? "play" : "pause"}
+ onClick={this.mediaState === "paused" ? this.Play : this.Pause}
+ >
+ {" "}
+ <FontAwesomeIcon
+ icon={this.mediaState === "paused" ? "play" : "pause"}
+ size={"1x"}
+ />
</div>
- <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}>
- <FontAwesomeIcon icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ <div
+ className="audiobox-buttons"
+ title={this._trimming ? "finish" : "trim"}
+ onClick={this._trimming ? this.finishTrim : this.startTrim}
+ >
+ <FontAwesomeIcon
+ icon={this._trimming ? "check" : "cut"}
+ size={"1x"}
+ />
+ </div>
+ <div
+ className="audiobox-timeline"
+ style={{
+ top: 0,
+ height: `100%`,
+ left: AudioBox.playheadWidth,
+ width: `calc(100% - ${AudioBox.playheadWidth}px)`,
+ background: "white",
+ }}
+ >
+ {this.renderTimeline}
+ </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)))}
+ </div>
+ <div className="audioBox-total-time">
+ {this._trimming || !this._trimEnd ?
+ formatTime(Math.round(NumCast(this.duration)))
+ : formatTime(Math.round(NumCast(this.trimDuration)))}
</div>
- <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div>
- </div>
- :
- <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}>
- RECORD
- </button>}
- </div> :
- <div className="audiobox-controls" style={{ pointerEvents: this._isAnyChildContentActive || this.isContentActive() ? "all" : "none" }} >
- <div className="audiobox-dictation" />
- <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} >
- <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.mediaState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.mediaState === "paused" ? "play" : "pause"} size={"1x"} /></div>
- <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
- {this.renderTimeline}
- </div>
- {this.audio}
- <div className="audioBox-current-time">
- {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
- </div>
- <div className="audioBox-total-time">
- {formatTime(Math.round(this.duration))}
</div>
</div>
- </div>
- }
- </div>;
+ )}
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ee3266647..2bd79df64 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -91,6 +91,7 @@ export interface DocComponentView {
setFocus?: () => void;
fieldKey?: string;
annotationKey?: string;
+ getTitle?: () => string;
}
export interface DocumentViewSharedProps {
renderDepth: number;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 6a7793ff0..8d665b8a6 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -20,11 +20,26 @@ const LabelSchema = createSchema({});
type LabelDocument = makeInterface<[typeof LabelSchema, typeof documentSchema]>;
const LabelDocument = makeInterface(LabelSchema, documentSchema);
+export interface LabelBoxProps {
+ label?: string
+}
+
@observer
-export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument>(LabelDocument) {
+export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps), LabelDocument>(LabelDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); }
+ public static LayoutStringWithTitle(fieldType: { name: string }, fieldStr: string, label: string) {
+ return `<${fieldType.name} fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
+ }
private dropDisposer?: DragManager.DragDropDisposer;
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+
+ getTitle() {
+ return this.props.label || "";
+ }
+
protected createDropTarget = (ele: HTMLDivElement) => {
this.dropDisposer?.();
if (ele) {
@@ -65,8 +80,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
render() {
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
const missingParams = params?.filter(p => !this.paramsDoc[p]);
- params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
- const label = typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
+ params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
+ const label = this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
return (
<div className="labelBox-outerDiv"
onMouseLeave={action(() => this._mouseOver = false)}