soiz1's picture
Upload 2891 files
6bcb42f verified
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import { defineMessages, intlShape, injectIntl } from 'react-intl';
import VM from 'scratch-vm';
import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import PaintEditorWrapper from './paint-editor-wrapper.jsx';
import { connect } from 'react-redux';
import { handleFileUpload, costumeUpload } from '../lib/file-uploader.js';
import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import DragConstants from '../lib/drag-constants';
import { emptyCostume } from '../lib/empty-assets';
import sharedMessages from '../lib/shared-messages';
import downloadBlob from '../lib/download-blob';
import {
openCostumeLibrary,
openBackdropLibrary
} from '../reducers/modals';
import {
activateTab,
SOUNDS_TAB_INDEX
} from '../reducers/editor-tab';
import { setRestore } from '../reducers/restore-deletion';
import { showStandardAlert, closeAlertWithId } from '../reducers/alerts';
import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg';
import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
import paintIcon from '../components/action-menu/icon--paint.svg';
import surpriseIcon from '../components/action-menu/icon--surprise.svg';
import searchIcon from '../components/action-menu/icon--search.svg';
import { getCostumeLibrary, getBackdropLibrary } from '../lib/libraries/tw-async-libraries';
let messages = defineMessages({
addLibraryBackdropMsg: {
defaultMessage: 'Choose a Backdrop',
description: 'Button to add a backdrop in the editor tab',
id: 'gui.costumeTab.addBackdropFromLibrary'
},
addLibraryCostumeMsg: {
defaultMessage: 'Choose a Costume',
description: 'Button to add a costume in the editor tab',
id: 'gui.costumeTab.addCostumeFromLibrary'
},
addBlankCostumeMsg: {
defaultMessage: 'Paint',
description: 'Button to add a blank costume in the editor tab',
id: 'gui.costumeTab.addBlankCostume'
},
addSurpriseCostumeMsg: {
defaultMessage: 'Surprise',
description: 'Button to add a surprise costume in the editor tab',
id: 'gui.costumeTab.addSurpriseCostume'
},
addFileBackdropMsg: {
defaultMessage: 'Upload Backdrop',
description: 'Button to add a backdrop by uploading a file in the editor tab',
id: 'gui.costumeTab.addFileBackdrop'
},
addFileCostumeMsg: {
defaultMessage: 'Upload Costume',
description: 'Button to add a costume by uploading a file in the editor tab',
id: 'gui.costumeTab.addFileCostume'
}
});
messages = { ...messages, ...sharedMessages };
class CostumeTab extends React.Component {
constructor(props) {
super(props);
bindAll(this, [
'handleSelectCostume',
'handleDeleteCostume',
'handleDuplicateCostume',
'handleExportCostume',
'handleNewCostume',
'handleNewBlankCostume',
'handleSurpriseCostume',
'handleSurpriseBackdrop',
'handleFileUploadClick',
'handleCostumeUpload',
'handleDrop',
'setFileInput'
]);
const {
editingTarget,
sprites,
stage
} = props;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (target && target.currentCostume) {
this.state = { selectedCostumeIndex: target.currentCostume };
} else {
this.state = { selectedCostumeIndex: 0 };
}
}
componentWillReceiveProps(nextProps) {
const {
editingTarget,
sprites,
stage
} = nextProps;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (!target || !target.costumes) {
return;
}
if (this.props.editingTarget === editingTarget) {
// If costumes have been added or removed, change costumes to the editing target's
// current costume.
const oldTarget = this.props.sprites[editingTarget] ?
this.props.sprites[editingTarget] : this.props.stage;
// @todo: Find and switch to the index of the costume that is new. This is blocked by
// https://github.com/LLK/scratch-vm/issues/967
// Right now, you can land on the wrong costume if a costume changing script is running.
if (oldTarget.costumeCount !== target.costumeCount) {
this.setState({ selectedCostumeIndex: target.currentCostume });
}
} else {
// If switching editing targets, update the costume index
this.setState({ selectedCostumeIndex: target.currentCostume });
}
}
handleSelectCostume(costumeIndex) {
this.props.vm.editingTarget.setCostume(costumeIndex);
this.setState({ selectedCostumeIndex: costumeIndex });
}
handleDeleteCostume(costumeIndex) {
const restoreCostumeFun = this.props.vm.deleteCostume(costumeIndex);
this.props.dispatchUpdateRestore({
restoreFun: restoreCostumeFun,
deletedItem: 'Costume'
});
}
handleDuplicateCostume(costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex);
}
handleExportCostume(costumeIndex) {
const item = this.props.vm.editingTarget.sprite.costumes[costumeIndex];
const blob = new Blob([
this.props.vm.getExportedCostume(item)
], { type: item.asset.assetType.contentType });
downloadBlob(`${item.name}.${item.asset.dataFormat}`, blob);
}
handleNewCostume(costume, fromCostumeLibrary, targetId) {
const costumes = Array.isArray(costume) ? costume : [costume];
return Promise.all(costumes.map(c => {
if (fromCostumeLibrary) {
return this.props.vm.addCostumeFromLibrary(c.md5, c);
}
// If targetId is falsy, VM should default it to editingTarget.id
// However, targetId should be provided to prevent #5876,
// if making new costume takes a while
return this.props.vm.addCostume(c.md5, c, targetId);
}));
}
handleNewBlankCostume() {
const name = this.props.vm.editingTarget.isStage ?
this.props.intl.formatMessage(messages.backdrop, { index: 1 }) :
this.props.intl.formatMessage(messages.costume, { index: 1 });
this.handleNewCostume(emptyCostume(name));
}
async handleSurpriseCostume() {
const costumeLibraryContent = await getCostumeLibrary();
const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)];
const vmCostume = {
name: item.name,
md5: item.md5ext,
rotationCenterX: item.rotationCenterX,
rotationCenterY: item.rotationCenterY,
bitmapResolution: item.bitmapResolution,
skinId: null
};
if (item.fromPenguinModLibrary) {
vmCostume.fromPenguinModLibrary = true;
vmCostume.libraryId = item.libraryFilePage;
vmCostume.dataFormat = item.dataFormat;
};
this.handleNewCostume(vmCostume, true /* fromCostumeLibrary */);
}
async handleSurpriseBackdrop() {
const backdropLibraryContent = await getBackdropLibrary();
const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
const vmCostume = {
name: item.name,
md5: item.md5ext,
rotationCenterX: item.rotationCenterX,
rotationCenterY: item.rotationCenterY,
bitmapResolution: item.bitmapResolution,
skinId: null
};
if (item.fromPenguinModLibrary) {
vmCostume.fromPenguinModLibrary = true;
vmCostume.libraryId = item.libraryFilePage;
vmCostume.dataFormat = item.dataFormat;
};
this.handleNewCostume(vmCostume);
}
handleCostumeUpload(e) {
const vm = this.props.vm;
const targetId = this.props.vm.editingTarget.id;
this.props.onShowImporting();
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
costumeUpload(buffer, fileType, vm, vmCostumes => {
vmCostumes.forEach((costume, i) => {
costume.name = `${fileName}${i ? i + 1 : ''}`;
});
this.handleNewCostume(vmCostumes, false, targetId).then(() => {
if (fileIndex === fileCount - 1) {
this.props.onCloseImporting();
}
});
}, this.props.onCloseImporting);
}, this.props.onCloseImporting);
}
handleFileUploadClick() {
this.fileInput.click();
}
handleDrop(dropInfo) {
if (dropInfo.dragType === DragConstants.COSTUME) {
const sprite = this.props.vm.editingTarget.sprite;
const activeCostume = sprite.costumes[this.state.selectedCostumeIndex];
this.props.vm.reorderCostume(this.props.vm.editingTarget.id,
dropInfo.index, dropInfo.newIndex);
this.setState({ selectedCostumeIndex: sprite.costumes.indexOf(activeCostume) });
} else if (dropInfo.dragType === DragConstants.BACKPACK_COSTUME) {
this.props.vm.addCostume(dropInfo.payload.body, {
name: dropInfo.payload.name
});
} else if (dropInfo.dragType === DragConstants.BACKPACK_SOUND) {
this.props.onActivateSoundsTab();
this.props.vm.addSound({
md5: dropInfo.payload.body,
name: dropInfo.payload.name
});
}
}
setFileInput(input) {
this.fileInput = input;
}
formatCostumeDetails(size, optResolution) {
// If no resolution is given, assume that the costume is an SVG
const resolution = optResolution ? optResolution : 1;
// Convert size to stage units by dividing by resolution
// Round up width and height for scratch-flash compatibility
// https://github.com/LLK/scratch-flash/blob/9fbac92ef3d09ceca0c0782f8a08deaa79e4df69/src/ui/media/MediaInfo.as#L224-L237
return `${Math.ceil(size[0] / resolution)} x ${Math.ceil(size[1] / resolution)}`;
}
render() {
const {
dispatchUpdateRestore, // eslint-disable-line no-unused-vars
intl,
isRtl,
onNewLibraryBackdropClick,
onNewLibraryCostumeClick,
vm
} = this.props;
if (!vm.editingTarget) {
return null;
}
const isStage = vm.editingTarget.isStage;
const target = vm.editingTarget.sprite;
const addLibraryMessage = isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg;
const addFileMessage = isStage ? messages.addFileBackdropMsg : messages.addFileCostumeMsg;
const addSurpriseFunc = isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume;
const addLibraryFunc = isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick;
const addLibraryIcon = isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon;
const costumeData = target.costumes ? target.costumes.map(costume => ({
name: costume.name,
asset: costume.asset,
details: costume.size ? this.formatCostumeDetails(costume.size, costume.bitmapResolution) : null,
dragPayload: costume
})) : [];
return (
<AssetPanel
buttons={[
{
title: intl.formatMessage(addLibraryMessage),
img: addLibraryIcon,
onClick: addLibraryFunc
},
{
title: intl.formatMessage(addFileMessage),
img: fileUploadIcon,
onClick: this.handleFileUploadClick,
fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .jfif, .webp, .gif',
fileChange: this.handleCostumeUpload,
fileInput: this.setFileInput,
fileMultiple: true
},
{
title: intl.formatMessage(messages.addSurpriseCostumeMsg),
img: surpriseIcon,
onClick: addSurpriseFunc
},
{
title: intl.formatMessage(messages.addBlankCostumeMsg),
img: paintIcon,
onClick: this.handleNewBlankCostume
},
{
title: intl.formatMessage(addLibraryMessage),
img: searchIcon,
onClick: addLibraryFunc
}
]}
dragType={DragConstants.COSTUME}
isRtl={isRtl}
items={costumeData}
selectedItemIndex={this.state.selectedCostumeIndex}
onDeleteClick={target && target.costumes && target.costumes.length > 1 ?
this.handleDeleteCostume : null}
onDrop={this.handleDrop}
onDuplicateClick={this.handleDuplicateCostume}
onExportClick={this.handleExportCostume}
onItemClick={this.handleSelectCostume}
>
{target.costumes ?
<PaintEditorWrapper
selectedCostumeIndex={this.state.selectedCostumeIndex}
isDark={this.props.isDark}
/> :
null
}
</AssetPanel>
);
}
}
CostumeTab.propTypes = {
dispatchUpdateRestore: PropTypes.func,
editingTarget: PropTypes.string,
intl: intlShape,
isDark: PropTypes.bool,
isRtl: PropTypes.bool,
onActivateSoundsTab: PropTypes.func.isRequired,
onCloseImporting: PropTypes.func.isRequired,
onNewLibraryBackdropClick: PropTypes.func.isRequired,
onNewLibraryCostumeClick: PropTypes.func.isRequired,
onShowImporting: PropTypes.func.isRequired,
sprites: PropTypes.shape({
id: PropTypes.shape({
costumes: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string,
name: PropTypes.string.isRequired,
skinId: PropTypes.number
}))
})
}),
stage: PropTypes.shape({
sounds: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired
}))
}),
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
editingTarget: state.scratchGui.targets.editingTarget,
isRtl: state.locales.isRtl,
sprites: state.scratchGui.targets.sprites,
stage: state.scratchGui.targets.stage,
dragging: state.scratchGui.assetDrag.dragging
});
const mapDispatchToProps = dispatch => ({
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onNewLibraryBackdropClick: e => {
e.preventDefault();
dispatch(openBackdropLibrary());
},
onNewLibraryCostumeClick: e => {
e.preventDefault();
dispatch(openCostumeLibrary());
},
dispatchUpdateRestore: restoreState => {
dispatch(setRestore(restoreState));
},
onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')),
onShowImporting: () => dispatch(showStandardAlert('importingAsset'))
});
export default errorBoundaryHOC('Costume Tab')(
injectIntl(connect(
mapStateToProps,
mapDispatchToProps
)(CostumeTab))
);