@@ -420,6 +440,7 @@ export const CategoryTile = ({
imageAlt,
onSelect,
style,
+ disabled,
}: {|
id: string,
title: React.Node,
@@ -428,20 +449,23 @@ export const CategoryTile = ({
onSelect: () => void,
/** Props needed so that GridList component can adjust tile size */
style?: any,
+ disabled?: boolean,
|}) => {
- const classesForGridListItem = useStylesForGridListItem();
+ const classesForGridListItem = useStylesForGridListItem({
+ disabled,
+ });
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
): void => {
- if (shouldValidate(event)) {
+ if (shouldValidate(event) && !disabled) {
onSelect();
}
}}
style={style}
- onClick={onSelect}
+ onClick={!disabled ? onSelect : undefined}
>
void,
/** Props needed so that GridList component can adjust tile size */
style?: any,
owned: boolean,
+ disabled?: boolean,
|}) => {
- const classesForGridListItem = useStylesForGridListItem();
+ const classesForGridListItem = useStylesForGridListItem({
+ disabled,
+ });
return (
): void => {
- if (shouldValidate(event)) {
+ if (shouldValidate(event) && !disabled) {
onSelect();
}
}}
style={style}
- onClick={onSelect}
+ onClick={!disabled ? onSelect : undefined}
>
void,
@@ -530,6 +559,7 @@ export const ExampleTile = ({
style?: any,
customTitle?: string,
useQuickCustomizationThumbnail?: boolean,
+ disabled?: boolean,
|}) => {
const thumbnailImgUrl = React.useMemo(
() => {
@@ -546,27 +576,38 @@ export const ExampleTile = ({
[exampleShortHeader, useQuickCustomizationThumbnail]
);
- const classesForGridListItem = useStylesForGridListItem();
+ const classesForGridListItem = useStylesForGridListItem({ disabled });
return (
): void => {
- if (shouldValidate(event)) {
+ if (shouldValidate(event) && !disabled) {
onSelect();
}
}}
style={style}
- onClick={onSelect}
+ onClick={!disabled ? onSelect : undefined}
>
{exampleShortHeader ? (
-
+ thumbnailImgUrl ? (
+
+ ) : (
+
+ {exampleShortHeader.name}
+
+ )
) : (
void,
onSelectExampleShortHeader: ExampleShortHeader => void,
- onPreviewPrivateGameTemplateListingData: PrivateGameTemplateListingData => void,
- onOpenPrivateGameTemplateListingData: (
- privateGameTemplateListingData: PrivateGameTemplateListingData
- ) => void,
+ onSelectPrivateGameTemplateListingData: PrivateGameTemplateListingData => void,
onOpenLanguageDialog: () => void,
selectInAppTutorial: (tutorialId: string) => void,
onOpenProfile: () => void,
@@ -127,6 +123,7 @@ export type RenderEditorContainerProps = {|
i18n: I18nType
) => Promise,
onOpenTemplateFromTutorial: (tutorialId: string) => Promise,
+ onOpenPrivateGameTemplateListingData: PrivateGameTemplateListingData => void,
// Project save
onSave: () => Promise,
diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js
index e6af02633983..b4353293189f 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js
@@ -44,8 +44,7 @@ import Refresh from '../../../../UI/CustomSvgIcons/Refresh';
import ProjectFileListItem from './ProjectFileListItem';
import { type MenuItemTemplate } from '../../../../UI/Menu/Menu.flow';
import {
- getAllGameTemplatesAndExamplesFlaggedAsGameCount,
- getExampleAndTemplateItemsForBuildSection,
+ getExampleAndTemplateTiles,
getLastModifiedInfoByProjectId,
getProjectLineHeight,
transformCloudProjectsIntoFileMetadataWithStorageProviderName,
@@ -54,7 +53,6 @@ import ErrorBoundary from '../../../../UI/ErrorBoundary';
import InfoBar from '../../../../UI/Messages/InfoBar';
import GridList from '@material-ui/core/GridList';
import type { WindowSizeType } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
-import FlatButton from '../../../../UI/FlatButton';
import useAlertDialog from '../../../../UI/Alert/useAlertDialog';
import optionalRequire from '../../../../Utils/OptionalRequire';
import { deleteCloudProject } from '../../../../Utils/GDevelopServices/Project';
@@ -64,6 +62,8 @@ import ContextMenu, {
} from '../../../../UI/Menu/ContextMenu';
import type { ClientCoordinates } from '../../../../Utils/UseLongTouch';
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
+import ExampleStore from '../../../../AssetStore/ExampleStore';
+
const electron = optionalRequire('electron');
const path = optionalRequire('path');
@@ -115,7 +115,6 @@ type Props = {|
) => void,
storageProviders: Array,
i18n: I18nType,
- onOpenExampleStore: () => void,
onManageGame: (gameId: string) => void,
canManageGame: (gameId: string) => boolean,
closeProject: () => Promise,
@@ -139,7 +138,6 @@ const BuildSection = ({
onOpenRecentFile,
storageProviders,
i18n,
- onOpenExampleStore,
onManageGame,
canManageGame,
closeProject,
@@ -187,16 +185,6 @@ const BuildSection = ({
const columnsCount = getItemsColumns(windowSize, isLandscape);
- const allGameTemplatesAndExamplesFlaggedAsGameCount = React.useMemo(
- () =>
- getAllGameTemplatesAndExamplesFlaggedAsGameCount({
- privateGameTemplateListingDatas,
- exampleShortHeaders,
- columnsCount,
- }),
- [privateGameTemplateListingDatas, exampleShortHeaders, columnsCount]
- );
-
let projectFiles: Array = getRecentProjectFiles().filter(
file => file.fileMetadata
);
@@ -380,40 +368,27 @@ const BuildSection = ({
const examplesAndTemplatesToDisplay = React.useMemo(
() =>
- getExampleAndTemplateItemsForBuildSection({
+ getExampleAndTemplateTiles({
receivedGameTemplates: authenticatedUser.receivedGameTemplates,
privateGameTemplateListingDatas,
exampleShortHeaders,
onSelectPrivateGameTemplateListingData,
onSelectExampleShortHeader,
i18n,
- numberOfItemsExclusivelyInCarousel: showAllGameTemplates
- ? 0
- : isMobile
- ? 3
- : 5,
- numberOfItemsInCarousel: showAllGameTemplates ? 0 : isMobile ? 8 : 12,
- numberOfItemsInGrid: showAllGameTemplates
- ? allGameTemplatesAndExamplesFlaggedAsGameCount
- : isMobile
- ? 16
- : 20,
+ numberOfItemsExclusivelyInCarousel: isMobile ? 3 : 5,
+ numberOfItemsInCarousel: isMobile ? 8 : 12,
privateGameTemplatesPeriodicity: shouldDisplayPremiumGameTemplates
- ? isMobile
- ? 2
- : 3
+ ? 2
: 0,
}),
[
authenticatedUser.receivedGameTemplates,
- showAllGameTemplates,
exampleShortHeaders,
onSelectExampleShortHeader,
onSelectPrivateGameTemplateListingData,
privateGameTemplateListingDatas,
i18n,
isMobile,
- allGameTemplatesAndExamplesFlaggedAsGameCount,
shouldDisplayPremiumGameTemplates,
]
);
@@ -425,24 +400,19 @@ const BuildSection = ({
const skeletonLineHeight = getProjectLineHeight({ isMobile });
const pageContent = showAllGameTemplates ? (
- setShowAllGameTemplates(false)}>
-
-
- {examplesAndTemplatesToDisplay.gridItems}
-
-
- See more}
- onClick={onOpenExampleStore}
- />
-
+ setShowAllGameTemplates(false)}
+ flexBody
+ >
+
+
) : (
@@ -475,7 +445,7 @@ const BuildSection = ({
displayItemTitles={false}
browseAllLabel={Browse all templates}
onBrowseAllClick={() => setShowAllGameTemplates(true)}
- items={examplesAndTemplatesToDisplay.carouselItems}
+ items={examplesAndTemplatesToDisplay.carouselThumbnailItems}
browseAllIcon={}
roundedImages
displayArrowsOnDesktop
@@ -523,7 +493,7 @@ const BuildSection = ({
isMobile ? (
Create
) : (
- Create from scratch
+ Create new game
)
}
onClick={onOpenNewProjectSetupDialog}
@@ -672,7 +642,7 @@ const BuildSection = ({
cellHeight="auto"
spacing={cellSpacing}
>
- {examplesAndTemplatesToDisplay.gridItems}
+ {examplesAndTemplatesToDisplay.gridItemsCompletingCarousel}
diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js
index 4109e45303d1..386e09bf2a8f 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js
@@ -164,6 +164,70 @@ const formatExampleShortHeaderForCarousel = ({
};
};
+const formatItemForCarousel = ({
+ item,
+ onSelectGameTemplate,
+ onSelectExample,
+ i18n,
+ receivedGameTemplates,
+}: {
+ item: PrivateGameTemplateListingData | ExampleShortHeader,
+ onSelectGameTemplate: PrivateGameTemplateListingData => void,
+ onSelectExample: ExampleShortHeader => void,
+ i18n: I18nType,
+ receivedGameTemplates: ?Array,
+}): CarouselThumbnail => {
+ if (item.previewImageUrls) {
+ return formatExampleShortHeaderForCarousel({
+ exampleShortHeader: item,
+ onSelectExample: onSelectExample,
+ });
+ } else {
+ return formatGameTemplateListingDataForCarousel({
+ i18n,
+ onSelectGameTemplate: onSelectGameTemplate,
+ gameTemplateListingData: item,
+ receivedGameTemplates: receivedGameTemplates,
+ });
+ }
+};
+
+const formatItemForGrid = ({
+ item,
+ onSelectGameTemplate,
+ onSelectExample,
+ i18n,
+ receivedGameTemplates,
+}: {
+ item: PrivateGameTemplateListingData | ExampleShortHeader,
+ onSelectGameTemplate: PrivateGameTemplateListingData => void,
+ onSelectExample: ExampleShortHeader => void,
+ i18n: I18nType,
+ receivedGameTemplates: ?Array,
+}): React.Node => {
+ if (item.previewImageUrls) {
+ return (
+ onSelectExample(item)}
+ />
+ );
+ } else {
+ const isTemplateOwned =
+ !!receivedGameTemplates &&
+ !!receivedGameTemplates.find(
+ receivedGameTemplate => receivedGameTemplate.id === item.id
+ );
+ return (
+ onSelectGameTemplate(item)}
+ owned={isTemplateOwned}
+ />
+ );
+ }
+};
+
/**
* This method allocates examples and private game templates between the
* build section carousel and grid.
@@ -171,17 +235,17 @@ const formatExampleShortHeaderForCarousel = ({
* should appear in the carousel only. The rest appears in both the carousel
* and the grid.
*/
-export const getExampleAndTemplateItemsForBuildSection = ({
+export const getExampleAndTemplateTiles = ({
receivedGameTemplates,
privateGameTemplateListingDatas,
exampleShortHeaders,
onSelectPrivateGameTemplateListingData,
onSelectExampleShortHeader,
i18n,
- numberOfItemsExclusivelyInCarousel,
- numberOfItemsInCarousel,
- numberOfItemsInGrid,
+ numberOfItemsExclusivelyInCarousel = 0,
+ numberOfItemsInCarousel = 0,
privateGameTemplatesPeriodicity,
+ showOwnedGameTemplatesFirst,
}: {|
receivedGameTemplates: ?Array,
privateGameTemplateListingDatas?: ?Array,
@@ -191,133 +255,156 @@ export const getExampleAndTemplateItemsForBuildSection = ({
) => void,
onSelectExampleShortHeader: (exampleShortHeader: ExampleShortHeader) => void,
i18n: I18nType,
- numberOfItemsExclusivelyInCarousel: number,
- numberOfItemsInCarousel: number,
- numberOfItemsInGrid: number,
+ numberOfItemsExclusivelyInCarousel?: number,
+ numberOfItemsInCarousel?: number,
privateGameTemplatesPeriodicity: number,
+ showOwnedGameTemplatesFirst?: boolean,
|}): {|
- carouselItems: Array,
- gridItems: Array,
+ carouselThumbnailItems: Array,
+ gridItemsCompletingCarousel: Array,
+ allGridItems: Array,
|} => {
if (!exampleShortHeaders || !privateGameTemplateListingDatas) {
- return { carouselItems: [], gridItems: [] };
+ return {
+ carouselThumbnailItems: [],
+ gridItemsCompletingCarousel: [],
+ allGridItems: [],
+ };
}
const exampleShortHeadersWithThumbnails = exampleShortHeaders.filter(
exampleShortHeader =>
!!exampleShortHeader.previewImageUrls &&
!!exampleShortHeader.previewImageUrls[0]
);
+ const exampleShortHeadersWithoutThumbnails = exampleShortHeaders.filter(
+ exampleShortHeader =>
+ !exampleShortHeader.previewImageUrls ||
+ !exampleShortHeader.previewImageUrls[0]
+ );
+
+ const carouselItems: Array<
+ PrivateGameTemplateListingData | ExampleShortHeader
+ > = [];
+ const itemsCompletingCarousel: Array<
+ PrivateGameTemplateListingData | ExampleShortHeader
+ > = [];
+ const allItems: Array<
+ PrivateGameTemplateListingData | ExampleShortHeader
+ > = [];
+
+ const maxIndex = Math.max(
+ exampleShortHeadersWithThumbnails.length,
+ privateGameTemplateListingDatas.length
+ );
- const carouselItems: Array = [];
- const gridItems = [];
+ let gameTemplateIndex = 0;
let exampleIndex = 0;
- let privateGameTemplateIndex = 0;
- for (
- let i = 0;
- i < numberOfItemsInGrid + numberOfItemsExclusivelyInCarousel;
- ++i
- ) {
+ for (let index = 0; index < maxIndex; index++) {
+ if (
+ gameTemplateIndex >= privateGameTemplateListingDatas.length &&
+ exampleIndex >= exampleShortHeadersWithThumbnails.length
+ ) {
+ break;
+ }
+ const privateGameTemplateListingData =
+ privateGameTemplateListingDatas[gameTemplateIndex];
+ const exampleShortHeader = exampleShortHeadersWithThumbnails[exampleIndex];
+
const shouldAddPrivateGameTemplate =
- i % privateGameTemplatesPeriodicity ===
- privateGameTemplatesPeriodicity - 1;
+ privateGameTemplatesPeriodicity &&
+ index >= 1 && // Do not add them too early.
+ index % privateGameTemplatesPeriodicity === 0;
- // At one point, we might run out of private game templates to display while
- // it is assumed that we have enough examples to display. This boolean is used
- // to know if we actually could add a private game template. This way, indices
- // can be increased accordingly.
- let privateGameTemplateActuallyAdded = false;
- if (i < numberOfItemsInCarousel) {
- // There should always be enough private game templates to sparsely fill the carousel.
- privateGameTemplateActuallyAdded = shouldAddPrivateGameTemplate;
- carouselItems.push(
- shouldAddPrivateGameTemplate
- ? formatGameTemplateListingDataForCarousel({
- i18n,
- onSelectGameTemplate: onSelectPrivateGameTemplateListingData,
- gameTemplateListingData:
- privateGameTemplateListingDatas[privateGameTemplateIndex],
- receivedGameTemplates: receivedGameTemplates,
- })
- : formatExampleShortHeaderForCarousel({
- exampleShortHeader:
- exampleShortHeadersWithThumbnails[exampleIndex],
- onSelectExample: onSelectExampleShortHeader,
- })
- );
+ // First handle example.
+ if (exampleShortHeader) {
+ // Handle carousel.
+ if (carouselItems.length < numberOfItemsInCarousel) {
+ carouselItems.push(exampleShortHeader);
+ }
+ // Handle grid.
+ allItems.push(exampleShortHeader);
+ if (carouselItems.length > numberOfItemsExclusivelyInCarousel) {
+ itemsCompletingCarousel.push(exampleShortHeader);
+ }
}
- if (i >= numberOfItemsExclusivelyInCarousel) {
- if (shouldAddPrivateGameTemplate) {
- const privateGameTemplateListingData =
- privateGameTemplateListingDatas[privateGameTemplateIndex];
- if (privateGameTemplateListingData) {
- const isTemplateOwned =
- !!receivedGameTemplates &&
- !!receivedGameTemplates.find(
- receivedGameTemplate =>
- receivedGameTemplate.id === privateGameTemplateListingData.id
- );
- gridItems.push(
- {
- onSelectPrivateGameTemplateListingData(
- privateGameTemplateListingData
- );
- }}
- owned={isTemplateOwned}
- key={privateGameTemplateListingData.id}
- />
- );
- privateGameTemplateActuallyAdded = true;
- }
+
+ // Then handle private game template if in the right periodicity.
+ if (shouldAddPrivateGameTemplate && privateGameTemplateListingData) {
+ // Handle carousel.
+ if (carouselItems.length < numberOfItemsInCarousel) {
+ carouselItems.push(privateGameTemplateListingData);
}
- if (!privateGameTemplateActuallyAdded) {
- const exampleShortHeader =
- exampleShortHeadersWithThumbnails[exampleIndex];
- gridItems.push(
- onSelectExampleShortHeader(exampleShortHeader)}
- key={exampleShortHeader.name}
- />
- );
+ // Handle grid.
+ if (privateGameTemplateListingData) {
+ allItems.push(privateGameTemplateListingData);
+ if (carouselItems.length > numberOfItemsExclusivelyInCarousel) {
+ itemsCompletingCarousel.push(privateGameTemplateListingData);
+ }
}
}
- if (privateGameTemplateActuallyAdded) privateGameTemplateIndex++;
- else exampleIndex++;
- if (
- exampleIndex >= exampleShortHeadersWithThumbnails.length &&
- privateGameTemplateIndex >= privateGameTemplateListingDatas.length
- ) {
- break;
+
+ // Increment the index for the next iteration.
+ if (shouldAddPrivateGameTemplate) {
+ gameTemplateIndex++;
}
+ exampleIndex++;
}
- return { carouselItems, gridItems };
-};
+ // Finally, add examples without thumbnails to the grid.
+ exampleShortHeadersWithoutThumbnails.forEach(exampleShortHeader => {
+ allItems.push(exampleShortHeader);
+ });
-export const getAllGameTemplatesAndExamplesFlaggedAsGameCount = ({
- privateGameTemplateListingDatas,
- exampleShortHeaders,
- columnsCount,
-}: {
- privateGameTemplateListingDatas: ?(PrivateGameTemplateListingData[]),
- exampleShortHeaders: ?(ExampleShortHeader[]),
- columnsCount: number,
-}) => {
- return (
- Math.floor(
- ((privateGameTemplateListingDatas
- ? privateGameTemplateListingDatas.length
- : 0) +
- (exampleShortHeaders
- ? exampleShortHeaders.filter(
- exampleShortHeader =>
- exampleShortHeader.tags.includes('game') ||
- exampleShortHeader.tags.includes('Starter')
- ).length
- : 0)) /
- columnsCount
- ) * columnsCount
+ const carouselThumbnailItems = carouselItems.map(item =>
+ formatItemForCarousel({
+ item,
+ onSelectGameTemplate: onSelectPrivateGameTemplateListingData,
+ onSelectExample: onSelectExampleShortHeader,
+ i18n,
+ receivedGameTemplates,
+ })
+ );
+
+ const gridItemsCompletingCarousel = itemsCompletingCarousel.map(item =>
+ formatItemForGrid({
+ item,
+ onSelectGameTemplate: onSelectPrivateGameTemplateListingData,
+ onSelectExample: onSelectExampleShortHeader,
+ i18n,
+ receivedGameTemplates,
+ })
);
+
+ const allGridItems = allItems
+ .sort((item1, item2) => {
+ if (showOwnedGameTemplatesFirst) {
+ const isItem1ATemplateOwned =
+ !!item1.sellerId && // Private game template
+ !!receivedGameTemplates &&
+ !!receivedGameTemplates.find(
+ receivedGameTemplate => receivedGameTemplate.id === item1.id
+ );
+ const isItem2ATemplateOwned =
+ !!item2.sellerId && // Private game template
+ !!receivedGameTemplates &&
+ !!receivedGameTemplates.find(
+ receivedGameTemplate => receivedGameTemplate.id === item2.id
+ );
+ if (isItem1ATemplateOwned && !isItem2ATemplateOwned) return -1;
+ if (!isItem1ATemplateOwned && isItem2ATemplateOwned) return 1;
+ }
+
+ return 0;
+ })
+ .map(item =>
+ formatItemForGrid({
+ item,
+ onSelectGameTemplate: onSelectPrivateGameTemplateListingData,
+ onSelectExample: onSelectExampleShortHeader,
+ i18n,
+ receivedGameTemplates,
+ })
+ );
+
+ return { carouselThumbnailItems, gridItemsCompletingCarousel, allGridItems };
};
diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/MainPage.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/MainPage.js
index 86b7b77abacd..33dabb045065 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/MainPage.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/MainPage.js
@@ -132,7 +132,6 @@ export const TutorialsRow = ({
);
type Props = {|
- onOpenExampleStore: () => void,
onTabChange: (tab: HomeTab) => void,
onSelectCategory: (?TutorialCategory) => void,
tutorials: Array,
@@ -140,7 +139,6 @@ type Props = {|
|};
const MainPage = ({
- onOpenExampleStore,
onTabChange,
onSelectCategory,
tutorials,
@@ -180,11 +178,6 @@ const MainPage = ({
action: () =>
Window.openExternalURL('https://wiki.gdevelop.io/gdevelop5/'),
},
- {
- title: Examples,
- description: Have a look at existing games from the inside,
- action: onOpenExampleStore,
- },
{
title: Forums,
description: Ask your questions to the community,
diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/index.js
index e6671d3c45ec..f54d742429eb 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/index.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/index.js
@@ -115,7 +115,6 @@ const styles = {
};
type Props = {|
- onOpenExampleStore: () => void,
onTabChange: (tab: HomeTab) => void,
selectInAppTutorial: (tutorialId: string) => void,
initialCategory: TutorialCategory | null,
@@ -123,7 +122,6 @@ type Props = {|
|};
const LearnSection = ({
- onOpenExampleStore,
onTabChange,
selectInAppTutorial,
initialCategory,
@@ -172,7 +170,6 @@ const LearnSection = ({
return !selectedCategory ? (
void,
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise,
- onOpenExampleStore: () => void,
onSelectExampleShortHeader: ExampleShortHeader => void,
- onPreviewPrivateGameTemplateListingData: (
- privateGameTemplateListingData: PrivateGameTemplateListingData
- ) => void,
+ onSelectPrivateGameTemplateListingData: PrivateGameTemplateListingData => void,
onOpenPrivateGameTemplateListingData: (
privateGameTemplateListingData: PrivateGameTemplateListingData
) => void,
@@ -175,9 +172,8 @@ export const HomePage = React.memo(
onChooseProject,
onOpenRecentFile,
onOpenNewProjectSetupDialog,
- onOpenExampleStore,
onSelectExampleShortHeader,
- onPreviewPrivateGameTemplateListingData,
+ onSelectPrivateGameTemplateListingData,
onOpenPrivateGameTemplateListingData,
onOpenProjectManager,
onOpenLanguageDialog,
@@ -547,10 +543,9 @@ export const HomePage = React.memo(
onOpenNewProjectSetupDialog={onOpenNewProjectSetupDialog}
onSelectExampleShortHeader={onSelectExampleShortHeader}
onSelectPrivateGameTemplateListingData={
- onPreviewPrivateGameTemplateListingData
+ onSelectPrivateGameTemplateListingData
}
onOpenRecentFile={onOpenRecentFile}
- onOpenExampleStore={onOpenExampleStore}
onManageGame={onManageGame}
canManageGame={canManageGame}
storageProviders={storageProviders}
@@ -560,7 +555,6 @@ export const HomePage = React.memo(
)}
{activeTab === 'learn' && (
Promise,
allowNetworkPreview: boolean,
onOpenHomePage: () => void,
- onCreateBlank: () => void,
+ onCreateProject: () => void,
onOpenProject: () => void,
onSaveProject: () => Promise,
onSaveProjectAs: () => void,
@@ -115,7 +115,7 @@ const useMainFrameCommands = (handlers: CommandHandlers) => {
});
useCommand('CREATE_NEW_PROJECT', true, {
- handler: handlers.onCreateBlank,
+ handler: handlers.onCreateProject,
});
useCommand('OPEN_PROJECT', true, {
diff --git a/newIDE/app/src/MainFrame/MainMenu.js b/newIDE/app/src/MainFrame/MainMenu.js
index e1e67c28e0dd..ae7387c730de 100644
--- a/newIDE/app/src/MainFrame/MainMenu.js
+++ b/newIDE/app/src/MainFrame/MainMenu.js
@@ -37,8 +37,7 @@ export type MainMenuCallbacks = {|
onCloseApp: () => void,
onExportProject: () => void,
onInviteCollaborators: () => void,
- onCreateProject: (open?: boolean) => void,
- onCreateBlank: () => void,
+ onCreateProject: () => void,
onOpenProjectManager: (open?: boolean) => void,
onOpenHomePage: () => void,
onOpenDebugger: () => void,
@@ -63,7 +62,7 @@ export type MainMenuEvent =
| 'main-menu-close-app'
| 'main-menu-export'
| 'main-menu-invite-collaborators'
- | 'main-menu-create-template'
+ | 'main-menu-create-project'
| 'main-menu-create-blank'
| 'main-menu-open-project-manager'
| 'main-menu-open-home-page'
@@ -88,8 +87,7 @@ const getMainMenuEventCallback = (
'main-menu-close-app': callbacks.onCloseApp,
'main-menu-export': callbacks.onExportProject,
'main-menu-invite-collaborators': callbacks.onInviteCollaborators,
- 'main-menu-create-template': callbacks.onCreateProject,
- 'main-menu-create-blank': callbacks.onCreateBlank,
+ 'main-menu-create-project': callbacks.onCreateProject,
'main-menu-open-project-manager': callbacks.onOpenProjectManager,
'main-menu-open-home-page': callbacks.onOpenHomePage,
'main-menu-open-debugger': callbacks.onOpenDebugger,
@@ -115,20 +113,9 @@ export const buildMainMenuDeclarativeTemplate = ({
label: i18n._(t`File`),
submenu: [
{
- label: i18n._(t`Create`),
- submenu: [
- {
- label: i18n._(t`New empty project...`),
- accelerator: getElectronAccelerator(
- shortcutMap['CREATE_NEW_PROJECT']
- ),
- onClickSendEvent: 'main-menu-create-blank',
- },
- {
- label: i18n._(t`New project from template...`),
- onClickSendEvent: 'main-menu-create-template',
- },
- ],
+ label: i18n._(t`Create a game`),
+ accelerator: getElectronAccelerator(shortcutMap['CREATE_NEW_PROJECT']),
+ onClickSendEvent: 'main-menu-create-project',
},
{ type: 'separator' },
{
diff --git a/newIDE/app/src/MainFrame/UseExampleOrGameTemplateDialogs.js b/newIDE/app/src/MainFrame/UseExampleOrGameTemplateDialogs.js
deleted file mode 100644
index a8ee9f45814b..000000000000
--- a/newIDE/app/src/MainFrame/UseExampleOrGameTemplateDialogs.js
+++ /dev/null
@@ -1,208 +0,0 @@
-// @flow
-
-import * as React from 'react';
-import {
- listAllExamples,
- type ExampleShortHeader,
-} from '../Utils/GDevelopServices/Example';
-import type { PrivateGameTemplateListingData } from '../Utils/GDevelopServices/Shop';
-import ExampleStoreDialog from '../AssetStore/ExampleStore/ExampleStoreDialog';
-import { ExampleDialog } from '../AssetStore/ExampleStore/ExampleDialog';
-import PrivateGameTemplateInformationDialog from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog';
-import { PrivateGameTemplateStoreContext } from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
-import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
-import LoaderModal from '../UI/LoaderModal';
-
-type Props = {|
- isProjectOpening: boolean,
- onOpenNewProjectSetupDialog: () => void,
-|};
-
-const useExampleOrGameTemplateDialogs = ({
- isProjectOpening,
- onOpenNewProjectSetupDialog,
-}: Props) => {
- const [isFetchingExample, setIsFetchingExample] = React.useState(false);
- const [
- exampleStoreDialogOpen,
- setExampleStoreDialogOpen,
- ] = React.useState(false);
- const [
- selectedExampleShortHeader,
- setSelectedExampleShortHeader,
- ] = React.useState(null);
- const [
- selectedPrivateGameTemplate,
- setSelectedPrivateGameTemplate,
- ] = React.useState{|
- privateGameTemplateListingData: PrivateGameTemplateListingData,
- /**
- * At the moment, only MainFrame uses this hook and handles the selected private
- * game template in both build and store sections in this single variable.
- * But the store section handles the preview of the game template content (unlike
- * the build section that needs this hook to open the information dialog) so we
- * let the possibility to select a game template without opening the dialog
- * (This selected game template is then used by the NewProjectSetupDialog to use).
- */
- openDialog: boolean,
- |}>(null);
-
- const { receivedGameTemplates } = React.useContext(AuthenticatedUserContext);
- const { privateGameTemplateListingDatas } = React.useContext(
- PrivateGameTemplateStoreContext
- );
-
- const closeExampleStoreDialog = React.useCallback(
- ({
- deselectExampleAndGameTemplate,
- }: {|
- deselectExampleAndGameTemplate: boolean,
- |}) => {
- setExampleStoreDialogOpen(false);
- if (deselectExampleAndGameTemplate) {
- setSelectedExampleShortHeader(null);
- setSelectedPrivateGameTemplate(null);
- }
- },
- [setExampleStoreDialogOpen]
- );
- const openExampleStoreDialog = React.useCallback(
- () => {
- setExampleStoreDialogOpen(true);
- },
- [setExampleStoreDialogOpen]
- );
-
- const privateGameTemplateListingDatasFromSameCreator: ?Array = React.useMemo(
- () => {
- if (
- !selectedPrivateGameTemplate ||
- !privateGameTemplateListingDatas ||
- !receivedGameTemplates
- )
- return null;
-
- const receivedGameTemplateIds = receivedGameTemplates.map(
- template => template.id
- );
-
- return privateGameTemplateListingDatas
- .filter(
- template =>
- template.sellerId ===
- selectedPrivateGameTemplate.privateGameTemplateListingData
- .sellerId &&
- !receivedGameTemplateIds.includes(template.sellerId)
- )
- .sort((template1, template2) =>
- template1.name.localeCompare(template2.name)
- );
- },
- [
- selectedPrivateGameTemplate,
- privateGameTemplateListingDatas,
- receivedGameTemplates,
- ]
- );
-
- const fetchAndOpenNewProjectSetupDialogForExample = React.useCallback(
- async (exampleSlug: string) => {
- try {
- setIsFetchingExample(true);
- const fetchedAllExamples = await listAllExamples();
- const exampleShortHeader = fetchedAllExamples.exampleShortHeaders.find(
- exampleShortHeader => exampleShortHeader.slug === exampleSlug
- );
- if (!exampleShortHeader) {
- throw new Error(
- `Unable to find the example with slug "${exampleSlug}"`
- );
- }
-
- setSelectedExampleShortHeader(exampleShortHeader);
- } catch (error) {
- console.error('Error caught while opening example:', error);
- return;
- } finally {
- setIsFetchingExample(false);
- }
-
- onOpenNewProjectSetupDialog();
- },
- [setSelectedExampleShortHeader, onOpenNewProjectSetupDialog]
- );
-
- const renderExampleOrGameTemplateDialogs = () => {
- return (
- <>
- {isFetchingExample && }
- {exampleStoreDialogOpen && (
-
- closeExampleStoreDialog({ deselectExampleAndGameTemplate: true })
- }
- isProjectOpening={isProjectOpening}
- selectedExampleShortHeader={selectedExampleShortHeader}
- selectedPrivateGameTemplateListingData={
- selectedPrivateGameTemplate
- ? selectedPrivateGameTemplate.privateGameTemplateListingData
- : null
- }
- onSelectExampleShortHeader={setSelectedExampleShortHeader}
- onSelectPrivateGameTemplateListingData={privateGameTemplateListingData =>
- privateGameTemplateListingData
- ? setSelectedPrivateGameTemplate({
- privateGameTemplateListingData,
- openDialog: true,
- })
- : setSelectedPrivateGameTemplate(null)
- }
- onOpenNewProjectSetupDialog={onOpenNewProjectSetupDialog}
- />
- )}
- {!!selectedExampleShortHeader && (
- setSelectedExampleShortHeader(null)}
- />
- )}
- {!!selectedPrivateGameTemplate &&
- selectedPrivateGameTemplate.openDialog && (
-
- setSelectedPrivateGameTemplate({
- privateGameTemplateListingData,
- openDialog: true,
- })
- }
- onClose={() => setSelectedPrivateGameTemplate(null)}
- privateGameTemplateListingDatasFromSameCreator={
- privateGameTemplateListingDatasFromSameCreator
- }
- />
- )}
- >
- );
- };
- return {
- selectedExampleShortHeader,
- selectedPrivateGameTemplateListingData: selectedPrivateGameTemplate
- ? selectedPrivateGameTemplate.privateGameTemplateListingData
- : null,
- closeExampleStoreDialog,
- openExampleStoreDialog,
- onSelectExampleShortHeader: setSelectedExampleShortHeader,
- onSelectPrivateGameTemplate: setSelectedPrivateGameTemplate,
- renderExampleOrGameTemplateDialogs,
- fetchAndOpenNewProjectSetupDialogForExample,
- };
-};
-
-export default useExampleOrGameTemplateDialogs;
diff --git a/newIDE/app/src/MainFrame/UseNewProjectDialog.js b/newIDE/app/src/MainFrame/UseNewProjectDialog.js
new file mode 100644
index 000000000000..b6a535ca5137
--- /dev/null
+++ b/newIDE/app/src/MainFrame/UseNewProjectDialog.js
@@ -0,0 +1,246 @@
+// @flow
+
+import * as React from 'react';
+import { type I18n as I18nType } from '@lingui/core';
+import {
+ listAllExamples,
+ type ExampleShortHeader,
+} from '../Utils/GDevelopServices/Example';
+import type { PrivateGameTemplateListingData } from '../Utils/GDevelopServices/Shop';
+import { PrivateGameTemplateStoreContext } from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
+import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
+import LoaderModal from '../UI/LoaderModal';
+import NewProjectSetupDialog, {
+ type NewProjectSetup,
+} from '../ProjectCreation/NewProjectSetupDialog';
+import { type StorageProvider } from '../ProjectsStorage';
+
+type Props = {|
+ isProjectOpening: boolean,
+ newProjectSetupDialogOpen: boolean,
+ setNewProjectSetupDialogOpen: boolean => void,
+ createEmptyProject: NewProjectSetup => Promise,
+ createProjectFromExample: (
+ exampleShortHeader: ExampleShortHeader,
+ newProjectSetup: NewProjectSetup,
+ i18n: I18nType
+ ) => Promise,
+ createProjectFromPrivateGameTemplate: (
+ privateGameTemplateListingData: PrivateGameTemplateListingData,
+ newProjectSetup: NewProjectSetup
+ ) => Promise,
+ createProjectFromAIGeneration: (
+ projectFileUrl: string,
+ newProjectSetup: NewProjectSetup
+ ) => Promise,
+ storageProviders: Array,
+|};
+
+const useExampleOrGameTemplateDialogs = ({
+ isProjectOpening,
+ newProjectSetupDialogOpen,
+ setNewProjectSetupDialogOpen,
+ createEmptyProject,
+ createProjectFromExample,
+ createProjectFromPrivateGameTemplate,
+ createProjectFromAIGeneration,
+ storageProviders,
+}: Props) => {
+ const [isFetchingExample, setIsFetchingExample] = React.useState(false);
+ const [
+ selectedPrivateGameTemplateListingData,
+ setSelectedPrivateGameTemplateListingData,
+ ] = React.useState(null);
+ const [
+ selectedExampleShortHeader,
+ setSelectedExampleShortHeader,
+ ] = React.useState(null);
+ const [preventBackHome, setPreventBackHome] = React.useState(true);
+ const [preventBackDetails, setPreventBackDetails] = React.useState(false);
+
+ const { receivedGameTemplates } = React.useContext(AuthenticatedUserContext);
+ const { privateGameTemplateListingDatas } = React.useContext(
+ PrivateGameTemplateStoreContext
+ );
+
+ const closeNewProjectDialog = React.useCallback(
+ () => {
+ setPreventBackHome(false);
+ setPreventBackDetails(false);
+ setSelectedExampleShortHeader(null);
+ setSelectedPrivateGameTemplateListingData(null);
+ setNewProjectSetupDialogOpen(false);
+ },
+ [setNewProjectSetupDialogOpen]
+ );
+ const openNewProjectDialog = React.useCallback(
+ () => {
+ setPreventBackHome(false);
+ setPreventBackDetails(false);
+ setSelectedExampleShortHeader(null);
+ setSelectedPrivateGameTemplateListingData(null);
+ setNewProjectSetupDialogOpen(true);
+ },
+ [setNewProjectSetupDialogOpen]
+ );
+
+ const privateGameTemplateListingDatasFromSameCreator: ?Array = React.useMemo(
+ () => {
+ if (
+ !selectedPrivateGameTemplateListingData ||
+ !privateGameTemplateListingDatas ||
+ !receivedGameTemplates
+ )
+ return null;
+
+ const receivedGameTemplateIds = receivedGameTemplates.map(
+ template => template.id
+ );
+
+ return privateGameTemplateListingDatas
+ .filter(
+ template =>
+ template.sellerId ===
+ selectedPrivateGameTemplateListingData.sellerId &&
+ !receivedGameTemplateIds.includes(template.sellerId)
+ )
+ .sort((template1, template2) =>
+ template1.name.localeCompare(template2.name)
+ );
+ },
+ [
+ selectedPrivateGameTemplateListingData,
+ privateGameTemplateListingDatas,
+ receivedGameTemplates,
+ ]
+ );
+
+ const onSelectPrivateGameTemplateListingData = React.useCallback(
+ ({
+ privateGameTemplateListingData,
+ preventBackHome,
+ preventBackDetails,
+ }: {|
+ privateGameTemplateListingData: ?PrivateGameTemplateListingData,
+ preventBackHome?: boolean,
+ preventBackDetails?: boolean,
+ |}) => {
+ setSelectedPrivateGameTemplateListingData(privateGameTemplateListingData);
+ setPreventBackHome(!!preventBackHome);
+ setPreventBackDetails(!!preventBackDetails);
+ if (privateGameTemplateListingData) {
+ setNewProjectSetupDialogOpen(true);
+ }
+ },
+ [setSelectedPrivateGameTemplateListingData, setNewProjectSetupDialogOpen]
+ );
+
+ const onSelectExampleShortHeader = React.useCallback(
+ ({
+ exampleShortHeader,
+ preventBackHome,
+ preventBackDetails,
+ }: {|
+ exampleShortHeader: ?ExampleShortHeader,
+ preventBackHome?: boolean,
+ preventBackDetails?: boolean,
+ |}) => {
+ setSelectedExampleShortHeader(exampleShortHeader);
+ setPreventBackHome(!!preventBackHome);
+ setPreventBackDetails(!!preventBackDetails);
+ if (exampleShortHeader) {
+ setNewProjectSetupDialogOpen(true);
+ }
+ },
+ [setSelectedExampleShortHeader, setNewProjectSetupDialogOpen]
+ );
+
+ const fetchAndOpenNewProjectSetupDialogForExample = React.useCallback(
+ async (exampleSlug: string) => {
+ try {
+ setIsFetchingExample(true);
+ const fetchedAllExamples = await listAllExamples();
+ const exampleShortHeader = fetchedAllExamples.exampleShortHeaders.find(
+ exampleShortHeader => exampleShortHeader.slug === exampleSlug
+ );
+ if (!exampleShortHeader) {
+ throw new Error(
+ `Unable to find the example with slug "${exampleSlug}"`
+ );
+ }
+
+ onSelectExampleShortHeader({
+ exampleShortHeader,
+ preventBackHome: false,
+ });
+ } catch (error) {
+ console.error('Error caught while opening example:', error);
+ return;
+ } finally {
+ setIsFetchingExample(false);
+ }
+ },
+ [onSelectExampleShortHeader]
+ );
+
+ const renderNewProjectDialog = () => {
+ return (
+ <>
+ {isFetchingExample && }
+ {newProjectSetupDialogOpen && (
+ {
+ const projectFileUrl = generatedProject.fileUrl;
+ if (!projectFileUrl) return;
+ await createProjectFromAIGeneration(projectFileUrl, projectSetup);
+ }}
+ storageProviders={storageProviders}
+ selectedExampleShortHeader={selectedExampleShortHeader}
+ onSelectExampleShortHeader={exampleShortHeader =>
+ onSelectExampleShortHeader({
+ exampleShortHeader,
+ preventBackHome: false,
+ })
+ }
+ selectedPrivateGameTemplateListingData={
+ selectedPrivateGameTemplateListingData
+ }
+ onSelectPrivateGameTemplateListingData={privateGameTemplateListingData =>
+ onSelectPrivateGameTemplateListingData({
+ privateGameTemplateListingData,
+ preventBackHome: false,
+ })
+ }
+ privateGameTemplateListingDatasFromSameCreator={
+ privateGameTemplateListingDatasFromSameCreator
+ }
+ preventBackHome={preventBackHome}
+ preventBackDetails={preventBackDetails}
+ />
+ )}
+ >
+ );
+ };
+ return {
+ selectedExampleShortHeader: selectedExampleShortHeader,
+ selectedPrivateGameTemplateListingData: selectedPrivateGameTemplateListingData,
+ closeNewProjectDialog,
+ openNewProjectDialog,
+ onSelectExampleShortHeader,
+ onSelectPrivateGameTemplateListingData,
+ renderNewProjectDialog,
+ fetchAndOpenNewProjectSetupDialogForExample,
+ };
+};
+
+export default useExampleOrGameTemplateDialogs;
diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js
index b7f4c172d495..1f6d314a35d4 100644
--- a/newIDE/app/src/MainFrame/index.js
+++ b/newIDE/app/src/MainFrame/index.js
@@ -137,7 +137,7 @@ import HotReloadLogsDialog from '../HotReload/HotReloadLogsDialog';
import { useDiscordRichPresence } from '../Utils/UpdateDiscordRichPresence';
import { delay } from '../Utils/Delay';
import { type ExtensionShortHeader } from '../Utils/GDevelopServices/Extension';
-import useExampleOrGameTemplateDialogs from './UseExampleOrGameTemplateDialogs';
+import useNewProjectDialog from './UseNewProjectDialog';
import { findAndLogProjectPreviewErrors } from '../Utils/ProjectErrorsChecker';
import { renameResourcesInProject } from '../ResourcesList/ResourceUtils';
import { NewResourceDialog } from '../ResourcesList/NewResourceDialog';
@@ -156,7 +156,6 @@ import {
} from '../Utils/Analytics/EventSender';
import { useLeaderboardReplacer } from '../Leaderboard/UseLeaderboardReplacer';
import useAlertDialog from '../UI/Alert/useAlertDialog';
-import NewProjectSetupDialog from '../ProjectCreation/NewProjectSetupDialog';
import {
useResourceMover,
type ResourceMover,
@@ -520,19 +519,6 @@ const MainFrame = (props: Props) => {
? currentProject.isFolderProject()
: false,
});
- const {
- selectedExampleShortHeader,
- selectedPrivateGameTemplateListingData,
- onSelectExampleShortHeader,
- onSelectPrivateGameTemplate,
- renderExampleOrGameTemplateDialogs,
- closeExampleStoreDialog,
- openExampleStoreDialog,
- fetchAndOpenNewProjectSetupDialogForExample,
- } = useExampleOrGameTemplateDialogs({
- isProjectOpening,
- onOpenNewProjectSetupDialog: () => setNewProjectSetupDialogOpen(true),
- });
const gamesList = useGamesList();
@@ -911,7 +897,6 @@ const MainFrame = (props: Props) => {
currentProject: project,
currentFileMetadata: fileMetadata,
}));
- closeExampleStoreDialog({ deselectExampleAndGameTemplate: false });
// Load all the EventsFunctionsExtension when the game is loaded. If they are modified,
// their editor will take care of reloading them.
@@ -952,7 +937,6 @@ const MainFrame = (props: Props) => {
setState,
closeProject,
preferences,
- closeExampleStoreDialog,
eventsFunctionsExtensionsState,
getStorageProvider,
getStorageProviderOperations,
@@ -1143,7 +1127,6 @@ const MainFrame = (props: Props) => {
createProjectFromPrivateGameTemplate,
createProjectFromInAppTutorial,
createProjectFromTutorial,
- createProjectWithLogin,
createProjectFromAIGeneration,
} = useCreateProject({
beforeCreatingProject: () => {
@@ -1157,7 +1140,6 @@ const MainFrame = (props: Props) => {
options,
}) => {
setNewProjectSetupDialogOpen(false);
- closeExampleStoreDialog({ deselectExampleAndGameTemplate: true });
if (options.openQuickCustomizationDialog) {
setQuickCustomizationDialogOpenedFromGameId(oldProjectId);
} else {
@@ -1198,6 +1180,23 @@ const MainFrame = (props: Props) => {
onGameRegistered: gamesList.fetchGames,
});
+ const {
+ onSelectExampleShortHeader,
+ onSelectPrivateGameTemplateListingData,
+ renderNewProjectDialog,
+ fetchAndOpenNewProjectSetupDialogForExample,
+ openNewProjectDialog,
+ } = useNewProjectDialog({
+ isProjectOpening,
+ newProjectSetupDialogOpen,
+ setNewProjectSetupDialogOpen,
+ createEmptyProject,
+ createProjectFromExample,
+ createProjectFromPrivateGameTemplate,
+ createProjectFromAIGeneration,
+ storageProviders: props.storageProviders,
+ });
+
const closeApp = React.useCallback((): void => {
return Window.quit();
}, []);
@@ -3393,7 +3392,7 @@ const MainFrame = (props: Props) => {
onLaunchNetworkPreview: launchNetworkPreview,
onLaunchPreviewWithDiagnosticReport: launchPreviewWithDiagnosticReport,
onOpenHomePage: openHomePage,
- onCreateBlank: () => setNewProjectSetupDialogOpen(true),
+ onCreateProject: () => setNewProjectSetupDialogOpen(true),
onOpenProject: () => openOpenFromStorageProviderDialog(),
onSaveProject: saveProject,
onSaveProjectAs: saveProjectAs,
@@ -3454,8 +3453,7 @@ const MainFrame = (props: Props) => {
onCloseApp: closeApp,
onExportProject: () => openShareDialog('publish'),
onInviteCollaborators: () => openShareDialog('invite'),
- onCreateProject: openExampleStoreDialog,
- onCreateBlank: () => setNewProjectSetupDialogOpen(true),
+ onCreateProject: () => setNewProjectSetupDialogOpen(true),
onOpenProjectManager: () => openProjectManager(true),
onOpenHomePage: openHomePage,
onOpenDebugger: openDebugger,
@@ -3663,27 +3661,30 @@ const MainFrame = (props: Props) => {
).length,
onChooseProject: () => openOpenFromStorageProviderDialog(),
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
- onOpenNewProjectSetupDialog: () => {
- setNewProjectSetupDialogOpen(true);
- },
+ onOpenNewProjectSetupDialog: openNewProjectDialog,
onOpenProjectManager: () => openProjectManager(true),
askToCloseProject,
closeProject,
- onOpenExampleStore: openExampleStoreDialog,
- onSelectExampleShortHeader: onSelectExampleShortHeader,
- onCreateProjectFromExample: createProjectFromExample,
- onPreviewPrivateGameTemplateListingData: privateGameTemplateListingData =>
- onSelectPrivateGameTemplate({
+ onSelectExampleShortHeader: exampleShortHeader => {
+ onSelectExampleShortHeader({
+ exampleShortHeader,
+ preventBackHome: true,
+ });
+ },
+ onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => {
+ onSelectPrivateGameTemplateListingData({
privateGameTemplateListingData,
- openDialog: true,
- }),
+ preventBackHome: true,
+ });
+ },
onOpenPrivateGameTemplateListingData: privateGameTemplateListingData => {
- onSelectPrivateGameTemplate({
+ onSelectPrivateGameTemplateListingData({
privateGameTemplateListingData,
- openDialog: false,
+ preventBackHome: true,
+ preventBackDetails: true,
});
- setNewProjectSetupDialogOpen(true);
},
+ onCreateProjectFromExample: createProjectFromExample,
onOpenProfile: () => openProfileDialog(true),
onOpenLanguageDialog: () => openLanguageDialog(true),
onOpenPreferences: () => openPreferencesDialog(true),
@@ -3813,31 +3814,7 @@ const MainFrame = (props: Props) => {
}}
/>
)}
- {// Render example or game template dialogs before NewProjectSetupDialog to make sure it's always displayed on top
- renderExampleOrGameTemplateDialogs()}
- {newProjectSetupDialogOpen && (
- setNewProjectSetupDialogOpen(false)}
- onCreateEmptyProject={createEmptyProject}
- onCreateFromExample={createProjectFromExample}
- onCreateProjectFromPrivateGameTemplate={
- createProjectFromPrivateGameTemplate
- }
- onCreateWithLogin={createProjectWithLogin}
- onCreateFromAIGeneration={async (generatedProject, projectSetup) => {
- const projectFileUrl = generatedProject.fileUrl;
- if (!projectFileUrl) return;
- await createProjectFromAIGeneration(projectFileUrl, projectSetup);
- }}
- storageProviders={props.storageProviders}
- selectedExampleShortHeader={selectedExampleShortHeader}
- selectedPrivateGameTemplateListingData={
- selectedPrivateGameTemplateListingData
- }
- />
- )}
+ {renderNewProjectDialog()}
{cloudProjectFileMetadataToRecover && (
Promise,
+ isProjectOpening?: boolean,
+ isGeneratingProject: boolean,
+ storageProvider: StorageProvider,
+ saveAsLocation: ?SaveAsLocation,
+ generatingProjectId: ?string,
+ onGenerationClosed: () => void,
+ generationPrompt: string,
+ onGenerationPromptChange: (generationPrompt: string) => void,
+|};
+
+const AIPromptField = ({
+ onCreateFromAIGeneration,
+ isProjectOpening,
+ isGeneratingProject,
+ storageProvider,
+ saveAsLocation,
+ generatingProjectId,
+ onGenerationClosed,
+ generationPrompt,
+ onGenerationPromptChange,
+}: Props): React.Node => {
+ const { authenticated, limits } = React.useContext(AuthenticatedUserContext);
+ const isOnline = useOnlineStatus();
+ const generationCurrentUsage = limits
+ ? limits.quotas['ai-project-generation']
+ : null;
+ const canGenerateProjectFromPrompt =
+ generationCurrentUsage && !generationCurrentUsage.limitReached;
+ const disabled = isProjectOpening || isGeneratingProject;
+
+ return (
+ <>
+
+
+
+ onGenerationPromptChange(text)}
+ floatingLabelText={AI prompt}
+ floatingLabelFixed
+ translatableHintText={
+ !authenticated || !isOnline
+ ? t`Log in to enter a prompt`
+ : t`Type a prompt or generate one`
+ }
+ endAdornment={
+ onGenerationPromptChange(generatePrompt())}
+ tooltip={t`Generate random prompt`}
+ disabled={
+ disabled ||
+ !authenticated ||
+ !isOnline ||
+ !canGenerateProjectFromPrompt
+ }
+ >
+
+
+ }
+ />
+
+ {authenticated && !canGenerateProjectFromPrompt && (
+
+
+
+
+
+ You've used all your daily pre-made AI scenes! Generate as
+ many as you want with a subscription.
+
+
+
+
+
+ )}
+
+ {isGeneratingProject && generatingProjectId && (
+
+ )}
+ >
+ );
+};
+
+export default AIPromptField;
diff --git a/newIDE/app/src/ProjectCreation/CreateProject.js b/newIDE/app/src/ProjectCreation/CreateProject.js
index 7fc0de8b75af..9fe8a6456db6 100644
--- a/newIDE/app/src/ProjectCreation/CreateProject.js
+++ b/newIDE/app/src/ProjectCreation/CreateProject.js
@@ -46,17 +46,6 @@ export const addDefaultLightToAllLayers = (layout: gdLayout): void => {
}
};
-const addDefaultLightToProject = (project: gdProject): void => {
- for (
- let layoutIndex = 0;
- layoutIndex < project.getLayoutsCount();
- layoutIndex++
- ) {
- const layout = project.getLayoutAt(layoutIndex);
- addDefaultLightToAllLayers(layout);
- }
-};
-
export const createNewEmptyProject = (): NewProjectSource => {
const project: gdProject = gd.ProjectHelper.createNewGDJSProject();
@@ -68,20 +57,6 @@ export const createNewEmptyProject = (): NewProjectSource => {
};
};
-export const createNewProjectWithDefaultLogin = (): NewProjectSource => {
- const url =
- 'https://resources.gdevelop-app.com/examples-database/login-template.json';
- sendNewGameCreated({
- exampleUrl: url,
- exampleSlug: 'login-template',
- });
- const newProjectSource = getNewProjectSourceFromUrl(url);
- if (newProjectSource.project) {
- addDefaultLightToProject(newProjectSource.project);
- }
- return newProjectSource;
-};
-
export const createNewProjectFromAIGeneratedProject = (
generatedProjectUrl: string
): NewProjectSource => {
diff --git a/newIDE/app/src/ProjectCreation/EmptyAndBaseProjects.js b/newIDE/app/src/ProjectCreation/EmptyAndBaseProjects.js
new file mode 100644
index 000000000000..53be7b5e0201
--- /dev/null
+++ b/newIDE/app/src/ProjectCreation/EmptyAndBaseProjects.js
@@ -0,0 +1,137 @@
+// @flow
+import * as React from 'react';
+import { I18n } from '@lingui/react';
+import { type StorageProvider, type SaveAsLocation } from '../ProjectsStorage';
+import Add from '../UI/CustomSvgIcons/Add';
+import Text from '../UI/Text';
+import { Trans } from '@lingui/macro';
+import { Column, Line } from '../UI/Grid';
+import { type GDevelopTheme } from '../UI/Theme';
+import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
+import { ExampleTile } from '../AssetStore/ShopTiles';
+import { ExampleStoreContext } from '../AssetStore/ExampleStore/ExampleStoreContext';
+import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
+import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
+import { GridList, GridListTile } from '@material-ui/core';
+import { shouldValidate } from '../UI/KeyboardShortcuts/InteractionKeys';
+import classes from './EmptyAndBaseProjects.module.css';
+import classNames from 'classnames';
+import { getItemsColumns } from './NewProjectSetupDialog';
+
+const getStyles = (theme: GDevelopTheme) => ({
+ grid: {
+ margin: 0,
+ // Remove the scroll capability of the grid, the scroll view handles it.
+ overflow: 'unset',
+ },
+ cellSpacing: 2,
+});
+
+type EmptyProjectTileProps = {|
+ onSelectEmptyProject: () => void,
+ disabled?: boolean,
+ /** Props needed so that GridList component can adjust tile size */
+ style?: any,
+|};
+
+const EmptyProjectTile = ({
+ onSelectEmptyProject,
+ disabled,
+ style,
+}: EmptyProjectTileProps) => {
+ return (
+
+
+
): void => {
+ if (shouldValidate(event) && !disabled) {
+ onSelectEmptyProject();
+ }
+ }}
+ >
+
+
+
+ Empty project
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+type Props = {|
+ onSelectEmptyProject: () => void,
+ onSelectExampleShortHeader: (exampleShortHeader: ExampleShortHeader) => void,
+ disabled?: boolean,
+ storageProvider: StorageProvider,
+ saveAsLocation: ?SaveAsLocation,
+|};
+
+const EmptyAndBaseProjects = ({
+ onSelectExampleShortHeader,
+ onSelectEmptyProject,
+ disabled,
+ storageProvider,
+ saveAsLocation,
+}: Props): React.Node => {
+ const gdevelopTheme = React.useContext(GDevelopThemeContext);
+ const styles = getStyles(gdevelopTheme);
+ const { exampleShortHeaders } = React.useContext(ExampleStoreContext);
+ const baseExampleShortHeaders = React.useMemo(
+ () => {
+ // todo proper filter on base tag
+ return exampleShortHeaders ? exampleShortHeaders.slice(0, 3) : [];
+ },
+ [exampleShortHeaders]
+ );
+ const { windowSize, isLandscape } = useResponsiveWindowSize();
+ // To avoid layout shift while the items are loading.
+ const columnsCount = baseExampleShortHeaders
+ ? getItemsColumns(windowSize, isLandscape)
+ : 1;
+
+ return (
+
+ {({ i18n }) => (
+
+
+ {baseExampleShortHeaders.map(exampleShortHeader => (
+ onSelectExampleShortHeader(exampleShortHeader)}
+ key={exampleShortHeader.name}
+ disabled={disabled}
+ />
+ ))}
+
+ )}
+
+ );
+};
+
+export default EmptyAndBaseProjects;
diff --git a/newIDE/app/src/ProjectCreation/EmptyAndBaseProjects.module.css b/newIDE/app/src/ProjectCreation/EmptyAndBaseProjects.module.css
new file mode 100644
index 000000000000..8f9f71505348
--- /dev/null
+++ b/newIDE/app/src/ProjectCreation/EmptyAndBaseProjects.module.css
@@ -0,0 +1,23 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.emptyProject {
+ margin: 4px;
+ border: 1px solid var(--theme-text-disabled-color);
+ border-radius: 8px;
+ height: 100%;
+ padding: 4px;
+ display: flex;
+}
+
+.emptyProject:hover {
+ background-color: rgba(250, 250, 250, 0.08)
+}
+
+.emptyProject:focus {
+ background-color: rgba(250, 250, 250, 0.08)
+}
+
diff --git a/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js b/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js
index eb403bdfc951..98e3c71a36d3 100644
--- a/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js
+++ b/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js
@@ -5,19 +5,17 @@ import { type StorageProvider, type SaveAsLocation } from '../ProjectsStorage';
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import TextField from '../UI/TextField';
-import AuthenticatedUserContext, {
- type AuthenticatedUser,
-} from '../Profile/AuthenticatedUserContext';
+import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import generateName from '../Utils/ProjectNameGenerator';
import IconButton from '../UI/IconButton';
-import { ColumnStackLayout, LineStackLayout } from '../UI/Layout';
+import { ColumnStackLayout } from '../UI/Layout';
import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders';
import { findEmptyPathInWorkspaceFolder } from '../ProjectsStorage/LocalFileStorageProvider/LocalPathFinder';
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 { Column, Line, Spacer } from '../UI/Grid';
import {
checkIfHasTooManyCloudProjects,
MaxProjectCountAlertMessage,
@@ -26,9 +24,7 @@ import { SubscriptionSuggestionContext } from '../Profile/Subscription/Subscript
import optionalRequire from '../Utils/OptionalRequire';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import Checkbox from '../UI/Checkbox';
-import { MarkdownText } from '../UI/MarkdownText';
import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext';
-import { useOnlineStatus } from '../Utils/OnlineStatus';
import Refresh from '../UI/CustomSvgIcons/Refresh';
import {
createGeneratedProject,
@@ -40,24 +36,45 @@ import ResolutionOptions, {
defaultCustomWidth,
defaultCustomHeight,
} from './ResolutionOptions';
-import Text from '../UI/Text';
-import generatePrompt from '../Utils/ProjectPromptGenerator';
-import ProjectGeneratingDialog from './ProjectGeneratingDialog';
import useAlertDialog from '../UI/Alert/useAlertDialog';
-import RobotIcon from './RobotIcon';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { type I18n as I18nType } from '@lingui/core';
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 { CLOUD_PROJECT_NAME_MAX_LENGTH } from '../Utils/GDevelopServices/Project';
-import { Accordion, AccordionBody, AccordionHeader } from '../UI/Accordion';
+import AIPromptField from './AIPromptField';
+import EmptyAndBaseProjects from './EmptyAndBaseProjects';
+import TextButton from '../UI/TextButton';
+import ChevronArrowLeft from '../UI/CustomSvgIcons/ChevronArrowLeft';
+import ExampleInformationPage from '../AssetStore/ExampleStore/ExampleInformationPage';
+import PrivateGameTemplateInformationPage from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage';
+import ExampleStore from '../AssetStore/ExampleStore';
+import Text from '../UI/Text';
+import {
+ useResponsiveWindowSize,
+ type WindowSizeType,
+} from '../UI/Responsive/ResponsiveWindowMeasurer';
+import { PrivateGameTemplateStoreContext } from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
+import { getUserProductPurchaseUsageType } from '../AssetStore/ProductPageHelper';
const electron = optionalRequire('electron');
const remote = optionalRequire('@electron/remote');
const app = remote ? remote.app : null;
+export const getItemsColumns = (
+ windowSize: WindowSizeType,
+ isLandscape: boolean
+) => {
+ return windowSize === 'small' && !isLandscape ? 2 : 4;
+};
+
+const generateProjectName = (nameToAppend: ?string) =>
+ (nameToAppend ? `${generateName()} (${nameToAppend})` : generateName()).slice(
+ 0,
+ CLOUD_PROJECT_NAME_MAX_LENGTH
+ );
+
export type NewProjectSetup = {|
storageProvider: StorageProvider,
saveAsLocation: ?SaveAsLocation,
@@ -66,12 +83,11 @@ export type NewProjectSetup = {|
width?: number,
orientation?: 'landscape' | 'portrait' | 'default',
optimizeForPixelArt?: boolean,
- allowPlayersToLogIn?: boolean,
openQuickCustomizationDialog?: boolean,
|};
type Props = {|
- isOpeningProject?: boolean,
+ isProjectOpening?: boolean,
onClose: () => void,
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise,
onCreateFromExample: (
@@ -84,43 +100,63 @@ type Props = {|
newProjectSetup: NewProjectSetup,
i18n: I18nType
) => Promise,
- onCreateWithLogin: (newProjectSetup: NewProjectSetup) => Promise,
onCreateFromAIGeneration: (
generatedProject: GeneratedProject,
newProjectSetup: NewProjectSetup
) => Promise,
selectedExampleShortHeader: ?ExampleShortHeader,
+ onSelectExampleShortHeader: (exampleShortHeader: ?ExampleShortHeader) => void,
selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData,
+ onSelectPrivateGameTemplateListingData: (
+ privateGameTemplateListingData: ?PrivateGameTemplateListingData
+ ) => void,
storageProviders: Array,
- authenticatedUser: AuthenticatedUser,
+ privateGameTemplateListingDatasFromSameCreator: ?Array,
+ preventBackHome?: boolean,
+ preventBackDetails?: boolean,
|};
const NewProjectSetupDialog = ({
- isOpeningProject,
+ isProjectOpening,
onClose,
onCreateEmptyProject,
onCreateFromExample,
onCreateProjectFromPrivateGameTemplate,
- onCreateWithLogin,
onCreateFromAIGeneration,
selectedExampleShortHeader,
+ onSelectExampleShortHeader,
selectedPrivateGameTemplateListingData,
+ onSelectPrivateGameTemplateListingData,
storageProviders,
- authenticatedUser,
+ privateGameTemplateListingDatasFromSameCreator,
+ preventBackHome,
+ preventBackDetails,
}: Props): React.Node => {
- const generateProjectName = () =>
- (selectedExampleShortHeader
- ? `${generateName()} (${selectedExampleShortHeader.name})`
- : selectedPrivateGameTemplateListingData
- ? `${generateName()} (${selectedPrivateGameTemplateListingData.name})`
- : generateName()
- ).slice(0, CLOUD_PROJECT_NAME_MAX_LENGTH);
-
- const { getAuthorizationHeader, profile } = React.useContext(
- AuthenticatedUserContext
+ const authenticatedUser = React.useContext(AuthenticatedUserContext);
+ const { windowSize, isLandscape } = useResponsiveWindowSize();
+ const {
+ profile,
+ getAuthorizationHeader,
+ limits,
+ authenticated,
+ onOpenLoginDialog,
+ onOpenCreateAccountDialog,
+ receivedGameTemplates,
+ gameTemplatePurchases,
+ } = authenticatedUser;
+ const [
+ emptyProjectSelected,
+ setEmptyProjectSelected,
+ ] = React.useState(false);
+ const [page, setPage] = React.useState<'home' | 'details' | 'create'>(
+ selectedExampleShortHeader || selectedPrivateGameTemplateListingData
+ ? 'details'
+ : 'home'
+ );
+ const { privateGameTemplateListingDatas } = React.useContext(
+ PrivateGameTemplateStoreContext
);
const { showAlert } = useAlertDialog();
- const isOnline = useOnlineStatus();
const { values, setNewProjectsDefaultStorageProviderName } = React.useContext(
PreferencesContext
);
@@ -156,9 +192,6 @@ const NewProjectSetupDialog = ({
const [optimizeForPixelArt, setOptimizeForPixelArt] = React.useState(
false
);
- const [allowPlayersToLogIn, setAllowPlayersToLogIn] = React.useState(
- false
- );
const newProjectsDefaultFolder = app
? findEmptyPathInWorkspaceFolder(app, values.newProjectsDefaultFolder || '')
: '';
@@ -197,7 +230,7 @@ const NewProjectSetupDialog = ({
if (preferredStorageProvider) return preferredStorageProvider;
// If preferred storage provider not found, push Cloud storage provider if user authenticated.
- if (authenticatedUser.authenticated) {
+ if (authenticated) {
if (cloudStorageProvider) return cloudStorageProvider;
}
@@ -216,20 +249,12 @@ const NewProjectSetupDialog = ({
: null
);
- const generationCurrentUsage = authenticatedUser.limits
- ? authenticatedUser.limits.quotas['ai-project-generation']
- : null;
- const canGenerateProjectFromPrompt =
- generationCurrentUsage && !generationCurrentUsage.limitReached;
-
const needUserAuthenticationForStorage =
- storageProvider.needUserAuthentication && !authenticatedUser.authenticated;
- const { limits } = authenticatedUser;
+ storageProvider.needUserAuthentication && !authenticated;
const hasTooManyCloudProjects =
storageProvider.internalName === 'Cloud' &&
checkIfHasTooManyCloudProjects(authenticatedUser);
- const hasNotSelectedAStorageProvider =
- storageProvider.internalName === 'Empty';
+ const hasSelectedAStorageProvider = storageProvider.internalName !== 'Empty';
const selectedWidth =
resolutionOptions[resolutionOption].width ||
@@ -241,10 +266,26 @@ const NewProjectSetupDialog = ({
defaultCustomHeight;
const selectedOrientation = resolutionOptions[resolutionOption].orientation;
- const isLoading = isGeneratingProject || isOpeningProject;
+ const isLoading = isGeneratingProject || isProjectOpening;
- const isStartingProjectFromScratch =
- !selectedExampleShortHeader && !selectedPrivateGameTemplateListingData;
+ const isSelectedGameTemplateOwned = React.useMemo(
+ () =>
+ !selectedPrivateGameTemplateListingData ||
+ !!getUserProductPurchaseUsageType({
+ productId: selectedPrivateGameTemplateListingData
+ ? selectedPrivateGameTemplateListingData.id
+ : null,
+ receivedProducts: receivedGameTemplates,
+ productPurchases: gameTemplatePurchases,
+ allProductListingDatas: privateGameTemplateListingDatas,
+ }),
+ [
+ gameTemplatePurchases,
+ selectedPrivateGameTemplateListingData,
+ privateGameTemplateListingDatas,
+ receivedGameTemplates,
+ ]
+ );
// On the local app, prefer to always have something saved so that the user is not blocked.
// On the web-app, allow to create a project without saving it, unless a private game template is selected
@@ -252,16 +293,19 @@ const NewProjectSetupDialog = ({
const shouldAllowCreatingProjectWithoutSaving =
!electron && !selectedPrivateGameTemplateListingData;
- const shouldNotAllowCreatingProject =
- isLoading ||
- needUserAuthenticationForStorage ||
- hasTooManyCloudProjects ||
- (hasNotSelectedAStorageProvider &&
- !shouldAllowCreatingProjectWithoutSaving);
+ const shouldAllowCreatingProject =
+ !isLoading &&
+ ((page === 'home' && generationPrompt) ||
+ (page === 'details' && isSelectedGameTemplateOwned) ||
+ (page === 'create' &&
+ !needUserAuthenticationForStorage &&
+ !hasTooManyCloudProjects &&
+ (hasSelectedAStorageProvider ||
+ shouldAllowCreatingProjectWithoutSaving)));
const generateProject = React.useCallback(
async () => {
- if (shouldNotAllowCreatingProject) return;
+ if (!shouldAllowCreatingProject) return;
if (!profile) return;
setIsGeneratingProject(true);
@@ -298,7 +342,7 @@ const NewProjectSetupDialog = ({
}
},
[
- shouldNotAllowCreatingProject,
+ shouldAllowCreatingProject,
getAuthorizationHeader,
generationPrompt,
profile,
@@ -310,14 +354,19 @@ const NewProjectSetupDialog = ({
]
);
- const onValidate = React.useCallback(
+ const onCreateGameClick = React.useCallback(
async (i18n: I18nType) => {
- if (generationPrompt) {
- generateProject();
+ if (!shouldAllowCreatingProject) return;
+
+ if (page === 'details') {
+ setPage('create');
return;
}
- if (shouldNotAllowCreatingProject) return;
+ if (page === 'home' && generationPrompt) {
+ generateProject();
+ return;
+ }
setProjectNameError(null);
if (!projectName) {
@@ -344,7 +393,6 @@ const NewProjectSetupDialog = ({
width: selectedWidth,
orientation: selectedOrientation,
optimizeForPixelArt,
- allowPlayersToLogIn,
};
if (selectedExampleShortHeader) {
@@ -369,16 +417,13 @@ const NewProjectSetupDialog = ({
},
i18n
);
- } else if (allowPlayersToLogIn) {
- await onCreateWithLogin(projectSetup);
- return;
} else {
await onCreateEmptyProject(projectSetup);
}
},
[
generationPrompt,
- shouldNotAllowCreatingProject,
+ shouldAllowCreatingProject,
projectName,
storageProvider,
saveAsLocation,
@@ -387,14 +432,13 @@ const NewProjectSetupDialog = ({
selectedWidth,
selectedOrientation,
optimizeForPixelArt,
- allowPlayersToLogIn,
selectedExampleShortHeader,
generateProject,
selectedPrivateGameTemplateListingData,
onCreateFromExample,
onCreateProjectFromPrivateGameTemplate,
- onCreateWithLogin,
onCreateEmptyProject,
+ page,
]
);
@@ -406,254 +450,322 @@ const NewProjectSetupDialog = ({
[setProjectName, projectNameError]
);
+ // Update project name when the example or private game template changes.
+ React.useEffect(
+ () => {
+ if (selectedExampleShortHeader) {
+ setProjectName(generateProjectName(selectedExampleShortHeader.name));
+ }
+ if (selectedPrivateGameTemplateListingData) {
+ setProjectName(
+ generateProjectName(selectedPrivateGameTemplateListingData.name)
+ );
+ }
+ if (emptyProjectSelected) {
+ setProjectName(generateProjectName());
+ }
+ },
+ [
+ selectedExampleShortHeader,
+ selectedPrivateGameTemplateListingData,
+ emptyProjectSelected,
+ ]
+ );
+
+ const onBack = React.useCallback(
+ () => {
+ if (page === 'create') {
+ if (emptyProjectSelected) {
+ if (!preventBackHome) {
+ setEmptyProjectSelected(false);
+ setPage('home');
+ }
+ } else {
+ if (!preventBackDetails) {
+ setPage('details');
+ }
+ }
+ }
+ if (page === 'details' && !preventBackHome) {
+ if (selectedExampleShortHeader) onSelectExampleShortHeader(null);
+ if (selectedPrivateGameTemplateListingData)
+ onSelectPrivateGameTemplateListingData(null);
+ setPage('home');
+ }
+ },
+ [
+ emptyProjectSelected,
+ setEmptyProjectSelected,
+ selectedExampleShortHeader,
+ onSelectExampleShortHeader,
+ selectedPrivateGameTemplateListingData,
+ onSelectPrivateGameTemplateListingData,
+ page,
+ preventBackHome,
+ preventBackDetails,
+ ]
+ );
+
+ const shouldShowBackButton = React.useMemo(
+ () => {
+ if (page === 'home') return false;
+ if (page === 'details') return !preventBackHome;
+ if (
+ page === 'create' &&
+ !preventBackDetails &&
+ !(preventBackHome && emptyProjectSelected)
+ )
+ return true;
+ },
+ [page, preventBackHome, preventBackDetails, emptyProjectSelected]
+ );
+
+ const shouldUseSmallWidth =
+ (page === 'details' && !!selectedExampleShortHeader) || page === 'create';
+ const shouldUseFullHeight =
+ page === 'home' ||
+ (page === 'details' && !!selectedPrivateGameTemplateListingData);
+
return (
{({ i18n }) => (
)}
diff --git a/newIDE/app/src/UI/Dialog.js b/newIDE/app/src/UI/Dialog.js
index e7da6f5b488a..c559c7e8fd69 100644
--- a/newIDE/app/src/UI/Dialog.js
+++ b/newIDE/app/src/UI/Dialog.js
@@ -148,22 +148,24 @@ const useDangerousStylesForDialog = (dangerLevel?: 'warning' | 'danger') =>
// Customize scrollbar inside Dialog so that it gives a bit of space
// to the content.
-const useStylesForDialogContent = makeStyles({
- root: {
- '&::-webkit-scrollbar': {
- width: 11,
- },
- '&::-webkit-scrollbar-track': {
- background: 'rgba(0, 0, 0, 0.04)',
- borderRadius: 6,
- },
- '&::-webkit-scrollbar-thumb': {
- border: '3px solid rgba(0, 0, 0, 0)',
- backgroundClip: 'padding-box',
- borderRadius: 6,
+const useStylesForDialogContent = ({ forceScroll }: { forceScroll: boolean }) =>
+ makeStyles({
+ root: {
+ ...(forceScroll ? { overflowY: 'scroll' } : {}), // Force a scrollbar to prevent layout shifts.
+ '&::-webkit-scrollbar': {
+ width: 11,
+ },
+ '&::-webkit-scrollbar-track': {
+ background: 'rgba(0, 0, 0, 0.04)',
+ borderRadius: 6,
+ },
+ '&::-webkit-scrollbar-thumb': {
+ border: '3px solid rgba(0, 0, 0, 0)',
+ backgroundClip: 'padding-box',
+ borderRadius: 6,
+ },
},
- },
-});
+ })();
// We support a subset of the props supported by Material-UI v0.x Dialog
// They should be self descriptive - refer to Material UI docs otherwise.
@@ -219,6 +221,8 @@ type DialogProps = {|
fullHeight?: boolean,
fullscreen?: 'never-even-on-mobile' | 'always-even-on-desktop',
actionsFullWidthOnMobile?: boolean,
+ // Useful when the content of the dialog can change and we want to avoid layout shifts.
+ forceScrollVisible?: boolean,
id?: ?string,
|};
@@ -249,6 +253,7 @@ const Dialog = ({
exceptionallyStillAllowRenderingInstancesEditors,
fullscreen,
actionsFullWidthOnMobile,
+ forceScrollVisible,
}: DialogProps) => {
const preferences = React.useContext(PreferencesContext);
const gdevelopTheme = React.useContext(GDevelopThemeContext);
@@ -265,7 +270,9 @@ const Dialog = ({
: isMobile;
const classesForDangerousDialog = useDangerousStylesForDialog(dangerLevel);
- const classesForDialogContent = useStylesForDialogContent();
+ const classesForDialogContent = useStylesForDialogContent({
+ forceScroll: !!forceScrollVisible,
+ });
const dialogActions = React.useMemo(
() => (
diff --git a/newIDE/app/src/Utils/UseCreateProject.js b/newIDE/app/src/Utils/UseCreateProject.js
index ce4db1b19963..e843d6e5a4c5 100644
--- a/newIDE/app/src/Utils/UseCreateProject.js
+++ b/newIDE/app/src/Utils/UseCreateProject.js
@@ -8,7 +8,6 @@ import {
createNewProjectFromExampleShortHeader,
createNewProjectFromPrivateGameTemplate,
createNewProjectFromTutorialTemplate,
- createNewProjectWithDefaultLogin,
type NewProjectSource,
} from '../ProjectCreation/CreateProject';
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
@@ -379,15 +378,6 @@ const useCreateProject = ({
[beforeCreatingProject, createProject, tutorials]
);
- const createProjectWithLogin = React.useCallback(
- async (newProjectSetup: NewProjectSetup) => {
- beforeCreatingProject();
- const newProjectSource = createNewProjectWithDefaultLogin();
- await createProject(newProjectSource, newProjectSetup);
- },
- [beforeCreatingProject, createProject]
- );
-
const createProjectFromAIGeneration = React.useCallback(
async (projectFileUrl: string, newProjectSetup: NewProjectSetup) => {
beforeCreatingProject();
@@ -405,7 +395,6 @@ const useCreateProject = ({
createProjectFromPrivateGameTemplate,
createProjectFromInAppTutorial,
createProjectFromTutorial,
- createProjectWithLogin,
createProjectFromAIGeneration,
};
};
diff --git a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js b/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js
deleted file mode 100644
index 8eefd094cc6c..000000000000
--- a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// @flow
-import * as React from 'react';
-import { action } from '@storybook/addon-actions';
-
-import paperDecorator from '../../../PaperDecorator';
-import { ExampleDialog } from '../../../../AssetStore/ExampleStore/ExampleDialog';
-import { exampleFromFutureVersion } from '../../../../fixtures/GDevelopServicesTestData';
-
-export default {
- title: 'AssetStore/ExampleStore/ExampleDialog',
- component: ExampleDialog,
- decorators: [paperDecorator],
-};
-
-export const FutureVersion = () => (
-
-);
diff --git a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js b/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js
deleted file mode 100644
index bada9e26a5ab..000000000000
--- a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// @flow
-import * as React from 'react';
-import { action } from '@storybook/addon-actions';
-
-import paperDecorator from '../../../PaperDecorator';
-import { ExampleStore } from '../../../../AssetStore/ExampleStore';
-import FixedHeightFlexContainer from '../../../FixedHeightFlexContainer';
-import { ExampleStoreStateProvider } from '../../../../AssetStore/ExampleStore/ExampleStoreContext';
-
-export default {
- title: 'AssetStore/ExampleStore',
- component: ExampleStore,
- decorators: [paperDecorator],
-};
-
-export const Default = () => (
-
-
-
-
-
-);
diff --git a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js b/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js
deleted file mode 100644
index 07671aea95b5..000000000000
--- a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// @flow
-import * as React from 'react';
-import paperDecorator from '../../../PaperDecorator';
-import { action } from '@storybook/addon-actions';
-import { ExampleStoreStateProvider } from '../../../../AssetStore/ExampleStore/ExampleStoreContext';
-import ExampleStoreDialog from '../../../../AssetStore/ExampleStore/ExampleStoreDialog';
-
-export default {
- title: 'Project Creation/ExampleStoreDialog',
- component: ExampleStoreDialog,
- decorators: [paperDecorator],
-};
-
-export const Default = () => (
-
-
-
-);
diff --git a/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js b/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js
index a8e3d5f086a5..bb034d5d7d7a 100644
--- a/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js
+++ b/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js
@@ -93,12 +93,11 @@ const WrappedHomePage = ({
storageProviders={[CloudStorageProvider]}
onChooseProject={() => action('onChooseProject')()}
onOpenRecentFile={() => action('onOpenRecentFile')()}
- onOpenExampleStore={() => action('onOpenExampleStore')()}
onSelectExampleShortHeader={() =>
action('onSelectExampleShortHeader')()
}
- onPreviewPrivateGameTemplateListingData={() =>
- action('onPreviewPrivateGameTemplateListingData')()
+ onSelectPrivateGameTemplateListingData={() =>
+ action('onSelectPrivateGameTemplateListingData')()
}
onOpenPrivateGameTemplateListingData={() =>
action('onOpenPrivateGameTemplateListingData')()
diff --git a/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js b/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js
index fef4525fe5da..9f9d870a2064 100644
--- a/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js
+++ b/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js
@@ -44,7 +44,6 @@ export const Default = () => (
>
{}}
selectInAppTutorial={action('selectInAppTutorial')}
onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')}
@@ -66,7 +65,6 @@ export const NotAuthenticated = () => (
>
{}}
selectInAppTutorial={action('selectInAppTutorial')}
onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')}
@@ -90,7 +88,6 @@ export const EducationSubscriber = () => (
>
{}}
selectInAppTutorial={action('selectInAppTutorial')}
onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')}
@@ -114,7 +111,6 @@ export const EducationTeacher = () => (
>
{}}
selectInAppTutorial={action('selectInAppTutorial')}
onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')}
@@ -135,7 +131,6 @@ export const Loading = () => (
>
{}}
selectInAppTutorial={action('selectInAppTutorial')}
onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')}
diff --git a/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js b/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js
index ba8ec853a160..6c424ded3a60 100644
--- a/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js
+++ b/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js
@@ -15,6 +15,7 @@ import {
geometryMonsterExampleShortHeader,
fakePrivateGameTemplateListingData,
} from '../../../fixtures/GDevelopServicesTestData';
+import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
export default {
title: 'Project Creation/NewProjectSetupDialog',
@@ -24,149 +25,179 @@ export default {
export const OpenAndNotAuthenticated = () => {
return (
- 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}
- />
+
+ action('click on close')()}
+ onCreateEmptyProject={() => action('create empty')()}
+ onCreateFromExample={() => action('create from example')()}
+ onCreateFromAIGeneration={() => action('create from AI generation')()}
+ onCreateProjectFromPrivateGameTemplate={() =>
+ action('create project from private game template')()
+ }
+ selectedExampleShortHeader={null}
+ selectedPrivateGameTemplateListingData={null}
+ onSelectExampleShortHeader={() => action('select example')()}
+ onSelectPrivateGameTemplateListingData={() =>
+ action('select private game template')()
+ }
+ privateGameTemplateListingDatasFromSameCreator={[]}
+ />
+
);
};
export const OpenAndAuthenticated = () => {
return (
- 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}
- />
+
+ action('click on close')()}
+ onCreateEmptyProject={() => action('create empty')()}
+ onCreateFromExample={() => action('create from example')()}
+ onCreateFromAIGeneration={() => action('create from AI generation')()}
+ onCreateProjectFromPrivateGameTemplate={() =>
+ action('create project from private game template')()
+ }
+ selectedExampleShortHeader={null}
+ selectedPrivateGameTemplateListingData={null}
+ onSelectExampleShortHeader={() => action('select example')()}
+ onSelectPrivateGameTemplateListingData={() =>
+ action('select private game template')()
+ }
+ privateGameTemplateListingDatasFromSameCreator={[]}
+ />
+
);
};
export const Opening = () => {
return (
- 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}
- />
+
+ action('click on close')()}
+ onCreateEmptyProject={() => action('create empty')()}
+ onCreateFromExample={() => action('create from example')()}
+ onCreateFromAIGeneration={() => action('create from AI generation')()}
+ onCreateProjectFromPrivateGameTemplate={() =>
+ action('create project from private game template')()
+ }
+ selectedExampleShortHeader={null}
+ selectedPrivateGameTemplateListingData={null}
+ onSelectExampleShortHeader={() => action('select example')()}
+ onSelectPrivateGameTemplateListingData={() =>
+ action('select private game template')()
+ }
+ privateGameTemplateListingDatasFromSameCreator={[]}
+ />
+
);
};
export const LimitsReached = () => {
return (
- 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}
- />
+
+ action('click on close')()}
+ onCreateEmptyProject={() => action('create empty')()}
+ onCreateFromExample={() => action('create from example')()}
+ onCreateFromAIGeneration={() => action('create from AI generation')()}
+ onCreateProjectFromPrivateGameTemplate={() =>
+ action('create project from private game template')()
+ }
+ selectedExampleShortHeader={null}
+ selectedPrivateGameTemplateListingData={null}
+ onSelectExampleShortHeader={() => action('select example')()}
+ onSelectPrivateGameTemplateListingData={() =>
+ action('select private game template')()
+ }
+ privateGameTemplateListingDatasFromSameCreator={[]}
+ />
+
);
};
export const FromExample = () => {
return (
- action('click on close')()}
- onCreateEmptyProject={() => action('create empty')()}
- onCreateFromExample={() => action('create from example')()}
- onCreateWithLogin={() => action('create with login')()}
- onCreateFromAIGeneration={() => action('create from AI generation')()}
- selectedExampleShortHeader={geometryMonsterExampleShortHeader}
- onCreateProjectFromPrivateGameTemplate={() =>
- action('create project from private game template')()
- }
- selectedPrivateGameTemplateListingData={null}
- />
+
+ action('click on close')()}
+ onCreateEmptyProject={() => action('create empty')()}
+ onCreateFromExample={() => action('create from example')()}
+ onCreateFromAIGeneration={() => action('create from AI generation')()}
+ selectedExampleShortHeader={geometryMonsterExampleShortHeader}
+ onCreateProjectFromPrivateGameTemplate={() =>
+ action('create project from private game template')()
+ }
+ selectedPrivateGameTemplateListingData={null}
+ onSelectExampleShortHeader={() => action('select example')()}
+ onSelectPrivateGameTemplateListingData={() =>
+ action('select private game template')()
+ }
+ privateGameTemplateListingDatasFromSameCreator={[]}
+ />
+
);
};
export const FromPrivateGameTemplate = () => {
return (
- action('click on close')()}
- onCreateEmptyProject={() => action('create empty')()}
- onCreateFromExample={() => action('create from example')()}
- onCreateWithLogin={() => action('create with login')()}
- onCreateFromAIGeneration={() => action('create from AI generation')()}
- selectedExampleShortHeader={null}
- onCreateProjectFromPrivateGameTemplate={() =>
- action('create project from private game template')()
- }
- selectedPrivateGameTemplateListingData={
- fakePrivateGameTemplateListingData
- }
- />
+
+ action('click on close')()}
+ onCreateEmptyProject={() => action('create empty')()}
+ onCreateFromExample={() => action('create from example')()}
+ onCreateFromAIGeneration={() => action('create from AI generation')()}
+ selectedExampleShortHeader={null}
+ onCreateProjectFromPrivateGameTemplate={() =>
+ action('create project from private game template')()
+ }
+ selectedPrivateGameTemplateListingData={
+ fakePrivateGameTemplateListingData
+ }
+ onSelectExampleShortHeader={() => action('select example')()}
+ onSelectPrivateGameTemplateListingData={() =>
+ action('select private game template')()
+ }
+ privateGameTemplateListingDatasFromSameCreator={[]}
+ />
+
);
};