Add possibility to choose project name before it is created

A random and fun name is generated as a suggestion
This commit is contained in:
AlexandreS
2021-12-14 17:51:26 +01:00
committed by GitHub
parent 8c717ba910
commit a4d0c591a8
13 changed files with 2420 additions and 223 deletions

View File

@@ -28,8 +28,8 @@ import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext'
import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer';
import { ExampleDialog } from '../../../AssetStore/ExampleStore/ExampleDialog';
import optionalRequire from '../../../Utils/OptionalRequire';
import { findEmptyPath } from '../../../ProjectCreation/LocalPathFinder';
import LocalProjectPreCreationDialog from '../../../ProjectCreation/LocalProjectPreCreationDialog';
import { findEmptyPathInDefaultFolder } from '../../../ProjectCreation/LocalPathFinder';
import ProjectPreCreationDialog from '../../../ProjectCreation/ProjectPreCreationDialog';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnCreateBlankFunction,
@@ -38,9 +38,9 @@ import {
import RaisedButtonWithSplitMenu from '../../../UI/RaisedButtonWithSplitMenu';
import PreferencesContext from '../../Preferences/PreferencesContext';
import { type FileMetadataAndStorageProviderName } from '../../../ProjectsStorage';
import generateName from '../../../Utils/ProjectNameGenerator';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
const app = electron ? electron.remote.app : null;
const styles = {
@@ -146,6 +146,7 @@ export const HomePage = React.memo<Props>(
}));
const windowWidth = useResponsiveWindowWidth();
const [newProjectName, setNewProjectName] = React.useState<string>('');
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const { getRecentProjectFiles } = React.useContext(PreferencesContext);
const {
@@ -178,16 +179,7 @@ export const HomePage = React.memo<Props>(
]
);
const computeDefaultProjectPath = (): string =>
app && path
? findEmptyPath(
path.join(app.getPath('documents'), 'GDevelop projects')
)
: '';
const [outputPath, setOutputPath] = React.useState<string>(
computeDefaultProjectPath()
);
const [outputPath, setOutputPath] = React.useState<string>('');
const [
preCreationDialogOpen,
setPreCreationDialogOpen,
@@ -254,44 +246,40 @@ export const HomePage = React.memo<Props>(
[]
);
const createBlankProject = async (i18n: I18nType) => {
setIsOpening(true);
try {
const projectMetadata = await onCreateBlank({
i18n,
outputPath,
});
if (!projectMetadata) return;
const { project, storageProvider, fileMetadata } = projectMetadata;
setPreCreationDialogOpen(false);
setOutputPath(computeDefaultProjectPath());
onOpenProjectAfterCreation({
project,
storageProvider,
fileMetadata,
});
} finally {
setIsOpening(false);
const openPreCreationDialog = React.useCallback((open: boolean) => {
if (open) {
setOutputPath(app ? findEmptyPathInDefaultFolder(app) : '');
setNewProjectName(generateName());
}
};
const createProjectFromExample = async (i18n: I18nType) => {
if (!selectedExample) return;
setPreCreationDialogOpen(open);
}, []);
const createProject = async (i18n: I18nType) => {
setIsOpening(true);
try {
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
exampleShortHeader: selectedExample,
});
if (projectMetadata) {
const { storageProvider, fileMetadata } = projectMetadata;
setPreCreationDialogOpen(false);
setSelectedExample(null);
setOutputPath(computeDefaultProjectPath());
onOpenProjectAfterCreation({ storageProvider, fileMetadata });
let projectMetadata;
if (selectedExample) {
projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
projectName: newProjectName,
exampleShortHeader: selectedExample,
});
} else {
projectMetadata = await onCreateBlank({
i18n,
outputPath,
projectName: newProjectName,
});
}
if (!projectMetadata) return;
openPreCreationDialog(false);
setSelectedExample(null);
onOpenProjectAfterCreation({ ...projectMetadata });
} finally {
setIsOpening(false);
}
@@ -322,9 +310,7 @@ export const HomePage = React.memo<Props>(
<FlatButton
label={<Trans>Create a blank project</Trans>}
onClick={() => {
electron
? setPreCreationDialogOpen(true)
: createBlankProject(i18n);
openPreCreationDialog(true);
}}
primary
/>
@@ -520,24 +506,20 @@ export const HomePage = React.memo<Props>(
onClose={() => setSelectedExample(null)}
exampleShortHeader={selectedExample}
onOpen={() => {
electron
? setPreCreationDialogOpen(true)
: createProjectFromExample(i18n);
openPreCreationDialog(true);
}}
/>
)}
{preCreationDialogOpen && (
<LocalProjectPreCreationDialog
<ProjectPreCreationDialog
open
isOpening={isOpening}
onClose={() => setPreCreationDialogOpen(false)}
onCreate={() =>
selectedExample
? createProjectFromExample(i18n)
: createBlankProject(i18n)
}
outputPath={outputPath}
onChangeOutputPath={setOutputPath}
onClose={() => openPreCreationDialog(false)}
onCreate={() => createProject(i18n)}
outputPath={electron ? outputPath : undefined}
onChangeOutputPath={electron ? setOutputPath : undefined}
projectName={newProjectName}
onChangeProjectName={setNewProjectName}
/>
)}
</>

View File

@@ -1971,11 +1971,13 @@ const MainFrame = (props: Props) => {
project,
storageProvider,
fileMetadata,
projectName,
shouldCloseDialog,
}: {|
project?: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
projectName?: string,
shouldCloseDialog?: boolean,
|}) => {
if (shouldCloseDialog)
@@ -1987,7 +1989,11 @@ const MainFrame = (props: Props) => {
else if (!!fileMetadata) state = await openFromFileMetadata(fileMetadata);
if (state) {
if (state.currentProject) state.currentProject.resetProjectUuid();
if (state.currentProject) {
const { currentProject } = state;
currentProject.resetProjectUuid();
if (projectName) currentProject.setName(projectName);
}
openSceneOrProjectManager({
currentProject: state.currentProject,
editorTabs: state.editorTabs,

View File

@@ -8,6 +8,8 @@ import {
type OnCreateFromExampleShortHeaderFunction,
type OnOpenProjectAfterCreationFunction,
} from '../ProjectCreation/CreateProjectDialog';
import ProjectPreCreationDialog from './ProjectPreCreationDialog';
import generateName from '../Utils/ProjectNameGenerator';
type Props = {|
onOpen: OnOpenProjectAfterCreationFunction,
@@ -18,21 +20,27 @@ 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,
exampleShortHeader: ExampleShortHeader
) => {
const createProjectFromExample = async (i18n: I18nType) => {
if (!selectedExampleShortHeader) return;
setIsOpening(true);
try {
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
exampleShortHeader: exampleShortHeader,
projectName: newProjectName,
exampleShortHeader: selectedExampleShortHeader,
});
if (projectMetadata) {
const { storageProvider, fileMetadata } = projectMetadata;
onOpen({ storageProvider, fileMetadata, shouldCloseDialog: true });
onOpen({ ...projectMetadata, shouldCloseDialog: true });
}
} finally {
setIsOpening(false);
@@ -42,12 +50,24 @@ export default function BrowserExamples({
return (
<I18n>
{({ i18n }) => (
<ExampleStore
isOpening={isOpening}
onOpen={(exampleShortHeader: ExampleShortHeader) =>
createProjectFromExample(i18n, exampleShortHeader)
}
/>
<>
<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

@@ -13,9 +13,8 @@ import { GamesShowcase } from '../GamesShowcase';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import Window from '../Utils/Window';
import PublishIcon from '@material-ui/icons/Publish';
import { findEmptyPath } from './LocalPathFinder';
import { findEmptyPathInDefaultFolder } from './LocalPathFinder';
import optionalRequire from '../Utils/OptionalRequire.js';
const path = optionalRequire('path');
const electron = optionalRequire('electron');
const app = electron ? electron.remote.app : null;
@@ -33,6 +32,7 @@ export type OnOpenProjectAfterCreationFunction = ({|
project?: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
projectName?: string,
shouldCloseDialog?: boolean,
|}) => Promise<void>;
@@ -45,19 +45,23 @@ export type CreateProjectDialogWithComponentsProps = {|
export type OnCreateBlankFunction = ({|
i18n: I18nType,
projectName: string,
outputPath?: string,
|}) => Promise<?{|
project: gdProject,
storageProvider: ?StorageProvider,
projectName?: string,
fileMetadata: ?FileMetadata,
|}>;
export type OnCreateFromExampleShortHeaderFunction = ({|
i18n: I18nType,
exampleShortHeader: ExampleShortHeader,
projectName: string,
outputPath?: string,
|}) => Promise<?{|
storageProvider: StorageProvider,
projectName: string,
fileMetadata: FileMetadata,
|}>;
@@ -70,9 +74,7 @@ type Props = {|
export default class CreateProjectDialog extends React.Component<Props, State> {
state = {
currentTab: this.props.initialTab,
outputPath: app
? findEmptyPath(path.join(app.getPath('documents'), 'GDevelop projects'))
: '',
outputPath: app ? findEmptyPathInDefaultFolder(app) : '',
};
_onChangeTab = (newTab: CreateProjectDialogTabs) => {

View File

@@ -8,11 +8,12 @@ import { ExampleStore } from '../AssetStore/ExampleStore';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { Column } from '../UI/Grid';
import { showErrorBox } from '../UI/Messages/MessageBox';
import LocalProjectPreCreationDialog from './LocalProjectPreCreationDialog';
import ProjectPreCreationDialog from './ProjectPreCreationDialog';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnOpenProjectAfterCreationFunction,
} from '../ProjectCreation/CreateProjectDialog';
import generateName from '../Utils/ProjectNameGenerator';
type Props = {|
onOpen: OnOpenProjectAfterCreationFunction,
@@ -42,6 +43,9 @@ export default function LocalExamples({
onCreateFromExampleShortHeader,
}: Props) {
const [isOpening, setIsOpening] = React.useState<boolean>(false);
const [newProjectName, setNewProjectName] = React.useState<string>(
generateName()
);
const [
selectedExampleShortHeader,
setSelectedExampleShortShortHeader,
@@ -55,11 +59,11 @@ export default function LocalExamples({
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
projectName: newProjectName,
exampleShortHeader: selectedExampleShortHeader,
});
if (!!projectMetadata) {
const { storageProvider, fileMetadata } = projectMetadata;
onOpen({ storageProvider, fileMetadata, shouldCloseDialog: true });
onOpen({ ...projectMetadata, shouldCloseDialog: true });
}
} finally {
setIsOpening(false);
@@ -80,13 +84,15 @@ export default function LocalExamples({
/>
</Column>
{selectedExampleShortHeader && (
<LocalProjectPreCreationDialog
<ProjectPreCreationDialog
open
isOpening={isOpening}
onClose={() => setSelectedExampleShortShortHeader(null)}
onCreate={() => createProjectFromExample(i18n)}
outputPath={outputPath}
onChangeOutputPath={onChangeOutputPath}
projectName={newProjectName}
onChangeProjectName={setNewProjectName}
/>
)}
</>

View File

@@ -1,9 +1,10 @@
// @flow
import generateName from '../Utils/NewNameGenerator';
import optionalRequire from '../Utils/OptionalRequire.js';
const path = optionalRequire('path');
var fs = optionalRequire('fs-extra');
export const findEmptyPath = basePath => {
const findEmptyPath = (basePath: string) => {
if (!path) return basePath;
const folderName = generateName('My project', name => {
@@ -17,3 +18,9 @@ export const findEmptyPath = basePath => {
return path.join(basePath, folderName);
};
export const findEmptyPathInDefaultFolder = (electronApp: any): string => {
return findEmptyPath(
path.join(electronApp.getPath('documents'), 'GDevelop projects')
);
};

View File

@@ -1,60 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import RaisedButton from '../UI/RaisedButton';
import { Column } from '../UI/Grid';
import LocalFolderPicker from '../UI/LocalFolderPicker';
type Props = {|
open: boolean,
isOpening?: boolean,
onClose: () => void,
onCreate: () => void | Promise<void>,
outputPath: string,
onChangeOutputPath: (outputPath: string) => void,
|};
const LocalProjectPreCreationDialog = ({
open,
isOpening,
onClose,
onCreate,
outputPath,
onChangeOutputPath,
}: Props): React.Node => {
return (
<Dialog
title={<Trans>Project settings</Trans>}
maxWidth="sm"
open={open}
actions={[
<FlatButton
disabled={isOpening}
key="cancel"
label={<Trans>Cancel</Trans>}
onClick={onClose}
/>,
<RaisedButton
primary
disabled={isOpening}
key="create"
label={<Trans>Create project</Trans>}
onClick={onCreate}
/>,
]}
>
<Column>
<LocalFolderPicker
fullWidth
value={outputPath}
onChange={onChangeOutputPath}
type="create-game"
/>
</Column>
</Dialog>
);
};
export default LocalProjectPreCreationDialog;

View File

@@ -0,0 +1,104 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import RaisedButton from '../UI/RaisedButton';
import { Column, Spacer } from '../UI/Grid';
import LocalFolderPicker from '../UI/LocalFolderPicker';
import TextField from '../UI/TextField';
type Props = {|
open: boolean,
isOpening?: boolean,
onClose: () => void,
onCreate: () => void | Promise<void>,
outputPath?: string,
onChangeOutputPath?: (outputPath: string) => void,
projectName: string,
onChangeProjectName: (name: string) => void,
|};
const ProjectPreCreationDialog = ({
open,
isOpening,
onClose,
onCreate,
outputPath,
onChangeOutputPath,
projectName,
onChangeProjectName,
}: Props): React.Node => {
const [projectNameError, setProjectNameError] = React.useState<?React.Node>(
null
);
const onValidate = React.useCallback(
() => {
setProjectNameError(null);
if (!projectName) {
setProjectNameError(
<Trans>Please enter a name for your project.</Trans>
);
return;
}
onCreate();
},
[onCreate, projectName]
);
const _onChangeProjectName = React.useCallback(
(event, text) => {
if (projectNameError) setProjectNameError(null);
onChangeProjectName(text);
},
[onChangeProjectName, projectNameError]
);
return (
<Dialog
title={<Trans>Project settings</Trans>}
maxWidth="sm"
open={open}
actions={[
<FlatButton
disabled={isOpening}
key="cancel"
label={<Trans>Cancel</Trans>}
onClick={onClose}
/>,
<RaisedButton
primary
disabled={isOpening}
key="create"
label={<Trans>Create project</Trans>}
onClick={onValidate}
/>,
]}
>
<Column>
<TextField
type="text"
errorText={projectNameError}
disabled={isOpening}
value={projectName}
onChange={_onChangeProjectName}
floatingLabelText={<Trans>Project name</Trans>}
/>
{onChangeOutputPath && (
<>
<Spacer />
<LocalFolderPicker
fullWidth
value={outputPath || ''}
onChange={onChangeOutputPath}
type="create-game"
/>
</>
)}
</Column>
</Dialog>
);
};
export default ProjectPreCreationDialog;

View File

@@ -1,52 +1,48 @@
// @flow
import { t } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import UrlStorageProvider from '../../ProjectsStorage/UrlStorageProvider';
import { type StorageProvider, type FileMetadata } from '../../ProjectsStorage';
import { getExample } from '../../Utils/GDevelopServices/Example';
import { sendNewGameCreated } from '../../Utils/Analytics/EventSender';
import { showErrorBox } from '../../UI/Messages/MessageBox';
import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example';
import {
type OnCreateBlankFunction,
type OnCreateFromExampleShortHeaderFunction,
} from '../CreateProjectDialog';
const gd: libGDevelop = global.gd;
// Signatures of this function and its arguments contain useless arguments
// because this function has to match the signature of LocalCreation.onCreateBlank
// that needs these arguments.
export const onCreateBlank = async ({
export const onCreateBlank: OnCreateBlankFunction = async ({
i18n,
outputPath,
}: {|
i18n: I18nType,
outputPath?: string,
|}): Promise<?{|
project: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
|}> => {
projectName,
}) => {
sendNewGameCreated('');
const project = gd.ProjectHelper.createNewGDJSProject();
return { project, storageProvider: null, fileMetadata: null };
return {
project,
projectName,
storageProvider: null,
fileMetadata: null,
};
};
export const onCreateFromExampleShortHeader = async ({
export const onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction = async ({
i18n,
exampleShortHeader,
projectName,
outputPath,
}: {|
i18n: I18nType,
exampleShortHeader: ExampleShortHeader,
outputPath?: string,
|}): Promise<?{|
storageProvider: StorageProvider,
fileMetadata: FileMetadata,
|}> => {
}) => {
try {
const example = await getExample(exampleShortHeader);
sendNewGameCreated(example.projectFileUrl);
return {
storageProvider: UrlStorageProvider,
projectName,
fileMetadata: {
fileIdentifier: example.projectFileUrl,
},

View File

@@ -1,33 +1,28 @@
// @flow
import axios from 'axios';
import { t } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import LocalFileStorageProvider from '../../ProjectsStorage/LocalFileStorageProvider';
import { type StorageProvider, type FileMetadata } from '../../ProjectsStorage';
import optionalRequire from '../../Utils/OptionalRequire.js';
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 { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example';
import { showGameFileCreationError } from '../LocalExamples';
import {
type OnCreateBlankFunction,
type OnCreateFromExampleShortHeaderFunction,
} from '../CreateProjectDialog';
const gd: libGDevelop = global.gd;
const path = optionalRequire('path');
var fs = optionalRequire('fs-extra');
export const onCreateBlank = async ({
export const onCreateBlank: OnCreateBlankFunction = async ({
i18n,
outputPath,
}: {|
i18n: I18nType,
outputPath?: string,
|}): Promise<?{|
project: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
|}> => {
projectName,
}) => {
if (!fs || !outputPath) return;
try {
@@ -45,21 +40,16 @@ export const onCreateBlank = async ({
project,
storageProvider: LocalFileStorageProvider,
fileMetadata: { fileIdentifier: filePath },
projectName,
};
};
export const onCreateFromExampleShortHeader = async ({
export const onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction = async ({
i18n,
exampleShortHeader,
outputPath,
}: {|
i18n: I18nType,
exampleShortHeader: ExampleShortHeader,
outputPath?: string,
|}): Promise<?{|
storageProvider: StorageProvider,
fileMetadata: FileMetadata,
|}> => {
projectName,
}) => {
if (!fs || !outputPath) return;
try {
const example = await getExample(exampleShortHeader);
@@ -82,6 +72,7 @@ export const onCreateFromExampleShortHeader = async ({
return {
storageProvider: LocalFileStorageProvider,
fileMetadata: { fileIdentifier: localFilePath },
projectName,
};
} catch (error) {
showErrorBox({

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +0,0 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { action } from '@storybook/addon-actions';
import muiDecorator from '../ThemeDecorator';
import paperDecorator from '../PaperDecorator';
import LocalProjectPreCreationDialog from '../../ProjectCreation/LocalProjectPreCreationDialog';
export default {
title: 'Project Creation/LocalProjectPreCreationDialog',
component: LocalProjectPreCreationDialog,
decorators: [paperDecorator, muiDecorator],
};
export const Open = () => (
<LocalProjectPreCreationDialog
open
outputPath="/path/to/project/file.json"
onChangeOutputPath={action('change output path')}
onClose={() => action('click on close')()}
onCreate={() => action('click on create')()}
/>
);
export const Disabled = () => (
<LocalProjectPreCreationDialog
open
isOpening
outputPath="/path/to/project/file.json"
onChangeOutputPath={action('change output path')}
onClose={() => action('click on close')()}
onCreate={() => action('click on create')()}
/>
);

View File

@@ -0,0 +1,45 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { action } from '@storybook/addon-actions';
import muiDecorator from '../ThemeDecorator';
import paperDecorator from '../PaperDecorator';
import ProjectPreCreationDialog from '../../ProjectCreation/ProjectPreCreationDialog';
export default {
title: 'Project Creation/ProjectPreCreationDialog',
component: ProjectPreCreationDialog,
decorators: [paperDecorator, muiDecorator],
};
export const Open = () => {
const [projectName, setProjectName] = React.useState('Project Name');
return (
<ProjectPreCreationDialog
open
outputPath="/path/to/project/file.json"
onChangeOutputPath={action('change output path')}
onClose={() => action('click on close')()}
onCreate={() => action('click on create')()}
projectName={projectName}
onChangeProjectName={setProjectName}
/>
);
};
export const Disabled = () => {
const [projectName, setProjectName] = React.useState('Project Name');
return (
<ProjectPreCreationDialog
open
isOpening
outputPath="/path/to/project/file.json"
onChangeOutputPath={action('change output path')}
onClose={() => action('click on close')()}
onCreate={() => action('click on create')()}
projectName={projectName}
onChangeProjectName={setProjectName}
/>
);
};