From 5ad8289e8ca3b6ff8304ac4fa3c2d3adaa9abf6d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 1 Sep 2022 14:52:04 -0400
Subject: afixed loadingBox to show error message for unsupported videos. 
 fixed video uploading to generate errors for unsupported (Chrome) codecs.  
 fixed videos that don't have codec support to still play audio and show
 duration.  fixed dragging rotated documents to show and place the rotted
 document WYSIWYG

---
 src/client/views/nodes/VideoBox.tsx | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0ff15f93b..6ff11258d 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -396,10 +396,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // sets video info on load
     videoLoad = action(() => {
-        const aspect = this.player!.videoWidth / this.player!.videoHeight;
-        Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
-        Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
-        this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+        const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
+        if (aspect) {
+            Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
+            Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
+            this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+        }
         if (Number.isFinite(this.player!.duration)) {
             this.rawDuration = this.player!.duration;
         } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
-- 
cgit v1.2.3-70-g09d2


From e70360946815cdcde434e25eb592e1b919bb4105 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 6 Sep 2022 14:06:02 -0400
Subject: final cleanup of dragging rotated images.  fixed loading of youtube
 videos and displaying errors when the upload fails

---
 src/client/documents/Documents.ts                  |  13 +-
 src/client/util/DragManager.ts                     |  39 ++--
 src/client/views/PreviewCursor.tsx                 |  55 ++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  12 +-
 src/client/views/nodes/LoadingBox.tsx              |  10 +-
 src/client/views/nodes/VideoBox.scss               |  20 +-
 src/client/views/nodes/VideoBox.tsx                | 212 ++++++++++-----------
 src/server/ApiManagers/UploadManager.ts            |   2 +-
 src/server/DashUploadUtils.ts                      |  48 +++--
 src/server/SharedMediaTypes.ts                     |  38 ++--
 11 files changed, 257 insertions(+), 194 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8c3b91177..7111cb233 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,5 +1,4 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { files } from 'jszip';
 import { action, runInAction } from 'mobx';
 import { basename } from 'path';
 import { DateField } from '../../fields/DateField';
@@ -1780,6 +1779,9 @@ export namespace DocUtils {
                     proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
                 }
             }
+            if (Upload.isVideoInformation(result)) {
+                proto['data-duration'] = result.duration;
+            }
             generatedDocuments.push(doc);
         }
     }
@@ -1814,7 +1816,7 @@ export namespace DocUtils {
             source: { name, type },
             result,
         } of await Networking.UploadYoutubeToServer(videoId)) {
-            name && type && processFileupload(generatedDocuments, name, type, result, options);
+            name && processFileupload(generatedDocuments, name, type, result, options);
         }
         return generatedDocuments;
     }
@@ -1826,7 +1828,12 @@ export namespace DocUtils {
                 source: { name, type },
                 result,
             } = upfiles.lastElement();
-            name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
+            if ((result as any).message) {
+                if (overwriteDoc) {
+                    overwriteDoc.isLoading = false;
+                    overwriteDoc.errorMessage = (result as any).message;
+                }
+            } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
         });
     }
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index dfd916e92..cec158d23 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -382,29 +382,28 @@ export namespace DragManager {
                 }
             }
             const rect = ele.getBoundingClientRect();
-            const rotWidth = (rot > 45 && rot < 135) || (rot > 215 && rot < 305) ? rect.height : rect.width; //rect.width * Math.cos((rot * Math.PI) / 180) + rect.height * Math.sin((rot * Math.PI) / 180);
-            const scaling = rot ? rotWidth / ele.offsetWidth : rect.width / (ele.offsetWidth || rect.width);
+            const w = ele.offsetWidth || rect.width;
+            const h = ele.offsetHeight || rect.height;
+            const rotR = -(rot / 180) * Math.PI;
+            const tl = [0, 0];
+            const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
+            const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
+            const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w];
+            const minx = Math.min(tl[0], tr[0], br[0], bl[0]);
+            const maxx = Math.max(tl[0], tr[0], br[0], bl[0]);
+            const miny = Math.min(tl[1], tr[1], br[1], bl[1]);
+            const maxy = Math.max(tl[1], tr[1], br[1], bl[1]);
+            const scaling = rect.width / (Math.abs(maxx - minx) || 1);
 
             elesCont.left = Math.min(rect.left, elesCont.left);
             elesCont.top = Math.min(rect.top, elesCont.top);
             elesCont.right = Math.max(rect.right, elesCont.right);
             elesCont.bottom = Math.max(rect.bottom, elesCont.bottom);
-            const rotRad = (rot / 180) * Math.PI;
-            xs.push(
-                (rot > 90 && rot <= 270 ? rect.right : rect.left) + //
-                    (rot > 270 ? -scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) +
-                    (rot <= 90 || rot > 180 ? scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) +
-                    (options?.offsetX || 0)
-            );
-            ys.push(
-                rect.top + //
-                    (rot > 180 ? -scaling * (ele.offsetWidth * Math.sin(rotRad)) : 0) +
-                    (rot >= 90 && rot < 270 ? -scaling * (ele.offsetHeight * Math.cos(rotRad)) : 0) +
-                    (options?.offsetY || 0)
-            );
+            xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left);
+            ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top);
             scalings.push(scaling);
             Object.assign(dragElement.style, {
-                opacity: '0',
+                opacity: '0.7',
                 position: 'absolute',
                 margin: '0',
                 top: '0',
@@ -415,9 +414,9 @@ export namespace DragManager {
                 borderRadius: getComputedStyle(ele).borderRadius,
                 zIndex: globalCssVariables.contextMenuZindex,
                 transformOrigin: '0 0',
-                width: rot ? '' : `${rect.width / scaling}px`,
-                height: rot ? '' : `${rect.height / scaling}px`,
-                transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg)`,
+                width: '',
+                height: '',
+                transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
             });
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
 
@@ -431,8 +430,6 @@ export namespace DragManager {
             [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none'));
 
             dragDiv.appendChild(dragElement);
-            scalings[scalings.length - 1] = rect.width / dragElement.getBoundingClientRect().width;
-            setTimeout(() => (dragElement.style.opacity = '0.7'));
             if (dragElement !== ele) {
                 const children = [Array.from(ele.children), Array.from(dragElement.children)];
                 while (children[0].length) {
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 68f5f072d..4c17d5a97 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -4,9 +4,9 @@ import 'normalize.css';
 import * as React from 'react';
 import { Doc } from '../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
-import { returnFalse } from '../../Utils';
+import { emptyFunction, returnFalse } from '../../Utils';
 import { DocServer } from '../DocServer';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
 import { Transform } from '../util/Transform';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
@@ -16,15 +16,25 @@ import './PreviewCursor.scss';
 export class PreviewCursor extends React.Component<{}> {
     static _onKeyPress?: (e: KeyboardEvent) => void;
     static _getTransform: () => Transform;
-    static _addDocument: (doc: Doc | Doc[]) => void;
+    static _addDocument: (doc: Doc | Doc[]) => boolean;
     static _addLiveTextDoc: (doc: Doc) => void;
     static _nudge?: undefined | ((x: number, y: number) => boolean);
+    static _slowLoadDocuments?: (
+        files: File[] | string,
+        options: DocumentOptions,
+        generatedDocuments: Doc[],
+        text: string,
+        completed: ((doc: Doc[]) => void) | undefined,
+        clientX: number,
+        clientY: number,
+        addDocument: (doc: Doc | Doc[]) => boolean
+    ) => Promise<void>;
     @observable static _clickPoint = [0, 0];
     @observable public static Visible = false;
     constructor(props: any) {
         super(props);
         document.addEventListener('keydown', this.onKeyPress);
-        document.addEventListener('paste', this.paste);
+        document.addEventListener('paste', this.paste, true);
     }
 
     paste = async (e: ClipboardEvent) => {
@@ -38,20 +48,16 @@ export class PreviewCursor extends React.Component<{}> {
             if (plain) {
                 // tests for youtube and makes video document
                 if (plain.indexOf('www.youtube.com/watch') !== -1) {
-                    const url = plain.replace('youtube.com/watch?v=', 'youtube.com/embed/');
-                    undoBatch(() =>
-                        PreviewCursor._addDocument(
-                            Docs.Create.VideoDocument(url, {
-                                title: url,
-                                _width: 400,
-                                _height: 315,
-                                _nativeWidth: 600,
-                                _nativeHeight: 472.5,
-                                x: newPoint[0],
-                                y: newPoint[1],
-                            })
-                        )
-                    )();
+                    const batch = UndoManager.StartBatch('youtube upload');
+                    const generatedDocuments: Doc[] = [];
+                    const options = {
+                        title: plain,
+                        _width: 400,
+                        _height: 315,
+                        x: newPoint[0],
+                        y: newPoint[1],
+                    };
+                    PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, newPoint[0], newPoint[1], PreviewCursor._addDocument).then(batch.end);
                 } else if (re.test(plain)) {
                     const url = plain;
                     undoBatch(() =>
@@ -184,7 +190,17 @@ export class PreviewCursor extends React.Component<{}> {
         addLiveText: (doc: Doc) => void,
         getTransform: () => Transform,
         addDocument: undefined | ((doc: Doc | Doc[]) => boolean),
-        nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean)
+        nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean),
+        slowLoadDocuments: (
+            files: File[] | string,
+            options: DocumentOptions,
+            generatedDocuments: Doc[],
+            text: string,
+            completed: ((doc: Doc[]) => void) | undefined,
+            clientX: number,
+            clientY: number,
+            addDocument: (doc: Doc | Doc[]) => boolean
+        ) => Promise<void>
     ) {
         this._clickPoint = [x, y];
         this._onKeyPress = onKeyPress;
@@ -192,6 +208,7 @@ export class PreviewCursor extends React.Component<{}> {
         this._getTransform = getTransform;
         this._addDocument = addDocument || returnFalse;
         this._nudge = nudge;
+        this._slowLoadDocuments = slowLoadDocuments;
         this.Visible = true;
     }
     render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0c4de681a..947bd8aaa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -52,7 +52,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import React = require('react');
-import e = require('connect-flash');
 
 export type collectionFreeformViewProps = {
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -1858,6 +1857,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined}
                 nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge}
                 addDocTab={this.addDocTab}
+                slowLoadDocuments={this.slowLoadDocuments}
                 trySelectCluster={this.trySelectCluster}
                 activeDocuments={this.getActiveDocuments}
                 selectDocuments={this.selectDocuments}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 65a11cbcb..58a00bbac 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -40,6 +40,16 @@ interface MarqueeViewProps {
     nudge?: (x: number, y: number, nudgeTime?: number) => boolean;
     ungroup?: () => void;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
+    slowLoadDocuments: (
+        files: File[] | string,
+        options: DocumentOptions,
+        generatedDocuments: Doc[],
+        text: string,
+        completed: ((doc: Doc[]) => void) | undefined,
+        clientX: number,
+        clientY: number,
+        addDocument: (doc: Doc | Doc[]) => boolean
+    ) => Promise<void>;
 }
 
 export interface MarqueeViewBounds {
@@ -330,7 +340,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this._downY = y;
             const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
             if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
-                PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+                PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments);
             }
             this.clearSelection();
         }
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 462ad425a..acf728ebb 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -1,12 +1,10 @@
 import { observer } from 'mobx-react';
-import { ViewBoxAnnotatableComponent } from '../DocComponent';
-import { FieldView, FieldViewProps } from './FieldView';
 import * as React from 'react';
-import './LoadingBox.scss';
 import ReactLoading from 'react-loading';
 import { StrCast } from '../../../fields/Types';
-import { computed } from 'mobx';
-import { Docs } from '../../documents/Documents';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from './FieldView';
+import './LoadingBox.scss';
 
 /**
  * LoadingBox Class represents a placeholder doc for documents that are currently
@@ -39,7 +37,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return (
             <div className="loadingBoxContainer" style={{ background: this.rootDoc.isLoading ? '' : 'red' }}>
                 <div className="textContainer">
-                    <p className="headerText">{this.rootDoc.isLoading ? 'Loading:' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}</p>
+                    <p className="headerText">{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}</p>
                     <span className="text">{StrCast(this.rootDoc.title)}</span>
                     {!this.rootDoc.isLoading ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
                 </div>
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index aa51714da..c2aee7a1b 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,4 @@
-@import "../global/globalCssVariables.scss";
+@import '../global/globalCssVariables.scss';
 
 .mini-viewer {
     cursor: grab;
@@ -97,7 +97,7 @@
     height: 40px;
     padding: 0 10px 0 7px;
     transition: opacity 0.3s;
-    z-index: 100001;
+    z-index: 10001;
 
     .timecode-controls {
         display: flex;
@@ -114,7 +114,8 @@
         }
     }
 
-    .toolbar-slider.volume, .toolbar-slider.zoom {
+    .toolbar-slider.volume,
+    .toolbar-slider.zoom {
         width: 50px;
     }
 
@@ -157,7 +158,8 @@
     }
 }
 
-.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive {
+.videoBox-content-fullScreen,
+.videoBox-content-fullScreen-interactive {
     display: flex;
     justify-content: center;
     align-items: flex-end;
@@ -175,16 +177,16 @@ video::-webkit-media-controls {
     display: none !important;
 }
 
-input[type="range"] {
+input[type='range'] {
     -webkit-appearance: none;
     background: none;
 }
 
-input[type="range"]:focus {
+input[type='range']:focus {
     outline: none;
 }
 
-input[type="range"]::-webkit-slider-runnable-track {
+input[type='range']::-webkit-slider-runnable-track {
     width: 100%;
     height: 10px;
     cursor: pointer;
@@ -193,7 +195,7 @@ input[type="range"]::-webkit-slider-runnable-track {
     border-radius: 10px;
 }
 
-input[type="range"]::-webkit-slider-thumb {
+input[type='range']::-webkit-slider-thumb {
     box-shadow: 0;
     border: 0;
     height: 12px;
@@ -203,4 +205,4 @@ input[type="range"]::-webkit-slider-thumb {
     cursor: pointer;
     -webkit-appearance: none;
     margin-top: -1px;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 6ff11258d..bfb8c1528 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -935,112 +935,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     };
 
-    @computed get UIButtons() {
-        const bounds = this.props.docViewPath().lastElement().getBounds();
-        const width = (bounds?.right || 0) - (bounds?.left || 0);
-        const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
-        return (
-            <>
-                <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
-                    <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
-                </div>
-
-                {this.timeline && width > 150 && (
-                    <div className="timecode-controls">
-                        <div className="timecode-current">{formatTime(curTime)}</div>
-
-                        {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
-                            <div className="timeline-slider">
-                                <input
-                                    type="range"
-                                    step="0.1"
-                                    min={this.timeline.clipStart}
-                                    max={this.timeline.clipEnd}
-                                    value={curTime}
-                                    className="toolbar-slider time-progress"
-                                    onPointerDown={action((e: React.PointerEvent) => {
-                                        e.stopPropagation();
-                                        this._scrubbing = true;
-                                    })}
-                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
-                                    onPointerUp={action((e: React.PointerEvent) => {
-                                        e.stopPropagation();
-                                        this._scrubbing = false;
-                                    })}
-                                />
-                            </div>
-                        ) : (
-                            <div>/</div>
-                        )}
-
-                        <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
-                    </div>
-                )}
-
-                <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
-                    <FontAwesomeIcon icon="expand" />
-                </div>
-
-                {!this._fullScreen && width > 300 && (
-                    <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
-                        <FontAwesomeIcon icon="eye" />
-                    </div>
-                )}
-
-                {!this._fullScreen && width > 300 && (
-                    <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
-                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
-                    </div>
-                )}
-
-                <div
-                    className="videobox-button"
-                    title={this._muted ? 'unmute' : 'mute'}
-                    onPointerDown={e => {
-                        e.stopPropagation();
-                        this.toggleMute();
-                    }}>
-                    <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
-                </div>
-                {width > 300 && (
-                    <input
-                        type="range"
-                        style={{ width: `min(25%, 50px)` }}
-                        step="0.1"
-                        min="0"
-                        max="1"
-                        value={this._muted ? 0 : this._volume}
-                        className="toolbar-slider volume"
-                        onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
-                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
-                    />
-                )}
-
-                {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
-                    <>
-                        <div className="videobox-button" title="zoom">
-                            <FontAwesomeIcon icon="search-plus" />
-                        </div>
-                        <input
-                            type="range"
-                            step="0.1"
-                            min="1"
-                            max="5"
-                            value={this.timeline?._zoomFactor}
-                            className="toolbar-slider zoom"
-                            onPointerDown={(e: React.PointerEvent) => {
-                                e.stopPropagation();
-                            }}
-                            onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                                this.zoom(Number(e.target.value));
-                            }}
-                        />
-                    </>
-                )}
-            </>
-        );
-    }
-
     // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
@@ -1149,6 +1043,112 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             </div>
         );
     }
+
+    @computed get UIButtons() {
+        const bounds = this.props.docViewPath().lastElement().getBounds();
+        const width = (bounds?.right || 0) - (bounds?.left || 0);
+        const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+        return (
+            <>
+                <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
+                    <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
+                </div>
+
+                {this.timeline && width > 150 && (
+                    <div className="timecode-controls">
+                        <div className="timecode-current">{formatTime(curTime)}</div>
+
+                        {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
+                            <div className="timeline-slider">
+                                <input
+                                    type="range"
+                                    step="0.1"
+                                    min={this.timeline.clipStart}
+                                    max={this.timeline.clipEnd}
+                                    value={curTime}
+                                    className="toolbar-slider time-progress"
+                                    onPointerDown={action((e: React.PointerEvent) => {
+                                        e.stopPropagation();
+                                        this._scrubbing = true;
+                                    })}
+                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+                                    onPointerUp={action((e: React.PointerEvent) => {
+                                        e.stopPropagation();
+                                        this._scrubbing = false;
+                                    })}
+                                />
+                            </div>
+                        ) : (
+                            <div>/</div>
+                        )}
+
+                        <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
+                    </div>
+                )}
+
+                <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
+                    <FontAwesomeIcon icon="expand" />
+                </div>
+
+                {!this._fullScreen && width > 300 && (
+                    <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
+                        <FontAwesomeIcon icon="eye" />
+                    </div>
+                )}
+
+                {!this._fullScreen && width > 300 && (
+                    <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
+                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
+                    </div>
+                )}
+
+                <div
+                    className="videobox-button"
+                    title={this._muted ? 'unmute' : 'mute'}
+                    onPointerDown={e => {
+                        e.stopPropagation();
+                        this.toggleMute();
+                    }}>
+                    <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
+                </div>
+                {width > 300 && (
+                    <input
+                        type="range"
+                        style={{ width: `min(25%, 50px)` }}
+                        step="0.1"
+                        min="0"
+                        max="1"
+                        value={this._muted ? 0 : this._volume}
+                        className="toolbar-slider volume"
+                        onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+                    />
+                )}
+
+                {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
+                    <>
+                        <div className="videobox-button" title="zoom">
+                            <FontAwesomeIcon icon="search-plus" />
+                        </div>
+                        <input
+                            type="range"
+                            step="0.1"
+                            min="1"
+                            max="5"
+                            value={this.timeline?._zoomFactor}
+                            className="toolbar-slider zoom"
+                            onPointerDown={(e: React.PointerEvent) => {
+                                e.stopPropagation();
+                            }}
+                            onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+                                this.zoom(Number(e.target.value));
+                            }}
+                        />
+                    </>
+                )}
+            </>
+        );
+    }
 }
 
 VideoBox._nativeControls = false;
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 787e331c5..0b6e18743 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -86,7 +86,7 @@ export default class UploadManager extends ApiManager {
                         const videoId = JSON.parse(payload).videoId;
                         const results: Upload.FileResponse[] = [];
                         const result = await DashUploadUtils.uploadYoutube(videoId);
-                        result && !(result.result instanceof Error) && results.push(result);
+                        result && results.push(result);
                         _success(res, results);
                         resolve();
                     });
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index ef7192ecc..28e26e51e 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -17,8 +17,6 @@ import { resolvedServerUrl } from './server_Initialization';
 import { AcceptableMedia, Upload } from './SharedMediaTypes';
 import request = require('request-promise');
 import formidable = require('formidable');
-import { file } from 'jszip';
-import { csvParser } from './DataVizUtils';
 const { exec } = require('child_process');
 const parse = require('pdf-parse');
 const ffmpeg = require('fluent-ffmpeg');
@@ -102,16 +100,41 @@ export namespace DashUploadUtils {
     }
 
     export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
-        console.log('UPLOAD ' + videoId);
         return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => {
-            exec('youtube-dl -o ' + (videoId + '.mp4') + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', (error: any, stdout: any, stderr: any) => {
-                if (error) console.log(`error: ${error.message}`);
-                else if (stderr) console.log(`stderr: ${stderr}`);
-                else {
-                    console.log(`stdout: ${stdout}`);
-                    const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' };
-                    const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: null, length: 0, mime: '', toJson: () => undefined as any }) };
-                    res(MoveParsedFile(file, Directory.videos));
+            console.log('Uploading YouTube video: ' + videoId);
+            exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' -f "bestvideo[filesize<5M]+bestaudio/bestvideo+bestaudio"', (error: any, stdout: any, stderr: any) => {
+                if (error) {
+                    console.log(`error: Error: ${error.message}`);
+                    res({
+                        source: {
+                            size: 0,
+                            path: videoId,
+                            name: videoId,
+                            type: '',
+                            toJSON: () => ({ name: videoId, path: videoId }),
+                        },
+                        result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}).  Error: ${error.message}` },
+                    });
+                } else if (stderr) {
+                    console.log(`stderr: StdError: ${stderr}`);
+                    res({
+                        source: {
+                            size: 0,
+                            path: videoId,
+                            name: videoId,
+                            type: '',
+                            toJSON: () => ({ name: videoId, path: videoId }),
+                        },
+                        result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}).  Error: ${stderr}` },
+                    });
+                } else {
+                    exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' --get-duration', (error: any, stdout: any, stderr: any) => {
+                        const time = Array.from(stdout.trim().split(':')).reverse();
+                        const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
+                        const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' };
+                        const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: duration.toString(), mime: '', toJson: () => undefined as any }) };
+                        res(MoveParsedFile(file, Directory.videos));
+                    });
                 }
             });
         });
@@ -352,7 +375,7 @@ export namespace DashUploadUtils {
      * @param suffix If the file doesn't have a suffix and you want to provide it one
      * to appear in the new location
      */
-    export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string): Promise<Upload.FileResponse> {
+    export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number): Promise<Upload.FileResponse> {
         const { path: sourcePath } = file;
         let name = path.basename(sourcePath);
         suffix && (name += suffix);
@@ -368,6 +391,7 @@ export namespace DashUploadUtils {
                                   agnostic: getAccessPaths(destination, name),
                               },
                               rawText: text,
+                              duration,
                           },
                 });
             });
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index cde95526f..7db1c2dae 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -2,36 +2,45 @@ import { ExifData } from 'exif';
 import { File } from 'formidable';
 
 export namespace AcceptableMedia {
-    export const gifs = [".gif"];
-    export const pngs = [".png"];
-    export const jpgs = [".jpg", ".jpeg"];
-    export const webps = [".webp"];
-    export const tiffs = [".tiff"];
+    export const gifs = ['.gif'];
+    export const pngs = ['.png'];
+    export const jpgs = ['.jpg', '.jpeg'];
+    export const webps = ['.webp'];
+    export const tiffs = ['.tiff'];
     export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs];
-    export const videoFormats = [".mov", ".mp4", ".quicktime", ".mkv", ".x-matroska;codecs=avc1"];
-    export const applicationFormats = [".pdf"];
-    export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"];
+    export const videoFormats = ['.mov', '.mp4', '.quicktime', '.mkv', '.x-matroska;codecs=avc1'];
+    export const applicationFormats = ['.pdf'];
+    export const audioFormats = ['.wav', '.mp3', '.mpeg', '.flac', '.au', '.aiff', '.m4a', '.webm'];
 }
 
 export namespace Upload {
-
     export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
-        return "nativeWidth" in uploadResponse;
+        return 'nativeWidth' in uploadResponse;
+    }
+
+    export function isVideoInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.VideoInformation {
+        return 'duration' in uploadResponse;
     }
 
     export interface FileInformation {
         accessPaths: AccessPathInfo;
         rawText?: string;
+        duration?: number;
     }
 
-    export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error };
+    export type FileResponse<T extends FileInformation = FileInformation> = { source: File; result: T | Error };
 
     export type ImageInformation = FileInformation & InspectionResults;
 
+    export type VideoInformation = FileInformation & VideoResults;
+
     export interface AccessPathInfo {
-        [suffix: string]: { client: string, server: string };
+        [suffix: string]: { client: string; server: string };
     }
 
+    export interface VideoResults {
+        duration: number;
+    }
     export interface InspectionResults {
         source: string;
         requestable: string;
@@ -44,8 +53,7 @@ export namespace Upload {
     }
 
     export interface EnrichedExifData {
-        data: ExifData & ExifData["gps"];
+        data: ExifData & ExifData['gps'];
         error?: string;
     }
-
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3-70-g09d2


From 2d8f763d763080fd52e940abb1a98b41e2da23fd Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 Sep 2022 14:28:44 -0400
Subject: fixed dragging stackedTimeline entries so that they appear during
 drag.   fixed cursors for video/pdf/stackedTimeline to be a little more
 clear.   fixed initial corner resize of pdfs.  fixed videobox marquee drag
 when viewScale is undefined.

---
 src/client/util/DragManager.ts                       |  9 +++++++--
 src/client/views/DocumentDecorations.tsx             |  3 +++
 src/client/views/MarqueeAnnotator.scss               |  9 ++++-----
 src/client/views/MarqueeAnnotator.tsx                |  3 +--
 .../views/collections/CollectionStackedTimeline.scss |  4 +++-
 .../views/collections/CollectionStackedTimeline.tsx  |  4 +++-
 .../collections/collectionFreeForm/MarqueeView.scss  | 20 +++++++++-----------
 src/client/views/nodes/VideoBox.tsx                  |  4 ++--
 8 files changed, 32 insertions(+), 24 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index cec158d23..664933de0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -357,10 +357,13 @@ export namespace DragManager {
         let rot = 0;
         const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
         const dragElements = eles.map(ele => {
+            let useDim = false;
             if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
                 ele = ele.parentElement.parentElement.parentElement;
                 const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1');
                 if (rotStr) rot = Number(rotStr);
+            } else {
+                useDim = true;
             }
             if (rot < 0) rot += 360;
             if (!ele.parentNode) dragDiv.appendChild(ele);
@@ -402,6 +405,8 @@ export namespace DragManager {
             xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left);
             ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top);
             scalings.push(scaling);
+            const width = useDim ? getComputedStyle(ele).width : '';
+            const height = useDim ? getComputedStyle(ele).height : '';
             Object.assign(dragElement.style, {
                 opacity: '0.7',
                 position: 'absolute',
@@ -414,8 +419,8 @@ export namespace DragManager {
                 borderRadius: getComputedStyle(ele).borderRadius,
                 zIndex: globalCssVariables.contextMenuZindex,
                 transformOrigin: '0 0',
-                width: '',
-                height: '',
+                width,
+                height,
                 transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
             });
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3589e014a..a79f727a7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -476,6 +476,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
                 if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                     const doc = Document(docView.rootDoc);
+                    if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) {
+                        doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth;
+                    }
                     const nwidth = docView.nativeWidth;
                     const nheight = docView.nativeHeight;
                     let docheight = doc._height || 0;
diff --git a/src/client/views/MarqueeAnnotator.scss b/src/client/views/MarqueeAnnotator.scss
index c90d48a65..5c65f35e9 100644
--- a/src/client/views/MarqueeAnnotator.scss
+++ b/src/client/views/MarqueeAnnotator.scss
@@ -1,12 +1,11 @@
-
 .marqueeAnnotator-annotationBox {
     position: absolute;
     background-color: rgba(245, 230, 95, 0.616);
 }
 
-
 .marqueeAnnotator-dragBox {
-    position:absolute;
+    position: absolute;
     background-color: transparent;
-    opacity: 0.1;
-}
\ No newline at end of file
+    opacity: 0.2;
+    cursor: default;
+}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index f90ad8bb5..d9a989309 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -271,8 +271,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                     width: `${this._width}px`,
                     height: `${this._height}px`,
                     border: `${this._width === 0 ? '' : '2px dashed black'}`,
-                    opacity: 0.2,
-                }}></div>
+                }}/>
         );
     }
 }
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index aa8502c20..5a107d2ca 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -10,6 +10,7 @@
     border-width: 0 2px 0 2px;
 
     &:hover {
+        cursor: default;
         .collectionStackedTimeline-hover {
             display: block;
         }
@@ -109,14 +110,15 @@
             height: 100%;
             width: 10px;
             pointer-events: all;
-            cursor: ew-resize;
             z-index: 100;
         }
         .collectionStackedTimeline-resizer {
             right: 0;
+            cursor: e-resize;
         }
         .collectionStackedTimeline-left-resizer {
             left: 0;
+            cursor: w-resize;
         }
     }
 
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 2543624d3..b29abf083 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -244,6 +244,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     if (!wasSelecting) {
                         this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
                         wasSelecting = true;
+                        this._timelineWrapper && (this._timelineWrapper.style.cursor = 'ew-resize');
                     }
                     this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
                     return false;
@@ -260,6 +261,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                         setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
                     }
                     (!isClick || !wasSelecting) && (this._markerEnd = undefined);
+                    this._timelineWrapper && (this._timelineWrapper.style.cursor = '');
                 }),
                 (e, doubleTap) => {
                     if (e.button !== 2) {
@@ -561,7 +563,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
             <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
                 <div
                     className="collectionStackedTimeline-timelineContainer"
-                    style={{ width: this.props.PanelWidth() }}
+                    style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }}
                     onWheel={e => e.stopPropagation()}
                     onScroll={this.setScroll}
                     onMouseMove={e => this.isContentActive() && this.onHover(e)}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 41e4d6b6a..e0f5cbe5b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,16 +1,14 @@
-
 .marqueeView {
     position: inherit;
-    top:0;
-    left:0;
-    width:100%;
-    height:100%;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
     overflow: hidden;
     border-radius: inherit;
     user-select: none;
 }
 
-
 .marqueeView:focus-within {
     overflow: hidden;
 }
@@ -22,13 +20,13 @@
     border-color: black;
     pointer-events: none;
     .marquee-legend {
-        bottom:-18px;
-        left:0;
+        bottom: -18px;
+        left: 0;
         position: absolute;
         font-size: 9;
-        white-space:nowrap;
+        white-space: nowrap;
     }
     .marquee-legend::after {
-        content: "Press <space> for lasso"
+        content: 'Press <space> for lasso';
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index bfb8c1528..5a3594ffc 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -551,7 +551,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return !field ? (
             <div key="loading">Loading</div>
         ) : (
-            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
+            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'default' }}>
                 <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
                     {this._fullScreen && (
                         <div
@@ -855,7 +855,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // starts marquee selection
     marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
             setupMoveUpEvents(
                 this,
                 e,
-- 
cgit v1.2.3-70-g09d2


From 4315a0378bc54ae9eaa684d416839f635c38e865 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 12 Sep 2022 16:18:42 -0400
Subject: fixed rotating UI for video boxes.  fixed generating error response
 for unsupported video/audio formats.

---
 src/client/views/DocumentDecorations.tsx |  8 +++----
 src/client/views/nodes/VideoBox.tsx      | 38 +++++++++++++++++++++-----------
 src/server/DashUploadUtils.ts            |  3 +++
 3 files changed, 31 insertions(+), 18 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a79f727a7..832d0a35c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -738,11 +738,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 </>
                             )}
 
-                            {useRotation && (
-                                <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                    {'⟲'}
-                                </div>
-                            )}
+                            <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                {'⟲'}
+                            </div>
 
                             {useRounding && (
                                 <div
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 5a3594ffc..b45ee7f6e 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -914,21 +914,33 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // renders video controls
     componentUI = (boundsLeft: number, boundsTop: number) => {
-        const bounds = this.props.docViewPath().lastElement().getBounds();
-        const left = bounds?.left || 0;
-        const right = bounds?.right || 0;
-        const top = bounds?.top || 0;
-        const height = (bounds?.bottom || 0) - top;
-        const width = Math.max(right - left, 100);
-        const uiHeight = Math.max(25, Math.min(50, height / 10));
-        const uiMargin = Math.min(10, height / 20);
-        const vidHeight = (height * this.heightPercent) / 100;
-        const yPos = top + vidHeight - uiHeight - uiMargin;
-        const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10;
+        const xf = this.props.ScreenToLocalTransform().inverse();
+        const height = this.props.PanelHeight();
+        const vidHeight = (height * this.heightPercent) / 100 / this.scaling();
+        const vidWidth = this.props.PanelWidth() / this.scaling();
+        const uiHeight = 25;
+        const uiMargin = 10;
+        const yBot = xf.transformPoint(0, vidHeight)[1];
+        // prettier-ignore
+        const yMid =  (xf.transformPoint(0, 0)[1] +
+                       xf.transformPoint(0, height / this.scaling())[1]) /  2;
+        const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
+        const xRight = xf.transformPoint(vidWidth, 0)[0];
         const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
-        return this._fullScreen || right - left < 50 ? null : (
+        return this._fullScreen || (xRight - xPos) * 2 < 50 ? null : (
             <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
-                <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}>
+                <div
+                    className="videoBox-ui"
+                    style={{
+                        transformOrigin: 'top left',
+                        transform: `rotate(${NumCast(this.rootDoc.jitterRotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
+                        left: xPos,
+                        top: yMid,
+                        height: uiHeight,
+                        width: (xRight - xPos) * 2 - 20,
+                        transition: this._clicking ? 'top 0.5s' : '',
+                        opacity,
+                    }}>
                     {this.UIButtons}
                 </div>
             </div>
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index e94ef8534..8cf657da4 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -142,6 +142,7 @@ export namespace DashUploadUtils {
                     const result = await UploadImage(path, basename(path));
                     return { source: file, result };
                 }
+                return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${name}). Please convert to an .jpg` } };
             case 'video':
                 if (format.includes('x-matroska')) {
                     console.log('case video');
@@ -179,6 +180,7 @@ export namespace DashUploadUtils {
                 if (videoFormats.includes(format)) {
                     return MoveParsedFile(file, Directory.videos);
                 }
+                return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } };
             case 'application':
                 if (applicationFormats.includes(format)) {
                     return UploadPdf(file);
@@ -191,6 +193,7 @@ export namespace DashUploadUtils {
                 if (audioFormats.includes(format)) {
                     return UploadAudio(file, format);
                 }
+                return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${name}). Please convert to an .mp3` } };
             case 'text':
                 if (types[1] == 'csv') {
                     return UploadCsv(file);
-- 
cgit v1.2.3-70-g09d2


From 0046f6eab6ec28b509398b5184922296014a66d0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 21 Sep 2022 13:24:02 -0400
Subject:  added cropping for videos.  made images render at full res when
 selected.

---
 src/client/views/nodes/ImageBox.tsx |  2 +-
 src/client/views/nodes/VideoBox.tsx | 53 +++++++++++++++++++++++++++++++++++--
 2 files changed, 52 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 11f221448..d3d68d835 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -74,7 +74,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth,
                 selected: this.props.isSelected(),
             }),
-            ({ forceFull, scrSize, selected }) => (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'),
+            ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
             { fireImmediately: true, delay: 1000 }
         );
         this._disposers.path = reaction(
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b45ee7f6e..ac472aa89 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
+import { ObjectField } from '../../../fields/ObjectField';
 const path = require('path');
 
 /**
@@ -397,7 +398,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // sets video info on load
     videoLoad = action(() => {
         const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
-        if (aspect) {
+        if (aspect && !this.dataDoc.viewScaleMin) {
             Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
             Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
             this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
@@ -570,7 +571,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                         key="video"
                         autoPlay={this._screenCapture}
                         ref={this.setVideoRef}
-                        style={this._fullScreen ? this.fullScreenSize() : {}}
+                        style={this._fullScreen ? this.fullScreenSize() : this.dataDoc.viewScaleMin ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
                         onCanPlay={this.videoLoad}
                         controls={VideoBox._nativeControls}
                         onPlay={() => this.Play()}
@@ -986,6 +987,53 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
     }
 
+    crop = (region: Doc | undefined, addCrop?: boolean) => {
+        if (!region) return;
+        const cropping = Doc.MakeCopy(region, true);
+        Doc.GetProto(region).backgroundColor = 'transparent';
+        Doc.GetProto(region).lockedPosition = true;
+        Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
+        Doc.GetProto(region).isPushpin = true;
+        region._timecodeToHide = region._timecodeToShow;
+        this.addDocument(region);
+        const anchx = NumCast(cropping.x);
+        const anchy = NumCast(cropping.y);
+        const anchw = NumCast(cropping._width);
+        const anchh = NumCast(cropping._height);
+        const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+        cropping.title = 'crop: ' + this.rootDoc.title;
+        cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
+        cropping.y = NumCast(this.rootDoc.y);
+        cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
+        cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
+        cropping.timecodeToHide = undefined;
+        cropping.timecodeToShow = undefined;
+        cropping.isLinkButton = undefined;
+        const croppingProto = Doc.GetProto(cropping);
+        croppingProto.annotationOn = undefined;
+        croppingProto.isPrototype = true;
+        croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
+        croppingProto.type = DocumentType.VID;
+        croppingProto.layout = VideoBox.LayoutString('data');
+        croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
+        croppingProto['data-nativeWidth'] = anchw;
+        croppingProto['data-nativeHeight'] = anchh;
+        croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
+        croppingProto.viewScale = viewScale;
+        croppingProto.viewScaleMin = viewScale;
+        croppingProto.panX = anchx / viewScale;
+        croppingProto.panY = anchy / viewScale;
+        croppingProto.panXMin = anchx / viewScale;
+        croppingProto.panXMax = anchw / viewScale;
+        croppingProto.panYMin = anchy / viewScale;
+        croppingProto.panYMax = anchh / viewScale;
+        if (addCrop) {
+            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+        }
+        this.props.bringToFront(cropping);
+        return cropping;
+    };
+
     savedAnnotations = () => this._savedAnnotations;
     render() {
         const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -1048,6 +1096,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             savedAnnotations={this.savedAnnotations}
                             annotationLayer={this._annotationLayer.current}
                             mainCont={this._mainCont.current}
+                            anchorMenuCrop={this.crop}
                         />
                     )}
                     {this.renderTimeline}
-- 
cgit v1.2.3-70-g09d2


From 4484aff465c8f4f242b98d31558483eff245ab95 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 21 Sep 2022 13:31:20 -0400
Subject: no video ui for cropped videos

---
 src/client/views/nodes/VideoBox.tsx | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index ac472aa89..aabe3eb25 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -398,7 +398,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // sets video info on load
     videoLoad = action(() => {
         const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
-        if (aspect && !this.dataDoc.viewScaleMin) {
+        if (aspect && !this.isCropped) {
             Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
             Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
             this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
@@ -571,7 +571,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                         key="video"
                         autoPlay={this._screenCapture}
                         ref={this.setVideoRef}
-                        style={this._fullScreen ? this.fullScreenSize() : this.dataDoc.viewScaleMin ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
+                        style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
                         onCanPlay={this.videoLoad}
                         controls={VideoBox._nativeControls}
                         onPlay={() => this.Play()}
@@ -928,7 +928,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
         const xRight = xf.transformPoint(vidWidth, 0)[0];
         const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
-        return this._fullScreen || (xRight - xPos) * 2 < 50 ? null : (
+        return this._fullScreen || this.isCropped || (xRight - xPos) * 2 < 50 ? null : (
             <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                 <div
                     className="videoBox-ui"
@@ -987,6 +987,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
     }
 
+    @computed get isCropped() {
+        return this.dataDoc.viewScaleMin; // bcz: hack to identify a cropped video
+    }
     crop = (region: Doc | undefined, addCrop?: boolean) => {
         if (!region) return;
         const cropping = Doc.MakeCopy(region, true);
-- 
cgit v1.2.3-70-g09d2


From 69719257b8275cb19d63960a6ec552a0c118d988 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 21 Sep 2022 16:43:54 -0400
Subject: fixed navigating to cropped video regions.

---
 .../collections/CollectionStackedTimeline.tsx      | 27 ++++++++++++++++++++--
 src/client/views/nodes/VideoBox.tsx                | 17 ++++++++++----
 2 files changed, 38 insertions(+), 6 deletions(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index b29abf083..c694e17fb 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,7 +2,7 @@ import React = require('react');
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -23,11 +23,13 @@ import { AudioWaveform } from '../AudioWaveform';
 import { CollectionSubView } from '../collections/CollectionSubView';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
-import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps, DocumentViewSharedProps } from '../nodes/DocumentView';
 import { LabelBox } from '../nodes/LabelBox';
 import './CollectionStackedTimeline.scss';
 import { VideoBox } from '../nodes/VideoBox';
 import { ImageField } from '../../../fields/URLField';
+import { StyleProp } from '../StyleProvider';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 
 export type CollectionStackedTimelineProps = {
     Play: () => void;
@@ -693,6 +695,7 @@ interface StackedTimelineAnchorProps {
     width: number;
     height: number;
     toTimeline: (screen_delta: number, width: number) => number;
+    styleProvider?: (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
     playLink: (linkDoc: Doc) => void;
     setTime: (time: number) => void;
     startTag: string;
@@ -803,6 +806,25 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
         return [resetTitle];
     };
 
+    innerStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
+        if (property === StyleProp.Decorations && doc && NumCast(doc.timecodeToHide) - NumCast(doc.timecodeToShow) < 0.0002) {
+            return (
+                <div className="styleProvider-lock">
+                    <FontAwesomeIcon
+                        icon={'camera'}
+                        style={{ color: 'red' }}
+                        onClick={e => {
+                            LinkFollower.FollowLink(undefined, doc, props as DocumentViewSharedProps, e.altKey);
+                            e.stopPropagation();
+                        }}
+                        size="lg"
+                    />
+                </div>
+            );
+        }
+        return this.props.styleProvider?.(doc, props, property);
+    };
+
     // renders anchor LabelBox
     renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
         const anchor = observable({ view: undefined as any });
@@ -819,6 +841,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                     ref={action((r: DocumentView | null) => (anchor.view = r))}
                     Document={mark}
                     DataDoc={undefined}
+                    styleProvider={this.innerStyleProvider}
                     renderDepth={this.props.renderDepth + 1}
                     LayoutTemplate={undefined}
                     LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index aabe3eb25..4bcd79641 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -948,6 +948,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     };
 
+    scrollFocus = (doc: Doc, smooth: boolean) => {
+        if (doc !== this.rootDoc) {
+            const showTime = Cast(doc._timecodeToShow, 'number', null);
+            showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
+            return 0.1;
+        }
+    };
+
     // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
@@ -981,15 +989,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             </div>
         );
     }
+    @computed get isCropped() {
+        return this.dataDoc.videoCrop; // bcz: hack to identify a cropped video
+    }
 
     // renders annotation layer
     @computed get annotationLayer() {
         return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
     }
 
-    @computed get isCropped() {
-        return this.dataDoc.viewScaleMin; // bcz: hack to identify a cropped video
-    }
     crop = (region: Doc | undefined, addCrop?: boolean) => {
         if (!region) return;
         const cropping = Doc.MakeCopy(region, true);
@@ -997,7 +1005,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
         Doc.GetProto(region).isPushpin = true;
-        region._timecodeToHide = region._timecodeToShow;
+        region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
         this.addDocument(region);
         const anchx = NumCast(cropping.x);
         const anchy = NumCast(cropping.y);
@@ -1021,6 +1029,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
         croppingProto['data-nativeWidth'] = anchw;
         croppingProto['data-nativeHeight'] = anchh;
+        croppingProto.videoCrop = true;
         croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
         croppingProto.viewScale = viewScale;
         croppingProto.viewScaleMin = viewScale;
-- 
cgit v1.2.3-70-g09d2


From d40a8e4506672f9d0ad505409f4b181d73762c28 Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Wed, 21 Sep 2022 23:23:37 -0400
Subject: fixed video duration in presbox media controls

---
 src/client/views/nodes/VideoBox.tsx       | 1 +
 src/client/views/nodes/trails/PresBox.tsx | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/VideoBox.tsx')

diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4bcd79641..f7f558bb4 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -405,6 +405,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
         if (Number.isFinite(this.player!.duration)) {
             this.rawDuration = this.player!.duration;
+            this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration;
         } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
     });
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 40cda4f7e..042c55a46 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1427,7 +1427,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const activeItem: Doc = this.activeItem;
         const targetDoc: Doc = this.targetDoc;
         const clipStart: number = NumCast(activeItem.clipStart);
-        const clipEnd: number = NumCast(activeItem.clipEnd);
+        const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem.duration));
         const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
         if (activeItem && targetDoc) {
             return (
-- 
cgit v1.2.3-70-g09d2

