From e4791f989024410443059bee424606c0567bc5f7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 22 Mar 2021 00:30:21 -0400
Subject: fixed audio/video synchronizatoin for screengrab videos.  fixed
 following isLinkButton's in stackedTimelines.   Made links from screenGrab
 videos to document whn presented in lightbox.

---
 .../views/collections/CollectionStackedTimeline.tsx    | 18 ++++++++----------
 1 file changed, 8 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 16a1c02f7..cdb2468f2 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -18,6 +18,7 @@ import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView";
 import { LabelBox } from "../nodes/LabelBox";
 import "./CollectionStackedTimeline.scss";
 import { Transform } from "../../util/Transform";
+import { LinkManager } from "../../util/LinkManager";
 
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
@@ -59,9 +60,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
         super(props);
         // onClick play scripts
         CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
-        CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
         CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
-        CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
     }
 
     componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); }
@@ -77,9 +76,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
     }
     toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration));
     rangeClickScript = () => CollectionStackedTimeline.RangeScript;
-    labelClickScript = () => CollectionStackedTimeline.LabelScript;
     rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
-    labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript;
 
     // for creating key anchors with key events
     @action
@@ -175,12 +172,12 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
     playOnClick = (anchorDoc: Doc, clientX: number) => {
         const seekTimeInSeconds = this.anchorStart(anchorDoc);
         const endTime = this.anchorEnd(anchorDoc);
-        if (this.layoutDoc.autoPlay) {
+        if (this.layoutDoc.autoPlayAnchors) {
             if (this.props.playing()) this.props.Pause();
             else this.props.playFrom(seekTimeInSeconds, endTime);
         } else {
             if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) {
-                if (!this.layoutDoc.autoPlay && this.props.playing()) {
+                if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
                     this.props.Pause();
                 } else {
                     this.props.Play();
@@ -194,17 +191,18 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
 
     @action
     clickAnchor = (anchorDoc: Doc, clientX: number) => {
+        if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
         const seekTimeInSeconds = this.anchorStart(anchorDoc);
         const endTime = this.anchorEnd(anchorDoc);
         if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
             if (this.props.playing()) this.props.Pause();
-            else if (this.layoutDoc.autoPlay) this.props.Play();
-            else if (!this.layoutDoc.autoPlay) {
+            else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
+            else if (!this.layoutDoc.autoPlayAnchors) {
                 const rect = this._timeline?.getBoundingClientRect();
                 rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
             }
         } else {
-            if (this.layoutDoc.autoPlay) this.props.playFrom(seekTimeInSeconds, endTime);
+            if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime);
             else this.props.setTime(seekTimeInSeconds);
         }
         return { select: true };
@@ -276,7 +274,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                 parentActive={out => this.props.isSelected(out) || this.props.isChildActive()}
                 rootSelected={returnFalse}
                 onClick={script}
-                onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript}
+                onDoubleClick={this.layoutDoc.autoPlayAnchors ? undefined : doublescript}
                 ignoreAutoHeight={false}
                 hideResizeHandles={true}
                 bringToFront={emptyFunction}
-- 
cgit v1.2.3-70-g09d2


From 3c8325a99c4cec82148039d381df8377dcd4ee3a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 22 Mar 2021 01:36:24 -0400
Subject: made stackedTimeline capable of auto showing the annotations that
 were made on it when played back.

---
 src/client/util/LinkManager.ts                     |   4 +-
 .../collections/CollectionStackedTimeline.tsx      | 181 ++++++++++++++-------
 2 files changed, 120 insertions(+), 65 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index dde68401e..bf973c3d6 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -100,7 +100,7 @@ export class LinkManager {
     // follows a link - if the target is on screen, it highlights/pans to it.
     // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place
     // depending on the followLinkLocation property of the source (or the link itself as a fallback);
-    public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => {
+    public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
         const batch = UndoManager.StartBatch("follow link click");
         // open up target if it's not already in view ...
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
@@ -131,7 +131,7 @@ export class LinkManager {
                 docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
             }
         };
-        LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
+        LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
     }
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index cdb2468f2..0c960b935 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,7 +1,6 @@
 import React = require("react");
-import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, runInAction, reaction } from "mobx";
 import { observer } from "mobx-react";
-import { computedFn } from "mobx-utils";
 import { Doc, Opt, DocListCast } from "../../../fields/Doc";
 import { Id } from "../../../fields/FieldSymbols";
 import { List } from "../../../fields/List";
@@ -14,11 +13,12 @@ import { Scripting } from "../../util/Scripting";
 import { SelectionManager } from "../../util/SelectionManager";
 import { undoBatch } from "../../util/UndoManager";
 import { CollectionSubView } from "../collections/CollectionSubView";
-import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView";
+import { DocumentView, DocAfterFocusFunc, DocFocusFunc, DocumentViewProps } from "../nodes/DocumentView";
 import { LabelBox } from "../nodes/LabelBox";
 import "./CollectionStackedTimeline.scss";
 import { Transform } from "../../util/Transform";
 import { LinkManager } from "../../util/LinkManager";
+import { computedFn } from "mobx-utils";
 
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
@@ -209,28 +209,6 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
     }
 
 
-    // starting the drag event for anchor resizing
-    onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
-        this._timeline?.setPointerCapture(e.pointerId);
-        const newTime = (e: PointerEvent) => {
-            const rect = (e.target as any).getBoundingClientRect();
-            return this.toTimeline(e.clientX - rect.x, rect.width);
-        };
-        const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
-            const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
-            if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
-            else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
-            return false;
-        };
-        setupMoveUpEvents(this, e,
-            (e) => changeAnchor(anchor, left, newTime(e)),
-            (e) => {
-                this.props.setTime(newTime(e));
-                this._timeline?.releasePointerCapture(e.pointerId);
-            },
-            emptyFunction);
-    }
-
     // 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();
@@ -252,8 +230,116 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
         placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
         return level;
     }
+    currentTimecode = () => this.currentTime;
+    render() {
+        const timelineContentWidth = this.props.PanelWidth();
+        const timelineContentHeight = this.props.PanelHeight();
+        const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
+        const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
+        const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
+        const isActive = this.props.isChildActive() || this.props.isSelected(false);
+        return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline}
+            onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}>
+            {drawAnchors.map(d => {
+                const start = this.anchorStart(d.anchor);
+                const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
+                const left = start / this.duration * timelineContentWidth;
+                const top = d.level / maxLevel * timelineContentHeight;
+                const timespan = end - start;
+                return this.props.Document.hideAnchors ? (null) :
+                    <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
+                        style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${timelineContentHeight / maxLevel}px` }}
+                        onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
+                        <StackedTimelineAnchor {...this.props}
+                            mark={d.anchor}
+                            rangeClickScript={this.rangeClickScript}
+                            rangePlayScript={this.rangePlayScript}
+                            left={left}
+                            top={top}
+                            width={timelineContentWidth * timespan / this.duration}
+                            height={timelineContentHeight / maxLevel}
+                            toTimeline={this.toTimeline}
+                            layoutDoc={this.layoutDoc}
+                            currentTimecode={this.currentTimecode}
+                            _timeline={this._timeline}
+                            stackedTimeline={this}
+                        />
+                    </div>;
+            })}
+            {this.selectionContainer}
+            <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
+        </div>;
+    }
+}
 
-    renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+interface StackedTinelineAnchorProps {
+    mark: Doc;
+    rangeClickScript: () => ScriptField;
+    rangePlayScript: () => ScriptField;
+    left: number;
+    top: number;
+    width: number;
+    height: number;
+    toTimeline: (screen_delta: number, width: number) => number;
+    playLink: (linkDoc: Doc) => void;
+    setTime: (time: number) => void;
+    isChildActive: () => boolean;
+    startTag: string;
+    endTag: string;
+    renderDepth: number;
+    layoutDoc: Doc;
+    ScreenToLocalTransform: () => Transform;
+    _timeline: HTMLDivElement | null;
+    focus: DocFocusFunc;
+    currentTimecode: () => number;
+    isSelected: (outsideReaction?: boolean) => boolean;
+    stackedTimeline: CollectionStackedTimeline;
+}
+@observer
+class StackedTimelineAnchor extends React.Component<StackedTinelineAnchorProps> {
+    _lastTimecode: number;
+    _disposer: IReactionDisposer | undefined;
+    constructor(props: any) {
+        super(props);
+        this._lastTimecode = this.props.currentTimecode();
+    }
+    componentDidMount() {
+        this._disposer = reaction(() => this.props.currentTimecode(),
+            (time) => {
+                if (DocListCast(this.props.mark.links).length &&
+                    time > NumCast(this.props.mark[this.props.startTag]) &&
+                    time < NumCast(this.props.mark[this.props.endTag]) &&
+                    this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) {
+                    LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true);
+                }
+                this._lastTimecode = time;
+            });
+    }
+    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);
+        const newTime = (e: PointerEvent) => {
+            const rect = (e.target as any).getBoundingClientRect();
+            return this.props.toTimeline(e.clientX - rect.x, rect.width);
+        };
+        const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
+            const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
+            if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
+            else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
+            return false;
+        };
+        setupMoveUpEvents(this, e,
+            (e) => changeAnchor(anchor, left, newTime(e)),
+            (e) => {
+                this.props.setTime(newTime(e));
+                this.props._timeline?.releasePointerCapture(e.pointerId);
+            },
+            emptyFunction);
+    }
+    renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
         const anchor = observable({ view: undefined as any });
         const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => {
             this.props.playLink(mark);
@@ -274,54 +360,23 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                 parentActive={out => this.props.isSelected(out) || this.props.isChildActive()}
                 rootSelected={returnFalse}
                 onClick={script}
-                onDoubleClick={this.layoutDoc.autoPlayAnchors ? undefined : doublescript}
+                onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
                 ignoreAutoHeight={false}
                 hideResizeHandles={true}
                 bringToFront={emptyFunction}
-                scriptContext={this} />
+                scriptContext={this.props.stackedTimeline} />
         };
     });
-    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);
+    render() {
+        const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height);
         return <>
             {inner.view}
             {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
                 <>
-                    <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, mark, true)} />
-                    <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, 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)} />
                 </>}
         </>;
-    });
-
-    render() {
-        const timelineContentWidth = this.props.PanelWidth();
-        const timelineContentHeight = this.props.PanelHeight();
-        const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
-        const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
-        const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
-        const isActive = this.props.isChildActive() || this.props.isSelected(false);
-        return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline}
-            onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}>
-            {drawAnchors.map(d => {
-                const start = this.anchorStart(d.anchor);
-                const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
-                const left = start / this.duration * timelineContentWidth;
-                const top = d.level / maxLevel * timelineContentHeight;
-                const timespan = end - start;
-                return this.props.Document.hideAnchors ? (null) :
-                    <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
-                        style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${timelineContentHeight / maxLevel}px` }}
-                        onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
-                        {this.renderAnchor(d.anchor, this.rangeClickScript, this.rangePlayScript,
-                            left,
-                            top,
-                            timelineContentWidth * timespan / this.duration,
-                            timelineContentHeight / maxLevel)}
-                    </div>;
-            })}
-            {this.selectionContainer}
-            <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
-        </div>;
     }
 }
 Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From d8445ef3b2402530b5139b94eeb0f333c1fed661 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 22 Mar 2021 14:55:45 -0400
Subject: adjusted playLink in videos to start 1/4 sec early to trigger
 following link to anchor. changed text sidebars to show titles.  fixed title
 autoHeight computations in formattedTextBox. fixed styleprovider to compute
 headerMargin correctly by inquiring style for ShowTitle.

---
 src/client/views/SidebarAnnos.tsx                  |  8 +++++++-
 src/client/views/StyleProvider.tsx                 |  4 +++-
 .../collections/CollectionStackedTimeline.tsx      |  4 ++--
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 22 ++++++++++++----------
 5 files changed, 25 insertions(+), 15 deletions(-)

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

diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 2d49938a9..9d085e2b6 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -1,6 +1,6 @@
 import { computed } from 'mobx';
 import { observer } from "mobx-react";
-import { Doc, DocListCast, StrListCast } from "../../fields/Doc";
+import { Doc, DocListCast, StrListCast, Opt } from "../../fields/Doc";
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { NumCast, StrCast } from '../../fields/Types';
@@ -15,6 +15,7 @@ import { SearchBox } from './search/SearchBox';
 import "./SidebarAnnos.scss";
 import { StyleProp } from './StyleProvider';
 import React = require("react");
+import { DocumentViewProps } from './nodes/DocumentView';
 
 interface ExtraProps {
     fieldKey: string;
@@ -71,6 +72,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey());
     docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
 
+    sidebarStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => {
+        if (property === StyleProp.ShowTitle) return "title";
+        return this.props.styleProvider?.(doc, props, property)
+    }
     render() {
         const renderTag = (tag: string) => {
             const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
@@ -94,6 +99,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                         PanelWidth={this.panelWidth}
                         xMargin={0}
                         yMargin={0}
+                        styleProvider={this.sidebarStyleProvider}
                         docFilters={this.docFilters}
                         scaleField={this.sidebarKey() + "-scale"}
                         isAnnotationOverlay={false}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 1ee99817c..617652ab7 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -69,6 +69,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps |
     const isBackground = () => StrListCast(doc?._layerTags).includes(StyleLayers.Background);
     const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
     const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
+    const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
 
     switch (property.split(":")[0]) {
         case StyleProp.DocContents: return undefined;
@@ -88,7 +89,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps |
         case StyleProp.Hidden: return BoolCast(doc?._hidden);
         case StyleProp.BorderRounding: return StrCast(doc?._borderRounding);
         case StyleProp.TitleHeight: return 15;
-        case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || doc?.type === DocumentType.RTF) && doc?._showTitle && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0;
+        case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) ||
+            doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0;
         case StyleProp.BackgroundColor: {
             if (isAnchor && docProps) return "transparent";
             if (isCaption) return "rgba(0,0,0 ,0.4)";
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 0c960b935..de75a3c4a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -170,7 +170,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
 
     @action
     playOnClick = (anchorDoc: Doc, clientX: number) => {
-        const seekTimeInSeconds = this.anchorStart(anchorDoc);
+        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
         const endTime = this.anchorEnd(anchorDoc);
         if (this.layoutDoc.autoPlayAnchors) {
             if (this.props.playing()) this.props.Pause();
@@ -192,7 +192,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
     @action
     clickAnchor = (anchorDoc: Doc, clientX: number) => {
         if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
-        const seekTimeInSeconds = this.anchorStart(anchorDoc);
+        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
         const endTime = this.anchorEnd(anchorDoc);
         if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
             if (this.props.playing()) this.props.Pause();
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 42258168c..5fd897629 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -493,7 +493,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
     }
 
     playLink = (doc: Doc) => {
-        const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0;
+        const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0) - .25);
         const endTime = this._stackedTimeline.current?.anchorEnd(doc);
         if (startTime !== undefined) {
             if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ada409081..0c0b09b9a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -759,8 +759,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         this.props.contentsActive?.(this.active);
         this._cachedLinks = DocListCast(this.Document.links);
         this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight());
-        this._disposers.autoHeight = reaction(() => ({ scrollHeight: this.scrollHeight, width: NumCast(this.layoutDoc._width) }),
-            ({ width, scrollHeight }) => width && this.autoHeight && this.resetNativeHeight(scrollHeight)
+        this._disposers.scrollHeight = reaction(() => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }),
+            ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight)
         );
         this._disposers.componentHeights = reaction(  // set the document height when one of the component heights changes and autoHeight is on
             () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight }),
@@ -1393,14 +1393,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     }
     tryUpdateScrollHeight() {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
-            const proseHeight = this.ProseRef?.scrollHeight || 0;
-            const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
-            if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
-                const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
-                if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
-                    setScrollHeight();
-                } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template.  So set a timeout and hope its arrived...
-            }
+            setTimeout(() => { // bcz: don't know why this is needed, but without it, the size of the textbox is too big as it includes the size of the title header.  after the timeout, the size seems to get computed correctly.
+                const proseHeight = this.ProseRef?.scrollHeight || 0;
+                const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+                if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+                    const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
+                    if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+                        setScrollHeight();
+                    } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template.  So set a timeout and hope its arrived...
+                }
+            });
         }
     }
     fitToBox = () => this.props.Document._fitToBox;
-- 
cgit v1.2.3-70-g09d2


From d0515c81be9f4292eaf165762ce15e7bc8d1737a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 25 Mar 2021 02:39:52 -0400
Subject: moved dictation view to be a component of the screenshotbox

---
 src/client/util/CurrentUserUtils.ts                |  4 +-
 src/client/util/LinkManager.ts                     |  6 +-
 src/client/views/LightboxView.tsx                  |  8 +--
 .../collections/CollectionStackedTimeline.tsx      |  3 +-
 src/client/views/nodes/ScreenshotBox.tsx           | 77 ++++++++++++++++------
 src/client/views/nodes/VideoBox.tsx                |  4 +-
 src/fields/util.ts                                 |  2 +-
 7 files changed, 70 insertions(+), 34 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 05e560f51..0fb32970a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -423,7 +423,7 @@ export class CurrentUserUtils {
             ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0;
         }
         if (doc.emptyScreenshot === undefined) {
-            doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) });
+            doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) });
         }
         if (doc.emptyAudio === undefined) {
             doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
@@ -453,7 +453,7 @@ export class CurrentUserUtils {
             { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc, noviceMode: true },
             { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyImage as Doc },
             { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyComparison as Doc, noviceMode: true },
-            { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc },
+            { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc, noviceMode: true },
             { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyAudio as Doc, noviceMode: true },
             { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyButton as Doc },
             { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true },
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index bd27c9e56..159011516 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -135,14 +135,14 @@ export class LinkManager {
 
 
     // follows a link - if the target is on screen, it highlights/pans to it.
-    // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place
+    // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
     // depending on the followLinkLocation property of the source (or the link itself as a fallback);
     public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
         const batch = UndoManager.StartBatch("follow link click");
         // open up target if it's not already in view ...
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
             const createTabForTarget = (didFocus: boolean) => new Promise<ViewAdjustment>(res => {
-                const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation) || followLoc;
+                const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc);
                 docViewProps.addDocTab(doc, where);
                 setTimeout(() => {
                     const targDocView = DocumentManager.Instance.getFirstDocumentView(doc);
@@ -195,7 +195,7 @@ export class LinkManager {
                         const containerDoc = Cast(target.annotationOn, Doc, null) || target;
                         const targetContext = Cast(containerDoc?.context, Doc, null);
                         const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
-                        DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "add:right"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished);
+                        DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished);
                     }
                 } else {
                     finished?.();
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 5715b62b0..84738112f 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -114,11 +114,11 @@ export class LightboxView extends React.Component<LightboxViewProps> {
     @action public static Next() {
         const doc = LightboxView._doc!;
         const target = LightboxView._docTarget = LightboxView._future?.pop();
-        const docView = target && DocumentManager.Instance.getLightboxDocumentView(target);
-        if (docView && target) {
-            const l = DocUtils.MakeLinkToActiveAudio(target);
+        const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
+        if (targetDocView && target) {
+            const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target);
             l && (Cast(l.anchor2, Doc, null).backgroundColor = "lightgreen");
-            docView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
+            targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
         } else {
             if (!target && LightboxView.path.length) {
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index de75a3c4a..db02ab986 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -19,6 +19,7 @@ import "./CollectionStackedTimeline.scss";
 import { Transform } from "../../util/Transform";
 import { LinkManager } from "../../util/LinkManager";
 import { computedFn } from "mobx-utils";
+import { LightboxView } from "../LightboxView";
 
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
@@ -306,7 +307,7 @@ class StackedTimelineAnchor extends React.Component<StackedTinelineAnchorProps>
     componentDidMount() {
         this._disposer = reaction(() => this.props.currentTimecode(),
             (time) => {
-                if (DocListCast(this.props.mark.links).length &&
+                if (!LightboxView.LightboxDoc && DocListCast(this.props.mark.links).length &&
                     time > NumCast(this.props.mark[this.props.startTag]) &&
                     time < NumCast(this.props.mark[this.props.endTag]) &&
                     this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index dba730d12..d1da2fcd5 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -24,6 +24,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import "./ScreenshotBox.scss";
 import { VideoBox } from "./VideoBox";
 import { TraceMobx } from "../../../fields/util";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
 declare class MediaRecorder {
     constructor(e: any, options?: any);  // whatever MediaRecorder has
 }
@@ -40,6 +41,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
     @observable _screenCapture = false;
     @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
 
+    constructor(props: any) {
+        super(props);
+        this.setupDictation();
+    }
     getAnchor = () => {
         const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
         return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide",
@@ -59,6 +64,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
     }
 
     componentDidMount() {
+        this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
         this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
     }
     componentWillUnmount() {
@@ -108,10 +114,12 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
                 if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
                     this.dataDoc.type = DocumentType.VID;
                     this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+                    this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+                    this.layoutDoc._fitWidth = undefined;
                     this.dataDoc[this.props.fieldKey] = new VideoField(Utils.prepend(result.accessPaths.agnostic.client));
+                    this.props.addDocTab?.(Cast(this.dataDoc[this.fieldKey + "-dictation"], Doc, null), "add:bottom");
                 } else alert("video conversion failed");
             };
-            this.setupDictation();
             this._audioRec.start();
             this._videoRec.start();
             this.dataDoc.mediaState = "recording";
@@ -126,6 +134,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
     });
 
     setupDictation = () => {
+        if (this.dataDoc[this.fieldKey + "-dictation"]) return;
         const dictationText = CurrentUserUtils.GetNewTextDoc("",
             NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
             NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
@@ -133,32 +142,58 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
         dictationTextProto.recordingSource = this.dataDoc;
         dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
         dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
-        this.props.addDocument?.(dictationText) || this.props.addDocTab(dictationText, "add:bottom");
+        this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
     }
     contentFunc = () => [this.content];
+    videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth();
+    formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight())
     render() {
         TraceMobx();
         return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
             <div className="videoBox-viewer" >
-                <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
-                    PanelHeight={this.props.PanelHeight}
-                    PanelWidth={this.props.PanelWidth}
-                    focus={this.props.focus}
-                    isSelected={this.props.isSelected}
-                    isAnnotationOverlay={true}
-                    select={emptyFunction}
-                    active={returnFalse}
-                    scaling={returnOne}
-                    whenActiveChanged={emptyFunction}
-                    removeDocument={returnFalse}
-                    moveDocument={returnFalse}
-                    addDocument={returnFalse}
-                    CollectionView={undefined}
-                    ScreenToLocalTransform={this.props.ScreenToLocalTransform}
-                    renderDepth={this.props.renderDepth + 1}
-                    ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
-                    {this.contentFunc}
-                </CollectionFreeFormView>
+                <div style={{ position: "relative", height: this.videoPanelHeight() }}>
+                    <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+                        PanelHeight={this.videoPanelHeight}
+                        PanelWidth={this.props.PanelWidth}
+                        focus={this.props.focus}
+                        isSelected={this.props.isSelected}
+                        isAnnotationOverlay={true}
+                        select={emptyFunction}
+                        active={returnFalse}
+                        scaling={returnOne}
+                        whenActiveChanged={emptyFunction}
+                        removeDocument={returnFalse}
+                        moveDocument={returnFalse}
+                        addDocument={returnFalse}
+                        CollectionView={undefined}
+                        ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                        renderDepth={this.props.renderDepth + 1}
+                        ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+                        {this.contentFunc}
+                    </CollectionFreeFormView></div>
+                <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
+                    <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+                        Document={this.dataDoc[this.fieldKey + "-dictation"]}
+                        fieldKey={"text"}
+                        PanelHeight={this.formattedPanelHeight}
+                        PanelWidth={this.props.PanelWidth}
+                        focus={this.props.focus}
+                        isSelected={this.props.isSelected}
+                        isAnnotationOverlay={true}
+                        select={emptyFunction}
+                        active={returnFalse}
+                        scaling={returnOne}
+                        xMargin={25}
+                        yMargin={10}
+                        whenActiveChanged={emptyFunction}
+                        removeDocument={returnFalse}
+                        moveDocument={returnFalse}
+                        addDocument={returnFalse}
+                        CollectionView={undefined}
+                        ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                        renderDepth={this.props.renderDepth + 1}
+                        ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+                    </FormattedTextBox></div>
             </div>
             {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
                 <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 5a60f9312..4e03589d6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -182,7 +182,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     private createRealSummaryLink = (relative: string) => {
         const url = this.choosePath(Utils.prepend(relative));
-        const width = this.layoutDoc._width || 0;
+        const width = this.layoutDoc._width || 1;
         const height = this.layoutDoc._height || 0;
         const imageSummary = Docs.Create.ImageDocument(url, {
             _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
@@ -548,7 +548,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
     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) : this.props.PanelHeight() * 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);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 6c9c9d45c..ea91cc057 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -19,7 +19,7 @@ function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
 }
 
-const tracing = true;
+const tracing = false;
 export function TraceMobx() {
     tracing && trace();
 }
-- 
cgit v1.2.3-70-g09d2


From 5db728e099e25a807f69b316d813f662daf3eb7b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 25 Mar 2021 13:29:58 -0400
Subject: fixed videos in lightbox to not auto-follow links.  fixed audiotags
 to higlight more clearly when target of a link follow.

---
 src/client/views/collections/CollectionStackedTimeline.tsx | 6 +++---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx  | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index db02ab986..2dcdf58c2 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -273,7 +273,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
     }
 }
 
-interface StackedTinelineAnchorProps {
+interface StackedTimelineAnchorProps {
     mark: Doc;
     rangeClickScript: () => ScriptField;
     rangePlayScript: () => ScriptField;
@@ -297,7 +297,7 @@ interface StackedTinelineAnchorProps {
     stackedTimeline: CollectionStackedTimeline;
 }
 @observer
-class StackedTimelineAnchor extends React.Component<StackedTinelineAnchorProps> {
+class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
     _lastTimecode: number;
     _disposer: IReactionDisposer | undefined;
     constructor(props: any) {
@@ -307,7 +307,7 @@ class StackedTimelineAnchor extends React.Component<StackedTinelineAnchorProps>
     componentDidMount() {
         this._disposer = reaction(() => this.props.currentTimecode(),
             (time) => {
-                if (!LightboxView.LightboxDoc && DocListCast(this.props.mark.links).length &&
+                if (!Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc) && DocListCast(this.props.mark.links).length &&
                     time > NumCast(this.props.mark[this.props.startTag]) &&
                     time < NumCast(this.props.mark[this.props.endTag]) &&
                     this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2a1cc854c..369f5ab39 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -762,9 +762,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 }
                 editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
                 const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId;
-                addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" });
+                addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow", transform: "scale(3)", "transform-origin": "left bottom" });
                 setTimeout(() => this._focusSpeed = undefined, this._focusSpeed);
-                setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 1500));
+                setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000));
             }
         }
 
-- 
cgit v1.2.3-70-g09d2


From c7619302a639c61096249362167e75dca6dbb376 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 25 Mar 2021 15:41:06 -0400
Subject: added a dictation area to the stackedTimeline view for A/V.  Now
 dictation annotations scroll in place.

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/views/LightboxView.tsx                  |  2 +-
 .../collections/CollectionStackedTimeline.tsx      | 29 +++++++++++++++++++---
 src/client/views/nodes/ScreenshotBox.tsx           |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  3 +++
 .../views/nodes/formattedText/FormattedTextBox.tsx |  3 ++-
 6 files changed, 34 insertions(+), 7 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4e05793d4..9406b374e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1051,7 +1051,7 @@ export namespace DocUtils {
     export function MakeLinkToActiveAudio(doc: Doc, broadcastEvent = true) {
         broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1);
         return DocUtils.ActiveRecordings.map(audio =>
-            DocUtils.MakeLink({ doc: doc }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline")).lastElement();
+            DocUtils.MakeLink({ doc: doc }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline"));
     }
 
     export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 84738112f..07ebe5fa4 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -116,7 +116,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         const target = LightboxView._docTarget = LightboxView._future?.pop();
         const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
         if (targetDocView && target) {
-            const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target);
+            const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = "lightgreen");
             targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 2dcdf58c2..66b74277b 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -7,7 +7,7 @@ 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, StopEvent } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent, returnOne } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
 import { Scripting } from "../../util/Scripting";
 import { SelectionManager } from "../../util/SelectionManager";
@@ -20,6 +20,7 @@ import { Transform } from "../../util/Transform";
 import { LinkManager } from "../../util/LinkManager";
 import { computedFn } from "mobx-utils";
 import { LightboxView } from "../LightboxView";
+import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox";
 
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
@@ -234,7 +235,8 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
     currentTimecode = () => this.currentTime;
     render() {
         const timelineContentWidth = this.props.PanelWidth();
-        const timelineContentHeight = this.props.PanelHeight();
+        const timelineContentHeight = this.props.PanelHeight() * 2 / 3;
+        const dictationHeight = this.props.PanelHeight() / 3;
         const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
         const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
         const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
@@ -268,6 +270,25 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                     </div>;
             })}
             {this.selectionContainer}
+            <div style={{ position: "absolute", height: dictationHeight, top: timelineContentHeight, background: "tan" }}>
+                <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                    Document={this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")]}
+                    PanelHeight={() => dictationHeight}
+                    isAnnotationOverlay={true}
+                    select={emptyFunction}
+                    active={returnFalse}
+                    scaling={returnOne}
+                    xMargin={25}
+                    yMargin={10}
+                    whenActiveChanged={emptyFunction}
+                    removeDocument={returnFalse}
+                    moveDocument={returnFalse}
+                    addDocument={returnFalse}
+                    CollectionView={undefined}
+                    renderDepth={this.props.renderDepth + 1}>
+                </DocumentView>
+            </div>
+
             <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
         </div>;
     }
@@ -307,7 +328,9 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
     componentDidMount() {
         this._disposer = reaction(() => this.props.currentTimecode(),
             (time) => {
-                if (!Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc) && DocListCast(this.props.mark.links).length &&
+                const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null);
+                const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
+                if ((isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc)) && DocListCast(this.props.mark.links).length &&
                     time > NumCast(this.props.mark[this.props.startTag]) &&
                     time < NumCast(this.props.mark[this.props.endTag]) &&
                     this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index d1da2fcd5..6c019d6f2 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -117,7 +117,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
                     this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
                     this.layoutDoc._fitWidth = undefined;
                     this.dataDoc[this.props.fieldKey] = new VideoField(Utils.prepend(result.accessPaths.agnostic.client));
-                    this.props.addDocTab?.(Cast(this.dataDoc[this.fieldKey + "-dictation"], Doc, null), "add:bottom");
                 } else alert("video conversion failed");
             };
             this._audioRec.start();
@@ -138,6 +137,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
         const dictationText = CurrentUserUtils.GetNewTextDoc("",
             NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
             NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
+        dictationText._autoHeight = false;
         const dictationTextProto = Doc.GetProto(dictationText);
         dictationTextProto.recordingSource = this.dataDoc;
         dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4e03589d6..d5981c5ba 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -389,6 +389,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
         <div className="videoBox-timelineButton" key="timeline" onPointerDown={this.onTimelineHdlDown} style={{ bottom: `${100 - this.heightPercent}%` }}>
             <FontAwesomeIcon icon={"eye"} size="lg" />
         </div>,
+        <div className="videoBox-timelineButton" key="timeline" onPointerDown={this.onTimelineHdlDown} style={{ bottom: `${100 - this.heightPercent}%` }}>
+            <FontAwesomeIcon icon={"eye"} size="lg" />
+        </div>,
         VideoBox._showControls ? (null) : [
             // <div className="control-background">
             <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} >
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 369f5ab39..aaf3a938e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -667,7 +667,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             if (this._break) {
                 const textanchor = Docs.Create.TextanchorDocument({ title: "dictation anchor" });
                 this.addDocument(textanchor);
-                const link = DocUtils.MakeLinkToActiveAudio(textanchor, false);
+                const link = DocUtils.MakeLinkToActiveAudio(textanchor, false).lastElement();
+                link && (Doc.GetProto(link).isDictation = true);
                 if (!link) return;
                 const audioanchor = Cast(link.anchor2, Doc, null);
                 if (!audioanchor) return;
-- 
cgit v1.2.3-70-g09d2


From 36028677c61be7ab5937e864e881fa91a3b82d8c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 26 Mar 2021 13:22:13 -0400
Subject: extracted AudioWaveform out of AudioBox in order to use in
 CollectionStackedTimelineView to share between audioBox and videoBox.

---
 src/client/util/DictationManager.ts                |  2 +-
 src/client/util/DragManager.ts                     |  2 +-
 src/client/util/LinkManager.ts                     | 20 +++---
 src/client/views/AudioWaveform.scss                | 17 +++++
 src/client/views/AudioWaveform.tsx                 | 61 +++++++++++++++++
 src/client/views/DocumentDecorations.tsx           |  2 +-
 src/client/views/SidebarAnnos.tsx                  |  2 +-
 .../collections/CollectionStackedTimeline.scss     |  7 ++
 .../collections/CollectionStackedTimeline.tsx      | 80 +++++++++++++---------
 src/client/views/nodes/AudioBox.scss               | 18 -----
 src/client/views/nodes/AudioBox.tsx                | 41 +----------
 src/client/views/nodes/DocumentView.tsx            |  8 +--
 src/client/views/nodes/ScreenshotBox.tsx           |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  9 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/server/DashUploadUtils.ts                      |  5 +-
 16 files changed, 165 insertions(+), 113 deletions(-)
 create mode 100644 src/client/views/AudioWaveform.scss
 create mode 100644 src/client/views/AudioWaveform.tsx

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

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index f00cdce1e..a93b2f573 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -89,7 +89,7 @@ export namespace DictationManager {
         export const listen = async (options?: Partial<ListeningOptions>) => {
             if (pendingListen instanceof Promise) return pendingListen.then(pl => innerListen(options));
             return innerListen(options);
-        }
+        };
         const innerListen = async (options?: Partial<ListeningOptions>) => {
             let results: string | undefined;
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 1809a77bf..38d0ecaa6 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -388,7 +388,7 @@ export namespace DragManager {
             if (dragElement !== ele) {
                 const children = [Array.from(ele.children), Array.from(dragElement.children)];
                 while (children[0].length) {
-                    const childs = [children[0].pop(), children[1].pop()]
+                    const childs = [children[0].pop(), children[1].pop()];
                     if (childs[0]?.children) {
                         children[0].push(...Array.from(childs[0].children));
                         children[1].push(...Array.from(childs[1]!.children));
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 159011516..3c3d5c3b8 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -33,7 +33,7 @@ export class LinkManager {
     constructor() {
         LinkManager._instance = this;
         setTimeout(() => {
-            LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase as Doc)];
+            LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)];
             const addLinkToDoc = (link: Doc): any => {
                 const a1 = link?.anchor1;
                 const a2 = link?.anchor2;
@@ -43,7 +43,7 @@ export class LinkManager {
                     Doc.GetProto(a2)[DirectLinksSym].add(link);
                     Doc.GetProto(link)[DirectLinksSym].add(link);
                 }
-            }
+            };
             const remLinkFromDoc = (link: Doc): any => {
                 const a1 = link?.anchor1;
                 const a2 = link?.anchor2;
@@ -53,23 +53,23 @@ export class LinkManager {
                     Doc.GetProto(a2)[DirectLinksSym].delete(link);
                     Doc.GetProto(link)[DirectLinksSym].delete(link);
                 }
-            }
+            };
             const watchUserLinks = (userLinks: List<Doc>) => {
                 const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field;  // see List.ts.  data structure is not a simple list of Docs, but a list of ProxyField/Fields
                 observe(userLinks, change => {
-                    switch (change.type) {
+                    switch (change.type as any) {
                         case "splice":
                             (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
                             (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
                             break;
-                        case "update": let oldValue = change.oldValue;
+                        case "update": //let oldValue = change.oldValue;
                     }
                 }, true);
-            }
+            };
             observe(LinkManager.userDocs, change => {
-                switch (change.type) {
+                switch (change.type as any) {
                     case "splice": (change as any).added.forEach(watchUserLinks); break;
-                    case "update": let oldValue = change.oldValue;
+                    case "update": //let oldValue = change.oldValue;
                 }
             }, true);
         });
@@ -82,7 +82,9 @@ export class LinkManager {
     public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); }
 
     public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor
-    public getAllDirectLinks(anchor: Doc): Doc[] { return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); } // finds all links that contain the given anchor
+    public getAllDirectLinks(anchor: Doc): Doc[] {
+        return Array.from(Doc.GetProto(anchor)[DirectLinksSym]);
+    } // finds all links that contain the given anchor
     public getAllLinks(): Doc[] { return []; }//this.allLinks(); }
 
     // allLinks = computedFn(function allLinks(this: any): Doc[] {
diff --git a/src/client/views/AudioWaveform.scss b/src/client/views/AudioWaveform.scss
new file mode 100644
index 000000000..e20434a25
--- /dev/null
+++ b/src/client/views/AudioWaveform.scss
@@ -0,0 +1,17 @@
+.audioWaveform  {
+    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;
+    }
+}
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
new file mode 100644
index 000000000..7ff9c1071
--- /dev/null
+++ b/src/client/views/AudioWaveform.tsx
@@ -0,0 +1,61 @@
+import React = require("react");
+import axios from "axios";
+import { action, computed } 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 } from "../../fields/Types";
+import { numberRange } from "../../Utils";
+import "./AudioWaveform.scss";
+
+export interface AudioWaveformProps {
+    duration: number;
+    mediaPath: string;
+    dataDoc: Doc;
+    PanelHeight: () => number;
+}
+
+@observer
+export class AudioWaveform extends React.Component<AudioWaveformProps> {
+    public static NUMBER_OF_BUCKETS = 100;
+    @computed get _waveHeight() { return Math.max(50, this.props.PanelHeight()); }
+    componentDidMount() {
+        const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []);
+        if (!audioBuckets.length) {
+            this.props.dataDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
+            setTimeout(this.createWaveformBuckets);
+        }
+    }
+    // decodes the audio file into peaks for generating the waveform
+    createWaveformBuckets = async () => {
+        axios({ url: this.props.mediaPath, responseType: "arraybuffer" })
+            .then(response => {
+                const context = new window.AudioContext();
+                context.decodeAudioData(response.data,
+                    action(buffer => {
+                        const decodedAudioData = buffer.getChannelData(0);
+                        const bucketDataSize = Math.floor(decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS);
+                        const brange = Array.from(Array(bucketDataSize));
+                        this.props.dataDoc.audioBuckets = new List<number>(
+                            numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map((i: number) =>
+                                brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2));
+                    }));
+            });
+    }
+
+    render() {
+        const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []);
+        return <div className="audioWaveform">
+            <Waveform
+                color={"darkblue"}
+                height={this._waveHeight}
+                barWidth={0.1}
+                pos={this.props.duration}
+                duration={this.props.duration}
+                peaks={audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS ? audioBuckets : undefined}
+                progressColor={"blue"} />
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index da50ba4d8..a92891ee5 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -94,7 +94,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
         setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => {
             !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString);
             this._edtingTitle = true;
-            this._keyinput.current && setTimeout(this._keyinput.current!.focus);
+            this._keyinput.current && setTimeout(this._keyinput.current.focus);
         }));
     }
 
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 87887483f..6b0b928b3 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -79,7 +79,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
 
     sidebarStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => {
         if (property === StyleProp.ShowTitle) return StrCast(this.props.layoutDoc["sidebar-childShowTitle"], "title");
-        return this.props.styleProvider?.(doc, props, property)
+        return this.props.styleProvider?.(doc, props, property);
     }
     render() {
         const renderTag = (tag: string) => {
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index cc56831f3..2b9f3d782 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -58,4 +58,11 @@
             left: 0;
         }
     }
+
+    .collectionStackedTimeline-waveform {
+        position: absolute; 
+        width: 100%; 
+        top: 0; 
+        left: 0;
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 66b74277b..2191a3df5 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,26 +1,26 @@
 import React = require("react");
-import { action, computed, IReactionDisposer, observable, runInAction, reaction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, Opt, DocListCast } from "../../../fields/Doc";
+import { computedFn } from "mobx-utils";
+import { Doc, DocListCast } 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, StopEvent, returnOne } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
+import { LinkManager } from "../../util/LinkManager";
 import { Scripting } from "../../util/Scripting";
 import { SelectionManager } from "../../util/SelectionManager";
+import { Transform } from "../../util/Transform";
 import { undoBatch } from "../../util/UndoManager";
+import { AudioWaveform } from "../AudioWaveform";
 import { CollectionSubView } from "../collections/CollectionSubView";
-import { DocumentView, DocAfterFocusFunc, DocFocusFunc, DocumentViewProps } from "../nodes/DocumentView";
+import { LightboxView } from "../LightboxView";
+import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView";
 import { LabelBox } from "../nodes/LabelBox";
 import "./CollectionStackedTimeline.scss";
-import { Transform } from "../../util/Transform";
-import { LinkManager } from "../../util/LinkManager";
-import { computedFn } from "mobx-utils";
-import { LightboxView } from "../LightboxView";
-import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox";
 
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
@@ -35,6 +35,7 @@ export type CollectionStackedTimelineProps = {
     isChildActive: () => boolean;
     startTag: string;
     endTag: string;
+    mediaPath: string;
 };
 
 @observer
@@ -232,11 +233,42 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
         placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
         return level;
     }
+
+    dictationHeight = () => this.props.PanelHeight() / 3;
+    timelineContentHeight = () => this.props.PanelHeight() * 2 / 3;
+    @computed get renderDictation() {
+        const dictation = Cast(this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")], Doc, null);
+        return !dictation ? (null) : <div style={{ position: "absolute", height: this.dictationHeight(), top: this.timelineContentHeight(), background: "tan" }}>
+            <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                Document={dictation}
+                PanelHeight={this.dictationHeight}
+                isAnnotationOverlay={true}
+                select={emptyFunction}
+                active={returnFalse}
+                scaling={returnOne}
+                xMargin={25}
+                yMargin={10}
+                whenActiveChanged={emptyFunction}
+                removeDocument={returnFalse}
+                moveDocument={returnFalse}
+                addDocument={returnFalse}
+                CollectionView={undefined}
+                renderDepth={this.props.renderDepth + 1}>
+            </DocumentView>
+        </div>;
+    }
+    @computed get renderAudioWaveform() {
+        return !this.props.mediaPath ? (null) : <div style={{ position: "absolute", width: "100%", top: 0, left: 0 }}>
+            <AudioWaveform
+                duration={this.duration}
+                mediaPath={this.props.mediaPath}
+                dataDoc={this.dataDoc}
+                PanelHeight={this.timelineContentHeight} />
+        </div>;
+    }
     currentTimecode = () => this.currentTime;
     render() {
         const timelineContentWidth = this.props.PanelWidth();
-        const timelineContentHeight = this.props.PanelHeight() * 2 / 3;
-        const dictationHeight = this.props.PanelHeight() / 3;
         const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
         const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
         const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
@@ -247,11 +279,11 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                 const start = this.anchorStart(d.anchor);
                 const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
                 const left = start / this.duration * timelineContentWidth;
-                const top = d.level / maxLevel * timelineContentHeight;
+                const top = d.level / maxLevel * this.timelineContentHeight();
                 const timespan = end - start;
                 return this.props.Document.hideAnchors ? (null) :
                     <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
-                        style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${timelineContentHeight / maxLevel}px` }}
+                        style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${this.timelineContentHeight() / maxLevel}px` }}
                         onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
                         <StackedTimelineAnchor {...this.props}
                             mark={d.anchor}
@@ -260,7 +292,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                             left={left}
                             top={top}
                             width={timelineContentWidth * timespan / this.duration}
-                            height={timelineContentHeight / maxLevel}
+                            height={this.timelineContentHeight() / maxLevel}
                             toTimeline={this.toTimeline}
                             layoutDoc={this.layoutDoc}
                             currentTimecode={this.currentTimecode}
@@ -270,24 +302,8 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                     </div>;
             })}
             {this.selectionContainer}
-            <div style={{ position: "absolute", height: dictationHeight, top: timelineContentHeight, background: "tan" }}>
-                <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
-                    Document={this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")]}
-                    PanelHeight={() => dictationHeight}
-                    isAnnotationOverlay={true}
-                    select={emptyFunction}
-                    active={returnFalse}
-                    scaling={returnOne}
-                    xMargin={25}
-                    yMargin={10}
-                    whenActiveChanged={emptyFunction}
-                    removeDocument={returnFalse}
-                    moveDocument={returnFalse}
-                    addDocument={returnFalse}
-                    CollectionView={undefined}
-                    renderDepth={this.props.renderDepth + 1}>
-                </DocumentView>
-            </div>
+            {this.renderAudioWaveform}
+            {this.renderDictation}
 
             <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
         </div>;
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index fc881ca25..3fcb024df 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -144,24 +144,6 @@
                 border-radius: 3px;
                 z-index: 1000;
                 overflow: hidden;
-
-                .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-total-time,
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 2f7a6cfd8..06a27c22a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,18 +1,15 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import axios from "axios";
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import Waveform from "react-audio-waveform";
 import { DateField } from "../../../fields/DateField";
 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 { makeInterface } from "../../../fields/Schema";
 import { ComputedField } from "../../../fields/ScriptField";
 import { Cast, NumCast } from "../../../fields/Types";
 import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils";
+import { emptyFunction, formatTime, Utils } from "../../../Utils";
 import { DocUtils } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -35,7 +32,6 @@ const AudioDocument = makeInterface(documentSchema);
 export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
     public static Enabled = false;
-    public static NUMBER_OF_BUCKETS = 100;
     static playheadWidth = 30; // width of playhead
     static heightPercent = 80; // height of timeline in percent of height of audioBox.
     static Instance: AudioBox;
@@ -295,35 +291,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
         e.stopPropagation();
     }
 
-    // returns the audio waveform
-    @computed get waveform() {
-        const audioBuckets = Cast(this.dataDoc.audioBuckets, listSpec("number"), []);
-        !audioBuckets.length && setTimeout(() => this.createWaveformBuckets());
-        return <Waveform
-            color={"darkblue"}
-            height={this._waveHeight}
-            barWidth={0.1}
-            pos={this.duration}
-            duration={this.duration}
-            peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined}
-            progressColor={"blue"} />;
-    }
-
-    // decodes the audio file into peaks for generating the waveform
-    createWaveformBuckets = async () => {
-        axios({ url: this.path, responseType: "arraybuffer" })
-            .then(response => (new (window.AudioContext)()).decodeAudioData(response.data,
-                action(buffer => {
-                    const decodedAudioData = buffer.getChannelData(0);
-                    const bucketDataSize = Math.floor(decodedAudioData.length / AudioBox.NUMBER_OF_BUCKETS);
-                    const brange = Array.from(Array(bucketDataSize));
-                    this.dataDoc.audioBuckets = new List<number>(
-                        numberRange(AudioBox.NUMBER_OF_BUCKETS).map(i =>
-                            brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2));
-                }))
-            );
-    }
-
     playing = () => this.mediaState === "playing";
     playLink = (link: Doc) => {
         const stack = this._stackedTimeline.current;
@@ -353,6 +320,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
     @computed get renderTimeline() {
         return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
             fieldKey={this.annotationKey}
+            mediaPath={this.path}
             renderDepth={this.props.renderDepth + 1}
             startTag={"_timecodeToShow" /* audioStart */}
             endTag={"_timecodeToHide" /* audioEnd */}
@@ -407,9 +375,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
                     <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} >
                         <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.mediaState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.mediaState === "paused" ? "play" : "pause"} size={"1x"} /></div>
                         <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
-                            <div className="waveform">
-                                {this.waveform}
-                            </div>
                             {this.renderTimeline}
                         </div>
                         {this.audio}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index df769a407..8a2a755ac 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1102,16 +1102,16 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     render() {
         TraceMobx();
-        const xshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
-        const yshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
+        const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
+        const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
         return (<div className="contentFittingDocumentView">
             {!this.props.Document || !this.props.PanelWidth() ? (null) : (
                 <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
                     style={{
                         position: this.props.Document.isInkMask ? "absolute" : undefined,
                         transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
-                        width: xshift ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
-                        height: yshift ?? (this.fitWidth ? `${this.panelHeight}px` :
+                        width: xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
+                        height: yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
                             `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
                     }}>
                     <DocumentViewInternal {...this.props}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 6c019d6f2..aba7be68d 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -146,7 +146,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
     }
     contentFunc = () => [this.content];
     videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth();
-    formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight())
+    formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
     render() {
         TraceMobx();
         return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4e03589d6..da05b0c13 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -297,9 +297,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     // returns the path of the audio file
     @computed get audiopath() {
-        const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField);
-        const path = (field instanceof AudioField) ? field.url.href : "";
-        return path === nullAudio ? "" : path;
+        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;
@@ -322,7 +322,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
                         <source src={field.url.href} type="video/mp4" />
                         Not supported.
                     </video>
-                    {!this.audiopath ? (null) :
+                    {!this.audiopath || this.audiopath === field.url.href ? (null) :
                         <audio ref={this.setAudioRef} className={`audiobox-control${this.active() ? "-interactive" : ""}`}>
                             <source src={this.audiopath} type="audio/mpeg" />
                         Not supported.
@@ -510,6 +510,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
         return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
             <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
                 fieldKey={this.annotationKey}
+                mediaPath={this.audiopath}
                 renderDepth={this.props.renderDepth + 1}
                 startTag={"_timecodeToShow" /* videoStart */}
                 endTag={"_timecodeToHide" /* videoEnd */}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index aaf3a938e..abfc63b40 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1153,7 +1153,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                             setTimeout(func);
                         }
                         else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, "number", null)); // bcz: would be nice to find the next audio tag in the doc and play until that
-                    }
+                    };
                     func();
                 }
             });
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index e2c0e7ac9..ff6b2381c 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -16,7 +16,7 @@ import { resolvedServerUrl } from "./server_Initialization";
 import { AcceptableMedia, Upload } from './SharedMediaTypes';
 import request = require('request-promise');
 const parse = require('pdf-parse');
-var ffmpeg = require("fluent-ffmpeg");
+const ffmpeg = require("fluent-ffmpeg");
 const requestImageSize = require("../client/util/request-image-size");
 
 export enum SizeSuffix {
@@ -75,7 +75,8 @@ export namespace DashUploadUtils {
                 if (format.includes("x-matroska")) {
                     await new Promise(res => ffmpeg(file.path)
                         .videoCodec("copy") // this will copy the data instead of reencode it
-                        .save(file.path.replace(".mkv", ".mp4")).on('end', () => res()));
+                        .save(file.path.replace(".mkv", ".mp4"))
+                        .on('end', res));
                     file.path = file.path.replace(".mkv", ".mp4");
                     format = ".mp4";
                 }
-- 
cgit v1.2.3-70-g09d2


From 317dd86ab8bad7afc927a1d6dc7e5d458bac4340 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 26 Mar 2021 13:42:05 -0400
Subject: from last

---
 .../views/collections/CollectionStackedTimeline.scss      |  1 +
 .../views/collections/CollectionStackedTimeline.tsx       | 15 ++++++++-------
 2 files changed, 9 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 2b9f3d782..19ad5cefa 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -64,5 +64,6 @@
         width: 100%; 
         top: 0; 
         left: 0;
+        pointer-events: none;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 2191a3df5..73148560a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -258,13 +258,14 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
         </div>;
     }
     @computed get renderAudioWaveform() {
-        return !this.props.mediaPath ? (null) : <div style={{ position: "absolute", width: "100%", top: 0, left: 0 }}>
-            <AudioWaveform
-                duration={this.duration}
-                mediaPath={this.props.mediaPath}
-                dataDoc={this.dataDoc}
-                PanelHeight={this.timelineContentHeight} />
-        </div>;
+        return !this.props.mediaPath ? (null) :
+            <div className="collectionStackedTimeline-waveform" >
+                <AudioWaveform
+                    duration={this.duration}
+                    mediaPath={this.props.mediaPath}
+                    dataDoc={this.dataDoc}
+                    PanelHeight={this.timelineContentHeight} />
+            </div>;
     }
     currentTimecode = () => this.currentTime;
     render() {
-- 
cgit v1.2.3-70-g09d2


From a1a9803096adde0cd932e703080948ef1d1724f7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 29 Mar 2021 12:22:35 -0400
Subject: fixed doc decorations for dictation.  gave dictation docs a title. 
 minor cleanup.

---
 src/client/views/PropertiesView.tsx                |  2 +-
 .../collections/CollectionStackedTimeline.tsx      |  5 +++--
 .../nodes/CollectionFreeFormDocumentView.scss      |  1 +
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  1 -
 src/client/views/nodes/DocumentView.tsx            |  4 ++--
 src/client/views/nodes/FieldView.tsx               | 23 +---------------------
 src/client/views/nodes/ScreenshotBox.tsx           |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 8 files changed, 10 insertions(+), 30 deletions(-)

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

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 73c688029..0b2cdde6f 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -288,7 +288,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     addDocument={returnFalse}
                     moveDocument={undefined}
                     removeDocument={returnFalse}
-                    parentActive={() => false}
+                    parentActive={returnFalse}
                     whenActiveChanged={emptyFunction}
                     addDocTab={returnFalse}
                     pinToPres={emptyFunction}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 73148560a..c0cebf021 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -8,7 +8,7 @@ import { List } from "../../../fields/List";
 import { listSpec, makeInterface } from "../../../fields/Schema";
 import { ComputedField, ScriptField } from "../../../fields/ScriptField";
 import { Cast, NumCast } from "../../../fields/Types";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent, returnTrue } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
 import { LinkManager } from "../../util/LinkManager";
 import { Scripting } from "../../util/Scripting";
@@ -236,6 +236,7 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
 
     dictationHeight = () => this.props.PanelHeight() / 3;
     timelineContentHeight = () => this.props.PanelHeight() * 2 / 3;
+    dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight());
     @computed get renderDictation() {
         const dictation = Cast(this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")], Doc, null);
         return !dictation ? (null) : <div style={{ position: "absolute", height: this.dictationHeight(), top: this.timelineContentHeight(), background: "tan" }}>
@@ -244,10 +245,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
                 PanelHeight={this.dictationHeight}
                 isAnnotationOverlay={true}
                 select={emptyFunction}
-                active={returnFalse}
                 scaling={returnOne}
                 xMargin={25}
                 yMargin={10}
+                ScreenToLocalTransform={this.dictationScreenToLocalTransform}
                 whenActiveChanged={emptyFunction}
                 removeDocument={returnFalse}
                 moveDocument={returnFalse}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index da287649e..724394025 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -5,4 +5,5 @@
     touch-action: manipulation;
     top: 0;
     left: 0;
+    pointer-events: none;
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 58fb005b5..e96fc088e 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -176,7 +176,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 zIndex: this.ZInd,
                 mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any,
                 display: this.ZInd === -99 ? "none" : undefined,
-                pointerEvents: "none"
             }} >
 
             {Doc.UserDoc().renderStyle !== "comic" ? (null) :
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8a2a755ac..80831708d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -293,7 +293,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (e.cancelBubble && this.active) {
             this.removeMoveListeners();
         }
-        else if (!e.cancelBubble && (this.props.isSelected(true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+        else if (!e.cancelBubble && (this.active || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
             const touch = me.touchEvent.changedTouches.item(0);
             if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
                 if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
@@ -530,7 +530,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
-        else if (!e.cancelBubble && (this.props.isSelected(true) || this.props.parentActive(true) || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+        else if (!e.cancelBubble && (this.active || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
             if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
                 if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
                     document.removeEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 465c18309..97f53aac0 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -73,28 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> {
             return <p>{field.date.toLocaleString()}</p>;
         }
         else if (field instanceof Doc) {
-            return <p><b>{field.title && field.title.toString()}</b></p>;
-            //return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
-            // let returnHundred = () => 100;
-            // return (
-            //     <DocumentContentsView Document={field}
-            //         addDocument={undefined}
-            //         addDocTab={this.props.addDocTab}
-            //         removeDocument={undefined}
-            //         ScreenToLocalTransform={Transform.Identity}
-            //         ContentScaling={returnOne}
-            //         PanelWidth={returnHundred}
-            //         PanelHeight={returnHundred}
-            //         renderDepth={0} //TODO Why is renderDepth reset?
-            //         focus={emptyFunction}
-            //         isSelected={this.props.isSelected}
-            //         select={returnFalse}
-            //         layoutKey={"layout"}
-            //         ContainingCollectionView={this.props.ContainingCollectionView}
-            //         parentActive={this.props.active}
-            //         whenActiveChanged={this.props.whenActiveChanged}
-            //         bringToFront={emptyFunction} />
-            // );
+            return <p><b>{field.title?.toString()}</b></p>;
         }
         else if (field instanceof List) {
             return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : ""}  </div>;
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index aba7be68d..c00c79eb9 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -134,7 +134,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S
 
     setupDictation = () => {
         if (this.dataDoc[this.fieldKey + "-dictation"]) return;
-        const dictationText = CurrentUserUtils.GetNewTextDoc("",
+        const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
             NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
             NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
         dictationText._autoHeight = false;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index da05b0c13..efcddd7b3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -564,7 +564,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
             style={{
                 pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
                 borderRadius
-            }} >
+            }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
             <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
                 <div style={{ position: "absolute", transition: this.transition, width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}>
                     <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
-- 
cgit v1.2.3-70-g09d2

