Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/gui/src/components/loader/loader.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
color: white;
}


.scoped-loader {
z-index: calc(var(--clipcc-z-index-loader) - 1);
border-radius: 0.5rem;
}

.scoped-loader .title {
font-size: 1.5rem;
margin: 0;
}


.fullscreen {
/* Break out of the layout using position: fixed to cover the whole screen */
position: fixed;
Expand Down
42 changes: 42 additions & 0 deletions packages/gui/src/components/loader/scoped-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import classNames from 'classnames';
import styles from './loader.css';

import topBlock from './top-block.svg';
import middleBlock from './middle-block.svg';
import bottomBlock from './bottom-block.svg';

export interface ScopedLoaderProps {
/** The text to display while loading */
text?: string;
}

export default function ScopedLoaderComponent (props: ScopedLoaderProps) {
return (
<div
className={classNames(styles.background, styles.scopedLoader)}
>
<div className={styles.container}>
<div className={styles.blockAnimation}>
<img
className={styles.topBlock}
src={topBlock}
/>
<img
className={styles.middleBlock}
src={middleBlock}
/>
<img
className={styles.bottomBlock}
src={bottomBlock}
/>
</div>
{props.text ? (
<div className={styles.title}>
{props.text}
</div>
) : null}
</div>
</div>
);
}
9 changes: 6 additions & 3 deletions packages/gui/src/containers/blocks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import defaultsDeep from 'lodash.defaultsdeep';
import makeToolbox from '../lib/make-toolbox';
import PropTypes from 'prop-types';
import React from 'react';
import {injectBlock} from '../lib/blocks-loader-hoc.tsx';
import VMScratchBlocks, {setRecordSoundCallback} from '../lib/blocks';
import VM from 'clipcc-vm';

Expand Down Expand Up @@ -51,7 +52,7 @@ const DroppableBlocks = DropAreaHOC([
class Blocks extends React.Component {
constructor (props) {
super(props);
this.ScratchBlocks = VMScratchBlocks(props.vm);
this.ScratchBlocks = VMScratchBlocks(props.vm, props.blocks);
bindAll(this, [
'attachVM',
'checkoutWsByProccode',
Expand Down Expand Up @@ -403,7 +404,8 @@ class Blocks extends React.Component {
targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '',
this.props.hideNonVanillaBlocks
);
} catch {
} catch (e) {
console.error(`Error making toolbox:`, e);
return null;
}
}
Expand Down Expand Up @@ -660,6 +662,7 @@ class Blocks extends React.Component {

Blocks.propTypes = {
anyModalVisible: PropTypes.bool,
blocks: PropTypes.object, // eslint-disable-line react/forbid-prop-types
hideNonVanillaBlocks: PropTypes.bool.isRequired,
canUseCloud: PropTypes.bool,
customProceduresVisible: PropTypes.bool,
Expand Down Expand Up @@ -776,5 +779,5 @@ export default errorBoundaryHOC('Blocks')(
connect(
mapStateToProps,
mapDispatchToProps
)(Blocks)
)(injectBlock(Blocks))
);
10 changes: 7 additions & 3 deletions packages/gui/src/containers/custom-procedures.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import defaultsDeep from 'lodash.defaultsdeep';
import PropTypes from 'prop-types';
import React from 'react';
import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx';
import * as ScratchBlocks from 'clipcc-block';
import {connect} from 'react-redux';
import {injectBlock} from '../lib/blocks-loader-hoc.tsx';

class CustomProcedures extends React.Component {
constructor (props) {
Expand Down Expand Up @@ -39,6 +39,7 @@ class CustomProcedures extends React.Component {
{rtl: this.props.isRtl}
);

const {blocks: ScratchBlocks} = this.props;
this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig);

// Create the procedure declaration block for editing the mutation.
Expand Down Expand Up @@ -176,6 +177,8 @@ class CustomProcedures extends React.Component {
}

CustomProcedures.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
blocks: PropTypes.object,
isRtl: PropTypes.bool,
state: PropTypes.shape({
proccode: PropTypes.string,
Expand All @@ -200,6 +203,7 @@ CustomProcedures.propTypes = {
})
};

/** @import * as ScratchBlocks from 'clipcc-block' */
/** @type {ScratchBlocks.BlocklyOptions} */
CustomProcedures.defaultOptions = {
zoom: {
Expand All @@ -224,6 +228,6 @@ const mapStateToProps = state => ({
new: state.scratchGui.customProcedures.new
});

export default connect(
export default injectBlock(connect(
mapStateToProps
)(CustomProcedures);
)(CustomProcedures));
13 changes: 13 additions & 0 deletions packages/gui/src/containers/gui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import themeManagerHOC from '../lib/theme-manager-hoc.jsx';

import GUIComponent from '../components/gui/gui';
import {setIsScratchDesktop} from '../lib/isScratchDesktop';
import {isScratchPaintLoaded, getScratchPaint} from '../lib/paint-loader';
import type {RootState} from '../lib/app-state-hoc';
import type {PropsOf} from '../lib/type-traits';

Expand Down Expand Up @@ -119,6 +120,9 @@ class GUI extends React.Component<GUIProps> {
setIsScratchDesktop(!!this.props.isScratchDesktop);
this.props.onStorageInit!(storage);
this.props.onVmInit!(this.props.vm);
if (!this.props.isPlayerOnly) {
this.preloadPaint();
}
}
override componentDidUpdate (prevProps: GUIProps) {
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
Expand All @@ -129,6 +133,15 @@ class GUI extends React.Component<GUIProps> {
// At this time the project view in www doesn't need to know when a project is unloaded
this.props.onProjectLoaded!();
}
if (!this.props.isPlayerOnly && prevProps.isPlayerOnly) {
this.preloadPaint();
}
}
preloadPaint () {
// Preload paint editor when main editor get ready
if (!isScratchPaintLoaded()) {
getScratchPaint();
}
}
override render () {
if (this.props.isError) {
Expand Down
12 changes: 8 additions & 4 deletions packages/gui/src/containers/paint-editor-wrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import VM from 'clipcc-vm';
import PaintEditor from 'clipcc-paint';
import {inlineSvgFonts} from 'clipcc-svg-renderer';
import {injectPaint} from '../lib/paint-loader-hoc';

import {connect} from 'react-redux';

Expand Down Expand Up @@ -56,8 +56,10 @@ class PaintEditorWrapper extends React.Component {
const {
selectedCostumeIndex,
vm,
paint,
...componentProps
} = this.props;
const PaintEditor = paint.default;

return (
<PaintEditor
Expand All @@ -82,7 +84,9 @@ PaintEditorWrapper.propTypes = {
selectedCostumeIndex: PropTypes.number.isRequired,
stageHeight: PropTypes.number.isRequired,
stageWidth: PropTypes.number.isRequired,
vm: PropTypes.instanceOf(VM)
vm: PropTypes.instanceOf(VM),
// eslint-disable-next-line react/forbid-prop-types
paint: PropTypes.object
};

const mapStateToProps = (state, {selectedCostumeIndex}) => {
Expand All @@ -107,6 +111,6 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => {
};
};

export default connect(
export default injectPaint(connect(
mapStateToProps
)(PaintEditorWrapper);
)(PaintEditorWrapper));
21 changes: 11 additions & 10 deletions packages/gui/src/lib/app-state-hoc.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import {Provider} from 'react-redux';
import {createStore, combineReducers, compose} from 'redux';
import type {Reducer, Store, StoreEnhancer} from 'redux';
import type {Store} from 'redux';
import ConnectedIntlProvider from './connected-intl-provider.jsx';

import localesReducer, {initLocale, localesInitialState} from '../reducers/locales';
Expand All @@ -11,6 +11,7 @@ import {setPlayer, setFullScreen} from '../reducers/mode';

import locales from 'clipcc-l10n';
import {detectLocale} from './detect-locale';
import {setInitialReducers, setStore} from '../reducers/utils';
import type {GuiState} from '../reducers/gui';

type ComposeEnhancers = typeof compose;
Expand Down Expand Up @@ -48,7 +49,7 @@ const AppStateHOC = function <P extends OwnProps> (
localesOnly?: boolean
): React.ComponentType<P> {
class AppStateWrapper extends React.Component<P> {
private store!: Store<unknown>;
private store!: Store<Partial<RootState>>;

constructor (props: P) {
super(props);
Expand All @@ -62,8 +63,9 @@ const AppStateHOC = function <P extends OwnProps> (
// browser modal
const reducers = {locales: localesReducer};
const initialState = {locales: initializedLocales};
const enhancer: StoreEnhancer<unknown> = composeEnhancers();
const reducer = combineReducers(reducers);
const enhancer = composeEnhancers();
setInitialReducers(reducers);
const reducer = combineReducers<{locales: LocalesState}>(reducers);
this.store = createStore(
reducer,
initialState,
Expand All @@ -82,8 +84,6 @@ const AppStateHOC = function <P extends OwnProps> (
initPlayer,
initTelemetryModal
} = guiRedux;
// eslint-disable-next-line global-require, @typescript-eslint/no-require-imports
const {ScratchPaintReducer}: {ScratchPaintReducer: Reducer<unknown>} = require('clipcc-paint');

let initializedGui: GuiState = guiInitialState;
if (props.isFullScreen || props.isPlayerOnly) {
Expand All @@ -98,21 +98,22 @@ const AppStateHOC = function <P extends OwnProps> (
}
const reducers = {
locales: localesReducer,
scratchGui: guiReducer,
scratchPaint: ScratchPaintReducer
scratchGui: guiReducer
};
const initialState = {
locales: initializedLocales,
scratchGui: initializedGui
};
const enhancer: StoreEnhancer<unknown> = composeEnhancers(guiMiddleware);
const reducer = combineReducers(reducers);
const enhancer = composeEnhancers(guiMiddleware);
setInitialReducers(reducers);
const reducer = combineReducers<RootState>(reducers);
this.store = createStore(
reducer,
initialState,
enhancer
);
}
setStore(this.store);
}
override componentDidUpdate (prevProps: Readonly<P>) {
if (localesOnly) return;
Expand Down
5 changes: 3 additions & 2 deletions packages/gui/src/lib/backpack/block-to-image.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import computedStyleToInlineStyle from 'computed-style-to-inline-style';
import * as ScratchBlocks from 'clipcc-block';
import {getScratchBlocks} from '../blocks-loader';

/**
* Given a blockId, return a data-uri image that can be used to create a thumbnail.
* @param {string} blockId the ID of the block to imagify
* @returns {Promise} resolves to a data-url of a picture of the blocks
*/
export default function (blockId) {
export default async function (blockId) {
const ScratchBlocks = await getScratchBlocks();
// Not sure any better way to access the scratch-blocks workspace than this...
const block = ScratchBlocks.common.getMainWorkspace().getBlockById(blockId);
const blockSvg = block.getSvgRoot().cloneNode(true /* deep */);
Expand Down
33 changes: 33 additions & 0 deletions packages/gui/src/lib/blocks-loader-hoc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import {getScratchBlocks} from './blocks-loader';
import ScopedLoaderComponent from '../components/loader/scoped-loader';

interface BlockLoaderProps {
blocks: typeof import('clipcc-block');
}

export function injectBlock<Component extends React.ComponentType<BlockLoaderProps>> (WrappedComponent: Component) {
class BlockLoaderHOC extends React.Component {
state = {
loaded: false
};
blocks: typeof import('clipcc-block') | null = null;

override async componentDidMount () {
if (!this.state.loaded) {
this.blocks = await getScratchBlocks();
this.setState({loaded: true});
}
}

override render () {
if (!this.state.loaded) {
return <ScopedLoaderComponent text="Loading blocks..." />;
}

return <WrappedComponent {...this.props} blocks={this.blocks!} />;
}
}

return BlockLoaderHOC;
}
25 changes: 25 additions & 0 deletions packages/gui/src/lib/blocks-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// eslint-disable-next-line import/no-mutable-exports
export let ScratchBlocksModule: typeof import('clipcc-block') | null = null;

/**
* Check if the ScratchBlocks module is loaded.
* @returns True if the ScratchBlocks module is loaded, false otherwise.
*/
export function isScratchBlocksLoaded () {
return !!ScratchBlocksModule;
}

/**
* Get the ScratchBlocks module, which is loaded asynchronously to reduce the initial bundle size.
* @returns A promise that resolves to the ScratchBlocks module.
*/
export async function getScratchBlocks () {
if (!ScratchBlocksModule) {
// eslint-disable-next-line require-atomic-updates
ScratchBlocksModule = await import(
/* webpackChunkName: "clipcc-block" */
'clipcc-block'
);
}
return ScratchBlocksModule;
}
6 changes: 3 additions & 3 deletions packages/gui/src/lib/blocks.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as ScratchBlocks from 'clipcc-block';

/**
* @typedef {import('clipcc-vm')} VirtualMachine
* @import * as ScratchBlocks from 'clipcc-block'
*/

/**
Expand All @@ -20,9 +19,10 @@ export const setRecordSoundCallback = callback => {
/**
* Connect scratch blocks with the vm
* @param {VirtualMachine} vm - The scratch vm
* @param {ScratchBlocks} ScratchBlocks - The scratch blocks to connect
* @returns {ScratchBlocks} ScratchBlocks connected with the vm
*/
export default function (vm) {
export default function (vm, ScratchBlocks) {

const jsonForMenuBlock = function (name, menuOptionsFn, category, start) {
return {
Expand Down
Loading
Loading