diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js deleted file mode 100644 index 96d5c9febed8..000000000000 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js +++ /dev/null @@ -1,191 +0,0 @@ -// @flow -import { t } from '@lingui/macro'; -import { Trans } from '@lingui/macro'; -import * as React from 'react'; -import Dialog from '../../UI/Dialog'; -import FlatButton from '../../UI/FlatButton'; -import { - type ExampleShortHeader, - type Example, - getExample, -} from '../../Utils/GDevelopServices/Example'; -import { isCompatibleWithAsset } from '../../Utils/GDevelopServices/Asset'; -import PlaceholderError from '../../UI/PlaceholderError'; -import { MarkdownText } from '../../UI/MarkdownText'; -import Text from '../../UI/Text'; -import AlertMessage from '../../UI/AlertMessage'; -import { getIDEVersion } from '../../Version'; -import { Column, Line } from '../../UI/Grid'; -import Divider from '@material-ui/core/Divider'; -import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout'; -import { ExampleThumbnailOrIcon } from './ExampleThumbnailOrIcon'; -import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu'; -import Window from '../../Utils/Window'; -import optionalRequire from '../../Utils/OptionalRequire'; -import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip'; -import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip'; -import { ExampleSizeChip } from '../../UI/ExampleSizeChip'; -const isDev = Window.isDev(); - -const electron = optionalRequire('electron'); - -type Props = {| - exampleShortHeader: ExampleShortHeader, - isOpening: boolean, - onClose: () => void, - onOpen: () => void, -|}; - -export const openExampleInWebApp = (example: Example) => { - Window.openExternalURL( - `${ - isDev ? 'http://localhost:3000' : 'https://editor.gdevelop.io' - }/?create-from-example=${example.slug}` - ); -}; - -export function ExampleDialog({ - isOpening, - exampleShortHeader, - onClose, - onOpen, -}: Props) { - const [error, setError] = React.useState(null); - const [example, setExample] = React.useState(null); - - const loadExample = React.useCallback( - async () => { - setError(null); - try { - const example = await getExample(exampleShortHeader); - setExample(example); - } catch (error) { - setError(error); - } - }, - [exampleShortHeader] - ); - - React.useEffect( - () => { - loadExample(); - }, - [loadExample] - ); - - const isCompatible = isCompatibleWithAsset( - getIDEVersion(), - exampleShortHeader - ); - const hasIcon = exampleShortHeader.previewImageUrls.length > 0; - - const canOpenExample = !isOpening && isCompatible; - const onOpenExample = React.useCallback( - () => { - if (canOpenExample) onOpen(); - }, - [onOpen, canOpenExample] - ); - - return ( - Back} - primary={false} - onClick={onClose} - disabled={isOpening} - />, - Not compatible : Open - } - primary - onClick={onOpenExample} - disabled={!canOpenExample || isOpening} - buildMenuTemplate={i18n => [ - { - label: electron - ? i18n._(t`Open in the web-app`) - : i18n._(t`Open in a new tab`), - disabled: !example, - click: () => { - if (example) openExampleInWebApp(example); - }, - }, - ]} - />, - ]} - open - cannotBeDismissed={isOpening} - onRequestClose={onClose} - onApply={onOpenExample} - > - - {!isCompatible && ( - - - Unfortunately, this example requires a newer version of GDevelop - to work. Update GDevelop to be able to open this example. - - - )} - - {hasIcon ? ( - - ) : null} - - { - -
- {exampleShortHeader.difficultyLevel && ( - - )} - {exampleShortHeader.codeSizeLevel && ( - - )} - {exampleShortHeader.authors && - exampleShortHeader.authors.map(author => ( - - ))} -
-
- } - {exampleShortHeader.shortDescription} -
-
- - {example && example.description && ( - - - - - - - )} - {!example && error && ( - - - Can't load the example. Verify your internet connection or try - again later. - - - )} -
-
- ); -} diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js new file mode 100644 index 000000000000..f85effecf263 --- /dev/null +++ b/newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js @@ -0,0 +1,134 @@ +// @flow +import { Trans } from '@lingui/macro'; +import * as React from 'react'; +import { + type ExampleShortHeader, + type Example, + getExample, +} from '../../Utils/GDevelopServices/Example'; +import { isCompatibleWithAsset } from '../../Utils/GDevelopServices/Asset'; +import PlaceholderError from '../../UI/PlaceholderError'; +import { MarkdownText } from '../../UI/MarkdownText'; +import Text from '../../UI/Text'; +import AlertMessage from '../../UI/AlertMessage'; +import { getIDEVersion } from '../../Version'; +import { Column, Line } from '../../UI/Grid'; +import Divider from '@material-ui/core/Divider'; +import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout'; +import { ExampleThumbnailOrIcon } from './ExampleThumbnailOrIcon'; +import Window from '../../Utils/Window'; +import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip'; +import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip'; +import { ExampleSizeChip } from '../../UI/ExampleSizeChip'; +const isDev = Window.isDev(); + +type Props = {| + exampleShortHeader: ExampleShortHeader, +|}; + +export const openExampleInWebApp = (example: Example) => { + Window.openExternalURL( + `${ + isDev ? 'http://localhost:3000' : 'https://editor.gdevelop.io' + }/?create-from-example=${example.slug}` + ); +}; + +const ExampleInformationPage = ({ exampleShortHeader }: Props) => { + const [error, setError] = React.useState(null); + const [example, setExample] = React.useState(null); + + const loadExample = React.useCallback( + async () => { + setError(null); + try { + const example = await getExample(exampleShortHeader); + setExample(example); + } catch (error) { + setError(error); + } + }, + [exampleShortHeader] + ); + + React.useEffect( + () => { + loadExample(); + }, + [loadExample] + ); + + const isCompatible = isCompatibleWithAsset( + getIDEVersion(), + exampleShortHeader + ); + const hasIcon = exampleShortHeader.previewImageUrls.length > 0; + + return ( + + {!isCompatible && ( + + + Unfortunately, this example requires a newer version of GDevelop to + work. Update GDevelop to be able to open this example. + + + )} + + {hasIcon ? ( + + ) : null} + + { + +
+ {exampleShortHeader.difficultyLevel && ( + + )} + {exampleShortHeader.codeSizeLevel && ( + + )} + {exampleShortHeader.authors && + exampleShortHeader.authors.map(author => ( + + ))} +
+
+ } + {exampleShortHeader.shortDescription} +
+
+ + {example && example.description && ( + + + + + + + )} + {!example && error && ( + + + Can't load the example. Verify your internet connection or try again + later. + + + )} +
+ ); +}; + +export default ExampleInformationPage; diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js deleted file mode 100644 index 1499cd9beb39..000000000000 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js +++ /dev/null @@ -1,174 +0,0 @@ -// @flow -import { t } from '@lingui/macro'; -import { type I18n as I18nType } from '@lingui/core'; -import * as React from 'react'; -import { - type ExampleShortHeader, - getExample, -} from '../../Utils/GDevelopServices/Example'; -import { isCompatibleWithAsset } from '../../Utils/GDevelopServices/Asset'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Text from '../../UI/Text'; -import { Trans } from '@lingui/macro'; -import { Column, Line } from '../../UI/Grid'; -import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu'; -import { getIDEVersion } from '../../Version'; -import { ExampleThumbnailOrIcon } from './ExampleThumbnailOrIcon'; -import optionalRequire from '../../Utils/OptionalRequire'; -import { openExampleInWebApp } from './ExampleDialog'; -import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip'; -import { ExampleSizeChip } from '../../UI/ExampleSizeChip'; -import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip'; -import HighlightedText from '../../UI/Search/HighlightedText'; -import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem'; -import { ResponsiveLineStackLayout } from '../../UI/Layout'; -import useAlertDialog from '../../UI/Alert/useAlertDialog'; - -const electron = optionalRequire('electron'); - -const styles = { - container: { - display: 'flex', - overflow: 'hidden', - paddingTop: 8, - paddingBottom: 8, - paddingRight: 8, - }, - button: { - alignItems: 'flex-start', - textAlign: 'left', - flex: 1, - }, -}; - -type Props = {| - exampleShortHeader: ExampleShortHeader, - matches: ?Array, - isOpening: boolean, - onChoose: () => void, - onOpen: () => void, - onHeightComputed: number => void, -|}; - -const ExampleListItem = ({ - exampleShortHeader, - matches, - isOpening, - onChoose, - onOpen, - onHeightComputed, -}: Props) => { - const { showAlert } = useAlertDialog(); - // Report the height of the item once it's known. - const containerRef = React.useRef(null); - React.useLayoutEffect(() => { - if (containerRef.current) - onHeightComputed(containerRef.current.getBoundingClientRect().height); - }); - - const isCompatible = isCompatibleWithAsset( - getIDEVersion(), - exampleShortHeader - ); - - const fetchAndOpenExampleInWebApp = React.useCallback( - async (i18n: I18nType) => { - try { - const example = await getExample(exampleShortHeader); - openExampleInWebApp(example); - } catch (error) { - await showAlert({ - title: t`Unable to fetch the example.`, - message: t`Verify your internet connection or try again later.`, - }); - } - }, - [exampleShortHeader, showAlert] - ); - - const renderExampleField = (field: 'shortDescription' | 'name') => { - const originalField = exampleShortHeader[field]; - - if (!matches) return originalField; - const nameMatches = matches.filter(match => match.key === field); - if (nameMatches.length === 0) return originalField; - - return ( - - ); - }; - - return ( -
- - - - {!!exampleShortHeader.previewImageUrls.length && ( - - - - )} - - {renderExampleField('name')} - -
- {exampleShortHeader.difficultyLevel && ( - - )} - {exampleShortHeader.codeSizeLevel && ( - - )} - {exampleShortHeader.authors && - exampleShortHeader.authors.map(author => ( - - ))} -
-
- - {renderExampleField('shortDescription')} - -
-
-
- - - Open} - disabled={isOpening || !isCompatible} - onClick={onOpen} - buildMenuTemplate={i18n => [ - { - label: i18n._(t`Open details`), - click: onChoose, - }, - { - label: electron - ? i18n._(t`Open in the web-app`) - : i18n._(t`Open in a new tab`), - click: () => { - fetchAndOpenExampleInWebApp(i18n); - }, - }, - ]} - /> - - -
-
- ); -}; - -export default ExampleListItem; diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js deleted file mode 100644 index 1c56430e812e..000000000000 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js +++ /dev/null @@ -1,86 +0,0 @@ -// @flow -import { Trans } from '@lingui/macro'; -import { I18n } from '@lingui/react'; -import * as React from 'react'; -import { ExampleStore } from '../../AssetStore/ExampleStore'; -import Dialog, { DialogPrimaryButton } from '../../UI/Dialog'; -import FlatButton from '../../UI/FlatButton'; -import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example'; -import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; - -export type ExampleStoreDialogProps = {| - open: boolean, - onClose: () => void, - selectedExampleShortHeader: ?ExampleShortHeader, - selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData, - onSelectExampleShortHeader: (exampleShortHeader: ?ExampleShortHeader) => void, - onSelectPrivateGameTemplateListingData: ( - privateGameTemplateListingData: ?PrivateGameTemplateListingData - ) => void, - onOpenNewProjectSetupDialog: () => void, - isProjectOpening: boolean, -|}; - -const ExampleStoreDialog = ({ - open, - onClose, - selectedExampleShortHeader, - selectedPrivateGameTemplateListingData, - onSelectExampleShortHeader, - onSelectPrivateGameTemplateListingData, - onOpenNewProjectSetupDialog, - isProjectOpening, -}: ExampleStoreDialogProps) => { - const actions = React.useMemo( - () => [ - Close} - primary={false} - onClick={onClose} - />, - Create a blank project} - primary - onClick={onOpenNewProjectSetupDialog} - />, - ], - [onClose, onOpenNewProjectSetupDialog] - ); - - if (!open) return null; - - return ( - - {({ i18n }) => ( - Create a new project} - actions={actions} - onRequestClose={onClose} - onApply={onOpenNewProjectSetupDialog} - open={open} - fullHeight - flexColumnBody - > - - - )} - - ); -}; - -export default ExampleStoreDialog; diff --git a/newIDE/app/src/AssetStore/ExampleStore/index.js b/newIDE/app/src/AssetStore/ExampleStore/index.js index de57ec7d9d1b..1efc1fdb05ad 100644 --- a/newIDE/app/src/AssetStore/ExampleStore/index.js +++ b/newIDE/app/src/AssetStore/ExampleStore/index.js @@ -1,105 +1,104 @@ // @flow import * as React from 'react'; +import { type I18n as I18nType } from '@lingui/core'; import SearchBar, { type SearchBarInterface } from '../../UI/SearchBar'; -import { Column, Line } from '../../UI/Grid'; +import { Column, Line, Spacer } from '../../UI/Grid'; import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example'; import { ExampleStoreContext } from './ExampleStoreContext'; -import { ListSearchResults } from '../../UI/Search/ListSearchResults'; -import ExampleListItem from './ExampleListItem'; -import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem'; import { sendExampleDetailsOpened, sendGameTemplateInformationOpened, } from '../../Utils/Analytics/EventSender'; -import { t } from '@lingui/macro'; +import { t, Trans } from '@lingui/macro'; import { useShouldAutofocusInput } from '../../UI/Responsive/ScreenTypeMeasurer'; import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; -import PrivateGameTemplateListItem from '../PrivateGameTemplates/PrivateGameTemplateListItem'; import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext'; import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext'; +import { GridList } from '@material-ui/core'; +import { getExampleAndTemplateTiles } from '../../MainFrame/EditorContainers/HomePage/BuildSection/utils'; +import BackgroundText from '../../UI/BackgroundText'; + +const styles = { + grid: { + margin: 0, + // Remove the scroll capability of the grid, the scroll view handles it. + overflow: 'unset', + }, +}; -const getItemUniqueId = ( - item: ExampleShortHeader | PrivateGameTemplateListingData -) => item.id; +// Filter out examples that aren't games. +const gameFilter = ( + item: PrivateGameTemplateListingData | ExampleShortHeader +) => { + if (item.previewImageUrls) { + // It's an example, filter out examples that are not games or have no thumbnail. + return item.tags.includes('game') && !!item.previewImageUrls[0]; + } + // It's a game template, trust it's been filtered correctly. + return true; +}; type Props = {| - isOpening: boolean, - onOpenNewProjectSetupDialog: () => void, - focusOnMount?: boolean, - selectedExampleShortHeader: ?ExampleShortHeader, - onSelectExampleShortHeader: (?ExampleShortHeader) => void, - selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData, - onSelectPrivateGameTemplateListingData: ( - ?PrivateGameTemplateListingData - ) => void, + onSelectExampleShortHeader: ExampleShortHeader => void, + onSelectPrivateGameTemplateListingData: PrivateGameTemplateListingData => void, + i18n: I18nType, + onlyShowGames?: boolean, + columnsCount: number, + rowToInsert?: {| + row: number, + element: React.Node, + |}, |}; -export const ExampleStore = ({ - isOpening, - onOpenNewProjectSetupDialog, - focusOnMount, - // The example store is "controlled" by the parent. Useful as selected items are - // needed in MainFrame, to display them in NewProjectSetupDialog. - selectedExampleShortHeader, +const ExampleStore = ({ onSelectExampleShortHeader, - selectedPrivateGameTemplateListingData, onSelectPrivateGameTemplateListingData, + i18n, + onlyShowGames, + columnsCount, + rowToInsert, }: Props) => { const { receivedGameTemplates } = React.useContext(AuthenticatedUserContext); const { - exampleFilters, exampleShortHeadersSearchResults, - error: exampleStoreError, fetchExamplesAndFilters, - filtersState: exampleStoreFiltersState, - searchText, + searchText: exampleStoreSearchText, setSearchText: setExampleStoreSearchText, } = React.useContext(ExampleStoreContext); const { - gameTemplateFilters, - error: gameTemplateStoreError, fetchGameTemplates, exampleStore: { privateGameTemplateListingDatasSearchResults, - filtersState: gameTemplateStoreFiltersState, setSearchText: setGameTemplateStoreSearchText, }, } = React.useContext(PrivateGameTemplateStoreContext); + const [localSearchText, setLocalSearchText] = React.useState( + exampleStoreSearchText + ); const shouldAutofocusSearchbar = useShouldAutofocusInput(); const searchBarRef = React.useRef(null); React.useEffect( () => { - if (focusOnMount && shouldAutofocusSearchbar && searchBarRef.current) + if (shouldAutofocusSearchbar && searchBarRef.current) searchBarRef.current.focus(); }, - [shouldAutofocusSearchbar, focusOnMount] - ); - - // Tags are applied to both examples and game templates. - const tagsHandler = React.useMemo( - () => ({ - add: (tag: string) => { - exampleStoreFiltersState.addFilter(tag); - gameTemplateStoreFiltersState.addFilter(tag); - }, - remove: (tag: string) => { - exampleStoreFiltersState.removeFilter(tag); - gameTemplateStoreFiltersState.removeFilter(tag); - }, - // We use the same tags for both examples and game templates, so we can - // use the tags from either store. - chosenTags: exampleStoreFiltersState.chosenFilters, - }), - [exampleStoreFiltersState, gameTemplateStoreFiltersState] + [shouldAutofocusSearchbar] ); // We search in both examples and game templates stores. const setSearchText = React.useCallback( (searchText: string) => { - setExampleStoreSearchText(searchText); - setGameTemplateStoreSearchText(searchText); + if (searchText.length < 2) { + // Prevent searching with less than 2 characters, as it does not return any results. + setExampleStoreSearchText(''); + setGameTemplateStoreSearchText(''); + } else { + setExampleStoreSearchText(searchText); + setGameTemplateStoreSearchText(searchText); + } + setLocalSearchText(searchText); }, [setExampleStoreSearchText, setGameTemplateStoreSearchText] ); @@ -120,155 +119,127 @@ export const ExampleStore = ({ [fetchGameTemplatesAndExamples] ); - const getExampleShortHeaderMatches = ( - exampleShortHeader: ExampleShortHeader - ): SearchMatch[] => { - if (!exampleShortHeadersSearchResults) return []; - const exampleMatches = exampleShortHeadersSearchResults.find( - result => result.item.id === exampleShortHeader.id - ); - return exampleMatches ? exampleMatches.matches : []; - }; - - const getPrivateAssetPackListingDataMatches = ( - privateGameTemplateListingData: PrivateGameTemplateListingData - ): SearchMatch[] => { - if (!privateGameTemplateListingDatasSearchResults) return []; - const gameTemplateMatches = privateGameTemplateListingDatasSearchResults.find( - result => result.item.id === privateGameTemplateListingData.id - ); - return gameTemplateMatches ? gameTemplateMatches.matches : []; - }; - - const searchItems: ( - | ExampleShortHeader - | PrivateGameTemplateListingData - )[] = React.useMemo( + const resultTiles: React.Node[] = React.useMemo( () => { - const searchItems = []; - const privateGameTemplateItems = privateGameTemplateListingDatasSearchResults - ? privateGameTemplateListingDatasSearchResults.map(({ item }) => item) - : []; - const exampleShortHeaderItems = exampleShortHeadersSearchResults - ? exampleShortHeadersSearchResults.map(({ item }) => item) - : []; - - if (searchText || tagsHandler.chosenTags.size > 0) { - return [...privateGameTemplateItems, ...exampleShortHeaderItems]; - } - - for (let i = 0; i < exampleShortHeaderItems.length; ++i) { - searchItems.push(exampleShortHeaderItems[i]); - if (i % 2 === 1 && privateGameTemplateItems.length > 0) { - const nextPrivateGameTemplateIndex = Math.floor(i / 2); - if (nextPrivateGameTemplateIndex < privateGameTemplateItems.length) - searchItems.push( - privateGameTemplateItems[nextPrivateGameTemplateIndex] - ); - } - } - - return searchItems; + return getExampleAndTemplateTiles({ + receivedGameTemplates, + privateGameTemplateListingDatas: privateGameTemplateListingDatasSearchResults + ? privateGameTemplateListingDatasSearchResults + .map(({ item }) => item) + .filter( + privateGameTemplateListingData => + !onlyShowGames || gameFilter(privateGameTemplateListingData) + ) + : [], + exampleShortHeaders: exampleShortHeadersSearchResults + ? exampleShortHeadersSearchResults + .map(({ item }) => item) + .filter( + exampleShortHeader => + !onlyShowGames || gameFilter(exampleShortHeader) + ) + : [], + onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => { + sendGameTemplateInformationOpened({ + gameTemplateName: privateGameTemplateListingData.name, + gameTemplateId: privateGameTemplateListingData.id, + source: 'examples-list', + }); + onSelectPrivateGameTemplateListingData( + privateGameTemplateListingData + ); + }, + onSelectExampleShortHeader: exampleShortHeader => { + sendExampleDetailsOpened(exampleShortHeader.slug); + onSelectExampleShortHeader(exampleShortHeader); + }, + i18n, + privateGameTemplatesPeriodicity: 1, + showOwnedGameTemplatesFirst: true, + }).allGridItems; }, [ - exampleShortHeadersSearchResults, + receivedGameTemplates, privateGameTemplateListingDatasSearchResults, - searchText, - tagsHandler, + exampleShortHeadersSearchResults, + onSelectPrivateGameTemplateListingData, + onSelectExampleShortHeader, + i18n, + onlyShowGames, ] ); - const defaultTags = React.useMemo( + const nodesToDisplay: React.Node[] = React.useMemo( () => { - const allDefaultTags = [ - ...(exampleFilters ? exampleFilters.defaultTags : []), - ...(gameTemplateFilters ? gameTemplateFilters.defaultTags : []), - ]; - const uniqueTags = new Set(allDefaultTags); - return Array.from(uniqueTags); + const numberOfTilesToDisplayUntilRowToInsert = rowToInsert + ? rowToInsert.row * columnsCount + : 0; + const firstTiles = resultTiles.slice( + 0, + numberOfTilesToDisplayUntilRowToInsert + ); + const lastTiles = resultTiles.slice( + numberOfTilesToDisplayUntilRowToInsert + ); + return [ + + {firstTiles} + , + rowToInsert ? ( + {rowToInsert.element} + ) : null, + lastTiles.length > 0 ? ( + + {lastTiles} + + ) : null, + ].filter(Boolean); }, - [exampleFilters, gameTemplateFilters] + [columnsCount, rowToInsert, resultTiles] ); return ( - + {}} - tagsHandler={tagsHandler} - tags={defaultTags} ref={searchBarRef} placeholder={t`Search examples`} /> - - { - if (item.authorIds) { - // This is an ExampleShortHeader. - return ( - { - sendExampleDetailsOpened(item.slug); - onSelectExampleShortHeader(item); - }} - onOpen={() => { - onSelectExampleShortHeader(item); - onOpenNewProjectSetupDialog(); - }} - /> - ); - } - if (item.listing) { - // This is a PrivateGameTemplateListingData. - const isTemplateOwned = - !!receivedGameTemplates && - !!receivedGameTemplates.find( - template => template.id === item.id - ); - return ( - { - onSelectPrivateGameTemplateListingData(item); - sendGameTemplateInformationOpened({ - gameTemplateName: item.name, - gameTemplateId: item.id, - source: 'examples-list', - }); - }} - owned={isTemplateOwned} - /> - ); - } - return null; // Should not happen. - }} - /> - + {resultTiles.length === 0 ? ( + + + + + No results returned for your search. Try something else! + + + {rowToInsert && {rowToInsert.element}} + + ) : ( + nodesToDisplay + )} ); }; + +export default ExampleStore; diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js deleted file mode 100644 index 5e6a8ce16902..000000000000 --- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow -import * as React from 'react'; -import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; -import { Trans } from '@lingui/macro'; -import Dialog from '../../UI/Dialog'; -import FlatButton from '../../UI/FlatButton'; -import PrivateGameTemplateInformationPage from './PrivateGameTemplateInformationPage'; - -type Props = {| - privateGameTemplateListingData: PrivateGameTemplateListingData, - privateGameTemplateListingDatasFromSameCreator: ?Array, - onGameTemplateOpen: PrivateGameTemplateListingData => void, - onCreateWithGameTemplate: PrivateGameTemplateListingData => void, - onClose: () => void, -|}; - -const PrivateGameTemplateInformationDialog = ({ - privateGameTemplateListingData, - privateGameTemplateListingDatasFromSameCreator, - onGameTemplateOpen, - onCreateWithGameTemplate, - onClose, -}: Props) => { - return ( - Back} - primary={false} - onClick={onClose} - />, - ]} - open - onRequestClose={onClose} - minHeight="lg" - flexColumnBody - > - - - ); -}; - -export default PrivateGameTemplateInformationDialog; diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js index 07290619e838..47d38ce11e61 100644 --- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js +++ b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js @@ -119,8 +119,9 @@ type Props = {| privateGameTemplateListingDatasFromSameCreator?: ?Array, onGameTemplateOpen: PrivateGameTemplateListingData => void, onAssetPackOpen?: PrivateAssetPackListingData => void, - onCreateWithGameTemplate: PrivateGameTemplateListingData => void, + onCreateWithGameTemplate?: PrivateGameTemplateListingData => void, simulateAppStoreProduct?: boolean, + hideOpenAction?: boolean, |}; const PrivateGameTemplateInformationPage = ({ @@ -130,6 +131,7 @@ const PrivateGameTemplateInformationPage = ({ onAssetPackOpen, onCreateWithGameTemplate, simulateAppStoreProduct, + hideOpenAction, }: Props) => { const { id, name, sellerId } = privateGameTemplateListingData; const { privateGameTemplateListingDatas } = React.useContext( @@ -285,7 +287,7 @@ const PrivateGameTemplateInformationPage = ({ const onClickBuy = React.useCallback( async () => { if (!gameTemplate) return; - if (isAlreadyReceived) { + if (isAlreadyReceived && onCreateWithGameTemplate) { onCreateWithGameTemplate(privateGameTemplateListingData); return; } @@ -329,7 +331,7 @@ const PrivateGameTemplateInformationPage = ({ return; } - if (isAlreadyReceived) { + if (isAlreadyReceived && onCreateWithGameTemplate) { onCreateWithGameTemplate(privateGameTemplateListingData); return; } @@ -412,7 +414,7 @@ const PrivateGameTemplateInformationPage = ({ {errorText} ) : isFetching ? ( - + ) : gameTemplate && sellerPublicProfile ? ( @@ -523,17 +525,7 @@ const PrivateGameTemplateInformationPage = ({ ownedLicense={userGameTemplatePurchaseUsageType} /> - {isAlreadyReceived ? ( - - onCreateWithGameTemplate( - privateGameTemplateListingData - ) - } - label={Open template} - /> - ) : ( + {!isAlreadyReceived ? ( <> {!shouldUseOrSimulateAppStoreProduct && ( @@ -553,7 +545,17 @@ const PrivateGameTemplateInformationPage = ({ /> )} - )} + ) : !hideOpenAction && onCreateWithGameTemplate ? ( + + onCreateWithGameTemplate( + privateGameTemplateListingData + ) + } + label={Open template} + /> + ) : null} diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js deleted file mode 100644 index 6d5785f8c277..000000000000 --- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js +++ /dev/null @@ -1,176 +0,0 @@ -// @flow -import * as React from 'react'; -import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Text from '../../UI/Text'; -import { Trans } from '@lingui/macro'; -import { Column, Line } from '../../UI/Grid'; -import HighlightedText from '../../UI/Search/HighlightedText'; -import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem'; -import { ResponsiveLineStackLayout } from '../../UI/Layout'; -import { iconWithBackgroundStyle } from '../../UI/IconContainer'; -import Lightning from '../../UI/CustomSvgIcons/Lightning'; -import { CorsAwareImage } from '../../UI/CorsAwareImage'; -import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases'; -import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; -import FlatButton from '../../UI/FlatButton'; -import { capitalize } from 'lodash'; -import Chip from '../../UI/Chip'; -import ProductPriceTag from '../ProductPriceTag'; - -const styles = { - container: { - display: 'flex', - overflow: 'hidden', - paddingTop: 8, - paddingBottom: 8, - paddingRight: 8, - }, - button: { - alignItems: 'flex-start', - textAlign: 'left', - flex: 1, - }, - iconBackground: { - flex: 0, - display: 'flex', - justifyContent: 'left', - }, - icon: { - ...iconWithBackgroundStyle, - padding: 1, - aspectRatio: '16 / 9', - }, - priceTagContainer: { - position: 'absolute', - top: 10, - left: 10, - cursor: 'default', - }, - chip: { - marginRight: 2, - marginBottom: 2, - }, -}; - -type Props = {| - privateGameTemplateListingData: PrivateGameTemplateListingData, - matches: ?Array, - isOpening: boolean, - onChoose: () => void, - onHeightComputed: number => void, - owned: boolean, -|}; - -const PrivateGameTemplateListItem = ({ - privateGameTemplateListingData, - matches, - isOpening, - onChoose, - onHeightComputed, - owned, -}: Props) => { - const { isMobile } = useResponsiveWindowSize(); - // Report the height of the item once it's known. - const containerRef = React.useRef(null); - React.useLayoutEffect(() => { - if (containerRef.current) - onHeightComputed(containerRef.current.getBoundingClientRect().height); - }); - - const renderGameTemplateField = (field: 'description' | 'name') => { - const originalField = privateGameTemplateListingData[field]; - - if (!matches) return originalField; - const nameMatches = matches.filter(match => match.key === field); - if (nameMatches.length === 0) return originalField; - - return ( - - ); - }; - - return ( -
- - - - {!!privateGameTemplateListingData.thumbnailUrls.length && ( - - -
- -
-
- )} - - {renderGameTemplateField('name')} - -
- {privateGameTemplateListingData.isSellerGDevelop && ( - } - variant="outlined" - color="secondary" - size="small" - style={styles.chip} - label={Ready-made} - key="premium" - /> - )} - {privateGameTemplateListingData.categories.map(category => ( - - ))} -
-
- - {renderGameTemplateField('description')} - -
-
-
- - - Open} - disabled={isOpening} - onClick={onChoose} - /> - - -
-
- ); -}; - -export default PrivateGameTemplateListItem; diff --git a/newIDE/app/src/AssetStore/ShopTiles.js b/newIDE/app/src/AssetStore/ShopTiles.js index 0a17114db47d..109453983c17 100644 --- a/newIDE/app/src/AssetStore/ShopTiles.js +++ b/newIDE/app/src/AssetStore/ShopTiles.js @@ -28,6 +28,7 @@ import RaisedButton from '../UI/RaisedButton'; import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; import { ResponsiveLineStackLayout } from '../UI/Layout'; import Skeleton from '@material-ui/lab/Skeleton'; +import EmptyMessage from '../UI/EmptyMessage'; const styles = { priceTagContainer: { @@ -110,16 +111,19 @@ const styles = { }, }; -const useStylesForGridListItem = makeStyles(theme => - createStyles({ - tile: { - transition: 'transform 0.3s ease-in-out', - '&:hover': { - transform: 'scale(1.02)', - }, - }, - }) -); +const useStylesForGridListItem = ({ disabled }: { disabled?: boolean }) => + makeStyles(theme => + createStyles({ + tile: !disabled + ? { + transition: 'transform 0.3s ease-in-out', + '&:hover': { + transform: 'scale(1.02)', + }, + } + : {}, + }) + )(); export const AssetCardTile = ({ assetShortHeader, @@ -127,25 +131,29 @@ export const AssetCardTile = ({ size, margin, hideShortDescription, + disabled, }: {| assetShortHeader: AssetShortHeader, onOpenDetails: () => void, size: number, margin?: number, hideShortDescription?: boolean, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onOpenDetails(); } }} - onClick={onOpenDetails} + onClick={!disabled ? onOpenDetails : undefined} style={{ margin, }} @@ -164,24 +172,28 @@ export const AssetFolderTile = ({ tag, onSelect, style, + disabled, }: {| tag: string, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, + 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} > @@ -199,24 +211,28 @@ export const PublicAssetPackTile = ({ assetPack, onSelect, style, + disabled, }: {| assetPack: PublicAssetPack, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, + 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, /** 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} >
@@ -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(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 }) => ( New Project} + title={Create a new game} id="project-pre-creation-dialog" - maxWidth="sm" + maxWidth={shouldUseSmallWidth ? 'sm' : 'md'} actions={[ + Create new game} + onClick={() => onCreateGameClick(i18n)} + id="create-project-button" + />, + ]} + secondaryActions={[ Cancel} onClick={onClose} />, - Create project} - onClick={() => onValidate(i18n)} - id="create-project-button" - />, ]} cannotBeDismissed={isLoading} onRequestClose={onClose} - onApply={() => onValidate(i18n)} + onApply={() => onCreateGameClick(i18n)} + fullHeight={shouldUseFullHeight} + flexColumnBody + forceScrollVisible > - - {isStartingProjectFromScratch && ( - setResolutionOption(key)} - selectedOption={resolutionOption} - disabled={isLoading} - customHeight={customHeight} - customWidth={customWidth} - onCustomHeightChange={setCustomHeight} - onCustomWidthChange={setCustomWidth} - /> - )} - Project name} - endAdornment={ - setProjectName(generateProjectName())} - tooltip={t`Generate random name`} - disabled={isLoading} - > - - - } - autoFocus="desktop" - maxLength={CLOUD_PROJECT_NAME_MAX_LENGTH} - /> - Where to store this project} - value={storageProvider.internalName} - onChange={(e, i, newValue: string) => { - setNewProjectsDefaultStorageProviderName(newValue); - const newStorageProvider = - storageProviders.find( - ({ internalName }) => internalName === newValue - ) || emptyStorageProvider; - setStorageProvider(newStorageProvider); - - // Reset the save as location, to avoid mixing it between storage providers - // and give a chance to the storage provider to set it to a default value. - setSaveAsLocation(null); - }} - > - {storageProviders - // Filter out storage providers who are supposed to be used for storage initially - // (for example: the "URL" storage provider, which is read only, - // or the "DownloadFile" storage provider, which is not a persistent storage). - .filter( - storageProvider => - !!storageProvider.renderNewProjectSaveAsLocationChooser - ) - .map(storageProvider => ( - - ))} - {shouldAllowCreatingProjectWithoutSaving && ( - - )} - - {needUserAuthenticationForStorage && ( - - - - Create an account to store your project online. - - } + + {shouldShowBackButton && ( + <> + + } + label={Back} + onClick={onBack} + disabled={isProjectOpening || isGeneratingProject} /> - + + )} - {!needUserAuthenticationForStorage && - storageProvider.renderNewProjectSaveAsLocationChooser && - storageProvider.renderNewProjectSaveAsLocationChooser({ - projectName, - saveAsLocation, - setSaveAsLocation, - newProjectsDefaultFolder, - })} - {isStartingProjectFromScratch && ( - - - - setGenerationPrompt(text)} - floatingLabelText={AI prompt} - floatingLabelFixed - translatableHintText={ - !authenticatedUser.authenticated || !isOnline - ? t`Log in to enter a prompt` - : t`Type a prompt or generate one` - } - endAdornment={ - setGenerationPrompt(generatePrompt())} - tooltip={t`Generate random prompt`} - disabled={ - isLoading || - !authenticatedUser.authenticated || - !isOnline || - !canGenerateProjectFromPrompt - } - > - - - } - /> - - {authenticatedUser.authenticated && - !canGenerateProjectFromPrompt && ( - - - - - - You've used all your daily pre-made AI scenes! - Generate as many as you want with a subscription. - - - - - - )} - - - - Advanced options - - - - - Optimize for Pixel Art} - onCheck={(e, checked) => { - setOptimizeForPixelArt(checked); + {page === 'home' && ( + + { + onSelectExampleShortHeader(exampleShortHeader); + setPage('details'); + }} + storageProvider={storageProvider} + saveAsLocation={saveAsLocation} + onSelectEmptyProject={() => { + setEmptyProjectSelected(true); + setPage('create'); + }} + disabled={isProjectOpening || isGeneratingProject} + /> + + Remix an existing game + + { + onSelectExampleShortHeader(exampleShortHeader); + setPage('details'); + }} + onSelectPrivateGameTemplateListingData={privateGameTemplateListingData => { + onSelectPrivateGameTemplateListingData( + privateGameTemplateListingData + ); + setPage('details'); + }} + i18n={i18n} + columnsCount={getItemsColumns(windowSize, isLandscape)} + onlyShowGames + rowToInsert={{ + row: 2, + element: ( + { + setGeneratingProjectId(null); + setIsGeneratingProject(false); }} - disabled={isLoading} + generationPrompt={generationPrompt} + onGenerationPromptChange={setGenerationPrompt} /> - Allow players to authenticate in-game - } - onCheck={(e, checked) => { - setAllowPlayersToLogIn(checked); - }} - disabled={isLoading || !isOnline} - tooltipOrHelperText={ - + ), + }} + /> + + )} + {page === 'details' && + (selectedExampleShortHeader ? ( + + ) : selectedPrivateGameTemplateListingData ? ( + + ) : null)} + {page === 'create' && ( + + {emptyProjectSelected && ( + setResolutionOption(key)} + selectedOption={resolutionOption} + disabled={isLoading} + customHeight={customHeight} + customWidth={customWidth} + onCustomHeightChange={setCustomHeight} + onCustomWidthChange={setCustomWidth} + /> + )} + Project name} + endAdornment={ + setProjectName(generateProjectName())} + tooltip={t`Generate random name`} + disabled={isLoading} + > + + + } + autoFocus="desktop" + maxLength={CLOUD_PROJECT_NAME_MAX_LENGTH} + /> + Where to store this project} + value={storageProvider.internalName} + onChange={(e, i, newValue: string) => { + setNewProjectsDefaultStorageProviderName(newValue); + const newStorageProvider = + storageProviders.find( + ({ internalName }) => internalName === newValue + ) || emptyStorageProvider; + setStorageProvider(newStorageProvider); + + // Reset the save as location, to avoid mixing it between storage providers + // and give a chance to the storage provider to set it to a default value. + setSaveAsLocation(null); + }} + > + {storageProviders + // Filter out storage providers who are supposed to be used for storage initially + // (for example: the "URL" storage provider, which is read only, + // or the "DownloadFile" storage provider, which is not a persistent storage). + .filter( + storageProvider => + !!storageProvider.renderNewProjectSaveAsLocationChooser + ) + .map(storageProvider => ( + + ))} + {shouldAllowCreatingProjectWithoutSaving && ( + + )} + + {needUserAuthenticationForStorage && ( + + + + Create an account to store your project online. + } /> - - - + + + )} + {!needUserAuthenticationForStorage && + storageProvider.renderNewProjectSaveAsLocationChooser && + storageProvider.renderNewProjectSaveAsLocationChooser({ + projectName, + saveAsLocation, + setSaveAsLocation, + newProjectsDefaultFolder, + })} + {emptyProjectSelected && ( + Optimize for Pixel Art} + onCheck={(e, checked) => { + setOptimizeForPixelArt(checked); + }} + disabled={isLoading} + /> + )} + {limits && hasTooManyCloudProjects ? ( + + openSubscriptionDialog({ + analyticsMetadata: { + reason: 'Cloud Project limit reached', + }, + }) + } + /> + ) : null} )} - {limits && hasTooManyCloudProjects ? ( - - openSubscriptionDialog({ - analyticsMetadata: { - reason: 'Cloud Project limit reached', - }, - }) - } - /> - ) : null} - - {isGeneratingProject && generatingProjectId && ( - { - setGeneratingProjectId(null); - setIsGeneratingProject(false); - }} - /> - )} + )} 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={[]} + /> + ); };