From 20add09510fb02b144d421910a56d3f3896b1f90 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 26 Jan 2021 01:38:55 -0500
Subject: preparing to unify VideoBox timeline with AudioBox timeline.  changed
 names from videoStart/audioStart,End to anchorStart,EndTime and
 _displayTimecode to _timecodeToShow etc

---
 src/client/views/collections/CollectionView.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 83c639871..6b9b1a3c0 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -158,14 +158,14 @@ export class CollectionView extends Touchable<CollectionViewProps> {
                             const pushpin = Docs.Create.FontIconDocument({
                                 title: "pushpin", label: "",
                                 icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
-                                _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
+                                _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null)
                             });
                             pushpin.isPushpin = true;
                             Doc.GetProto(pushpin).annotationOn = doc.annotationOn;
                             Doc.SetInPlace(doc, "annotationOn", undefined, true);
                             Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin);
                             const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", "");
-                            doc.displayTimecode = undefined;
+                            doc._timecodeToShow = undefined;
                         }
                         doc._stayInCollection = undefined;
                         doc.context = this.props.Document;
-- 
cgit v1.2.3-70-g09d2


From eb2e88ef810eed9c1d31b3b2fdc3ba848f067c53 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 26 Jan 2021 20:12:55 -0500
Subject: made StackedTimeline a collectionview and renamed
 CollectionStackedTimeline.  Now timeline anchors will observe filters.

---
 src/client/views/GlobalKeyHandler.ts               |   6 +-
 .../collections/CollectionStackedTimeline.scss     | 373 ++++++++++++++++++++
 .../collections/CollectionStackedTimeline.tsx      | 324 ++++++++++++++++++
 src/client/views/collections/CollectionView.tsx    |   3 +-
 src/client/views/nodes/AudioBox.tsx                |  49 ++-
 src/client/views/nodes/StackedTimeline.scss        | 374 ---------------------
 src/client/views/nodes/StackedTimeline.tsx         | 335 ------------------
 src/client/views/nodes/VideoBox.tsx                |  27 +-
 8 files changed, 752 insertions(+), 739 deletions(-)
 create mode 100644 src/client/views/collections/CollectionStackedTimeline.scss
 create mode 100644 src/client/views/collections/CollectionStackedTimeline.tsx
 delete mode 100644 src/client/views/nodes/StackedTimeline.scss
 delete mode 100644 src/client/views/nodes/StackedTimeline.tsx

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index a07ba0a77..e56ba38dd 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -24,7 +24,7 @@ import { DocumentDecorations } from "./DocumentDecorations";
 import { InkStrokeProperties } from "./InkStrokeProperties";
 import { MainView } from "./MainView";
 import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
-import { StackedTimeline } from "./nodes/StackedTimeline";
+import { CollectionStackedTimeline } from "./collections/CollectionStackedTimeline";
 import { AnchorMenu } from "./pdf/AnchorMenu";
 import { SearchBox } from "./search/SearchBox";
 
@@ -121,8 +121,8 @@ export class KeyManager {
                     DragManager.AbortDrag();
                 } else if (CollectionDockingView.Instance.HasFullScreen) {
                     CollectionDockingView.Instance.CloseFullScreen();
-                } else if (StackedTimeline.SelectingRegion) {
-                    StackedTimeline.SelectingRegion = undefined;
+                } else if (CollectionStackedTimeline.SelectingRegion) {
+                    CollectionStackedTimeline.SelectingRegion = undefined;
                     doDeselect = false;
                 } else {
                     doDeselect = !ContextMenu.Instance.closeMenu();
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
new file mode 100644
index 000000000..1bb5bc720
--- /dev/null
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -0,0 +1,373 @@
+.audiobox-container,
+.audiobox-container-interactive {
+    width: 100%;
+    height: 100%;
+    position: inherit;
+    display: flex;
+    position: relative;
+    cursor: default;
+
+    .audiobox-inner {
+        width:100%;
+        height: 100%;
+    }
+
+    .audiobox-buttons {
+        display: flex;
+        width: 100%;
+        align-items: center;
+        height: 100%;
+
+        .audiobox-dictation {
+            position: relative;
+            width: 30px;
+            height: 100%;
+            align-items: center;
+            display: inherit;
+            background: dimgray;
+            left: 0px;
+        }
+
+        .audiobox-dictation:hover {
+            color: white;
+            cursor: pointer;
+        }
+    }
+
+    .audiobox-handle {
+        width: 20px;
+        height: 100%;
+        display: inline-block;
+    }
+
+    .audiobox-control,
+    .audiobox-control-interactive {
+        top: 0;
+        max-height: 32px;
+        width: 100%;
+        display: inline-block;
+        pointer-events: none;
+    }
+
+    .audiobox-control-interactive {
+        pointer-events: all;
+    }
+
+    .audiobox-record {
+        pointer-events: all;
+        width: 100%;
+        height: 100%;
+        position: relative;
+        pointer-events: none;
+    }
+
+    .audiobox-record-interactive {
+        pointer-events: all;
+        width: 100%;
+        height: 100%;
+        position: relative;
+
+        
+    }
+
+    .recording {
+        margin-top: auto;
+        margin-bottom: auto;
+        width: 100%;
+        height: 100%;
+        position: relative;
+        padding-right: 5px;
+        display: flex;
+        background-color: red;      
+
+        .time {
+            position: relative;
+            height: 100%;
+            width: 100%;
+            font-size: 20;
+            text-align: center;
+            top: 5;
+        }
+
+        .buttons {
+            position: relative;
+            margin-top: auto;
+            margin-bottom: auto;
+            width: 25px;
+            padding: 5px;
+        }
+
+        .buttons:hover {
+            background-color: crimson;
+        }
+    }
+
+    .audiobox-controls {
+        width: 100%;
+        height: 100%;
+        position: relative;
+        display: flex;
+        padding-left: 2px;
+        background: black;
+
+        .audiobox-dictation {
+            position: absolute;
+            width: 30px;
+            height: 100%;
+            align-items: center;
+            display: inherit;
+            background: dimgray;
+            left: 0px;
+        }
+
+        .audiobox-player {
+            margin-top: auto;
+            margin-bottom: auto;
+            width: 100%;
+            position: relative;
+            padding-right: 5px;
+            display: flex;
+
+            .audiobox-playhead {
+                position: relative;
+                margin-top: auto;
+                margin-bottom: auto;
+                margin-right: 2px;
+                height: 25px;
+                padding: 2px;
+                border-radius: 50%;
+                background-color: black;
+                color: white;
+            }
+
+            .audiobox-playhead:hover {
+                // background-color:  black;
+                // border-radius: 5px;
+                background-color: grey;
+                color: lightgrey;
+            }
+
+            .audiobox-dictation {
+                position: relative;
+                margin-top: auto;
+                margin-bottom: auto;
+                width: 25px;
+                padding: 2px;
+                align-items: center;
+                display: inherit;
+                background: dimgray;
+            }
+
+            .audiobox-timeline {
+                position: absolute;
+                width: 100%;
+                border: gray solid 1px;
+                border-radius: 3px;
+                z-index: 1000;
+                overflow: hidden;
+
+                .audiobox-container {
+                    position: absolute;
+                    width: 10px;
+                    top: 2.5%;
+                    height: 0px;
+                    background: lightblue;
+                    border-radius: 5px;
+                    // box-shadow: black 2px 2px 1px;
+                    opacity: 0.3;
+                    z-index: 500;
+                    border-style: solid;
+                    border-color: darkblue;
+                    border-width: 1px;
+                }
+
+                .audiobox-current {
+                    width: 1px;
+                    height: 100%;
+                    background-color: red;
+                    position: absolute;
+                    top: 0px;
+                }
+
+                .waveform {
+                    position: relative;
+                    width: 100%;
+                    height: 100%;
+                    overflow: hidden;
+                    z-index: -1000;
+                    bottom: 0;
+                    pointer-events: none;
+                    div {
+                        height: 100% !important;
+                        width: 100%  !important;
+                    }
+                    canvas { 
+                        height: 100% !important;
+                        width: 100%  !important;
+                    }
+                }
+
+                .audiobox-linker,
+                .audiobox-linker-mini {
+                    position: absolute;
+                    width: 15px;
+                    min-height: 10px;
+                    height: 15px;
+                    margin-left: -2.55px;
+                    background: gray;
+                    border-radius: 100%;
+                    opacity: 0.9;
+                    box-shadow: black 2px 2px 1px;
+
+                    .linkAnchorBox-cont {
+                        position: relative !important;
+                        height: 100% !important;
+                        width: 100% !important;
+                        left: unset !important;
+                        top: unset !important;
+                    }
+                }
+
+                .audiobox-linker-mini {
+                    width: 8px;
+                    min-height: 8px;
+                    height: 8px;
+                    box-shadow: black 1px 1px 1px;
+                    margin-left: -1;
+                    margin-top: -2;
+
+                    .linkAnchorBox-cont {
+                        position: relative !important;
+                        height: 100% !important;
+                        width: 100% !important;
+                        left: unset !important;
+                        top: unset !important;
+                    }
+                }
+
+                .audiobox-linker:hover,
+                .audiobox-linker-mini:hover {
+                    opacity: 1;
+                }
+
+                .audiobox-marker-container,
+                .audiobox-marker-minicontainer {
+                    position: absolute;
+                    width: 10px;
+                    height: 10px;
+                    top: 2.5%;
+                    background: gray;
+                    border-radius: 50%;
+                    box-shadow: black 2px 2px 1px;
+                    overflow: visible;
+                    cursor: pointer;
+
+                    .audiobox-marker {
+                        position: relative;
+                        height: 100%;
+                        // height: calc(100% - 15px);
+                        width: 100%;
+                        //margin-top: 15px;
+                    }
+
+                    .audio-marker:hover {
+                        border: orange 2px solid;
+                    }
+                }
+
+                .audiobox-marker-timeline,
+                .audiobox-marker-minicontainer {
+                    position: absolute;
+                    width: 10px;
+                    height: 90%;
+                    top: 2.5%;
+                    border-radius: 5px;
+
+                    .left-resizer {
+                        background: dimgrey;     
+                    }
+                    .resizer {
+                        background: dimgrey;
+                    }
+
+                    .audiobox-marker {
+                        position: relative;
+                        height: calc(100% - 15px);
+                        margin-top: 15px;
+                    }
+
+                    .audio-marker:hover {
+                        border: orange 2px solid;
+                    }
+
+                    .resizer {
+                        position: absolute;
+                        top: 0;
+                        right: 0;
+                        pointer-events: all;
+                        cursor: ew-resize;
+                        height: 100%;
+                        width: 10px;
+                        z-index: 100;
+                    }
+
+                    .click {
+                        position: relative;
+                        height: 100%;
+                        width: 100%;
+                        z-index: 100;
+                    }
+
+                    .left-resizer {
+                        position: absolute;
+                        left: 0;
+                        top : 0;
+                        pointer-events: all;
+                        cursor: ew-resize;
+                        height: 100%;
+                        width: 10px;
+                        z-index: 100;
+                    }
+                }
+
+                .audiobox-marker-timeline:hover,
+                .audiobox-marker-minicontainer:hover {
+                    opacity: 0.8;
+                }
+
+                .audiobox-marker-minicontainer {
+                    width: 5px;
+                    border-radius: 1px;
+
+                    .audiobox-marker {
+                        position: relative;
+                        height: calc(100% - 8px);
+                        margin-top: 8px;
+                    }
+                }
+            }
+        }
+    }
+}
+
+@media only screen and (max-device-width: 480px) {
+    .audiobox-dictation {
+        font-size: 5em;
+        display: flex;
+        width: 100;
+        justify-content: center;
+        flex-direction: column;
+        align-items: center;
+    }
+
+    .audiobox-container .audiobox-record,
+    .audiobox-container-interactive .audiobox-record {
+        font-size: 3em;
+    }
+
+    .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
+    .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
+    .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead {
+        width: 70px;
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
new file mode 100644
index 000000000..1775250fa
--- /dev/null
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -0,0 +1,324 @@
+import React = require("react");
+import { action, computed, IReactionDisposer, observable } from "mobx";
+import { observer } from "mobx-react";
+import { computedFn } from "mobx-utils";
+import { Doc, Opt } from "../../../fields/Doc";
+import { Id } from "../../../fields/FieldSymbols";
+import { List } from "../../../fields/List";
+import { listSpec, makeInterface } from "../../../fields/Schema";
+import { ComputedField, ScriptField } from "../../../fields/ScriptField";
+import { Cast, NumCast } from "../../../fields/Types";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { Scripting } from "../../util/Scripting";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionSubView } from "../collections/CollectionSubView";
+import { DocumentView } from "../nodes/DocumentView";
+import { LabelBox } from "../nodes/LabelBox";
+import "./CollectionStackedTimeline.scss";
+
+type PanZoomDocument = makeInterface<[]>;
+const PanZoomDocument = makeInterface();
+export type CollectionStackedTimelineProps = {
+    duration: number;
+    Play: () => void;
+    Pause: () => void;
+    playLink: (linkDoc: Doc) => void;
+    playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
+    playing: () => boolean;
+    setTime: (time: number) => void;
+    isChildActive: () => boolean;
+};
+
+@observer
+export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) {
+    static RangeScript: ScriptField;
+    static LabelScript: ScriptField;
+    static RangePlayScript: ScriptField;
+    static LabelPlayScript: ScriptField;
+
+    _disposers: { [name: string]: IReactionDisposer } = {};
+    _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack!  this must be called _doubleTime since setupMoveDragEvents will use that field name
+    _ele: HTMLAudioElement | null = null;
+    _start: number = 0;
+    _left: boolean = false;
+    _dragging = false;
+    _play: any = null;
+    _audioRef = React.createRef<HTMLDivElement>();
+    _timeline: Opt<HTMLDivElement>;
+    _markerStart: number = 0;
+    _currAnchor: Opt<Doc>;
+
+    @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
+    @observable _markerEnd: number = 0;
+    @observable _position: number = 0;
+    @computed get anchorDocs() { return this.childDocs; }
+    @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); }
+
+    constructor(props: any) {
+        super(props);
+        // onClick play scripts
+        CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
+        CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
+        CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
+        CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
+    }
+
+    // for creating key anchors with key events
+    @action
+    keyEvents = (e: KeyboardEvent) => {
+        if (e.target instanceof HTMLInputElement) return;
+        if (!this.props.playing()) return; // can't create if video is not playing
+        switch (e.key) {
+            case "x": // currently set to x, but can be a different key
+                const currTime = this.currentTime;
+                if (this._start) {
+                    this._markerStart = currTime;
+                    // this._start = false;
+                    // this._visible = true;
+                } else {
+                    this.createAnchor(this._markerStart, currTime);
+                    // this._start = true;
+                    // this._visible = false;
+                }
+        }
+    }
+
+    anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)));
+    anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)));
+
+    getLinkData(l: Doc) {
+        let la1 = l.anchor1 as Doc;
+        let la2 = l.anchor2 as Doc;
+        const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime));
+        if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+            la1 = l.anchor2 as Doc;
+            la2 = l.anchor1 as Doc;
+        }
+        return { la1, la2, linkTime };
+    }
+
+    // ref for timeline
+    timelineRef = (timeline: HTMLDivElement) => {
+        this._timeline = timeline;
+    }
+
+    // updates the anchor with the new time
+    @action
+    changeAnchor = (anchor: Opt<Doc>, time: number) => {
+        anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time);
+    }
+
+    // checks if the two anchors are the same with start and end time
+    isSame = (m1: any, m2: any) => {
+        return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2);
+    }
+
+    @computed get selectionContainer() {
+        return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
+            left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.props.duration * 100}%`,
+            width: `${Math.abs(this._markerStart - this._markerEnd) / this.props.duration * 100}%`, height: "100%", top: "0%"
+        }} />;
+    }
+
+    // starting the drag event for anchor resizing
+    @action
+    onPointerDownTimeline = (e: React.PointerEvent): void => {
+        const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect();
+        if (rect && e.target !== this._audioRef.current && this.props.active()) {
+            const wasPlaying = this.props.playing();
+            if (wasPlaying) this.props.Pause();
+            else if (!this._doubleTime) {
+                this._doubleTime = setTimeout(() => {
+                    this._doubleTime = undefined;
+                    this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration);
+                }, 300);
+            }
+            this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+            CollectionStackedTimeline.SelectingRegion = this;
+            setupMoveUpEvents(this, e,
+                action(e => {
+                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+                    return false;
+                }),
+                action((e, movement) => {
+                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+                    if (this._markerEnd < this._markerStart) {
+                        const tmp = this._markerStart;
+                        this._markerStart = this._markerEnd;
+                        this._markerEnd = tmp;
+                    }
+                    CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd);
+                    CollectionStackedTimeline.SelectingRegion = undefined;
+                }),
+                (e, doubleTap) => {
+                    this.props.select(false);
+                    e.shiftKey && this.createAnchor(this.currentTime);
+                    !wasPlaying && doubleTap && this.props.Play();
+                }
+                , this.props.isSelected(true) || this.props.isChildActive());
+        }
+    }
+
+    @action
+    createAnchor(anchorStartTime?: number, anchorEndTime?: number) {
+        if (anchorStartTime === undefined) return this.props.Document;
+        const anchor = Docs.Create.LabelDocument({
+            title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any,
+            useLinkSmallAnchor: true,
+            hideLinkButton: true,
+            anchorStartTime,
+            anchorEndTime,
+            annotationOn: this.props.Document
+        });
+        if (Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), null) !== undefined) {
+            Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []).push(anchor);
+        } else {
+            this.dataDoc[this.props.fieldKey] = new List<Doc>([anchor]);
+        }
+        return anchor;
+    }
+
+    // play back the audio from time
+    @action
+    playOnClick = (anchorDoc: Doc) => {
+        this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration));
+        return { select: true };
+    }
+
+    // play back the audio from time
+    @action
+    clickAnchor = (anchorDoc: Doc) => {
+        if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc);
+        this.props.setTime(this.anchorStart(anchorDoc));
+        return { select: true };
+    }
+
+    toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.duration));
+    // starting the drag event for anchor resizing
+    onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => {
+        this._currAnchor = m;
+        this._left = left;
+        this._timeline?.setPointerCapture(e.pointerId);
+        setupMoveUpEvents(this, e,
+            (e) => {
+                const rect = (e.target as any).getBoundingClientRect();
+                this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width));
+                return false;
+            },
+            (e) => {
+                const rect = (e.target as any).getBoundingClientRect();
+                this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width));
+                this._timeline?.releasePointerCapture(e.pointerId);
+            },
+            emptyFunction);
+    }
+
+    rangeClickScript = () => CollectionStackedTimeline.RangeScript;
+    labelClickScript = () => CollectionStackedTimeline.LabelScript;
+    rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
+    labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript;
+
+    // makes sure no anchors overlaps each other by setting the correct position and width
+    getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => {
+        const timelineContentWidth = this.props.PanelWidth();
+        const x1 = this.anchorStart(m);
+        const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration);
+        let max = 0;
+        const overlappedLevels = new Set(placed.map(p => {
+            const y1 = p.anchorStartTime;
+            const y2 = p.anchorEndTime;
+            if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) ||
+                (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) {
+                max = Math.max(max, p.level);
+                return p.level;
+            }
+        }));
+        let level = max + 1;
+        for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
+
+        placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
+        return level;
+    }
+
+    renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+        const anchor = observable({ view: undefined as any });
+        return {
+            anchor, view: <DocumentView key="view"  {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+                ref={action((r: DocumentView | null) => anchor.view = r)}
+                Document={mark}
+                DataDoc={undefined}
+                PanelWidth={() => width}
+                PanelHeight={() => height}
+                renderDepth={this.props.renderDepth + 1}
+                focus={() => this.props.playLink(mark)}
+                rootSelected={returnFalse}
+                LayoutTemplate={undefined}
+                LayoutTemplateString={LabelBox.LayoutString("data")}
+                ContainingCollectionDoc={this.props.Document}
+                removeDocument={this.props.removeDocument}
+                ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)}
+                parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()}
+                whenActiveChanged={this.props.whenActiveChanged}
+                onClick={script}
+                onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript}
+                ignoreAutoHeight={false}
+                bringToFront={emptyFunction}
+                scriptContext={this} />
+        };
+    });
+    renderAnchor = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+        const inner = this.renderInner(mark, script, doublescript, x, y, width, height);
+        return <>
+            {inner.view}
+            {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
+                <>
+                    <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} />
+                    <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} />
+                </>}
+        </>;
+    });
+
+    render() {
+        const timelineContentWidth = this.props.PanelWidth();
+        const timelineContentHeight = this.props.PanelHeight();
+        const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
+        const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
+        const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
+        return <div className="audiobox-timeline" style={{ height: "100%", width: "100%" }} ref={this.timelineRef}
+            onClick={e => {
+                if (this.props.isChildActive() || this.props.isSelected(false)) {
+                    e.stopPropagation(); e.preventDefault();
+                }
+            }}
+            onPointerDown={e => {
+                if (this.props.isChildActive() || this.props.isSelected(false)) {
+                    e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e);
+                }
+            }}>
+            {drawAnchors.map(d => {
+                const m = d.anchor;
+                const start = this.anchorStart(m);
+                const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.props.duration);
+                const left = start / this.props.duration * timelineContentWidth;
+                const top = d.level / maxLevel * timelineContentHeight;
+                const timespan = end - start;
+                return this.props.Document.hideAnchors ? (null) :
+                    <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]}
+                        style={{ left, top, width: `${timespan / this.props.duration * 100}%`, height: `${1 / maxLevel * 100}%` }}
+                        onClick={e => { this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} >
+                        {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript,
+                            left,
+                            top,
+                            timelineContentWidth * timespan / this.props.duration,
+                            timelineContentHeight / maxLevel)}
+                    </div>;
+            })}
+            {this.selectionContainer}
+            <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
+                style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }}
+            />
+        </div>;
+    }
+}
+Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6b9b1a3c0..03d8606d7 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -59,7 +59,8 @@ export enum CollectionViewType {
     //Staff = "staff",
     Map = "map",
     Grid = "grid",
-    Pile = "pileup"
+    Pile = "pileup",
+    StackedTimeline = "stacked timeline"
 }
 export interface CollectionViewProps extends FieldViewProps {
     isAnnotationOverlay?: boolean;  // is the collection an annotation overlay (eg an overlay on an image/video/etc)
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index f509bfd64..c8bec74fb 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -9,14 +9,15 @@ import { Doc, DocListCast, Opt } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
 import { List } from "../../../fields/List";
 import { createSchema, listSpec, makeInterface } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
+import { ComputedField } from "../../../fields/ScriptField";
 import { Cast, NumCast } from "../../../fields/Types";
 import { AudioField, nullAudio } from "../../../fields/URLField";
-import { formatTime, numberRange, Utils } from "../../../Utils";
+import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils";
 import { DocUtils } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SnappingManager } from "../../util/SnappingManager";
+import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
@@ -24,7 +25,6 @@ import "./AudioBox.scss";
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
 import { LinkDocPreview } from "./LinkDocPreview";
-import { StackedTimeline } from "./StackedTimeline";
 declare class MediaRecorder {
     // whatever MediaRecorder has
     constructor(e: any);
@@ -46,7 +46,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
     _disposers: { [name: string]: IReactionDisposer } = {};
     _ele: HTMLAudioElement | null = null;
     _audioRef = React.createRef<HTMLDivElement>();
-    _stackedTimeline = React.createRef<StackedTimeline>();
+    _stackedTimeline = React.createRef<CollectionStackedTimeline>();
     _recorder: any;
     _recordStart = 0;
     _pauseStart = 0;
@@ -327,30 +327,43 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
             );
     }
 
-    playing = () => { return this.audioState === "playing"; }
+    playing = () => this.audioState === "playing";
     playLink = (link: Doc) => {
         if (link.annotationOn === this.rootDoc) {
             if (this.layoutDoc.playOnSelect) this.playFrom(this._stackedTimeline.current?.anchorStart(link) || 0, this._stackedTimeline.current?.anchorEnd(link));
             else this._ele!.currentTime = this.layoutDoc._currentTimecode = (this._stackedTimeline.current?.anchorStart(link) || 0);
         }
-        else this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => {
-            const { la1, la2 } = this.getLinkData(l);
-            const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null));
-            const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null));
-            if (startTime !== undefined) {
-                if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
-                else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
-            }
-        });
+        else {
+            this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => {
+                const { la1, la2 } = this.getLinkData(l);
+                const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null));
+                const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null));
+                if (startTime !== undefined) {
+                    if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
+                    else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
+                }
+            });
+        }
     }
 
     @computed get renderTimeline() {
-        return <StackedTimeline ref={this._stackedTimeline}
+        return <CollectionStackedTimeline ref={this._stackedTimeline}
             Document={this.props.Document}
-            dataDoc={this.dataDoc}
-            anchorProps={this.props}
+            fieldKey={this.annotationKey}
             renderDepth={this.props.renderDepth + 1}
-            annotationKey={this.annotationKey}
+            parentActive={this.props.parentActive}
+            focus={emptyFunction}
+            styleProvider={this.props.styleProvider}
+            docFilters={this.props.docFilters}
+            docRangeFilters={this.props.docRangeFilters}
+            searchFilterDocs={this.props.searchFilterDocs}
+            rootSelected={this.props.rootSelected}
+            addDocTab={this.props.addDocTab}
+            pinToPres={this.props.pinToPres}
+            bringToFront={emptyFunction}
+            ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+            ContainingCollectionView={this.props.ContainingCollectionView}
+            CollectionView={undefined}
             duration={this.duration}
             playFrom={this.playFrom}
             setTime={(time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time}
diff --git a/src/client/views/nodes/StackedTimeline.scss b/src/client/views/nodes/StackedTimeline.scss
deleted file mode 100644
index da7310794..000000000
--- a/src/client/views/nodes/StackedTimeline.scss
+++ /dev/null
@@ -1,374 +0,0 @@
-.audiobox-container,
-.audiobox-container-interactive {
-    width: 100%;
-    height: 100%;
-    position: inherit;
-    display: flex;
-    position: relative;
-    cursor: default;
-
-    .audiobox-inner {
-        width:100%;
-        height: 100%;
-    }
-
-    .audiobox-buttons {
-        display: flex;
-        width: 100%;
-        align-items: center;
-        height: 100%;
-
-        .audiobox-dictation {
-            position: relative;
-            width: 30px;
-            height: 100%;
-            align-items: center;
-            display: inherit;
-            background: dimgray;
-            left: 0px;
-        }
-
-        .audiobox-dictation:hover {
-            color: white;
-            cursor: pointer;
-        }
-    }
-
-    .audiobox-handle {
-        width: 20px;
-        height: 100%;
-        display: inline-block;
-    }
-
-    .audiobox-control,
-    .audiobox-control-interactive {
-        top: 0;
-        max-height: 32px;
-        width: 100%;
-        display: inline-block;
-        pointer-events: none;
-    }
-
-    .audiobox-control-interactive {
-        pointer-events: all;
-    }
-
-    .audiobox-record {
-        pointer-events: all;
-        width: 100%;
-        height: 100%;
-        position: relative;
-        pointer-events: none;
-    }
-
-    .audiobox-record-interactive {
-        pointer-events: all;
-        width: 100%;
-        height: 100%;
-        position: relative;
-
-        
-    }
-
-    .recording {
-        margin-top: auto;
-        margin-bottom: auto;
-        width: 100%;
-        height: 100%;
-        position: relative;
-        padding-right: 5px;
-        display: flex;
-        background-color: red;      
-
-        .time {
-            position: relative;
-            height: 100%;
-            width: 100%;
-            font-size: 20;
-            text-align: center;
-            top: 5;
-        }
-
-        .buttons {
-            position: relative;
-            margin-top: auto;
-            margin-bottom: auto;
-            width: 25px;
-            padding: 5px;
-        }
-
-        .buttons:hover {
-            background-color: crimson;
-        }
-    }
-
-    .audiobox-controls {
-        width: 100%;
-        height: 100%;
-        position: relative;
-        display: flex;
-        padding-left: 2px;
-        background: black;
-
-        .audiobox-dictation {
-            position: absolute;
-            width: 30px;
-            height: 100%;
-            align-items: center;
-            display: inherit;
-            background: dimgray;
-            left: 0px;
-        }
-
-        .audiobox-player {
-            margin-top: auto;
-            margin-bottom: auto;
-            width: 100%;
-            position: relative;
-            padding-right: 5px;
-            display: flex;
-
-            .audiobox-playhead {
-                position: relative;
-                margin-top: auto;
-                margin-bottom: auto;
-                margin-right: 2px;
-                height: 25px;
-                padding: 2px;
-                border-radius: 50%;
-                background-color: black;
-                color: white;
-            }
-
-            .audiobox-playhead:hover {
-                // background-color:  black;
-                // border-radius: 5px;
-                background-color: grey;
-                color: lightgrey;
-            }
-
-            .audiobox-dictation {
-                position: relative;
-                margin-top: auto;
-                margin-bottom: auto;
-                width: 25px;
-                padding: 2px;
-                align-items: center;
-                display: inherit;
-                background: dimgray;
-            }
-
-            .audiobox-timeline {
-                position: absolute;
-                width: 100%;
-                border: gray solid 1px;
-                border-radius: 3px;
-                z-index: 1000;
-                overflow: hidden;
-
-                .audiobox-container {
-                    position: absolute;
-                    width: 10px;
-                    top: 2.5%;
-                    height: 0px;
-                    background: lightblue;
-                    border-radius: 5px;
-                    // box-shadow: black 2px 2px 1px;
-                    opacity: 0.3;
-                    z-index: 500;
-                    border-style: solid;
-                    border-color: darkblue;
-                    border-width: 1px;
-                }
-
-                .audiobox-current {
-                    width: 1px;
-                    height: 100%;
-                    background-color: red;
-                    position: absolute;
-                    top: 0px;
-                }
-
-                .waveform {
-                    position: relative;
-                    width: 100%;
-                    height: 100%;
-                    overflow: hidden;
-                    z-index: -1000;
-                    bottom: 0;
-                    pointer-events: none;
-                    div {
-                        height: 100% !important;
-                        width: 100%  !important;
-                    }
-                    canvas { 
-                        height: 100% !important;
-                        width: 100%  !important;
-                    }
-                }
-
-                .audiobox-linker,
-                .audiobox-linker-mini {
-                    position: absolute;
-                    width: 15px;
-                    min-height: 10px;
-                    height: 15px;
-                    margin-left: -2.55px;
-                    background: gray;
-                    border-radius: 100%;
-                    opacity: 0.9;
-                    box-shadow: black 2px 2px 1px;
-
-                    .linkAnchorBox-cont {
-                        position: relative !important;
-                        height: 100% !important;
-                        width: 100% !important;
-                        left: unset !important;
-                        top: unset !important;
-                    }
-                }
-
-                .audiobox-linker-mini {
-                    width: 8px;
-                    min-height: 8px;
-                    height: 8px;
-                    box-shadow: black 1px 1px 1px;
-                    margin-left: -1;
-                    margin-top: -2;
-
-                    .linkAnchorBox-cont {
-                        position: relative !important;
-                        height: 100% !important;
-                        width: 100% !important;
-                        left: unset !important;
-                        top: unset !important;
-                    }
-                }
-
-                .audiobox-linker:hover,
-                .audiobox-linker-mini:hover {
-                    opacity: 1;
-                }
-
-                .audiobox-marker-container,
-                .audiobox-marker-minicontainer {
-                    position: absolute;
-                    width: 10px;
-                    height: 10px;
-                    top: 2.5%;
-                    background: gray;
-                    border-radius: 50%;
-                    box-shadow: black 2px 2px 1px;
-                    overflow: visible;
-                    cursor: pointer;
-
-                    .audiobox-marker {
-                        position: relative;
-                        height: 100%;
-                        // height: calc(100% - 15px);
-                        width: 100%;
-                        //margin-top: 15px;
-                    }
-
-                    .audio-marker:hover {
-                        border: orange 2px solid;
-                    }
-                }
-
-                .audiobox-marker-timeline,
-                .audiobox-marker-minicontainer {
-                    position: absolute;
-                    width: 10px;
-                    height: 90%;
-                    top: 2.5%;
-                    border-radius: 5px;
-
-                    .left-resizer {
-                        background: dimgrey;     
-                    }
-                    .resizer {
-                        background: dimgrey;
-                    }
-
-                    .audiobox-marker {
-                        position: relative;
-                        height: calc(100% - 15px);
-                        margin-top: 15px;
-                    }
-
-                    .audio-marker:hover {
-                        border: orange 2px solid;
-                    }
-
-                    .resizer {
-                        position: absolute;
-                        top: 0;
-                        right: 0;
-                        pointer-events: all;
-                        cursor: ew-resize;
-                        height: 100%;
-                        width: 10px;
-                        z-index: 100;
-                    }
-
-                    .click {
-                        position: relative;
-                        height: 100%;
-                        width: 100%;
-                        z-index: 100;
-                    }
-
-                    .left-resizer {
-                        position: absolute;
-                        left: 0;
-                        top : 0;
-                        pointer-events: all;
-                        cursor: ew-resize;
-                        height: 100%;
-                        width: 10px;
-                        z-index: 100;
-                    }
-                }
-
-                .audiobox-marker-timeline:hover,
-                .audiobox-marker-minicontainer:hover {
-                    opacity: 0.8;
-                }
-
-                .audiobox-marker-minicontainer {
-                    width: 5px;
-                    border-radius: 1px;
-
-                    .audiobox-marker {
-                        position: relative;
-                        height: calc(100% - 8px);
-                        margin-top: 8px;
-                    }
-                }
-            }
-        }
-    }
-}
-
-
-@media only screen and (max-device-width: 480px) {
-    .audiobox-dictation {
-        font-size: 5em;
-        display: flex;
-        width: 100;
-        justify-content: center;
-        flex-direction: column;
-        align-items: center;
-    }
-
-    .audiobox-container .audiobox-record,
-    .audiobox-container-interactive .audiobox-record {
-        font-size: 3em;
-    }
-
-    .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
-    .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
-    .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead {
-        width: 70px;
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/StackedTimeline.tsx b/src/client/views/nodes/StackedTimeline.tsx
deleted file mode 100644
index 808ca982d..000000000
--- a/src/client/views/nodes/StackedTimeline.tsx
+++ /dev/null
@@ -1,335 +0,0 @@
-import React = require("react");
-import { action, computed, IReactionDisposer, observable } from "mobx";
-import { observer } from "mobx-react";
-import { computedFn } from "mobx-utils";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { Cast, NumCast } from "../../../fields/Types";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { Scripting } from "../../util/Scripting";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Transform } from "../../util/Transform";
-import "./StackedTimeline.scss";
-import { DocumentView, DocumentViewProps } from "./DocumentView";
-import { LabelBox } from "./LabelBox";
-
-export interface StackedTimelineProps {
-    Document: Doc;
-    dataDoc: Doc;
-    anchorProps: DocumentViewProps;
-    renderDepth: number;
-    annotationKey: string;
-    duration: number;
-    Play: () => void;
-    Pause: () => void;
-    playLink: (linkDoc: Doc) => void;
-    playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
-    playing: () => boolean;
-    setTime: (time: number) => void;
-    select: (ctrlKey: boolean) => void;
-    isSelected: (outsideReaction: boolean) => boolean;
-    whenActiveChanged: (isActive: boolean) => void;
-    removeDocument: (doc: Doc | Doc[]) => boolean;
-    ScreenToLocalTransform: () => Transform;
-    isChildActive: () => boolean;
-    active: () => boolean;
-    PanelWidth: () => number;
-    PanelHeight: () => number;
-}
-
-@observer
-export class StackedTimeline extends React.Component<StackedTimelineProps> {
-    static RangeScript: ScriptField;
-    static LabelScript: ScriptField;
-    static RangePlayScript: ScriptField;
-    static LabelPlayScript: ScriptField;
-
-    _disposers: { [name: string]: IReactionDisposer } = {};
-    _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack!  this must be called _doubleTime since setupMoveDragEvents will use that field name
-    _ele: HTMLAudioElement | null = null;
-    _start: number = 0;
-    _left: boolean = false;
-    _dragging = false;
-    _play: any = null;
-    _audioRef = React.createRef<HTMLDivElement>();
-    _timeline: Opt<HTMLDivElement>;
-    _markerStart: number = 0;
-    _currAnchor: Opt<Doc>;
-
-    @observable static SelectingRegion: StackedTimeline | undefined = undefined;
-    @observable _markerEnd: number = 0;
-    @observable _position: number = 0;
-    @computed get anchorDocs() { return DocListCast(this.props.dataDoc[this.props.annotationKey]); }
-    @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); }
-
-    constructor(props: Readonly<StackedTimelineProps>) {
-        super(props);
-        // onClick play scripts
-        StackedTimeline.RangeScript = StackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
-        StackedTimeline.LabelScript = StackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
-        StackedTimeline.RangePlayScript = StackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
-        StackedTimeline.LabelPlayScript = StackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
-    }
-
-    // for creating key anchors with key events
-    @action
-    keyEvents = (e: KeyboardEvent) => {
-        if (e.target instanceof HTMLInputElement) return;
-        if (!this.props.playing()) return; // can't create if video is not playing
-        switch (e.key) {
-            case "x": // currently set to x, but can be a different key
-                const currTime = this.currentTime;
-                if (this._start) {
-                    this._markerStart = currTime;
-                    // this._start = false;
-                    // this._visible = true;
-                } else {
-                    this.createAnchor(this._markerStart, currTime);
-                    // this._start = true;
-                    // this._visible = false;
-                }
-        }
-    }
-
-    anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)))
-    anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)))
-
-    getLinkData(l: Doc) {
-        let la1 = l.anchor1 as Doc;
-        let la2 = l.anchor2 as Doc;
-        const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime));
-        if (Doc.AreProtosEqual(la1, this.props.dataDoc)) {
-            la1 = l.anchor2 as Doc;
-            la2 = l.anchor1 as Doc;
-        }
-        return { la1, la2, linkTime };
-    }
-
-    // ref for timeline
-    timelineRef = (timeline: HTMLDivElement) => {
-        this._timeline = timeline;
-    }
-
-    // updates the anchor with the new time
-    @action
-    changeAnchor = (anchor: Opt<Doc>, time: number) => {
-        anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time);
-    }
-
-    // checks if the two anchors are the same with start and end time
-    isSame = (m1: any, m2: any) => {
-        return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2);
-    }
-
-    @computed get selectionContainer() {
-        return StackedTimeline.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
-            left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.props.duration * 100}%`,
-            width: `${Math.abs(this._markerStart - this._markerEnd) / this.props.duration * 100}%`, height: "100%", top: "0%"
-        }} />;
-    }
-
-    // starting the drag event for anchor resizing
-    @action
-    onPointerDownTimeline = (e: React.PointerEvent): void => {
-        const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect();
-        if (rect && e.target !== this._audioRef.current && this.props.active()) {
-            const wasPlaying = this.props.playing();
-            if (wasPlaying) this.props.Pause();
-            else if (!this._doubleTime) {
-                this._doubleTime = setTimeout(() => {
-                    this._doubleTime = undefined;
-                    this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration);
-                }, 300);
-            }
-            this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
-            StackedTimeline.SelectingRegion = this;
-            setupMoveUpEvents(this, e,
-                action(e => {
-                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
-                    return false;
-                }),
-                action((e, movement) => {
-                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
-                    if (this._markerEnd < this._markerStart) {
-                        const tmp = this._markerStart;
-                        this._markerStart = this._markerEnd;
-                        this._markerEnd = tmp;
-                    }
-                    StackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd);
-                    StackedTimeline.SelectingRegion = undefined;
-                }),
-                (e, doubleTap) => {
-                    this.props.select(false);
-                    e.shiftKey && this.createAnchor(this.currentTime);
-                    !wasPlaying && doubleTap && this.props.Play();
-                }
-                , this.props.isSelected(true) || this.props.isChildActive());
-        }
-    }
-
-    @action
-    createAnchor(anchorStartTime?: number, anchorEndTime?: number) {
-        if (anchorStartTime === undefined) return this.props.Document;
-        const anchor = Docs.Create.LabelDocument({
-            title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any,
-            useLinkSmallAnchor: true,
-            hideLinkButton: true,
-            anchorStartTime,
-            anchorEndTime,
-            annotationOn: this.props.Document
-        });
-        if (Cast(this.props.dataDoc[this.props.annotationKey], listSpec(Doc), null) !== undefined) {
-            Cast(this.props.dataDoc[this.props.annotationKey], listSpec(Doc), []).push(anchor);
-        } else {
-            this.props.dataDoc[this.props.annotationKey] = new List<Doc>([anchor]);
-        }
-        return anchor;
-    }
-
-    // play back the audio from time
-    @action
-    playOnClick = (anchorDoc: Doc) => {
-        this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration));
-        return { select: true };
-    }
-
-    // play back the audio from time
-    @action
-    clickAnchor = (anchorDoc: Doc) => {
-        if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc);
-        this.props.setTime(this.anchorStart(anchorDoc));
-        return { select: true };
-    }
-
-    toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.duration));
-    // starting the drag event for anchor resizing
-    onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => {
-        this._currAnchor = m;
-        this._left = left;
-        this._timeline?.setPointerCapture(e.pointerId);
-        setupMoveUpEvents(this, e,
-            (e) => {
-                const rect = (e.target as any).getBoundingClientRect();
-                this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width));
-                return false;
-            },
-            (e) => {
-                const rect = (e.target as any).getBoundingClientRect();
-                this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width));
-                this._timeline?.releasePointerCapture(e.pointerId);
-            },
-            emptyFunction);
-    }
-
-    rangeClickScript = () => StackedTimeline.RangeScript;
-    labelClickScript = () => StackedTimeline.LabelScript;
-    rangePlayScript = () => StackedTimeline.RangePlayScript;
-    labelPlayScript = () => StackedTimeline.LabelPlayScript;
-
-    // makes sure no anchors overlaps each other by setting the correct position and width
-    getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => {
-        const timelineContentWidth = this.props.PanelWidth();
-        const x1 = this.anchorStart(m);
-        const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration);
-        let max = 0;
-        const overlappedLevels = new Set(placed.map(p => {
-            const y1 = p.anchorStartTime;
-            const y2 = p.anchorEndTime;
-            if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) ||
-                (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) {
-                max = Math.max(max, p.level);
-                return p.level;
-            }
-        }));
-        let level = max + 1;
-        for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
-
-        placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
-        return level;
-    }
-
-    renderInner = computedFn(function (this: StackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
-        const anchor = observable({ view: undefined as any });
-        return {
-            anchor, view: <DocumentView key="view"  {...OmitKeys(this.props.anchorProps, ["NativeWidth", "NativeHeight"]).omit}
-                ref={action((r: DocumentView | null) => anchor.view = r)}
-                Document={mark}
-                DataDoc={undefined}
-                PanelWidth={() => width}
-                PanelHeight={() => height}
-                renderDepth={this.props.renderDepth + 1}
-                focus={() => this.props.playLink(mark)}
-                rootSelected={returnFalse}
-                LayoutTemplate={undefined}
-                LayoutTemplateString={LabelBox.LayoutString("data")}
-                ContainingCollectionDoc={this.props.Document}
-                removeDocument={this.props.removeDocument}
-                ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)}
-                parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()}
-                whenActiveChanged={this.props.whenActiveChanged}
-                onClick={script}
-                onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript}
-                ignoreAutoHeight={false}
-                bringToFront={emptyFunction}
-                scriptContext={this} />
-        };
-    });
-    renderAnchor = computedFn(function (this: StackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
-        const inner = this.renderInner(mark, script, doublescript, x, y, width, height);
-        return <>
-            {inner.view}
-            {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
-                <>
-                    <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} />
-                    <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} />
-                </>}
-        </>;
-    });
-
-    render() {
-        const timelineContentWidth = this.props.PanelWidth();
-        const timelineContentHeight = this.props.PanelHeight();
-        const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
-        const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
-        const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
-        return <div className="audiobox-timeline" style={{ height: "100%", width: "100%" }} ref={this.timelineRef}
-            onClick={e => {
-                if (this.props.isChildActive() || this.props.isSelected(false)) {
-                    e.stopPropagation(); e.preventDefault();
-                }
-            }}
-            onPointerDown={e => {
-                if (this.props.isChildActive() || this.props.isSelected(false)) {
-                    e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e);
-                }
-            }}>
-            {drawAnchors.map(d => {
-                const m = d.anchor;
-                const start = this.anchorStart(m);
-                const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.props.duration);
-                const left = start / this.props.duration * timelineContentWidth;
-                const top = d.level / maxLevel * timelineContentHeight;
-                const timespan = end - start;
-                return this.props.Document.hideAnchors ? (null) :
-                    <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]}
-                        style={{ left, top, width: `${timespan / this.props.duration * 100}%`, height: `${1 / maxLevel * 100}%` }}
-                        onClick={e => { this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} >
-                        {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript,
-                            left,
-                            top,
-                            timelineContentWidth * timespan / this.props.duration,
-                            timelineContentHeight / maxLevel)}
-                    </div>;
-            })}
-            {this.selectionContainer}
-            <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
-                style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }}
-            />
-        </div>
-    }
-}
-Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index f89f54309..bfac7dc1c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -16,6 +16,7 @@ import { Networking } from "../../Network";
 import { SelectionManager } from "../../util/SelectionManager";
 import { SnappingManager } from "../../util/SnappingManager";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
@@ -25,7 +26,6 @@ import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
 import { LinkDocPreview } from "./LinkDocPreview";
-import { StackedTimeline } from "./StackedTimeline";
 import "./VideoBox.scss";
 const path = require('path');
 
@@ -46,7 +46,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
     private _videoRef: HTMLVideoElement | null = null;
     private _youtubeIframeId: number = -1;
     private _youtubeContentCreated = false;
-    private _stackedTimeline = React.createRef<StackedTimeline>();
+    private _stackedTimeline = React.createRef<CollectionStackedTimeline>();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _playRegionTimer: any = null;
@@ -72,8 +72,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
         VideoBox.Instance = this;
     }
 
-    anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)))
-    anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)))
+    anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)));
+    anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)));
 
     getAnchor = () => {
         return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc;
@@ -484,12 +484,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
     // returns the timeline
     @computed get renderTimeline() {
         return <div style={{ width: "100%", height: `${100 - this.heightPercent}%`, position: "absolute" }}>
-            <StackedTimeline ref={this._stackedTimeline}
+            <CollectionStackedTimeline ref={this._stackedTimeline}
                 Document={this.props.Document}
-                dataDoc={this.dataDoc}
-                anchorProps={this.props}
+                fieldKey={this.annotationKey}
                 renderDepth={this.props.renderDepth + 1}
-                annotationKey={this.annotationKey + "-timeline"}
+                parentActive={this.props.parentActive}
+                focus={emptyFunction}
+                styleProvider={this.props.styleProvider}
+                docFilters={this.props.docFilters}
+                docRangeFilters={this.props.docRangeFilters}
+                searchFilterDocs={this.props.searchFilterDocs}
+                rootSelected={this.props.rootSelected}
+                addDocTab={this.props.addDocTab}
+                pinToPres={this.props.pinToPres}
+                bringToFront={emptyFunction}
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+                ContainingCollectionView={this.props.ContainingCollectionView}
+                CollectionView={undefined}
                 duration={this.duration}
                 playFrom={this.playFrom}
                 setTime={(time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time}
-- 
cgit v1.2.3-70-g09d2

