Improve homepage layout and Create project dialog options (#3386)

This commit is contained in:
AlexandreS
2021-12-21 14:28:13 +01:00
committed by GitHub
parent 6bf293bcb5
commit 650975ba6e
11 changed files with 219 additions and 283 deletions

View File

@@ -8,7 +8,6 @@ import Authentication from './Utils/GDevelopServices/Authentication';
import './UI/icomoon-font.css'; // Styles for Icomoon font.
// Import for browser only IDE
import BrowserExamples from './ProjectCreation/BrowserExamples';
import BrowserIntroDialog from './MainFrame/BrowserIntroDialog';
import browserResourceSources from './ResourcesList/BrowserResourceSources';
import browserResourceExternalEditors from './ResourcesList/BrowserResourceExternalEditors';
@@ -84,7 +83,7 @@ export const create = (authentication: Authentication) => {
renderCreateDialog={props => (
<CreateProjectDialog
{...props}
examplesComponent={BrowserExamples}
onCreateBlank={onCreateBlank}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
/>
)}

View File

@@ -8,7 +8,6 @@ import Authentication from './Utils/GDevelopServices/Authentication';
import './UI/icomoon-font.css'; // Styles for Icomoon font.
// Import for Electron powered IDE.
import LocalExamples from './ProjectCreation/LocalExamples';
import localResourceSources from './ResourcesList/LocalResourceSources';
import localResourceExternalEditors from './ResourcesList/LocalResourceExternalEditors';
import LocalPreviewLauncher from './Export/LocalExporters/LocalPreviewLauncher';
@@ -82,7 +81,7 @@ export const create = (authentication: Authentication) => {
renderCreateDialog={props => (
<CreateProjectDialog
{...props}
examplesComponent={LocalExamples}
onCreateBlank={onCreateBlank}
onCreateFromExampleShortHeader={
onCreateFromExampleShortHeader
}

View File

@@ -6,6 +6,7 @@ import { Trans, t } from '@lingui/macro';
import Language from '@material-ui/icons/Language';
import ForumIcon from '@material-ui/icons/Forum';
import HelpIcon from '@material-ui/icons/Help';
import AddCircleOutline from '@material-ui/icons/AddCircleOutline';
import FlatButton from '../../../UI/FlatButton';
import IconButton from '../../../UI/IconButton';
@@ -330,7 +331,7 @@ export const HomePage = React.memo<Props>(
onClick={() => {
openPreCreationDialog(true);
}}
primary
icon={<AddCircleOutline />}
/>
)}
{!project && canOpen && (
@@ -363,7 +364,7 @@ export const HomePage = React.memo<Props>(
</ResponsiveLineStackLayout>
</div>
<Carousel
title={<Trans>Start from a template</Trans>}
title={<Trans>Start from an example</Trans>}
items={examples ? prepareExamples(examples) : null}
displayItemTitles
onBrowseAllClick={onOpenExamples}
@@ -534,6 +535,9 @@ export const HomePage = React.memo<Props>(
isOpening={isOpening}
onClose={() => openPreCreationDialog(false)}
onCreate={() => createProject(i18n)}
onClickGenerateProjectName={() =>
setNewProjectName(generateName())
}
outputPath={electron ? outputPath : undefined}
onChangeOutputPath={electron ? setOutputPath : undefined}
projectName={newProjectName}

View File

@@ -1,74 +0,0 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { ExampleStore } from '../AssetStore/ExampleStore';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnOpenProjectAfterCreationFunction,
} from '../ProjectCreation/CreateProjectDialog';
import ProjectPreCreationDialog from './ProjectPreCreationDialog';
import generateName from '../Utils/ProjectNameGenerator';
type Props = {|
onOpen: OnOpenProjectAfterCreationFunction,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
|};
export default function BrowserExamples({
onOpen,
onCreateFromExampleShortHeader,
}: Props) {
const [
selectedExampleShortHeader,
setSelectedExampleShortShortHeader,
] = React.useState<?ExampleShortHeader>(null);
const [newProjectName, setNewProjectName] = React.useState<string>(
generateName()
);
const [isOpening, setIsOpening] = React.useState(false);
const createProjectFromExample = async (i18n: I18nType) => {
if (!selectedExampleShortHeader) return;
setIsOpening(true);
try {
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
projectName: newProjectName,
exampleShortHeader: selectedExampleShortHeader,
});
if (projectMetadata) {
onOpen({ ...projectMetadata, shouldCloseDialog: true });
}
} finally {
setIsOpening(false);
}
};
return (
<I18n>
{({ i18n }) => (
<>
<ExampleStore
isOpening={isOpening}
onOpen={async (example: ?ExampleShortHeader) =>
setSelectedExampleShortShortHeader(example)
}
/>
{selectedExampleShortHeader && (
<ProjectPreCreationDialog
open
isOpening={isOpening}
onClose={() => setSelectedExampleShortShortHeader(null)}
onCreate={() => createProjectFromExample(i18n)}
projectName={newProjectName}
onChangeProjectName={setNewProjectName}
/>
)}
</>
)}
</I18n>
);
}

View File

@@ -1,8 +1,11 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import PublishIcon from '@material-ui/icons/Publish';
import { ExampleStore } from '../AssetStore/ExampleStore';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import { Tabs, Tab } from '../UI/Tabs';
@@ -12,9 +15,11 @@ import { type StorageProvider, type FileMetadata } from '../ProjectsStorage';
import { GamesShowcase } from '../GamesShowcase';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import Window from '../Utils/Window';
import PublishIcon from '@material-ui/icons/Publish';
import ProjectPreCreationDialog from './ProjectPreCreationDialog';
import { findEmptyPathInDefaultFolder } from './LocalPathFinder';
import optionalRequire from '../Utils/OptionalRequire.js';
import RaisedButton from '../UI/RaisedButton';
import generateName from '../Utils/ProjectNameGenerator';
const electron = optionalRequire('electron');
const app = electron ? electron.remote.app : null;
@@ -23,11 +28,6 @@ export type CreateProjectDialogTabs =
| 'tutorials'
| 'games-showcase';
type State = {|
currentTab: CreateProjectDialogTabs,
outputPath: string,
|};
export type OnOpenProjectAfterCreationFunction = ({|
project?: gdProject,
storageProvider: ?StorageProvider,
@@ -67,99 +67,191 @@ export type OnCreateFromExampleShortHeaderFunction = ({|
type Props = {|
...CreateProjectDialogWithComponentsProps,
examplesComponent: any,
onCreateBlank: OnCreateBlankFunction,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
|};
export default class CreateProjectDialog extends React.Component<Props, State> {
state = {
currentTab: this.props.initialTab,
outputPath: app ? findEmptyPathInDefaultFolder(app) : '',
};
const CreateProjectDialog = ({
open,
onClose,
onOpen,
onCreateFromExampleShortHeader,
onCreateBlank,
initialTab,
}: Props) => {
const [currentTab, setCurrentTab] = React.useState<CreateProjectDialogTabs>(
initialTab
);
const [outputPath, setOutputPath] = React.useState<string>(
app ? findEmptyPathInDefaultFolder(app) : ''
);
const [isOpening, setIsOpening] = React.useState<boolean>(false);
const [newProjectName, setNewProjectName] = React.useState<string>('');
const [
selectedExampleShortHeader,
setSelectedExampleShortShortHeader,
] = React.useState<?ExampleShortHeader>(null);
const [
preCreationDialogOpen,
setPreCreationDialogOpen,
] = React.useState<boolean>(false);
_onChangeTab = (newTab: CreateProjectDialogTabs) => {
this.setState({
currentTab: newTab,
});
};
const openPreCreationDialog = React.useCallback((open: boolean) => {
if (open) {
setOutputPath(app ? findEmptyPathInDefaultFolder(app) : '');
setNewProjectName(generateName());
}
setPreCreationDialogOpen(open);
}, []);
_showExamples = () => this._onChangeTab('examples');
const actions = React.useMemo(
() => [
<RaisedButton
key="create-blank"
label={<Trans>Create a blank project</Trans>}
primary={false}
onClick={() => openPreCreationDialog(true)}
/>,
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary={false}
onClick={onClose}
/>,
],
[onClose, openPreCreationDialog]
);
render() {
const {
open,
onClose,
onOpen,
onCreateFromExampleShortHeader,
} = this.props;
if (!open) return null;
const ExamplesComponent = this.props.examplesComponent;
return (
<Dialog
title={<Trans>Create a new project</Trans>}
actions={[
const secondaryActions = React.useMemo(
() => {
if (currentTab === 'games-showcase')
return [
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary={false}
onClick={onClose}
key="submit-game-showcase"
onClick={() => {
Window.openExternalURL(
'https://docs.google.com/forms/d/e/1FAIpQLSfjiOnkbODuPifSGuzxYY61vB5kyMWdTZSSqkJsv3H6ePRTQA/viewform?usp=sf_link'
);
}}
primary
icon={<PublishIcon />}
label={<Trans>Submit your game to the showcase</Trans>}
/>,
]}
secondaryActions={[
this.state.currentTab === 'games-showcase' ? (
<FlatButton
key="submit-game-showcase"
onClick={() => {
Window.openExternalURL(
'https://docs.google.com/forms/d/e/1FAIpQLSfjiOnkbODuPifSGuzxYY61vB5kyMWdTZSSqkJsv3H6ePRTQA/viewform?usp=sf_link'
);
}}
primary
icon={<PublishIcon />}
label={<Trans>Submit your game to the showcase</Trans>}
/>
) : null,
this.state.currentTab === 'examples' ? (
<FlatButton
key="submit-example"
onClick={() => {
Window.openExternalURL(
'https://github.com/GDevelopApp/GDevelop-examples/issues/new/choose'
);
}}
primary
icon={<PublishIcon />}
label={<Trans>Submit your project as an example</Trans>}
/>
) : null,
]}
cannotBeDismissed={false}
onRequestClose={onClose}
open={open}
noMargin
fullHeight
flexBody
>
<Column expand noMargin>
<Tabs value={this.state.currentTab} onChange={this._onChangeTab}>
<Tab label={<Trans>Examples</Trans>} value="examples" />
<Tab label={<Trans>Tutorials</Trans>} value="tutorials" />
<Tab label={<Trans>Games showcase</Trans>} value="games-showcase" />
</Tabs>
{this.state.currentTab === 'examples' && (
<ExamplesComponent
onOpen={onOpen}
onChangeOutputPath={outputPath => this.setState({ outputPath })}
outputPath={this.state.outputPath}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
];
if (currentTab === 'examples')
return [
<FlatButton
key="submit-example"
onClick={() => {
Window.openExternalURL(
'https://github.com/GDevelopApp/GDevelop-examples/issues/new/choose'
);
}}
primary
icon={<PublishIcon />}
label={<Trans>Submit your project as an example</Trans>}
/>,
];
},
[currentTab]
);
if (!open) return null;
const createProject = async (i18n: I18nType) => {
setIsOpening(true);
try {
let projectMetadata;
if (selectedExampleShortHeader) {
projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
projectName: newProjectName,
exampleShortHeader: selectedExampleShortHeader,
});
} else {
projectMetadata = await onCreateBlank({
i18n,
outputPath,
projectName: newProjectName,
});
}
if (!projectMetadata) return;
openPreCreationDialog(false);
setSelectedExampleShortShortHeader(null);
onOpen({ ...projectMetadata });
} finally {
setIsOpening(false);
}
};
return (
<I18n>
{({ i18n }) => (
<>
<Dialog
title={<Trans>Create a new project</Trans>}
actions={actions}
secondaryActions={secondaryActions}
cannotBeDismissed={false}
onRequestClose={onClose}
open={open}
noMargin
fullHeight
flexBody
>
<Column expand noMargin>
<Tabs
value={currentTab}
onChange={(newTab: CreateProjectDialogTabs) => {
setCurrentTab(newTab);
}}
>
<Tab label={<Trans>Examples</Trans>} value="examples" />
<Tab label={<Trans>Tutorials</Trans>} value="tutorials" />
<Tab
label={<Trans>Games showcase</Trans>}
value="games-showcase"
/>
</Tabs>
{currentTab === 'examples' && (
<Column noMargin expand useFullHeight>
<ExampleStore
isOpening={isOpening}
onOpen={async (example: ?ExampleShortHeader) => {
setSelectedExampleShortShortHeader(example);
openPreCreationDialog(true);
}}
/>
</Column>
)}
{currentTab === 'tutorials' && <TutorialsList />}
{currentTab === 'games-showcase' && <GamesShowcase />}
</Column>
</Dialog>
{preCreationDialogOpen && (
<ProjectPreCreationDialog
open
isOpening={isOpening}
onClose={() => openPreCreationDialog(false)}
onCreate={() => createProject(i18n)}
onClickGenerateProjectName={() =>
setNewProjectName(generateName())
}
outputPath={electron ? outputPath : undefined}
onChangeOutputPath={electron ? setOutputPath : undefined}
projectName={newProjectName}
onChangeProjectName={setNewProjectName}
/>
)}
{this.state.currentTab === 'tutorials' && <TutorialsList />}
{this.state.currentTab === 'games-showcase' && <GamesShowcase />}
</Column>
</Dialog>
);
}
}
</>
)}
</I18n>
);
};
export default CreateProjectDialog;

View File

@@ -1,102 +0,0 @@
// @flow
import { t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Divider from '@material-ui/core/Divider';
import { ExampleStore } from '../AssetStore/ExampleStore';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { Column } from '../UI/Grid';
import { showErrorBox } from '../UI/Messages/MessageBox';
import ProjectPreCreationDialog from './ProjectPreCreationDialog';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnOpenProjectAfterCreationFunction,
} from '../ProjectCreation/CreateProjectDialog';
import generateName from '../Utils/ProjectNameGenerator';
type Props = {|
onOpen: OnOpenProjectAfterCreationFunction,
onChangeOutputPath: (outputPath: string) => void,
outputPath: string,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
|};
export const showGameFileCreationError = (
i18n: I18nType,
outputPath: string,
rawError: Error
) => {
showErrorBox({
message: i18n._(
t`Unable to create the game in the specified folder. Check that you have permissions to write in this folder: ${outputPath} or choose another folder.`
),
rawError,
errorId: 'local-example-creation-error',
});
};
export default function LocalExamples({
outputPath,
onChangeOutputPath,
onOpen,
onCreateFromExampleShortHeader,
}: Props) {
const [isOpening, setIsOpening] = React.useState<boolean>(false);
const [newProjectName, setNewProjectName] = React.useState<string>(
generateName()
);
const [
selectedExampleShortHeader,
setSelectedExampleShortShortHeader,
] = React.useState<?ExampleShortHeader>(null);
const createProjectFromExample = async (i18n: I18nType) => {
if (!selectedExampleShortHeader) return;
setIsOpening(true);
try {
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
projectName: newProjectName,
exampleShortHeader: selectedExampleShortHeader,
});
if (!!projectMetadata) {
onOpen({ ...projectMetadata, shouldCloseDialog: true });
}
} finally {
setIsOpening(false);
}
};
return (
<I18n>
{({ i18n }) => (
<>
<Column noMargin expand useFullHeight>
<Divider />
<ExampleStore
isOpening={isOpening}
onOpen={async (example: ?ExampleShortHeader) =>
setSelectedExampleShortShortHeader(example)
}
/>
</Column>
{selectedExampleShortHeader && (
<ProjectPreCreationDialog
open
isOpening={isOpening}
onClose={() => setSelectedExampleShortShortHeader(null)}
onCreate={() => createProjectFromExample(i18n)}
outputPath={outputPath}
onChangeOutputPath={onChangeOutputPath}
projectName={newProjectName}
onChangeProjectName={setNewProjectName}
/>
)}
</>
)}
</I18n>
);
}

View File

@@ -2,6 +2,7 @@
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import Refresh from '@material-ui/icons/Refresh';
import FlatButton from '../UI/FlatButton';
import RaisedButton from '../UI/RaisedButton';
import { Column, Spacer } from '../UI/Grid';
@@ -13,6 +14,7 @@ type Props = {|
isOpening?: boolean,
onClose: () => void,
onCreate: () => void | Promise<void>,
onClickGenerateProjectName: () => void,
outputPath?: string,
onChangeOutputPath?: (outputPath: string) => void,
projectName: string,
@@ -24,6 +26,7 @@ const ProjectPreCreationDialog = ({
isOpening,
onClose,
onCreate,
onClickGenerateProjectName,
outputPath,
onChangeOutputPath,
projectName,
@@ -57,7 +60,7 @@ const ProjectPreCreationDialog = ({
return (
<Dialog
title={<Trans>Project settings</Trans>}
title={<Trans>New Project</Trans>}
maxWidth="sm"
open={open}
onRequestClose={onClose}
@@ -77,7 +80,7 @@ const ProjectPreCreationDialog = ({
/>,
]}
>
<Column>
<Column noMargin>
<TextField
type="text"
errorText={projectNameError}
@@ -85,6 +88,7 @@ const ProjectPreCreationDialog = ({
value={projectName}
onChange={_onChangeProjectName}
floatingLabelText={<Trans>Project name</Trans>}
endAdornment={<Refresh onClick={onClickGenerateProjectName} />}
/>
{onChangeOutputPath && (
<>

View File

@@ -1,6 +1,7 @@
// @flow
import axios from 'axios';
import { t } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import LocalFileStorageProvider from '../../ProjectsStorage/LocalFileStorageProvider';
import optionalRequire from '../../Utils/OptionalRequire.js';
@@ -8,7 +9,6 @@ import { getExample } from '../../Utils/GDevelopServices/Example';
import { sendNewGameCreated } from '../../Utils/Analytics/EventSender';
import { showErrorBox } from '../../UI/Messages/MessageBox';
import { writeAndCheckFile } from '../../ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter';
import { showGameFileCreationError } from '../LocalExamples';
import {
type OnCreateBlankFunction,
type OnCreateFromExampleShortHeaderFunction,
@@ -28,7 +28,13 @@ export const onCreateBlank: OnCreateBlankFunction = async ({
try {
fs.mkdirsSync(outputPath);
} catch (error) {
showGameFileCreationError(i18n, outputPath, error);
showErrorBox({
message: i18n._(
t`Unable to create the game in the specified folder. Check that you have permissions to write in this folder: ${outputPath} or choose another folder.`
),
rawError: error,
errorId: 'local-example-creation-error',
});
return;
}

View File

@@ -6,6 +6,7 @@ import GridList from '@material-ui/core/GridList';
import { GridListTile } from '@material-ui/core';
import ArrowBackIos from '@material-ui/icons/ArrowBackIos';
import ArrowForwardIos from '@material-ui/icons/ArrowForwardIos';
import ListOutlined from '@material-ui/icons/ListOutlined';
import { Skeleton } from '@material-ui/lab';
import Window from '../Utils/Window';
@@ -375,6 +376,7 @@ const Carousel = <ThumbnailType: Thumbnail>({
(browseAllLink ? openLinkCallback(browseAllLink) : () => {})
}
label={<Trans>Browse all</Trans>}
icon={<ListOutlined />}
/>
</Line>
{error ? (

View File

@@ -22,6 +22,9 @@ export const Open = () => {
onChangeOutputPath={action('change output path')}
onClose={() => action('click on close')()}
onCreate={() => action('click on create')()}
onClickGenerateProjectName={() =>
action('click on generate new project name')()
}
projectName={projectName}
onChangeProjectName={setProjectName}
/>
@@ -38,6 +41,9 @@ export const Disabled = () => {
onChangeOutputPath={action('change output path')}
onClose={() => action('click on close')()}
onCreate={() => action('click on create')()}
onClickGenerateProjectName={() =>
action('click on generate new project name')()
}
projectName={projectName}
onChangeProjectName={setProjectName}
/>

View File

@@ -2852,10 +2852,10 @@ storiesOf('Project Creation/CreateProjectDialog', module)
<ExampleStoreStateProvider>
<CreateProjectDialog
open
examplesComponent={Placeholder}
onClose={action('onClose')}
onOpen={action('onOpen')}
onOpen={action('On open project after it is created')}
initialTab="examples"
onCreateBlank={() => action('create blank project')}
onCreateFromExampleShortHeader={() => action('create from example')}
/>
</ExampleStoreStateProvider>
@@ -2864,10 +2864,10 @@ storiesOf('Project Creation/CreateProjectDialog', module)
<ExampleStoreStateProvider>
<CreateProjectDialog
open
examplesComponent={Placeholder}
onClose={action('onClose')}
onOpen={action('onOpen')}
onOpen={action('On open project after it is created')}
initialTab="games-showcase"
onCreateBlank={() => action('create blank project')}
onCreateFromExampleShortHeader={() => action('create from example')}
/>
</ExampleStoreStateProvider>