From 358f9e266ef264442aea1e2c7d5d959a19f7624c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 22 Jul 2022 14:11:30 -0400
Subject: adjusted native dim scaling slightly combining props.scaling and
 props.ContentScaling into props.NativeDimScaling and fixing some resizing
 behaviors for fitWidth freeformviews and native-sized text boxes.   Also
 fixed clicking on presboxe elements to not drag.

---
 src/client/views/nodes/VideoBox.tsx | 6 +++---
 1 file changed, 3 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 b1f7d8023..681f7c5b2 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -898,7 +898,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
 
-    scaling = () => this.props.scaling?.() || 1;
+    scaling = () => this.props.NativeDimScaling?.() || 1;
 
     panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
     panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);
@@ -911,7 +911,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             .scale(100 / this.heightPercent);
     };
 
-    marqueeFitScaling = () => ((this.props.scaling?.() || 1) * this.heightPercent) / 100;
+    marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100;
     marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0];
 
     timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
@@ -1124,7 +1124,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             ScreenToLocalTransform={this.screenToLocalTransform}
                             docFilters={this.timelineDocFilter}
                             select={emptyFunction}
-                            scaling={returnOne}
+                            NativeDimScaling={returnOne}
                             whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                             removeDocument={this.removeDocument}
                             moveDocument={this.moveDocument}
-- 
cgit v1.2.3-70-g09d2


From b830a5dc8df82886a9304ebe02fe93cdf1c0b521 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 28 Jul 2022 14:10:32 -0400
Subject: fixed video thumbnail generation to have a lock that prevents
 multiple attempts to create the thumbnails at the same time.  fixed
 ImageUpload to catch resize errors and not crash the server.

---
 .../collections/CollectionStackedTimeline.tsx      |   9 +-
 src/client/views/nodes/VideoBox.tsx                |  16 +-
 src/server/ApiManagers/UploadManager.ts            | 556 +++++++++++----------
 3 files changed, 297 insertions(+), 284 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 714d8421a..52126884a 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 } from '../../../fields/Doc';
+import { Doc, DocListCast, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -289,10 +289,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
             this._hoverTime = this.toTimeline(clientX - rect.x, rect.width);
             if (this.dataDoc.thumbnails) {
                 const nearest = Math.floor((this._hoverTime / this.props.rawDuration) * VideoBox.numThumbnails);
-                const thumbnails = Cast(this.dataDoc.thumbnails, listSpec('string'), []);
-                const imgField = thumbnails && thumbnails.length > 0 ? new ImageField(thumbnails[nearest]) : new ImageField('');
-                const src = imgField && imgField.url.href ? imgField.url.href.replace('.png', '_s.png') : '';
-                this._thumbnail = src ? src : undefined;
+                const thumbnails = StrListCast(this.dataDoc.thumbnails);
+                const imgField = thumbnails?.length > 0 ? new ImageField(thumbnails[nearest]) : undefined;
+                this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_s.png') : undefined;
             }
         }
     };
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 681f7c5b2..4af4d2020 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -418,31 +418,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // extracts video thumbnails and saves them as field of doc
     getVideoThumbnails = () => {
-        const video = document.createElement('video');
+        if (this.dataDoc.thumbnails !== undefined) return;
+        this.dataDoc.thumbnails = new List<string>();
         const thumbnailPromises: Promise<any>[] = [];
+        const video = document.createElement('video');
 
-        video.onloadedmetadata = () => {
-            video.currentTime = 0;
-        };
+        video.onloadedmetadata = () => (video.currentTime = 0);
 
         video.onseeked = () => {
             const canvas = document.createElement('canvas');
             canvas.height = video.videoHeight;
             canvas.width = video.videoWidth;
             const ctx = canvas.getContext('2d');
-            ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
+            ctx?.drawImage(video, 0, 0, canvas.width, canvas.height, 0, 0, 25, 25);
             const imgUrl = canvas.toDataURL();
             const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
             const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
             const filename = basename(encodedFilename);
-            thumbnailPromises.push(VideoBox.convertDataUri(imgUrl, filename));
+            thumbnailPromises?.push(VideoBox.convertDataUri(imgUrl, filename, true));
             const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
             if (newTime < video.duration) {
                 video.currentTime = newTime;
             } else {
-                Promise.all(thumbnailPromises).then(thumbnails => {
-                    this.dataDoc.thumbnails = new List<string>(thumbnails);
-                });
+                Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc.thumbnails = new List<string>(thumbnails)));
             }
         };
 
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 332ba3d35..787e331c5 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -1,291 +1,308 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method, _success } from "../RouteManager";
+import ApiManager, { Registration } from './ApiManager';
+import { Method, _success } from '../RouteManager';
 import * as formidable from 'formidable';
 import v4 = require('uuid/v4');
 const AdmZip = require('adm-zip');
-import { extname, basename, dirname, } from 'path';
-import { createReadStream, createWriteStream, unlink, writeFile } from "fs";
-import { publicDirectory, filesDirectory } from "..";
-import { Database } from "../database";
-import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
+import { extname, basename, dirname } from 'path';
+import { createReadStream, createWriteStream, unlink, writeFile } from 'fs';
+import { publicDirectory, filesDirectory } from '..';
+import { Database } from '../database';
+import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
 import * as sharp from 'sharp';
-import { AcceptableMedia, Upload } from "../SharedMediaTypes";
-import { normalize } from "path";
-import RouteSubscriber from "../RouteSubscriber";
+import { AcceptableMedia, Upload } from '../SharedMediaTypes';
+import { normalize } from 'path';
+import RouteSubscriber from '../RouteSubscriber';
 const imageDataUri = require('image-data-uri');
-import { SolrManager } from "./SearchManager";
+import { SolrManager } from './SearchManager';
 const fs = require('fs');
 
 export enum Directory {
-      parsed_files = "parsed_files",
-      images = "images",
-      videos = "videos",
-      pdfs = "pdfs",
-      text = "text",
-      pdf_thumbnails = "pdf_thumbnails",
-      audio = "audio",
-      csv = "csv",
+    parsed_files = 'parsed_files',
+    images = 'images',
+    videos = 'videos',
+    pdfs = 'pdfs',
+    text = 'text',
+    pdf_thumbnails = 'pdf_thumbnails',
+    audio = 'audio',
+    csv = 'csv',
 }
 
 export function serverPathToFile(directory: Directory, filename: string) {
-      return normalize(`${filesDirectory}/${directory}/${filename}`);
+    return normalize(`${filesDirectory}/${directory}/${filename}`);
 }
 
 export function pathToDirectory(directory: Directory) {
-      return normalize(`${filesDirectory}/${directory}`);
+    return normalize(`${filesDirectory}/${directory}`);
 }
 
 export function clientPathToFile(directory: Directory, filename: string) {
-      return `/files/${directory}/${filename}`;
+    return `/files/${directory}/${filename}`;
 }
 
 export default class UploadManager extends ApiManager {
-
     protected initialize(register: Registration): void {
-          
-            register({
-                  method: Method.POST,
-                  subscription: "/concatVideos",
-              secureHandler: async ({ req, res }) => {
-                    // req.body contains the array of server paths to the videos
-                    _success(res, await DashUploadUtils.concatVideos(req.body));
-                  }
-            });
-
-            register({
-                  method: Method.POST,
-                  subscription: "/uploadFormData",
-                  secureHandler: async ({ req, res }) => {
-                        const form = new formidable.IncomingForm();
-                        form.keepExtensions = true;
-                        form.uploadDir = pathToDirectory(Directory.parsed_files);
-                        return new Promise<void>(resolve => {
-                            form.parse(req, async (_err, _fields, files) => {
-                                    const results: Upload.FileResponse[] = [];
-                                    for (const key in files) {
-                                          const f = files[key];
-                                          if (!Array.isArray(f)) {
-                                                const result = await DashUploadUtils.upload(f);
-                                                result && !(result.result instanceof Error) && results.push(result);
-                                          }
-                                    }
-                                    _success(res, results);
-                                    resolve();
-                              });
-                        });
-                  }
-            });
-
-            register({
-                  method: Method.POST,
-                  subscription: "/uploadYoutubeVideo",
-                  secureHandler: async ({ req, res }) => {
-                        //req.readableBuffer.head.data
-                        return new Promise<void>(async resolve => {
-                              req.addListener("data", async (args) => {
-                                    console.log(args);
-                                    const payload = String.fromCharCode.apply(String, args);
-                                    const videoId = JSON.parse(payload).videoId;
-                                    const results: Upload.FileResponse[] = [];
-                                    const result = await DashUploadUtils.uploadYoutube(videoId);
-                                    result && !(result.result instanceof Error) && results.push(result);
-                                    _success(res, results);
-                                    resolve();
-                              });
-                        });
-                  }
-            });
+        register({
+            method: Method.POST,
+            subscription: '/concatVideos',
+            secureHandler: async ({ req, res }) => {
+                // req.body contains the array of server paths to the videos
+                _success(res, await DashUploadUtils.concatVideos(req.body));
+            },
+        });
 
-            register({
-                  method: Method.POST,
-                  subscription: new RouteSubscriber("youtubeScreenshot"),
-                  secureHandler: async ({ req, res }) => {
-                        const { id, timecode } = req.body;
-                        const convert = (raw: string) => {
-                              const number = Math.floor(Number(raw));
-                              const seconds = number % 60;
-                              const minutes = (number - seconds) / 60;
-                              return `${minutes}m${seconds}s`;
-                        };
-                        const suffix = timecode ? `&t=${convert(timecode)}` : ``;
-                        const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
-                        const buffer = await captureYoutubeScreenshot(targetUrl);
-                        if (!buffer) {
-                              return res.send();
+        register({
+            method: Method.POST,
+            subscription: '/uploadFormData',
+            secureHandler: async ({ req, res }) => {
+                const form = new formidable.IncomingForm();
+                form.keepExtensions = true;
+                form.uploadDir = pathToDirectory(Directory.parsed_files);
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, _fields, files) => {
+                        const results: Upload.FileResponse[] = [];
+                        for (const key in files) {
+                            const f = files[key];
+                            if (!Array.isArray(f)) {
+                                const result = await DashUploadUtils.upload(f);
+                                result && !(result.result instanceof Error) && results.push(result);
+                            }
                         }
-                        const resolvedName = `youtube_capture_${id}_${suffix}.png`;
-                        const resolvedPath = serverPathToFile(Directory.images, resolvedName);
-                        return new Promise<void>(resolve => {
-                              writeFile(resolvedPath, buffer, async error => {
-                                    if (error) {
-                                          return res.send();
-                                    }
-                                    await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
-                                    res.send({
-                                          accessPaths: {
-                                                agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
-                                          }
-                                    } as Upload.FileInformation);
-                                    resolve();
-                              });
-                        });
-                  }
-            });
+                        _success(res, results);
+                        resolve();
+                    });
+                });
+            },
+        });
 
-            register({
-                  method: Method.POST,
-                  subscription: "/uploadRemoteImage",
-                  secureHandler: async ({ req, res }) => {
+        register({
+            method: Method.POST,
+            subscription: '/uploadYoutubeVideo',
+            secureHandler: async ({ req, res }) => {
+                //req.readableBuffer.head.data
+                return new Promise<void>(async resolve => {
+                    req.addListener('data', async args => {
+                        console.log(args);
+                        const payload = String.fromCharCode.apply(String, args);
+                        const videoId = JSON.parse(payload).videoId;
+                        const results: Upload.FileResponse[] = [];
+                        const result = await DashUploadUtils.uploadYoutube(videoId);
+                        result && !(result.result instanceof Error) && results.push(result);
+                        _success(res, results);
+                        resolve();
+                    });
+                });
+            },
+        });
 
-                        const { sources } = req.body;
-                        if (Array.isArray(sources)) {
-                              const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
-                              return res.send(results);
+        register({
+            method: Method.POST,
+            subscription: new RouteSubscriber('youtubeScreenshot'),
+            secureHandler: async ({ req, res }) => {
+                const { id, timecode } = req.body;
+                const convert = (raw: string) => {
+                    const number = Math.floor(Number(raw));
+                    const seconds = number % 60;
+                    const minutes = (number - seconds) / 60;
+                    return `${minutes}m${seconds}s`;
+                };
+                const suffix = timecode ? `&t=${convert(timecode)}` : ``;
+                const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
+                const buffer = await captureYoutubeScreenshot(targetUrl);
+                if (!buffer) {
+                    return res.send();
+                }
+                const resolvedName = `youtube_capture_${id}_${suffix}.png`;
+                const resolvedPath = serverPathToFile(Directory.images, resolvedName);
+                return new Promise<void>(resolve => {
+                    writeFile(resolvedPath, buffer, async error => {
+                        if (error) {
+                            return res.send();
                         }
-                        res.send();
-                  }
-            });
+                        await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
+                        res.send({
+                            accessPaths: {
+                                agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName),
+                            },
+                        } as Upload.FileInformation);
+                        resolve();
+                    });
+                });
+            },
+        });
 
-            register({
-                  method: Method.POST,
-                  subscription: "/uploadDoc",
-                  secureHandler: ({ req, res }) => {
+        register({
+            method: Method.POST,
+            subscription: '/uploadRemoteImage',
+            secureHandler: async ({ req, res }) => {
+                const { sources } = req.body;
+                if (Array.isArray(sources)) {
+                    const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
+                    return res.send(results);
+                }
+                res.send();
+            },
+        });
 
-                        const form = new formidable.IncomingForm();
-                        form.keepExtensions = true;
-                        // let path = req.body.path;
-                        const ids: { [id: string]: string } = {};
-                        let remap = true;
-                        const getId = (id: string): string => {
-                              if (!remap) return id;
-                              if (id.endsWith("Proto")) return id;
-                              if (id in ids) {
-                                    return ids[id];
-                              } else {
-                                    return ids[id] = v4();
-                              }
-                        };
-                        const mapFn = (doc: any) => {
-                              if (doc.id) {
-                                    doc.id = getId(doc.id);
-                              }
-                              for (const key in doc.fields) {
-                                    if (!doc.fields.hasOwnProperty(key)) { continue; }
-                                    const field = doc.fields[key];
-                                    if (field === undefined || field === null) { continue; }
+        register({
+            method: Method.POST,
+            subscription: '/uploadDoc',
+            secureHandler: ({ req, res }) => {
+                const form = new formidable.IncomingForm();
+                form.keepExtensions = true;
+                // let path = req.body.path;
+                const ids: { [id: string]: string } = {};
+                let remap = true;
+                const getId = (id: string): string => {
+                    if (!remap) return id;
+                    if (id.endsWith('Proto')) return id;
+                    if (id in ids) {
+                        return ids[id];
+                    } else {
+                        return (ids[id] = v4());
+                    }
+                };
+                const mapFn = (doc: any) => {
+                    if (doc.id) {
+                        doc.id = getId(doc.id);
+                    }
+                    for (const key in doc.fields) {
+                        if (!doc.fields.hasOwnProperty(key)) {
+                            continue;
+                        }
+                        const field = doc.fields[key];
+                        if (field === undefined || field === null) {
+                            continue;
+                        }
 
-                                    if (field.__type === "Doc") {
-                                          mapFn(field);
-                                    } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                                          field.fieldId = getId(field.fieldId);
-                                    } else if (field.__type === "script" || field.__type === "computed") {
-                                          if (field.captures) {
-                                                field.captures.fieldId = getId(field.captures.fieldId);
-                                          }
-                                    } else if (field.__type === "list") {
-                                          mapFn(field);
-                                    } else if (typeof field === "string") {
-                                          const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
-                                          doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
-                                                return `${p1}${getId(p2)}"`;
-                                          });
-                                    } else if (field.__type === "RichTextField") {
-                                          const re = /("href"\s*:\s*")(.*?)"/g;
-                                          field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
-                                                return `${p1}${getId(p2)}"`;
-                                          });
-                                    }
-                              }
-                        };
-                        return new Promise<void>(resolve => {
-                              form.parse(req, async (_err, fields, files) => {
-                                    remap = fields.remap !== "false";
-                                    let id: string = "";
+                        if (field.__type === 'Doc') {
+                            mapFn(field);
+                        } else if (field.__type === 'proxy' || field.__type === 'prefetch_proxy') {
+                            field.fieldId = getId(field.fieldId);
+                        } else if (field.__type === 'script' || field.__type === 'computed') {
+                            if (field.captures) {
+                                field.captures.fieldId = getId(field.captures.fieldId);
+                            }
+                        } else if (field.__type === 'list') {
+                            mapFn(field);
+                        } else if (typeof field === 'string') {
+                            const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+                            doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+                                return `${p1}${getId(p2)}"`;
+                            });
+                        } else if (field.__type === 'RichTextField') {
+                            const re = /("href"\s*:\s*")(.*?)"/g;
+                            field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+                                return `${p1}${getId(p2)}"`;
+                            });
+                        }
+                    }
+                };
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, fields, files) => {
+                        remap = fields.remap !== 'false';
+                        let id: string = '';
+                        try {
+                            for (const name in files) {
+                                const f = files[name];
+                                const path_2 = Array.isArray(f) ? '' : f.path;
+                                const zip = new AdmZip(path_2);
+                                zip.getEntries().forEach((entry: any) => {
+                                    if (!entry.entryName.startsWith('files/')) return;
+                                    let directory = dirname(entry.entryName) + '/';
+                                    const extension = extname(entry.entryName);
+                                    const base = basename(entry.entryName).split('.')[0];
                                     try {
-                                          for (const name in files) {
-                                                const f = files[name];
-                                                const path_2 = Array.isArray(f) ? "" : f.path;
-                                                const zip = new AdmZip(path_2);
-                                                zip.getEntries().forEach((entry: any) => {
-                                                      if (!entry.entryName.startsWith("files/")) return;
-                                                      let directory = dirname(entry.entryName) + "/";
-                                                      const extension = extname(entry.entryName);
-                                                      const base = basename(entry.entryName).split(".")[0];
-                                                      try {
-                                                            zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
-                                                            directory = "/" + directory;
-
-                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
-                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
-                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
-                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
-                                                      } catch (e) {
-                                                            console.log(e);
-                                                      }
-                                                });
-                                                const json = zip.getEntry("doc.json");
-                                                try {
-                                                      const data = JSON.parse(json.getData().toString("utf8"));
-                                                      const datadocs = data.docs;
-                                                      id = getId(data.id);
-                                                      const docs = Object.keys(datadocs).map(key => datadocs[key]);
-                                                      docs.forEach(mapFn);
-                                                      await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
-                                                            Database.Instance.replace(doc.id, doc, (err, r) => {
-                                                                  err && console.log(err);
-                                                                  res();
-                                                            }, true);
-                                                      })));
-                                                } catch (e) { console.log(e); }
-                                                unlink(path_2, () => { });
-                                          }
-                                          SolrManager.update();
-                                          res.send(JSON.stringify(id || "error"));
-                                    } catch (e) { console.log(e); }
-                                    resolve();
-                              });
-                        });
-                  }
-            });
-
-            register({
-                  method: Method.POST,
-                  subscription: "/inspectImage",
-                  secureHandler: async ({ req, res }) => {
+                                        zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+                                        directory = '/' + directory;
 
-                        const { source } = req.body;
-                        if (typeof source === "string") {
-                              return res.send(await DashUploadUtils.InspectImage(source));
+                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_o' + extension));
+                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_s' + extension));
+                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_m' + extension));
+                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_l' + extension));
+                                    } catch (e) {
+                                        console.log(e);
+                                    }
+                                });
+                                const json = zip.getEntry('doc.json');
+                                try {
+                                    const data = JSON.parse(json.getData().toString('utf8'));
+                                    const datadocs = data.docs;
+                                    id = getId(data.id);
+                                    const docs = Object.keys(datadocs).map(key => datadocs[key]);
+                                    docs.forEach(mapFn);
+                                    await Promise.all(
+                                        docs.map(
+                                            (doc: any) =>
+                                                new Promise<void>(res => {
+                                                    Database.Instance.replace(
+                                                        doc.id,
+                                                        doc,
+                                                        (err, r) => {
+                                                            err && console.log(err);
+                                                            res();
+                                                        },
+                                                        true
+                                                    );
+                                                })
+                                        )
+                                    );
+                                } catch (e) {
+                                    console.log(e);
+                                }
+                                unlink(path_2, () => {});
+                            }
+                            SolrManager.update();
+                            res.send(JSON.stringify(id || 'error'));
+                        } catch (e) {
+                            console.log(e);
                         }
-                        res.send({});
-                  }
-            });
+                        resolve();
+                    });
+                });
+            },
+        });
+
+        register({
+            method: Method.POST,
+            subscription: '/inspectImage',
+            secureHandler: async ({ req, res }) => {
+                const { source } = req.body;
+                if (typeof source === 'string') {
+                    return res.send(await DashUploadUtils.InspectImage(source));
+                }
+                res.send({});
+            },
+        });
 
         register({
             method: Method.POST,
-            subscription: "/uploadURI",
+            subscription: '/uploadURI',
             secureHandler: ({ req, res }) => {
                 const uri = req.body.uri;
                 const filename = req.body.name;
                 const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
                 const deleteFiles = req.body.replaceRootFilename;
                 if (!uri || !filename) {
-                    res.status(401).send("incorrect parameters specified");
+                    res.status(401).send('incorrect parameters specified');
                     return;
                 }
                 if (deleteFiles) {
-                    const path = serverPathToFile(Directory.images, "");
+                    const path = serverPathToFile(Directory.images, '');
                     const regex = new RegExp(`${deleteFiles}.*`);
-                    fs.readdirSync(path).filter((f: any) => regex.test(f)).map((f: any) => fs.unlinkSync(path + f));
+                    fs.readdirSync(path)
+                        .filter((f: any) => regex.test(f))
+                        .map((f: any) => fs.unlinkSync(path + f));
                 }
                 return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
                     const ext = extname(savedName).toLowerCase();
                     const { pngs, jpgs } = AcceptableMedia;
-                    const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }] : [
-                        { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Small },
-                        { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium },
-                        { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Large },
-                    ];
+                    const resizers = !origSuffix
+                        ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium }]
+                        : [
+                              { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Small },
+                              { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Medium },
+                              { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: SizeSuffix.Large },
+                          ];
                     let isImage = false;
                     if (pngs.includes(ext)) {
                         resizers.forEach(element => {
@@ -301,49 +318,48 @@ export default class UploadManager extends ApiManager {
                     if (isImage) {
                         resizers.forEach(resizer => {
                             const path = serverPathToFile(Directory.images, InjectSize(filename, resizer.suffix) + ext);
-                            createReadStream(savedName).on("error", e => console.log("Resizing read:" + e))
+                            createReadStream(savedName)
+                                .on('error', e => console.log('Resizing read:' + e))
                                 .pipe(resizer.resizer)
-                                .pipe(createWriteStream(path).on("error", e => console.log("Resizing write: " + e)));
+                                .on('error', e => console.log('Resizing write: ' + e))
+                                .pipe(createWriteStream(path).on('error', e => console.log('Resizing write: ' + e)));
                         });
-
-                              }
-                              res.send(clientPathToFile(Directory.images, filename + ext));
-                        });
-                  }
-            });
-
-      }
-
+                    }
+                    res.send(clientPathToFile(Directory.images, filename + ext));
+                });
+            },
+        });
+    }
 }
 function delay(ms: number) {
-      return new Promise(resolve => setTimeout(resolve, ms));
+    return new Promise(resolve => setTimeout(resolve, ms));
 }
 /**
  * On success, returns a buffer containing the bytes of a screenshot
  * of the video (optionally, at a timecode) specified by @param targetUrl.
- * 
+ *
  * On failure, returns undefined.
  */
 async function captureYoutubeScreenshot(targetUrl: string) {
-      // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
-      // const page = await browser.newPage();
-      // // await page.setViewport({ width: 1920, height: 1080 });
+    // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
+    // const page = await browser.newPage();
+    // // await page.setViewport({ width: 1920, height: 1080 });
 
-      // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
+    // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
 
-      // const videoPlayer = await page.$('.html5-video-player');
-      // videoPlayer && await page.focus("video");
-      // await delay(7000);
-      // const ad = await page.$('.ytp-ad-skip-button-text');
-      // await ad?.click();
-      // await videoPlayer?.click();
-      // await delay(1000);
-      // // hide youtube player controls.
-      // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
+    // const videoPlayer = await page.$('.html5-video-player');
+    // videoPlayer && await page.focus("video");
+    // await delay(7000);
+    // const ad = await page.$('.ytp-ad-skip-button-text');
+    // await ad?.click();
+    // await videoPlayer?.click();
+    // await delay(1000);
+    // // hide youtube player controls.
+    // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
 
-      // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
-      // await browser.close();
+    // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
+    // await browser.close();
 
-      // return buffer;
-      return null;
-}
\ No newline at end of file
+    // return buffer;
+    return null;
+}
-- 
cgit v1.2.3-70-g09d2


From fe5b49705069eebccec22e6cad29b007a60e3bec Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 28 Jul 2022 14:22:58 -0400
Subject: from last

---
 src/client/views/collections/CollectionStackedTimeline.tsx |  2 +-
 src/client/views/nodes/VideoBox.tsx                        | 11 ++++-------
 2 files changed, 5 insertions(+), 8 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 52126884a..48e3abbc7 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -291,7 +291,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                 const nearest = Math.floor((this._hoverTime / this.props.rawDuration) * VideoBox.numThumbnails);
                 const thumbnails = StrListCast(this.dataDoc.thumbnails);
                 const imgField = thumbnails?.length > 0 ? new ImageField(thumbnails[nearest]) : undefined;
-                this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_s.png') : undefined;
+                this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_m.png') : undefined;
             }
         }
     };
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4af4d2020..a3d501153 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -427,15 +427,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         video.onseeked = () => {
             const canvas = document.createElement('canvas');
-            canvas.height = video.videoHeight;
-            canvas.width = video.videoWidth;
-            const ctx = canvas.getContext('2d');
-            ctx?.drawImage(video, 0, 0, canvas.width, canvas.height, 0, 0, 25, 25);
-            const imgUrl = canvas.toDataURL();
+            canvas.height = 100;
+            canvas.width = 100;
+            canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
             const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
             const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
-            const filename = basename(encodedFilename);
-            thumbnailPromises?.push(VideoBox.convertDataUri(imgUrl, filename, true));
+            thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
             const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
             if (newTime < video.duration) {
                 video.currentTime = newTime;
-- 
cgit v1.2.3-70-g09d2


From 0178de4ab9ffd11630b700f9c02468b74beabd14 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 17 Aug 2022 12:31:26 -0400
Subject: fixed dragging docs on web and pdf to work better by temporarily
 adding transparent docs to the opaque layer so they can get drop events.  
 cleaned eraser and pen interaction code and made erasing strokes work faster
 and avoid hanging by not intersecting strokes that are already partially
 deleted.

---
 src/Utils.ts                                       |   3 +-
 src/client/documents/Documents.ts                  |  26 +---
 src/client/views/collections/CollectionSubView.tsx |  12 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 140 ++++++---------------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   5 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/VideoBox.tsx                |  11 +-
 src/client/views/nodes/WebBox.scss                 |  17 +--
 src/client/views/nodes/WebBox.tsx                  | 110 ++++++++--------
 src/client/views/pdf/PDFViewer.tsx                 |  75 +++++------
 10 files changed, 153 insertions(+), 252 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 528a429d0..9e002ebd4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -111,6 +111,7 @@ export namespace Utils {
 
     const isTransparentFunctionHack = 'isTransparent(__value__)';
     export const noRecursionHack = '__noRecursion';
+    export const noDragsDocFilter = 'noDragDocs:any:check';
     export function IsRecursiveFilter(val: string) {
         return !val.includes(noRecursionHack);
     }
@@ -125,7 +126,7 @@ export namespace Utils {
         // bcz: isTransparent(__value__) is a hack.  it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
         return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`; // bcz: hack.  noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
     }
-    export function PropUnsetFilter(prop: string) {
+    export function IsPropUnsetFilter(prop: string) {
         return `${prop}:any,${noRecursionHack}:unset`;
     }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cf9ed43e1..e579bfd8a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1143,30 +1143,6 @@ export namespace Docs {
 }
 
 export namespace DocUtils {
-    export function Excluded(d: Doc, docFilters: string[]) {
-        const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
-        docFilters.forEach(filter => {
-            const fields = filter.split(':');
-            const key = fields[0];
-            const value = fields[1];
-            const modifiers = fields[2];
-            if (!filterFacets[key]) {
-                filterFacets[key] = {};
-            }
-            filterFacets[key][value] = modifiers;
-        });
-
-        if (d.z) return false;
-        for (const facetKey of Object.keys(filterFacets)) {
-            const facet = filterFacets[facetKey];
-            const xs = Object.keys(facet).filter(value => facet[value] === 'x');
-            const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value));
-            if (failsNotEqualFacets) {
-                return true;
-            }
-        }
-        return false;
-    }
     /**
      * @param docs
      * @param docFilters
@@ -1200,7 +1176,7 @@ export namespace DocUtils {
                       return false;
                   }
 
-                  for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies')) {
+                  for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(':')[0])) {
                       const facet = filterFacets[facetKey];
 
                       // facets that match some value in the field of the document (e.g. some text field)
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5479929bd..e33bb77de 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -88,11 +88,11 @@ export function CollectionSubView<X>(moreProps?: X) {
         }
         collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters);
         collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []);
+        // child filters apply to the descendants of the documents in this collection
         childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
+        // unrecursive filters apply to the documents in the collection, but no their children.  See Utils.noRecursionHack
         unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
         childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()];
-        IsFiltered = () =>
-            this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined;
         searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
         @computed.struct get childDocs() {
             TraceMobx();
@@ -122,13 +122,11 @@ export function CollectionSubView<X>(moreProps?: X) {
                 return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one
             }
 
-            // console.log(Doc.ActiveDashboard._docFilters);
-            // if (!this.props.Document._docFilters && this.props.Document.currentFilter) {
-            //     (this.props.Document.currentFilter as Doc).filterBoolean = (this.props.ContainingCollectionDoc?.currentFilter as Doc)?.filterBoolean;
-            // }
             const docsforFilter: Doc[] = [];
             childDocs.forEach(d => {
-                // if (DocUtils.Excluded(d, docFilters)) return;
+                // dragging facets
+                const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter));
+                if (dragged && DragManager.docsBeingDragged.includes(d)) return false;
                 let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0;
                 if (notFiltered) {
                     notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f3074543b..45a5e30ff 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -109,8 +109,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _hLines: number[] | undefined;
     @observable _vLines: number[] | undefined;
     @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
-    @observable _pullCoords: number[] = [0, 0];
-    @observable _pullDirection: string = '';
     @observable _showAnimTimeline = false;
     @observable _clusterSets: Doc[][] = [];
     @observable _deleteList: DocumentView[] = [];
@@ -465,20 +463,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     getClusterColor = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => {
         let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props'  used to be renderDepth + 1
-        if (property !== StyleProp.BackgroundColor) return styleProp;
-        const cluster = NumCast(doc?.cluster);
-        if (this.Document._useClusters) {
-            if (this._clusterSets.length <= cluster) {
-                setTimeout(() => doc && this.updateCluster(doc));
-            } else {
-                // choose a cluster color from a palette
-                const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
-                styleProp = colors[cluster % colors.length];
-                const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
-                // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
-                set?.map(s => (styleProp = StrCast(s.backgroundColor)));
-            }
-        } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray";
+        switch (property) {
+            case StyleProp.BackgroundColor:
+                const cluster = NumCast(doc?.cluster);
+                if (this.Document._useClusters) {
+                    if (this._clusterSets.length <= cluster) {
+                        setTimeout(() => doc && this.updateCluster(doc));
+                    } else {
+                        // choose a cluster color from a palette
+                        const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
+                        styleProp = colors[cluster % colors.length];
+                        const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
+                        // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
+                        set?.map(s => (styleProp = StrCast(s.backgroundColor)));
+                    }
+                }
+                break;
+        }
         return styleProp;
     };
 
@@ -523,17 +524,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.Pen:
                         break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
                     case InkTool.Eraser:
-                        document.addEventListener('pointermove', this.onEraserMove);
-                        document.addEventListener('pointerup', this.onEraserUp);
                         this._batch = UndoManager.StartBatch('collectionErase');
-                        e.stopPropagation();
-                        e.preventDefault();
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.None:
                         if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
-                            document.addEventListener('pointermove', this.onPointerMove);
-                            document.addEventListener('pointerup', this.onPointerUp);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
                         }
                         break;
                 }
@@ -698,23 +695,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     @action
     onEraserUp = (e: PointerEvent): void => {
-        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
-            document.removeEventListener('pointermove', this.onEraserMove);
-            document.removeEventListener('pointerup', this.onEraserUp);
-            this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
-            this._deleteList = [];
-            this._batch?.end();
-        }
-    };
-
-    @action
-    onPointerUp = (e: PointerEvent): void => {
-        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
-            document.removeEventListener('pointermove', this.onPointerMove);
-            document.removeEventListener('pointerup', this.onPointerUp);
-            this.removeMoveListeners();
-            this.removeEndListeners();
-        }
+        this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+        this._deleteList = [];
+        this._batch?.end();
     };
 
     onClick = (e: React.MouseEvent) => {
@@ -752,46 +735,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      *  However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
      */
     @action
-    onEraserMove = (e: PointerEvent) => {
+    onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
         const currPoint = { X: e.clientX, Y: e.clientY };
-        this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => {
+        this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
             if (!this._deleteList.includes(intersect.inkView)) {
                 this._deleteList.push(intersect.inkView);
                 SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || '1');
                 SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black');
                 // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                !e.shiftKey &&
+                if (!e.shiftKey) {
                     this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
                         GestureOverlay.Instance.dispatchGesture(
                             GestureUtils.Gestures.Stroke,
                             segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
                         )
                     );
+                }
                 // Lower ink opacity to give the user a visual indicator of deletion.
                 intersect.inkView.layoutDoc.opacity = 0.5;
+                intersect.inkView.layoutDoc.dontIntersect = true;
             }
         });
-        this._lastX = currPoint.X;
-        this._lastY = currPoint.Y;
-
-        e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-        e.preventDefault();
+        return false;
     };
 
     @action
-    onPointerMove = (e: PointerEvent): void => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
+    onPointerMove = (e: PointerEvent): boolean => {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false;
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             Doc.ActiveTool = InkTool.None;
             if (this.props.isContentActive(true)) e.stopPropagation();
         } else if (!e.cancelBubble) {
             if (this.tryDragCluster(e, this._hitCluster)) {
-                document.removeEventListener('pointermove', this.onPointerMove);
-                document.removeEventListener('pointerup', this.onPointerUp);
+                return true;
             } else this.pan(e);
-            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-            e.preventDefault();
         }
+        return false;
     };
 
     /**
@@ -801,6 +780,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
         const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
         const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
+
         return this.childDocs
             .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
             .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
@@ -888,7 +868,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const tVals: number[] = [];
         // Iterating through all ink strokes in the current freeform collection.
         this.childDocs
-            .filter(doc => doc.type === DocumentType.INK)
+            .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
             .forEach(doc => {
                 const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
                 const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
@@ -919,7 +899,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
                         e.preventDefault();
                         document.removeEventListener('pointermove', this.onPointerMove);
-                        document.removeEventListener('pointerup', this.onPointerUp);
                         return;
                     }
                     // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected'
@@ -967,13 +946,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
                         // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
 
-                        if (!this._pullDirection) {
-                            // if we are not bezel movement
-                            this.pan({ clientX: centerX, clientY: centerY });
-                        } else {
-                            this._pullCoords = [centerX, centerY];
-                        }
-
                         this._lastX = centerX;
                         this._lastY = centerY;
                     }
@@ -998,24 +970,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
                 this._lastX = centerX;
                 this._lastY = centerY;
-                const screenBox = this._mainCont?.getBoundingClientRect();
-
-                // determine if we are using a bezel movement
-                if (screenBox) {
-                    if (screenBox.right - centerX < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'right';
-                    } else if (centerX - screenBox.left < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'left';
-                    } else if (screenBox.bottom - centerY < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'bottom';
-                    } else if (centerY - screenBox.top < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'top';
-                    }
-                }
 
                 this.removeMoveListeners();
                 this.addMoveListeners();
@@ -1027,19 +981,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     cleanUpInteractions = () => {
-        switch (this._pullDirection) {
-            case 'left':
-            case 'right':
-            case 'top':
-            case 'bottom':
-                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: 'New Collection' }), this._pullDirection);
-        }
-
-        this._pullDirection = '';
-        this._pullCoords = [0, 0];
-
-        document.removeEventListener('pointermove', this.onPointerMove);
-        document.removeEventListener('pointerup', this.onPointerUp);
         this.removeMoveListeners();
         this.removeEndListeners();
     };
@@ -1380,7 +1321,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const { z, color, zIndex } = params.pair.layout;
         const { x, y, opacity } =
             this.Document._currentFrame === undefined
-                ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) }
+                ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.childOpacity ? this.props.childOpacity() : this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) }
                 : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame));
         return {
             x: NumCast(x),
@@ -1915,7 +1856,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 getContainerTransform={this.getContainerTransform}
                 getTransform={this.getTransform}
                 isAnnotationOverlay={this.isAnnotationOverlay}>
-                <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}>
+                <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}>
                     {this.layoutDoc._backgroundGridShow ? (
                         <div>
                             <CollectionFreeFormBackgroundGrid // bcz : UGHH  don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!?
@@ -1996,15 +1937,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 {this._firstRender ? this.placeholder : this.marqueeView}
                 {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
 
-                <div
-                    className={'pullpane-indicator'}
-                    style={{
-                        display: this._pullDirection ? 'block' : 'none',
-                        top: clientRect ? (this._pullDirection === 'bottom' ? this._pullCoords[1] - clientRect.y : 0) : 'auto',
-                        left: clientRect ? (this._pullDirection === 'right' ? this._pullCoords[0] - clientRect.x : 0) : 'auto',
-                        width: clientRect ? (this._pullDirection === 'left' ? this._pullCoords[0] - clientRect.left : this._pullDirection === 'right' ? clientRect.right - this._pullCoords[0] : clientRect.width) : 0,
-                        height: clientRect ? (this._pullDirection === 'top' ? this._pullCoords[1] - clientRect.top : this._pullDirection === 'bottom' ? clientRect.bottom - this._pullCoords[1] : clientRect.height) : 0,
-                    }}></div>
                 {
                     // uncomment to show snap lines
                     <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 78f351f4f..e19e2d525 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable, trace } from 'mobx';
+import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
@@ -6,13 +6,12 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DashColor, numberRange, OmitKeys } from '../../../Utils';
+import { numberRange } from '../../../Utils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { Transform } from '../../util/Transform';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { DocComponent } from '../DocComponent';
-import { InkingStroke } from '../InkingStroke';
 import { StyleProp } from '../StyleProvider';
 import './CollectionFreeFormDocumentView.scss';
 import { DocumentView, DocumentViewProps } from './DocumentView';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bcc55eab4..172adcafe 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -970,7 +970,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     collectionFilters = () => StrListCast(this.props.Document._docFilters);
     collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
     @computed get showFilterIcon() {
-        return this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined;
+        return this.collectionFilters().length || this.collectionRangeDocFilters().length
+            ? 'hasFilter'
+            : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length
+            ? 'inheritsFilter'
+            : undefined;
     }
     rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
     panelHeight = () => this.props.PanelHeight() - this.headerMargin;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a3d501153..0ff15f93b 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -831,16 +831,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // stretches vertically or horizontally depending on video orientation so video fits full screen
     fullScreenSize() {
         if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
-            return { height: '100%' };
-        } else {
-            return { width: '100%' };
+            //prettier-ignore
+            return ({ height: '100%' });
         }
+        //prettier-ignore
+        return ({ width: '100%' });
     }
 
     // for zoom slider, sets timeline waveform zoom
-    zoom = (zoom: number) => {
-        this.timeline?.setZoom(zoom);
-    };
+    zoom = (zoom: number) => this.timeline?.setZoom(zoom);
 
     // plays link
     playLink = (doc: Doc) => {
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index d8dd074a5..85986ff27 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,9 +1,11 @@
-@import "../global/globalCssVariables.scss";
-
+@import '../global/globalCssVariables.scss';
 
 .webBox {
     height: 100%;
-    position: relative;
+    width: 100%;
+    top: 0;
+    left: 0;
+    position: absolute;
     display: flex;
 
     .webBox-sideResizer {
@@ -84,7 +86,6 @@
             background: none;
         }
 
-
         .webBox-overlayCont {
             position: absolute;
             width: calc(100% - 40px);
@@ -95,7 +96,7 @@
             justify-content: center;
             align-items: center;
             overflow: hidden;
-            transition: left .5s;
+            transition: left 0.5s;
             pointer-events: all;
 
             .webBox-searchBar {
@@ -158,7 +159,7 @@
             left: 0;
             cursor: text;
             padding: 15px;
-            height: 100%
+            height: 100%;
         }
 
         .webBox-cont {
@@ -235,7 +236,7 @@
         height: 25px;
         align-items: center;
 
-        >svg {
+        > svg {
             margin: auto;
         }
     }
@@ -257,4 +258,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index ca9f363c1..6c2e42f86 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as WebRequest from 'web-request';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -35,6 +35,7 @@ import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
 import './WebBox.scss';
 import React = require('react');
+import { DragManager } from '../../util/DragManager';
 const { CreateImage } = require('./WebBoxRenderer');
 const _global = (window /* browser */ || global) /* node */ as any;
 const htmlToText = require('html-to-text');
@@ -818,6 +819,56 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
     }
 
+    @computed get webpage() {
+        const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+        const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
+        const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
+        const renderAnnotations = (docFilters?: () => string[]) => (
+            <CollectionFreeFormView
+                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                renderDepth={this.props.renderDepth + 1}
+                isAnnotationOverlay={true}
+                fieldKey={this.annotationKey}
+                CollectionView={undefined}
+                setPreviewCursor={this.setPreviewCursor}
+                PanelWidth={this.panelWidth}
+                PanelHeight={this.panelHeight}
+                ScreenToLocalTransform={this.scrollXf}
+                NativeDimScaling={returnOne}
+                dropAction={'alias'}
+                docFilters={docFilters}
+                select={emptyFunction}
+                bringToFront={emptyFunction}
+                styleProvider={this.childStyleProvider}
+                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                removeDocument={this.removeDocument}
+                moveDocument={this.moveDocument}
+                addDocument={this.addDocument}
+                childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
+                pointerEvents={this.annotationPointerEvents}
+            />
+        );
+        return (
+            <div
+                className={'webBox-outerContent'}
+                ref={this._outerRef}
+                style={{
+                    height: `${100 / scale}%`,
+                    pointerEvents,
+                }}
+                onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe
+                onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
+                onPointerDown={this.onMarqueeDown}>
+                <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}>
+                    {this.content}
+                    {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>}
+                    {renderAnnotations(this.opaqueFilter)}
+                    {this.annotationLayer}
+                </div>
+            </div>
+        );
+    }
+
     @computed get searchUI() {
         return (
             <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}>
@@ -859,9 +910,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
     scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
-    basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')];
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
-    opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+    opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (doc.textInlineAnnotations) return 'none';
@@ -871,37 +921,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
     annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() ? 'all' : 'none');
     render() {
-        const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
         const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+        const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
-        const renderAnnotations = (docFilters?: () => string[]) => (
-            <CollectionFreeFormView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                renderDepth={this.props.renderDepth + 1}
-                isAnnotationOverlay={true}
-                fieldKey={this.annotationKey}
-                CollectionView={undefined}
-                setPreviewCursor={this.setPreviewCursor}
-                PanelWidth={this.panelWidth}
-                PanelHeight={this.panelHeight}
-                ScreenToLocalTransform={this.scrollXf}
-                NativeDimScaling={returnOne}
-                dropAction={'alias'}
-                docFilters={docFilters || this.basicFilter}
-                dontRenderDocuments={docFilters ? false : true}
-                select={emptyFunction}
-                bringToFront={emptyFunction}
-                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                removeDocument={this.removeDocument}
-                moveDocument={this.moveDocument}
-                addDocument={this.addDocument}
-                styleProvider={this.childStyleProvider}
-                childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
-                pointerEvents={this.annotationPointerEvents}
-            />
-        );
         return (
-            <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? 'none' : undefined }}>
+            <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}>
                 <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
                 <div
                     className="webBox-container"
@@ -911,27 +935,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         pointerEvents,
                     }}
                     onContextMenu={this.specificContextMenu}>
-                    <div
-                        className={'webBox-outerContent'}
-                        ref={this._outerRef}
-                        style={{
-                            height: `${100 / scale}%`,
-                            pointerEvents,
-                        }}
-                        onWheel={e => {
-                            e.stopPropagation();
-                            e.preventDefault();
-                        }} // block wheel events from propagating since they're handled by the iframe
-                        onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
-                        onPointerDown={this.onMarqueeDown}>
-                        <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}>
-                            {this.content}
-                            <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
-                            {renderAnnotations(this.opaqueFilter)}
-                            {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
-                            {this.annotationLayer}
-                        </div>
-                    </div>
+                    {this.webpage}
                     {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
                         <div style={{ transformOrigin: 'top left', transform: `scale(${1 / scale})` }}>
                             <MarqueeAnnotator
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 2c83082b7..a45edfbca 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -9,6 +9,7 @@ import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
+import { DragManager } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SharingManager } from '../../util/SharingManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -487,9 +488,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
     overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1));
     panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
     panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
-    basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')];
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
-    opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+    opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (doc.textInlineAnnotations) return 'none';
@@ -498,56 +498,43 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this.props.styleProvider?.(doc, props, property);
     };
 
-    renderAnnotations = (docFilters?: () => string[], dontRender?: boolean) => (
-        <CollectionFreeFormView
-            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-            isAnnotationOverlay={true}
-            fieldKey={this.props.fieldKey + '-annotations'}
-            setPreviewCursor={this.setPreviewCursor}
-            PanelHeight={this.panelHeight}
-            PanelWidth={this.panelWidth}
-            dropAction={'alias'}
-            select={emptyFunction}
-            bringToFront={emptyFunction}
-            docFilters={docFilters || this.basicFilter}
-            styleProvider={this.childStyleProvider}
-            dontRenderDocuments={dontRender}
-            CollectionView={undefined}
-            ScreenToLocalTransform={this.overlayTransform}
-            renderDepth={this.props.renderDepth + 1}
-        />
+    renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
+        <div
+            className="pdfViewerDash-overlay"
+            style={{
+                mixBlendMode: mixBlendMode,
+                display: display,
+                transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
+            }}>
+            <CollectionFreeFormView
+                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                renderDepth={this.props.renderDepth + 1}
+                isAnnotationOverlay={true}
+                fieldKey={this.props.fieldKey + '-annotations'}
+                CollectionView={undefined}
+                setPreviewCursor={this.setPreviewCursor}
+                PanelHeight={this.panelHeight}
+                PanelWidth={this.panelWidth}
+                ScreenToLocalTransform={this.overlayTransform}
+                dropAction={'alias'}
+                docFilters={docFilters}
+                select={emptyFunction}
+                bringToFront={emptyFunction}
+                styleProvider={this.childStyleProvider}
+            />
+        </div>
     );
     @computed get overlayTransparentAnnotations() {
-        return this.renderAnnotations(this.transparentFilter, false);
+        return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined);
     }
     @computed get overlayOpaqueAnnotations() {
-        return this.renderAnnotations(this.opaqueFilter, false);
-    }
-    @computed get overlayClickableAnnotations() {
-        return <div style={{ height: NumCast(this.props.rootDoc.scrollHeight) }}>{this.renderAnnotations(undefined, true)}</div>;
+        return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined);
     }
     @computed get overlayLayer() {
         return (
             <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}>
-                <div
-                    className="pdfViewerDash-overlay"
-                    style={{
-                        pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none',
-                        mixBlendMode: 'multiply',
-                        transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
-                    }}>
-                    {this.overlayTransparentAnnotations}
-                </div>
-                <div
-                    className="pdfViewerDash-overlay"
-                    style={{
-                        pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none',
-                        mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined,
-                        transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
-                    }}>
-                    {this.overlayOpaqueAnnotations}
-                    {this.overlayClickableAnnotations}
-                </div>
+                {this.overlayTransparentAnnotations}
+                {this.overlayOpaqueAnnotations}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


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/Network.ts                   |  97 +++++++------
 src/client/documents/Documents.ts       |   7 +-
 src/client/util/DragManager.ts          |  44 ++++--
 src/client/views/nodes/DocumentView.tsx |   4 +-
 src/client/views/nodes/LoadingBox.scss  |   2 +
 src/client/views/nodes/LoadingBox.tsx   |   4 +-
 src/client/views/nodes/VideoBox.tsx     |  10 +-
 src/server/DashUploadUtils.ts           | 236 +++++++++++++++++---------------
 8 files changed, 225 insertions(+), 179 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index c781d4b6b..a222b320f 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,57 +1,54 @@
-import { Utils } from "../Utils";
+import { Utils } from '../Utils';
 import requestPromise = require('request-promise');
-import { Upload } from "../server/SharedMediaTypes";
+import { Upload } from '../server/SharedMediaTypes';
 
 export namespace Networking {
+    export async function FetchFromServer(relativeRoute: string) {
+        return (await fetch(relativeRoute)).text();
+    }
 
-      export async function FetchFromServer(relativeRoute: string) {
-            return (await fetch(relativeRoute)).text();
-      }
+    export async function PostToServer(relativeRoute: string, body?: any) {
+        const options = {
+            uri: Utils.prepend(relativeRoute),
+            method: 'POST',
+            body,
+            json: true,
+        };
+        return requestPromise.post(options);
+    }
 
-      export async function PostToServer(relativeRoute: string, body?: any) {
-            const options = {
-                  uri: Utils.prepend(relativeRoute),
-                  method: "POST",
-                  body,
-                  json: true
-            };
-            return requestPromise.post(options);
-      }
-
-      /**
-       * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint 
-       * with the mapping of GUID to filem as parameters.
-       * 
-       * @param files the files to be uploaded to the server
-       * @returns the response as a json from the server
-       */
-      export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
-            const formData = new FormData();
-            if (Array.isArray(files)) {
-                  if (!files.length) {
-                        return [];
-                  }
-                  files.forEach(file => formData.append(Utils.GenerateGuid(), file));
-            } else {
-                  formData.append(Utils.GenerateGuid(), files);
+    /**
+     * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint
+     * with the mapping of GUID to filem as parameters.
+     *
+     * @param files the files to be uploaded to the server
+     * @returns the response as a json from the server
+     */
+    export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+        const formData = new FormData();
+        if (Array.isArray(files)) {
+            if (!files.length) {
+                return [];
             }
-            const parameters = {
-                  method: 'POST',
-                  body: formData
-            };
-            const response = await fetch("/uploadFormData", parameters);
-            return response.json();
-      }
-  
-      export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
-            const parameters = {
-                  method: 'POST',
-                  body: JSON.stringify({ videoId }),
-                  json: true
-            };
-            const response = await fetch("/uploadYoutubeVideo", parameters);
-            return response.json();
-      }
-    
+            files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+        } else {
+            formData.append(Utils.GenerateGuid(), files);
+        }
+        const parameters = {
+            method: 'POST',
+            body: formData,
+        };
+        const response = await fetch('/uploadFormData', parameters);
+        return response.json();
+    }
 
-}
\ No newline at end of file
+    export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
+        const parameters = {
+            method: 'POST',
+            body: JSON.stringify({ videoId }),
+            json: true,
+        };
+        const response = await fetch('/uploadYoutubeVideo', parameters);
+        return response.json();
+    }
+}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b22e16633..8c3b91177 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1849,7 +1849,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 && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
         });
     }
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 6386c87a0..dfd916e92 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -344,8 +344,7 @@ export namespace DragManager {
         }
         Object.assign(dragDiv.style, { width: '', height: '', overflow: '' });
         dragDiv.hidden = false;
-        const scaleXs: number[] = [],
-            scaleYs: number[] = [],
+        const scalings: number[] = [],
             xs: number[] = [],
             ys: number[] = [];
 
@@ -355,8 +354,15 @@ export namespace DragManager {
             top: Number.MAX_SAFE_INTEGER,
             bottom: Number.MIN_SAFE_INTEGER,
         };
+        let rot = 0;
         const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
         const dragElements = eles.map(ele => {
+            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);
+            }
+            if (rot < 0) rot += 360;
             if (!ele.parentNode) dragDiv.appendChild(ele);
             const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
             const children = Array.from(dragElement.children);
@@ -376,19 +382,29 @@ export namespace DragManager {
                 }
             }
             const rect = ele.getBoundingClientRect();
-            const scaleX = rect.width / (ele.offsetWidth || rect.width);
-            const scaleY = scaleX; //ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX;
+            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);
 
             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);
-            xs.push(rect.left + (options?.offsetX || 0));
-            ys.push(rect.top + (options?.offsetY || 0));
-            scaleXs.push(scaleX);
-            scaleYs.push(scaleY);
+            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)
+            );
+            scalings.push(scaling);
             Object.assign(dragElement.style, {
-                opacity: '0.7',
+                opacity: '0',
                 position: 'absolute',
                 margin: '0',
                 top: '0',
@@ -399,9 +415,9 @@ export namespace DragManager {
                 borderRadius: getComputedStyle(ele).borderRadius,
                 zIndex: globalCssVariables.contextMenuZindex,
                 transformOrigin: '0 0',
-                width: `${rect.width / scaleX}px`,
-                height: `${rect.height / scaleY}px`,
-                transform: `translate(${xs[0]}px, ${ys[0]}px) scale(${scaleX}, ${scaleY})`,
+                width: rot ? '' : `${rect.width / scaling}px`,
+                height: rot ? '' : `${rect.height / scaling}px`,
+                transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg)`,
             });
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
 
@@ -415,6 +431,8 @@ 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) {
@@ -542,7 +560,7 @@ export namespace DragManager {
             const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
             lastPt = { x, y };
 
-            dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px)  scale(${scaleXs[i]}, ${scaleYs[i]})`));
+            dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg)  scale(${scalings[i]})`));
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
         };
         const upHandler = (e: PointerEvent) => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 01fadb48d..113574a64 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -498,8 +498,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 .ScreenToLocalTransform()
                 .scale(this.NativeDimScaling)
                 .transformDirection(x - left, y - top);
-            dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]);
-            dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
+            // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]);  // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
+            // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
             dragData.dropAction = dropAction;
             dragData.treeViewDoc = this.props.treeViewDoc;
             dragData.removeDocument = this.props.removeDocument;
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index f6912f547..d63ed2575 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -4,6 +4,8 @@
     align-content: center;
     justify-content: center;
     background-color: #fdfdfd;
+    height: 100%;
+    align-items: center;
 }
 
 .textContainer {
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index f3243f6cd..462ad425a 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -37,9 +37,9 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     render() {
         return (
-            <div className="loadingBoxContainer">
+            <div className="loadingBoxContainer" style={{ background: this.rootDoc.isLoading ? '' : 'red' }}>
                 <div className="textContainer">
-                    <p className="headerText">{this.rootDoc.isLoading ? 'Loading:' : 'Error Loading File:'}</p>
+                    <p className="headerText">{this.rootDoc.isLoading ? 'Loading:' : 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.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']);
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index cae35da60..ef7192ecc 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -4,33 +4,33 @@ import * as exifr from 'exifr';
 import { File } from 'formidable';
 import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
 import * as path from 'path';
-import { basename } from "path";
+import { basename } from 'path';
 import * as sharp from 'sharp';
 import { Stream } from 'stream';
 import { filesDirectory, publicDirectory } from '.';
 import { Opt } from '../fields/Doc';
-import { ParsedPDF } from "../server/PdfTypes";
+import { ParsedPDF } from '../server/PdfTypes';
 import { Utils } from '../Utils';
 import { createIfNotExists } from './ActionUtilities';
 import { clientPathToFile, Directory, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager';
-import { resolvedServerUrl } from "./server_Initialization";
+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 { exec } = require('child_process');
 const parse = require('pdf-parse');
-const ffmpeg = require("fluent-ffmpeg");
-const fs = require("fs");
-const requestImageSize = require("../client/util/request-image-size");
+const ffmpeg = require('fluent-ffmpeg');
+const fs = require('fs');
+const requestImageSize = require('../client/util/request-image-size');
 
 export enum SizeSuffix {
-    Small = "_s",
-    Medium = "_m",
-    Large = "_l",
-    Original = "_o",
-    None = ""
+    Small = '_s',
+    Medium = '_m',
+    Large = '_l',
+    Original = '_o',
+    None = '',
 }
 
 export function InjectSize(filename: string, size: SizeSuffix) {
@@ -43,7 +43,6 @@ function isLocal() {
 }
 
 export namespace DashUploadUtils {
-
     export interface Size {
         width: number;
         suffix: SizeSuffix;
@@ -59,19 +58,19 @@ export namespace DashUploadUtils {
         return AcceptableMedia.imageFormats.includes(path.extname(url).toLowerCase());
     }
 
-    const size = "content-length";
-    const type = "content-type";
+    const size = 'content-length';
+    const type = 'content-type';
+
+    const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr
 
-    const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr 
-    
     export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> {
         // make a list of paths to create the ordered text file for ffmpeg
         const inputListName = 'concat.txt';
         const textFilePath = path.join(filesDirectory, inputListName);
         // make a list of paths to create the ordered text file for ffmpeg
-      const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
+        const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
         // write the text file to the file system
-        writeFile(textFilePath, filePathsText, (err) => console.log(err));
+        writeFile(textFilePath, filePathsText, err => console.log(err));
 
         // make output file name based on timestamp
         const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
@@ -81,87 +80,110 @@ export namespace DashUploadUtils {
         // concatenate the videos
         await new Promise((resolve, reject) => {
             var merge = ffmpeg();
-            merge.input(textFilePath)
-            .inputOptions(['-f concat', '-safe 0'])
+            merge
+                .input(textFilePath)
+                .inputOptions(['-f concat', '-safe 0'])
                 .outputOptions('-c copy')
                 //.videoCodec("copy")
                 .save(outputFilePath)
-                .on("error", reject)
-                .on("end", resolve);
-        })
-
-      // delete concat.txt from the file system
-      unlinkSync(textFilePath);
-      // delete the old segment videos from the server
-      filePaths.forEach(filePath => unlinkSync(filePath));
-
-      // return the path(s) to the output file
-      return {
-          accessPaths: getAccessPaths(Directory.videos, outputFileName)
-        }
+                .on('error', reject)
+                .on('end', resolve);
+        });
+
+        // delete concat.txt from the file system
+        unlinkSync(textFilePath);
+        // delete the old segment videos from the server
+        filePaths.forEach(filePath => unlinkSync(filePath));
+
+        // return the path(s) to the output file
+        return {
+            accessPaths: getAccessPaths(Directory.videos, outputFileName),
+        };
     }
 
     export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
-        console.log("UPLOAD " + videoId);
+        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));
-                    }
-                });
+            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));
+                }
+            });
         });
     }
 
     export async function upload(file: File): Promise<Upload.FileResponse> {
         const { type, path, name } = file;
-        const types = type?.split("/") ?? [];
+        const types = type?.split('/') ?? [];
 
         const category = types[0];
         let format = `.${types[1]}`;
         console.log(green(`Processing upload of file (${name}) and format (${format}) with upload type (${type}) in category (${category}).`));
-        
+
         switch (category) {
-            case "image":
+            case 'image':
                 if (imageFormats.includes(format)) {
                     const result = await UploadImage(path, basename(path));
                     return { source: file, result };
                 }
-            case "video":
-                if (format.includes("x-matroska")) {
-                    console.log("case video");
-                    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));
-                    file.path = file.path.replace(".mkv", ".mp4");
-                    format = ".mp4";
+            case 'video':
+                if (format.includes('x-matroska')) {
+                    console.log('case video');
+                    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)
+                    );
+                    file.path = file.path.replace('.mkv', '.mp4');
+                    format = '.mp4';
+                }
+                if (format.includes('quicktime')) {
+                    let abort = false;
+                    await new Promise<void>(res =>
+                        ffmpeg.ffprobe(file.path, (err: any, metadata: any) => {
+                            if (metadata.streams.some((stream: any) => stream.codec_name === 'hevc')) {
+                                abort = true;
+                            }
+                            res();
+                        })
+                    );
+                    if (abort) return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } };
+                    // bcz: instead of aborting, we could convert the file using the code below to an mp4.  Problem is that this takes a long time and will clog up the server.
+                    // await new Promise(res =>
+                    //     ffmpeg(file.path)
+                    //         .videoCodec('libx264') // this will copy the data instead of reencode it
+                    //         .audioCodec('mp2')
+                    //         .save(file.path.replace('.MOV', '.mp4').replace('.mov', '.mp4'))
+                    //         .on('end', res)
+                    // );
+                    // file.path = file.path.replace('.mov', '.mp4').replace('.MOV', '.mp4');
+                    // format = '.mp4';
                 }
                 if (videoFormats.includes(format)) {
                     return MoveParsedFile(file, Directory.videos);
                 }
-            case "application":
+            case 'application':
                 if (applicationFormats.includes(format)) {
                     return UploadPdf(file);
                 }
-            case "audio":
-                const components = format.split(";");
+            case 'audio':
+                const components = format.split(';');
                 if (components.length > 1) {
                     format = components[0];
                 }
                 if (audioFormats.includes(format)) {
                     return UploadAudio(file, format);
                 }
-            case "text":
-                if (types[1] == "csv") {
+            case 'text':
+                if (types[1] == 'csv') {
                     return UploadCsv(file);
                 }
-            
         }
 
         console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
@@ -176,22 +198,21 @@ export namespace DashUploadUtils {
             const name = path.basename(sourcePath);
             const textFilename = `${name.substring(0, name.length - 4)}.txt`;
             const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
-            writeStream.write(result.text, error => error ? reject(error) : resolve());
+            writeStream.write(result.text, error => (error ? reject(error) : resolve()));
         });
         return MoveParsedFile(file, Directory.pdfs, undefined, result.text);
     }
 
     async function UploadCsv(file: File) {
-       const { path: sourcePath } = file;
-       // read the file as a string
+        const { path: sourcePath } = file;
+        // read the file as a string
         const data = readFileSync(sourcePath, 'utf8');
         // split the string into an array of lines
         return MoveParsedFile(file, Directory.csv, undefined, data);
         // console.log(csvParser(data));
-
     }
 
-    const manualSuffixes = [".webm"];
+    const manualSuffixes = ['.webm'];
 
     async function UploadAudio(file: File, format: string) {
         const suffix = manualSuffixes.includes(format) ? format : undefined;
@@ -200,22 +221,22 @@ export namespace DashUploadUtils {
 
     /**
      * Uploads an image specified by the @param source to Dash's /public/files/
-     * directory, and returns information generated during that upload 
-     * 
+     * directory, and returns information generated during that upload
+     *
      * @param {string} source is either the absolute path of an already uploaded image or
      * the url of a remote image
      * @param {string} filename dictates what to call the image. If not specified,
      * the name {@param prefix}_upload_{GUID}
      * @param {string} prefix is a string prepended to the generated image name in the
      * event that @param filename is not specified
-     * 
+     *
      * @returns {ImageUploadInformation | Error} This method returns
      * 1) the paths to the uploaded images (plural due to resizing)
      * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed
      * 3) the size of the image, in bytes (4432130)
      * 4) the content type of the image, i.e. image/(jpeg | png | ...)
      */
-    export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<Upload.ImageInformation | Error> => {
+    export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => {
         const metadata = await InspectImage(source);
         if (metadata instanceof Error) {
             return metadata;
@@ -225,12 +246,12 @@ export namespace DashUploadUtils {
 
     export async function buildFileDirectories() {
         if (!existsSync(publicDirectory)) {
-            console.error("\nPlease ensure that the following directory exists...\n");
+            console.error('\nPlease ensure that the following directory exists...\n');
             console.log(publicDirectory);
             process.exit(0);
         }
         if (!existsSync(filesDirectory)) {
-            console.error("\nPlease ensure that the following directory exists...\n");
+            console.error('\nPlease ensure that the following directory exists...\n');
             console.log(filesDirectory);
             process.exit(0);
         }
@@ -252,7 +273,7 @@ export namespace DashUploadUtils {
     /**
      * Based on the url's classification as local or remote, gleans
      * as much information as possible about the specified image
-     * 
+     *
      * @param source is the path or url to the image in question
      */
     export const InspectImage = async (source: string): Promise<Upload.InspectionResults | Error> => {
@@ -265,9 +286,9 @@ export namespace DashUploadUtils {
          */
         if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) {
             const [ext, data] = rawMatches.slice(1, 3);
-            const resolved = filename = `upload_${Utils.GenerateGuid()}.${ext}`;
+            const resolved = (filename = `upload_${Utils.GenerateGuid()}.${ext}`);
             const error = await new Promise<Error | null>(resolve => {
-                writeFile(serverPathToFile(Directory.images, resolved), data, "base64", resolve);
+                writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve);
             });
             if (error !== null) {
                 return error;
@@ -276,12 +297,12 @@ export namespace DashUploadUtils {
         }
         let resolvedUrl: string;
         /**
-         * 
+         *
          * At this point, we want to take whatever url we have and make sure it's requestable.
          * Anything that's hosted by some other website already is, but if the url is a local file url
          * (locates the file on this server machine), we have to resolve the client side url by cutting out the
          * basename subtree (i.e. /images/<some_guid>.<ext>) and put it on the end of the server's url.
-         * 
+         *
          * This can always be localhost, regardless of whether this is on the server or not, since we (the server, not the client)
          * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:<port> refers to the same thing
          * as the full dash-release.eastus.cloudapp.azure.com:<port>.
@@ -290,18 +311,18 @@ export namespace DashUploadUtils {
         if (matches === null) {
             resolvedUrl = source;
         } else {
-            resolvedUrl = `${resolvedServerUrl}/${matches[1].split("\\").join("/")}`;
+            resolvedUrl = `${resolvedServerUrl}/${matches[1].split('\\').join('/')}`;
         }
         // See header comments: not all image files have exif data (I believe only JPG is the only format that can have it)
         const exifData = await parseExifData(resolvedUrl);
         const results = {
             exifData,
-            requestable: resolvedUrl
+            requestable: resolvedUrl,
         };
         // Use the request library to parse out file level image information in the headers
-        const { headers } = (await new Promise<any>((resolve, reject) => {
-            request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res));
-        }).catch(console.error));
+        const { headers } = await new Promise<any>((resolve, reject) => {
+            request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res)));
+        }).catch(console.error);
         try {
             // Compute the native width and height ofthe image with an npm module
             const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl);
@@ -313,7 +334,7 @@ export namespace DashUploadUtils {
                 nativeWidth,
                 nativeHeight,
                 filename,
-                ...results
+                ...results,
             };
         } catch (e: any) {
             console.log(e);
@@ -340,12 +361,14 @@ export namespace DashUploadUtils {
             rename(sourcePath, destinationPath, error => {
                 resolve({
                     source: file,
-                    result: error ? error : {
-                        accessPaths: {
-                            agnostic: getAccessPaths(destination, name)
-                        },
-                        rawText: text
-                    }
+                    result: error
+                        ? error
+                        : {
+                              accessPaths: {
+                                  agnostic: getAccessPaths(destination, name),
+                              },
+                              rawText: text,
+                          },
                 });
             });
         });
@@ -354,19 +377,19 @@ export namespace DashUploadUtils {
     export function getAccessPaths(directory: Directory, fileName: string) {
         return {
             client: clientPathToFile(directory, fileName),
-            server: serverPathToFile(directory, fileName)
+            server: serverPathToFile(directory, fileName),
         };
     }
 
-    export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<Upload.ImageInformation> => {
+    export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
         const { requestable, source, ...remaining } = metadata;
-        const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split("/")[1].toLowerCase()}`;
+        const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`;
         const { images } = Directory;
         const information: Upload.ImageInformation = {
             accessPaths: {
-                agnostic: getAccessPaths(images, resolved)
+                agnostic: getAccessPaths(images, resolved),
             },
-            ...metadata
+            ...metadata,
         };
         const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
         for (const suffix of Object.keys(writtenFiles)) {
@@ -383,9 +406,9 @@ export namespace DashUploadUtils {
             const val: any = layer[key];
             if (val instanceof Buffer) {
                 layer[key] = val.toString();
-            } else if (Array.isArray(val) && typeof val[0] === "number") {
+            } else if (Array.isArray(val) && typeof val[0] === 'number') {
                 layer[key] = Buffer.from(val).toString();
-            } else if (typeof val === "object") {
+            } else if (typeof val === 'object') {
                 bufferConverterRec(val);
             }
         }
@@ -410,20 +433,20 @@ export namespace DashUploadUtils {
     const pngOptions = {
         compressionLevel: 9,
         adaptiveFiltering: true,
-        force: true
+        force: true,
     };
 
     export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) {
         const writtenFiles: { [suffix: string]: string } = {};
         for (const { resizer, suffix } of resizers(path.extname(outputFileName))) {
-            const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix));
+            const outputPath = path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix)));
             await new Promise<void>(async (resolve, reject) => {
                 const source = streamProvider();
                 let readStream: Stream = source instanceof Promise ? await source : source;
                 if (resizer) {
                     readStream = readStream.pipe(resizer.withMetadata());
                 }
-                readStream.pipe(createWriteStream(outputPath)).on("close", resolve).on("error", reject);
+                readStream.pipe(createWriteStream(outputPath)).on('close', resolve).on('error', reject);
             });
         }
         return writtenFiles;
@@ -442,15 +465,14 @@ export namespace DashUploadUtils {
                     initial = initial.webp();
                 } else if (tiffs.includes(ext)) {
                     initial = initial.tiff();
-                } else if (ext === ".gif") {
+                } else if (ext === '.gif') {
                     initial = undefined;
                 }
                 return {
                     resizer: initial,
-                    suffix
+                    suffix,
                 };
-            })
+            }),
         ];
     }
-
-}
\ No newline at end of file
+}
-- 
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

