From ec5c878fb4c5f7e03fdd214c0841cf2ebf983e8c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 11 Feb 2020 15:48:06 -0500 Subject: refactored image upload data response format, google docs working for specific interactions --- src/client/util/Import & Export/DirectoryImportBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/client/util/Import & Export') diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 071015193..d04f56e57 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -120,8 +120,8 @@ export default class DirectoryImportBox extends React.Component runInAction(() => this.completed += batch.length); }); - await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => { - const path = Utils.prepend(clientAccessPath); + await Promise.all(uploads.map(async ({ name, type, accessPaths, exifData }) => { + const path = Utils.prepend(accessPaths.agnostic.client); const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name }); const { data, error } = exifData; if (document) { -- cgit v1.2.3-70-g09d2 From f33ad290f1de3a01f2c4536f03f040e09771f82e Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 16 Feb 2020 00:50:23 -0500 Subject: improved file upload api --- src/client/Network.ts | 14 ++- .../util/Import & Export/DirectoryImportBox.tsx | 17 ++-- src/client/views/collections/CollectionSubView.tsx | 56 +++++------ src/new_fields/Doc.ts | 6 +- src/scraping/buxton/final/BuxtonImporter.ts | 7 +- src/server/ApiManagers/GooglePhotosManager.ts | 5 +- src/server/ApiManagers/UploadManager.ts | 8 +- src/server/DashUploadUtils.ts | 107 +++++++-------------- src/server/SharedMediaTypes.ts | 39 ++++++++ src/server/database.ts | 6 +- 10 files changed, 137 insertions(+), 128 deletions(-) (limited to 'src/client/util/Import & Export') diff --git a/src/client/Network.ts b/src/client/Network.ts index ccf60f199..6982ecf19 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -1,5 +1,6 @@ import { Utils } from "../Utils"; import requestPromise = require('request-promise'); +import { Upload } from "../server/SharedMediaTypes"; export namespace Networking { @@ -17,12 +18,21 @@ export namespace Networking { return requestPromise.post(options); } - export async function PostFormDataToServer(relativeRoute: string, formData: FormData) { + 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); + } const parameters = { method: 'POST', body: formData }; - const response = await fetch(relativeRoute, parameters); + const response = await fetch("/uploadFormData", parameters); return response.json(); } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index d04f56e57..3d8bcbab7 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -22,7 +22,7 @@ import "./DirectoryImportBox.scss"; import { Networking } from "../../Network"; import { BatchedArray } from "array-batcher"; import * as path from 'path'; -import { AcceptibleMedia } from "../../../server/SharedMediaTypes"; +import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes"; const unsupported = ["text/html", "text/plain"]; @@ -107,20 +107,21 @@ export default class DirectoryImportBox extends React.Component runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); const batched = BatchedArray.from(validated, { batchSize: 15 }); - const uploads = await batched.batchedMapAsync(async (batch, collector) => { - const formData = new FormData(); - + const uploads = await batched.batchedMapAsync>(async (batch, collector) => { batch.forEach(file => { sizes.push(file.size); modifiedDates.push(file.lastModified); - formData.append(Utils.GenerateGuid(), file); }); - - collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData))); + collector.push(...(await Networking.UploadFilesToServer(batch))); runInAction(() => this.completed += batch.length); }); - await Promise.all(uploads.map(async ({ name, type, accessPaths, exifData }) => { + await Promise.all(uploads.map(async response => { + const { source: { type }, result } = response; + if (result instanceof Error) { + return; + } + const { accessPaths, exifData } = result; const path = Utils.prepend(accessPaths.agnostic.client); const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name }); const { data, error } = exifData; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 0963e1ea6..e0b2d524b 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; @@ -25,6 +25,7 @@ import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { Networking } from "../../Network"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; import { InteractionUtils } from "../../util/InteractionUtils"; +import { Upload } from "../../../server/SharedMediaTypes"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -288,6 +289,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { } const { items } = e.dataTransfer; const { length } = items; + const files: File[] = []; if (length) { const batch = UndoManager.StartBatch("collection view drop"); const promises: Promise[] = []; @@ -307,41 +309,31 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { }); promises.push(prom); } - const type = item.type; if (item.kind === "file") { const file = item.getAsFile(); - const formData = new FormData(); - - if (!file || !file.type) { - continue; - } - - formData.append('file', file); - const dropFileName = file ? file.name : "-empty-"; - promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => { - results.map(action((result: any) => { - const { accessPaths, nativeWidth, nativeHeight, contentSize } = result; - if (Object.keys(accessPaths).length) { - const full = { ...options, _width: 300, title: dropFileName }; - const pathname = Utils.prepend(accessPaths.agnostic.client); - Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - if (doc) { - const proto = Doc.GetProto(doc); - proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); - nativeWidth && (proto["data-nativeWidth"] = nativeWidth); - nativeHeight && (proto["data-nativeHeight"] = nativeHeight); - contentSize && (proto.contentSize = contentSize); - this.props?.addDocument(doc); - } - }); - } else { - alert("Upload failed..."); - } - })); - })); + file && file.type && files.push(file); } } - + (await Networking.UploadFilesToServer(files)).forEach(({ source: { name, type }, result }) => { + if (result instanceof Error) { + alert(`Upload failed: ${result.message}`); + return; + } + const full = { ...options, _width: 300, title: name }; + const pathname = Utils.prepend(result.accessPaths.agnostic.client); + Docs.Get.DocumentFromType(type, pathname, full).then(doc => { + if (doc) { + const proto = Doc.GetProto(doc); + proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + if (Upload.isImageInformation(result)) { + proto["data-nativeWidth"] = result.nativeWidth; + proto["data-nativeHeight"] = result.nativeHeight; + proto.contentSize = result.contentSize; + } + this.props?.addDocument(doc); + } + }); + }); if (promises.length) { Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); } else { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 55c0660c0..a722f552e 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -112,10 +112,10 @@ export class Doc extends RefField { // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter has: (target, key) => key in target.__fields, ownKeys: target => { - let obj = {} as any; + const obj = {} as any; Object.assign(obj, target.___fields); runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); - return Object.keys(obj) + return Object.keys(obj); }, getOwnPropertyDescriptor: (target, prop) => { if (prop.toString() === "__LAYOUT__") { @@ -864,7 +864,7 @@ Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); Scripting.addGlobal(function curPresentationItem() { const curPres = Doc.UserDoc().curPresentation as Doc; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; -}) +}); Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs = new List([doc]); }); Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP && (!excludeCollections || !Cast(d.data, listSpec(Doc), null))); diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts index 47d6bbe83..8041343fd 100644 --- a/src/scraping/buxton/final/BuxtonImporter.ts +++ b/src/scraping/buxton/final/BuxtonImporter.ts @@ -8,6 +8,7 @@ const StreamZip = require('node-stream-zip'); const createImageSizeStream = require("image-size-stream"); import { parseXml } from "libxmljs"; import { strictEqual } from "assert"; +import { Readable, PassThrough } from "stream"; interface DocumentContents { body: string; @@ -293,15 +294,15 @@ async function writeImages(zip: any): Promise { const imageUrls: ImageData[] = []; for (const mediaPath of imageEntries) { - const streamImage = () => new Promise((resolve, reject) => { + const streamImage = () => new Promise((resolve, reject) => { zip.stream(mediaPath, (error: any, stream: any) => error ? reject(error) : resolve(stream)); }); const { width, height, type } = await new Promise(async resolve => { - const sizeStream = createImageSizeStream().on('size', (dimensions: Dimensions) => { + const sizeStream = (createImageSizeStream() as PassThrough).on('size', (dimensions: Dimensions) => { readStream.destroy(); resolve(dimensions); - }); + }).on("error", () => readStream.destroy()); const readStream = await streamImage(); readStream.pipe(sizeStream); }); diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts index 04b724f4b..25c54ee2e 100644 --- a/src/server/ApiManagers/GooglePhotosManager.ts +++ b/src/server/ApiManagers/GooglePhotosManager.ts @@ -8,6 +8,7 @@ import { Opt } from "../../new_fields/Doc"; import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils"; import { Database } from "../database"; import { red } from "colors"; +import { Upload } from "../SharedMediaTypes"; const prefix = "google_photos_"; const remoteUploadError = "None of the preliminary uploads to Google's servers was successful."; @@ -139,7 +140,7 @@ export default class GooglePhotosManager extends ApiManager { return; } let failed = 0; - const completed: Opt[] = []; + const completed: Opt[] = []; for (const { baseUrl } of mediaItems) { // start by getting the content size of the remote image const results = await DashUploadUtils.InspectImage(baseUrl); @@ -151,7 +152,7 @@ export default class GooglePhotosManager extends ApiManager { const { contentSize, ...attributes } = results; // check to see if we have uploaded a Google user content image *specifically via this route* already // that has this exact content size - const found: Opt = await Database.Auxiliary.QueryUploadHistory(contentSize); + const found: Opt = await Database.Auxiliary.QueryUploadHistory(contentSize); if (!found) { // if we haven't, then upload it locally to Dash's server const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, undefined, prefix, false).catch(error => _error(res, downloadError, error)); diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 4d09528f4..8f2a5ea3e 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -4,12 +4,12 @@ import * as formidable from 'formidable'; import v4 = require('uuid/v4'); const AdmZip = require('adm-zip'); import { extname, basename, dirname } from 'path'; -import { createReadStream, createWriteStream, unlink, readFileSync } from "fs"; +import { createReadStream, createWriteStream, unlink } from "fs"; import { publicDirectory, filesDirectory } from ".."; import { Database } from "../database"; -import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils"; +import { DashUploadUtils } from "../DashUploadUtils"; import * as sharp from 'sharp'; -import { AcceptibleMedia } from "../SharedMediaTypes"; +import { AcceptibleMedia, Upload } from "../SharedMediaTypes"; import { normalize } from "path"; const imageDataUri = require('image-data-uri'); @@ -47,7 +47,7 @@ export default class UploadManager extends ApiManager { form.keepExtensions = true; return new Promise(resolve => { form.parse(req, async (_err, _fields, files) => { - const results: any[] = []; + const results: Upload.FileResponse[] = []; for (const key in files) { const result = await DashUploadUtils.upload(files[key]); result && results.push(result); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 913ddc1c3..b66651ef2 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -3,9 +3,9 @@ import { Utils } from '../Utils'; import * as path from 'path'; import * as sharp from 'sharp'; import request = require('request-promise'); -import { ExifData, ExifImage } from 'exif'; +import { ExifImage } from 'exif'; import { Opt } from '../new_fields/Doc'; -import { AcceptibleMedia } from './SharedMediaTypes'; +import { AcceptibleMedia, Upload } from './SharedMediaTypes'; import { filesDirectory } from '.'; import { File } from 'formidable'; import { basename } from "path"; @@ -14,7 +14,7 @@ import { ParsedPDF } from "../server/PdfTypes"; const parse = require('pdf-parse'); import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager'; import { red } from 'colors'; -import { Writable } from 'stream'; +import { Stream } from 'stream'; const requestImageSize = require("../client/util/request-image-size"); export enum SizeSuffix { @@ -40,13 +40,6 @@ export namespace DashUploadUtils { suffix: SizeSuffix; } - export interface ImageFileResponse { - name: string; - path: string; - type: string; - exif: Opt; - } - export const Sizes: { [size: string]: Size } = { SMALL: { width: 100, suffix: SizeSuffix.Small }, MEDIUM: { width: 400, suffix: SizeSuffix.Medium }, @@ -60,20 +53,9 @@ export namespace DashUploadUtils { const size = "content-length"; const type = "content-type"; - export interface ImageUploadInformation { - accessPaths: AccessPathInfo; - exifData: EnrichedExifData; - contentSize?: number; - contentType?: string; - } - - export interface AccessPathInfo { - [suffix: string]: { client: string, server: string }; - } - const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia; - export async function upload(file: File): Promise { + export async function upload(file: File): Promise { const { type, path, name } = file; const types = type.split("/"); @@ -83,33 +65,33 @@ export namespace DashUploadUtils { switch (category) { case "image": if (imageFormats.includes(format)) { - const results = await UploadImage(path, basename(path)); - return { ...results, name, type }; + const result = await UploadImage(path, basename(path)); + return { source: file, result }; } case "video": if (videoFormats.includes(format)) { - return MoveParsedFile(path, Directory.videos); + return MoveParsedFile(file, Directory.videos); } case "application": if (applicationFormats.includes(format)) { - return UploadPdf(path); + return UploadPdf(file); } } console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`)); - return { accessPaths: {} }; + return { source: file, result: new Error(`Could not upload unsupported file (${name}) with upload type (${type}).`) }; } - async function UploadPdf(absolutePath: string) { - const dataBuffer = readFileSync(absolutePath); + async function UploadPdf(file: File) { + const { path, name } = file; + const dataBuffer = readFileSync(path); const result: ParsedPDF = await parse(dataBuffer); - const parsedName = basename(absolutePath); await new Promise((resolve, reject) => { - const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`; + 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()); }); - return MoveParsedFile(absolutePath, Directory.pdfs); + return MoveParsedFile(file, Directory.pdfs); } /** @@ -123,13 +105,13 @@ export namespace DashUploadUtils { * @param {string} prefix is a string prepended to the generated image name in the * event that @param filename is not specified * - * @returns {ImageUploadInformation} This method returns + * @returns {ImageUploadInformation | Error} This method returns * 1) the paths to the uploaded images (plural due to resizing) - * 2) the file name of each of the resized images + * 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; @@ -137,22 +119,6 @@ export namespace DashUploadUtils { return UploadInspectedImage(metadata, filename || metadata.filename, prefix); }; - export interface InspectionResults { - source: string; - requestable: string; - exifData: EnrichedExifData; - contentSize: number; - contentType: string; - nativeWidth: number; - nativeHeight: number; - filename?: string; - } - - export interface EnrichedExifData { - data: ExifData; - error?: string; - } - export async function buildFileDirectories() { const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`)); return Promise.all(pending); @@ -175,7 +141,7 @@ export namespace DashUploadUtils { * * @param source is the path or url to the image in question */ - export const InspectImage = async (source: string): Promise => { + export const InspectImage = async (source: string): Promise => { let rawMatches: RegExpExecArray | null; let filename: string | undefined; if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) { @@ -216,14 +182,17 @@ export namespace DashUploadUtils { }; }; - export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise> { + export async function MoveParsedFile(file: File, destination: Directory): Promise { + const { name, path: sourcePath } = file; return new Promise(resolve => { - const filename = basename(absolutePath); - const destinationPath = serverPathToFile(destination, filename); - rename(absolutePath, destinationPath, error => { - resolve(error ? undefined : { - accessPaths: { - agnostic: getAccessPaths(destination, filename) + const destinationPath = serverPathToFile(destination, name); + rename(sourcePath, destinationPath, error => { + resolve({ + source: file, + result: error ? error : { + accessPaths: { + agnostic: getAccessPaths(destination, name) + } } }); }); @@ -237,16 +206,16 @@ export namespace DashUploadUtils { }; } - export const UploadInspectedImage = async (metadata: 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 extension = `.${remaining.contentType.split("/")[1].toLowerCase()}`; const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${extension}`; const { images } = Directory; - const information: ImageUploadInformation = { + const information: Upload.ImageInformation = { accessPaths: { agnostic: getAccessPaths(images, resolved) }, - ...remaining + ...metadata }; const outputPath = pathToDirectory(Directory.images); const writtenFiles = await outputResizedImages(() => request(requestable), outputPath, resolved, extension); @@ -259,9 +228,9 @@ export namespace DashUploadUtils { return information; }; - const parseExifData = async (source: string): Promise => { + const parseExifData = async (source: string): Promise => { const image = await request.get(source, { encoding: null }); - return new Promise(resolve => { + return new Promise(resolve => { new ExifImage({ image }, (error, data) => { let reason: Opt = undefined; if (error) { @@ -279,18 +248,14 @@ export namespace DashUploadUtils { force: true }; - export interface ReadStreamLike { - pipe: (dest: Writable) => Writable; - } - - export async function outputResizedImages(readStreamSource: () => ReadStreamLike | Promise, outputPath: string, fileName: string, ext: string) { + export async function outputResizedImages(readStreamSource: () => Stream | Promise, outputPath: string, fileName: string, ext: string) { const writtenFiles: { [suffix: string]: string } = {}; for (const { resizer, suffix } of resizers(ext)) { const resolved = writtenFiles[suffix] = InjectSize(fileName, suffix); await new Promise(async (resolve, reject) => { const writeStream = createWriteStream(path.resolve(outputPath, resolved)); - let readStream: ReadStreamLike; const source = readStreamSource(); + let readStream: Stream; if (source instanceof Promise) { readStream = await source; } else { @@ -320,7 +285,7 @@ export namespace DashUploadUtils { initial = initial.webp(); } else if (tiffs.includes(ext)) { initial = initial.tiff(); - } else { + } else if (ext === ".gif") { initial = undefined; } return { diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index 274b4f01e..185e787cc 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -1,3 +1,6 @@ +import { ExifData } from 'exif'; +import { File } from 'formidable'; + export namespace AcceptibleMedia { export const gifs = [".gif"]; export const pngs = [".png"]; @@ -7,4 +10,40 @@ export namespace AcceptibleMedia { export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs]; export const videoFormats = [".mov", ".mp4"]; export const applicationFormats = [".pdf"]; +} + +export namespace Upload { + + export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation { + return "nativeWidth" in uploadResponse; + } + + export interface FileInformation { + accessPaths: AccessPathInfo; + } + + export type FileResponse = { source: File, result: T | Error }; + + export type ImageInformation = FileInformation & InspectionResults; + + export interface AccessPathInfo { + [suffix: string]: { client: string, server: string }; + } + + export interface InspectionResults { + source: string; + requestable: string; + exifData: EnrichedExifData; + contentSize: number; + contentType: string; + nativeWidth: number; + nativeHeight: number; + filename?: string; + } + + export interface EnrichedExifData { + data: ExifData; + error?: string; + } + } \ No newline at end of file diff --git a/src/server/database.ts b/src/server/database.ts index 83ce865c6..055f04c49 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -2,12 +2,12 @@ import * as mongodb from 'mongodb'; import { Transferable } from './Message'; import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; -import { DashUploadUtils } from './DashUploadUtils'; import { Credentials } from 'google-auth-library'; import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; import { IDatabase } from './IDatabase'; import { MemoryDatabase } from './MemoryDatabase'; import * as mongoose from 'mongoose'; +import { Upload } from './SharedMediaTypes'; export namespace Database { @@ -297,7 +297,7 @@ export namespace Database { }; export const QueryUploadHistory = async (contentSize: number) => { - return SanitizedSingletonQuery({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory); + return SanitizedSingletonQuery({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory); }; export namespace GoogleAuthenticationToken { @@ -326,7 +326,7 @@ export namespace Database { } - export const LogUpload = async (information: DashUploadUtils.ImageUploadInformation) => { + export const LogUpload = async (information: Upload.ImageInformation) => { const bundle = { _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)), ...information -- cgit v1.2.3-70-g09d2 From 5c139f0001cd10e5699a716c1e64d2c2c0b2d800 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 28 Feb 2020 12:17:03 -0500 Subject: allowing different text templates to be the default. fixing transparency. removing audio by default from notes. --- src/client/documents/Documents.ts | 5 +++- src/client/util/Import & Export/ImageUtils.ts | 2 +- src/client/util/RichTextSchema.tsx | 8 ++--- src/client/views/InkingControl.tsx | 3 +- src/client/views/TemplateMenu.tsx | 5 ++++ .../collectionFreeForm/CollectionFreeFormView.tsx | 4 +-- .../collections/collectionFreeForm/MarqueeView.tsx | 3 +- src/client/views/nodes/DocumentView.tsx | 6 ++-- src/client/views/nodes/FormattedTextBox.scss | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 34 +++++++++++++--------- src/client/views/nodes/ImageBox.tsx | 19 ++++++------ src/new_fields/documentSchemas.ts | 13 +++++---- 12 files changed, 63 insertions(+), 41 deletions(-) (limited to 'src/client/util/Import & Export') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a0b8a6382..aab44f668 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -77,6 +77,8 @@ export interface DocumentOptions { _gridGap?: number; // gap between items in masonry view _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts + _xPadding?: number; + _yPadding?: number; _itemIndex?: number; // which item index the carousel viewer is showing _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts x?: number; @@ -174,7 +176,7 @@ export namespace Docs { const TemplateMap: TemplateMap = new Map([ [DocumentType.TEXT, { layout: { view: FormattedTextBox, dataField: data }, - options: { _height: 150 } + options: { _height: 150, _xMargin: 10, _yMargin: 10 } }], [DocumentType.HIST, { layout: { view: HistogramBox, dataField: data }, @@ -407,6 +409,7 @@ export namespace Docs { const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "childDropAction", "_annotationOn", "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor", + "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_color", "isButton", "isBackground", "removeDropProperties", "treeViewOpen"]; /** diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index ff909cc6b..ab8c73d15 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -24,7 +24,7 @@ export namespace ImageUtils { const proto = Doc.GetProto(document); proto["data-nativeWidth"] = nativeWidth; proto["data-nativeHeight"] = nativeHeight; - proto.contentSize = contentSize; + proto.contentSize = contentSize ? contentSize : undefined; return data !== undefined; }; diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index a56aac9b1..4a80a1af8 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -910,11 +910,11 @@ export class DashFieldView { } e.preventDefault(); } - if (e.key === "Enter" && e.ctrlKey) { - Doc.addEnumerationToTextField(self._textBoxDoc, node.attrs.fieldKey, [Docs.Create.TextDocument(self._fieldSpan.innerText, { title: self._fieldSpan.innerText })]); - e.preventDefault(); - } else if (e.key === "Enter") { + if (e.key === "Enter") { e.preventDefault(); + if (e.ctrlKey) { + Doc.addEnumerationToTextField(self._textBoxDoc, node.attrs.fieldKey, [Docs.Create.TextDocument(self._fieldSpan.innerText, { title: self._fieldSpan.innerText })]); + } let newText = self._fieldSpan.innerText.startsWith(":=") ? ":=-computed-" : self._fieldSpan.innerText; // look for a document whose id === the fieldKey being displayed. If there's a match, then that document // holds the different enumerated values for the field in the titles of its collected documents. diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 374c2df27..5cd3c265d 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -29,8 +29,7 @@ export class InkingControl { if (number < 0) { number = 0xFFFFFFFF + number + 1; } - - return number.toString(16).toUpperCase(); + return (number < 16 ? "0" : "") + number.toString(16).toUpperCase(); } @undoBatch diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 595c3817e..5029b4074 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -61,6 +61,10 @@ export class TemplateMenu extends React.Component { DocumentView.FloatDoc(topDocView, ex, ey); } + toggleAudio = (e: React.ChangeEvent): void => { + this.props.docViews.map(dv => dv.props.Document._showAudio = e.target.checked); + } + @undoBatch @action toggleTemplate = (event: React.ChangeEvent, template: Template): void => { @@ -102,6 +106,7 @@ export class TemplateMenu extends React.Component { const templateMenu: Array = []; this.props.templates.forEach((checked, template) => templateMenu.push()); + templateMenu.push(); templateMenu.push(); templateMenu.push(); this._addedKeys && Array.from(this._addedKeys).map(layout => diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4458c7dcf..ca8d5e18b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -51,8 +51,8 @@ export const panZoomSchema = createSchema({ arrangeInit: ScriptField, useClusters: "boolean", fitToBox: "boolean", - xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set panTransformType: "string", scrollHeight: "number", fitX: "number", diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b1cca027d..b8e9acf85 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -18,6 +18,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); import { CollectionView } from "../CollectionView"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -104,7 +105,7 @@ export class MarqueeView extends React.Component 48 && e.keyCode <= 57) { const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 06ca9b5f4..7683ad269 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -909,17 +909,19 @@ export class DocumentView extends DocComponent(Docu render() { if (!(this.props.Document instanceof Doc)) return (null); const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); + const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor; const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); const borderRounding = this.layoutDoc.borderRounding; const localScale = fullDegree; + console.log("Background = " + this.props.Document.title + " " + finalColor); const highlightColors = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] : ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - return
Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} style={{ @@ -931,7 +933,7 @@ export class DocumentView extends DocComponent(Docu outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, - background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor, + background: finalColor, width: "100%", height: "100%", opacity: this.Document.opacity diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index c203ca0c3..db2bb751f 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -95,8 +95,8 @@ .formattedTextBox-inner-rounded, .formattedTextBox-inner { - padding: 10px 10px; height: 100%; + white-space: pre-wrap; } // .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index a320cff75..3567dbbd5 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -375,12 +375,15 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"); + public static DefaultLayout: Doc | string | undefined; specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; + this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => FormattedTextBox.DefaultLayout = this.props.Document.proto as Doc, icon: "eye" }); + funcs.push({ description: "Reset Default Layout", event: () => FormattedTextBox.DefaultLayout = undefined, icon: "eye" }); !this.props.Document.expandedTemplate && funcs.push({ description: "Make Template", event: () => { this.props.Document.isTemplateDoc = true; Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document); }, icon: "eye" }); - funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.props.Document._showSidebar = !this.props.Document._showSidebar; }, icon: "expand-arrows-alt" }); - funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Menubar", event: () => { e.stopPropagation(); this.toggleMenubar(); }, icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" }); + funcs.push({ description: "Record Bullet", event: () => this.recordBullet(), icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => funcs.push({ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { @@ -1125,7 +1128,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & onPointerLeave={action(() => this._entered = false)} >
-
+
{!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
this.toggleSidebar()} /> : @@ -1152,15 +1159,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this.toggleSidebar()} />
} -
{ - this._recording ? this.stopDictation(true) : this.recordDictation(); - setTimeout(() => this._editorView!.focus(), 500); - e.stopPropagation(); - }} > - -
+ {!this.props.Document._showAudio ? (null) : +
{ + this._recording ? this.stopDictation(true) : this.recordDictation(); + setTimeout(() => this._editorView!.focus(), 500); + e.stopPropagation(); + }} > + +
}
); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 99000a0a9..c46191270 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -407,15 +407,16 @@ export class ImageBox extends DocAnnotatableComponent
}
-
- -
+ {!this.props.Document._showAudio ? (null) : +
+ +
} {this.considerDownloadIcon} {this.considerGooglePhotosLink()} diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts index f3726cb5a..7006163e0 100644 --- a/src/new_fields/documentSchemas.ts +++ b/src/new_fields/documentSchemas.ts @@ -15,9 +15,14 @@ export const documentSchema = createSchema({ _nativeHeight: "number", // " _width: "number", // width of document in its container's coordinate system _height: "number", // " - _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document - _showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document - _showTitleHover: "string", // the showTitle should be shown only on hover + _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitToBox is set + _xMargin: "number", // margin added on left/right of most documents to add separation from their container + _yMargin: "number", // margin added on top/bottom of most documents to add separation from their container + _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document + _showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document + _showTitleHover: "string", // the showTitle should be shown only on hover + _showAudio: "boolean", // whether to show the audio record icon on documents _freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents _LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews _pivotField: "string", // specifies which field should be used as the timeline/pivot axis @@ -53,8 +58,6 @@ export const documentSchema = createSchema({ strokeWidth: "number", fontSize: "string", fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view - xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set letterSpacing: "string", textTransform: "string" }); -- cgit v1.2.3-70-g09d2