diff --git a/newIDE/app/src/AssetStore/ExampleStore/index.js b/newIDE/app/src/AssetStore/ExampleStore/index.js
index de57ec7d9d1b..c346b52c60e7 100644
--- a/newIDE/app/src/AssetStore/ExampleStore/index.js
+++ b/newIDE/app/src/AssetStore/ExampleStore/index.js
@@ -1,105 +1,105 @@
// @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/GridList';
+import { getExampleAndTemplateTiles } from '../../MainFrame/EditorContainers/HomePage/BuildSection/utils';
+import BackgroundText from '../../UI/BackgroundText';
+import { ColumnStackLayout } from '../../UI/Layout';
+
+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 +120,129 @@ 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 (
-
- );
-};
-
-export default PrivateGameTemplateInformationDialog;
diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js
index 07290619e838..2cf446fa3c8c 100644
--- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js
+++ b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js
@@ -119,7 +119,7 @@ type Props = {|
privateGameTemplateListingDatasFromSameCreator?: ?Array,
onGameTemplateOpen: PrivateGameTemplateListingData => void,
onAssetPackOpen?: PrivateAssetPackListingData => void,
- onCreateWithGameTemplate: PrivateGameTemplateListingData => void,
+ onCreateWithGameTemplate?: PrivateGameTemplateListingData => void,
simulateAppStoreProduct?: boolean,
|};
@@ -285,7 +285,7 @@ const PrivateGameTemplateInformationPage = ({
const onClickBuy = React.useCallback(
async () => {
if (!gameTemplate) return;
- if (isAlreadyReceived) {
+ if (isAlreadyReceived && onCreateWithGameTemplate) {
onCreateWithGameTemplate(privateGameTemplateListingData);
return;
}
@@ -329,7 +329,7 @@ const PrivateGameTemplateInformationPage = ({
return;
}
- if (isAlreadyReceived) {
+ if (isAlreadyReceived && onCreateWithGameTemplate) {
onCreateWithGameTemplate(privateGameTemplateListingData);
return;
}
@@ -412,7 +412,7 @@ const PrivateGameTemplateInformationPage = ({
{errorText}
) : isFetching ? (
-
+
) : gameTemplate && sellerPublicProfile ? (
@@ -523,17 +523,7 @@ const PrivateGameTemplateInformationPage = ({
ownedLicense={userGameTemplatePurchaseUsageType}
/>
- {isAlreadyReceived ? (
-
- onCreateWithGameTemplate(
- privateGameTemplateListingData
- )
- }
- label={Open template}
- />
- ) : (
+ {!isAlreadyReceived ? (
<>
{!shouldUseOrSimulateAppStoreProduct && (
@@ -553,7 +543,17 @@ const PrivateGameTemplateInformationPage = ({
/>
)}
>
- )}
+ ) : 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