From bafc47fe100002e37c7abcada3dc44f3bfb66f62 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 23 Sep 2021 11:16:53 -0400
Subject: a bunch of fixes to audio timelines to support undoing clips,
 simplifying audioWaveform and having it recompute when a clip is made.

---
 src/client/views/AudioWaveform.tsx | 117 ++++++++++++++-----------------------
 1 file changed, 45 insertions(+), 72 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 8f3b7c2cd..1e676e1f0 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,7 +12,8 @@ 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;
@@ -22,19 +23,28 @@ export interface AudioWaveformProps {
 @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 NumCast(this.props.layoutDoc.clipStart); }
+    @computed get clipEnd() { return NumCast(this.props.layoutDoc.clipEnd, this.props.rawDuration); }
+    @computed get audioBuckets() { return Cast(this.props.layoutDoc.audioBuckets, listSpec("number"), []); }
+    @computed get audioBucketRange() { return Cast(this.props.layoutDoc.audioBucketRange, listSpec("number"), [-1, -1]); }
+    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(() => [this.clipStart, this.clipEnd, this.audioBuckets.length, ...this.audioBucketRange],
+            (range) => {
+                if (range[2] !== AudioWaveform.NUMBER_OF_BUCKETS || range[3] !== range[0] || range[4] !== range[1]) {
+                    this.props.layoutDoc.audioBucketRange = new List<number>([range[0], range[1]]); // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nearly the same time.
+                    this.props.layoutDoc.audioBuckets = new List<number>(numberRange(AudioWaveform.NUMBER_OF_BUCKETS));
+                    setTimeout(this.createWaveformBuckets);
+                }
+            }, { fireImmediately: true });
+
     }
 
     // decodes the audio file into peaks for generating the waveform
@@ -45,81 +55,44 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                 context.decodeAudioData(
                     response.data,
                     action((buffer) => {
-                        const decodedAudioData = buffer.getChannelData(0);
+                        const rawDecodedAudioData = buffer.getChannelData(0);
+                        const startInd = this.clipStart / this.props.rawDuration;
+                        const endInd = this.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.audioBucketRange = new List<number>([this.clipStart, this.clipEnd]);
+                        this.props.layoutDoc.audioBuckets = 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>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 9675e948be8a7ea2d86c8ca68a89c09452ece0e7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 23 Sep 2021 23:22:39 -0400
Subject: added code for editing the original waveform, not a clip when
 trimming is activated.

---
 src/client/views/AudioWaveform.tsx                 | 25 ++++++++++++----------
 .../collections/CollectionStackedTimeline.tsx      |  2 ++
 src/client/views/nodes/AudioBox.tsx                | 13 ++++++++---
 3 files changed, 26 insertions(+), 14 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 1e676e1f0..7d83ea3dc 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -17,6 +17,8 @@ export interface AudioWaveformProps {
     mediaPath: string;
     layoutDoc: Doc;
     trimming: boolean;
+    clipStart: number;
+    clipEnd: number;
     PanelHeight: () => number;
 }
 
@@ -28,20 +30,22 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
         return Math.max(50, this.props.PanelHeight());
     }
 
-    @computed get clipStart() { return NumCast(this.props.layoutDoc.clipStart); }
-    @computed get clipEnd() { return NumCast(this.props.layoutDoc.clipEnd, this.props.rawDuration); }
-    @computed get audioBuckets() { return Cast(this.props.layoutDoc.audioBuckets, listSpec("number"), []); }
-    @computed get audioBucketRange() { return Cast(this.props.layoutDoc.audioBucketRange, listSpec("number"), [-1, -1]); }
+    @computed get clipStart() { return this.props.clipStart; }
+    @computed get clipEnd() { return this.props.clipEnd; }
+    audioBucketField = (start: number, end: number) => { return "audioBuckets-" + start.toFixed(2) + "-" + end.toFixed(2); }
+    @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)], listSpec("number"), []); }
     componentWillUnmount() {
         this._disposer?.();
     }
     componentDidMount() {
-        this._disposer = reaction(() => [this.clipStart, this.clipEnd, this.audioBuckets.length, ...this.audioBucketRange],
+        this._disposer = reaction(() => [this.clipStart, this.clipEnd, this.audioBuckets.length],
             (range) => {
-                if (range[2] !== AudioWaveform.NUMBER_OF_BUCKETS || range[3] !== range[0] || range[4] !== range[1]) {
-                    this.props.layoutDoc.audioBucketRange = new List<number>([range[0], range[1]]); // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nearly the same time.
-                    this.props.layoutDoc.audioBuckets = new List<number>(numberRange(AudioWaveform.NUMBER_OF_BUCKETS));
-                    setTimeout(this.createWaveformBuckets);
+                if (range[2] !== AudioWaveform.NUMBER_OF_BUCKETS) {
+                    if (!this.props.layoutDoc[this.audioBucketField(range[0], range[1])]) {
+                        // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time.
+                        this.props.layoutDoc[this.audioBucketField(range[0], range[1])] = new List<number>(numberRange(AudioWaveform.NUMBER_OF_BUCKETS));
+                        setTimeout(this.createWaveformBuckets);
+                    }
                 }
             }, { fireImmediately: true });
 
@@ -74,8 +78,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                                     0
                                 ) / 2
                         );
-                        this.props.layoutDoc.audioBucketRange = new List<number>([this.clipStart, this.clipEnd]);
-                        this.props.layoutDoc.audioBuckets = new List<number>(bucketList);
+                        this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)] = new List<number>(bucketList);
                     })
                 );
             }
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 929bfa055..e00e66295 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -547,6 +547,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
                     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}
                 />
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index f6d6ff440..6a25ffaeb 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -577,9 +577,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                 PanelWidth={this.timelineWidth}
                 PanelHeight={this.timelineHeight}
                 rawDuration={this.rawDuration}
-                clipStart={this.clipStart}
-                clipEnd={this.clipEnd}
-                clipDuration={this.duration}
+
+                // this edits the entire waveform when trimming is activated
+                clipStart={this._trimming ? 0 : this.clipStart}
+                clipEnd={this._trimming ? this.rawDuration : this.clipEnd}
+                clipDuration={this._trimming ? this.rawDuration : this.duration}
+                // this edits just the current waveform clip when trimming is activated
+                // clipStart={this.clipStart}
+                // clipEnd={this.clipEnd}
+                // clipDuration={this.duration}
+
                 trimming={this._trimming}
                 trimStart={this.trimStartFunc}
                 trimEnd={this.trimEndFunc}
-- 
cgit v1.2.3-70-g09d2


From a4b3114f3792c80e20a3f40053ec4724729c1100 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 24 Sep 2021 00:40:30 -0400
Subject: added doubleclick on clip button to switch to editing entire timeline
 vs. single click to edit just the clip. fixed some audio timeline bugs with
 printing out current time and time span when trimming.  cleaned up
 audioWaveform bucket fields.

---
 src/client/views/AudioWaveform.tsx                 | 24 +++----
 .../collections/CollectionStackedTimeline.tsx      | 13 ++--
 src/client/views/nodes/AudioBox.tsx                | 78 ++++++++++++----------
 3 files changed, 56 insertions(+), 59 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 7d83ea3dc..f7b117130 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -32,27 +32,25 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
 
     @computed get clipStart() { return this.props.clipStart; }
     @computed get clipEnd() { return this.props.clipEnd; }
-    audioBucketField = (start: number, end: number) => { return "audioBuckets-" + start.toFixed(2) + "-" + end.toFixed(2); }
+    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() {
-        this._disposer = reaction(() => [this.clipStart, this.clipEnd, this.audioBuckets.length],
-            (range) => {
-                if (range[2] !== AudioWaveform.NUMBER_OF_BUCKETS) {
-                    if (!this.props.layoutDoc[this.audioBucketField(range[0], range[1])]) {
-                        // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time.
-                        this.props.layoutDoc[this.audioBucketField(range[0], range[1])] = new List<number>(numberRange(AudioWaveform.NUMBER_OF_BUCKETS));
-                        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();
@@ -60,8 +58,8 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                     response.data,
                     action((buffer) => {
                         const rawDecodedAudioData = buffer.getChannelData(0);
-                        const startInd = this.clipStart / this.props.rawDuration;
-                        const endInd = this.clipEnd / this.props.rawDuration;
+                        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(
@@ -78,7 +76,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                                     0
                                 ) / 2
                         );
-                        this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)] = new List<number>(bucketList);
+                        this.props.layoutDoc[fieldKey] = new List<number>(bucketList);
                     })
                 );
             }
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index e00e66295..7859d3c3f 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -50,7 +50,6 @@ import { DragManager } from "../../util/DragManager";
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
 export type CollectionStackedTimelineProps = {
-    clipDuration: number;
     Play: () => void;
     Pause: () => void;
     playLink: (linkDoc: Doc) => void;
@@ -65,6 +64,7 @@ export type CollectionStackedTimelineProps = {
     trimming: boolean;
     clipStart: number;
     clipEnd: number;
+    clipDuration: number;
     trimStart: () => number;
     trimEnd: () => number;
     trimDuration: () => number;
@@ -165,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;
@@ -282,12 +282,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                 this.props.isSelected(true) || this.props.isContentActive(),
                 undefined,
                 () => {
-                    !wasPlaying &&
-                        (this.props.trimming && this.clipDuration ?
-                            this.props.setTime(((clientX - rect.x) / rect.width) * this.clipDuration)
-                            :
-                            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);
                 }
             );
         }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6a25ffaeb..bfc15cea8 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -16,7 +16,7 @@ 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, OmitKeys } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, setupMoveUpEvents, returnFalse } from "../../../Utils";
 import { DocUtils } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -48,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;
@@ -74,14 +75,19 @@ 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 _trimming: number = AudioBox.ScopeNone;
     @observable _trimStart: number = NumCast(this.layoutDoc.clipStart);
     @observable _trimEnd: number | undefined = Cast(this.layoutDoc.clipEnd, "number");
-    @computed get clipStart() { return NumCast(this.layoutDoc.clipStart); }
-    @computed get clipEnd() { return NumCast(this.layoutDoc.clipEnd, this.duration); }
-    @computed get trimStart() { return this._trimming ? this._trimStart : NumCast(this.layoutDoc.clipStart); }
+    @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 && this._trimEnd !== undefined ? this._trimEnd : NumCast(this.layoutDoc.clipEnd, this.duration);
+        return this._trimming !== AudioBox.ScopeNone && this._trimEnd !== undefined ? this._trimEnd : NumCast(this.layoutDoc.clipEnd, this.clipDuration);
     }
 
     @computed get mediaState():
@@ -113,13 +119,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
     @computed get rawDuration() {
         return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
     }
-    @computed get duration() {
-        return NumCast(this.layoutDoc.clipEnd, NumCast(this.layoutDoc.clipStart) + NumCast(this.dataDoc[`${this.fieldKey}-duration`])) - NumCast(this.layoutDoc.clipStart);
-        // NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
-    }
-    @computed get trimDuration() {
-        return this.trimEnd - this.trimStart;
-    }
     @computed get anchorDocs() {
         return DocListCast(this.dataDoc[this.annotationKey]);
     }
@@ -269,7 +268,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                 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;
@@ -309,7 +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.duration;
+                if (this._trimEnd === undefined) this._trimEnd = this.clipDuration;
             }
         };
         this._recordStart = new Date().getTime();
@@ -359,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);
@@ -378,7 +377,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
     // for play button
     Play = (e?: any) => {
         let start;
-        if (this._ended || this._ele!.currentTime === this.duration) {
+        if (this._ended || this._ele!.currentTime === this.clipDuration) {
             start = NumCast(this.layoutDoc.clipStart);
             this._ended = false;
         }
@@ -494,11 +493,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
 
     // shows trim controls
     @action
-    startTrim = () => {
+    startTrim = (scope: number) => {
         if (this.mediaState === "playing") {
             this.Pause();
         }
-        this._trimming = true;
+        this._trimming = scope;
     }
 
     // hides trim controls and displays new clip
@@ -510,7 +509,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
         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 = false;
+        this._trimming = AudioBox.ScopeNone;
     });
 
     @action
@@ -579,15 +578,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                 rawDuration={this.rawDuration}
 
                 // this edits the entire waveform when trimming is activated
-                clipStart={this._trimming ? 0 : this.clipStart}
-                clipEnd={this._trimming ? this.rawDuration : this.clipEnd}
-                clipDuration={this._trimming ? this.rawDuration : this.duration}
+                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}
+                trimming={this._trimming !== AudioBox.ScopeNone}
                 trimStart={this.trimStartFunc}
                 trimEnd={this.trimEndFunc}
                 trimDuration={this.trimDurationFunc}
@@ -596,6 +595,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
             />
         );
     }
+    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 =
@@ -697,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>
@@ -719,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>
-- 
cgit v1.2.3-70-g09d2


From cbf22b4ccaab14a7c8cb62137ea09b58a001e13a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 24 Sep 2021 18:45:33 -0400
Subject: improved initial waveform display when making a clipping.

---
 src/client/views/AudioWaveform.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index f7b117130..0a441552e 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -42,7 +42,8 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
             ({ 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));
+                    const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration)], listSpec("number"), []);
+                    this.props.layoutDoc[fieldKey] = new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length));
                     setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd));
                 }
             }, { fireImmediately: true });
-- 
cgit v1.2.3-70-g09d2


From 6bcf4ae5f3953ba10ba1fba6c7d2246514a90eed Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 24 Sep 2021 22:31:22 -0400
Subject: refactored trim stuff out of audiobox into collectionstackedtimeline
 so that videobox can reuse trimming

---
 src/client/views/AudioWaveform.tsx                 |   5 +-
 src/client/views/ContextMenuItem.tsx               |   4 +-
 .../collections/CollectionStackedTimeline.tsx      | 193 +++++-------
 src/client/views/nodes/AudioBox.tsx                | 350 ++++++++-------------
 src/client/views/nodes/VideoBox.tsx                |  59 ++--
 5 files changed, 248 insertions(+), 363 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 0a441552e..0e9c00656 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -16,7 +16,6 @@ export interface AudioWaveformProps {
     rawDuration: number; // length of underlying media data
     mediaPath: string;
     layoutDoc: Doc;
-    trimming: boolean;
     clipStart: number;
     clipEnd: number;
     PanelHeight: () => number;
@@ -42,8 +41,8 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
             ({ 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.
-                    const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration)], listSpec("number"), []);
-                    this.props.layoutDoc[fieldKey] = new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length));
+                    const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration)], listSpec("number"));
+                    this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length));
                     setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd));
                 }
             }, { fireImmediately: true });
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index c3921d846..25d00f701 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -39,7 +39,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
 
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
         if ("event" in this.props) {
-            this.props.closeMenu && this.props.closeMenu();
+            this.props.closeMenu?.();
             let batch: UndoManager.Batch | undefined;
             if (this.props.undoable !== false) {
                 batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
@@ -90,7 +90,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
                         </span>
                     ) : null}
                     <div className="contextMenu-description">
-                        {this.props.description.replace(":","")}
+                        {this.props.description.replace(":", "")}
                     </div>
                 </div>
             );
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 43f78cf78..48014921a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -61,24 +61,21 @@ export type CollectionStackedTimelineProps = {
     mediaPath: string;
     dictationKey: string;
     rawDuration: number;
-    trimming: boolean;
-    clipStart: number;
-    clipEnd: number;
-    clipDuration: number;
-    trimStart: () => number;
-    trimEnd: () => number;
-    trimDuration: () => number;
-    setStartTrim: (newStart: number) => void;
-    setEndTrim: (newEnd: number) => void;
+    fieldKey: string;
 };
 
+export enum TrimScope {
+    All = 2,
+    Clip = 1,
+    None = 0,
+}
+
 @observer
 export class CollectionStackedTimeline extends CollectionSubView<
     PanZoomDocument,
     CollectionStackedTimelineProps
 >(PanZoomDocument) {
-    @observable static SelectingRegion: CollectionStackedTimeline | undefined =
-        undefined;
+    @observable static SelectingRegion: CollectionStackedTimeline | undefined;
     static RangeScript: ScriptField;
     static LabelScript: ScriptField;
     static RangePlayScript: ScriptField;
@@ -87,37 +84,43 @@ export class CollectionStackedTimeline extends CollectionSubView<
     private _timeline: HTMLDivElement | null = null;
     private _markerStart: number = 0;
     @observable _markerEnd: number = 0;
+    @observable _trimming: number = TrimScope.None;
+    @observable _trimStart: number = 0;
+    @observable _trimEnd: number = 0;
 
-    get minLength() {
-        const rect = this._timeline?.getBoundingClientRect();
-        if (rect) {
-            return 0.05 * this.clipDuration;
-        }
-        return 0;
-    }
+    get minTrimLength() { return this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0; }
+    @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; }
+    @computed get trimDuration() { return this.trimEnd - this.trimStart; }
+    @computed get trimEnd() { return this.IsTrimming !== TrimScope.None ? this._trimEnd : this.clipEnd; }
 
-    get trimStart() {
-        return this.props.trimStart();
-    }
+    @computed get clipStart() { return this.IsTrimming === TrimScope.All ? 0 : NumCast(this.layoutDoc.clipStart); }
+    @computed get clipDuration() { return this.clipEnd - this.clipStart; }
+    @computed get clipEnd() { return this.IsTrimming === TrimScope.All ? this.props.rawDuration : NumCast(this.layoutDoc.clipEnd, this.props.rawDuration); }
 
-    get trimEnd() {
-        return this.props.trimEnd();
-    }
+    @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }
 
-    get clipDuration() {
-        return this.props.clipDuration;
-    }
+    public get IsTrimming() { return this._trimming; }
 
-    @computed get currentTime() {
-        return NumCast(this.layoutDoc._currentTimecode);
+    @action
+    public StartTrimming(scope: TrimScope) {
+        this._trimStart = this.clipStart;
+        this._trimEnd = this.clipEnd;
+        this._trimming = scope;
+    }
+    @action
+    public StopTrimming() {
+        this.layoutDoc.clipStart = this.trimStart;
+        this.layoutDoc.clipEnd = this.trimEnd;
+        this._trimming = TrimScope.None;
     }
+
     @computed get selectionContainer() {
         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}%`,
+                    left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.trimDuration) * 100}%`,
+                    width: `${(Math.abs(this._markerStart - this._markerEnd) / this.trimDuration) * 100}%`,
                 }}
             />
         );
@@ -145,28 +148,21 @@ export class CollectionStackedTimeline extends CollectionSubView<
     componentDidMount() {
         document.addEventListener("keydown", this.keyEvents, true);
     }
+
+    @action
     componentWillUnmount() {
         document.removeEventListener("keydown", this.keyEvents, true);
         if (CollectionStackedTimeline.SelectingRegion === this) {
-            runInAction(
-                () => (CollectionStackedTimeline.SelectingRegion = undefined)
-            );
+            CollectionStackedTimeline.SelectingRegion = undefined;
         }
     }
 
-    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
-        );
-    }
+    anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]))
+    anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val));
     toTimeline = (screen_delta: number, width: number) => {
         return Math.max(
-            this.props.clipStart,
-            Math.min(this.props.clipEnd, (screen_delta / width) * this.props.clipDuration + this.props.clipStart));
+            this.clipStart,
+            Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart));
     }
 
     rangeClickScript = () => CollectionStackedTimeline.RangeScript;
@@ -233,10 +229,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         !wasSelecting &&
                         CollectionStackedTimeline.SelectingRegion !== this
                     ) {
-                        this._markerStart = this._markerEnd = this.toTimeline(
-                            clientX - rect.x,
-                            rect.width
-                        );
+                        this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
                         CollectionStackedTimeline.SelectingRegion = this;
                         wasSelecting = true;
                     }
@@ -254,7 +247,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         !isClick &&
                         CollectionStackedTimeline.SelectingRegion === this &&
                         Math.abs(movement[0]) > 15 &&
-                        !this.props.trimming
+                        !this.IsTrimming
                     ) {
                         const anchor = CollectionStackedTimeline.createAnchor(
                             this.rootDoc,
@@ -287,7 +280,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                             this.currentTime
                         );
                     } else {
-                        !wasPlaying && this.props.setTime(((clientX - rect.x) / rect.width) * this.clipDuration + this.props.clipStart);
+                        !wasPlaying && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
                     }
                 }
             );
@@ -304,21 +297,19 @@ export class CollectionStackedTimeline extends CollectionSubView<
             e,
             action((e, [], []) => {
                 if (rect && this.props.isContentActive()) {
-                    this.props.setStartTrim(Math.min(
+                    this._trimStart = Math.min(
                         Math.max(
                             this.trimStart + (e.movementX / rect.width) * this.clipDuration,
                             0
                         ),
-                        this.trimEnd - this.minLength
-                    ));
+                        this.trimEnd - this.minTrimLength
+                    );
                 }
                 return false;
             }),
             emptyFunction,
             action((e, doubleTap) => {
-                if (doubleTap) {
-                    this.props.setStartTrim(this.props.clipStart);
-                }
+                doubleTap && (this._trimStart = this.clipStart);
             })
         );
     }
@@ -332,21 +323,19 @@ export class CollectionStackedTimeline extends CollectionSubView<
             e,
             action((e, [], []) => {
                 if (rect && this.props.isContentActive()) {
-                    this.props.setEndTrim(Math.max(
+                    this._trimEnd = Math.max(
                         Math.min(
                             this.trimEnd + (e.movementX / rect.width) * this.clipDuration,
-                            this.props.clipStart + this.clipDuration
+                            this.clipStart + this.clipDuration
                         ),
-                        this.trimStart + this.minLength
-                    ));
+                        this.trimStart + this.minTrimLength
+                    );
                 }
                 return false;
             }),
             emptyFunction,
             action((e, doubleTap) => {
-                if (doubleTap) {
-                    this.props.setEndTrim(this.props.clipEnd);
-                }
+                doubleTap && (this._trimEnd = this.clipEnd);
             })
         );
     }
@@ -356,17 +345,16 @@ export class CollectionStackedTimeline extends CollectionSubView<
         if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
         if (!super.onInternalDrop(e, de)) return false;
 
-
         // 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 timelinePt = this.toTimeline(x, this.props.PanelWidth());
         for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
             const d = Doc.GetProto(docDragData.droppedDocuments[i]);
-            if (d._timecodeToHide !== undefined) {
-                d._timecodeToHide = timelinePt + NumCast(d._timecodeToHide) - NumCast(d._timecodeToShow);
+            if (this.anchorEnd(d) !== undefined) {
+                d[d._timecodeToHide === undefined ? this.props.endTag : "_timecodeToHide"] = timelinePt + this.anchorEnd(d) - this.anchorStart(d);
             }
-            d._timecodeToShow = timelinePt;
+            d[d._timecodToShow === undefined ? this.props.startTag : "_timecodToShow"] = timelinePt;
         }
 
         return true;
@@ -403,7 +391,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         });
         Doc.GetProto(anchor)[startTag] = anchorStartTime;
         Doc.GetProto(anchor)[endTag] = anchorEndTime;
-        if (Cast(dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+        if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
             Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
         } else {
             dataDoc[fieldKey] = new List<Doc>([anchor]);
@@ -546,10 +534,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
                     duration={this.clipDuration}
                     mediaPath={this.props.mediaPath}
                     layoutDoc={this.layoutDoc}
-                    clipStart={this.props.clipStart}
-                    clipEnd={this.props.clipEnd}
+                    clipStart={this.clipStart}
+                    clipEnd={this.clipEnd}
                     PanelHeight={this.timelineContentHeight}
-                    trimming={this.props.trimming}
                 />
             </div>
         );
@@ -582,12 +569,12 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         d.anchor,
                         start + (10 / timelineContentWidth) * this.clipDuration
                     );
-                    if (end < this.props.clipStart || start > this.props.clipEnd) return (null);
-                    const left = Math.max((start - this.props.clipStart) / this.clipDuration * timelineContentWidth, 0);
+                    if (end < this.clipStart || start > this.clipEnd) return (null);
+                    const left = Math.max((start - this.clipStart) / this.clipDuration * timelineContentWidth, 0);
                     const top = (d.level / maxLevel) * this.timelineContentHeight();
                     const timespan = end - start;
                     const width = (timespan / this.clipDuration) * timelineContentWidth;
-                    const height = (this.timelineContentHeight()) / maxLevel;
+                    const height = this.timelineContentHeight() / maxLevel;
                     return this.props.Document.hideAnchors ? null : (
                         <div
                             className={"collectionStackedTimeline-marker-timeline"}
@@ -624,28 +611,28 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         </div>
                     );
                 })}
-                {!this.props.trimming && this.selectionContainer}
+                {!this.IsTrimming && this.selectionContainer}
                 {this.renderAudioWaveform}
                 {this.renderDictation}
 
                 <div
                     className="collectionStackedTimeline-current"
                     style={{
-                        left: `${((this.currentTime - this.props.clipStart) / this.clipDuration) * 100}%`,
+                        left: `${((this.currentTime - this.clipStart) / this.clipDuration) * 100}%`,
                     }}
                 />
 
-                {this.props.trimming && (
+                {this.IsTrimming && (
                     <>
                         <div
                             className="collectionStackedTimeline-trim-shade"
-                            style={{ width: `${((this.trimStart - this.props.clipStart) / this.clipDuration) * 100}%` }}
+                            style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }}
                         ></div>
 
                         <div
                             className="collectionStackedTimeline-trim-controls"
                             style={{
-                                left: `${((this.trimStart - this.props.clipStart) / this.clipDuration) * 100}%`,
+                                left: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%`,
                                 width: `${((this.trimEnd - this.trimStart) / this.clipDuration) * 100}%`,
                             }}
                         >
@@ -662,8 +649,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         <div
                             className="collectionStackedTimeline-trim-shade"
                             style={{
-                                left: `${((this.trimEnd - this.props.clipStart) / this.clipDuration) * 100}%`,
-                                width: `${((this.props.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`,
+                                left: `${((this.trimEnd - this.clipStart) / this.clipDuration) * 100}%`,
+                                width: `${((this.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`,
                             }}
                         ></div>
                     </>
@@ -754,8 +741,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
             return this.props.toTimeline(e.clientX - rect.x, rect.width);
         };
         const changeAnchor = (anchor: Doc, left: boolean, time: number | undefined) => {
-            const timelineOnly =
-                Cast(anchor[this.props.startTag], "number", null) !== undefined;
+            const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
             if (timelineOnly) {
                 if (!left && time !== undefined && time <= NumCast(anchor[this.props.startTag])) time = undefined;
                 Doc.SetInPlace(
@@ -767,9 +753,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                 if (!left) Doc.SetInPlace(anchor, "borderRounding", time !== undefined ? undefined : "100%", true);
             }
             else {
-                left
-                    ? (anchor._timecodeToShow = time)
-                    : (anchor._timecodeToHide = time);
+                anchor[left ? "_timecodeToShow" : "_timecodeToHide"] = time;
             }
             return false;
         };
@@ -803,10 +787,9 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
         mark: Doc,
         script: undefined | (() => ScriptField),
         doublescript: undefined | (() => ScriptField),
-        x: number,
-        y: number,
-        width: number,
-        height: number
+        screenXf: () => Transform,
+        width: () => number,
+        height: () => number
     ) {
         const anchor = observable({ view: undefined as any });
         const focusFunc = (
@@ -825,24 +808,20 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                 <DocumentView
                     key="view"
                     {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
-                    ref={action((r: DocumentView | null) => (anchor.view = r))}
+                    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={this.props.isDocumentActive}
-                    PanelWidth={() => width}
-                    PanelHeight={() => height}
-                    ScreenToLocalTransform={() =>
-                        this.props.ScreenToLocalTransform().translate(-x, -y)
-                    }
+                    PanelWidth={width}
+                    PanelHeight={height}
+                    ScreenToLocalTransform={screenXf}
                     focus={focusFunc}
                     rootSelected={returnFalse}
                     onClick={script}
-                    onDoubleClick={
-                        this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript
-                    }
+                    onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
                     ignoreAutoHeight={false}
                     hideResizeHandles={true}
                     bringToFront={emptyFunction}
@@ -852,15 +831,17 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
         };
     });
 
+    anchorScreenToLocalXf = () => this.props.ScreenToLocalTransform().translate(-this.props.left, -this.props.top);
+    width = () => this.props.width;
+    height = () => this.props.height;
     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
+            this.anchorScreenToLocalXf,
+            this.width,
+            this.height
         );
         return (
             <>
@@ -876,9 +857,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                             <div
                                 key="right"
                                 className="collectionStackedTimeline-resizer"
-                                onPointerDown={(e) =>
-                                    this.onAnchorDown(e, this.props.mark, false)
-                                }
+                                onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, false)}
                             />
                         </>
                     )}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index bfc15cea8..81367ed19 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -14,15 +14,15 @@ import { Doc, DocListCast, Opt } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
 import { makeInterface } from "../../../fields/Schema";
 import { ComputedField } from "../../../fields/ScriptField";
-import { Cast, NumCast } from "../../../fields/Types";
+import { Cast, NumCast, DateCast } from "../../../fields/Types";
 import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime, OmitKeys, setupMoveUpEvents, returnFalse } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } 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 { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import {
@@ -43,20 +43,21 @@ declare class MediaRecorder {
 type AudioDocument = makeInterface<[typeof documentSchema]>;
 const AudioDocument = makeInterface(documentSchema);
 
+enum media_state {
+    PendingRecording = "pendingRecording",
+    Recording = "recording",
+    Paused = "paused",
+    Playing = "playing"
+};
 @observer
-export class AudioBox extends ViewBoxAnnotatableComponent<
-    ViewBoxAnnotatableProps & FieldViewProps,
-    AudioDocument
->(AudioDocument) {
+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 = 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;
 
+    _dropDisposer?: DragManager.DragDropDisposer;
     _disposers: { [name: string]: IReactionDisposer } = {};
     _ele: HTMLAudioElement | null = null;
     _stackedTimeline = React.createRef<CollectionStackedTimeline>();
@@ -68,81 +69,39 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
     _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;
-    @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 recordingStart() { return DateCast(this.dataDoc[this.fieldKey + "-recordingStart"])?.date.getTime(); }
+    @computed get rawDuration() { 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; }
+    @computed get mediaState() { return this.layoutDoc.mediaState as media_state; }
+    set mediaState(value) { this.layoutDoc.mediaState = value; }
 
-    @computed get mediaState():
-        | undefined
-        | "pendingRecording"
-        | "recording"
-        | "paused"
-        | "playing" {
-        return this.layoutDoc.mediaState as
-            | undefined
-            | "pendingRecording"
-            | "recording"
-            | "paused"
-            | "playing";
-    }
-    set mediaState(value) {
-        this.layoutDoc.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 rawDuration() {
-        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;
-    }
+    get timeline() { return this._stackedTimeline.current; }
 
     constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
         super(props);
         AudioBox.Instance = this;
     }
 
+    public static SetScrubTime = action((timeInMillisFrom1970: number) => {
+        AudioBox._scrubTime = 0;
+        AudioBox._scrubTime = timeInMillisFrom1970;
+    });
+
     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) ||
+            this.timeline?.anchorStart(la2) ||
+            this.timeline?.anchorStart(la1) ||
             0;
         if (Doc.AreProtosEqual(la1, this.dataDoc)) {
             la1 = l.anchor2 as Doc;
@@ -152,47 +111,42 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
     }
 
     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 === media_state.Recording
+                ? (Date.now() - (this.recordingStart || 0)) / 1000
+                : undefined)
+        ) || this.rootDoc;
     }
 
     componentWillUnmount() {
-        this.dropDisposer?.();
+        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.mediaState = this.path ? media_state.Paused : undefined as any as media_state;
 
         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,
+            () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1
+                ? NumCast(this.Document._triggerAudio, null)
+                : undefined,
             (start) =>
-                start !== undefined &&
-                setTimeout(() => {
+                start !== undefined && setTimeout(() => {
                     this.playFrom(start);
                     setTimeout(() => {
                         this.Document._currentTimecode = start;
@@ -203,13 +157,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
         );
 
         this._disposers.audioStop = reaction(
-            () =>
-                this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
-                    ? Cast(this.Document._audioStop, "number", null)
-                    : undefined,
+            () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
+                ? Cast(this.Document._audioStop, "number", null)
+                : undefined,
             (audioStop) =>
-                audioStop !== undefined &&
-                setTimeout(() => {
+                audioStop !== undefined && setTimeout(() => {
                     this.Pause();
                     setTimeout(() => (this.Document._audioStop = undefined), 10);
                 }), // wait for mainCont and try again to play
@@ -220,27 +172,25 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
     // for updating the timecode
     @action
     timecodeChanged = () => {
-        const htmlEle = this._ele;
-        if (this.mediaState !== "recording" && htmlEle) {
+        if (this.mediaState !== media_state.Recording && this._ele) {
             this.links
                 .map((l) => this.getLinkData(l))
                 .forEach(({ la1, la2, linkTime }) => {
                     if (
                         linkTime > NumCast(this.layoutDoc._currentTimecode) &&
-                        linkTime < htmlEle.currentTime
+                        linkTime < this._ele!.currentTime
                     ) {
                         Doc.linkFollowHighlight(la1);
                     }
                 });
-            this.layoutDoc._currentTimecode = htmlEle.currentTime;
-
+            this.layoutDoc._currentTimecode = this._ele.currentTime;
         }
     }
 
     // pause play back
     Pause = action(() => {
         this._ele!.pause();
-        this.mediaState = "paused";
+        this.mediaState = media_state.Paused;
     });
 
     // play audio for documents created during recording
@@ -251,32 +201,30 @@ 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, fullPlay: boolean = false): any => {
         clearTimeout(this._play);
         if (Number.isNaN(this._ele?.duration)) {
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
-        } else if (this._ele && AudioBox.Enabled) {
-            if (seekTimeInSeconds < 0) {
-                if (seekTimeInSeconds > -1) {
-                    setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
-                } 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.timeline && this._ele && AudioBox.Enabled) {
+            const end = Math.min(this.timeline.trimEnd, endTime ?? this.timeline.trimEnd);
+            const start = Math.max(this.timeline.trimStart, seekTimeInSeconds);
+            if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) {
                 this._ele.currentTime = start;
                 this._ele.play();
-                runInAction(() => (this.mediaState = "playing"));
-                if (endTime !== this.clipDuration) {
-                    this._play = setTimeout(
+                runInAction(() => this.mediaState = media_state.Playing);
+                if (end !== this.timeline.clipDuration) {
+                    return this._play = setTimeout(
                         () => {
-                            this._ended = fullPlay ? true : this._ended;
+                            if (fullPlay) this.setAnchorTime(this.timeline!.trimStart);
                             this.Pause();
                         },
                         (end - start) * 1000
                     ); // use setTimeout to play a specific duration
                 }
+            }
+            if (seekTimeInSeconds < 0 && seekTimeInSeconds > -1) {
+                setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
             } else {
                 this.Pause();
             }
@@ -285,7 +233,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
 
     // update the recording time
     updateRecordTime = () => {
-        if (this.mediaState === "recording") {
+        if (this.mediaState === media_state.Recording) {
             setTimeout(this.updateRecordTime, 30);
             if (this._paused) {
                 this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
@@ -300,22 +248,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
     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.fieldKey + "-recordingStart"] = new DateField();
         DocUtils.ActiveRecordings.push(this);
         this._recorder.ondataavailable = async (e: any) => {
             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.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
             }
         };
         this._recordStart = new Date().getTime();
-        runInAction(() => (this.mediaState = "recording"));
+        runInAction(() => this.mediaState = media_state.Recording);
         setTimeout(this.updateRecordTime, 0);
         this._recorder.start();
-        setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
+        setTimeout(() => this.stopRecording(), 60 * 60 * 1000); // stop after an hour
     }
 
     // context menu
@@ -353,17 +298,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
 
     // 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.mediaState = "paused";
-        this._trimEnd = this.clipDuration;
-        this.layoutDoc.clipStart = 0;
-        this.layoutDoc.clipEnd = this.clipDuration;
-        this._stream?.getAudioTracks()[0].stop();
-        const ind = DocUtils.ActiveRecordings.indexOf(this);
-        ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+        if (this._recorder) {
+            this._recorder.stop();
+            this._recorder = undefined;
+            this.dataDoc[this.fieldKey + "-duration"] =
+                (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+            this.mediaState = media_state.Paused;
+            this._stream?.getAudioTracks()[0].stop();
+            const ind = DocUtils.ActiveRecordings.indexOf(this);
+            ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+        }
     });
 
     // button for starting and stopping the recording
@@ -376,16 +320,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
 
     // for play button
     Play = (e?: any) => {
-        let start;
-        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);
+        const eleTime = this._ele!.currentTime;
+        const start = eleTime === this.timeline?.trimDuration ? NumCast(this.layoutDoc.trimStart) : eleTime;
+        this.playFrom(start, undefined, true);
         e?.stopPropagation?.();
     }
 
@@ -402,7 +339,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
         );
         Doc.GetProto(newDoc).recordingSource = this.dataDoc;
         Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(
-            `self.recordingSource["${this.props.fieldKey}-recordingStart"]`
+            `self.recordingSource["${this.fieldKey}-recordingStart"]`
         );
         Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(
             "self.recordingSource.mediaState"
@@ -420,7 +357,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
 
     // returns the path of the audio file
     @computed get path() {
-        const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+        const field = Cast(this.props.Document[this.fieldKey], AudioField);
         const path = field instanceof AudioField ? field.url.href : "";
         return path === nullAudio ? "" : path;
     }
@@ -460,68 +397,33 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
         e.stopPropagation();
     }
 
-    playing = () => this.mediaState === "playing";
+    playing = () => this.mediaState === media_state.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));
+                this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
             } else {
                 this._ele!.currentTime = this.layoutDoc._currentTimecode =
-                    stack?.anchorStart(link) || 0;
+                    this.timeline?.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);
+                    const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2);
+                    const endTime = this.timeline?.anchorEnd(la1) || this.timeline?.anchorEnd(la2);
                     if (startTime !== undefined) {
                         if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
-                            endTime
-                                ? this.playFrom(startTime, endTime)
-                                : this.playFrom(startTime);
+                            this.playFrom(startTime, endTime);
                         } else {
-                            this._ele!.currentTime = this.layoutDoc._currentTimecode =
-                                startTime;
+                            this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
                         }
                     }
                 });
         }
     }
 
-    // shows trim controls
-    @action
-    startTrim = (scope: number) => {
-        if (this.mediaState === "playing") {
-            this.Pause();
-        }
-        this._trimming = scope;
-    }
-
-    // hides trim controls and displays new clip
-    @undoBatch
-    finishTrim = action(() => {
-        if (this.mediaState === "playing") {
-            this.Pause();
-        }
-        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) => {
-        this._trimStart = newStart;
-    }
-
-    @action
-    setEndTrim = (newEnd: number) => {
-        this._trimEnd = newEnd;
-    }
-
     isActiveChild = () => this._isAnyChildContentActive;
     timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
         this.props.whenChildContentsActiveChanged(
@@ -543,9 +445,6 @@ 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
@@ -576,31 +475,28 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                 PanelWidth={this.timelineWidth}
                 PanelHeight={this.timelineHeight}
                 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}
             />
         );
     }
+    // hides trim controls and displays new clip
+    @undoBatch
+    finishTrim = action(() => {
+        this.Pause();
+        this.setAnchorTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this._ele!.currentTime), this.timeline?.trimStart || 0));
+        this.timeline?.StopTrimming();
+    });
+    startTrim = (scope: TrimScope) => {
+        this.Pause();
+        this.timeline?.StartTrimming(scope);
+    }
+
     onClipPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
+        this.timeline && 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);
+                this.startTrim(TrimScope.All);
+            } else if (this.timeline) {
+                this.Pause();
+                this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
             }
         }));
     }
@@ -613,12 +509,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
         return (
             <div
                 ref={r => {
-                    if (r && this._stackedTimeline.current) {
-                        this.dropDisposer?.();
-                        this.dropDisposer = DragManager.MakeDropTarget(r,
+                    if (r && this.timeline) {
+                        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);
+                                de.complete.docDragData && this.timeline!.internalDocDrop(e, de, de.complete.docDragData, xp);
                             }
                             , this.layoutDoc, undefined);
                     }
@@ -644,7 +540,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                                 size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
                             />
                         </div>
-                        {this.mediaState === "recording" || this.mediaState === "paused" ? (
+                        {this.mediaState === media_state.Recording || this.mediaState === media_state.Playing ? (
                             <div className="recording" onClick={(e) => e.stopPropagation()}>
                                 <div className="recording-buttons" onClick={this.recordClick}>
                                     <FontAwesomeIcon
@@ -694,22 +590,22 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                             >
                                 <div
                                     className="audiobox-buttons"
-                                    title={this.mediaState === "paused" ? "play" : "pause"}
-                                    onClick={this.mediaState === "paused" ? this.Play : this.Pause}
+                                    title={this.mediaState === media_state.Paused ? "play" : "pause"}
+                                    onClick={this.mediaState === media_state.Paused ? this.Play : this.Pause}
                                 >
                                     {" "}
                                     <FontAwesomeIcon
-                                        icon={this.mediaState === "paused" ? "play" : "pause"}
+                                        icon={this.mediaState === media_state.Paused ? "play" : "pause"}
                                         size={"1x"}
                                     />
                                 </div>
                                 <div
                                     className="audiobox-buttons"
-                                    title={this._trimming !== AudioBox.ScopeNone ? "finish" : "trim"}
+                                    title={this.timeline?.IsTrimming !== TrimScope.None ? "finish" : "trim"}
                                     onPointerDown={this.onClipPointerDown}
                                 >
                                     <FontAwesomeIcon
-                                        icon={this._trimming !== AudioBox.ScopeNone ? "check" : "cut"}
+                                        icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"}
                                         size={"1x"}
                                     />
                                 </div>
@@ -727,10 +623,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
                                 </div>
                                 {this.audio}
                                 <div className="audioBox-current-time">
-                                    {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.clipStart)))}
+                                    {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}
                                 </div>
                                 <div className="audioBox-total-time">
-                                    {formatTime(Math.round(NumCast(this.clipDuration)))}
+                                    {this.timeline && formatTime(Math.round(NumCast(this.timeline?.clipDuration)))}
                                 </div>
                             </div>
                         </div>
@@ -738,4 +634,4 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
             </div>
         );
     }
-}
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8b33842ff..af65cce9f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -16,7 +16,7 @@ import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SelectionManager } from "../../util/SelectionManager";
 import { SnappingManager } from "../../util/SnappingManager";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
+import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
@@ -31,6 +31,7 @@ import { DocumentManager } from "../../util/DocumentManager";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { Tooltip } from "@material-ui/core";
 import { AnchorMenu } from "../pdf/AnchorMenu";
+import { undoBatch } from "../../util/UndoManager";
 const path = require('path');
 
 type VideoDocument = makeInterface<[typeof documentSchema]>;
@@ -101,7 +102,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this._playing = true;
         try {
             this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
-            update && this.player?.play();
+            update && this.player && this.playFrom(this.player.currentTime);
             update && this._audioPlayer?.play();
             update && this._youtubePlayer?.playVideo();
             this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
@@ -301,6 +302,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
             subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
             subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
+            subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
+            subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
+            subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
+            subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
             ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
         }
     }
@@ -483,34 +488,47 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return this.addDocument(doc);
     }
 
+    get timeline() { return this._stackedTimeline.current; }
     // play back the video from time
     @action
-    playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
+    playFrom = (seekTimeInSeconds: number, endTime?: number) => {
         clearTimeout(this._playRegionTimer);
-        this._playRegionDuration = endTime - seekTimeInSeconds;
         if (Number.isNaN(this.player?.duration)) {
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
-        } else if (this.player) {
-            if (seekTimeInSeconds < 0) {
-                if (seekTimeInSeconds > -1) {
-                    setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
-                } else {
-                    this.Pause();
-                }
-            } else if (seekTimeInSeconds <= this.player.duration) {
-                this.player.currentTime = seekTimeInSeconds;
+        }
+        else if (this.player) {
+            const end = Math.min(this.timeline?.trimEnd ?? this.duration, endTime ?? this.timeline?.trimEnd ?? this.duration);
+            const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
+            this._playRegionDuration = end - seekTimeInSeconds;
+            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.duration)) {
+                this.player.currentTime = start;
                 this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
                 this.player.play();
                 this._audioPlayer?.play();
                 runInAction(() => this._playing = true);
                 if (endTime !== this.duration) {
-                    this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration
+                    return this._playRegionTimer =
+                        setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration
                 }
+            }
+            if (seekTimeInSeconds < 0 && seekTimeInSeconds > -1) {
+                setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
             } else {
                 this.Pause();
             }
         }
     }
+    // hides trim controls and displays new clip
+    @undoBatch
+    finishTrim = action(() => {
+        this.Pause();
+        this._stackedTimeline.current?.StopTrimming();
+    });
+    startTrim = (scope: TrimScope) => {
+        this.Pause();
+        this._stackedTimeline.current?.StartTrimming(scope);
+    }
+
 
     playLink = (doc: Doc) => {
         const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0));
@@ -524,7 +542,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     playing = () => this._playing;
     timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
     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;
+    setAnchorTime = (time: number) => {
+        this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+    }
     timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
     trimEndFunc = () => this.duration;
     @computed get renderTimeline() {
@@ -550,15 +570,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 playLink={this.playLink}
                 PanelHeight={this.timelineHeight}
                 rawDuration={this.duration}
-                clipDuration={this.duration}
-                clipStart={0}
-                clipEnd={this.duration}
-                trimming={false}
-                trimStart={returnZero}
-                trimEnd={this.trimEndFunc}
-                trimDuration={this.trimEndFunc}
-                setStartTrim={emptyFunction}
-                setEndTrim={emptyFunction}
             />
         </div>;
     }
-- 
cgit v1.2.3-70-g09d2


From a793b7e981e46b7c98bee6cce3faaf3a5b05f6ae Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 26 Sep 2021 02:55:23 -0400
Subject: fixed warnings, fixed bug following link w/auto play.  plus
 refactorings.

---
 src/client/views/AudioWaveform.tsx                 |  11 +-
 .../collections/CollectionStackedTimeline.tsx      |  78 ++--
 src/client/views/nodes/AudioBox.tsx                | 291 +++++++--------
 src/client/views/nodes/LabelBox.tsx                |   2 +-
 src/client/views/nodes/VideoBox.tsx                | 405 ++++++++++-----------
 5 files changed, 373 insertions(+), 414 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 0e9c00656..270b3869c 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -25,14 +25,13 @@ export interface AudioWaveformProps {
 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 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"), []); }
+
+    audioBucketField = (start: number, end: number) => "audioBuckets/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_");
+
     componentWillUnmount() {
         this._disposer?.();
     }
@@ -87,7 +86,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
             <div className="audioWaveform">
                 <Waveform
                     color={Colors.MEDIUM_BLUE}
-                    height={this._waveHeight}
+                    height={this.waveHeight}
                     barWidth={0.1}
                     pos={this.props.duration}
                     duration={this.props.duration}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index cbf232d3d..e1e2990a0 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -98,34 +98,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }
 
-    public get IsTrimming() { return this._trimming; }
-
-    @action
-    public StartTrimming(scope: TrimScope) {
-        this._trimStart = this.clipStart;
-        this._trimEnd = this.clipEnd;
-        this._trimming = scope;
-    }
-    @action
-    public StopTrimming() {
-        this.layoutDoc.clipStart = this.trimStart;
-        this.layoutDoc.clipEnd = this.trimEnd;
-        this._trimming = TrimScope.None;
-    }
-
-    @computed get selectionContainer() {
-        const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
-        return markerEnd === undefined ? null : (
-            <div
-                className="collectionStackedTimeline-selector"
-                style={{
-                    left: `${((Math.min(this._markerStart, markerEnd) - this.trimStart) / this.trimDuration) * 100}%`,
-                    width: `${(Math.abs(this._markerStart - markerEnd) / this.trimDuration) * 100}%`,
-                }}
-            />
-        );
-    }
-
     constructor(props: any) {
         super(props);
         // onClick play scripts
@@ -157,8 +129,23 @@ export class CollectionStackedTimeline extends CollectionSubView<
         }
     }
 
-    anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]))
-    anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val));
+    public get IsTrimming() { return this._trimming; }
+
+    @action
+    public StartTrimming(scope: TrimScope) {
+        this._trimStart = this.clipStart;
+        this._trimEnd = this.clipEnd;
+        this._trimming = scope;
+    }
+    @action
+    public StopTrimming() {
+        this.layoutDoc.clipStart = this.trimStart;
+        this.layoutDoc.clipEnd = this.trimEnd;
+        this._trimming = TrimScope.None;
+    }
+
+    anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
+    anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val) ?? null);
     toTimeline = (screen_delta: number, width: number) => {
         return Math.max(
             this.clipStart,
@@ -345,13 +332,13 @@ export class CollectionStackedTimeline extends CollectionSubView<
         const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
         const x = localPt[0] - docDragData.offset[0];
         const timelinePt = this.toTimeline(x, this.props.PanelWidth());
-        for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
-            const d = Doc.GetProto(docDragData.droppedDocuments[i]);
+        docDragData.droppedDocuments.forEach(drop => {
+            const d = Doc.GetProto(drop);
             if (this.anchorEnd(d) !== undefined) {
                 d[d._timecodeToHide === undefined ? this.props.endTag : "_timecodeToHide"] = timelinePt + this.anchorEnd(d) - this.anchorStart(d);
             }
             d[d._timecodToShow === undefined ? this.props.startTag : "_timecodToShow"] = timelinePt;
-        }
+        });
 
         return true;
     }
@@ -483,9 +470,12 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     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());
+    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+    currentTimecode = () => this.currentTime;
+
     @computed get renderDictation() {
         const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
         return !dictation ? null : (
@@ -537,9 +527,19 @@ export class CollectionStackedTimeline extends CollectionSubView<
             </div>
         );
     }
+    @computed get selectionContainer() {
+        const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
+        return markerEnd === undefined ? null : (
+            <div
+                className="collectionStackedTimeline-selector"
+                style={{
+                    left: `${((Math.min(this._markerStart, markerEnd) - this.trimStart) / this.trimDuration) * 100}%`,
+                    width: `${(Math.abs(this._markerStart - markerEnd) / this.trimDuration) * 100}%`,
+                }}
+            />
+        );
+    }
 
-    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
-    currentTimecode = () => this.currentTime;
     render() {
         const timelineContentWidth = this.props.PanelWidth();
         const overlaps: {
@@ -760,7 +760,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
             e,
             (e) => {
                 if (!undo) undo = UndoManager.StartBatch("drag anchor");
-                return changeAnchor(anchor, left, newTime(e))
+                return changeAnchor(anchor, left, newTime(e));
             },
             (e) => {
                 this.props.setTime(newTime(e));
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6e6558030..fa78d2301 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,6 +1,6 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { DateField } from "../../../fields/DateField";
 import { Doc, DocListCast } from "../../../fields/Doc";
@@ -19,10 +19,8 @@ import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionS
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
-import { Colors } from "../global/globalEnums";
 import "./AudioBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
-import { LinkDocPreview } from "./LinkDocPreview";
 
 declare class MediaRecorder {
     constructor(e: any); // whatever MediaRecorder has
@@ -36,10 +34,14 @@ enum media_state {
     Recording = "recording",
     Paused = "paused",
     Playing = "playing"
-};
+}
 @observer
 export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, AudioDocument>(AudioDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
+    public static SetScrubTime = action((timeInMillisFrom1970: number) => {
+        AudioBox._scrubTime = 0;
+        AudioBox._scrubTime = timeInMillisFrom1970;
+    });
     public static Enabled = false;
     static playheadWidth = 40; // width of playhead
     static heightPercent = 75; // height of timeline in percent of height of audioBox.
@@ -63,13 +65,30 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @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 mediaState() { return this.layoutDoc.mediaState as media_state; }
+    @computed get path() { // returns the path of the audio file
+        const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || "";
+        return path === nullAudio ? "" : path;
+    }
     set mediaState(value) { this.layoutDoc.mediaState = value; }
 
-    get timeline() { return this._stackedTimeline.current; }
-    public static SetScrubTime = action((timeInMillisFrom1970: number) => {
-        AudioBox._scrubTime = 0;
-        AudioBox._scrubTime = timeInMillisFrom1970;
-    });
+    get timeline() { return this._stackedTimeline.current; } // can't be computed since it's not observable
+
+    componentWillUnmount() {
+        this._dropDisposer?.();
+        Object.values(this._disposers).forEach((disposer) => disposer?.());
+        const ind = DocUtils.ActiveRecordings.indexOf(this);
+        ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+    }
+
+    @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 ? media_state.Paused : undefined as any as media_state;
+
+        this.path && this.setAnchorTime(NumCast(this.layoutDoc.clipStart));
+        this.path && this.timecodeChanged();
+    }
 
     getLinkData(l: Doc) {
         let la1 = l.anchor1 as Doc;
@@ -100,34 +119,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         ) || this.rootDoc;
     }
 
-    componentWillUnmount() {
-        this._dropDisposer?.();
-        Object.values(this._disposers).forEach((disposer) => disposer?.());
-        const ind = DocUtils.ActiveRecordings.indexOf(this);
-        ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
-    }
-
-    @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 ? media_state.Paused : undefined as any as media_state;
-
-        this.path && this.setAnchorTime(NumCast(this.layoutDoc.clipStart));
-        this.path && this.timecodeChanged();
-    }
-
     // for updating the timecode
     @action
     timecodeChanged = () => {
         if (this.mediaState !== media_state.Recording && this._ele) {
             this.links
-                .map((l) => this.getLinkData(l))
+                .map(l => this.getLinkData(l))
                 .forEach(({ la1, la2, linkTime }) => {
-                    if (
-                        linkTime > NumCast(this.layoutDoc._currentTimecode) &&
-                        linkTime < this._ele!.currentTime
-                    ) {
+                    if (linkTime > NumCast(this.layoutDoc._currentTimecode) &&
+                        linkTime < this._ele!.currentTime) {
                         Doc.linkFollowHighlight(la1);
                     }
                 });
@@ -135,23 +135,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    // pause play back
-    Pause = action(() => {
-        this._ele!.pause();
-        this.mediaState = media_state.Paused;
-    });
-
-    // play audio for documents created during recording
-    playFromTime = (absoluteTime: number) => {
-        this.recordingStart &&
-            this.playFrom((absoluteTime - this.recordingStart) / 1000);
-    }
-
     // play back the audio from time
     @action
-    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false): any => {
-        clearTimeout(this._play);
-        if (Number.isNaN(this._ele?.duration)) {
+    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
+        clearTimeout(this._play); // abort any previous clip ending
+        if (Number.isNaN(this._ele?.duration)) { // audio element isn't loaded yet... wait 1/2 second and try again
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
         }
         else if (this.timeline && this._ele && AudioBox.Enabled) {
@@ -160,18 +148,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) {
                 this._ele.currentTime = start;
                 this._ele.play();
-                runInAction(() => this.mediaState = media_state.Playing);
-                if (end !== this.timeline.clipDuration) {
-                    return this._play = setTimeout(
-                        () => {
-                            if (fullPlay) this.setAnchorTime(this.timeline!.trimStart);
-                            this.Pause();
-                        },
-                        (end - start) * 1000
-                    ); // use setTimeout to play a specific duration
-                }
-            } else if (seekTimeInSeconds < 0 && seekTimeInSeconds > -1) {
-                setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+                this.mediaState = media_state.Playing;
+                this._play = setTimeout(
+                    () => {
+                        if (fullPlay) this.setAnchorTime(this.timeline!.trimStart);
+                        this.Pause();
+                    },
+                    (end - start) * 1000);
             } else {
                 this.Pause();
             }
@@ -185,8 +168,7 @@ 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;
             }
         }
     }
@@ -205,9 +187,22 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         };
         this._recordStart = new Date().getTime();
         runInAction(() => this.mediaState = media_state.Recording);
-        setTimeout(this.updateRecordTime, 0);
+        setTimeout(this.updateRecordTime);
         this._recorder.start();
-        setTimeout(() => this.stopRecording(), 60 * 60 * 1000); // stop after an hour
+        setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour
+    }
+
+    @action
+    stopRecording = () => {
+        if (this._recorder) {
+            this._recorder.stop();
+            this._recorder = undefined;
+            this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+            this.mediaState = media_state.Paused;
+            this._stream?.getAudioTracks()[0].stop();
+            const ind = DocUtils.ActiveRecordings.indexOf(this);
+            ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+        }
     }
 
     // context menu
@@ -243,22 +238,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         });
     }
 
-    // stops the recording
-    stopRecording = action(() => {
-        if (this._recorder) {
-            this._recorder.stop();
-            this._recorder = undefined;
-            this.dataDoc[this.fieldKey + "-duration"] =
-                (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
-            this.mediaState = media_state.Paused;
-            this._stream?.getAudioTracks()[0].stop();
-            const ind = DocUtils.ActiveRecordings.indexOf(this);
-            ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
-        }
-    });
-
     // button for starting and stopping the recording
-    recordClick = (e: React.MouseEvent) => {
+    Record = (e: React.MouseEvent) => {
         if (e.button === 0 && !e.ctrlKey) {
             this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
             e.stopPropagation();
@@ -267,12 +248,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // for play button
     Play = (e?: any) => {
-        const eleTime = this._ele!.currentTime;
-        const start = eleTime === this.timeline?.trimDuration ? this.timeline.trimStart : eleTime;
+        const eleTime = this._ele?.currentTime || 0;
+        const start = eleTime === this.timeline?.trimEnd ? this.timeline.trimStart : eleTime;
         this.playFrom(start, undefined, true);
         e?.stopPropagation?.();
     }
 
+    // pause play back
+    @action
+    Pause = () => {
+        this._ele?.pause();
+        this.mediaState = media_state.Paused;
+    }
+
     // creates a text document for dictation
     onFile = (e: any) => {
         const newDoc = CurrentUserUtils.GetNewTextDoc(
@@ -302,27 +290,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this._ele = e;
     }
 
-    // returns the path of the audio file
-    @computed get path() {
-        const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || "";
-        return path === nullAudio ? "" : path;
-    }
-
-    // returns the html audio element
-    @computed get audio() {
-        return <audio ref={this.setRef}
-            onLoadedData={action(e => {
-                const duration = this._ele?.duration;
-                if (duration && duration !== Infinity) {
-                    this.dataDoc[this.fieldKey + "-duration"] = duration;
-                }
-            })}
-            className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
-            <source src={this.path} type="audio/mpeg" />
-            Not supported.
-        </audio>;
-    }
-
     // pause the time during recording phase
     @action
     recordPause = (e: React.MouseEvent) => {
@@ -341,14 +308,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         e.stopPropagation();
     }
 
-    playing = () => this.mediaState === media_state.Playing;
     playLink = (link: Doc) => {
         if (link.annotationOn === this.rootDoc) {
             if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
                 this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
             } else {
-                this._ele!.currentTime = this.layoutDoc._currentTimecode =
-                    this.timeline?.anchorStart(link) || 0;
+                this._ele!.currentTime = this.layoutDoc._currentTimecode = this.timeline?.anchorStart(link) || 0;
             }
         } else {
             this.links
@@ -368,6 +333,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    playing = () => this.mediaState === media_state.Playing;
     isActiveChild = () => this._isAnyChildContentActive;
     timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
         this.props.whenChildContentsActiveChanged(
@@ -380,54 +346,21 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 -AudioBox.playheadWidth,
                 (-(100 - AudioBox.heightPercent) / 200) * this.props.PanelHeight()
             )
-    setAnchorTime = (time: number) => {
-        (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
-    }
+    setAnchorTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time;
 
+    timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
     timelineHeight = () =>
         (((this.props.PanelHeight() * AudioBox.heightPercent) / 100) *
             AudioBox.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}
-                {...OmitKeys(this.props, ["CollectionFreeFormDocumentView"]).omit}
-                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}
-                playFrom={this.playFrom}
-                setTime={this.setAnchorTime}
-                playing={this.playing}
-                whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
-                moveDocument={this.moveDocument}
-                addDocument={this.addDocument}
-                removeDocument={this.removeDocument}
-                ScreenToLocalTransform={this.timelineScreenToLocal}
-                Play={this.Play}
-                Pause={this.Pause}
-                isContentActive={this.props.isContentActive}
-                isAnyChildContentActive={this.isAnyChildContentActive}
-                playLink={this.playLink}
-                PanelWidth={this.timelineWidth}
-                PanelHeight={this.timelineHeight}
-                rawDuration={this.rawDuration}
-            />
-        );
-    }
-    // hides trim controls and displays new clip
+
+
     @undoBatch
-    finishTrim = action(() => {
+    finishTrim = () => { // hides trim controls and displays new clip
         this.Pause();
         this.setAnchorTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this._ele!.currentTime), this.timeline?.trimStart || 0));
         this.timeline?.StopTrimming();
-    });
+    }
+
     startTrim = (scope: TrimScope) => {
         this.Pause();
         this.timeline?.StartTrimming(scope);
@@ -444,6 +377,18 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }));
     }
 
+    setupTimelineDrop = (r: HTMLDivElement | null) => {
+        if (r && this.timeline) {
+            this._dropDisposer?.();
+            this._dropDisposer = DragManager.MakeDropTarget(r,
+                (e, de) => {
+                    const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+                    de.complete.docDragData && this.timeline!.internalDocDrop(e, de, de.complete.docDragData, xp);
+                },
+                this.layoutDoc, undefined);
+        }
+    }
+
     @computed get recordingControls() {
         return <div className="audiobox-buttons">
             <div className="audiobox-dictation" onClick={this.onFile}>
@@ -453,8 +398,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
             </div>
             {[media_state.Recording, media_state.Playing].includes(this.mediaState) ?
-                <div className="recording" onClick={(e) => e.stopPropagation()}>
-                    <div className="recording-buttons" onClick={this.recordClick}>
+                <div className="recording" onClick={e => e.stopPropagation()}>
+                    <div className="recording-buttons" onClick={this.Record}>
                         <FontAwesomeIcon
                             icon="stop"
                             size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
@@ -522,16 +467,52 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         </div>;
     }
 
-    setupTimelineDrop = (r: HTMLDivElement | null) => {
-        if (r && this.timeline) {
-            this._dropDisposer?.();
-            this._dropDisposer = DragManager.MakeDropTarget(r,
-                (e, de) => {
-                    const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
-                    de.complete.docDragData && this.timeline!.internalDocDrop(e, de, de.complete.docDragData, xp);
-                },
-                this.layoutDoc, undefined);
-        }
+    @computed get renderTimeline() {
+        return (
+            <CollectionStackedTimeline
+                ref={this._stackedTimeline}
+                {...OmitKeys(this.props, ["CollectionFreeFormDocumentView"]).omit}
+                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}
+                playFrom={this.playFrom}
+                setTime={this.setAnchorTime}
+                playing={this.playing}
+                whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
+                moveDocument={this.moveDocument}
+                addDocument={this.addDocument}
+                removeDocument={this.removeDocument}
+                ScreenToLocalTransform={this.timelineScreenToLocal}
+                Play={this.Play}
+                Pause={this.Pause}
+                isContentActive={this.props.isContentActive}
+                isAnyChildContentActive={this.isAnyChildContentActive}
+                playLink={this.playLink}
+                PanelWidth={this.timelineWidth}
+                PanelHeight={this.timelineHeight}
+                rawDuration={this.rawDuration}
+            />
+        );
+    }
+    // returns the html audio element
+    @computed get audio() {
+        return <audio ref={this.setRef}
+            onLoadedData={action(e => {
+                const duration = this._ele?.duration;
+                if (duration && duration !== Infinity) {
+                    this.dataDoc[this.fieldKey + "-duration"] = duration;
+                }
+            })}
+            className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+            <source src={this.path} type="audio/mpeg" />
+            Not supported.
+        </audio>;
     }
 
     render() {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 935c878ee..97b1aac86 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -111,7 +111,7 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
                                 verticalAlign: "center",
                                 textAlign: "center",
                                 whiteSpace: "nowrap"
-                            })
+                            });
                         }
                     }}>{label.startsWith("#") ? (null) : label}</span>
                 </div>
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3435c2a24..2befb4128 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,5 +1,6 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Tooltip } from "@material-ui/core";
 import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from 'request-promise';
@@ -8,13 +9,16 @@ import { documentSchema } from "../../../fields/documentSchemas";
 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, returnZero } from "../../../Utils";
+import { AudioField, VideoField } from "../../../fields/URLField";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
+import { DocumentType } from "../../documents/DocumentTypes";
 import { Networking } from "../../Network";
 import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { DocumentManager } from "../../util/DocumentManager";
 import { SelectionManager } from "../../util/SelectionManager";
 import { SnappingManager } from "../../util/SnappingManager";
+import { undoBatch } from "../../util/UndoManager";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline";
 import { ContextMenu } from "../ContextMenu";
@@ -22,16 +26,10 @@ import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { MarqueeAnnotator } from "../MarqueeAnnotator";
+import { AnchorMenu } from "../pdf/AnchorMenu";
 import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
-import { LinkDocPreview } from "./LinkDocPreview";
 import "./VideoBox.scss";
-import { DragManager } from "../../util/DragManager";
-import { DocumentManager } from "../../util/DocumentManager";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { Tooltip } from "@material-ui/core";
-import { AnchorMenu } from "../pdf/AnchorMenu";
-import { undoBatch } from "../../util/UndoManager";
 const path = require('path');
 
 type VideoDocument = makeInterface<[typeof documentSchema]>;
@@ -40,14 +38,30 @@ const VideoDocument = makeInterface(documentSchema);
 @observer
 export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, VideoDocument>(VideoDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+    static async convertDataUri(imageUri: string, returnedFilename: string) {
+        try {
+            const posting = Utils.prepend("/uploadURI");
+            const returnedUri = await rp.post(posting, {
+                body: {
+                    uri: imageUri,
+                    name: returnedFilename
+                },
+                json: true,
+            });
+            return returnedUri;
+
+        } catch (e) {
+            console.log("VideoBox :" + e);
+        }
+    }
     static _youtubeIframeCounter: number = 0;
-    static Instance: VideoBox;
     static heightPercent = 60; // height of timeline in percent of height of videoBox.
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _youtubePlayer: YT.Player | undefined = undefined;
     private _videoRef: HTMLVideoElement | null = null;
     private _youtubeIframeId: number = -1;
     private _youtubeContentCreated = false;
+    private _audioPlayer: HTMLAudioElement | null = null;
     private _stackedTimeline = React.createRef<CollectionStackedTimeline>();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -62,47 +76,52 @@ 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;
+    @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+
+    @computed get youtubeVideoId() {
+        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+        return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+    }
+    // returns the path of the audio file
+    @computed get audiopath() {
+        const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
+        const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
+        return field?.url.href ?? vfield?.url.href ?? "";
     }
 
+    private get timeline() { return this._stackedTimeline.current; }
     private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; }
     public get player(): HTMLVideoElement | null { return this._videoRef; }
 
-    constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
-        super(props);
-        VideoBox.Instance = this;
-    }
-
-    getAnchor = () => {
-        const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
-        const marquee = AnchorMenu.Instance.GetAnchor?.();
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+    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.
+        if (this.youtubeVideoId) {
+            const youtubeaspect = 400 / 315;
+            const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+            const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+            if (!nativeWidth || !nativeHeight) {
+                if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
+                Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
+                this.layoutDoc._height = (this.layoutDoc._width || 0) / youtubeaspect;
+            }
+        }
     }
 
-    videoLoad = () => {
-        const aspect = this.player!.videoWidth / this.player!.videoHeight;
-        Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
-        Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
-        this.layoutDoc._height = (this.layoutDoc._width || 0) / aspect;
-        if (Number.isFinite(this.player!.duration)) {
-            this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration;
-        }
+    componentWillUnmount() {
+        this.Pause();
+        Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
     }
 
     @action public Play = (update: boolean = true) => {
         this._playing = true;
+        const eleTime = this.player?.currentTime || 0;
+        const start = eleTime >= (this.timeline?.trimEnd || 0) ? this.timeline?.trimStart || 0 : eleTime;
         try {
             this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
-            update && this.player && this.playFrom(this.player.currentTime);
+            update && this.player && this.playFrom(start, undefined, true);
             update && this._audioPlayer?.play();
             update && this._youtubePlayer?.playVideo();
             this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
@@ -190,7 +209,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    private createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+    createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
         const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
         const width = this.layoutDoc._width || 1;
         const height = this.layoutDoc._height || 0;
@@ -208,11 +227,25 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
     }
 
+    getAnchor = () => {
+        const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
+        const marquee = AnchorMenu.Instance.GetAnchor?.();
+        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+    }
+
+    videoLoad = () => {
+        const aspect = this.player!.videoWidth / this.player!.videoHeight;
+        Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
+        Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
+        this.layoutDoc._height = (this.layoutDoc._width || 0) / aspect;
+        if (Number.isFinite(this.player!.duration)) {
+            this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration;
+        }
+    }
+
     @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) {
@@ -220,25 +253,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    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.
-        if (this.youtubeVideoId) {
-            const youtubeaspect = 400 / 315;
-            const nativeWidth = Doc.NativeWidth(this.layoutDoc);
-            const nativeHeight = Doc.NativeHeight(this.layoutDoc);
-            if (!nativeWidth || !nativeHeight) {
-                if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
-                Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
-                this.layoutDoc._height = (this.layoutDoc._width || 0) / youtubeaspect;
-            }
-        }
-    }
-
-    componentWillUnmount() {
-        this.Pause();
-        Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
-    }
-
     @action
     setVideoRef = (vref: HTMLVideoElement | null) => {
         this._videoRef = vref;
@@ -252,23 +266,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    public static async convertDataUri(imageUri: string, returnedFilename: string) {
-        try {
-            const posting = Utils.prepend("/uploadURI");
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename
-                },
-                json: true,
-            });
-            return returnedUri;
-
-        } catch (e) {
-            console.log("VideoBox :" + e);
-        }
-    }
-
     specificContextMenu = (e: React.MouseEvent): void => {
         const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         if (field) {
@@ -294,48 +291,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    // returns the path of the audio file
-    @computed get audiopath() {
-        const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
-        const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
-        return field?.url.href ?? vfield?.url.href ?? "";
-    }
-    // ref for updating time
-    _audioPlayer: HTMLAudioElement | null = null;
-    setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
-    @computed get content() {
-        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
-        const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
-        return !field ? <div key="loading">Loading</div> :
-            <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}>
-                <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
-                    <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
-                        style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
-                        onCanPlay={this.videoLoad}
-                        controls={VideoBox._nativeControls}
-                        onPlay={() => this.Play()}
-                        onSeeked={this.updateTimecode}
-                        onPause={() => this.Pause()}
-                        onClick={e => e.preventDefault()}>
-                        <source src={field.url.href} type="video/mp4" />
-                        Not supported.
-                    </video>
-                    {!this.audiopath || this.audiopath === field.url.href ? (null) :
-                        <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
-                            <source src={this.audiopath} type="audio/mpeg" />
-                            Not supported.
-                        </audio>}
-                </div>
-            </div>;
-    }
-
-    @computed get youtubeVideoId() {
-        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
-        return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
-    }
-
-    @action youtubeIframeLoaded = (e: any) => {
+    @action
+    youtubeIframeLoaded = (e: any) => {
         if (!this._youtubeContentCreated) {
             this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
             return;
@@ -344,7 +301,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         this.loadYouTube(e.target);
     }
-    private loadYouTube = (iframe: any) => {
+    loadYouTube = (iframe: any) => {
         let started = true;
         const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
             if (started && event.data === YT.PlayerState.PLAYING) {
@@ -376,39 +333,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             });
         }
     }
-    private get uIButtons() {
-        const curTime = (this.layoutDoc._currentTimecode || 0);
-        const nonNativeControls = [
-            <Tooltip title={<div className="dash-tooltip">{"playback"}</div>} key="play" placement="bottom">
-                <div className="videoBox-play" onPointerDown={this.onPlayDown} >
-                    <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
-                </div>
-            </Tooltip>,
-            <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} key="time" placement="bottom">
-                <div className="videoBox-time" onPointerDown={this.onResetDown} >
-                    <span>{formatTime(curTime)}</span>
-                    <span style={{ fontSize: 8 }}>{" " + Math.floor((curTime - Math.trunc(curTime)) * 100).toString().padStart(2, "0")}</span>
-                </div>
-            </Tooltip>,
-            <Tooltip title={<div className="dash-tooltip">{"view full screen"}</div>} key="full" placement="bottom">
-                <div className="videoBox-full" onPointerDown={this.FullScreen}>
-                    <FontAwesomeIcon icon="expand" size="lg" />
-                </div>
-            </Tooltip>];
-        return <div className="videoBox-ui">
-            {[...(VideoBox._nativeControls ? [] : nonNativeControls),
-            <Tooltip title={<div className="dash-tooltip">{"snapshot current frame"}</div>} key="snap" placement="bottom">
-                <div className="videoBox-snapshot" onPointerDown={this.onSnapshotDown} >
-                    <FontAwesomeIcon icon="camera" size="lg" />
-                </div>
-            </Tooltip>,
-            <Tooltip title={<div className="dash-tooltip">{"show annotation timeline"}</div>} key="timeline" placement="bottom">
-                <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}>
-                    <FontAwesomeIcon icon="eye" size="lg" />
-                </div>
-            </Tooltip>,]}
-        </div>;
-    }
 
     onPlayDown = () => this._playing ? this.Pause() : this.Play();
 
@@ -425,10 +349,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }, emptyFunction, () => this.Snapshot());
     }
 
-    onTimelineHdlDown = action((e: React.PointerEvent) => {
+    @action
+    onTimelineHdlDown = (e: React.PointerEvent) => {
         this._clicking = true;
         setupMoveUpEvents(this, e,
-            action((e: PointerEvent) => {
+            action(encodeURIComponent => {
                 this._clicking = false;
                 if (this.props.isContentActive()) {
                     const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
@@ -440,28 +365,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
                 setTimeout(action(() => this._clicking = false), 500);
             }, this.props.isContentActive(), this.props.isContentActive());
-    });
+    }
 
     onResetDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e,
-            (e: PointerEvent) => {
+            e => {
                 this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
                 e.stopImmediatePropagation();
                 return false;
             },
             emptyFunction,
-            (e: PointerEvent) => this.layoutDoc._currentTimecode = 0);
-    }
-
-    @computed get youtubeContent() {
-        this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
-        this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
-        const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
-        const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0)));
-        return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
-            onPointerLeave={this.updateTimecode}
-            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
-            src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+            e => this.layoutDoc._currentTimecode = 0);
     }
 
     @action.bound
@@ -472,30 +386,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return this.addDocument(doc);
     }
 
-    get timeline() { return this._stackedTimeline.current; }
     // play back the video from time
     @action
-    playFrom = (seekTimeInSeconds: number, endTime?: number) => {
+    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
         clearTimeout(this._playRegionTimer);
         if (Number.isNaN(this.player?.duration)) {
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
         }
         else if (this.player) {
-            const end = Math.min(this.timeline?.trimEnd ?? this.duration, endTime ?? this.timeline?.trimEnd ?? this.duration);
+            const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
             const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
-            this._playRegionDuration = end - seekTimeInSeconds;
-            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.duration)) {
+            this._playRegionDuration = end - start;
+            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
                 this.player.currentTime = start;
                 this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
                 this.player.play();
                 this._audioPlayer?.play();
-                runInAction(() => this._playing = true);
-                if (endTime !== this.duration) {
-                    return this._playRegionTimer =
-                        setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration
-                }
-            } else if (seekTimeInSeconds < 0 && seekTimeInSeconds > -1) {
-                setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+                this._playing = true;
+                this._playRegionTimer = setTimeout(
+                    () => {
+                        if (fullPlay) this.setAnchorTime(this.timeline?.trimStart || 0);
+                        this.Pause();
+                    }, this._playRegionDuration * 1000);
             } else {
                 this.Pause();
             }
@@ -507,12 +419,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.Pause();
         this._stackedTimeline.current?.StopTrimming();
     });
+
     startTrim = (scope: TrimScope) => {
         this.Pause();
         this._stackedTimeline.current?.StartTrimming(scope);
     }
 
-
     playLink = (doc: Doc) => {
         const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0));
         const endTime = this._stackedTimeline.current?.anchorEnd(doc);
@@ -522,14 +434,109 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    playing = () => this._playing;
+    marqueeDown = (e: React.PointerEvent) => {
+        if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+            setupMoveUpEvents(this, e, action(e => {
+                MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+                this._marqueeing = [e.clientX, e.clientY];
+                return true;
+            }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+        }
+    }
+
+    @action
+    finishMarquee = () => {
+        this._marqueeing = undefined;
+        this.props.select(true);
+    }
+
     timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
     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;
-    }
+    setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
     timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
-    trimEndFunc = () => this.duration;
+    playing = () => this._playing;
+
+    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
+    scaling = () => this.props.scaling?.() || 1;
+    panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
+    panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+    screenToLocalTransform = () => {
+        const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
+        return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
+    }
+    marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
+    marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+    timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+
+    @computed get content() {
+        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
+        const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
+        const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+        return !field ? <div key="loading">Loading</div> :
+            <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}>
+                <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
+                    <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+                        style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
+                        onCanPlay={this.videoLoad}
+                        controls={VideoBox._nativeControls}
+                        onPlay={() => this.Play()}
+                        onSeeked={this.updateTimecode}
+                        onPause={() => this.Pause()}
+                        onClick={e => e.preventDefault()}>
+                        <source src={field.url.href} type="video/mp4" />
+                        Not supported.
+                    </video>
+                    {!this.audiopath || this.audiopath === field.url.href ? (null) :
+                        <audio ref={e => this._audioPlayer = e} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+                            <source src={this.audiopath} type="audio/mpeg" />
+                            Not supported.
+                        </audio>}
+                </div>
+            </div>;
+    }
+    @computed get youtubeContent() {
+        this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
+        this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
+        const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+        const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0)));
+        return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+            onPointerLeave={this.updateTimecode}
+            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+            src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+    }
+    @computed get uIButtons() {
+        const curTime = (this.layoutDoc._currentTimecode || 0);
+        const nonNativeControls = [
+            <Tooltip title={<div className="dash-tooltip">{"playback"}</div>} key="play" placement="bottom">
+                <div className="videoBox-play" onPointerDown={this.onPlayDown} >
+                    <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
+                </div>
+            </Tooltip>,
+            <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} key="time" placement="bottom">
+                <div className="videoBox-time" onPointerDown={this.onResetDown} >
+                    <span>{formatTime(curTime)}</span>
+                    <span style={{ fontSize: 8 }}>{" " + Math.floor((curTime - Math.trunc(curTime)) * 100).toString().padStart(2, "0")}</span>
+                </div>
+            </Tooltip>,
+            <Tooltip title={<div className="dash-tooltip">{"view full screen"}</div>} key="full" placement="bottom">
+                <div className="videoBox-full" onPointerDown={this.FullScreen}>
+                    <FontAwesomeIcon icon="expand" size="lg" />
+                </div>
+            </Tooltip>];
+        return <div className="videoBox-ui">
+            {[...(VideoBox._nativeControls ? [] : nonNativeControls),
+            <Tooltip title={<div className="dash-tooltip">{"snapshot current frame"}</div>} key="snap" placement="bottom">
+                <div className="videoBox-snapshot" onPointerDown={this.onSnapshotDown} >
+                    <FontAwesomeIcon icon="camera" size="lg" />
+                </div>
+            </Tooltip>,
+            <Tooltip title={<div className="dash-tooltip">{"show annotation timeline"}</div>} key="timeline" placement="bottom">
+                <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}>
+                    <FontAwesomeIcon icon="eye" size="lg" />
+                </div>
+            </Tooltip>,]}
+        </div>;
+    }
     @computed get renderTimeline() {
         return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
             <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
@@ -552,41 +559,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 Pause={this.Pause}
                 playLink={this.playLink}
                 PanelHeight={this.timelineHeight}
-                rawDuration={this.duration}
+                rawDuration={this.rawDuration}
             />
         </div>;
     }
-
     @computed get annotationLayer() {
         return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
     }
-
-    marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
-            setupMoveUpEvents(this, e, action(e => {
-                MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                this._marqueeing = [e.clientX, e.clientY];
-                return true;
-            }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
-        }
-    }
-
-    finishMarquee = action(() => {
-        this._marqueeing = undefined;
-        this.props.select(true);
-    });
-
-    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-    scaling = () => this.props.scaling?.() || 1;
-    panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
-    panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
-    screenToLocalTransform = () => {
-        const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
-        return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
-    }
-    marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
-    marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
-    timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
     render() {
         const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
         const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
-- 
cgit v1.2.3-70-g09d2


From 6cec290f98103827727905874c5a9c5ced0bcca8 Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Thu, 14 Oct 2021 16:21:03 -0400
Subject: pre-redesign changes (zoom but no scroll)

---
 src/client/views/AudioWaveform.tsx                 |  26 +--
 .../collections/CollectionStackedTimeline.scss     | 167 ++++++++++---------
 .../collections/CollectionStackedTimeline.tsx      |  46 +++---
 src/client/views/nodes/AudioBox.scss               | 178 +++++++++++++--------
 src/client/views/nodes/AudioBox.tsx                |  14 ++
 5 files changed, 256 insertions(+), 175 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 270b3869c..7b9b1aa81 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -18,6 +18,7 @@ export interface AudioWaveformProps {
     layoutDoc: Doc;
     clipStart: number;
     clipEnd: number;
+    zoomFactor: number;
     PanelHeight: () => number;
 }
 
@@ -28,28 +29,30 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
     @computed get waveHeight() { return Math.max(50, this.props.PanelHeight()); }
     @computed get clipStart() { return this.props.clipStart; }
     @computed get clipEnd() { return this.props.clipEnd; }
-    @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)], listSpec("number"), []); }
+    @computed get zoomFactor() { return this.props.zoomFactor; }
+    @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); }
 
-    audioBucketField = (start: number, end: number) => "audioBuckets/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_");
+    audioBucketField = (start: number, end: number, zoomFactor: number) => "audioBuckets/" + "/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_") + "/" + (zoomFactor * 10);
 
     componentWillUnmount() {
         this._disposer?.();
     }
     componentDidMount() {
-        this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd) }),
-            ({ clipStart, clipEnd, fieldKey }) => {
+        console.log("new waveform");
+        this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }),
+            ({ clipStart, clipEnd, fieldKey, zoomFactor }) => {
                 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.
-                    const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration)], listSpec("number"));
+                    const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec("number"));
                     this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length));
-                    setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd));
+                    setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor));
                 }
             }, { fireImmediately: true });
 
     }
 
     // decodes the audio file into peaks for generating the waveform
-    createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number) => {
+    createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => {
         axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then(
             (response) => {
                 const context = new window.AudioContext();
@@ -60,12 +63,15 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                         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 numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor);
+
+                        console.log(numBuckets);
 
                         const bucketDataSize = Math.floor(
-                            decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS
+                            decodedAudioData.length / numBuckets
                         );
                         const brange = Array.from(Array(bucketDataSize));
-                        const bucketList = numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map(
+                        const bucketList = numberRange(numBuckets).map(
                             (i: number) =>
                                 brange.reduce(
                                     (p, x, j) =>
@@ -87,7 +93,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                 <Waveform
                     color={Colors.MEDIUM_BLUE}
                     height={this.waveHeight}
-                    barWidth={0.1}
+                    barWidth={10.0 / this.audioBuckets.length}
                     pos={this.props.duration}
                     duration={this.props.duration}
                     peaks={this.audioBuckets}
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 7a957ae5c..0ec5f9aef 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -1,94 +1,109 @@
 @import "../global/globalCssVariables.scss";
 
 .collectionStackedTimeline {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  z-index: 1000;
-  top: 0px;
-
-  .collectionStackedTimeline-trim-shade {
     position: absolute;
+    width: 100%;
     height: 100%;
-    background-color: $dark-gray;
-    opacity: 0.3;
-  }
+    z-index: 1000;
+    top: 0px;
+    // overflow-x: scroll;
 
-  .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%;
+    ::-webkit-scrollbar {
+        position: relative;
+        -webkit-appearance: none;
+        height: 5px;
+        background-color: white;
+    }
 
-    .collectionStackedTimeline-trim-handle {
-      background-color: $medium-blue;
-      height: 100%;
-      width: 5px;
-      cursor: ew-resize;
+    ::-webkit-scrollbar-thumb {
+        position: relative;
+        -webkit-appearance: none;
+        height: 5px;
+        background-color: $medium-gray;
     }
-  }
 
-  .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-trim-shade {
+        position: absolute;
+        height: 100%;
+        background-color: $dark-gray;
+        opacity: 0.3;
+    }
 
-  .collectionStackedTimeline-current {
-    width: 1px;
-    height: 100%;
-    background-color: $pink;
-    position: absolute;
-    top: 0px;
-    pointer-events: none;
-  }
+    .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-marker-timeline {
-    position: absolute;
-    top: 2.5%;
-    height: 95%;
-    border-radius: 4px;
-    background: $light-gray;
-    &:hover {
-      opacity: 1;
+        .collectionStackedTimeline-trim-handle {
+            background-color: $medium-blue;
+            height: 100%;
+            width: 5px;
+            cursor: ew-resize;
+        }
     }
 
-    .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-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-resizer {
-      right: 0;
+
+    .collectionStackedTimeline-current {
+        width: 1px;
+        height: 100%;
+        background-color: $pink;
+        position: absolute;
+        top: 0px;
+        pointer-events: none;
     }
-    .collectionStackedTimeline-left-resizer {
-      left: 0;
+
+    .collectionStackedTimeline-marker-timeline {
+        position: absolute;
+        top: 2.5%;
+        height: 95%;
+        border-radius: 4px;
+        background: $light-gray;
+        &:hover {
+            opacity: 1;
+        }
+
+        .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;
+        }
+        .collectionStackedTimeline-left-resizer {
+            left: 0;
+        }
     }
-  }
 
-  .collectionStackedTimeline-waveform {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-    top: 0;
-    left: 0;
-    pointer-events: none;
-  }
+    .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 b5c266526..5c02611bb 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -100,6 +100,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }
 
+    @computed get zoomFactor() { return this._zoomFactor }
+
     constructor(props: any) {
         super(props);
         // onClick play scripts
@@ -120,6 +122,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     componentDidMount() {
+        this.layoutDoc.clipStart = 0;
+        this.layoutDoc.clipEnd = this.props.rawDuration;
         document.addEventListener("keydown", this.keyEvents, true);
     }
 
@@ -135,7 +139,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     @action
     public StartTrimming(scope: TrimScope) {
-        console.log(this.minTrimLength);
         this._trimStart = this.clipStart;
         this._trimEnd = this.clipEnd;
         this._trimming = scope;
@@ -148,8 +151,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     @action
-    public setZoom(change: number) {
-        this._zoomFactor = Math.max(1, this._zoomFactor + change);
+    public setZoom(zoom: number) {
+        this._zoomFactor = zoom;
+        // console.log(this._timeline?.scrollWidth);
     }
 
     anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
@@ -216,7 +220,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
     @action
     onPointerDownTimeline = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
+        const scrollLeft = this._timeline?.scrollLeft;
         const clientX = e.clientX;
+        const diff = rect ? clientX - rect?.x : null;
         const shiftKey = e.shiftKey;
         if (rect && this.props.isContentActive()) {
             const wasPlaying = this.props.playing();
@@ -529,7 +535,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
     @computed get renderAudioWaveform() {
         return !this.props.mediaPath ? null : (
-            <div className="collectionStackedTimeline-waveform">
+            <div className="collectionStackedTimeline-waveform" style={{ width: `${this.zoomFactor * 100}%`, overflowX: "scroll" }}>
                 <AudioWaveform
                     rawDuration={this.props.rawDuration}
                     duration={this.clipDuration}
@@ -538,6 +544,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                     clipStart={this.clipStart}
                     clipEnd={this.clipEnd}
                     PanelHeight={this.timelineContentHeight}
+                    zoomFactor={this.zoomFactor}
                 />
             </div>
         );
@@ -556,7 +563,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     render() {
-        const timelineContentWidth = this.props.PanelWidth();
+        const timelineContentWidth = this.props.PanelWidth() * this.zoomFactor;
         const overlaps: {
             anchorStartTime: number;
             anchorEndTime: number;
@@ -573,8 +580,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
                 ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
                 onClick={(e) => this.isContentActive() && StopEvent(e)}
                 onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)}
-                style={{ width: `${this._zoomFactor * 100}%` }}
+                onPointerEnter={(e) => { console.log("scroll"); e.preventDefault(); e.stopPropagation(); }}
             >
+
                 {drawAnchors.map((d) => {
                     const start = this.anchorStart(d.anchor);
                     const end = this.anchorEnd(
@@ -868,19 +876,19 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                 {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)}
-                            />
-                        </>
-                    )}
+                    <>
+                        <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)}
+                        />
+                    </>
+                )}
             </>
         );
     }
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index b33c7f506..a2fdd38e5 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,51 +1,50 @@
 @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-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-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,
+    .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 {
+    .audiobox-record-interactive,
+    .audiobox-record {
         pointer-events: all;
         cursor: pointer;
         width: 100%;
@@ -59,45 +58,46 @@
         color: white;
         font-weight: bold;
         background-color: $dark-gray;
-   }
+    }
 
-   .audiobox-record {
+    .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;
+    .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;
-      width: 100%;
-      font-size: $large-header;
-      text-align: center;
-    }
+        .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;
-      }
-    }
+        .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;
+        .time,
+        .recording-buttons {
+            display: flex;
+            align-items: center;
+            padding: 5px;
+        }
     }
-  }
     .audiobox-buttons {
         display: flex;
         width: 100%;
@@ -267,6 +267,44 @@
                 right: 2px;
             }
 
+            .toolbar-slider {
+                position: absolute;
+                top: 75px;
+                left: 70px;
+            }
+
+            input[type="range"] {
+                width: calc(100% - 100px);
+                height: 16px;
+                -webkit-appearance: none;
+                background: none;
+            }
+
+            input[type="range"]:focus {
+                outline: none;
+            }
+
+            input[type="range"]::-webkit-slider-runnable-track {
+                width: 100%;
+                height: 5px;
+                cursor: pointer;
+                box-shadow: 0;
+                background: #dfdfdf;
+                border-radius: 3px;
+            }
+
+            input[type="range"]::-webkit-slider-thumb {
+                box-shadow: 0;
+                border: 0;
+                height: 7px;
+                width: 7px;
+                border-radius: 10px;
+                background: #4476f7;
+                cursor: pointer;
+                -webkit-appearance: none;
+                margin: -1px;
+            }
+
             .audiobox-zoom {
                 bottom: 0;
                 left: 30px;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 2eb34d27a..f2001adcd 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -45,6 +45,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     public static Enabled = false;
     static playheadWidth = 40; // width of playhead
     static heightPercent = 75; // height of timeline in percent of height of audioBox.
+    static zoomInterval = 0.1;
     @observable static _scrubTime = 0;
 
     _dropDisposer?: DragManager.DragDropDisposer;
@@ -374,6 +375,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }));
     }
 
+    zoom = (zoom: number) => {
+        this.timeline?.setZoom(zoom);
+    }
+
     setupTimelineDrop = (r: HTMLDivElement | null) => {
         if (r && this.timeline) {
             this._dropDisposer?.();
@@ -437,6 +442,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                         icon={this.mediaState === media_state.Paused ? "play" : "pause"}
                         size={"1x"} />
                 </div>
+
                 <div className="audiobox-buttons"
                     title={this.timeline?.IsTrimming !== TrimScope.None ? "finish" : "trim"}
                     onPointerDown={this.onClipPointerDown}
@@ -445,6 +451,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                         icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"}
                         size={"1x"} />
                 </div>
+
                 <div className="audiobox-timeline"
                     style={{
                         left: AudioBox.playheadWidth,
@@ -457,6 +464,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 <div className="audioBox-current-time">
                     {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}
                 </div>
+
+                {/* <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
+                    className="toolbar-slider" id="zoom-slider"
+                    onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(e.target.value); }}
+                /> */}
+
                 <div className="audioBox-total-time">
                     {this.timeline && formatTime(Math.round(NumCast(this.timeline?.clipDuration)))}
                 </div>
-- 
cgit v1.2.3-70-g09d2


From 3d76222b8cd41423bcdd7a91e4aee2826f329d9f Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Sat, 23 Oct 2021 21:36:30 -0400
Subject: audiobox UI update

---
 src/client/views/AudioWaveform.tsx                 |   3 -
 .../collections/CollectionStackedTimeline.scss     |   3 +-
 .../collections/CollectionStackedTimeline.tsx      |  10 +-
 src/client/views/nodes/AudioBox.scss               | 349 ++++++---------------
 src/client/views/nodes/AudioBox.tsx                | 120 ++++---
 src/client/views/nodes/ComparisonBox.tsx           |   4 +-
 6 files changed, 163 insertions(+), 326 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 7b9b1aa81..ca1dd6f36 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -38,7 +38,6 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
         this._disposer?.();
     }
     componentDidMount() {
-        console.log("new waveform");
         this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }),
             ({ clipStart, clipEnd, fieldKey, zoomFactor }) => {
                 if (!this.props.layoutDoc[fieldKey]) {
@@ -65,8 +64,6 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                         const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length));
                         const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor);
 
-                        console.log(numBuckets);
-
                         const bucketDataSize = Math.floor(
                             decodedAudioData.length / numBuckets
                         );
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 0ec5f9aef..34679e9e3 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -4,9 +4,8 @@
     position: absolute;
     width: 100%;
     height: 100%;
+    background: $off-white;
     z-index: 1000;
-    top: 0px;
-    // overflow-x: scroll;
 
     ::-webkit-scrollbar {
         position: relative;
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 5c02611bb..c79d21418 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -153,7 +153,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
     @action
     public setZoom(zoom: number) {
         this._zoomFactor = zoom;
-        // console.log(this._timeline?.scrollWidth);
     }
 
     anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
@@ -534,8 +533,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
         );
     }
     @computed get renderAudioWaveform() {
+        console.log(this.props.mediaPath)
         return !this.props.mediaPath ? null : (
-            <div className="collectionStackedTimeline-waveform" style={{ width: `${this.zoomFactor * 100}%`, overflowX: "scroll" }}>
+            <div className="collectionStackedTimeline-waveform">
                 <AudioWaveform
                     rawDuration={this.props.rawDuration}
                     duration={this.clipDuration}
@@ -580,7 +580,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                 ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
                 onClick={(e) => this.isContentActive() && StopEvent(e)}
                 onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)}
-                onPointerEnter={(e) => { console.log("scroll"); e.preventDefault(); e.stopPropagation(); }}
+                style={{ height: this.props.PanelHeight(), width: this.props.PanelWidth() }}
             >
 
                 {drawAnchors.map((d) => {
@@ -591,10 +591,10 @@ export class CollectionStackedTimeline extends CollectionSubView<
                     );
                     if (end < this.clipStart || start > this.clipEnd) return (null);
                     const left = Math.max((start - this.clipStart) / this.clipDuration * timelineContentWidth, 0);
-                    const top = (d.level / maxLevel) * this.timelineContentHeight();
+                    const top = (d.level / maxLevel) * this.props.PanelHeight();
                     const timespan = Math.max(0, end - this.clipStart) - Math.max(0, start - this.clipStart);
                     const width = (timespan / this.clipDuration) * timelineContentWidth;
-                    const height = this.timelineContentHeight() / maxLevel;
+                    const height = this.props.PanelHeight() / maxLevel;
                     return this.props.Document.hideAnchors ? null : (
                         <div
                             className={"collectionStackedTimeline-marker-timeline"}
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index a2fdd38e5..391507796 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,283 +1,144 @@
 @import "../global/globalCssVariables.scss";
 
-.audiobox-container,
-.audiobox-container-interactive {
+.audiobox-container {
     width: 100%;
     height: 100%;
     position: inherit;
     display: flex;
     position: relative;
     cursor: default;
+}
+
+.audiobox-recorder {
+    display: flex;
+    flex-direction: row;
+    overflow: hidden;
+    width: 100%;
+    height: 100%;
+    cursor: pointer;
 
-    .audiobox-buttons {
+    .audiobox-dictation {
+        width: 40px;
+        background: $medium-gray;
+        color: $dark-gray;
         display: flex;
-        width: 100%;
+        justify-content: center;
         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;
-            }
+        &:hover {
+            color: $black;
         }
     }
 
-    .audiobox-control,
-    .audiobox-control-interactive {
-        top: 0;
-        max-height: 32px;
+    .audiobox-start-record {
+        color: $white;
+        background: $dark-gray;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: $body-text;
         width: 100%;
-        display: inline-block;
-        pointer-events: none;
-    }
+        height: 100%;
+        gap: 5px;
 
-    .audiobox-control-interactive {
-        pointer-events: all;
+        &:hover {
+            background: $black;
+        }
     }
 
-    .audiobox-record-interactive,
-    .audiobox-record {
-        pointer-events: all;
-        cursor: pointer;
-        width: 100%;
-        height: 100%;
-        position: relative;
+    .recording-controls {
         display: flex;
         flex-direction: row;
         align-items: center;
         justify-content: center;
-        gap: 10px;
-        color: white;
-        font-weight: bold;
-        background-color: $dark-gray;
-    }
-
-    .audiobox-record {
-        pointer-events: none;
-    }
-
-    .recording {
-        margin-top: auto;
-        margin-bottom: auto;
+        gap: 5px;
         width: 100%;
         height: 100%;
-        position: relative;
-        padding-right: 5px;
-        display: flex;
-        background-color: $medium-blue;
+        background: $dark-gray;
+        color: white;
 
-        .time {
-            position: relative;
-            width: 100%;
+        .record-timecode {
             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 {
+        .record-button {
+            cursor: pointer;
+            width: 30px;
+            height: 30px;
+            border-radius: 50%;
+            background: $dark-gray;
             display: flex;
             align-items: center;
-            padding: 5px;
-        }
-    }
-    .audiobox-buttons {
-        display: flex;
-        width: 100%;
-        align-items: center;
-        height: 100%;
+            justify-content: center;
 
-        .audiobox-dictation {
-            position: relative;
-            width: 30px;
-            height: 100%;
-            align-items: center;
-            display: inherit;
-            background: $medium-gray;
-            left: 0px;
-            color: $dark-gray;
+            svg {
+                width: 15px;
+            }
 
             &:hover {
-                color: $black;
-                cursor: pointer;
+                background: $black;
             }
         }
     }
+}
 
-    .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-file {
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    align-items: space-between;
+    background: $dark-gray;
+    width: 100%;
+    height: 100%;
+    color: $white;
 
-    .recording {
-        margin-top: auto;
-        margin-bottom: auto;
-        width: 100%;
-        height: 100%;
-        position: relative;
-        padding-right: 5px;
+    .audiobox-controls {
         display: flex;
         flex-direction: row;
-        justify-content: center;
+        justify-content: space-between;
         align-items: center;
-        gap: 7px;
-        background-color: $medium-blue;
-        padding: 10px;
-
-        .time {
-            position: relative;
-            height: 100%;
-            width: 100%;
-            font-size: 16px;
-            text-align: center;
-            display: flex;
-            justify-content: center;
-            align-items: center;
-            font-weight: bold;
-        }
-
-        .buttons {
-            cursor: pointer;
-            position: relative;
-            margin-top: auto;
-            margin-bottom: auto;
-            width: 25px;
-            width: 25px;
-            padding: 5px;
-            color: $dark-gray;
-
-            &:hover {
-                color: $black;
-            }
-        }
-    }
-
-    .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;
-        }
+        height: 30px;
 
-        .audiobox-player {
-            margin-top: auto;
-            margin-bottom: auto;
-            width: 100%;
-            position: relative;
-            padding-right: 5px;
+        .controls-left {
             display: flex;
-            flex-direction: column;
-            justify-content: center;
+            flex-direction: row;
+            width: 100px;
 
-            .audiobox-buttons {
-                position: relative;
-                margin-top: auto;
-                margin-bottom: auto;
-                width: 30px;
-                height: 30px;
+            .audiobox-button {
+                margin: 2.5px;
+                cursor: pointer;
+                width: 25px;
+                height: 25px;
                 border-radius: 50%;
-                background-color: $dark-gray;
-                color: $white;
+                background: $dark-gray;
                 display: flex;
                 align-items: center;
                 justify-content: center;
-                left: 5px;
-
-                &:hover {
-                    background-color: $black;
-                }
 
                 svg {
-                    width: 100%;
-                    position: absolute;
-                    border-width: "thin";
-                    border-color: "white";
+                    width: 15px;
                 }
-            }
-
-            .audiobox-dictation {
-                position: relative;
-                margin-top: auto;
-                margin-bottom: auto;
-                width: 25px;
-                align-items: center;
-                display: inherit;
-                background: $medium-gray;
-            }
-
-            .audiobox-timeline {
-                position: absolute;
-                top: 0;
-                background: white;
-                height: 100%;
-                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;
+                &:hover {
+                    background: $black;
+                }
             }
+        }
 
-            .audioBox-total-time {
-                right: 2px;
-            }
+        .controls-right {
+            display: flex;
+            flex-direction: row;
 
-            .toolbar-slider {
-                position: absolute;
-                top: 75px;
-                left: 70px;
+            svg {
+                width: 10px;
             }
 
             input[type="range"] {
-                width: calc(100% - 100px);
-                height: 16px;
+                width: 70px;
                 -webkit-appearance: none;
                 background: none;
+                margin: 5px;
             }
 
             input[type="range"]:focus {
@@ -286,52 +147,46 @@
 
             input[type="range"]::-webkit-slider-runnable-track {
                 width: 100%;
-                height: 5px;
+                height: 6px;
                 cursor: pointer;
                 box-shadow: 0;
-                background: #dfdfdf;
+                background: $light-gray;
                 border-radius: 3px;
             }
 
             input[type="range"]::-webkit-slider-thumb {
                 box-shadow: 0;
                 border: 0;
-                height: 7px;
-                width: 7px;
+                height: 10px;
+                width: 10px;
                 border-radius: 10px;
-                background: #4476f7;
+                background: $medium-blue;
                 cursor: pointer;
                 -webkit-appearance: none;
-                margin: -1px;
-            }
-
-            .audiobox-zoom {
-                bottom: 0;
-                left: 30px;
-                width: 70px;
+                margin: -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-playback {
+        width: 100%;
+        height: calc(100% - 50px);
 
-    .audiobox-container .audiobox-record,
-    .audiobox-container-interactive .audiobox-record {
-        font-size: 3em;
+        .audiobox-timeline {
+            height: 100%;
+            width: 100%;
+            background: $white;
+        }
     }
 
-    .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;
+    .audiobox-timecodes {
+        display: flex;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+        width: 100%;
+        height: 20px;
+        padding: 3px;
+        font-size: $small-text;
     }
 }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index f2001adcd..48e324971 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -43,8 +43,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         AudioBox._scrubTime = timeInMillisFrom1970;
     });
     public static Enabled = false;
-    static playheadWidth = 40; // width of playhead
-    static heightPercent = 75; // height of timeline in percent of height of audioBox.
+    static topControlsHeight = 30; // width of playhead
+    static bottomControlsHeight = 20; // height of timeline in percent of height of audioBox.
     static zoomInterval = 0.1;
     @observable static _scrubTime = 0;
 
@@ -338,19 +338,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
         this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)
     timelineScreenToLocal = () =>
-        this.props
-            .ScreenToLocalTransform()
-            .translate(
-                -AudioBox.playheadWidth,
-                (-(100 - AudioBox.heightPercent) / 200) * this.props.PanelHeight()
-            )
+        this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight)
     setPlayheadTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time;
     playing = () => this.mediaState === media_state.Playing;
     isActiveChild = () => this._isAnyChildContentActive;
 
-    timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
-    timelineHeight = () => (this.props.PanelHeight() * (AudioBox.heightPercent / 100)) *// panelHeight * heightPercent is player height
-        (AudioBox.heightPercent / 100)   // * heightPercent is timeline height (as per css inline)
+    timelineWidth = () => this.props.PanelWidth();
+    timelineHeight = () => (this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight))
 
     @undoBatch
     finishTrim = () => { // hides trim controls and displays new clip
@@ -365,6 +359,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
 
     onClipPointerDown = (e: React.PointerEvent) => {
+        e.stopPropagation();
         this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
             if (doubleTap) {
                 this.startTrim(TrimScope.All);
@@ -392,90 +387,81 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
 
     @computed get recordingControls() {
-        return <div className="audiobox-buttons">
+        return <div className="audiobox-recorder">
             <div className="audiobox-dictation" onClick={this.onFile}>
                 <FontAwesomeIcon
-                    style={{ width: "30px" }}
-                    icon="file-alt"
-                    size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+                    size="2x"
+                    icon="file-alt" />
             </div>
             {[media_state.Recording, media_state.Playing].includes(this.mediaState) ?
-                <div className="recording" onClick={e => e.stopPropagation()}>
-                    <div className="recording-buttons" onClick={this.Record}>
+                <div className="recording-controls" onClick={e => e.stopPropagation()}>
+                    <div className="record-button" onClick={this.Record}>
                         <FontAwesomeIcon
-                            icon="stop"
-                            size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+                            size="2x"
+                            icon="stop" />
                     </div>
-                    <div className="recording-buttons"
-                        onClick={this._paused ? this.recordPlay : this.recordPause}
-                    >
+                    <div className="record-button" onClick={this._paused ? this.recordPlay : this.recordPause}>
                         <FontAwesomeIcon
-                            icon={this._paused ? "play" : "pause"}
-                            size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+                            size="2x"
+                            icon={this._paused ? "play" : "pause"} />
                     </div>
-                    <div className="time">
+                    <div className="record-timecode">
                         {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
                     </div>
                 </div>
                 :
-                <div className={`audiobox-record${this.props.isContentActive() ? "-interactive" : ""}`}>
+                <div className="audiobox-start-record">
                     <FontAwesomeIcon icon="microphone" />
                     RECORD
                 </div>}
-        </div>;
+        </div>
     }
 
     @computed get playbackControls() {
-        return <div className="audiobox-controls"
-            style={{ pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? "all" : "none", }}
-        >
-            <div className="audiobox-dictation" />
-            <div className="audiobox-player"
-                style={{ height: `${AudioBox.heightPercent}%` }}
-            >
-                <div className="audiobox-buttons"
-                    title={this.mediaState === media_state.Paused ? "play" : "pause"}
-                    onClick={this.mediaState === media_state.Paused ? this.Play : this.Pause}
-                >
-                    {" "}
-                    <FontAwesomeIcon
-                        icon={this.mediaState === media_state.Paused ? "play" : "pause"}
-                        size={"1x"} />
-                </div>
+        return <div className="audiobox-file" style={{ pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? "all" : "none", }}>
+            <div className="audiobox-controls">
+                <div className="controls-left">
+                    <div className="audiobox-button"
+                        title={this.mediaState === media_state.Paused ? "play" : "pause"}
+                        onPointerDown={this.mediaState === media_state.Paused ? this.Play : this.Pause}>
+                        <FontAwesomeIcon icon={this.mediaState === media_state.Paused ? "play" : "pause"} size={"1x"} />
+                    </div>
 
-                <div className="audiobox-buttons"
-                    title={this.timeline?.IsTrimming !== TrimScope.None ? "finish" : "trim"}
-                    onPointerDown={this.onClipPointerDown}
-                >
-                    <FontAwesomeIcon
-                        icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"}
-                        size={"1x"} />
+                    <div className="audiobox-button"
+                        title={this.timeline?.IsTrimming !== TrimScope.None ? "finish" : "trim"}
+                        onPointerDown={this.onClipPointerDown}>
+                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} size={"1x"} />
+                    </div>
                 </div>
+                <div className="controls-right">
+                    <FontAwesomeIcon icon={["fas", "search"]} />
+                    <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
+                        className="toolbar-slider" id="zoom-slider"
+                        onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
+                    />
+                </div>
+            </div>
 
-                <div className="audiobox-timeline"
-                    style={{
-                        left: AudioBox.playheadWidth,
-                        width: `calc(100% - ${AudioBox.playheadWidth}px)`,
-                    }}
-                >
+            <div className="audiobox-playback">
+                <div className="audiobox-timeline">
                     {this.renderTimeline}
                 </div>
-                {this.audio}
-                <div className="audioBox-current-time">
-                    {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}
-                </div>
+            </div>
 
-                {/* <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
-                    className="toolbar-slider" id="zoom-slider"
-                    onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
-                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(e.target.value); }}
-                /> */}
+            {this.audio}
 
-                <div className="audioBox-total-time">
+            <div className="audiobox-timecodes">
+                <div className="timecode-current">
+                    {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}
+                </div>
+                <div className="timecode-duration">
                     {this.timeline && formatTime(Math.round(NumCast(this.timeline?.clipDuration)))}
                 </div>
             </div>
-        </div>;
+
+
+        </div>
     }
 
     @computed get renderTimeline() {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index d80fb44cf..d47e8340c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -89,8 +89,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
         };
         const displayDoc = (which: string) => {
             var whichDoc = Cast(this.dataDoc[which], Doc, null);
-            if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null);
-            const targetDoc = Cast(whichDoc.annotationOn, Doc, null) ?? whichDoc;
+            // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null);
+            const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc;
             return whichDoc ? <>
                 <DocumentView
                     ref={(r) => {
-- 
cgit v1.2.3-70-g09d2


From 7f08f0fec32a28e1dc19f00021f99352f55a045c Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Thu, 28 Oct 2021 15:12:34 -0400
Subject: fixed marker document decorations on scroll in zoomed timeline

---
 src/client/views/AudioWaveform.tsx                 |   2 +-
 src/client/views/MainView.tsx                      |   2 +-
 .../collections/CollectionStackedTimeline.scss     |  32 +++---
 .../collections/CollectionStackedTimeline.tsx      |  16 ++-
 src/client/views/nodes/AudioBox.scss               | 123 ++++++++++++---------
 src/client/views/nodes/AudioBox.tsx                |  43 ++++++-
 6 files changed, 138 insertions(+), 80 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index ca1dd6f36..58384792e 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -90,7 +90,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                 <Waveform
                     color={Colors.MEDIUM_BLUE}
                     height={this.waveHeight}
-                    barWidth={10.0 / this.audioBuckets.length}
+                    barWidth={200.0 / this.audioBuckets.length}
                     pos={this.props.duration}
                     duration={this.props.duration}
                     peaks={this.audioBuckets}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 9a885fbf8..ce71dcc01 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -187,7 +187,7 @@ export class MainView extends React.Component {
             fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical,
             fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll,
             fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines,
-            fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen);
+            fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faSearchPlus, fa.faVolumeUp, fa.faVolumeMute);
         this.initAuthenticationRouters();
     }
 
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 843c5dcb5..19913350b 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -2,8 +2,24 @@
 
 .timeline-container {
     height: calc(100% - 50px);
-    overflow-x: scroll;
+    overflow-x: auto;
     border: none;
+    background-color: $white;
+    border: 2px solid $dark-gray;
+    border-width: 0 2px 0 2px;
+}
+
+::-webkit-scrollbar {
+    position: relative;
+    -webkit-appearance: none;
+    height: 5px;
+}
+
+::-webkit-scrollbar-thumb {
+    position: relative;
+    -webkit-appearance: none;
+    height: 5px;
+    background-color: $medium-gray;
 }
 
 .collectionStackedTimeline {
@@ -12,20 +28,6 @@
     z-index: 1000;
     height: 100%;
 
-    ::-webkit-scrollbar {
-        position: relative;
-        -webkit-appearance: none;
-        height: 5px;
-        background-color: white;
-    }
-
-    ::-webkit-scrollbar-thumb {
-        position: relative;
-        -webkit-appearance: none;
-        height: 5px;
-        background-color: $medium-gray;
-    }
-
     .collectionStackedTimeline-trim-shade {
         position: absolute;
         height: 100%;
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 82a6b2a66..37fd42bc1 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -89,6 +89,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     @observable _zoomFactor: number = 1;
 
+    @observable _scroll: number = 0;
+
     get minTrimLength() { return this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0; }
     @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; }
     @computed get trimDuration() { return this.trimEnd - this.trimStart; }
@@ -219,7 +221,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
     @action
     onPointerDownTimeline = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
-        const scrollLeft = this._timeline?.scrollLeft;
         const clientX = e.clientX;
         const diff = rect ? clientX - rect?.x : null;
         const shiftKey = e.shiftKey;
@@ -343,6 +344,11 @@ export class CollectionStackedTimeline extends CollectionSubView<
         );
     }
 
+    @action
+    setScroll = (e: React.MouseEvent) => {
+        this._scroll = e.currentTarget.scrollLeft;
+    }
+
     @action
     internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
         if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
@@ -533,7 +539,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
         );
     }
     @computed get renderAudioWaveform() {
-        console.log(this.props.mediaPath)
         return !this.props.mediaPath ? null : (
             <div className="collectionStackedTimeline-waveform">
                 <AudioWaveform
@@ -563,7 +568,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     render() {
-        const timelineContentWidth = this.props.PanelWidth() * this.zoomFactor;
+        const timelineContentWidth = this.props.PanelWidth() * this.zoomFactor - 4; // subtract size of container border
         const overlaps: {
             anchorStartTime: number;
             anchorEndTime: number;
@@ -576,7 +581,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
         const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
         return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}>
             <div className="timeline-container"
-                style={{ width: this.props.PanelWidth() }}>
+                style={{ width: this.props.PanelWidth() }}
+                onScroll={this.setScroll}>
                 <div
                     className="collectionStackedTimeline"
                     ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
@@ -616,7 +622,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                                     mark={d.anchor}
                                     rangeClickScript={this.rangeClickScript}
                                     rangePlayScript={this.rangePlayScript}
-                                    left={left}
+                                    left={left - this._scroll}
                                     top={top}
                                     width={width}
                                     height={height}
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index d466c6c3b..b3df7e79f 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -92,6 +92,62 @@
     height: 100%;
     color: $white;
 
+    .audiobox-button {
+        margin: 2.5px;
+        cursor: pointer;
+        width: 25px;
+        height: 25px;
+        border-radius: 50%;
+        background: $dark-gray;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        svg {
+            width: 15px;
+        }
+
+        &:hover {
+            background: $black;
+        }
+    }
+
+    svg {
+        width: 10px;
+    }
+
+    input[type="range"] {
+        width: 70px;
+        -webkit-appearance: none;
+        background: none;
+        margin: 5px;
+    }
+
+    input[type="range"]:focus {
+        outline: none;
+    }
+
+    input[type="range"]::-webkit-slider-runnable-track {
+        width: 100%;
+        height: 6px;
+        cursor: pointer;
+        box-shadow: 0;
+        background: $light-gray;
+        border-radius: 3px;
+    }
+
+    input[type="range"]::-webkit-slider-thumb {
+        box-shadow: 0;
+        border: 0;
+        height: 10px;
+        width: 10px;
+        border-radius: 10px;
+        background: $medium-blue;
+        cursor: pointer;
+        -webkit-appearance: none;
+        margin: -2px;
+    }
+
     .audiobox-controls {
         display: flex;
         flex-direction: row;
@@ -104,66 +160,20 @@
             display: flex;
             flex-direction: row;
             width: 100px;
-
-            .audiobox-button {
-                margin: 2.5px;
-                cursor: pointer;
-                width: 25px;
-                height: 25px;
-                border-radius: 50%;
-                background: $dark-gray;
-                display: flex;
-                align-items: center;
-                justify-content: center;
-
-                svg {
-                    width: 15px;
-                }
-
-                &:hover {
-                    background: $black;
-                }
-            }
         }
 
         .controls-right {
             display: flex;
             flex-direction: row;
 
-            svg {
-                width: 10px;
-            }
-
-            input[type="range"] {
-                width: 70px;
-                -webkit-appearance: none;
-                background: none;
-                margin: 5px;
-            }
-
-            input[type="range"]:focus {
-                outline: none;
-            }
-
-            input[type="range"]::-webkit-slider-runnable-track {
-                width: 100%;
-                height: 6px;
-                cursor: pointer;
-                box-shadow: 0;
-                background: $light-gray;
-                border-radius: 3px;
-            }
+            .audiobox-button {
+                width: 15px;
+                height: 15px;
+                margin: 0;
 
-            input[type="range"]::-webkit-slider-thumb {
-                box-shadow: 0;
-                border: 0;
-                height: 10px;
-                width: 10px;
-                border-radius: 10px;
-                background: $medium-blue;
-                cursor: pointer;
-                -webkit-appearance: none;
-                margin: -2px;
+                svg {
+                    width: 10px;
+                }
             }
         }
     }
@@ -171,6 +181,7 @@
     .audiobox-playback {
         width: 100%;
         height: calc(100% - 50px);
+        background: $white;
 
         .audiobox-timeline {
             height: 100%;
@@ -193,5 +204,11 @@
         height: 20px;
         padding: 3px;
         font-size: $small-text;
+
+        .bottom-controls-middle {
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+        }
     }
 }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 48e324971..3a3eb78e1 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -21,6 +21,7 @@ import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
 import "./AudioBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
+import { timeStamp } from "console";
 
 declare class MediaRecorder {
     constructor(e: any); // whatever MediaRecorder has
@@ -60,6 +61,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     _stream: MediaStream | undefined;
     _play: any = null;
 
+    @observable _volume: number = 1;
+    @observable _muted: boolean = false;
     @observable _paused: boolean = false;
     @computed get recordingStart() { return DateCast(this.dataDoc[this.fieldKey + "-recordingStart"])?.date.getTime(); }
     @computed get rawDuration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); }
@@ -155,8 +158,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this.mediaState = media_state.Playing;
                 this._play = setTimeout(
                     () => {
-                        if (fullPlay) this.setPlayheadTime(this.timeline!.trimStart);
                         this.Pause();
+                        if (fullPlay) this.setPlayheadTime(this.timeline!.trimStart);
                     },
                     (end - start) * 1000);
             } else {
@@ -260,6 +263,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     Pause = () => {
         this._ele?.pause();
         this.mediaState = media_state.Paused;
+        // clearTimeout(this._play); // stops clip from jumping back to beginning
     }
 
     // creates a text document for dictation
@@ -374,6 +378,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.timeline?.setZoom(zoom);
     }
 
+    @action
+    setVolume = (volume: number) => {
+        if (this._ele) {
+            this._volume = volume;
+            this._ele.volume = volume;
+        }
+    }
+
+    @action
+    toggleMute = (e: React.PointerEvent) => {
+        e.stopPropagation();
+        if (this._ele) {
+            this._muted = !this._muted;
+            this._ele.muted = this._muted;
+        }
+    }
+
     setupTimelineDrop = (r: HTMLDivElement | null) => {
         if (r && this.timeline) {
             this._dropDisposer?.();
@@ -434,11 +455,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     </div>
                 </div>
                 <div className="controls-right">
-                    <FontAwesomeIcon icon={["fas", "search"]} />
-                    <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
-                        className="toolbar-slider" id="zoom-slider"
+                    <div className="audiobox-button"
+                        title={this._muted ? "unmute" : "mute"}
+                        onPointerDown={this.toggleMute}>
+                        <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
+                    </div>
+                    <input type="range" step="0.1" min="0" max="1" value={this._volume}
+                        className="toolbar-slider" id="volume-slider"
                         onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
-                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
+                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.setVolume(Number(e.target.value)) }}
                     />
                 </div>
             </div>
@@ -455,6 +480,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 <div className="timecode-current">
                     {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}
                 </div>
+                <div className="bottom-controls-middle">
+                    <FontAwesomeIcon icon="search-plus" />
+                    <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
+                        className="toolbar-slider" id="zoom-slider"
+                        onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
+                    />
+                </div>
                 <div className="timecode-duration">
                     {this.timeline && formatTime(Math.round(NumCast(this.timeline?.clipDuration)))}
                 </div>
-- 
cgit v1.2.3-70-g09d2


From 3ba076f93e2aa182698a229b381a77765f11213e Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Thu, 28 Oct 2021 15:58:38 -0400
Subject: fixed marker drag and drop in scrolled zoomed timeline

---
 src/client/views/AudioWaveform.tsx                   | 20 +++++++++++++++++---
 .../views/collections/CollectionStackedTimeline.tsx  | 10 ++++++----
 src/client/views/nodes/AudioBox.tsx                  |  7 +++----
 3 files changed, 26 insertions(+), 11 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 58384792e..4d9c039c4 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -20,6 +20,7 @@ export interface AudioWaveformProps {
     clipEnd: number;
     zoomFactor: number;
     PanelHeight: () => number;
+    PanelWidth: () => number;
 }
 
 @observer
@@ -88,15 +89,28 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
         return (
             <div className="audioWaveform">
                 <Waveform
-                    color={Colors.MEDIUM_BLUE}
+                    color={Colors.MEDIUM_BLUE_ALT}
                     height={this.waveHeight}
-                    barWidth={200.0 / this.audioBuckets.length}
+                    barWidth={2}
                     pos={this.props.duration}
                     duration={this.props.duration}
                     peaks={this.audioBuckets}
-                    progressColor={Colors.MEDIUM_BLUE}
+                    progressColor={Colors.MEDIUM_BLUE_ALT}
                 />
             </div>
         );
     }
 }
+
+
+// export interface WaveformProps {
+//     barWidth: number;
+//     width: () => number;
+//     height: () => number;
+//     peaks: number[];
+//     color: string;
+// }
+
+// export class Waveform extends React.Component<WaveformProps> {
+
+// }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 37fd42bc1..b63835b00 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -357,7 +357,7 @@ 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 timelinePt = this.toTimeline(x, this.props.PanelWidth());
+        const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth());
         docDragData.droppedDocuments.forEach(drop => {
             const anchorEnd = this.anchorEnd(drop);
             if (anchorEnd !== undefined) {
@@ -466,7 +466,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         m: Doc,
         placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
     ) => {
-        const timelineContentWidth = this.props.PanelWidth();
+        const timelineContentWidth = this.timelineContentWidth();
         const x1 = this.anchorStart(m);
         const x2 = this.anchorEnd(
             m,
@@ -498,6 +498,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
     dictationHeightPercent = 50;
     dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
     timelineContentHeight = () => (this.props.PanelHeight() * this.dictationHeightPercent) / 100;
+    timelineContentWidth = () => (this.props.PanelWidth() * this.zoomFactor - 4);  // subtract size of container border
     dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight());
     isContentActive = () => this.props.isSelected() || this.props.isContentActive();
     currentTimecode = () => this.currentTime;
@@ -548,8 +549,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
                     layoutDoc={this.layoutDoc}
                     clipStart={this.clipStart}
                     clipEnd={this.clipEnd}
-                    PanelHeight={this.timelineContentHeight}
                     zoomFactor={this.zoomFactor}
+                    PanelHeight={this.timelineContentHeight}
+                    PanelWidth={this.timelineContentWidth}
                 />
             </div>
         );
@@ -568,7 +570,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     render() {
-        const timelineContentWidth = this.props.PanelWidth() * this.zoomFactor - 4; // subtract size of container border
+        const timelineContentWidth = this.timelineContentWidth();
         const overlaps: {
             anchorStartTime: number;
             anchorEndTime: number;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 3a3eb78e1..a896c7d90 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -158,8 +158,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this.mediaState = media_state.Playing;
                 this._play = setTimeout(
                     () => {
-                        this.Pause();
-                        if (fullPlay) this.setPlayheadTime(this.timeline!.trimStart);
+                        this.setPlayheadTime(this.timeline!.trimStart);
                     },
                     (end - start) * 1000);
             } else {
@@ -254,7 +253,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     Play = (e?: any) => {
         const eleTime = this._ele?.currentTime || 0;
         const start = eleTime === this.timeline?.trimEnd ? this.timeline.trimStart : eleTime;
-        this.playFrom(start, undefined, true);
+        this.playFrom(start, undefined);
         e?.stopPropagation?.();
     }
 
@@ -263,7 +262,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     Pause = () => {
         this._ele?.pause();
         this.mediaState = media_state.Paused;
-        // clearTimeout(this._play); // stops clip from jumping back to beginning
+        clearTimeout(this._play); // stops clip from jumping back to beginning
     }
 
     // creates a text document for dictation
-- 
cgit v1.2.3-70-g09d2


From bcae1802bc5277811476ce968a337813a7841fb6 Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Thu, 4 Nov 2021 16:09:13 -0400
Subject: smooth scroll in timeline while playing audio

---
 src/Utils.ts                                       |  20 ++++
 src/client/views/AudioWaveform.tsx                 | 118 +++++++++++++++++++--
 .../collections/CollectionStackedTimeline.scss     |  17 +--
 .../collections/CollectionStackedTimeline.tsx      |  59 ++++++++---
 src/client/views/nodes/AudioBox.tsx                |   4 +-
 5 files changed, 178 insertions(+), 40 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index bfb29fe8d..9faea8d60 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -514,6 +514,26 @@ export function smoothScroll(duration: number, element: HTMLElement | HTMLElemen
     };
     animateScroll();
 }
+
+export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) {
+    const elements = (element instanceof HTMLElement ? [element] : element);
+    const starts = elements.map(element => element.scrollLeft);
+    const startDate = new Date().getTime();
+
+    const animateScroll = () => {
+        const currentDate = new Date().getTime();
+        const currentTime = currentDate - startDate;
+        elements.map((element, i) => element.scrollLeft = easeInOutQuad(currentTime, starts[i], to - starts[i], duration));
+
+        if (currentTime < duration) {
+            requestAnimationFrame(animateScroll);
+        } else {
+            elements.forEach(element => element.scrollLeft = to);
+        }
+    };
+    animateScroll();
+}
+
 export function addStyleSheet(styleType: string = "text/css") {
     const style = document.createElement("style");
     style.type = styleType;
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 4d9c039c4..8a3c3c319 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -10,6 +10,7 @@ import { Cast, NumCast } from "../../fields/Types";
 import { numberRange } from "../../Utils";
 import "./AudioWaveform.scss";
 import { Colors } from "./global/globalEnums";
+import Color = require("color");
 
 export interface AudioWaveformProps {
     duration: number; // length of media clip
@@ -19,15 +20,15 @@ export interface AudioWaveformProps {
     clipStart: number;
     clipEnd: number;
     zoomFactor: number;
-    PanelHeight: () => number;
-    PanelWidth: () => number;
+    PanelHeight: number;
+    PanelWidth: 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 waveHeight() { return Math.max(50, this.props.PanelHeight); }
     @computed get clipStart() { return this.props.clipStart; }
     @computed get clipEnd() { return this.props.clipEnd; }
     @computed get zoomFactor() { return this.props.zoomFactor; }
@@ -88,10 +89,17 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
     render() {
         return (
             <div className="audioWaveform">
+                {/* <Waveform
+                    barWidth={2}
+                    width={() => this.props.PanelWidth}
+                    height={this.props.PanelHeight}
+                    peaks={this.audioBuckets}
+                    color={Colors.MEDIUM_BLUE}
+                /> */}
                 <Waveform
                     color={Colors.MEDIUM_BLUE_ALT}
                     height={this.waveHeight}
-                    barWidth={2}
+                    barWidth={200 / this.audioBuckets.length}
                     pos={this.props.duration}
                     duration={this.props.duration}
                     peaks={this.audioBuckets}
@@ -103,14 +111,102 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
 }
 
 
-// export interface WaveformProps {
-//     barWidth: number;
-//     width: () => number;
-//     height: () => number;
-//     peaks: number[];
-//     color: string;
-// }
+export interface WaveformProps {
+    barWidth: number;
+    width: () => number;
+    height: () => number;
+    peaks: number[];
+    color: string;
+}
 
+// @observer
 // export class Waveform extends React.Component<WaveformProps> {
+//     private _canvas: HTMLCanvasElement | null = null;
+
+//     get width() { return this.props.width(); }
+//     get height() { return this.props.height(); }
+//     get peaks() { return this.props.peaks; }
+
+//     componentDidMount() {
+//         this.drawBars();
+//     }
+
+//     drawBars() {
+//         const waveCanvasCtx = this._canvas?.getContext("2d");
+
+//         if (waveCanvasCtx) {
+//             const pixelRatio = window.devicePixelRatio;
+//             console.log(pixelRatio);
+
+//             const displayWidth = Math.round(this.width);
+//             const displayHeight = Math.round(this.height);
+//             waveCanvasCtx.canvas.width = this.width;
+//             waveCanvasCtx.canvas.height = this.height;
+//             waveCanvasCtx.canvas.style.width = `${displayWidth}px`;
+//             waveCanvasCtx.canvas.style.height = `${displayHeight}px`;
+
+//             waveCanvasCtx.clearRect(0, 0, this.width, this.height);
+
+//             const hasMinVals = [].some.call(this.peaks, (val) => val < 0);
+//             let filteredPeaks = this.peaks;
+//             if (hasMinVals) {
+//                 // If the first value is negative, add 1 to the filtered indices
+//                 let indexOffset = 0;
+//                 if (this.peaks[0] < 0) {
+//                     indexOffset = 1;
+//                 }
+//                 filteredPeaks = [].filter.call(
+//                     this.peaks,
+//                     (_, index) => (index + indexOffset) % 2 == 0
+//                 );
+//             }
+
+//             const $ = 0.5;
+//             const height = this.height;
+//             const offsetY = 0;
+//             const halfH = this.height / 2;
+//             const length = filteredPeaks.length;
+//             const bar = this.props.barWidth;
+//             const gap = 2;
+//             const step = bar + gap;
+
+//             let absmax = 1;
+//             absmax = this.absMax(filteredPeaks);
+
+//             const scale = length / this.width;
+
+//             waveCanvasCtx.fillStyle = this.props.color;
+
+//             for (let i = 0; i < this.width; i += step) {
+//                 let h = Math.round(filteredPeaks[Math.floor(i * scale)] / absmax * halfH)
+//                 if (h === 0) {
+//                     h = 1
+//                 }
+//                 waveCanvasCtx.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2)
+//             }
+//         }
+//     }
+
+//     absMax = (values: number[]) => {
+//         let max = -Infinity;
+//         for (const i in values) {
+//             const num = Math.abs(values[i]);
+//             if (num > max) {
+//                 max = num;
+//             }
+//         }
+
+//         return max;
+//     }
+
 
+//     render() {
+//         return this.props.peaks ? (
+//             <canvas
+//                 ref={(instance) => {
+//                     this._canvas = instance;
+//                 }}
+//             />
+//         ) : null
+//     }
 // }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 19913350b..fce105a44 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -3,25 +3,13 @@
 .timeline-container {
     height: calc(100% - 50px);
     overflow-x: auto;
+    overflow-y: hidden;
     border: none;
     background-color: $white;
     border: 2px solid $dark-gray;
     border-width: 0 2px 0 2px;
 }
 
-::-webkit-scrollbar {
-    position: relative;
-    -webkit-appearance: none;
-    height: 5px;
-}
-
-::-webkit-scrollbar-thumb {
-    position: relative;
-    -webkit-appearance: none;
-    height: 5px;
-    background-color: $medium-gray;
-}
-
 .collectionStackedTimeline {
     position: absolute;
     background: $off-white;
@@ -33,6 +21,7 @@
         height: 100%;
         background-color: $dark-gray;
         opacity: 0.3;
+        top: 0;
     }
 
     .collectionStackedTimeline-trim-controls {
@@ -43,6 +32,8 @@
         display: flex;
         justify-content: space-between;
         max-width: 100%;
+        top: 0;
+        left: 0;
 
         .collectionStackedTimeline-trim-handle {
             background-color: $medium-blue;
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index b63835b00..ced8a68e8 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -23,6 +23,8 @@ import {
     setupMoveUpEvents,
     StopEvent,
     returnTrue,
+    smoothScroll,
+    smoothScrollHorizontal,
 } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
 import { LinkManager } from "../../util/LinkManager";
@@ -81,6 +83,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
     static LabelPlayScript: ScriptField;
 
     private _timeline: HTMLDivElement | null = null;
+    private _timelineWrapper: HTMLDivElement | null = null;
     private _markerStart: number = 0;
     @observable _markerEnd: number | undefined;
     @observable _trimming: number = TrimScope.None;
@@ -345,8 +348,23 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     @action
-    setScroll = (e: React.MouseEvent) => {
-        this._scroll = e.currentTarget.scrollLeft;
+    setScroll = (e: React.UIEvent) => {
+        e.stopPropagation();
+        this._scroll = this._timelineWrapper!.scrollLeft;
+    }
+
+    @action
+    scrollToTime = (time: number) => {
+        console.log("rightmost visible time: " + this.toTimeline(this._scroll + this.props.PanelWidth(), this.timelineContentWidth));
+        if (this._timelineWrapper && time > this.toTimeline(this._scroll + this.props.PanelWidth(), this.timelineContentWidth)) {
+            console.log("scrolled");
+            this._scroll = Math.min(this._scroll + this.props.PanelWidth(), this.timelineContentWidth - this.props.PanelWidth());
+            smoothScrollHorizontal(100, this._timelineWrapper, this._scroll);
+        }
+        else if (this._timelineWrapper && time < this.toTimeline(this._scroll, this.timelineContentWidth)) {
+            this._scroll = time / this.timelineContentWidth * this.clipDuration;
+            smoothScrollHorizontal(100, this._timelineWrapper, this._scroll);
+        }
     }
 
     @action
@@ -357,7 +375,7 @@ 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 timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth());
+        const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth);
         docDragData.droppedDocuments.forEach(drop => {
             const anchorEnd = this.anchorEnd(drop);
             if (anchorEnd !== undefined) {
@@ -466,7 +484,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         m: Doc,
         placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
     ) => {
-        const timelineContentWidth = this.timelineContentWidth();
+        const timelineContentWidth = this.timelineContentWidth;
         const x1 = this.anchorStart(m);
         const x2 = this.anchorEnd(
             m,
@@ -497,9 +515,9 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     dictationHeightPercent = 50;
     dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
-    timelineContentHeight = () => (this.props.PanelHeight() * this.dictationHeightPercent) / 100;
-    timelineContentWidth = () => (this.props.PanelWidth() * this.zoomFactor - 4);  // subtract size of container border
-    dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight());
+    @computed get timelineContentHeight() { return this.props.PanelHeight() * this.dictationHeightPercent / 100; }
+    @computed get timelineContentWidth() { return this.props.PanelWidth() * this.zoomFactor - 4 };  // subtract size of container border
+    dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight);
     isContentActive = () => this.props.isSelected() || this.props.isContentActive();
     currentTimecode = () => this.currentTime;
 
@@ -510,7 +528,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                 style={{
                     position: "absolute",
                     height: "100%",
-                    top: this.timelineContentHeight(),
+                    top: this.timelineContentHeight,
                     background: Colors.LIGHT_BLUE,
                 }}
             >
@@ -570,7 +588,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 
     render() {
-        const timelineContentWidth = this.timelineContentWidth();
         const overlaps: {
             anchorStartTime: number;
             anchorEndTime: number;
@@ -584,25 +601,26 @@ export class CollectionStackedTimeline extends CollectionSubView<
         return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}>
             <div className="timeline-container"
                 style={{ width: this.props.PanelWidth() }}
-                onScroll={this.setScroll}>
+                onScroll={this.setScroll}
+                ref={(wrapper: HTMLDivElement | null) => (this._timelineWrapper = wrapper)}>
                 <div
                     className="collectionStackedTimeline"
                     ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
                     onClick={(e) => this.isContentActive() && StopEvent(e)}
                     onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)}
-                    style={{ width: timelineContentWidth }}>
+                    style={{ width: this.timelineContentWidth }}>
 
                     {drawAnchors.map((d) => {
                         const start = this.anchorStart(d.anchor);
                         const end = this.anchorEnd(
                             d.anchor,
-                            start + (10 / timelineContentWidth) * this.clipDuration
+                            start + (10 / this.timelineContentWidth) * this.clipDuration
                         );
                         if (end < this.clipStart || start > this.clipEnd) return (null);
-                        const left = Math.max((start - this.clipStart) / this.clipDuration * timelineContentWidth, 0);
+                        const left = Math.max((start - this.clipStart) / this.clipDuration * this.timelineContentWidth, 0);
                         const top = (d.level / maxLevel) * this.props.PanelHeight();
                         const timespan = Math.max(0, end - this.clipStart) - Math.max(0, start - this.clipStart);
-                        const width = (timespan / this.clipDuration) * timelineContentWidth;
+                        const width = (timespan / this.clipDuration) * this.timelineContentWidth;
                         const height = this.props.PanelHeight() / maxLevel;
                         return this.props.Document.hideAnchors ? null : (
                             <div
@@ -641,7 +659,18 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         );
                     })}
                     {!this.IsTrimming && this.selectionContainer}
-                    {this.renderAudioWaveform}
+                    {/* {this.renderAudioWaveform} */}
+                    <AudioWaveform
+                        rawDuration={this.props.rawDuration}
+                        duration={this.clipDuration}
+                        mediaPath={this.props.mediaPath}
+                        layoutDoc={this.layoutDoc}
+                        clipStart={this.clipStart}
+                        clipEnd={this.clipEnd}
+                        zoomFactor={this.zoomFactor}
+                        PanelHeight={this.timelineContentHeight}
+                        PanelWidth={this.timelineContentWidth}
+                    />
                     {this.renderDictation}
 
                     <div
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index a896c7d90..b51908e20 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -21,7 +21,7 @@ import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
 import "./AudioBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
-import { timeStamp } from "console";
+import { time, timeStamp } from "console";
 
 declare class MediaRecorder {
     constructor(e: any); // whatever MediaRecorder has
@@ -139,6 +139,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     }
                 });
             this.layoutDoc._currentTimecode = this._ele.currentTime;
+            console.log("current time: " + this.layoutDoc._currentTimecode);
+            this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode));
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 39c85293f6c3d385ea64ba0db8c9736dfaaec993 Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Sun, 20 Mar 2022 15:22:50 -0400
Subject: cleaned up files and added some comments

---
 src/client/views/AudioWaveform.tsx                 | 137 ++++---------------
 .../collections/CollectionStackedTimeline.tsx      | 102 ++++++++++----
 src/client/views/nodes/AudioBox.tsx                | 108 +++++++++++----
 src/client/views/nodes/VideoBox.tsx                | 146 ++++++++++++++++-----
 4 files changed, 295 insertions(+), 198 deletions(-)

(limited to 'src/client/views/AudioWaveform.tsx')

diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 8a3c3c319..525c0ce5a 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -1,16 +1,26 @@
 import React = require("react");
 import axios from "axios";
-import { action, computed, reaction, IReactionDisposer } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
 import { observer } from "mobx-react";
 import Waveform from "react-audio-waveform";
 import { Doc } from "../../fields/Doc";
 import { List } from "../../fields/List";
 import { listSpec } from "../../fields/Schema";
-import { Cast, NumCast } from "../../fields/Types";
+import { Cast } from "../../fields/Types";
 import { numberRange } from "../../Utils";
 import "./AudioWaveform.scss";
 import { Colors } from "./global/globalEnums";
-import Color = require("color");
+
+
+/**
+ * AudioWaveform
+ * 
+ * Used in CollectionStackedTimeline to render a canvas with a visual of an audio waveform for AudioBox and VideoBox documents.
+ * Uses react-audio-waveform package.
+ * Bins the audio data into audioBuckets which are passed to package to render the lines.
+ * Calculates new buckets each time a new zoom factor or new set of trim bounds is created and stores it in a field on the layout doc with a title indicating the bounds and zoom for that list (see audioBucketField)
+ */
+
 
 export interface AudioWaveformProps {
     duration: number; // length of media clip
@@ -26,19 +36,24 @@ export interface AudioWaveformProps {
 
 @observer
 export class AudioWaveform extends React.Component<AudioWaveformProps> {
-    public static NUMBER_OF_BUCKETS = 100;
+    public static NUMBER_OF_BUCKETS = 100; // number of buckets data is divided into to draw waveform lines
+
     _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; }
     @computed get zoomFactor() { return this.props.zoomFactor; }
-    @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); }
 
+    @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); }
     audioBucketField = (start: number, end: number, zoomFactor: number) => "audioBuckets/" + "/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_") + "/" + (zoomFactor * 10);
 
+
     componentWillUnmount() {
         this._disposer?.();
     }
+
     componentDidMount() {
         this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }),
             ({ clipStart, clipEnd, fieldKey, zoomFactor }) => {
@@ -49,7 +64,6 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
                     setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor));
                 }
             }, { fireImmediately: true });
-
     }
 
     // decodes the audio file into peaks for generating the waveform
@@ -86,16 +100,10 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
             }
         );
     }
+
     render() {
         return (
             <div className="audioWaveform">
-                {/* <Waveform
-                    barWidth={2}
-                    width={() => this.props.PanelWidth}
-                    height={this.props.PanelHeight}
-                    peaks={this.audioBuckets}
-                    color={Colors.MEDIUM_BLUE}
-                /> */}
                 <Waveform
                     color={Colors.MEDIUM_BLUE_ALT}
                     height={this.waveHeight}
@@ -108,105 +116,4 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
             </div>
         );
     }
-}
-
-
-export interface WaveformProps {
-    barWidth: number;
-    width: () => number;
-    height: () => number;
-    peaks: number[];
-    color: string;
-}
-
-// @observer
-// export class Waveform extends React.Component<WaveformProps> {
-//     private _canvas: HTMLCanvasElement | null = null;
-
-//     get width() { return this.props.width(); }
-//     get height() { return this.props.height(); }
-//     get peaks() { return this.props.peaks; }
-
-//     componentDidMount() {
-//         this.drawBars();
-//     }
-
-//     drawBars() {
-//         const waveCanvasCtx = this._canvas?.getContext("2d");
-
-//         if (waveCanvasCtx) {
-//             const pixelRatio = window.devicePixelRatio;
-//             console.log(pixelRatio);
-
-//             const displayWidth = Math.round(this.width);
-//             const displayHeight = Math.round(this.height);
-//             waveCanvasCtx.canvas.width = this.width;
-//             waveCanvasCtx.canvas.height = this.height;
-//             waveCanvasCtx.canvas.style.width = `${displayWidth}px`;
-//             waveCanvasCtx.canvas.style.height = `${displayHeight}px`;
-
-//             waveCanvasCtx.clearRect(0, 0, this.width, this.height);
-
-//             const hasMinVals = [].some.call(this.peaks, (val) => val < 0);
-//             let filteredPeaks = this.peaks;
-//             if (hasMinVals) {
-//                 // If the first value is negative, add 1 to the filtered indices
-//                 let indexOffset = 0;
-//                 if (this.peaks[0] < 0) {
-//                     indexOffset = 1;
-//                 }
-//                 filteredPeaks = [].filter.call(
-//                     this.peaks,
-//                     (_, index) => (index + indexOffset) % 2 == 0
-//                 );
-//             }
-
-//             const $ = 0.5;
-//             const height = this.height;
-//             const offsetY = 0;
-//             const halfH = this.height / 2;
-//             const length = filteredPeaks.length;
-//             const bar = this.props.barWidth;
-//             const gap = 2;
-//             const step = bar + gap;
-
-//             let absmax = 1;
-//             absmax = this.absMax(filteredPeaks);
-
-//             const scale = length / this.width;
-
-//             waveCanvasCtx.fillStyle = this.props.color;
-
-//             for (let i = 0; i < this.width; i += step) {
-//                 let h = Math.round(filteredPeaks[Math.floor(i * scale)] / absmax * halfH)
-//                 if (h === 0) {
-//                     h = 1
-//                 }
-//                 waveCanvasCtx.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2)
-//             }
-//         }
-//     }
-
-//     absMax = (values: number[]) => {
-//         let max = -Infinity;
-//         for (const i in values) {
-//             const num = Math.abs(values[i]);
-//             if (num > max) {
-//                 max = num;
-//             }
-//         }
-
-//         return max;
-//     }
-
-
-//     render() {
-//         return this.props.peaks ? (
-//             <canvas
-//                 ref={(instance) => {
-//                     this._canvas = instance;
-//                 }}
-//             />
-//         ) : null
-//     }
-// }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 7d9dc39ae..bd7d0083b 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -43,6 +43,19 @@ import {
 import { LabelBox } from "../nodes/LabelBox";
 import "./CollectionStackedTimeline.scss";
 
+
+
+/**
+ * CollectionStackedTimeline
+ * Main component: CollectionStackedTimeline.tsx
+ * Supporting Components: AudioWaveform
+ * 
+ * CollectionStackedTimeline is a collection view used for audio and video nodes to display a timeline of the temporal media documents with an audio waveform and markers for links and annotations
+ * The actual media is handled in the containing classes (AudioBox, VideoBox) but the timeline deals with rendering and updating timecodes, links, and trimming.
+ * When trimming there are two pairs of times that are tracked: trimStart and trimEnd are the bounds of the trim controls, clipStart and clipEnd are the actual trimmed playback bounds of the clip
+ */
+
+
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
 export type CollectionStackedTimelineProps = {
@@ -60,38 +73,42 @@ export type CollectionStackedTimelineProps = {
     fieldKey: string;
 };
 
+// trimming state: shows full clip, current trim bounds, or not trimming
 export enum TrimScope {
     All = 2,
     Clip = 1,
     None = 0,
 }
 
+
 @observer
 export class CollectionStackedTimeline extends CollectionSubView<
     PanZoomDocument,
     CollectionStackedTimelineProps
 >(PanZoomDocument) {
-    @observable static SelectingRegion: CollectionStackedTimeline | undefined;
-    @observable public static CurrentlyPlaying: Doc[];
+    @observable static SelectingRegion: CollectionStackedTimeline | undefined; // timeline selection region
+    @observable public static CurrentlyPlaying: Doc[]; // tracks all currently playing audio and video docs
 
     static RangeScript: ScriptField;
     static LabelScript: ScriptField;
     static RangePlayScript: ScriptField;
     static LabelPlayScript: ScriptField;
 
-    private _timeline: HTMLDivElement | null = null;
-    private _timelineWrapper: HTMLDivElement | null = null;
+    private _timeline: HTMLDivElement | null = null; // ref to actual timeline div
+    private _timelineWrapper: HTMLDivElement | null = null; // ref to timeline wrapper div for zooming and scrolling
     private _markerStart: number = 0;
     @observable _markerEnd: number | undefined;
     @observable _trimming: number = TrimScope.None;
-    @observable _trimStart: number = 0;
-    @observable _trimEnd: number = 0;
+    @observable _trimStart: number = 0; // trim controls start pos
+    @observable _trimEnd: number = 0; // trim controls end pos
 
     @observable _zoomFactor: number = 1;
 
     @observable _scroll: number = 0;
 
+    // ensures that clip doesn't get trimmed so small that controls cannot be adjusted anymore 
     get minTrimLength() { return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5) }
+
     @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; }
     @computed get trimDuration() { return this.trimEnd - this.trimStart; }
     @computed get trimEnd() { return this.IsTrimming !== TrimScope.None ? this._trimEnd : this.clipEnd; }
@@ -104,6 +121,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     @computed get zoomFactor() { return this._zoomFactor }
 
+
     constructor(props: any) {
         super(props);
         // onClick play scripts
@@ -135,6 +153,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         }
     }
 
+
     public get IsTrimming() { return this._trimming; }
 
     @action
@@ -155,24 +174,31 @@ export class CollectionStackedTimeline extends CollectionSubView<
         this._zoomFactor = zoom;
     }
 
+
     anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
     anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val) ?? null);
+
+
+    // converts screen pixel offset to time
     toTimeline = (screen_delta: number, width: number) => {
         return Math.max(
             this.clipStart,
             Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart));
     }
 
+
     rangeClickScript = () => CollectionStackedTimeline.RangeScript;
     rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
 
-    // for creating key anchors with key events
+
+    // handles key events for for creating key anchors, scrubbing, exiting trim
     @action
     keyEvents = (e: KeyboardEvent) => {
         if (
             !(e.target instanceof HTMLInputElement) &&
             this.props.isSelected(true)
         ) {
+            // if shift pressed scrub 1 second otherwise 1/10th
             const jump = e.shiftKey ? 1 : 0.1;
             e.stopPropagation();
             switch (e.key) {
@@ -196,6 +222,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
                     }
                     break;
                 case "Escape":
+                    // abandons current trim
                     this._trimStart = this.clipStart;
                     this._trimStart = this.clipEnd;
                     this._trimming = TrimScope.None;
@@ -210,6 +237,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         }
     }
 
+
     getLinkData(l: Doc) {
         let la1 = l.anchor1 as Doc;
         let la2 = l.anchor2 as Doc;
@@ -224,7 +252,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
         return { la1, la2, linkTime };
     }
 
-    // starting the drag event for anchor resizing
+
+    // handles dragging selection to create markers
     @action
     onPointerDownTimeline = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
@@ -299,6 +328,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
 
     }
 
+
+    // for dragging trim start handle
     @action
     trimLeft = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
@@ -325,6 +356,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         );
     }
 
+    // for dragging trim end handle
     @action
     trimRight = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
@@ -351,12 +383,15 @@ export class CollectionStackedTimeline extends CollectionSubView<
         );
     }
 
+
+    // for rendering scrolling when timeline zoomed
     @action
     setScroll = (e: React.UIEvent) => {
         e.stopPropagation();
         this._scroll = this._timelineWrapper!.scrollLeft;
     }
 
+    // smooth scrolls to time like when following links overflowed due to zoom
     @action
     scrollToTime = (time: number) => {
         if (this._timelineWrapper) {
@@ -371,6 +406,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
         }
     }
 
+
+    // handles dragging and dropping markers in timeline
     @action
     internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
         if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
@@ -396,6 +433,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
         return false;
     }
 
+
+    // creates marker on timeline
     @undoBatch
     @action
     static createAnchor(
@@ -430,6 +469,7 @@ export class CollectionStackedTimeline extends CollectionSubView<
         return anchor;
     }
 
+
     @action
     playOnClick = (anchorDoc: Doc, clientX: number) => {
         const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
@@ -521,14 +561,20 @@ export class CollectionStackedTimeline extends CollectionSubView<
         return level;
     }
 
+
     dictationHeightPercent = 50;
     dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
+
     @computed get timelineContentHeight() { return this.props.PanelHeight() * this.dictationHeightPercent / 100; }
     @computed get timelineContentWidth() { return this.props.PanelWidth() * this.zoomFactor - 4 };  // subtract size of container border
+
     dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight);
+
     isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+
     currentTimecode = () => this.currentTime;
 
+
     @computed get renderDictation() {
         const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
         return !dictation ? null : (
@@ -565,23 +611,8 @@ export class CollectionStackedTimeline extends CollectionSubView<
             </div>
         );
     }
-    @computed get renderAudioWaveform() {
-        return !this.props.mediaPath ? null : (
-            <div className="collectionStackedTimeline-waveform">
-                <AudioWaveform
-                    rawDuration={this.props.rawDuration}
-                    duration={this.clipDuration}
-                    mediaPath={this.props.mediaPath}
-                    layoutDoc={this.layoutDoc}
-                    clipStart={this.clipStart}
-                    clipEnd={this.clipEnd}
-                    zoomFactor={this.zoomFactor}
-                    PanelHeight={this.timelineContentHeight}
-                    PanelWidth={this.timelineContentWidth}
-                />
-            </div>
-        );
-    }
+
+    // renders selection region on timeline
     @computed get selectionContainer() {
         const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
         return markerEnd === undefined ? null : (
@@ -668,7 +699,6 @@ export class CollectionStackedTimeline extends CollectionSubView<
                         );
                     })}
                     {!this.IsTrimming && this.selectionContainer}
-                    {/* {this.renderAudioWaveform} */}
                     <AudioWaveform
                         rawDuration={this.props.rawDuration}
                         duration={this.clipDuration}
@@ -728,6 +758,12 @@ export class CollectionStackedTimeline extends CollectionSubView<
     }
 }
 
+
+/**
+ * StackedTimelineAnchor
+ * creates the anchors to display markers, links, and embedded documents on timeline
+ */
+
 interface StackedTimelineAnchorProps {
     mark: Doc;
     rangeClickScript: () => ScriptField;
@@ -753,20 +789,26 @@ interface StackedTimelineAnchorProps {
     trimStart: number;
     trimEnd: number;
 }
+
+
 @observer
 class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
     _lastTimecode: number;
     _disposer: IReactionDisposer | undefined;
+
     constructor(props: any) {
         super(props);
         this._lastTimecode = this.props.currentTimecode();
     }
 
+    // updates marker document title to reflect correct timecodes
     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)}`;
     }
+
+
     componentDidMount() {
         this._disposer = reaction(
             () => this.props.currentTimecode(),
@@ -805,9 +847,12 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
             }
         );
     }
+
     componentWillUnmount() {
         this._disposer?.();
     }
+
+
     // starting the drag event for anchor resizing
     onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
         this.props._timeline?.setPointerCapture(e.pointerId);
@@ -851,11 +896,15 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
         );
     }
 
+
+    // context menu
     contextMenuItems = () => {
         const resetTitle = { script: ScriptField.MakeFunction(`self.title = "#" + formatToTime(self["${this.props.startTag}"]) + "-" + formatToTime(self["${this.props.endTag}"])`)!, icon: "folder-plus", label: "Reset Title" };
         return [resetTitle];
     }
 
+
+    // renders anchor LabelBox
     renderInner = computedFn(function (
         this: StackedTimelineAnchor,
         mark: Doc,
@@ -910,6 +959,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
     anchorScreenToLocalXf = () => this.props.ScreenToLocalTransform().translate(-this.props.left, -this.props.top);
     width = () => this.props.width;
     height = () => this.props.height;
+
     render() {
         const inner = this.renderInner(
             this.props.mark,
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 9351bc3be..f5de31fcb 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -22,6 +22,22 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComp
 import "./AudioBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
 
+
+/**
+ * AudioBox
+ * Main component: AudioBox.tsx
+ * Supporting Components: CollectionStackedTimeline, AudioWaveform
+ * 
+ * AudioBox is a node that supports the recording and playback of audio files in Dash.
+ * When an audio file is importeed into Dash, it is immediately rendered as an AudioBox document.
+ * When a blank AudioBox node is created in Dash, audio recording controls are displayed and the user can start a recording which can be paused or stopped, and can use dictation to create a text transcript.
+ * Recording is done using the MediaDevices API to access the user's device microphone (see recordAudioAnnotation below)
+ * CollectionStackedTimeline handles AudioBox and VideoBox shared behavior, but AudioBox handles playing, pausing, etc because it contains <audio> element
+ * User can trim audio: nondestructive, just sets new bounds for playback and rendering timelin
+ */
+
+
+// used as a wrapper class for MediaStream from MediaDevices API
 declare class MediaRecorder {
     constructor(e: any); // whatever MediaRecorder has
 }
@@ -35,44 +51,42 @@ enum media_state {
     Paused = "paused",
     Playing = "playing"
 }
+
+
 @observer
 export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, AudioDocument>(AudioDocument) {
 
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
-    public static SetScrubTime = action((timeInMillisFrom1970: number) => {
-        AudioBox._scrubTime = 0;
-        AudioBox._scrubTime = timeInMillisFrom1970;
-    });
     public static Enabled = false;
-    static topControlsHeight = 30; // width of playhead
-    static bottomControlsHeight = 20; // height of timeline in percent of height of audioBox.
-    @observable static _scrubTime = 0;
+
+    static topControlsHeight = 30; // height of upper controls above timeline
+    static bottomControlsHeight = 20; // height of lower controls below timeline
 
     _dropDisposer?: DragManager.DragDropDisposer;
     _disposers: { [name: string]: IReactionDisposer } = {};
-    _ele: HTMLAudioElement | null = null;
-    _recorder: any;
+    _ele: HTMLAudioElement | null = null; // <audio> ref
+    _recorder: any; // MediaRecorder
     _recordStart = 0;
-    _pauseStart = 0;
+    _pauseStart = 0; // time when recording is paused (used to keep track of recording timecodes)
     _pauseEnd = 0;
     _pausedTime = 0;
-    _stream: MediaStream | undefined;
-    _play: any = null;
+    _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio
+    _play: any = null; // timeout for playback
 
-    @observable _stackedTimeline: any;
-    @observable _finished: boolean = false;
+    @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+    @observable _finished: boolean = false; // has playback reached end of clip
     @observable _volume: number = 1;
     @observable _muted: boolean = false;
-    @observable _paused: boolean = false;
+    @observable _paused: boolean = false; // is recording paused
     // @observable rawDuration: number = 0; // computed from the length of the audio element when loaded
     @computed get recordingStart() { return DateCast(this.dataDoc[this.fieldKey + "-recordingStart"])?.date.getTime(); }
     @computed get rawDuration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } // bcz: shouldn't be needed since it's computed from audio element
     // mehek: not 100% sure but i think due to the order in which things are loaded this is necessary ^^
     // if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly
 
-    @computed get miniPlayer() { return this.props.PanelHeight() < 50 }
+    @computed get miniPlayer() { return this.props.PanelHeight() < 50 } // used to collapse timeline when node is shrunk
     @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 pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct recording time
     @computed get mediaState() { return this.layoutDoc.mediaState as media_state; }
     @computed get path() { // returns the path of the audio file
         const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || "";
@@ -80,12 +94,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
     set mediaState(value) { this.layoutDoc.mediaState = value; }
 
-    get timeline() { return this._stackedTimeline; } // can't be computed since it's not observable
+    @computed get timeline() { return this._stackedTimeline; } // returns CollectionStackedTimeline ref
+
 
     componentWillUnmount() {
         this.removeCurrentlyPlaying();
         this._dropDisposer?.();
         Object.values(this._disposers).forEach((disposer) => disposer?.());
+
+        // removes doc from active recordings if recording when closed
         const ind = DocUtils.ActiveRecordings.indexOf(this);
         ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
     }
@@ -102,6 +119,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     getLinkData(l: Doc) {
         let la1 = l.anchor1 as Doc;
         let la2 = l.anchor2 as Doc;
@@ -131,7 +149,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         ) || this.rootDoc;
     }
 
-    // for updating the timecode
+
+    // updates timecode and shows it in timeline, follows links at time
     @action
     timecodeChanged = () => {
         if (this.mediaState !== media_state.Recording && this._ele) {
@@ -148,7 +167,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
-    // play back the audio from time
+    // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
     @action
     playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
         clearTimeout(this._play); // abort any previous clip ending
@@ -156,8 +175,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
         }
         else if (this.timeline && this._ele && AudioBox.Enabled) {
+            // trimBounds override requested playback bounds
             const end = Math.min(this.timeline.trimEnd, endTime ?? this.timeline.trimEnd);
             const start = Math.max(this.timeline.trimStart, seekTimeInSeconds);
+            // checks if times are within clip range
             if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) {
                 this._ele.currentTime = start;
                 this._ele.play();
@@ -165,6 +186,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this.addCurrentlyPlaying();
                 this._play = setTimeout(
                     () => {
+                        // need to keep track of if end of clip is reached so on next play, clip restarts
                         if (fullPlay) this._finished = true;
                         // removes from currently playing if playback has reached end of range marker
                         else this.removeCurrentlyPlaying();
@@ -177,6 +199,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     // removes from currently playing display
     @action
     removeCurrentlyPlaying = () => {
@@ -186,6 +209,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // adds doc to currently playing display
     @action
     addCurrentlyPlaying = () => {
         if (!CollectionStackedTimeline.CurrentlyPlaying) {
@@ -196,6 +220,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     // update the recording time
     updateRecordTime = () => {
         if (this.mediaState === media_state.Recording) {
@@ -227,6 +252,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour
     }
 
+    // stops recording
     @action
     stopRecording = () => {
         if (this._recorder) {
@@ -240,6 +266,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     // context menu
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
@@ -270,6 +297,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         });
     }
 
+
     // button for starting and stopping the recording
     Record = (e: React.MouseEvent) => {
         if (e.button === 0 && !e.ctrlKey) {
@@ -284,11 +312,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         if (this.timeline && this._ele) {
             const eleTime = this._ele.currentTime;
+
+            // if curr timecode outside of trim bounds, set it to start
             let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
+            // restarts clip if reached end on last play
             if (this._finished) {
                 this._finished = false;
                 start = this.timeline.trimStart;
             }
+
             this.playFrom(start, this.timeline.trimEnd, true);
         }
     }
@@ -299,12 +332,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         if (this._ele) {
             this._ele.pause();
             this.mediaState = media_state.Paused;
+
+            // if paused in the middle of playback, prevents restart on next play
             if (!this._finished) clearTimeout(this._play);
             this.removeCurrentlyPlaying();
         }
     }
 
-    // creates a text document for dictation
+    // for dictation button, creates a text document for dictation
     onFile = (e: any) => {
         const newDoc = CurrentUserUtils.GetNewTextDoc(
             "",
@@ -326,13 +361,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         e.stopPropagation();
     }
 
-    // ref for updating time
+
+    // sets <audio> ref for updating time
     setRef = (e: HTMLAudioElement | null) => {
         e?.addEventListener("timeupdate", this.timecodeChanged);
         e?.addEventListener("ended", () => { this._finished = true; this.Pause() });
         this._ele = e;
     }
 
+
     // pause the time during recording phase
     @action
     recordPause = (e: React.MouseEvent) => {
@@ -351,6 +388,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         e.stopPropagation();
     }
 
+
+    // plays link
     playLink = (link: Doc) => {
         if (link.annotationOn === this.rootDoc) {
             if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
@@ -376,30 +415,39 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     @action
     timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
-        this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)
+        this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive);
+
     timelineScreenToLocal = () =>
-        this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight)
+        this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight);
+
     setPlayheadTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time;
+
     playing = () => this.mediaState === media_state.Playing;
+
     isActiveChild = () => this._isAnyChildContentActive;
 
+    // timeline dimensions
     timelineWidth = () => this.props.PanelWidth();
     timelineHeight = () => (this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight))
 
+    // ends trim, hides trim controls and displays new clip
     @undoBatch
-    finishTrim = () => { // hides trim controls and displays new clip
+    finishTrim = () => {
         this.Pause();
         this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this._ele!.currentTime), this.timeline?.trimStart || 0));
         this.timeline?.StopTrimming();
     }
 
+    // displays trim controls to start trimming clip
     startTrim = (scope: TrimScope) => {
         this.Pause();
         this.timeline?.StartTrimming(scope);
     }
 
+    // for trim button, double click displays full clip, single displays curr trim bounds
     onClipPointerDown = (e: React.PointerEvent) => {
         e.stopPropagation();
         this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
@@ -412,10 +460,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }));
     }
 
+
+    // for zoom slider, sets timeline waveform zoom
     zoom = (zoom: number) => {
         this.timeline?.setZoom(zoom);
     }
 
+    // for volume slider sets volume
     @action
     setVolume = (volume: number) => {
         if (this._ele) {
@@ -427,6 +478,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // toggles audio muted
     @action
     toggleMute = () => {
         if (this._ele) {
@@ -435,6 +487,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     setupTimelineDrop = (r: HTMLDivElement | null) => {
         if (r && this.timeline) {
             this._dropDisposer?.();
@@ -447,6 +500,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // UI for recording, initially displayed when new audio created in Dash
     @computed get recordingControls() {
         return <div className="audiobox-recorder">
             <div className="audiobox-dictation" onClick={this.onFile}>
@@ -478,6 +533,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         </div>
     }
 
+    // UI for playback, displayed for imported or recorded clips, hides timeline and collapses controls when node is shrunk vertically
     @computed get playbackControls() {
         return <div className="audiobox-file" style={{
             pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? "all" : "none",
@@ -544,6 +600,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         </div>
     }
 
+    // gets CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
             <CollectionStackedTimeline
@@ -577,6 +634,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             />
         );
     }
+
     // returns the html audio element
     @computed get audio() {
         return <audio ref={this.setRef}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e47b41539..9797178b2 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,6 +1,5 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
 import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from 'request-promise';
@@ -32,9 +31,24 @@ import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 const path = require('path');
 
+
+/**
+ * VideoBox
+ * Main component: VideoBox.tsx
+ * Supporting Components: CollectionStackedTimeline
+ * 
+ * VideoBox is a node that supports the playback of video files in Dash.
+ * When a video file or YouTube video is importeed into Dash, it is immediately rendered as a VideoBox document.
+ * CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element
+ * User can trim video: nondestructive, just sets new bounds for playback and rendering timeline
+ * Like images, users can zoom and pan and it has an overlay layer allowing for annotations on top of the video at different times
+ */
+
+
 type VideoDocument = makeInterface<[typeof documentSchema]>;
 const VideoDocument = makeInterface(documentSchema);
 
+
 @observer
 export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, VideoDocument>(VideoDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
@@ -54,42 +68,45 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             console.log("VideoBox :" + e);
         }
     }
+
     static _youtubeIframeCounter: number = 0;
-    static heightPercent = 80; // height of timeline in percent of height of videoBox.
+    static heightPercent = 80; // height of video relative to videoBox when timeline is open
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _youtubePlayer: YT.Player | undefined = undefined;
-    private _videoRef: HTMLVideoElement | null = null;
-    private _contentRef: HTMLDivElement | null = null;
+    private _videoRef: HTMLVideoElement | null = null; // <video> ref
+    private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
     private _youtubeIframeId: number = -1;
     private _youtubeContentCreated = false;
     private _audioPlayer: HTMLAudioElement | null = null;
-    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
-    private _playRegionTimer: any = null;
-    private _playRegionDuration = 0;
-    @observable _stackedTimeline: any;
-    @observable static _nativeControls: boolean;
-    @observable _marqueeing: number[] | undefined;
+    private _playRegionTimer: any = null; // timeout for playback
+    @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+    @observable static _nativeControls: boolean; // default html controls
+    @observable _marqueeing: number[] | undefined; // coords for marquee selection
     @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
     @observable _screenCapture = false;
-    @observable _clicking = false;
+    @observable _clicking = false; // used for transition between showing/hiding timeline
     @observable _forceCreateYouTubeIFrame = false;
     @observable _playTimer?: NodeJS.Timeout = undefined;
     @observable _fullScreen = false;
     @observable _playing = false;
-    @observable _finished: boolean = false;
+    @observable _finished: boolean = false; // has playback reached end of clip
     @observable _volume: number = 1;
     @observable _muted: boolean = false;
 
     @computed get links() { return DocListCast(this.dataDoc.links); }
-    @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); }
+    @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
     // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
     @observable rawDuration: number = 0;
 
+
     @computed get youtubeVideoId() {
         const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
     }
+
+
     // returns the path of the audio file
     @computed get audiopath() {
         const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
@@ -97,12 +114,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return field?.url.href ?? vfield?.url.href ?? "";
     }
 
-    private get timeline() { return this._stackedTimeline; }
-    private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; }
+
+    @computed private get timeline() { return this._stackedTimeline; }
+    private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
     public get player(): HTMLVideoElement | null { return this._videoRef; }
 
+
     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.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
         if (this.youtubeVideoId) {
             const youtubeaspect = 400 / 315;
             const nativeWidth = Doc.NativeWidth(this.layoutDoc);
@@ -122,15 +141,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
     }
 
+
+    // plays video
     @action public Play = (update: boolean = true) => {
         this._playing = true;
         const eleTime = this.player?.currentTime || 0;
         if (this.timeline) {
             let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
             if (this._finished) {
+                // restarts video if reached end on previous play
                 this._finished = false;
                 start = this.timeline.trimStart;
             }
+
             try {
                 this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
                 update && this.player && this.playFrom(start, undefined, true);
@@ -144,6 +168,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.updateTimecode();
     }
 
+    // goes to time
     @action public Seek(time: number) {
         try {
             this._youtubePlayer?.seekTo(Math.round(time), true);
@@ -154,6 +179,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this._audioPlayer && (this._audioPlayer.currentTime = time);
     }
 
+    // pauses video
     @action public Pause = (update: boolean = true) => {
         this._playing = false;
         this.removeCurrentlyPlaying();
@@ -169,9 +195,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
         this._playTimer = undefined;
         this.updateTimecode();
-        if (!this._finished) clearTimeout(this._playRegionTimer);;
+        if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
     }
 
+    // toggles video full screen
     @action public FullScreen = () => {
         if (document.fullscreenElement == this._contentRef) {
             this._fullScreen = false;
@@ -189,6 +216,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // creates and links snapshot photo of current video frame
     @action public Snapshot(downX?: number, downY?: number) {
         const width = (this.layoutDoc._width || 0);
         const canvas = document.createElement('canvas');
@@ -231,6 +260,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // creates link for snapshot
     createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
         const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
         const width = this.layoutDoc._width || 1;
@@ -249,12 +279,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
     }
 
+
     getAnchor = () => {
         const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
         const marquee = AnchorMenu.Instance.GetAnchor?.();
         return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
     }
 
+
+    // sets video info on load
     videoLoad = action(() => {
         const aspect = this.player!.videoWidth / this.player!.videoHeight;
         Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
@@ -265,6 +298,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     })
 
+
+    // updates video time
     @action
     updateTimecode = () => {
         this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
@@ -275,6 +310,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // sets video element ref
     @action
     setVideoRef = (vref: HTMLVideoElement | null) => {
         this._videoRef = vref;
@@ -288,6 +325,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // set ref for div that wraps video and controls for fullscreen
     @action
     setContentRef = (cref: HTMLDivElement | null) => {
         this._contentRef = cref;
@@ -296,6 +334,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // context menu
     specificContextMenu = (e: React.MouseEvent): void => {
         const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         if (field) {
@@ -321,8 +361,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     // ref for updating time
     setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
+
+    // renders the video and audio
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
         const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
@@ -350,6 +393,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             </div>;
     }
 
+
     @action youtubeIframeLoaded = (e: any) => {
         if (!this._youtubeContentCreated) {
             this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
@@ -359,6 +403,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         this.loadYouTube(e.target);
     }
+
     loadYouTube = (iframe: any) => {
         let started = true;
         const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
@@ -392,14 +437,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // for play button
     onPlayDown = () => this._playing ? this.Pause() : this.Play();
 
+    // for fullscreen button
     onFullDown = (e: React.PointerEvent) => {
         this.FullScreen();
         e.stopPropagation();
         e.preventDefault();
     }
 
+    // for snapshot button
     onSnapshotDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, (e) => {
             this.Snapshot(e.clientX, e.clientY);
@@ -407,6 +456,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }, emptyFunction, () => this.Snapshot());
     }
 
+    // for show/hide timeline button, transitions between show/hide
     @action
     onTimelineHdlDown = (e: React.PointerEvent) => {
         this._clicking = true;
@@ -427,18 +477,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             }, this.props.isContentActive(), this.props.isContentActive());
     }
 
-    onResetDown = (e: React.PointerEvent) => {
-        const start = this.timeline?.clipStart || 0;
-        setupMoveUpEvents(this, e,
-            e => {
-                this.Seek(Math.max(start, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
-                e.stopImmediatePropagation();
-                return false;
-            },
-            emptyFunction,
-            (e: PointerEvent) => this.layoutDoc._currentTimecode = 0);
-    }
 
+    // removes video from currently playing display
     @action
     removeCurrentlyPlaying = () => {
         if (CollectionStackedTimeline.CurrentlyPlaying) {
@@ -447,6 +487,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // adds video to currently playing display
     @action
     addCurrentlyPlaying = () => {
         if (!CollectionStackedTimeline.CurrentlyPlaying) {
@@ -457,6 +498,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
         this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
@@ -468,6 +510,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
     }
 
+
+    // for annotating, adds doc with time info
     @action.bound
     addDocWithTimecode(doc: Doc | Doc[]): boolean {
         const docs = doc instanceof Doc ? [doc] : doc;
@@ -476,7 +520,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return this.addDocument(doc);
     }
 
-    // play back the video from time
+
+    // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
     @action
     playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
         clearTimeout(this._playRegionTimer);
@@ -484,9 +529,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
         }
         else if (this.player) {
+            // trimBounds override requested playback bounds
             const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
             const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
-            this._playRegionDuration = end - start;
+            const playRegionDuration = end - start;
+            // checks if times are within clip range
             if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
                 this.player.currentTime = start;
                 this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
@@ -496,16 +543,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this.addCurrentlyPlaying();
                 this._playRegionTimer = setTimeout(
                     () => {
+                        // need to keep track of if end of clip is reached so on next play, clip restarts
                         if (fullPlay) this._finished = true;
+                        // removes from currently playing if playback has reached end of range marker
                         else this.removeCurrentlyPlaying();
                         this.Pause();
-                    }, this._playRegionDuration * 1000);
+                    }, playRegionDuration * 1000);
             } else {
                 this.Pause();
             }
         }
     }
-    // hides trim controls and displays new clip
+
+
+    // ends trim, hides trim controls and displays new clip
     @undoBatch
     finishTrim = action(() => {
         this.Pause();
@@ -513,12 +564,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.timeline?.StopTrimming();
     });
 
+    // displays trim controls to start trimming clip
     startTrim = (scope: TrimScope) => {
         this.Pause();
         this.timeline?.StartTrimming(scope);
     }
 
+    // for trim button, double click displays full clip, single displays curr trim bounds
     onClipPointerDown = (e: React.PointerEvent) => {
+        // if timeline isn't shown, show first then trim
         this.heightPercent >= 100 && this.onTimelineHdlDown(e);
         this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
             if (doubleTap) {
@@ -530,6 +584,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }));
     }
 
+
+    // for volume slider sets volume
     @action
     setVolume = (volume: number) => {
         if (this.player) {
@@ -541,6 +597,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // toggles video mute
     @action
     toggleMute = () => {
         if (this.player) {
@@ -549,6 +606,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // stretches vertically or horizontally depending on video orientation so video fits full screen
     fullScreenSize() {
         if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
             return { height: "100%" }
@@ -558,10 +617,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // for zoom slider, sets timeline waveform zoom
     zoom = (zoom: number) => {
         this.timeline?.setZoom(zoom);
     }
 
+
+    // plays link
     playLink = (doc: Doc) => {
         const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
         const endTime = this.timeline?.anchorEnd(doc);
@@ -571,6 +634,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+
+    // starts marquee selection
     marqueeDown = (e: React.PointerEvent) => {
         if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
             setupMoveUpEvents(this, e, action(e => {
@@ -581,6 +646,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    // ends marquee selection
     @action
     finishMarquee = () => {
         this._marqueeing = undefined;
@@ -588,23 +654,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
 
     timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+
     timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+
     setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
     timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+
     playing = () => this._playing;
 
     contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
+
     scaling = () => this.props.scaling?.() || 1;
+
     panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
     panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+
     screenToLocalTransform = () => {
         const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
         return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
     }
+
     marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
     marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+
     timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
 
+
+    // renders video controls
     @computed get uIButtons() {
         const curTime = (this.layoutDoc._currentTimecode || 0) - (this.timeline?.clipStart || 0);
         return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent == 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
@@ -677,6 +754,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 </>}
         </div>
     }
+
+    // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
             <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
@@ -705,9 +784,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             />
         </div>;
     }
+
+    // renders annotation layer
     @computed get annotationLayer() {
         return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
     }
+
     render() {
         const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
         const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
-- 
cgit v1.2.3-70-g09d2

