Compare commits

...

1 Commits

Author SHA1 Message Date
Florian Rival
5193b617b3 Improve create new project dialog
TODO: disable button if no prompt entered
2024-03-25 13:55:56 +01:00
8 changed files with 250 additions and 125 deletions

View File

@@ -17,7 +17,9 @@ export type ExampleStoreDialogProps = {|
onSelectPrivateGameTemplateListingData: (
privateGameTemplateListingData: ?PrivateGameTemplateListingData
) => void,
onOpenNewProjectSetupDialog: () => void,
onOpenNewProjectSetupDialog: (
initialTab: 'from-scratch' | 'ai' | 'example'
) => void,
isProjectOpening: boolean,
|};
@@ -44,7 +46,7 @@ const ExampleStoreDialog = ({
id="create-blank-project-button"
label={<Trans>Create a blank project</Trans>}
primary
onClick={onOpenNewProjectSetupDialog}
onClick={() => onOpenNewProjectSetupDialog('from-scratch')}
/>,
],
[onClose, onOpenNewProjectSetupDialog]
@@ -59,7 +61,7 @@ const ExampleStoreDialog = ({
title={<Trans>Create a new project</Trans>}
actions={actions}
onRequestClose={onClose}
onApply={onOpenNewProjectSetupDialog}
onApply={() => onOpenNewProjectSetupDialog('from-scratch')}
open={open}
fullHeight
flexColumnBody
@@ -67,7 +69,9 @@ const ExampleStoreDialog = ({
<ExampleStore
focusOnMount
isOpening={isProjectOpening}
onOpenNewProjectSetupDialog={onOpenNewProjectSetupDialog}
onOpenNewProjectSetupDialog={() =>
onOpenNewProjectSetupDialog('example')
}
onSelectExampleShortHeader={onSelectExampleShortHeader}
onSelectPrivateGameTemplateListingData={
onSelectPrivateGameTemplateListingData

View File

@@ -98,7 +98,9 @@ export type RenderEditorContainerProps = {|
canInstallPrivateAsset: () => boolean,
// Project creation
onOpenNewProjectSetupDialog: () => void,
onOpenNewProjectSetupDialog: (
initialTab: 'from-scratch' | 'ai' | 'example'
) => void,
// Project save
onSave: () => Promise<void>,

View File

@@ -108,7 +108,7 @@ type Props = {|
canOpen: boolean,
onChooseProject: () => void,
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
onOpenNewProjectSetupDialog: () => void,
onOpenNewProjectSetupDialog: (initialTab: 'ai' | 'from-scratch') => void,
onSelectExampleShortHeader: (exampleShortHeader: ExampleShortHeader) => void,
onSelectPrivateGameTemplateListingData: (
privateGameTemplateListingData: PrivateGameTemplateListingData
@@ -179,6 +179,8 @@ const BuildSection = ({
setLastModifiedInfoByProjectId,
] = React.useState({});
const isMediumOrSmallScreen =
windowSize === 'small' || windowSize === 'medium';
const columnsCount = getItemsColumns(windowSize, isLandscape);
const allGameTemplatesAndExamplesFlaggedAsGameCount = React.useMemo(
@@ -488,34 +490,41 @@ const BuildSection = ({
primary
fullWidth={!canOpen}
label={
isMobile ? (
isMediumOrSmallScreen ? (
<Trans>Create</Trans>
) : (
<Trans>Create a project</Trans>
)
}
onClick={onOpenNewProjectSetupDialog}
onClick={() => onOpenNewProjectSetupDialog('from-scratch')}
icon={<Add fontSize="small" />}
id="home-create-project-button"
/>
<RaisedButton
primary
fullWidth={!canOpen}
label={
isMediumOrSmallScreen ? (
<Trans> AI prototype</Trans>
) : (
<Trans> Prototype with AI</Trans>
)
}
onClick={() => onOpenNewProjectSetupDialog('ai')}
id="home-create-project-button"
/>
{canOpen && (
<>
<Text>
<Trans>or</Trans>
</Text>
<Spacer />
<TextButton
secondary
label={
isMobile ? (
<Trans>Open</Trans>
) : (
<Trans>Open a project</Trans>
)
}
onClick={onChooseProject}
/>
</>
<TextButton
secondary
label={
isMediumOrSmallScreen ? (
<Trans>Import</Trans>
) : (
<Trans>Import a project</Trans>
)
}
onClick={onChooseProject}
/>
)}
</LineStackLayout>
</Column>

View File

@@ -114,7 +114,9 @@ type Props = {|
onOpenAbout: () => void,
// Project creation
onOpenNewProjectSetupDialog: () => void,
onOpenNewProjectSetupDialog: (
initialTab: 'from-scratch' | 'ai' | 'example'
) => void,
// Project save
onSave: () => Promise<void>,

View File

@@ -12,7 +12,9 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
type Props = {|
isProjectOpening: boolean,
onOpenNewProjectSetupDialog: () => void,
onOpenNewProjectSetupDialog: (
initialTab: 'from-scratch' | 'ai' | 'example'
) => void,
|};
const useExampleOrGameTemplateDialogs = ({
@@ -137,7 +139,7 @@ const useExampleOrGameTemplateDialogs = ({
<ExampleDialog
isOpening={isProjectOpening}
exampleShortHeader={selectedExampleShortHeader}
onOpen={onOpenNewProjectSetupDialog}
onOpen={() => onOpenNewProjectSetupDialog('example')}
onClose={() => setSelectedExampleShortHeader(null)}
/>
)}
@@ -148,7 +150,9 @@ const useExampleOrGameTemplateDialogs = ({
selectedPrivateGameTemplate.privateGameTemplateListingData
}
isPurchaseDialogOpen={!!purchasingGameTemplateListingData}
onCreateWithGameTemplate={onOpenNewProjectSetupDialog}
onCreateWithGameTemplate={() =>
onOpenNewProjectSetupDialog('example')
}
onGameTemplateOpen={privateGameTemplateListingData =>
setSelectedPrivateGameTemplate({
privateGameTemplateListingData,

View File

@@ -359,9 +359,9 @@ const MainFrame = (props: Props) => {
openPreferencesDialog,
] = React.useState<boolean>(false);
const [
newProjectSetupDialogOpen,
setNewProjectSetupDialogOpen,
] = React.useState<boolean>(false);
newProjectSetupDialogInitialTab,
setNewProjectSetupDialogInitialTab,
] = React.useState<null | 'from-scratch' | 'ai' | 'example'>(null);
const [isProjectOpening, setIsProjectOpening] = React.useState<boolean>(
false
@@ -498,7 +498,7 @@ const MainFrame = (props: Props) => {
openExampleStoreDialog,
} = useExampleOrGameTemplateDialogs({
isProjectOpening,
onOpenNewProjectSetupDialog: () => setNewProjectSetupDialogOpen(true),
onOpenNewProjectSetupDialog: setNewProjectSetupDialogInitialTab,
});
/**
@@ -1144,7 +1144,7 @@ const MainFrame = (props: Props) => {
},
getStorageProviderOperations,
afterCreatingProject: async ({ project, editorTabs, oldProjectId }) => {
setNewProjectSetupDialogOpen(false);
setNewProjectSetupDialogInitialTab(null);
closeExampleStoreDialog({ deselectExampleAndGameTemplate: true });
findLeaderboardsToReplace(project, oldProjectId);
openSceneOrProjectManager({
@@ -2960,7 +2960,7 @@ const MainFrame = (props: Props) => {
onLaunchDebugPreview: launchDebuggerAndPreview,
onLaunchNetworkPreview: launchNetworkPreview,
onOpenHomePage: openHomePage,
onCreateBlank: () => setNewProjectSetupDialogOpen(true),
onCreateBlank: () => setNewProjectSetupDialogInitialTab('from-scratch'),
onOpenProject: () => openOpenFromStorageProviderDialog(),
onSaveProject: saveProject,
onSaveProjectAs: saveProjectAs,
@@ -3020,7 +3020,7 @@ const MainFrame = (props: Props) => {
onExportProject: () => openShareDialog('publish'),
onInviteCollaborators: () => openShareDialog('invite'),
onCreateProject: openExampleStoreDialog,
onCreateBlank: () => setNewProjectSetupDialogOpen(true),
onCreateBlank: () => setNewProjectSetupDialogInitialTab('from-scratch'),
onOpenProjectManager: () => openProjectManager(true),
onOpenHomePage: openHomePage,
onOpenDebugger: openDebugger,
@@ -3224,9 +3224,7 @@ const MainFrame = (props: Props) => {
canInstallPrivateAsset,
onChooseProject: () => openOpenFromStorageProviderDialog(),
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
onOpenNewProjectSetupDialog: () => {
setNewProjectSetupDialogOpen(true);
},
onOpenNewProjectSetupDialog: setNewProjectSetupDialogInitialTab,
onOpenProjectManager: () => openProjectManager(true),
onCloseProject: () => askToCloseProject(),
onOpenExampleStore: openExampleStoreDialog,
@@ -3241,7 +3239,7 @@ const MainFrame = (props: Props) => {
privateGameTemplateListingData,
openDialog: false,
});
setNewProjectSetupDialogOpen(true);
setNewProjectSetupDialogInitialTab('example');
},
onOpenProfile: () => openProfileDialog(true),
onOpenLanguageDialog: () => openLanguageDialog(true),
@@ -3366,11 +3364,12 @@ const MainFrame = (props: Props) => {
}}
/>
)}
{newProjectSetupDialogOpen && (
{newProjectSetupDialogInitialTab && (
<NewProjectSetupDialog
initialTab={newProjectSetupDialogInitialTab}
authenticatedUser={authenticatedUser}
isOpeningProject={isProjectOpening}
onClose={() => setNewProjectSetupDialogOpen(false)}
onClose={() => setNewProjectSetupDialogInitialTab(null)}
onCreateEmptyProject={createEmptyProject}
onCreateFromExample={createProjectFromExample}
onCreateProjectFromPrivateGameTemplate={

View File

@@ -17,8 +17,8 @@ import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import CreateProfile from '../Profile/CreateProfile';
import Paper from '../UI/Paper';
import { Column, Line } from '../UI/Grid';
import LeftLoader from '../UI/LeftLoader';
import { Column, LargeSpacer, Line, Spacer } from '../UI/Grid';
import {
checkIfHasTooManyCloudProjects,
MaxProjectCountAlertMessage,
@@ -42,7 +42,6 @@ import ResolutionOptions, {
defaultCustomHeight,
} from './ResolutionOptions';
import Text from '../UI/Text';
import DismissableAlertMessage from '../UI/DismissableAlertMessage';
import generatePrompt from '../Utils/ProjectPromptGenerator';
import ProjectGeneratingDialog from './ProjectGeneratingDialog';
import useAlertDialog from '../UI/Alert/useAlertDialog';
@@ -53,6 +52,7 @@ import { I18n } from '@lingui/react';
import GetSubscriptionCard from '../Profile/Subscription/GetSubscriptionCard';
import { type PrivateGameTemplateListingData } from '../Utils/GDevelopServices/Shop';
import { extractGDevelopApiErrorStatusAndCode } from '../Utils/GDevelopServices/Errors';
import { Tabs } from '../UI/Tabs';
const electron = optionalRequire('electron');
const remote = optionalRequire('@electron/remote');
@@ -71,6 +71,7 @@ export type NewProjectSetup = {|
|};
type Props = {|
initialTab?: 'ai' | 'from-scratch' | 'example',
isOpeningProject?: boolean,
onClose: () => void,
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
@@ -96,6 +97,7 @@ type Props = {|
|};
const NewProjectSetupDialog = ({
initialTab,
isOpeningProject,
onClose,
onCreateEmptyProject,
@@ -108,6 +110,10 @@ const NewProjectSetupDialog = ({
storageProviders,
authenticatedUser,
}: Props): React.Node => {
const [currentTab, setCurrentTab] = React.useState<
'ai' | 'from-scratch' | 'example'
>(initialTab || 'ai');
const generateProjectName = () =>
selectedExampleShortHeader
? `${generateName()} (${selectedExampleShortHeader.name})`
@@ -220,7 +226,7 @@ const NewProjectSetupDialog = ({
? authenticatedUser.limits.quotas['ai-project-generation']
: null;
const canGenerateProjectFromPrompt =
generationCurrentUsage && !generationCurrentUsage.limitReached;
!!generationCurrentUsage && !generationCurrentUsage.limitReached;
const needUserAuthenticationForStorage =
storageProvider.needUserAuthentication && !authenticatedUser.authenticated;
@@ -243,7 +249,7 @@ const NewProjectSetupDialog = ({
const isLoading = isGeneratingProject || isOpeningProject;
const isStartingProjectFromScratch =
const isCreatingANewProject =
!selectedExampleShortHeader && !selectedPrivateGameTemplateListingData;
// On the local app, prefer to always have something saved so that the user is not blocked.
@@ -414,6 +420,24 @@ const NewProjectSetupDialog = ({
title={<Trans>New Project</Trans>}
id="project-pre-creation-dialog"
maxWidth="sm"
fixedContent={
isCreatingANewProject ? (
<Tabs
value={currentTab}
onChange={setCurrentTab}
options={[
{
value: 'ai',
label: <Trans>Create for me</Trans>,
},
{
value: 'from-scratch',
label: <Trans>Create from scratch</Trans>,
},
]}
/>
) : null
}
actions={[
<FlatButton
disabled={isLoading}
@@ -436,16 +460,20 @@ const NewProjectSetupDialog = ({
onApply={() => onValidate(i18n)}
>
<ColumnStackLayout noMargin>
{isStartingProjectFromScratch && (
<ResolutionOptions
onClick={key => setResolutionOption(key)}
selectedOption={resolutionOption}
disabled={isLoading}
customHeight={customHeight}
customWidth={customWidth}
onCustomHeightChange={setCustomHeight}
onCustomWidthChange={setCustomWidth}
/>
<LargeSpacer />
{isCreatingANewProject && currentTab === 'from-scratch' && (
<>
<ResolutionOptions
onClick={key => setResolutionOption(key)}
selectedOption={resolutionOption}
disabled={isLoading}
customHeight={customHeight}
customWidth={customWidth}
onCustomHeightChange={setCustomHeight}
onCustomWidthChange={setCustomWidth}
/>
<Spacer />
</>
)}
<TextField
type="text"
@@ -467,6 +495,69 @@ const NewProjectSetupDialog = ({
autoFocus="desktop"
maxLength={100}
/>
{isCreatingANewProject && currentTab === 'ai' && (
<ColumnStackLayout noMargin>
{authenticatedUser.authenticated &&
!canGenerateProjectFromPrompt && (
<GetSubscriptionCard subscriptionDialogOpeningReason="Generate project from prompt">
<Line>
<Column noMargin>
<Text noMargin>
<Trans>
You've used all your daily pre-made AI scenes!
Generate as many as you want with a subscription.
</Trans>
</Text>
</Column>
</Line>
</GetSubscriptionCard>
)}
<LineStackLayout
expand
noMargin
alignItems="center"
justifyContent="center"
>
<RobotIcon />
<TextField
type="text"
multiline
maxLength={200}
fullWidth
disabled={
isLoading ||
!authenticatedUser.authenticated ||
!isOnline ||
!canGenerateProjectFromPrompt
}
value={generationPrompt}
onChange={(e, text) => setGenerationPrompt(text)}
floatingLabelText={<Trans>AI prompt</Trans>}
floatingLabelFixed
translatableHintText={
!authenticatedUser.authenticated || !isOnline
? t`Log in to generate a project from a prompt`
: t`Type a prompt yourself or generate a random one`
}
endAdornment={
<IconButton
size="small"
onClick={() => setGenerationPrompt(generatePrompt())}
tooltip={t`Generate random prompt`}
disabled={
isLoading ||
!authenticatedUser.authenticated ||
!isOnline ||
!canGenerateProjectFromPrompt
}
>
<Refresh />
</IconButton>
}
/>
</LineStackLayout>
</ColumnStackLayout>
)}
<SelectField
fullWidth
disabled={isLoading}
@@ -533,73 +624,8 @@ const NewProjectSetupDialog = ({
setSaveAsLocation,
newProjectsDefaultFolder,
})}
{isStartingProjectFromScratch && (
{isCreatingANewProject && currentTab === 'from-scratch' && (
<ColumnStackLayout noMargin expand>
<DismissableAlertMessage
kind="info"
identifier="new-generate-project-from-prompt"
>
<Trans>NEW! Generate a pre-made AI scene with assets.</Trans>
</DismissableAlertMessage>
<LineStackLayout
expand
noMargin
alignItems="center"
justifyContent="center"
>
<RobotIcon />
<TextField
type="text"
multiline
maxLength={200}
fullWidth
disabled={
isLoading ||
!authenticatedUser.authenticated ||
!isOnline ||
!canGenerateProjectFromPrompt
}
value={generationPrompt}
onChange={(e, text) => setGenerationPrompt(text)}
floatingLabelText={<Trans>AI prompt</Trans>}
floatingLabelFixed
translatableHintText={
!authenticatedUser.authenticated || !isOnline
? t`Log in to generate a project from a prompt`
: t`Type a prompt yourself or generate a random one`
}
endAdornment={
<IconButton
size="small"
onClick={() => setGenerationPrompt(generatePrompt())}
tooltip={t`Generate random prompt`}
disabled={
isLoading ||
!authenticatedUser.authenticated ||
!isOnline ||
!canGenerateProjectFromPrompt
}
>
<Refresh />
</IconButton>
}
/>
</LineStackLayout>
{authenticatedUser.authenticated &&
!canGenerateProjectFromPrompt && (
<GetSubscriptionCard subscriptionDialogOpeningReason="Generate project from prompt">
<Line>
<Column noMargin>
<Text noMargin>
<Trans>
You've used all your daily pre-made AI scenes!
Generate as many as you want with a subscription.
</Trans>
</Text>
</Column>
</Line>
</GetSubscriptionCard>
)}
<Text size="sub-title">
<Trans>Advanced File options</Trans>
</Text>

View File

@@ -23,9 +23,10 @@ export default {
decorators: [paperDecorator, muiDecorator],
};
export const OpenAndNotAuthenticated = () => {
export const OpenOnAIAndNotAuthenticated = () => {
return (
<NewProjectSetupDialog
initialTab="ai"
authenticatedUser={fakeNotAuthenticatedUser}
storageProviders={[
UrlStorageProvider,
@@ -47,9 +48,59 @@ export const OpenAndNotAuthenticated = () => {
);
};
export const OpenAndAuthenticated = () => {
export const OpenOnAIAndAuthenticated = () => {
return (
<NewProjectSetupDialog
initialTab="ai"
authenticatedUser={fakeSilverAuthenticatedUser}
storageProviders={[
UrlStorageProvider,
CloudStorageProvider,
GoogleDriveStorageProvider,
DownloadFileStorageProvider,
]}
onClose={() => action('click on close')()}
onCreateEmptyProject={() => action('create empty')()}
onCreateFromExample={() => action('create from example')()}
onCreateWithLogin={() => action('create with login')()}
onCreateFromAIGeneration={() => action('create from AI generation')()}
onCreateProjectFromPrivateGameTemplate={() =>
action('create project from private game template')()
}
selectedExampleShortHeader={null}
selectedPrivateGameTemplateListingData={null}
/>
);
};
export const OpenOnFromScratchAndNotAuthenticated = () => {
return (
<NewProjectSetupDialog
initialTab="from-scratch"
authenticatedUser={fakeNotAuthenticatedUser}
storageProviders={[
UrlStorageProvider,
CloudStorageProvider,
GoogleDriveStorageProvider,
DownloadFileStorageProvider,
]}
onClose={() => action('click on close')()}
onCreateEmptyProject={() => action('create empty')()}
onCreateFromExample={() => action('create from example')()}
onCreateWithLogin={() => action('create with login')()}
onCreateFromAIGeneration={() => action('create from AI generation')()}
onCreateProjectFromPrivateGameTemplate={() =>
action('create project from private game template')()
}
selectedExampleShortHeader={null}
selectedPrivateGameTemplateListingData={null}
/>
);
};
export const OpenOnFromScratchAndAuthenticated = () => {
return (
<NewProjectSetupDialog
initialTab="from-scratch"
authenticatedUser={fakeSilverAuthenticatedUser}
storageProviders={[
UrlStorageProvider,
@@ -96,9 +147,37 @@ export const Opening = () => {
);
};
export const LimitsReached = () => {
export const OpenOnFromScratchAndLimitsReached = () => {
return (
<NewProjectSetupDialog
initialTab="from-scratch"
authenticatedUser={
fakeAuthenticatedUserWithNoSubscriptionAndTooManyCloudProjects
}
storageProviders={[
CloudStorageProvider,
UrlStorageProvider,
GoogleDriveStorageProvider,
DownloadFileStorageProvider,
]}
onClose={() => action('click on close')()}
onCreateEmptyProject={() => action('create empty')()}
onCreateFromExample={() => action('create from example')()}
onCreateWithLogin={() => action('create with login')()}
onCreateFromAIGeneration={() => action('create from AI generation')()}
onCreateProjectFromPrivateGameTemplate={() =>
action('create project from private game template')()
}
selectedExampleShortHeader={null}
selectedPrivateGameTemplateListingData={null}
/>
);
};
export const OpenOnAIAndLimitsReached = () => {
return (
<NewProjectSetupDialog
initialTab="ai"
authenticatedUser={
fakeAuthenticatedUserWithNoSubscriptionAndTooManyCloudProjects
}