Skip to content
7 changes: 5 additions & 2 deletions packages/scratch-gui/src/components/gui/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const GUIComponent = props => {
authorAvatarBadge,
basePath,
backdropLibraryVisible,
backpackConfigured,
backpackHost,
backpackVisible,
blocksId,
Expand Down Expand Up @@ -520,7 +521,7 @@ const GUIComponent = props => {
/> : null}
</TabPanel>
</Tabs>
{backpackVisible ? (
{backpackVisible && backpackConfigured ? (
<Backpack
host={backpackHost}
ariaRole="region"
Expand Down Expand Up @@ -575,6 +576,7 @@ GUIComponent.propTypes = {
authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // can be false
authorAvatarBadge: PropTypes.number,
backdropLibraryVisible: PropTypes.bool,
backpackConfigured: PropTypes.bool,
backpackHost: PropTypes.string,
backpackVisible: PropTypes.bool,
basePath: PropTypes.string,
Expand Down Expand Up @@ -690,7 +692,8 @@ const mapStateToProps = state => ({
blocksId: state.scratchGui.timeTravel.year.toString(),
stageSizeMode: state.scratchGui.stageSize.stageSize,
colorMode: state.scratchGui.settings.colorMode,
theme: state.scratchGui.settings.theme
theme: state.scratchGui.settings.theme,
backpackConfigured: !!state.scratchGui.config.storage?.backpackStorage
});

const mapDispatchToProps = dispatch => ({
Expand Down
97 changes: 55 additions & 42 deletions packages/scratch-gui/src/containers/backpack.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ 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 soundPayload from '../lib/backpack/sound-payload';
import costumePayload from '../lib/backpack/costume-payload';
import spritePayload from '../lib/backpack/sprite-payload';
import codePayload from '../lib/backpack/code-payload';
import {PayloadSerializableData} from '../lib/backpack/payload-serializable-data.ts';
import DragConstants from '../lib/drag-constants';
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
import {GUIStoragePropType} from '../gui-config';
Expand Down Expand Up @@ -53,15 +49,31 @@ class Backpack extends React.Component {
if (props.host) {
props.storage.setBackpackHost?.(props.host);
}
// Set initial session
this.updateBackpackSession(props);
}
componentDidMount () {
this.props.vm.addListener('BLOCK_DRAG_END', this.handleBlockDragEnd);
this.props.vm.addListener('BLOCK_DRAG_UPDATE', this.handleBlockDragUpdate);
}
componentDidUpdate (prevProps) {
// Update session when credentials change
if (prevProps.username !== this.props.username || prevProps.token !== this.props.token) {
this.updateBackpackSession(this.props);
}
}
componentWillUnmount () {
this.props.vm.removeListener('BLOCK_DRAG_END', this.handleBlockDragEnd);
this.props.vm.removeListener('BLOCK_DRAG_UPDATE', this.handleBlockDragUpdate);
}
updateBackpackSession (props) {
const {username, token} = props;
if (username && token) {
props.storage.backpackStorage?.setSession?.({username, token});
} else {
props.storage.backpackStorage?.setSession?.(null);
}
}
handleToggle () {
const newState = !this.state.expanded;
this.setState({expanded: newState, contents: []}, () => {
Expand All @@ -74,6 +86,7 @@ class Backpack extends React.Component {
}
handleDrop (dragInfo) {
const scratchStorage = this.props.storage.scratchStorage;
const backpackStorage = this.props.storage.backpackStorage;

let payloader = null;
let presaveAsset = null;
Expand Down Expand Up @@ -111,12 +124,22 @@ class Backpack extends React.Component {
}
return payload;
})
.then(payload => saveBackpackObject({
host: this.props.host,
token: this.props.token,
username: this.props.username,
...payload
}))
.then(payload => {
if (!backpackStorage) {
// Shouldn't happen as this component shouldn't be rendered without a backpack, but
// adding this just in case
return;
}

const serializableData = new PayloadSerializableData(payload);
return backpackStorage.save(
{
type: serializableData.getType(),
name: serializableData.getName()
},
serializableData
);
})
.then(item => {
this.setState({
loading: false,
Expand All @@ -131,12 +154,7 @@ class Backpack extends React.Component {
}
handleDelete (id) {
this.setState({loading: true}, () => {
deleteBackpackObject({
host: this.props.host,
token: this.props.token,
username: this.props.username,
id: id
})
this.props.storage.backpackStorage.delete(id)
.then(() => {
this.setState({
loading: false,
Expand All @@ -150,28 +168,23 @@ class Backpack extends React.Component {
});
}
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(error => {
this.setState({error: true, loading: false});
throw error;
this.setState({loading: true, error: false}, () => {
this.props.storage.backpackStorage.list({
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.setState({error: true, loading: false});
throw error;
});
});
}
handleBlockDragUpdate (isOutsideWorkspace) {
this.setState({
Expand Down
93 changes: 92 additions & 1 deletion packages/scratch-gui/src/gui-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface GUIConfig {

export interface GUIStorage {
scratchStorage: ScratchStorage;
backpackStorage?: GUIBackpackStorage;

// Called multiple times (as changes happen)
setProjectHost?(host: string): void;
Expand All @@ -31,8 +32,90 @@ export interface GUIStorage {
): Promise<{ id: ProjectId }>;

saveProjectThumbnail?(projectId: ProjectId, thumbnail: Blob): void;
}

export interface GUIBackpackStorage {
setSession?(session: BackpackSession | null | undefined): void;

list(request: BackpackListItemsInput): Promise<BackpackItem[]>;
save(item: BackpackSaveItemInput, data: SerializableData): Promise<BackpackItem>;
delete(id: string): Promise<void>;
}

export interface BackpackSession {
username: string;
token: string;
}

export interface BackpackListItemsInput {
limit: number,
offset: number
}

export interface BackpackSaveItemInput {
/**
* Type of backpack object
*/
type: BackpackItemType,

/**
* User-facing name of the object being saved
*/
name: string,
}

// TODO: Support backpack storage
export interface SerializableData {
mimeType(): string,
dataAsBase64(): Promise<string>,
thumbnailAsBase64(): Promise<string>
}

export type BackpackItemType = 'costume' | 'sound' | 'script' | 'sprite';

export interface BackpackItem {
/**
* A unique identifier for the backpack item.
* UUID format.
*/
id: string,

/**
* Name of the item
*/
name: string,

/**
* The type of backpack item
*/
type: BackpackItemType,

/**
* The path (URL without host) of the thumbnail
*/
thumbnail: string,

/**
* The full URL (incl. host) of the thumbnail
*/
thumbnailUrl: string,

/**
* The md5ext of the backpack item.
*
* Different backpack items are loaded from different places:
*
* - costume -> the md5ext specified here is loaded from
* the asset server (has to be registered on the storage instance)
* - sound -> same as above
* - script -> loaded from the backpack server using `bodyUrl`. The `body` field isn't used.
* - sprite -> same as above
*/
body: string,

/**
* The full URL (incl. host) of the backpack body
*/
bodyUrl: string,
}

export type TranslatorFunction = (
Expand All @@ -46,8 +129,16 @@ export interface MessageObject {
defaultMessage: string;
}

export const GUIBackpackStoragePropType = PropTypes.shape({
list: PropTypes.func.isRequired,
save: PropTypes.func.isRequired,
delete: PropTypes.func.isRequired,
setSession: PropTypes.func
});

export const GUIStoragePropType = PropTypes.shape({
scratchStorage: PropTypes.object.isRequired,
backpackStorage: GUIBackpackStoragePropType,

setProjectHost: PropTypes.func,
setProjectToken: PropTypes.func,
Expand Down
4 changes: 3 additions & 1 deletion packages/scratch-gui/src/index-standalone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export * from './exported-reducers';

export * from 'scratch-storage';

export * from './lib/legacy-backpack-storage';

export {default as buildDefaultProject} from './lib/default-project';

// TODO: Better typing once ScratchGUI has types

export type GUIProps = any; // ComponentPropsWithoutRef<typeof ScratchGUI>;

export type HigherOrderComponent = (component: ReactComponentLike) => ReactComponentLike;
Expand Down
Loading
Loading