import React from 'react'; import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; import BackpackComponent from '../components/backpack/backpack.jsx'; import { getBackpackContents, saveBackpackObject, deleteBackpackObject, soundPayload, costumePayload, spritePayload, codePayload } 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 VM from 'scratch-vm'; const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; const DroppableBackpack = DropAreaHOC(dragTypes)(BackpackComponent); class Backpack extends React.Component { constructor (props) { super(props); bindAll(this, [ 'handleDrop', 'handleToggle', 'handleDelete', '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) { 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(); } } handleDrop (dragInfo) { let payloader = null; switch (dragInfo.dragType) { case DragConstants.COSTUME: payloader = costumePayload; break; case DragConstants.SOUND: payloader = soundPayload; 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 => 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(() => { this.setState({error: true, loading: false}); }); }); } 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(() => { this.setState({error: true, loading: false}); }); }); } getContents () { if (this.props.token && this.props.username) { 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(() => { this.setState({error: true, loading: false}); }); }); } } 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: 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} onDrop={this.handleDrop} onMore={this.handleMore} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onToggle={this.props.host ? this.handleToggle : null} /> ); } } Backpack.propTypes = { 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 connect(mapStateToProps, mapDispatchToProps)(Backpack);