From 820d40eea4eeb5977889e0ef6c35f9092df44b4b Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Wed, 10 Aug 2022 17:08:47 -0400 Subject: created placeholder loading box but have to get location of final doc to update with loading box --- src/client/views/nodes/LoadingBox.tsx | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/client/views/nodes/LoadingBox.tsx (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx new file mode 100644 index 000000000..0e0619241 --- /dev/null +++ b/src/client/views/nodes/LoadingBox.tsx @@ -0,0 +1,31 @@ +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'; + +export interface LoadingBoxProps { + title: string; + text: string; +} + +@observer +export class LoadingBox extends ViewBoxAnnotatableComponent() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(LoadingBox, fieldKey); + } + + constructor(props: any) { + super(props); + } + + render() { + return ( +
+ Loading: {this.dataDoc.text} + +
+ ); + } +} -- cgit v1.2.3-70-g09d2 From bb2b38b0e47eaf8e64554d101b605bf35a178239 Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Tue, 16 Aug 2022 16:43:01 -0400 Subject: updated placeholder --- src/.DS_Store | Bin 10244 -> 10244 bytes src/client/documents/Documents.ts | 63 +++++++++++++------ src/client/util/CurrentUserUtils.ts | 1 + src/client/views/collections/CollectionSubView.tsx | 70 +++++++-------------- src/client/views/nodes/LoadingBox.tsx | 25 ++++++++ 5 files changed, 94 insertions(+), 65 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/.DS_Store b/src/.DS_Store index 4751acf44..4ed785983 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6ccb4358a..d8497e3af 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,4 +1,5 @@ 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'; @@ -794,7 +795,7 @@ export namespace Docs { * only when creating a DockDocument from the current user's already existing * main document. */ - function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string) { + function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) { const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_'); @@ -813,13 +814,22 @@ export namespace Docs { // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. dataProps[fieldKey + '-annotations'] = new List(); dataProps[fieldKey + '-sidebar'] = new List(); - const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); + + const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); + + if (placeholderDoc) { + dataDoc.proto = proto; + } const viewFirstProps: { [id: string]: any } = {}; viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; viewFirstProps['acl-Override'] = 'None'; viewFirstProps.author = Doc.CurrentUserEmail; - const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); + let viewDoc: Doc; + if (placeholderDoc) { + viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true); + } + viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); Doc.assign(viewDoc, viewProps, true, true); ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); @@ -883,14 +893,12 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options); } - export function LoadingDocument(title: string, text: string, width?: number, height?: number, options: DocumentOptions = {}) { - let myWidth = 300; - let myHeight = 300; - if (height && width) { - myWidth = width; - myHeight = height; - } - return InstanceFromProto(Prototypes.get(DocumentType.LOADING), '', { ...options, title, text, _width: myWidth, _height: myHeight }); + export const filesToDocs = new Map(); + + export function LoadingDocument(file: File, options: DocumentOptions, ytString?: string) { + const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), file.name, { _height: 300, _width: 300, ...options }); + // filesToDocs.set(loading, file); + return loading; } export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { @@ -969,13 +977,13 @@ export namespace Docs { return I; } - export function PdfDocument(url: string, options: DocumentOptions = {}) { + export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { const width = options._width || undefined; const height = options._height || undefined; const nwid = options._nativeWidth || undefined; const nhght = options._nativeHeight || undefined; if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width); - return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options); + return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options, undefined, undefined, undefined, overwriteDoc); } export function WebDocument(url: string, options: DocumentOptions = {}) { @@ -1462,8 +1470,8 @@ export namespace DocUtils { return created; } - export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { - let ctor: ((path: string, options: DocumentOptions) => Doc | Promise) | undefined = undefined; + export async function DocumentFromType(type: string, path: string, options: DocumentOptions, rootDoc?: Doc): Promise> { + let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise) | undefined = undefined; if (type.indexOf('image') !== -1) { ctor = Docs.Create.ImageDocument; if (!options._width) options._width = 300; @@ -1512,7 +1520,7 @@ export namespace DocUtils { options = { ...options, _width: 400, _height: 512, title: path }; } - return ctor ? ctor(path, options) : undefined; + return ctor ? ctor(path, options, rootDoc) : undefined; } export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void { @@ -1728,14 +1736,14 @@ export namespace DocUtils { return dd; } - async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) { + async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, rootDoc?: Doc) { if (result instanceof Error) { alert(`Upload failed: ${result.message}`); return; } const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); - const doc = await DocUtils.DocumentFromType(type, pathname, full); + const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; @@ -1813,6 +1821,25 @@ export namespace DocUtils { return generatedDocuments; } + export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) { + const generatedDocuments: Doc[] = []; + Networking.UploadFilesToServer([file]).then(upfiles => { + const { + source: { name, type }, + result, + } = upfiles.lastElement(); + console.log(name, type); + name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); + }); + } + + export function generatePlaceHolder(file: File, options: DocumentOptions) { + return Docs.Create.LoadingDocument(file, options); + // placeholder.file = file + // TODO: nda - modify loading doc so it only takes in options + // Docs.Create.LoadingDocument(options, ) + } + // copies the specified drag factory document export function copyDragFactory(dragFactory: Doc) { if (!dragFactory) return undefined; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index d19874720..492513a61 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -196,6 +196,7 @@ export class CurrentUserUtils { makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}), makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), + makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}), //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 79f629072..1ff4f5ab8 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -449,48 +449,6 @@ export function CollectionSubView(moreProps?: X) { this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end); } - /** - * Creates a placeholder doc view for files being uploaded and removes placeholder docs once files are uplodaded. - * - * @param files the files to upload that we want to create placeholders for - * @param options the document options (primarily the x and y coordinates to put doc) - * @param text in the case of youtube the text is the url to the video - * @returns a disposer action that removes the placeholders created after files get uploaded - */ - placeHolderDisposer = (files: File[] | string, options: DocumentOptions, text: string) => { - // TODO: nda - create a specialized view for placeholder upload with a spinner and ability to retry upload - let placeholders: Doc[] = []; - // handle yt case - if (typeof files === 'string') { - placeholders.push(Docs.Create.LoadingDocument('Loading...', text, 500, 500, { ...options })); - // placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 400, _height: 30 })); - } else { - // every other doc type is an array of File - - // Get the file names as a text - let textStr = ''; - files.forEach(file => { - textStr += file.name + '\n'; - }); - placeholders.push(Docs.Create.LoadingDocument('Loading...', textStr, 500, 500, { ...options })); - - // placeholders.push(Docs.Create.TextDocument('Loading: \n' + textStr, { ...options, title: files.length + ' files', _width: 500, _height: files.length * 40 })); - } - // disposer action to remove placeholders once files are uploaded - const remove = action(() => { - if (!this.props.DataDoc) { - return; - } - for (let i = 0; i < placeholders.length; i++) { - Doc.RemoveDocFromList(this.props.DataDoc, 'data', placeholders[i]); - } - }); - placeholders.forEach(pl => { - this.addDocument(pl); - }); - return remove; - }; - slowLoadDocuments = async ( files: File[] | string, options: DocumentOptions, @@ -501,13 +459,31 @@ export function CollectionSubView(moreProps?: X) { clientY: number, addDocument: (doc: Doc | Doc[]) => boolean ) => { + // create placeholder docs + // inside placeholder docs have some func that + // TODO: once loading thing is moved it should update the x and y of the file it is placeholder for - const disposer = this.placeHolderDisposer(files, options, text); + let pileUpDoc = undefined; // const disposer = OverlayView.Instance.addElement(, { x: clientX - 125, y: clientY - 125 }); if (typeof files === 'string') { - generatedDocuments.push(...(await DocUtils.uploadYoutubeVideo(files, options))); + // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder + // generatedDocuments.push(Docs.Create.LoadingDocument(files, options)); } else { - generatedDocuments.push(...(await DocUtils.uploadFilesToDocs(files, options))); + // uploadFilesToDocs and similar should return a placeholder, one for each placeholder + generatedDocuments.push( + ...files.map(file => { + const loading = Docs.Create.LoadingDocument(file, options); + // now that there is a doc do whatever slowload was going to do with that file + if (typeof file === 'string') { + // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder + // (await DocUtils.uploadYoutubeVideo(files, options))); + } else { + // uploadFilesToDocs and similar should return a placeholder, one for each placeholder + DocUtils.uploadFileToDoc(file, {}, loading); + } + return loading; + }) + ); } if (generatedDocuments.length) { // Creating a dash document @@ -523,7 +499,8 @@ export function CollectionSubView(moreProps?: X) { if (completed) completed(set); else { if (isFreeformView && generatedDocuments.length > 1) { - addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!); + pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!; + addDocument(pileUpDoc); } else { generatedDocuments.forEach(addDocument); } @@ -535,7 +512,6 @@ export function CollectionSubView(moreProps?: X) { alert('Document upload failed - possibly an unsupported file type.'); } } - disposer(); }; } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 0e0619241..9d4668dde 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -4,6 +4,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import * as React from 'react'; import './LoadingBox.scss'; import ReactLoading from 'react-loading'; +import { Docs, DocUtils } from '../../documents/Documents'; export interface LoadingBoxProps { title: string; @@ -16,6 +17,30 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { return FieldView.LayoutString(LoadingBox, fieldKey); } + componentDidMount() { + // const file = Docs.Create.filesToDocs.get(this.rootDoc); + // if (file) { + // console.log('Got to file'); + // Docs.Create.filesToDocs.delete(this.rootDoc); + // // now that there is a doc do whatever slowload was going to do with that file + // if (typeof file === 'string') { + // // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder + // // (await DocUtils.uploadYoutubeVideo(files, options))); + // } else { + // // uploadFilesToDocs and similar should return a placeholder, one for each placeholder + // DocUtils.uploadFileToDoc(file, {}, this.rootDoc); + // } + // } else { + // // check if file now exists on server or not + // // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading) + // // if it doesn't display an error message "upload failed" + // } + // query endpoints to: + // check if file now exists on server or not + // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading) + // if it doesn't display an error message "upload failed" + } + constructor(props: any) { super(props); } -- cgit v1.2.3-70-g09d2 From 27945b9a931fa9504404174dd08964556dc3aea2 Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Tue, 16 Aug 2022 21:16:03 -0400 Subject: added loading for diff doc types --- src/client/documents/Documents.ts | 47 ++++++++++++++++------ src/client/views/collections/CollectionSubView.tsx | 11 ++--- src/client/views/nodes/LoadingBox.scss | 5 +++ src/client/views/nodes/LoadingBox.tsx | 12 +++--- 4 files changed, 48 insertions(+), 27 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d8497e3af..e54fe16de 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -844,9 +844,9 @@ export namespace Docs { return viewDoc; } - export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}) { + export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) { const imgField = url instanceof ImageField ? url : new ImageField(url); - return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc); } export function PresDocument(options: DocumentOptions = {}) { @@ -857,12 +857,12 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); } - export function VideoDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options); + export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { + return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc); } - export function YoutubeDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options); + export function YoutubeDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { + return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options, undefined, undefined, undefined, overwriteDoc); } export function WebCamDocument(url: string, options: DocumentOptions = {}) { @@ -877,8 +877,16 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options); } - export function AudioDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); + export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { + return InstanceFromProto( + Prototypes.get(DocumentType.AUDIO), + new AudioField(url), + { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }, + undefined, + undefined, + undefined, + overwriteDoc + ); } export function RecordingDocument(url: string, options: DocumentOptions = {}) { @@ -895,8 +903,10 @@ export namespace Docs { export const filesToDocs = new Map(); - export function LoadingDocument(file: File, options: DocumentOptions, ytString?: string) { - const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), file.name, { _height: 300, _width: 300, ...options }); + export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) { + const fileName = typeof file == 'string' ? file : file.name; + options.title = fileName; + const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 150, ...options }); // filesToDocs.set(loading, file); return loading; } @@ -1114,8 +1124,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); } - export function DataVizDocument(url: string, options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }); + export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) { + return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc); } export function DockDocument(documents: Array, config: string, options: DocumentOptions, id?: string) { @@ -1809,6 +1819,18 @@ export namespace DocUtils { } return generatedDocuments; } + + export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) { + const generatedDocuments: Doc[] = []; + Networking.UploadYoutubeToServer(videoId).then(upfiles => { + const { + source: { name, type }, + result, + } = upfiles.lastElement(); + name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); + }); + } + export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { const generatedDocuments: Doc[] = []; const upfiles = await Networking.UploadFilesToServer(files); @@ -1828,7 +1850,6 @@ export namespace DocUtils { source: { name, type }, result, } = upfiles.lastElement(); - console.log(name, type); name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); }); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 1ff4f5ab8..30467efa0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -468,19 +468,16 @@ export function CollectionSubView(moreProps?: X) { if (typeof files === 'string') { // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder // generatedDocuments.push(Docs.Create.LoadingDocument(files, options)); + const loading = Docs.Create.LoadingDocument(files, options); + generatedDocuments.push(loading); + DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { // uploadFilesToDocs and similar should return a placeholder, one for each placeholder generatedDocuments.push( ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); // now that there is a doc do whatever slowload was going to do with that file - if (typeof file === 'string') { - // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder - // (await DocUtils.uploadYoutubeVideo(files, options))); - } else { - // uploadFilesToDocs and similar should return a placeholder, one for each placeholder - DocUtils.uploadFileToDoc(file, {}, loading); - } + DocUtils.uploadFileToDoc(file, {}, loading); return loading; }) ); diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss index 239faa78e..e8890cd82 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -5,3 +5,8 @@ justify-content: center; background-color: #fdfdfd; } + +.text { + text-overflow: ellipsis; + text-align: center; +} diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 9d4668dde..96620aff9 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -4,12 +4,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import * as React from 'react'; import './LoadingBox.scss'; import ReactLoading from 'react-loading'; -import { Docs, DocUtils } from '../../documents/Documents'; - -export interface LoadingBoxProps { - title: string; - text: string; -} +import { StrCast } from '../../../fields/Types'; @observer export class LoadingBox extends ViewBoxAnnotatableComponent() { @@ -18,6 +13,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { } componentDidMount() { + console.log(this.rootDoc); // const file = Docs.Create.filesToDocs.get(this.rootDoc); // if (file) { // console.log('Got to file'); @@ -48,7 +44,9 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { render() { return (
- Loading: {this.dataDoc.text} +

Loading:

+

+

{StrCast(this.rootDoc.title)}

); -- cgit v1.2.3-70-g09d2 From 94c38310c6b54d93e907007f20ba032d12697ca0 Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Thu, 18 Aug 2022 12:33:34 -0400 Subject: fixed sizing bug --- src/client/documents/Documents.ts | 14 +++++-------- src/client/views/collections/CollectionSubView.tsx | 2 ++ src/client/views/nodes/LoadingBox.tsx | 24 ++++++++++++++++++---- 3 files changed, 27 insertions(+), 13 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e54fe16de..f3ab7c788 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -827,9 +827,12 @@ export namespace Docs { viewFirstProps.author = Doc.CurrentUserEmail; let viewDoc: Doc; if (placeholderDoc) { + placeholderDoc._height = Number(options._height); + placeholderDoc._width = Number(options._width); viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true); + } else { + viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); } - viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); Doc.assign(viewDoc, viewProps, true, true); ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); @@ -901,7 +904,7 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options); } - export const filesToDocs = new Map(); + export const filesToDocs = new Map(); export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) { const fileName = typeof file == 'string' ? file : file.name; @@ -1854,13 +1857,6 @@ export namespace DocUtils { }); } - export function generatePlaceHolder(file: File, options: DocumentOptions) { - return Docs.Create.LoadingDocument(file, options); - // placeholder.file = file - // TODO: nda - modify loading doc so it only takes in options - // Docs.Create.LoadingDocument(options, ) - } - // copies the specified drag factory document export function copyDragFactory(dragFactory: Doc) { if (!dragFactory) return undefined; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 30467efa0..fd2c722d2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -470,6 +470,7 @@ export function CollectionSubView(moreProps?: X) { // generatedDocuments.push(Docs.Create.LoadingDocument(files, options)); const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); + Docs.Create.filesToDocs.set(loading, files); DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { // uploadFilesToDocs and similar should return a placeholder, one for each placeholder @@ -477,6 +478,7 @@ export function CollectionSubView(moreProps?: X) { ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); // now that there is a doc do whatever slowload was going to do with that file + Docs.Create.filesToDocs.set(loading, file); DocUtils.uploadFileToDoc(file, {}, loading); return loading; }) diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 96620aff9..249461b67 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import './LoadingBox.scss'; import ReactLoading from 'react-loading'; import { StrCast } from '../../../fields/Types'; +import { computed, observable } from 'mobx'; +import { Docs } from '../../documents/Documents'; @observer export class LoadingBox extends ViewBoxAnnotatableComponent() { @@ -12,6 +14,12 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { return FieldView.LayoutString(LoadingBox, fieldKey); } + @computed + private get isLoading() { + const file = Docs.Create.filesToDocs.get(this.rootDoc); + return file ? true : false; + } + componentDidMount() { console.log(this.rootDoc); // const file = Docs.Create.filesToDocs.get(this.rootDoc); @@ -44,10 +52,18 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { render() { return (
-

Loading:

-

-

{StrCast(this.rootDoc.title)}

- + {this.isLoading ? ( +
+

Loading:

+

+ {StrCast(this.rootDoc.title)} + +
+ ) : ( +
+ Error Loading File: {StrCast(this.rootDoc.title)} +
+ )}
); } -- cgit v1.2.3-70-g09d2 From b7c4d65a3bf04ff7d2c10d93be282a1b0a4650b3 Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Fri, 19 Aug 2022 12:32:23 -0400 Subject: added comments, cleaned up code, could not get spinner to center --- src/client/documents/Documents.ts | 8 ++- src/client/views/collections/CollectionSubView.tsx | 4 +- src/client/views/nodes/LoadingBox.scss | 21 +++++++ src/client/views/nodes/LoadingBox.tsx | 64 ++++++++++------------ 4 files changed, 58 insertions(+), 39 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f3ab7c788..9e140ccd1 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -815,6 +815,7 @@ export namespace Docs { dataProps[fieldKey + '-annotations'] = new List(); dataProps[fieldKey + '-sidebar'] = new List(); + // users placeholderDoc as proto if it exists const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); if (placeholderDoc) { @@ -826,6 +827,7 @@ export namespace Docs { viewFirstProps['acl-Override'] = 'None'; viewFirstProps.author = Doc.CurrentUserEmail; let viewDoc: Doc; + // determines whether viewDoc should be created using placeholder Doc or deafult if (placeholderDoc) { placeholderDoc._height = Number(options._height); placeholderDoc._width = Number(options._width); @@ -904,13 +906,13 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options); } - export const filesToDocs = new Map(); + // Mapping of all loading docs to files, i.e. keeps track of files being uploaded in current session + export const docsToFiles = new Map(); export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) { const fileName = typeof file == 'string' ? file : file.name; options.title = fileName; - const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 150, ...options }); - // filesToDocs.set(loading, file); + const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 200, ...options }); return loading; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index fd2c722d2..eef485b95 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -470,7 +470,7 @@ export function CollectionSubView(moreProps?: X) { // generatedDocuments.push(Docs.Create.LoadingDocument(files, options)); const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); - Docs.Create.filesToDocs.set(loading, files); + Docs.Create.docsToFiles.set(loading, files); DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { // uploadFilesToDocs and similar should return a placeholder, one for each placeholder @@ -478,7 +478,7 @@ export function CollectionSubView(moreProps?: X) { ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); // now that there is a doc do whatever slowload was going to do with that file - Docs.Create.filesToDocs.set(loading, file); + Docs.Create.docsToFiles.set(loading, file); DocUtils.uploadFileToDoc(file, {}, loading); return loading; }) diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss index e8890cd82..f6912f547 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -6,7 +6,28 @@ background-color: #fdfdfd; } +.textContainer { + margin: 5px; +} + +.textContainer { + justify-content: center; + align-content: center; +} + +.textContainer, .text { + overflow: hidden; text-overflow: ellipsis; + max-width: 80%; + text-align: center; +} + +.headerText { + text-align: center; + font-weight: bold; +} + +.spinner { text-align: center; } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 249461b67..ab8878fc1 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -5,46 +5,42 @@ import * as React from 'react'; import './LoadingBox.scss'; import ReactLoading from 'react-loading'; import { StrCast } from '../../../fields/Types'; -import { computed, observable } from 'mobx'; +import { computed } from 'mobx'; import { Docs } from '../../documents/Documents'; +/** + * LoadingBox Class represents a placeholder doc for documents that are currently + * being uploaded to the server and being fetched by the client. The LoadingBox doc is then used to + * generate the actual type of doc that is required once the document has been successfully uploaded. + * + * Design considerations: + * We are using the docToFiles map in Documents to keep track of all files being uploaded in one session of the client. + * If the file is not found we assume an error has occurred with the file upload, e.g. it has been interrupted by a client refresh + * or network issues. The docToFiles essentially gets reset everytime the page is refreshed. + * + * TODOs: + * 1) ability to query server to retrieve files that already exist if users upload duplicate files. + * 2) ability to restart upload if there is an error + * 3) detect network error and notify the user + * 4 )if file upload gets interrupted, it still gets uploaded to the server if there are no network interruptions which leads to unused space. this could be + * handled with (1) + * + * @author naafiyan + */ @observer export class LoadingBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LoadingBox, fieldKey); } - @computed - private get isLoading() { - const file = Docs.Create.filesToDocs.get(this.rootDoc); + /** + * Returns true if file is uploading, false otherwise + */ + @computed private get isLoading(): boolean { + const file = Docs.Create.docsToFiles.get(this.rootDoc); return file ? true : false; } - componentDidMount() { - console.log(this.rootDoc); - // const file = Docs.Create.filesToDocs.get(this.rootDoc); - // if (file) { - // console.log('Got to file'); - // Docs.Create.filesToDocs.delete(this.rootDoc); - // // now that there is a doc do whatever slowload was going to do with that file - // if (typeof file === 'string') { - // // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder - // // (await DocUtils.uploadYoutubeVideo(files, options))); - // } else { - // // uploadFilesToDocs and similar should return a placeholder, one for each placeholder - // DocUtils.uploadFileToDoc(file, {}, this.rootDoc); - // } - // } else { - // // check if file now exists on server or not - // // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading) - // // if it doesn't display an error message "upload failed" - // } - // query endpoints to: - // check if file now exists on server or not - // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading) - // if it doesn't display an error message "upload failed" - } - constructor(props: any) { super(props); } @@ -53,15 +49,15 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { return (
{this.isLoading ? ( -
-

Loading:

-

+
+

Loading:

{StrCast(this.rootDoc.title)}
) : ( -
- Error Loading File: {StrCast(this.rootDoc.title)} +
+

Error Loading File:

+ {StrCast(this.rootDoc.title)}
)}
-- cgit v1.2.3-70-g09d2 From 85626694045f3863f6df9351156808017753de9e Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Fri, 19 Aug 2022 14:37:57 -0400 Subject: cleaned up comments --- src/client/views/nodes/LoadingBox.tsx | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index ab8878fc1..90bf90095 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -24,6 +24,8 @@ import { Docs } from '../../documents/Documents'; * 3) detect network error and notify the user * 4 )if file upload gets interrupted, it still gets uploaded to the server if there are no network interruptions which leads to unused space. this could be * handled with (1) + * 5) Fixing the stacking view bug + * 6) Fixing the CSS * * @author naafiyan */ -- cgit v1.2.3-70-g09d2 From 7f2556568ab635274c483d102fa4555d12e14835 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 30 Aug 2022 13:55:41 -0400 Subject: fixed loadingDocument issues with overwriting height with NaN and setting 'data' field unnecessarily and at the expense of being able to set it for replacement docs when using animation frames --- src/client/documents/Documents.ts | 34 ++++++++++------------ src/client/views/GlobalKeyHandler.ts | 9 +++--- src/client/views/collections/CollectionSubView.tsx | 6 ++-- .../views/nodes/CollectionFreeFormDocumentView.tsx | 6 ++-- src/client/views/nodes/LoadingBox.tsx | 29 ++++-------------- 5 files changed, 30 insertions(+), 54 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9446eb70c..b22e16633 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -653,7 +653,7 @@ export namespace Docs { [ DocumentType.LOADING, { - layout: { view: LoadingBox, dataField: defaultDataKey }, + layout: { view: LoadingBox, dataField: '' }, options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' }, }, ], @@ -806,14 +806,15 @@ export namespace Docs { dataProps.isPrototype = true; dataProps.author = Doc.CurrentUserEmail; dataProps.creationDate = new DateField(); - dataProps[`${fieldKey}-lastModified`] = new DateField(); - - dataProps[fieldKey] = data; - - // so that the list of annotations is already initialised, prevents issues in addonly. - // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. - dataProps[fieldKey + '-annotations'] = new List(); - dataProps[fieldKey + '-sidebar'] = new List(); + if (fieldKey) { + dataProps[`${fieldKey}-lastModified`] = new DateField(); + dataProps[fieldKey] = data; + + // so that the list of annotations is already initialised, prevents issues in addonly. + // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. + dataProps[fieldKey + '-annotations'] = new List(); + dataProps[fieldKey + '-sidebar'] = new List(); + } // users placeholderDoc as proto if it exists const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); @@ -829,8 +830,8 @@ export namespace Docs { let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default if (placeholderDoc) { - placeholderDoc._height = Number(options._height); - placeholderDoc._width = Number(options._width); + placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined; + placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined; viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true); } else { viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); @@ -846,6 +847,7 @@ export namespace Docs { updateCachedAcls(dataDoc); updateCachedAcls(viewDoc); + return viewDoc; } @@ -905,15 +907,8 @@ export namespace Docs { export function ColorDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options); } - - // Mapping of all loading docs to files, i.e. keeps track of files being uploaded in current session - export const docsToFiles = new Map(); - export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) { - const fileName = typeof file == 'string' ? file : file.name; - options.title = fileName; - const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 200, ...options }); - return loading; + return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, ''); } export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { @@ -1756,6 +1751,7 @@ export namespace DocUtils { const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc); + rootDoc && (rootDoc.isLoading = undefined); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 88ce457c6..50518eec1 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -84,6 +84,7 @@ export class KeyManager { }); private unmodified = action((keyname: string, e: KeyboardEvent) => { + const hasFffView = SelectionManager.Views().some(dv => dv.props.CollectionFreeFormDocumentView?.()); switch (keyname) { case 'u': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { @@ -160,16 +161,16 @@ export class KeyManager { break; case 'arrowleft': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge(-1, 0)), 'nudge left'); - return { stopPropagation: true, preventDefault: true }; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowright': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(1, 0)), 'nudge right'); - return { stopPropagation: true, preventDefault: true }; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowup': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, -1)), 'nudge up'); - return { stopPropagation: true, preventDefault: true }; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowdown': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 1)), 'nudge down'); - return { stopPropagation: true, preventDefault: true }; + return { stopPropagation: hasFffView, preventDefault: hasFffView }; } return { diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e70cfb13c..4227955ef 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -464,14 +464,13 @@ export function CollectionSubView(moreProps?: X) { if (typeof files === 'string') { const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); - Docs.Create.docsToFiles.set(loading, files); + loading.isLoading = true; DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); - // now that there is a doc do whatever slowload was going to do with that file - Docs.Create.docsToFiles.set(loading, file); + loading.isLoading = true; DocUtils.uploadFileToDoc(file, {}, loading); return loading; }) @@ -516,5 +515,4 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes' import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; -import { OverlayView } from '../OverlayView'; import { CollectionView, CollectionViewProps } from './CollectionView'; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index cfa4bb160..7907470dc 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -35,7 +35,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { export class CollectionFreeFormDocumentView extends DocComponent() { public static animFields: { key: string; val?: number }[] = [{ key: '_height' }, { key: '_width' }, { key: 'x' }, { key: 'y' }, { key: '_scrollTop' }, { key: 'opacity', val: 1 }, { key: 'viewScale', val: 1 }, { key: 'panX' }, { key: 'panY' }]; // fields that are configured to be animatable using animation frames public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames - public static animDataFields = ['data', 'text']; // fields that are configured to be animatable using animation frames + public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames @observable _animPos: number[] | undefined = undefined; @observable _contentView: DocumentView | undefined | null; get displayName() { @@ -121,7 +121,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any); }); @@ -173,7 +173,7 @@ export class CollectionFreeFormDocumentView extends DocComponent (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val))); CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); - CollectionFreeFormDocumentView.animDataFields.forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode))); + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode))); const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 90bf90095..f3243f6cd 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -35,33 +35,14 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { return FieldView.LayoutString(LoadingBox, fieldKey); } - /** - * Returns true if file is uploading, false otherwise - */ - @computed private get isLoading(): boolean { - const file = Docs.Create.docsToFiles.get(this.rootDoc); - return file ? true : false; - } - - constructor(props: any) { - super(props); - } - render() { return (
- {this.isLoading ? ( -
-

Loading:

- {StrCast(this.rootDoc.title)} - -
- ) : ( -
-

Error Loading File:

- {StrCast(this.rootDoc.title)} -
- )} +
+

{this.rootDoc.isLoading ? 'Loading:' : 'Error Loading File:'}

+ {StrCast(this.rootDoc.title)} + {!this.rootDoc.isLoading ? null : } +
); } -- cgit v1.2.3-70-g09d2 From 5ad8289e8ca3b6ff8304ac4fa3c2d3adaa9abf6d Mon Sep 17 00:00:00 2001 From: bobzel 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/LoadingBox.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(files: File | File[]): Promise[]> { - 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(files: File | File[]): Promise[]> { + 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(videoId: string): Promise[]> { - 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(videoId: string): Promise[]> { + 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() { render() { return ( -
+
-

{this.rootDoc.isLoading ? 'Loading:' : 'Error Loading File:'}

+

{this.rootDoc.isLoading ? 'Loading:' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

{StrCast(this.rootDoc.title)} {!this.rootDoc.isLoading ? null : }
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 { - 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 { // 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 { - console.log("UPLOAD " + videoId); + console.log('UPLOAD ' + videoId); return new Promise>((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 { 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(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 => { + export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise => { 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 => { @@ -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(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/.) 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: refers to the same thing * as the full dash-release.eastus.cloudapp.azure.com:. @@ -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((resolve, reject) => { - request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res)); - }).catch(console.error)); + const { headers } = await new Promise((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 => { + export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise => { 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, 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(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 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/LoadingBox.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; @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 ) { 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 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; } export interface MarqueeViewBounds { @@ -330,7 +340,7 @@ export class MarqueeView extends React.Component() { return (
-

{this.rootDoc.isLoading ? 'Loading:' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

+

{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

{StrCast(this.rootDoc.title)} {!this.rootDoc.isLoading ? null : }
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 -
- -
- - {this.timeline && width > 150 && ( -
-
{formatTime(curTime)}
- - {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( -
- { - e.stopPropagation(); - this._scrubbing = true; - })} - onChange={(e: React.ChangeEvent) => this.setPlayheadTime(Number(e.target.value))} - onPointerUp={action((e: React.PointerEvent) => { - e.stopPropagation(); - this._scrubbing = false; - })} - /> -
- ) : ( -
/
- )} - -
{formatTime(this.timeline.clipDuration)}
-
- )} - -
- -
- - {!this._fullScreen && width > 300 && ( -
- -
- )} - - {!this._fullScreen && width > 300 && ( -
- -
- )} - -
{ - e.stopPropagation(); - this.toggleMute(); - }}> - -
- {width > 300 && ( - e.stopPropagation()} - onChange={(e: React.ChangeEvent) => this.setVolume(Number(e.target.value))} - /> - )} - - {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( - <> -
- -
- { - e.stopPropagation(); - }} - onChange={(e: React.ChangeEvent) => { - this.zoom(Number(e.target.value)); - }} - /> - - )} - - ); - } - // renders CollectionStackedTimeline @computed get renderTimeline() { return ( @@ -1149,6 +1043,112 @@ export class VideoBox extends ViewBoxAnnotatableComponent ); } + + @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 ( + <> +
+ +
+ + {this.timeline && width > 150 && ( +
+
{formatTime(curTime)}
+ + {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( +
+ { + e.stopPropagation(); + this._scrubbing = true; + })} + onChange={(e: React.ChangeEvent) => this.setPlayheadTime(Number(e.target.value))} + onPointerUp={action((e: React.PointerEvent) => { + e.stopPropagation(); + this._scrubbing = false; + })} + /> +
+ ) : ( +
/
+ )} + +
{formatTime(this.timeline.clipDuration)}
+
+ )} + +
+ +
+ + {!this._fullScreen && width > 300 && ( +
+ +
+ )} + + {!this._fullScreen && width > 300 && ( +
+ +
+ )} + +
{ + e.stopPropagation(); + this.toggleMute(); + }}> + +
+ {width > 300 && ( + e.stopPropagation()} + onChange={(e: React.ChangeEvent) => this.setVolume(Number(e.target.value))} + /> + )} + + {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( + <> +
+ +
+ { + e.stopPropagation(); + }} + onChange={(e: React.ChangeEvent) => { + 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 { - console.log('UPLOAD ' + videoId); return new Promise>((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 { + export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number): Promise { 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 = { source: File, result: T | Error }; + export type FileResponse = { 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 945c0cd12fb2a792d10800f81b52cbac8aa12a41 Mon Sep 17 00:00:00 2001 From: mehekj Date: Tue, 20 Sep 2022 11:52:44 -0400 Subject: display error instead of infinitely loading after refresh --- src/client/documents/Documents.ts | 4 +++- src/client/views/collections/CollectionSubView.tsx | 2 ++ src/client/views/nodes/LoadingBox.tsx | 9 +++++++++ src/fields/Doc.ts | 22 ++++++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 14e6fe5bb..4f652b6e4 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1758,7 +1758,7 @@ export namespace DocUtils { const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc); - rootDoc && (rootDoc.isLoading = undefined); + rootDoc && (rootDoc.isLoading = undefined) && Doc.removeCurrentlyLoading(rootDoc); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; @@ -1840,6 +1840,7 @@ export namespace DocUtils { if (overwriteDoc) { overwriteDoc.isLoading = false; overwriteDoc.errorMessage = (result as any).message; + Doc.removeCurrentlyLoading(overwriteDoc); } } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); }); @@ -1868,6 +1869,7 @@ export namespace DocUtils { if (overwriteDoc) { overwriteDoc.isLoading = false; overwriteDoc.errorMessage = (result as any).message; + Doc.removeCurrentlyLoading(overwriteDoc); } } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); }); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 7fc1a800b..3ae965af0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -465,12 +465,14 @@ export function CollectionSubView(moreProps?: X) { const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); loading.isLoading = true; + Doc.addCurrentlyLoading(loading); DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); loading.isLoading = true; + Doc.addCurrentlyLoading(loading); DocUtils.uploadFileToDoc(file, {}, loading); return loading; }) diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index acf728ebb..220cd0880 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -1,6 +1,8 @@ +import { observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; +import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; @@ -33,6 +35,13 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { return FieldView.LayoutString(LoadingBox, fieldKey); } + componentDidMount() { + if (!Doc.CurrentlyLoading || !Doc.CurrentlyLoading.includes(this.rootDoc)) { + this.rootDoc.isLoading = false; + this.rootDoc.errorMessage = 'Upload was interrupted, please try again'; + } + } + render() { return (
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index cf505c45b..74a3d8cf2 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -149,6 +149,28 @@ export class Doc extends RefField { public static set MainDocId(id: string | undefined) { this.mainDocId = id; } + + @observable public static CurrentlyLoading: Doc[]; + // removes from currently loading display + @action + public static removeCurrentlyLoading(doc: Doc) { + if (Doc.CurrentlyLoading) { + const index = Doc.CurrentlyLoading.indexOf(doc); + index !== -1 && Doc.CurrentlyLoading.splice(index, 1); + } + } + + // adds doc to currently loading display + @action + public static addCurrentlyLoading(doc: Doc) { + if (!Doc.CurrentlyLoading) { + Doc.CurrentlyLoading = []; + } + if (Doc.CurrentlyLoading.indexOf(doc) === -1) { + Doc.CurrentlyLoading.push(doc); + } + } + @observable public static GuestDashboard: Doc | undefined; @observable public static GuestTarget: Doc | undefined; @observable public static GuestMobile: Doc | undefined; -- cgit v1.2.3-70-g09d2 From 48e0885b405cbfe728b1eda78efc19e15a104426 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 22 Sep 2022 16:00:17 -0400 Subject: fixed error messages on loading to not generate temporary error message when there is no error. --- src/client/documents/Documents.ts | 22 ++++++---------------- src/client/views/collections/CollectionSubView.tsx | 2 -- src/client/views/nodes/LoadingBox.tsx | 12 +++++------- 3 files changed, 11 insertions(+), 25 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 029c3033d..f046af684 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -908,7 +908,7 @@ export namespace Docs { export function ColorDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options); } - export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) { + export function LoadingDocument(file: File | string, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, ''); } @@ -1758,7 +1758,6 @@ export namespace DocUtils { const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc); - rootDoc && (rootDoc.isLoading = undefined) && Doc.removeCurrentlyLoading(rootDoc); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; @@ -1790,6 +1789,9 @@ export namespace DocUtils { if (Upload.isVideoInformation(result)) { proto['data-duration'] = result.duration; } + if (rootDoc) { + Doc.removeCurrentlyLoading(rootDoc); + } generatedDocuments.push(doc); } } @@ -1818,17 +1820,6 @@ export namespace DocUtils { return tbox; } - export async function uploadYoutubeVideo(videoId: string, options: DocumentOptions) { - const generatedDocuments: Doc[] = []; - for (const { - source: { name, type }, - result, - } of await Networking.UploadYoutubeToServer(videoId)) { - name && processFileupload(generatedDocuments, name, type, result, options); - } - return generatedDocuments; - } - export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) { const generatedDocuments: Doc[] = []; Networking.UploadYoutubeToServer(videoId).then(upfiles => { @@ -1839,7 +1830,7 @@ export namespace DocUtils { if ((result as any).message) { if (overwriteDoc) { overwriteDoc.isLoading = false; - overwriteDoc.errorMessage = (result as any).message; + overwriteDoc.loadingError = (result as any).message; Doc.removeCurrentlyLoading(overwriteDoc); } } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); @@ -1867,8 +1858,7 @@ export namespace DocUtils { } = upfiles.lastElement() ?? { source: { name: '', type: '' }, result: { message: 'upload failed' } }; if ((result as any).message) { if (overwriteDoc) { - overwriteDoc.isLoading = false; - overwriteDoc.errorMessage = (result as any).message; + overwriteDoc.loadingError = (result as any).message; Doc.removeCurrentlyLoading(overwriteDoc); } } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 3ae965af0..30759b766 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -464,14 +464,12 @@ export function CollectionSubView(moreProps?: X) { if (typeof files === 'string') { const loading = Docs.Create.LoadingDocument(files, options); generatedDocuments.push(loading); - loading.isLoading = true; Doc.addCurrentlyLoading(loading); DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( ...files.map(file => { const loading = Docs.Create.LoadingDocument(file, options); - loading.isLoading = true; Doc.addCurrentlyLoading(loading); DocUtils.uploadFileToDoc(file, {}, loading); return loading; diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 220cd0880..53390328f 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -1,4 +1,3 @@ -import { observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; @@ -36,19 +35,18 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { } componentDidMount() { - if (!Doc.CurrentlyLoading || !Doc.CurrentlyLoading.includes(this.rootDoc)) { - this.rootDoc.isLoading = false; - this.rootDoc.errorMessage = 'Upload was interrupted, please try again'; + if (!Doc.CurrentlyLoading?.includes(this.rootDoc)) { + this.rootDoc.loadingError = 'Upload interrupted, please try again'; } } render() { return ( -
+
-

{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

+

{StrCast(this.rootDoc.loadingError, 'Loading (can take several minutes):')}

{StrCast(this.rootDoc.title)} - {!this.rootDoc.isLoading ? null : } + {this.rootDoc.loadingError ? null : }
); -- cgit v1.2.3-70-g09d2 From 357247845341ef5e92d74883aeea093351d18490 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 23 Sep 2022 15:29:22 -0400 Subject: added progress messages to youtube uploads --- src/client/Network.ts | 9 +++++++++ src/client/views/nodes/LoadingBox.tsx | 16 ++++++++++++++- src/server/ApiManagers/UploadManager.ts | 15 ++++++++++++++ src/server/DashUploadUtils.ts | 35 +++++++++------------------------ 4 files changed, 48 insertions(+), 27 deletions(-) (limited to 'src/client/views/nodes/LoadingBox.tsx') diff --git a/src/client/Network.ts b/src/client/Network.ts index 996eb35d8..19eff3b3b 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -62,4 +62,13 @@ export namespace Networking { const response = await fetch('/uploadYoutubeVideo', parameters); return response.json(); } + export async function QueryYoutubeProgress(videoId: string): Promise<{ progress: string }> { + const parameters = { + method: 'POST', + body: JSON.stringify({ videoId }), + json: true, + }; + const response = await fetch('/queryYoutubeProgress', parameters); + return response.json(); + } } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 53390328f..8c5255f80 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -1,8 +1,10 @@ +import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; +import { Networking } from '../../Network'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; @@ -34,17 +36,29 @@ export class LoadingBox extends ViewBoxAnnotatableComponent() { return FieldView.LayoutString(LoadingBox, fieldKey); } + _timer: any; + @observable progress = ''; componentDidMount() { if (!Doc.CurrentlyLoading?.includes(this.rootDoc)) { this.rootDoc.loadingError = 'Upload interrupted, please try again'; + } else { + const updateFunc = async () => { + const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc.title)); + runInAction(() => (this.progress = result.progress)); + this._timer = setTimeout(updateFunc, 1000); + }; + this._timer = setTimeout(updateFunc, 1000); } } + componentWillUnmount() { + clearTimeout(this._timer); + } render() { return (
-

{StrCast(this.rootDoc.loadingError, 'Loading (can take several minutes):')}

+

{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}

{StrCast(this.rootDoc.title)} {this.rootDoc.loadingError ? null : }
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 76cf36d44..fe4c475c9 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -106,6 +106,21 @@ export default class UploadManager extends ApiManager { }, }); + register({ + method: Method.POST, + subscription: '/queryYoutubeProgress', + secureHandler: async ({ req, res }) => { + return new Promise(async resolve => { + req.addListener('data', args => { + const payload = String.fromCharCode.apply(String, args); + const videoId = JSON.parse(payload).videoId; + _success(res, { progress: DashUploadUtils.QueryYoutubeProgress(videoId) }); + resolve(); + }); + }); + }, + }); + register({ method: Method.POST, subscription: new RouteSubscriber('youtubeScreenshot'), diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 2c549cc9f..45e88d8cc 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -117,6 +117,12 @@ export namespace DashUploadUtils { }; } + export function QueryYoutubeProgress(videoId: string) { + return uploadProgress.get(videoId) ?? 'failed'; + } + + let uploadProgress = new Map(); + export function uploadYoutube(videoId: string): Promise { return new Promise>((res, rej) => { console.log('Uploading YouTube video: ' + videoId); @@ -124,6 +130,7 @@ export namespace DashUploadUtils { const path = name.replace(/^-/, '__') + '.mp4'; const finalPath = serverPathToFile(Directory.videos, path); if (existsSync(finalPath)) { + uploadProgress.set(videoId, 'computing duration'); exec(`yt-dlp -o ${finalPath} "https://www.youtube.com/watch?v=${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); @@ -132,9 +139,7 @@ export namespace DashUploadUtils { } else { const ytdlp = spawn(`yt-dlp`, ['-o', path, videoId, '-f', 'mp4']); - ytdlp.stdout.on('data', function (data: any) { - console.log('stdout: ' + data.toString()); // bcz: somehow channel this back to the client... - }); + ytdlp.stdout.on('data', (data: any) => uploadProgress.set(videoId, data.toString())); let errors = ''; ytdlp.stderr.on('data', (data: any) => (errors = data.toString())); @@ -153,6 +158,7 @@ export namespace DashUploadUtils { result: { name: 'failed youtube query', message: `Could not upload video. ${errors}` }, }); } else { + uploadProgress.set(videoId, 'computing duration'); exec(`yt-dlp-o ${path} "https://www.youtube.com/watch?v=${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); @@ -162,29 +168,6 @@ export namespace DashUploadUtils { }); } }); - // exec(`yt-dlp -o ${path} "https://www.youtube.com/watch?v=${videoId}" -f "mp4"`, (error: any, stdout: any, stderr: any) => { - // if (error) { - // console.log(`error: Error: ${error.message}`); - // res({ - // source: { - // size: 0, - // path, - // name, - // type: '', - // toJSON: () => ({ name, path }), - // }, - // result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}). Error: ${error.message}` }, - // }); - // } else { - // exec(`yt-dlp-o ${path} "https://www.youtube.com/watch?v=${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, name, 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)); - // }); - // } - // }); } }); } -- cgit v1.2.3-70-g09d2