mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add tutorials in app (#3870)
- Added dismissable tutorials in events sheet, variable editors and object variable tab, add object dialog, sprite animations tab, export game for web tab, add new extension dialog. - Added a tutorial button.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import SearchBar from '../../UI/SearchBar';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
|
||||
import { ExtensionStoreContext } from './ExtensionStoreContext';
|
||||
import { ListSearchResults } from '../../UI/Search/ListSearchResults';
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
sendExtensionDetailsOpened,
|
||||
sendExtensionAddedToProject,
|
||||
} from '../../Utils/Analytics/EventSender';
|
||||
import useDismissableTutorialMessage from '../../Hints/useDismissableTutorialMessage';
|
||||
|
||||
const styles = {
|
||||
searchBar: {
|
||||
@@ -85,6 +86,10 @@ export const ExtensionStore = ({
|
||||
return extensionMatches ? extensionMatches.matches : [];
|
||||
};
|
||||
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
'intro-behaviors-and-functions'
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ResponsiveWindowMeasurer>
|
||||
@@ -98,6 +103,11 @@ export const ExtensionStore = ({
|
||||
tagsHandler={tagsHandler}
|
||||
tags={filters && filters.allTags}
|
||||
/>
|
||||
{DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column expand>{DismissableTutorialMessage}</Column>
|
||||
</Line>
|
||||
)}
|
||||
<ListSearchResults
|
||||
disableAutoTranslate // Search results text highlighting conflicts with dom handling by browser auto-translations features. Disables auto translation to prevent crashes.
|
||||
onRetry={fetchExtensionsAndFilters}
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
type EnumeratedObjectMetadata,
|
||||
} from '../ObjectsList/EnumerateObjects';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import { Column } from '../UI/Grid';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
|
||||
import { Tabs, Tab } from '../UI/Tabs';
|
||||
import { AssetStore } from '.';
|
||||
@@ -33,6 +33,7 @@ import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import { useResourceFetcher } from '../ProjectsStorage/ResourceFetcher';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
|
||||
|
||||
const ObjectListItem = ({
|
||||
objectMetadata,
|
||||
@@ -184,6 +185,10 @@ export default function NewObjectDialog({
|
||||
]
|
||||
);
|
||||
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
'intro-object-types'
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Add a new object</Trans>}
|
||||
@@ -230,6 +235,11 @@ export default function NewObjectDialog({
|
||||
)}
|
||||
{currentTab === 'new-object' && (
|
||||
<ScrollView>
|
||||
{DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column expand>{DismissableTutorialMessage}</Column>
|
||||
</Line>
|
||||
)}
|
||||
<List>
|
||||
{Object.keys(objectsByCategory).map(category => {
|
||||
const categoryObjectMetadata = objectsByCategory[category];
|
||||
|
@@ -128,6 +128,7 @@ const BehaviorsEditor = (props: Props) => {
|
||||
}
|
||||
actionLabel={<Trans>Add a behavior</Trans>}
|
||||
helpPagePath="/behaviors"
|
||||
tutorialId="intro-behaviors-and-functions"
|
||||
actionButtonId="add-behavior-button"
|
||||
onAdd={() => setNewBehaviorDialogOpen(true)}
|
||||
/>
|
||||
|
@@ -32,6 +32,11 @@ import ThemeConsumer from '../../UI/Theme/ThemeConsumer';
|
||||
import BottomButtons from './BottomButtons';
|
||||
import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder';
|
||||
import { CorsAwareImage } from '../../UI/CorsAwareImage';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { type Preferences } from '../../MainFrame/Preferences/PreferencesContext';
|
||||
import { type Tutorial } from '../../Utils/GDevelopServices/Tutorial';
|
||||
import TutorialMessage from '../../Hints/TutorialMessage';
|
||||
import getTutorial from '../../Hints/getTutorial';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const getThumbnail = ObjectsRenderingService.getThumbnail.bind(
|
||||
@@ -227,6 +232,9 @@ type EventsTreeProps = {|
|
||||
windowWidth: WidthType,
|
||||
eventsSheetHeight: number,
|
||||
fontSize?: number,
|
||||
|
||||
preferences: Preferences,
|
||||
tutorials: ?Array<Tutorial>,
|
||||
|};
|
||||
|
||||
// A node displayed by the SortableTree. Almost always represents an
|
||||
@@ -267,6 +275,26 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
this.onHeightsChanged();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: EventsTreeProps) {
|
||||
const {
|
||||
values: { hiddenTutorialHints },
|
||||
} = this.props.preferences;
|
||||
const {
|
||||
values: { hiddenTutorialHints: previousHiddenTutorialHints },
|
||||
} = prevProps.preferences;
|
||||
if (
|
||||
hiddenTutorialHints['intro-event-system'] !==
|
||||
previousHiddenTutorialHints['intro-event-system']
|
||||
) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
treeData: this.state.treeData.filter(
|
||||
data => data.key !== 'eventstree-tutorial-node'
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called whenever an event height has changed
|
||||
*/
|
||||
@@ -362,6 +390,11 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
};
|
||||
}
|
||||
);
|
||||
const tutorial = getTutorial(
|
||||
this.props.preferences,
|
||||
this.props.tutorials,
|
||||
'intro-event-system'
|
||||
);
|
||||
|
||||
// Add the bottom buttons if we're at the root
|
||||
const extraNodes = [
|
||||
@@ -382,6 +415,22 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
children: [],
|
||||
}
|
||||
: null,
|
||||
depth === 0 && eventsList.getEventsCount() !== 0 && tutorial
|
||||
? {
|
||||
title: () => (
|
||||
<Line justifyContent="center">
|
||||
<TutorialMessage tutorial={tutorial} />
|
||||
</Line>
|
||||
),
|
||||
event: null,
|
||||
indexInList: eventsList.getEventsCount() + 1,
|
||||
disabled: false,
|
||||
depth: 0,
|
||||
fixedHeight: 150,
|
||||
children: [],
|
||||
key: 'eventstree-tutorial-node',
|
||||
}
|
||||
: null,
|
||||
depth === 0 && eventsList.getEventsCount() === 0
|
||||
? {
|
||||
title: () => (
|
||||
@@ -390,6 +439,7 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
description={<Trans>Events define the rules of a game.</Trans>}
|
||||
actionLabel={<Trans>Add an event</Trans>}
|
||||
helpPagePath="/events"
|
||||
tutorialId="intro-event-system"
|
||||
onAdd={() =>
|
||||
this.props.onAddNewEvent(
|
||||
'BuiltinCommonInstructions::Standard',
|
||||
|
@@ -95,6 +95,8 @@ import {
|
||||
import LeaderboardContext, {
|
||||
type LeaderboardState,
|
||||
} from '../Leaderboard/LeaderboardContext';
|
||||
import { TutorialContext } from '../Tutorial/TutorialContext';
|
||||
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const zoomLevel = { min: 1, max: 50 };
|
||||
@@ -129,6 +131,7 @@ type ComponentProps = {|
|
||||
...Props,
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
preferences: Preferences,
|
||||
tutorials: ?Array<Tutorial>,
|
||||
leaderboardsManager: ?LeaderboardState,
|
||||
|};
|
||||
|
||||
@@ -1330,6 +1333,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
onCreateEventsFunction,
|
||||
tutorials,
|
||||
} = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
@@ -1414,6 +1418,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
|
||||
: 0
|
||||
}
|
||||
fontSize={preferences.values.eventsSheetZoomLevel}
|
||||
preferences={preferences}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
{this.state.showSearchPanel && (
|
||||
<SearchPanel
|
||||
@@ -1582,12 +1588,14 @@ const EventsSheet = (props, ref) => {
|
||||
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
const leaderboardsManager = React.useContext(LeaderboardContext);
|
||||
return (
|
||||
<EventsSheetComponentWithoutHandle
|
||||
ref={component}
|
||||
authenticatedUser={authenticatedUser}
|
||||
preferences={preferences}
|
||||
tutorials={tutorials}
|
||||
leaderboardsManager={leaderboardsManager}
|
||||
{...props}
|
||||
/>
|
||||
|
@@ -14,6 +14,7 @@ import { Tab, Tabs } from '../../UI/Tabs';
|
||||
import ExportHome from './ExportHome';
|
||||
import { getGame, type Game } from '../../Utils/GDevelopServices/Game';
|
||||
import { showWarningBox } from '../../UI/Messages/MessageBox';
|
||||
import TutorialButton from '../../UI/TutorialButton';
|
||||
|
||||
const styles = {
|
||||
icon: { width: 40, height: 40 },
|
||||
@@ -174,6 +175,14 @@ const ExportDialog = ({
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath={exporter.helpPage} />,
|
||||
exporter.exportPipeline.name === 'local-html5' ||
|
||||
exporter.exportPipeline.name === 'browser-html5' ? (
|
||||
<TutorialButton
|
||||
key="tutorial"
|
||||
tutorialId="export-to-itch"
|
||||
label="How to export to Itch.io"
|
||||
/>
|
||||
) : null,
|
||||
<FlatButton
|
||||
key="builds"
|
||||
label={<Trans>See this game builds</Trans>}
|
||||
|
@@ -13,6 +13,7 @@ import Poki from '../../UI/CustomSvgIcons/Poki';
|
||||
import CrazyGames from '../../UI/CustomSvgIcons/CrazyGames';
|
||||
import NewsGround from '../../UI/CustomSvgIcons/NewsGround';
|
||||
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
|
||||
|
||||
const getIconStyle = windowWidth => ({
|
||||
height: windowWidth === 'small' ? 30 : 48,
|
||||
@@ -25,6 +26,7 @@ export const ExplanationHeader = () => {
|
||||
const iconStyle = getIconStyle(windowWidth);
|
||||
return (
|
||||
<Column noMargin>
|
||||
<DismissableTutorialMessage tutorialId="export-to-itch" />
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
|
@@ -1,15 +1,5 @@
|
||||
// @flow
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import Window from '../Utils/Window';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import YouTubeIcon from '@material-ui/icons/YouTube';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import { TutorialContext } from '../Tutorial/TutorialContext';
|
||||
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
|
||||
import useDismissableTutorialMessage from './useDismissableTutorialMessage';
|
||||
|
||||
type Props = {|
|
||||
tutorialId: string,
|
||||
@@ -18,63 +8,14 @@ type Props = {|
|
||||
/**
|
||||
* Show a link to a tutorial that can be permanently hidden. Hidden tutorials
|
||||
* will be stored in preferences.
|
||||
* Use useDismissableTutorialMessage if you need to know if the tutorial can't be found
|
||||
* or was previously hidden before rendering.
|
||||
*/
|
||||
const DismissableTutorialMessage = ({ tutorialId }: Props) => {
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const { values, showTutorialHint } = preferences;
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
|
||||
if (values.hiddenTutorialHints[tutorialId]) return null;
|
||||
|
||||
if (!tutorials) return null; // Loading or errored, do not display the tutorial.
|
||||
const tutorial: ?Tutorial = tutorials.find(
|
||||
tutorial => tutorial.id === tutorialId
|
||||
);
|
||||
if (!tutorial) {
|
||||
console.warn(`Tutorial ${tutorialId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<AlertMessage
|
||||
kind={'info'}
|
||||
children={tutorial.title}
|
||||
renderLeftIcon={() => (
|
||||
<img
|
||||
alt=""
|
||||
style={{
|
||||
maxWidth: 128,
|
||||
maxHeight: 128,
|
||||
}}
|
||||
src={tutorial.thumbnailUrl}
|
||||
/>
|
||||
)}
|
||||
renderRightButton={() => (
|
||||
<RaisedButton
|
||||
icon={
|
||||
tutorial.type === 'video' ? <YouTubeIcon /> : <MenuBookIcon />
|
||||
}
|
||||
label={
|
||||
tutorial.type === 'video' ? (
|
||||
<Trans>Watch the tutorial</Trans>
|
||||
) : (
|
||||
<Trans>Read the tutorial</Trans>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
Window.openExternalURL(tutorial.link);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onHide={() => {
|
||||
showTutorialHint(tutorialId, false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
const {
|
||||
DismissableTutorialMessage: ReturnedDismissableTutorialMessage,
|
||||
} = useDismissableTutorialMessage(tutorialId);
|
||||
return ReturnedDismissableTutorialMessage;
|
||||
};
|
||||
|
||||
export default DismissableTutorialMessage;
|
||||
|
65
newIDE/app/src/Hints/TutorialMessage.js
Normal file
65
newIDE/app/src/Hints/TutorialMessage.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// @flow
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import Window from '../Utils/Window';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import YouTubeIcon from '@material-ui/icons/YouTube';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
|
||||
|
||||
type Props = {|
|
||||
tutorial: Tutorial,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Show a link to a tutorial that can be permanently hidden. Hidden tutorials
|
||||
* will be stored in preferences.
|
||||
*/
|
||||
const TutorialMessage = ({ tutorial }: Props) => {
|
||||
const { showTutorialHint } = React.useContext(PreferencesContext);
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<AlertMessage
|
||||
kind={'info'}
|
||||
children={tutorial.title}
|
||||
renderLeftIcon={() => (
|
||||
<img
|
||||
alt=""
|
||||
style={{
|
||||
maxWidth: 128,
|
||||
maxHeight: 128,
|
||||
}}
|
||||
src={tutorial.thumbnailUrl}
|
||||
/>
|
||||
)}
|
||||
renderRightButton={() => (
|
||||
<RaisedButton
|
||||
icon={
|
||||
tutorial.type === 'video' ? <YouTubeIcon /> : <MenuBookIcon />
|
||||
}
|
||||
label={
|
||||
tutorial.type === 'video' ? (
|
||||
<Trans>Watch the tutorial</Trans>
|
||||
) : (
|
||||
<Trans>Read the tutorial</Trans>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
Window.openExternalURL(tutorial.link);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onHide={() => {
|
||||
showTutorialHint(tutorial.id, false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default TutorialMessage;
|
28
newIDE/app/src/Hints/getTutorial.js
Normal file
28
newIDE/app/src/Hints/getTutorial.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
|
||||
import { type Preferences } from '../MainFrame/Preferences/PreferencesContext';
|
||||
|
||||
/**
|
||||
* Returns a tutorial if it can be found, otherwise returns null.
|
||||
*/
|
||||
const getTutorial = (
|
||||
preferences: Preferences,
|
||||
tutorials: ?Array<Tutorial>,
|
||||
tutorialId: string
|
||||
) => {
|
||||
if (!tutorials) return null; // Loading or errored, do not display the tutorial.
|
||||
|
||||
const { values } = preferences;
|
||||
if (values.hiddenTutorialHints[tutorialId]) return null;
|
||||
const tutorial: ?Tutorial = tutorials.find(
|
||||
tutorial => tutorial.id === tutorialId
|
||||
);
|
||||
if (!tutorial) {
|
||||
console.warn(`Tutorial with id ${tutorialId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return tutorial;
|
||||
};
|
||||
|
||||
export default getTutorial;
|
32
newIDE/app/src/Hints/useDismissableTutorialMessage.js
Normal file
32
newIDE/app/src/Hints/useDismissableTutorialMessage.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import { TutorialContext } from '../Tutorial/TutorialContext';
|
||||
import getTutorial from './getTutorial';
|
||||
import TutorialMessage from './TutorialMessage';
|
||||
|
||||
/**
|
||||
* Returns the DismissableTutorialMessage component if the tutorial can be found,
|
||||
* otherwise returns null.
|
||||
* Useful to use when you need to know if the component is null before rendering,
|
||||
* to avoid spacing issues when the component is hidden.
|
||||
*/
|
||||
const useDismissableTutorialMessage = (tutorialId: string) => {
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
const tutorial = getTutorial(preferences, tutorials, tutorialId);
|
||||
|
||||
const DismissableTutorialMessage = React.useMemo(
|
||||
() => {
|
||||
if (!tutorial) return null;
|
||||
return <TutorialMessage tutorial={tutorial} />;
|
||||
},
|
||||
[tutorial]
|
||||
);
|
||||
|
||||
return {
|
||||
DismissableTutorialMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDismissableTutorialMessage;
|
@@ -14,6 +14,8 @@ import ExternalPropertiesDialog, {
|
||||
import Text from '../../UI/Text';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { sendEventsExtractedAsFunction } from '../../Utils/Analytics/EventSender';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import TutorialButton from '../../UI/TutorialButton';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -176,6 +178,15 @@ export class ExternalEventsEditorContainer extends React.Component<
|
||||
onClick={this.openExternalPropertiesDialog}
|
||||
/>
|
||||
</Line>
|
||||
<Line justifyContent="flex-start" noMargin>
|
||||
<TutorialButton
|
||||
tutorialId="Intermediate-externals"
|
||||
label="Watch the tutorial"
|
||||
renderIfNotFound={
|
||||
<HelpButton helpPagePath="/interface/events-editor/external-events" />
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
</PlaceholderMessage>
|
||||
)}
|
||||
<ExternalPropertiesDialog
|
||||
|
@@ -19,6 +19,8 @@ import ExternalPropertiesDialog, {
|
||||
import { Line } from '../../UI/Grid';
|
||||
import Text from '../../UI/Text';
|
||||
import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings';
|
||||
import TutorialButton from '../../UI/TutorialButton';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -199,6 +201,15 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
onClick={this.openExternalPropertiesDialog}
|
||||
/>
|
||||
</Line>
|
||||
<Line justifyContent="flex-start" noMargin>
|
||||
<TutorialButton
|
||||
tutorialId="Intermediate-externals"
|
||||
label="Watch the tutorial"
|
||||
renderIfNotFound={
|
||||
<HelpButton helpPagePath="/interface/events-editor/external-events" />
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
</PlaceholderMessage>
|
||||
)}
|
||||
<ExternalPropertiesDialog
|
||||
|
@@ -83,8 +83,8 @@ export default function ExternalPropertiesDialog({
|
||||
>
|
||||
<Column>
|
||||
{helpTexts &&
|
||||
helpTexts.map(helpText => (
|
||||
<Line>
|
||||
helpTexts.map((helpText, index) => (
|
||||
<Line key={index}>
|
||||
<BackgroundText>{helpText}</BackgroundText>
|
||||
</Line>
|
||||
))}
|
||||
|
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Column, Spacer } from '../../../UI/Grid';
|
||||
import useDismissableTutorialMessage from '../../../Hints/useDismissableTutorialMessage';
|
||||
|
||||
/**
|
||||
* TODO: Use context directly in SpriteEditor
|
||||
* when switching SpriteEditor class component to functional component.
|
||||
*/
|
||||
const SpacedDismissableTutorialMessage = () => {
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
'intermediate-changing-animations'
|
||||
);
|
||||
return DismissableTutorialMessage ? (
|
||||
<Column>
|
||||
{DismissableTutorialMessage}
|
||||
<Spacer />
|
||||
</Column>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SpacedDismissableTutorialMessage;
|
@@ -40,6 +40,7 @@ import ScrollView from '../../../UI/ScrollView';
|
||||
import Checkbox from '../../../UI/Checkbox';
|
||||
import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import { EmptyPlaceholder } from '../../../UI/EmptyPlaceholder';
|
||||
import SpacedDismissableTutorialMessage from './SpacedDismissableTutorialMessage';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -317,11 +318,13 @@ class AnimationsListContainer extends React.Component<
|
||||
description={<Trans>Animations are a sequence of images.</Trans>}
|
||||
actionLabel={<Trans>Add an animation</Trans>}
|
||||
helpPagePath="/objects/sprite"
|
||||
tutorialId="intermediate-changing-animations"
|
||||
onAdd={this.addAnimation}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<SpacedDismissableTutorialMessage />
|
||||
<SortableAnimationsList
|
||||
spriteObject={this.props.spriteObject}
|
||||
objectName={this.props.objectName}
|
||||
|
@@ -25,6 +25,7 @@ import HotReloadPreviewButton, {
|
||||
import EffectsList from '../EffectsList';
|
||||
import VariablesList from '../VariablesList/index';
|
||||
import { sendBehaviorsEditorShown } from '../Utils/Analytics/EventSender';
|
||||
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export type ObjectEditorTab =
|
||||
@@ -97,6 +98,10 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
props.onRename(newObjectName);
|
||||
};
|
||||
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
'intro-variables'
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (currentTab === 'behaviors') {
|
||||
@@ -229,20 +234,30 @@ const InnerDialog = (props: InnerDialogProps) => {
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'variables' && (
|
||||
<VariablesList
|
||||
variablesContainer={props.object.getVariables()}
|
||||
emptyPlaceholderTitle={<Trans>Add your first object variable</Trans>}
|
||||
emptyPlaceholderDescription={
|
||||
<Trans>
|
||||
These variables hold additional information on an object.
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/object-variables'}
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
onComputeAllVariableNames={props.onComputeAllVariableNames}
|
||||
/>
|
||||
<Column expand noMargin>
|
||||
{props.object.getVariables().count() > 0 &&
|
||||
DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column expand>{DismissableTutorialMessage}</Column>
|
||||
</Line>
|
||||
)}
|
||||
<VariablesList
|
||||
variablesContainer={props.object.getVariables()}
|
||||
emptyPlaceholderTitle={
|
||||
<Trans>Add your first object variable</Trans>
|
||||
}
|
||||
emptyPlaceholderDescription={
|
||||
<Trans>
|
||||
These variables hold additional information on an object.
|
||||
</Trans>
|
||||
}
|
||||
helpPagePath={'/all-features/variables/object-variables'}
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
onComputeAllVariableNames={props.onComputeAllVariableNames}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
{currentTab === 'effects' && (
|
||||
<EffectsList
|
||||
|
@@ -9,17 +9,23 @@ import RaisedButton from '../UI/RaisedButton';
|
||||
import { Column, LargeSpacer } from './Grid';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import Text from '../UI/Text';
|
||||
import TutorialButton from './TutorialButton';
|
||||
|
||||
type Props = {|
|
||||
title: React.Node,
|
||||
description: React.Node,
|
||||
actionLabel: React.Node,
|
||||
helpPagePath?: string,
|
||||
tutorialId?: string,
|
||||
actionButtonId?: string,
|
||||
onAdd: () => void,
|
||||
isLoading?: boolean,
|
||||
|};
|
||||
|
||||
const DefaultHelpButton = ({ helpPagePath }: { helpPagePath?: string }) => (
|
||||
<HelpButton label={<Trans>Read the doc</Trans>} helpPagePath={helpPagePath} />
|
||||
);
|
||||
|
||||
/**
|
||||
* A placeholder for when there is no content to display.
|
||||
* Also take a look at EmptyMessage for a less visible message.
|
||||
@@ -49,10 +55,17 @@ export const EmptyPlaceholder = (props: Props) => (
|
||||
icon={props.isLoading ? <CircularProgress size={24} /> : <Add />}
|
||||
id={props.actionButtonId}
|
||||
/>
|
||||
<HelpButton
|
||||
label={<Trans>Read the doc</Trans>}
|
||||
helpPagePath={props.helpPagePath}
|
||||
/>
|
||||
{props.tutorialId ? (
|
||||
<TutorialButton
|
||||
tutorialId={props.tutorialId}
|
||||
label="Watch tutorial"
|
||||
renderIfNotFound={
|
||||
<DefaultHelpButton helpPagePath={props.helpPagePath} />
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DefaultHelpButton helpPagePath={props.helpPagePath} />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Column>
|
||||
</Container>
|
||||
|
43
newIDE/app/src/UI/TutorialButton/index.js
Normal file
43
newIDE/app/src/UI/TutorialButton/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import FlatButton from '../FlatButton';
|
||||
import Window from '../../Utils/Window';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { TutorialContext } from '../../Tutorial/TutorialContext';
|
||||
import { type Tutorial } from '../../Utils/GDevelopServices/Tutorial';
|
||||
import YouTube from '@material-ui/icons/YouTube';
|
||||
|
||||
type PropsType = {|
|
||||
tutorialId: ?string,
|
||||
label?: React.Node,
|
||||
renderIfNotFound?: React.Node,
|
||||
|};
|
||||
|
||||
/**
|
||||
* The button that can be used in any dialog to open a Youtube tutorial.
|
||||
*/
|
||||
const TutorialButton = (props: PropsType) => {
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
if (!tutorials || !props.tutorialId) return props.renderIfNotFound || null; // Loading or errored, do not display the tutorial.
|
||||
const tutorial: ?Tutorial = tutorials.find(
|
||||
tutorial => tutorial.id === props.tutorialId
|
||||
);
|
||||
if (!tutorial) {
|
||||
console.warn(`Tutorial with id ${props.tutorialId || ''} not found`);
|
||||
return props.renderIfNotFound || null;
|
||||
}
|
||||
return (
|
||||
<FlatButton
|
||||
onClick={() => {
|
||||
if (tutorial.link) {
|
||||
Window.openExternalURL(tutorial.link);
|
||||
}
|
||||
}}
|
||||
target="_blank"
|
||||
label={<Trans>{props.label || 'Tutorial'}</Trans>}
|
||||
icon={<YouTube />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TutorialButton;
|
@@ -84,6 +84,9 @@ export const getInstructionTutorialIds = (type: string): Array<string> => {
|
||||
case 'ChangeAnimation':
|
||||
case 'ChangeAnimationName':
|
||||
return ['intermediate-changing-animations'];
|
||||
case 'PopStartedTouch':
|
||||
case 'MouseButtonPressed':
|
||||
return ['intermediate-touchscreen-controls'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import HotReloadPreviewButton, {
|
||||
type HotReloadPreviewButtonProps,
|
||||
} from '../HotReload/HotReloadPreviewButton';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
|
||||
|
||||
type Props = {|
|
||||
onCancel: () => void,
|
||||
@@ -42,6 +44,9 @@ const VariablesEditorDialog = ({
|
||||
serializableObject: variablesContainer,
|
||||
onCancel,
|
||||
});
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
'intro-variables'
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -84,23 +89,30 @@ const VariablesEditorDialog = ({
|
||||
flexBody
|
||||
fullHeight
|
||||
>
|
||||
<VariablesList
|
||||
commitVariableValueOnBlur={
|
||||
// Reduce the number of re-renders by saving the variable value only when the field is blurred.
|
||||
// We don't do that by default because the VariablesList can be used in a component like
|
||||
// InstancePropertiesEditor, that can be unmounted at any time, before the text fields get a
|
||||
// chance to be blurred.
|
||||
true
|
||||
}
|
||||
variablesContainer={variablesContainer}
|
||||
emptyPlaceholderTitle={emptyPlaceholderTitle}
|
||||
emptyPlaceholderDescription={emptyPlaceholderDescription}
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
onComputeAllVariableNames={onComputeAllVariableNames}
|
||||
helpPagePath={helpPagePath}
|
||||
/>
|
||||
<Column expand noMargin>
|
||||
{variablesContainer.count() > 0 && DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column expand>{DismissableTutorialMessage}</Column>
|
||||
</Line>
|
||||
)}
|
||||
<VariablesList
|
||||
commitVariableValueOnBlur={
|
||||
// Reduce the number of re-renders by saving the variable value only when the field is blurred.
|
||||
// We don't do that by default because the VariablesList can be used in a component like
|
||||
// InstancePropertiesEditor, that can be unmounted at any time, before the text fields get a
|
||||
// chance to be blurred.
|
||||
true
|
||||
}
|
||||
variablesContainer={variablesContainer}
|
||||
emptyPlaceholderTitle={emptyPlaceholderTitle}
|
||||
emptyPlaceholderDescription={emptyPlaceholderDescription}
|
||||
onSizeUpdated={
|
||||
forceUpdate /*Force update to ensure dialog is properly positioned*/
|
||||
}
|
||||
onComputeAllVariableNames={onComputeAllVariableNames}
|
||||
helpPagePath={helpPagePath}
|
||||
/>
|
||||
</Column>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
@@ -453,6 +453,7 @@ export default class VariablesList extends React.Component<Props, State> {
|
||||
description={this.props.emptyPlaceholderDescription}
|
||||
actionLabel="Add a variable"
|
||||
helpPagePath={this.props.helpPagePath}
|
||||
tutorialId="intermediate-advanced-variables"
|
||||
onAdd={this.addVariable}
|
||||
/>
|
||||
</Column>
|
||||
|
@@ -245,6 +245,7 @@ import {
|
||||
ExamplesAccordion,
|
||||
} from '../Profile/ContributionsDetails';
|
||||
import ListIcon from '../UI/ListIcon';
|
||||
import { initialPreferences } from '../MainFrame/Preferences/PreferencesContext';
|
||||
|
||||
configureActions({
|
||||
depth: 2,
|
||||
@@ -268,6 +269,17 @@ const buildFakeMenuTemplate = () => [
|
||||
click: action('click option 2'),
|
||||
},
|
||||
];
|
||||
const eventsTreeTutorials = [
|
||||
{
|
||||
id: 'intro-event-system',
|
||||
title: 'Event system',
|
||||
description: 'Description 1',
|
||||
thumbnailUrl:
|
||||
'https://raw.githubusercontent.com/4ian/GDevelop/master/Core/docs/images/gdlogo.png',
|
||||
link: 'https://example.com/tutorial.html',
|
||||
type: 'video',
|
||||
},
|
||||
];
|
||||
|
||||
const hotReloadPreviewButtonProps: HotReloadPreviewButtonProps = {
|
||||
hasPreviewsRunning: false,
|
||||
@@ -2593,6 +2605,8 @@ storiesOf('EventsTree', module)
|
||||
screenType={'normal'}
|
||||
windowWidth={'medium'}
|
||||
eventsSheetHeight={500}
|
||||
preferences={initialPreferences}
|
||||
tutorials={eventsTreeTutorials}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
</div>
|
||||
@@ -2632,6 +2646,8 @@ storiesOf('EventsTree', module)
|
||||
screenType={'normal'}
|
||||
windowWidth={'small'}
|
||||
eventsSheetHeight={500}
|
||||
preferences={initialPreferences}
|
||||
tutorials={eventsTreeTutorials}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
</div>
|
||||
@@ -2671,6 +2687,8 @@ storiesOf('EventsTree', module)
|
||||
screenType={'normal'}
|
||||
windowWidth={'medium'}
|
||||
eventsSheetHeight={500}
|
||||
preferences={initialPreferences}
|
||||
tutorials={eventsTreeTutorials}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
</div>
|
||||
@@ -2710,6 +2728,8 @@ storiesOf('EventsTree', module)
|
||||
screenType={'normal'}
|
||||
windowWidth={'small'}
|
||||
eventsSheetHeight={500}
|
||||
preferences={initialPreferences}
|
||||
tutorials={eventsTreeTutorials}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user