Spaces:
Running
Running
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)); | |