File uploading abstractions

This commit is contained in:
Karishma Chadha committed 2018-04-24 12:54:38 -04:00
1 parent 2bf417045e
commit 73cd5b3019
4 files changed
+196 -160

No files matched your search

+8 -64
View File
@@ -2,15 +2,15 @@ import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import {defineMessages, intlShape, injectIntl} from 'react-intl';
import {importBitmap} from 'scratch-svg-renderer';
import VM from 'scratch-vm';
import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import PaintEditorWrapper from './paint-editor-wrapper.jsx';
import CostumeLibrary from './costume-library.jsx';
import BackdropLibrary from './backdrop-library.jsx';
import {connect} from 'react-redux';
import log from '../lib/log.js';
import {handleFileUpload, costumeUpload} from '../lib/fileUploader.js';
import {
closeCostumeLibrary,
@@ -169,68 +169,12 @@ class CostumeTab extends React.Component {
this.props.vm.addCostume(item.md5, vmCostume);
}
handleCostumeUpload (e) {
const thisFileInput = e.target;
let thisFile = null;
const reader = new FileReader();
reader.onload = () => {
// Reset the file input value now that we have everything we need
// so that the user can upload the same image multiple times
// if they choose
thisFileInput.value = null;
const storage = this.props.vm.runtime.storage;
const fileType = thisFile.type; // check what the browser thinks this is
// Only handling png and svg right now
let costumeFormat = null;
let assetType = null;
if (fileType === 'image/svg+xml') {
costumeFormat = storage.DataFormat.SVG;
assetType = storage.AssetType.ImageVector;
} else if (fileType === 'image/jpeg') {
costumeFormat = storage.DataFormat.JPG;
assetType = storage.AssetType.ImageBitmap;
} else if (fileType === 'image/png') {
costumeFormat = storage.DataFormat.PNG;
assetType = storage.AssetType.ImageBitmap;
}
if (!costumeFormat) return;
const addCostumeFromBuffer = (function (error, costumeBuffer) {
if (error) {
log.warn(`An error occurred while trying to extract image data: ${error}`);
return;
}
const md5 = storage.builtinHelper.cache(
assetType, costumeFormat, costumeBuffer);
const md5Ext = `${md5}.${costumeFormat}`;
const vmCostume = {
name: 'costume1',
dataFormat: costumeFormat,
md5: `${md5Ext}`
};
this.props.vm.addCostume(md5Ext, vmCostume);
}).bind(this);
if (costumeFormat === storage.DataFormat.SVG) {
// Must pass in file data as a Uint8Array,
// passing in an array buffer causes the sprite/costume
// thumbnails to not display because the data URI for the costume
// is invalid
addCostumeFromBuffer(null, new Uint8Array(reader.result));
} else {
// otherwise it's a bitmap
importBitmap(reader.result, addCostumeFromBuffer);
}
};
if (thisFileInput.files) {
thisFile = thisFileInput.files[0];
reader.readAsArrayBuffer(thisFile);
}
const storage = this.props.vm.runtime.storage;
const handleNewCostume = function (md5Ext, vmCostume) {
this.props.vm.addCostume(md5Ext, vmCostume);
}.bind(this);
const costumeOnload = costumeUpload.bind(this, storage, handleNewCostume);
handleFileUpload(e.target, costumeOnload);
}
handleFileUploadClick () {
this.fileInput.click();
+6 -32
View File
@@ -16,6 +16,7 @@ import SoundEditor from './sound-editor.jsx';
import SoundLibrary from './sound-library.jsx';
import soundLibraryContent from '../lib/libraries/sounds.json';
import {handleFileUpload, soundUpload} from '../lib/fileUploader.js';
import {connect} from 'react-redux';
@@ -106,41 +107,14 @@ class SoundTab extends React.Component {
}
handleSoundUpload (e) {
const thisFileInput = e.target;
let thisFile = null;
const reader = new FileReader();
reader.onload = () => {
// Reset the file input value now that we have everything we need
// so that the user can upload the same sound multiple times if
// they choose
thisFileInput.value = null;
// Cache the sound in storage
const soundBuffer = reader.result;
const storage = this.props.vm.runtime.storage;
const fileType = thisFile.type; // what file type does the browser think this is
const soundFormat = fileType === 'audio/mp3' ? storage.DataFormat.MP3 : storage.DataFormat.WAV;
const md5 = storage.builtinHelper.cache(
storage.AssetType.Sound,
soundFormat,
new Uint8Array(soundBuffer),
);
// Add the sound to vm
const newSound = {
format: '',
name: 'sound1',
dataFormat: soundFormat,
md5: `${md5}.${soundFormat}`
};
const storage = this.props.vm.runtime.storage;
const handleSound = function (newSound) {
this.props.vm.addSound(newSound).then(() => {
this.handleNewSound();
});
};
if (thisFileInput.files) {
thisFile = thisFileInput.files[0];
reader.readAsArrayBuffer(thisFile);
}
}.bind(this);
const soundOnload = soundUpload.bind(this, storage, handleSound);
handleFileUpload(e.target, soundOnload);
}
setFileInput (input) {
+8 -64
View File
@@ -10,8 +10,7 @@ import StageSelectorComponent from '../components/stage-selector/stage-selector.
import backdropLibraryContent from '../lib/libraries/backdrops.json';
import costumeLibraryContent from '../lib/libraries/costumes.json';
import {importBitmap} from 'scratch-svg-renderer';
import log from '../lib/log.js';
import {handleFileUpload, costumeUpload} from '../lib/fileUploader';
class StageSelector extends React.Component {
constructor (props) {
@@ -56,68 +55,13 @@ class StageSelector extends React.Component {
}
}
handleBackdropUpload (e) {
const thisFileInput = e.target;
let thisFile = null;
const reader = new FileReader();
reader.onload = () => {
// Reset the file input value now that we have everything we need
// so that the user can upload the same image multiple times
// if they choose
thisFileInput.value = null;
// Cache the image in storage
const storage = this.props.vm.runtime.storage;
const fileType = thisFile.type; // check what the browser thinks this is
// Only handling png and svg right now
let backdropFormat = null;
let assetType = null;
if (fileType === 'image/svg+xml') {
backdropFormat = storage.DataFormat.SVG;
assetType = storage.AssetType.ImageVector;
} else if (fileType === 'image/jpeg') {
backdropFormat = storage.DataFormat.JPG;
assetType = storage.AssetType.ImageBitmap;
} else if (fileType === 'image/png') {
backdropFormat = storage.DataFormat.PNG;
assetType = storage.AssetType.ImageBitmap;
}
if (!backdropFormat) return;
const addBackdropFromBuffer = (function (error, backdropBuffer) {
if (error) {
log.warn(`An error occurred while trying to extract image data: ${error}`);
return;
}
const md5 = storage.builtinHelper.cache(
assetType, backdropFormat, backdropBuffer);
const md5Ext = `${md5}.${backdropFormat}`;
const vmBackdrop = {
name: 'backdrop1',
dataFormat: backdropFormat,
md5: `${md5Ext}`
};
this.props.vm.addBackdrop(md5Ext, vmBackdrop);
this.props.onActivateTab(COSTUMES_TAB_INDEX);
}).bind(this);
if (backdropFormat === storage.DataFormat.SVG) {
// Must pass in file data as a Uint8Array,
// passing in an array buffer causes the sprite/costume
// thumbnails to not display because the data URI for the costume
// is invalid
addBackdropFromBuffer(null, new Uint8Array(reader.result));
} else {
// otherwise it's a bitmap
importBitmap(reader.result, addBackdropFromBuffer);
}
};
if (thisFileInput.files) {
thisFile = thisFileInput.files[0];
reader.readAsArrayBuffer(thisFile);
}
const storage = this.props.vm.runtime.storage;
const handleNewBackdrop = function (md5Ext, vmBackdrop) {
this.props.vm.addBackdrop(md5Ext, vmBackdrop);
this.props.onActivateTab(COSTUMES_TAB_INDEX);
}.bind(this);
const costumeOnload = costumeUpload.bind(this, storage, handleNewBackdrop);
handleFileUpload(e.target, costumeOnload);
}
handleFileUploadClick () {
this.fileInput.click();
+174
View File
@@ -0,0 +1,174 @@
import {importBitmap} from 'scratch-svg-renderer';
import log from './log.js';
/**
* Extract the file name given a string of the form fileName + ext
* @param {string} nameExt File name + extension (e.g. 'my_image.png')
* @return {string} The name without the extension, or the full name if
* there was no '.' in the string (e.g. 'my_image')
*/
const extractFileName = function (nameExt) {
// There could be multiple dots, but get the stuff before the first .
const nameParts = nameExt.split('.', 1); // we only care about the first .
return nameParts[0];
};
/**
* Handle a file upload given the input element that contains the file,
* and a function to handle loading the file.
* @param {Input} fileInput The <input/> element that contains the file being loaded
* @param {Function} onload The function that handles loading the file
*/
const handleFileUpload = function (fileInput, onload) {
let thisFile = null;
const reader = new FileReader();
reader.onload = () => {
// Reset the file input value now that we have everything we need
// so that the user can upload the same sound multiple times if
// they choose
fileInput.value = null;
const fileType = thisFile.type;
const fileName = extractFileName(thisFile.name);
onload(reader.result, fileType, fileName);
};
if (fileInput.files) {
thisFile = fileInput.files[0];
reader.readAsArrayBuffer(thisFile);
}
};
/**
* @typedef VMAsset
* @property {string} name The user-readable name of this asset - This will
* automatically get translated to a fresh name if this one already exists in the
* scope of this vm asset (e.g. if a sound already exists with the same name for
* the same target)
* @property {string} dataFormat The data format of this asset, typically
* the extension to be used for that particular asset, e.g. 'svg' for vector images
* @property {string} md5 The md5 hash of the asset data, followed by '.'' and dataFormat
*/
/**
* Cache an asset (costume, sound) in storage and return an object representation
* of the asset to track in the VM.
* @param {ScratchStorage} storage The storage to cache the asset in
* @param {string} fileName The name of the asset
* @param {AssetType} assetType A ScratchStorage AssetType indicating what kind of
* asset this is.
* @param {string} dataFormat The format of this data (typically the file extension)
* @param {UInt8Array} data The asset data buffer
* @return {VMAsset} An object representing this asset and relevant information
* which can be used to look up the data in storage
*/
const cacheAsset = function (storage, fileName, assetType, dataFormat, data) {
const md5 = storage.builtinHelper.cache(
assetType,
dataFormat,
data
);
return {
name: fileName,
dataFormat: dataFormat,
md5: `${md5}.${dataFormat}`
};
};
/**
* Handles loading a costume or a backdrop using the provided, context-relevant information.
* @param {ScratchStorage} storage The ScratchStorage instance to cache the costume data
* @param {Function} then The function to execute on the costume object returned after
* caching this costume in storage - This function should be responsible for
* adding the costume to the VM and handling other UI flow that should come after adding the costume
* @param {ArrayBuffer} fileData The costume data to load
* @param {string} fileType The MIME type of this file
* @param {string} costumeName The user-readable name to use for the costume.
*/
const costumeUpload = function (storage, then, fileData, fileType, costumeName) {
// Only handling png and svg right now
let costumeFormat = null;
let assetType = null;
switch (fileType) {
case 'image/svg+xml': {
costumeFormat = storage.DataFormat.SVG;
assetType = storage.AssetType.ImageVector;
break;
}
case 'image/jpeg': {
costumeFormat = storage.DataFormat.JPG;
assetType = storage.AssetType.ImageBitmap;
break;
}
case 'image/png': {
costumeFormat = storage.DataFormat.PNG;
assetType = storage.AssetType.ImageBitmap;
break;
}
default:
return;
}
const addCostumeFromBuffer = (function (error, costumeBuffer) {
if (error) {
log.warn(`An error occurred while trying to extract image data: ${error}`);
return;
}
const vmCostume = cacheAsset(storage, costumeName, assetType, costumeFormat, costumeBuffer);
then(vmCostume.md5, vmCostume);
});
if (costumeFormat === storage.DataFormat.SVG) {
// Must pass in file data as a Uint8Array,
// passing in an array buffer causes the sprite/costume
// thumbnails to not display because the data URI for the costume
// is invalid
addCostumeFromBuffer(null, new Uint8Array(fileData));
} else {
// otherwise it's a bitmap
importBitmap(fileData, addCostumeFromBuffer);
}
};
/**
* Handles loading a sound using the provided, context-relevant information.
* @param {ScratchStorage} storage The ScratchStorage instance to cache the sound data
* @param {Function} then The function to execute on the sound object of type VMAsset
* This function should be responsible for adding the sound to the VM
* as well as handling other UI flow that should come after adding the sound
* @param {ArrayBuffer} fileData The sound data to load
* @param {string} fileType The MIME type of this file; This function will exit
* early if the fileType is unexpected.
* @param {string} soundName The user-readable name to use for the sound.
*/
const soundUpload = function (storage, then, fileData, fileType, soundName) {
let soundFormat;
switch (fileType) {
case 'audio/mp3': {
soundFormat = storage.DataFormat.MP3;
break;
}
case 'audio/wav': { // TODO support audio/x-wav? Do we see this in the wild?
soundFormat = storage.DataFormat.WAV;
break;
}
default:
return;
}
const vmSound = cacheAsset(
storage,
soundName,
storage.AssetType.Sound,
soundFormat,
new Uint8Array(fileData));
then(vmSound);
};
export {
handleFileUpload,
costumeUpload,
soundUpload
};