Skip to content

Commit b265755

Browse files
authored
Merge pull request #374 from scratchfoundation/poc/uepr-400-cat-blocks
feat: support cat-blocks as an optional theme
2 parents 04f881f + a0dd45d commit b265755

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+672
-543
lines changed

package-lock.json

Lines changed: 52 additions & 170 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/scratch-gui/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
"balance-text": "3.3.1",
7272
"base64-loader": "1.0.0",
7373
"bowser": "1.9.4",
74-
"cat-blocks": "npm:[email protected]",
7574
"classnames": "2.5.1",
7675
"computed-style-to-inline-style": "3.0.0",
7776
"cookie": "0.6.0",
@@ -107,7 +106,7 @@
107106
"react-intl": "6.8.9",
108107
"react-modal": "3.16.3",
109108
"react-popover": "0.5.10",
110-
"react-redux": "8.1.3",
109+
"react-redux": "^8.0.0",
111110
"react-responsive": "9.0.2",
112111
"react-style-proptype": "3.2.2",
113112
"react-tabs": "5.2.0",
@@ -116,7 +115,7 @@
116115
"react-visibility-sensor": "5.1.1",
117116
"redux-throttle": "0.1.1",
118117
"scratch-audio": "2.0.268",
119-
"scratch-blocks": "1.2.5",
118+
"scratch-blocks": "1.3.0",
120119
"scratch-l10n": "6.1.25",
121120
"scratch-paint": "4.1.19",
122121
"scratch-render-fonts": "1.0.252",
@@ -146,6 +145,7 @@
146145
"@types/react-modal": "3.16.3",
147146
"babel-core": "7.0.0-bridge.0",
148147
"babel-loader": "9.2.1",
148+
"babel-plugin-react-intl": "3.5.1",
149149
"buffer": "6.0.3",
150150
"cheerio": "1.1.2",
151151
"cross-env": "7.0.3",

packages/scratch-gui/src/components/gui/gui.jsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx';
3434

3535
import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants';
3636
import {resolveStageSize} from '../../lib/screen-utils';
37-
import {themeMap} from '../../lib/themes';
37+
import {colorModeMap} from '../../lib/settings/color-mode/index.js';
38+
import {DEFAULT_THEME, themeMap} from '../../lib/settings/theme/index.js';
3839
import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options';
3940

4041
import styles from './gui.css';
@@ -43,6 +44,7 @@ import costumesIcon from './icon--costumes.svg';
4344
import soundsIcon from './icon--sounds.svg';
4445
import DebugModal from '../debug-modal/debug-modal.jsx';
4546
import {setPlatform} from '../../reducers/platform.js';
47+
import {setTheme} from '../../reducers/settings.js';
4648
import {PLATFORM} from '../../lib/platform.js';
4749

4850
// Cache this value to only retrieve it once the first time.
@@ -68,6 +70,7 @@ const GUIComponent = props => {
6870
blocksTabVisible,
6971
cardsVisible,
7072
canChangeLanguage,
73+
canChangeColorMode,
7174
canChangeTheme,
7275
canCreateNew,
7376
canEditTitle,
@@ -85,7 +88,9 @@ const GUIComponent = props => {
8588
onDebugModalClose,
8689
onTutorialSelect,
8790
enableCommunity,
91+
hasActiveMembership,
8892
isCreating,
93+
isFetchingUserData,
8994
isFullScreen,
9095
isPlayerOnly,
9196
isRtl,
@@ -130,6 +135,7 @@ const GUIComponent = props => {
130135
stageSizeMode,
131136
targetIsStage,
132137
telemetryModalVisible,
138+
colorMode,
133139
theme,
134140
tipsLibraryVisible,
135141
useExternalPeripheralList,
@@ -145,10 +151,23 @@ const GUIComponent = props => {
145151

146152
useEffect(() => {
147153
if (props.platform) {
154+
// TODO: This uses the imported `setPlatform` directly,
155+
// but it should probably use the dispatched version from props.
148156
setPlatform(props.platform);
149157
}
150158
}, [props.platform]);
151159

160+
useEffect(() => {
161+
if (
162+
!isFetchingUserData &&
163+
!themeMap[theme]?.isAvailable?.({hasActiveMembership})
164+
) {
165+
// If the preferred theme is not available, fall back to default.
166+
// TODO: It would be cleaner to do this on redux init.
167+
props.setTheme(DEFAULT_THEME);
168+
}
169+
}, [theme, hasActiveMembership, props.setTheme]);
170+
152171
const tabClassNames = {
153172
tabs: styles.tabs,
154173
tab: classNames(tabStyles.reactTabsTab, styles.tab),
@@ -260,6 +279,7 @@ const GUIComponent = props => {
260279
authorUsername={authorUsername}
261280
authorAvatarBadge={authorAvatarBadge}
262281
canChangeLanguage={canChangeLanguage}
282+
canChangeColorMode={canChangeColorMode}
263283
canChangeTheme={canChangeTheme}
264284
canCreateCopy={canCreateCopy}
265285
canCreateNew={canCreateNew}
@@ -270,6 +290,7 @@ const GUIComponent = props => {
270290
canShare={canShare}
271291
className={styles.menuBarPosition}
272292
enableCommunity={enableCommunity}
293+
hasActiveMembership={hasActiveMembership}
273294
isShared={isShared}
274295
isTotallyNormal={isTotallyNormal}
275296
logo={logo}
@@ -364,15 +385,15 @@ const GUIComponent = props => {
364385
<TabPanel className={tabClassNames.tabPanel}>
365386
<Box className={styles.blocksWrapper}>
366387
<Blocks
367-
key={`${blocksId}/${theme}`}
388+
key={`${blocksId}/${colorMode}/${theme}`}
368389
canUseCloud={canUseCloud}
369390
grow={1}
370391
isVisible={blocksTabVisible}
371392
options={{
372-
media: `${basePath}static/${themeMap[theme].blocksMediaFolder}/`
393+
media: `${basePath}static/${colorModeMap[colorMode].blocksMediaFolder}/`
373394
}}
374395
stageSize={stageSize}
375-
theme={theme}
396+
colorMode={colorMode}
376397
vm={vm}
377398
showNewFeatureCallouts={showNewFeatureCallouts}
378399
username={username}
@@ -445,6 +466,7 @@ GUIComponent.propTypes = {
445466
blocksTabVisible: PropTypes.bool,
446467
blocksId: PropTypes.string,
447468
canChangeLanguage: PropTypes.bool,
469+
canChangeColorMode: PropTypes.bool,
448470
canChangeTheme: PropTypes.bool,
449471
canCreateCopy: PropTypes.bool,
450472
canCreateNew: PropTypes.bool,
@@ -459,10 +481,12 @@ GUIComponent.propTypes = {
459481
costumeLibraryVisible: PropTypes.bool,
460482
costumesTabVisible: PropTypes.bool,
461483
debugModalVisible: PropTypes.bool,
484+
hasActiveMembership: PropTypes.bool,
462485
onDebugModalClose: PropTypes.func,
463486
onTutorialSelect: PropTypes.func,
464487
enableCommunity: PropTypes.bool,
465488
isCreating: PropTypes.bool,
489+
isFetchingUserData: PropTypes.bool,
466490
isFullScreen: PropTypes.bool,
467491
isPlayerOnly: PropTypes.bool,
468492
isRtl: PropTypes.bool,
@@ -499,13 +523,15 @@ GUIComponent.propTypes = {
499523
onUpdateProjectThumbnail: PropTypes.func,
500524
platform: PropTypes.oneOf(Object.keys(PLATFORM)),
501525
renderLogin: PropTypes.func,
526+
setTheme: PropTypes.func.isRequired,
502527
showComingSoon: PropTypes.bool,
503528
showNewFeatureCallouts: PropTypes.bool,
504529
soundsTabVisible: PropTypes.bool,
505530
stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)),
506531
setPlatform: PropTypes.func,
507532
targetIsStage: PropTypes.bool,
508533
telemetryModalVisible: PropTypes.bool,
534+
colorMode: PropTypes.string,
509535
theme: PropTypes.string,
510536
tipsLibraryVisible: PropTypes.bool,
511537
useExternalPeripheralList: PropTypes.bool, // true for CDM, false for normal Scratch Link
@@ -520,7 +546,9 @@ GUIComponent.defaultProps = {
520546
backpackVisible: false,
521547
basePath: './',
522548
blocksId: 'original',
549+
// TODO: Currently all of those are always true. Do we actually need them?
523550
canChangeLanguage: true,
551+
canChangeColorMode: true,
524552
canChangeTheme: true,
525553
canCreateNew: false,
526554
canEditTitle: false,
@@ -546,11 +574,13 @@ const mapStateToProps = state => ({
546574
// This is the button's mode, as opposed to the actual current state
547575
blocksId: state.scratchGui.timeTravel.year.toString(),
548576
stageSizeMode: state.scratchGui.stageSize.stageSize,
549-
theme: state.scratchGui.theme.theme
577+
colorMode: state.scratchGui.settings.colorMode,
578+
theme: state.scratchGui.settings.theme
550579
});
551580

552581
const mapDispatchToProps = dispatch => ({
553-
setPlatform: platform => dispatch(setPlatform(platform))
582+
setPlatform: platform => dispatch(setPlatform(platform)),
583+
setTheme: theme => dispatch(setTheme(theme))
554584
});
555585

556586
export default connect(mapStateToProps,

packages/scratch-gui/src/components/menu-bar/menu-bar.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,12 @@ class MenuBar extends React.Component {
452452
onClick={this.props.onClickLogo}
453453
/>
454454
</div>
455-
{(this.props.canChangeTheme || this.props.canChangeLanguage) && (<SettingsMenu
455+
{(this.props.canChangeColorMode || this.props.canChangeLanguage || this.props.canChangeTheme) &&
456+
(<SettingsMenu
456457
canChangeLanguage={this.props.canChangeLanguage}
458+
canChangeColorMode={this.props.canChangeColorMode}
457459
canChangeTheme={this.props.canChangeTheme}
460+
hasActiveMembership={this.props.hasActiveMembership}
458461
isRtl={this.props.isRtl}
459462
onRequestClose={this.props.onRequestCloseSettings}
460463
onRequestOpen={this.props.onClickSettings}
@@ -904,6 +907,7 @@ MenuBar.propTypes = {
904907
authorAvatarBadge: PropTypes.number,
905908
autoUpdateProject: PropTypes.func,
906909
canChangeLanguage: PropTypes.bool,
910+
canChangeColorMode: PropTypes.bool,
907911
canChangeTheme: PropTypes.bool,
908912
canCreateCopy: PropTypes.bool,
909913
canCreateNew: PropTypes.bool,
@@ -918,6 +922,7 @@ MenuBar.propTypes = {
918922
editMenuOpen: PropTypes.bool,
919923
enableCommunity: PropTypes.bool,
920924
fileMenuOpen: PropTypes.bool,
925+
hasActiveMembership: PropTypes.bool,
921926
intl: intlShape,
922927
isRtl: PropTypes.bool,
923928
isShared: PropTypes.bool,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import classNames from 'classnames';
2+
import PropTypes from 'prop-types';
3+
import React, {useMemo} from 'react';
4+
import {FormattedMessage} from 'react-intl';
5+
import {connect} from 'react-redux';
6+
7+
import check from './check.svg';
8+
import {MenuItem, Submenu} from '../menu/menu.jsx';
9+
10+
import styles from './settings-menu.css';
11+
12+
import dropdownCaret from './dropdown-caret.svg';
13+
14+
const intlMessageShape = PropTypes.shape({
15+
defaultMessage: PropTypes.string,
16+
description: PropTypes.string,
17+
id: PropTypes.string
18+
});
19+
20+
const PreferenceItem = props => {
21+
const item = props.item;
22+
23+
return (
24+
<MenuItem onClick={props.onClick}>
25+
<div className={styles.option}>
26+
<img
27+
className={classNames(styles.check, {[styles.selected]: props.isSelected})}
28+
src={check}
29+
/>
30+
{item.icon && <img
31+
className={styles.icon}
32+
src={item.icon}
33+
/>}
34+
<FormattedMessage {...item.label} />
35+
</div>
36+
</MenuItem>);
37+
};
38+
39+
PreferenceItem.propTypes = {
40+
isSelected: PropTypes.bool,
41+
onClick: PropTypes.func,
42+
item: PropTypes.shape({
43+
icon: PropTypes.string,
44+
label: intlMessageShape.isRequired
45+
})
46+
};
47+
48+
const PreferenceMenu = ({
49+
itemsMap,
50+
open,
51+
onChange,
52+
onRequestOpen,
53+
defaultMenuIconSrc,
54+
submenuLabel,
55+
selectedItemKey,
56+
isRtl
57+
}) => {
58+
const itemKeys = useMemo(() => Object.keys(itemsMap), [itemsMap]);
59+
const selectedItem = useMemo(() => itemsMap[selectedItemKey], [itemsMap, selectedItemKey]);
60+
return (
61+
<MenuItem expanded={open}>
62+
<div
63+
className={styles.option}
64+
onClick={onRequestOpen}
65+
>
66+
<img
67+
src={selectedItem.icon || defaultMenuIconSrc}
68+
style={{width: 24}}
69+
/>
70+
<span className={styles.submenuLabel}>
71+
<FormattedMessage {...submenuLabel} />
72+
</span>
73+
<img
74+
className={styles.expandCaret}
75+
src={dropdownCaret}
76+
/>
77+
</div>
78+
<Submenu place={isRtl ? 'left' : 'right'}>
79+
{itemKeys.map(itemKey => (
80+
<PreferenceItem
81+
key={itemKey}
82+
isSelected={itemKey === selectedItemKey}
83+
// eslint-disable-next-line react/jsx-no-bind
84+
onClick={() => onChange(itemKey)}
85+
item={itemsMap[itemKey]}
86+
/>)
87+
)}
88+
</Submenu>
89+
</MenuItem>
90+
);
91+
};
92+
93+
PreferenceMenu.propTypes = {
94+
itemsMap: PropTypes.objectOf(PropTypes.shape({
95+
icon: PropTypes.string,
96+
label: intlMessageShape.isRequired
97+
})).isRequired,
98+
open: PropTypes.bool,
99+
onChange: PropTypes.func,
100+
onRequestCloseSettings: PropTypes.func,
101+
onRequestOpen: PropTypes.func,
102+
defaultMenuIconSrc: PropTypes.string,
103+
submenuLabel: intlMessageShape.isRequired,
104+
selectedItemKey: PropTypes.string,
105+
isRtl: PropTypes.bool
106+
};
107+
108+
const mapStateToProps = state => ({
109+
isRtl: state.locales.isRtl
110+
});
111+
112+
export default connect(
113+
mapStateToProps
114+
)(PreferenceMenu);

packages/scratch-gui/src/components/menu-bar/settings-menu.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
width: 1.5rem;
33
}
44

5-
.theme-label {
5+
/* Unused? */
6+
.color-mode-label {
67
flex: 1;
78
}
89

0 commit comments

Comments
 (0)