File uploading abstractions
This commit is contained in:
1 parent
2bf417045e
commit
73cd5b3019
4 files changed
+196
-160
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Reference in new issue
Block a user