Spaces:
Runtime error
Runtime error
| import React from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import bindAll from 'lodash.bindall'; | |
| import {defineMessages, injectIntl, intlShape} from 'react-intl'; | |
| import BackpackComponent from '../components/backpack/backpack.jsx'; | |
| import { | |
| getBackpackContents, | |
| saveBackpackObject, | |
| deleteBackpackObject, | |
| updateBackpackObject, | |
| soundPayload, | |
| costumePayload, | |
| spritePayload, | |
| codePayload, | |
| LOCAL_API | |
| } from '../lib/backpack-api'; | |
| import DragConstants from '../lib/drag-constants'; | |
| import DropAreaHOC from '../lib/drop-area-hoc.jsx'; | |
| import {connect} from 'react-redux'; | |
| import storage from '../lib/storage'; | |
| import downloadBlob from '../lib/download-blob'; | |
| import VM from 'scratch-vm'; | |
| const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; | |
| const DroppableBackpack = DropAreaHOC(dragTypes)(BackpackComponent); | |
| const messages = defineMessages({ | |
| rename: { | |
| defaultMessage: 'New name:', | |
| description: 'Renaming a backpack item', | |
| id: 'tw.backpack.rename' | |
| } | |
| }); | |
| class Backpack extends React.Component { | |
| constructor (props) { | |
| super(props); | |
| bindAll(this, [ | |
| 'handleDrop', | |
| 'handleToggle', | |
| 'handleDelete', | |
| 'handleExport', | |
| 'handleRename', | |
| 'getBackpackAssetURL', | |
| 'getContents', | |
| 'handleMouseEnter', | |
| 'handleMouseLeave', | |
| 'handleBlockDragEnd', | |
| 'handleBlockDragUpdate', | |
| 'handleMore' | |
| ]); | |
| this.state = { | |
| // While the DroppableHOC manages drop interactions for asset tiles, | |
| // we still need to micromanage drops coming from the block workspace. | |
| // TODO this may be refactorable with the share-the-love logic in SpriteSelectorItem | |
| blockDragOutsideWorkspace: false, | |
| blockDragOverBackpack: false, | |
| error: false, | |
| itemsPerPage: 20, | |
| moreToLoad: false, | |
| loading: false, | |
| expanded: false, | |
| contents: [] | |
| }; | |
| // If a host is given, add it as a web source to the storage module | |
| // TODO remove the hacky flag that prevents double adding | |
| if (props.host && !storage._hasAddedBackpackSource && props.host !== LOCAL_API) { | |
| storage.addWebSource( | |
| [storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound], | |
| this.getBackpackAssetURL | |
| ); | |
| storage._hasAddedBackpackSource = true; | |
| } | |
| } | |
| componentDidMount () { | |
| this.props.vm.addListener('BLOCK_DRAG_END', this.handleBlockDragEnd); | |
| this.props.vm.addListener('BLOCK_DRAG_UPDATE', this.handleBlockDragUpdate); | |
| } | |
| componentWillUnmount () { | |
| this.props.vm.removeListener('BLOCK_DRAG_END', this.handleBlockDragEnd); | |
| this.props.vm.removeListener('BLOCK_DRAG_UPDATE', this.handleBlockDragUpdate); | |
| } | |
| getBackpackAssetURL (asset) { | |
| return `${this.props.host}/${asset.assetId}.${asset.dataFormat}`; | |
| } | |
| handleToggle () { | |
| const newState = !this.state.expanded; | |
| this.setState({expanded: newState, contents: []}, () => { | |
| // Emit resize on window to get blocks to resize | |
| window.dispatchEvent(new Event('resize')); | |
| }); | |
| if (newState) { | |
| this.getContents(); | |
| } | |
| } | |
| handleError (error) { | |
| this.setState({ | |
| error: `${error}`, | |
| loading: false | |
| }); | |
| // Log error to console and make the Promise reject. | |
| throw error; | |
| } | |
| handleDrop (dragInfo) { | |
| let payloader = null; | |
| let presaveAsset = null; | |
| switch (dragInfo.dragType) { | |
| case DragConstants.COSTUME: | |
| payloader = costumePayload; | |
| presaveAsset = dragInfo.payload.asset; | |
| break; | |
| case DragConstants.SOUND: | |
| payloader = soundPayload; | |
| presaveAsset = dragInfo.payload.asset; | |
| break; | |
| case DragConstants.SPRITE: | |
| payloader = spritePayload; | |
| break; | |
| case DragConstants.CODE: | |
| payloader = codePayload; | |
| break; | |
| } | |
| if (!payloader) return; | |
| // Creating the payload is async, so set loading before starting | |
| this.setState({loading: true}, () => { | |
| payloader(dragInfo.payload, this.props.vm) | |
| .then(payload => { | |
| // Force the asset to save to the asset server before storing in backpack | |
| // Ensures any asset present in the backpack is also on the asset server | |
| if (presaveAsset && !presaveAsset.clean && !this.props.host === LOCAL_API) { | |
| return storage.store( | |
| presaveAsset.assetType, | |
| presaveAsset.dataFormat, | |
| presaveAsset.data, | |
| presaveAsset.assetId | |
| ).then(() => payload); | |
| } | |
| return payload; | |
| }) | |
| .then(payload => saveBackpackObject({ | |
| host: this.props.host, | |
| token: this.props.token, | |
| username: this.props.username, | |
| ...payload | |
| })) | |
| .then(item => { | |
| this.setState({ | |
| loading: false, | |
| contents: [item].concat(this.state.contents) | |
| }); | |
| }) | |
| .catch(error => { | |
| this.handleError(error); | |
| }); | |
| }); | |
| } | |
| handleDelete (id) { | |
| this.setState({loading: true}, () => { | |
| deleteBackpackObject({ | |
| host: this.props.host, | |
| token: this.props.token, | |
| username: this.props.username, | |
| id: id | |
| }) | |
| .then(() => { | |
| this.setState({ | |
| loading: false, | |
| contents: this.state.contents.filter(o => o.id !== id) | |
| }); | |
| }) | |
| .catch(error => { | |
| this.handleError(error); | |
| }); | |
| }); | |
| } | |
| handleExport (id) { | |
| const item = this.findItemById(id); | |
| if (!item) return; | |
| if (!item.bodyData) return; | |
| const buffer = item.bodyData; | |
| const blob = new Blob([buffer], { type: item.mime }); | |
| let recommendedName = item.name; | |
| if (item.type === 'sprite') { | |
| recommendedName += '.pms'; | |
| } | |
| if (item.type === 'script') { | |
| recommendedName += '.pmb'; | |
| } | |
| downloadBlob(recommendedName, blob); | |
| } | |
| findItemById (id) { | |
| return this.state.contents.find(i => i.id === id); | |
| } | |
| async handleRename (id) { | |
| const item = this.findItemById(id); | |
| // prompt() returns Promise in desktop app | |
| // eslint-disable-next-line no-alert | |
| const newName = await prompt(this.props.intl.formatMessage(messages.rename), item.name); | |
| if (!newName) { | |
| return; | |
| } | |
| this.setState({loading: true}, () => { | |
| updateBackpackObject({ | |
| host: this.props.host, | |
| ...item, | |
| name: newName | |
| }) | |
| .then(newItem => { | |
| this.setState({ | |
| loading: false, | |
| contents: this.state.contents.map(i => (i === item ? newItem : i)) | |
| }); | |
| }) | |
| .catch(error => { | |
| this.handleError(error); | |
| }); | |
| }); | |
| } | |
| getContents () { | |
| if ((this.props.token && this.props.username) || this.props.host === LOCAL_API) { | |
| this.setState({loading: true, error: false}, () => { | |
| getBackpackContents({ | |
| host: this.props.host, | |
| token: this.props.token, | |
| username: this.props.username, | |
| offset: this.state.contents.length, | |
| limit: this.state.itemsPerPage | |
| }) | |
| .then(contents => { | |
| this.setState({ | |
| contents: this.state.contents.concat(contents), | |
| moreToLoad: contents.length === this.state.itemsPerPage, | |
| loading: false | |
| }); | |
| }) | |
| .catch(error => { | |
| this.handleError(error); | |
| }); | |
| }); | |
| } | |
| } | |
| handleBlockDragUpdate (isOutsideWorkspace) { | |
| this.setState({ | |
| blockDragOutsideWorkspace: isOutsideWorkspace | |
| }); | |
| } | |
| handleMouseEnter () { | |
| if (this.state.blockDragOutsideWorkspace) { | |
| this.setState({ | |
| blockDragOverBackpack: true | |
| }); | |
| } | |
| } | |
| handleMouseLeave () { | |
| this.setState({ | |
| blockDragOverBackpack: false | |
| }); | |
| } | |
| handleBlockDragEnd (blocks, topBlockId) { | |
| if (this.state.blockDragOverBackpack) { | |
| this.handleDrop({ | |
| dragType: DragConstants.CODE, | |
| payload: { | |
| blockObjects: this.props.vm.exportStandaloneBlocks(blocks), | |
| topBlockId: topBlockId | |
| } | |
| }); | |
| } | |
| this.setState({ | |
| blockDragOverBackpack: false, | |
| blockDragOutsideWorkspace: false | |
| }); | |
| } | |
| handleMore () { | |
| this.getContents(); | |
| } | |
| render () { | |
| return ( | |
| <DroppableBackpack | |
| blockDragOver={this.state.blockDragOverBackpack} | |
| contents={this.state.contents} | |
| error={this.state.error} | |
| expanded={this.state.expanded} | |
| loading={this.state.loading} | |
| showMore={this.state.moreToLoad} | |
| onDelete={this.handleDelete} | |
| onExport={this.handleExport} | |
| onRename={this.handleRename} | |
| onDrop={this.handleDrop} | |
| onMore={this.handleMore} | |
| onMouseEnter={this.handleMouseEnter} | |
| onMouseLeave={this.handleMouseLeave} | |
| onToggle={this.props.host ? this.handleToggle : null} | |
| /> | |
| ); | |
| } | |
| } | |
| Backpack.propTypes = { | |
| intl: intlShape, | |
| host: PropTypes.string, | |
| token: PropTypes.string, | |
| username: PropTypes.string, | |
| vm: PropTypes.instanceOf(VM) | |
| }; | |
| const getTokenAndUsername = state => { | |
| // Look for the session state provided by scratch-www | |
| if (state.session && state.session.session && state.session.session.user) { | |
| return { | |
| token: state.session.session.user.token, | |
| username: state.session.session.user.username | |
| }; | |
| } | |
| // Otherwise try to pull testing params out of the URL, or return nulls | |
| // TODO a hack for testing the backpack | |
| const tokenMatches = window.location.href.match(/[?&]token=([^&]*)&?/); | |
| const usernameMatches = window.location.href.match(/[?&]username=([^&]*)&?/); | |
| return { | |
| token: tokenMatches ? tokenMatches[1] : null, | |
| username: usernameMatches ? usernameMatches[1] : null | |
| }; | |
| }; | |
| const mapStateToProps = state => Object.assign( | |
| { | |
| dragInfo: state.scratchGui.assetDrag, | |
| vm: state.scratchGui.vm, | |
| blockDrag: state.scratchGui.blockDrag | |
| }, | |
| getTokenAndUsername(state) | |
| ); | |
| const mapDispatchToProps = () => ({}); | |
| export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Backpack)); | |