Allow defining up to 4 genres for a game

This commit is contained in:
Clément Pasteau
2022-03-04 09:35:44 +01:00
committed by GitHub
parent 5d42956669
commit 225884dfe4
10 changed files with 626 additions and 422 deletions

View File

@@ -536,17 +536,23 @@ void Project::UnserializeFrom(const SerializerElement& element) {
authorIds.push_back(authorIdsElement.GetChild(i).GetStringValue());
}
categories.clear();
auto& categoriesElement = propElement.GetChild("categories");
categoriesElement.ConsiderAsArray();
for (std::size_t i = 0; i < categoriesElement.GetChildrenCount(); ++i) {
categories.push_back(categoriesElement.GetChild(i).GetStringValue());
}
auto& playableDevicesElement = propElement.GetChild("playableDevices");
playableDevicesElement.ConsiderAsArray();
for (std::size_t i = 0; i < playableDevicesElement.GetChildrenCount(); ++i) {
const auto& playableDevice = playableDevicesElement.GetChild(i).GetStringValue();
const auto& playableDevice =
playableDevicesElement.GetChild(i).GetStringValue();
if (playableDevice == "keyboard") {
isPlayableWithKeyboard = true;
}
else if (playableDevice == "gamepad") {
} else if (playableDevice == "gamepad") {
isPlayableWithGamepad = true;
}
else if (playableDevice == "mobile") {
} else if (playableDevice == "mobile") {
isPlayableWithMobile = true;
}
}
@@ -751,6 +757,12 @@ void Project::SerializeTo(SerializerElement& element) const {
authorIdsElement.AddChild("").SetStringValue(authorId);
}
auto& categoriesElement = propElement.AddChild("categories");
categoriesElement.ConsiderAsArray();
for (const auto& category : categories) {
categoriesElement.AddChild("").SetStringValue(category);
}
auto& playableDevicesElement = propElement.AddChild("playableDevices");
playableDevicesElement.ConsiderAsArray();
if (isPlayableWithKeyboard) {
@@ -938,6 +950,8 @@ Project& Project::operator=(const Project& other) {
void Project::Init(const gd::Project& game) {
name = game.name;
categories = game.categories;
description = game.description;
firstLayout = game.firstLayout;
version = game.version;
windowWidth = game.windowWidth;

View File

@@ -65,6 +65,16 @@ class GD_CORE_API Project : public ObjectsContainer {
*/
const gd::String& GetName() const { return name; }
/**
* \brief Get the categories/genres of the project.
*/
const std::vector<gd::String>& GetCategories() const { return categories; };
/**
* \brief Get the categories of the project, to modify them (non-const).
*/
std::vector<gd::String>& GetCategories() { return categories; };
/**
* \brief Change the project description
*/
@@ -967,6 +977,8 @@ class GD_CORE_API Project : public ObjectsContainer {
gd::String author; ///< Game author name, for publishing purpose.
std::vector<gd::String>
authorIds; ///< Game author ids, from GDevelop users DB.
std::vector<gd::String>
categories; ///< Game categories
bool isPlayableWithKeyboard; ///< The project is playable with a keyboard.
bool isPlayableWithGamepad; ///< The project is playable with a gamepad.
bool isPlayableWithMobile; ///< The project is playable on a mobile.

View File

@@ -364,6 +364,7 @@ interface Project {
void SetName([Const] DOMString name);
[Const, Ref] DOMString GetName();
[Ref] VectorString GetCategories();
void SetDescription([Const] DOMString description);
[Const, Ref] DOMString GetDescription();
void SetVersion([Const] DOMString authorName);

View File

@@ -3,6 +3,7 @@ declare class gdProject extends gdObjectsContainer {
constructor(): void;
setName(name: string): void;
getName(): string;
getCategories(): gdVectorString;
setDescription(description: string): void;
getDescription(): string;
setVersion(authorName: string): void;

View File

@@ -2,7 +2,7 @@
import { Trans } from '@lingui/macro';
import { t } from '@lingui/macro';
import * as React from 'react';
import { format, formatISO } from 'date-fns';
import { formatISO } from 'date-fns';
import FlatButton from '../UI/FlatButton';
import { Line, Spacer } from '../UI/Grid';
import {
@@ -12,6 +12,7 @@ import {
getPublicGame,
setGameUserAcls,
getAclsFromAuthorIds,
getCategoryName,
} from '../Utils/GDevelopServices/Game';
import Dialog from '../UI/Dialog';
import { Tab, Tabs } from '../UI/Tabs';
@@ -40,6 +41,7 @@ import TextField from '../UI/TextField';
import KeyboardIcon from '@material-ui/icons/Keyboard';
import SportsEsportsIcon from '@material-ui/icons/SportsEsports';
import SmartphoneIcon from '@material-ui/icons/Smartphone';
import { I18n } from '@lingui/react';
const styles = {
tableRowStatColumn: {
@@ -155,6 +157,7 @@ export const GameDetailsDialog = ({
const updatedGame = await updateGame(getAuthorizationHeader, id, gameId, {
authorName: project.getAuthor() || 'Unspecified publisher',
gameName: project.getName() || 'Untitle game',
categories: project.getCategories().toJSArray() || [],
description: project.getDescription() || '',
playWithKeyboard: project.isPlayableWithKeyboard(),
playWithGamepad: project.isPlayableWithGamepad(),
@@ -216,352 +219,404 @@ export const GameDetailsDialog = ({
!!project && project.getProjectUuid() === game.id;
return (
<Dialog
title={
<span>
{game.gameName}
{' - '}
<Trans>Dashboard</Trans>
</span>
}
open
noMargin
onRequestClose={onClose}
maxWidth="md"
actions={[
<FlatButton
label={<Trans>Close</Trans>}
onClick={onClose}
key="close"
/>,
]}
secondaryActions={[
<HelpButton key="help" helpPagePath="/interface/games-dashboard" />,
]}
>
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tab label={<Trans>Details</Trans>} value="details" />
<Tab label={<Trans>Builds</Trans>} value="builds" />
<Tab label={<Trans>Analytics</Trans>} value="analytics" />
</Tabs>
<Line>
{currentTab === 'details' ? (
publicGameError ? (
<PlaceholderError onRetry={loadPublicGame}>
<Trans>There was an issue getting the game details.</Trans>{' '}
<Trans>Verify your internet connection or try again later.</Trans>
</PlaceholderError>
) : !publicGame ? (
<PlaceholderLoader />
) : (
<ColumnStackLayout expand>
{!isGameOpenedAsProject && (
<AlertMessage kind="info">
<I18n>
{({ i18n }) => (
<Dialog
title={
<span>
{game.gameName}
{' - '}
<Trans>Dashboard</Trans>
</span>
}
open
noMargin
onRequestClose={onClose}
maxWidth="md"
actions={[
<FlatButton
label={<Trans>Close</Trans>}
onClick={onClose}
key="close"
/>,
]}
secondaryActions={[
<HelpButton key="help" helpPagePath="/interface/games-dashboard" />,
]}
>
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tab label={<Trans>Details</Trans>} value="details" />
<Tab label={<Trans>Builds</Trans>} value="builds" />
<Tab label={<Trans>Analytics</Trans>} value="analytics" />
</Tabs>
<Line>
{currentTab === 'details' ? (
publicGameError ? (
<PlaceholderError onRetry={loadPublicGame}>
<Trans>There was an issue getting the game details.</Trans>{' '}
<Trans>
In order to update these details you have to open the game's
project.
Verify your internet connection or try again later.
</Trans>
</AlertMessage>
)}
<Line alignItems="center">
<Line expand justifyContent="flex-start" alignItems="center">
{authorUsernames && (
<>
<Text>
<Trans>Authors:</Trans>
</Text>
<Line>
{authorUsernames.map((username, index) => (
<React.Fragment key={username}>
<Spacer />
<Chip
size="small"
label={username}
color={index === 0 ? 'primary' : 'default'}
/>
</React.Fragment>
))}
</Line>
</>
</PlaceholderError>
) : !publicGame ? (
<PlaceholderLoader />
) : (
<ColumnStackLayout expand>
{!isGameOpenedAsProject && (
<AlertMessage kind="info">
<Trans>
In order to update these details you have to open the
game's project.
</Trans>
</AlertMessage>
)}
</Line>
<Line expand justifyContent="flex-end">
<Text>
<Trans>
Created on{' '}
{format(game.createdAt * 1000 /* TODO */, 'yyyy-MM-dd')}
</Trans>
</Text>
</Line>
</Line>
{(publicGame.playWithKeyboard ||
publicGame.playWithGamepad ||
publicGame.playWithMobile) && (
<Line expand justifyContent="flex-start" alignItems="center">
{publicGame.playWithKeyboard && <KeyboardIcon />}
{publicGame.playWithGamepad && <SportsEsportsIcon />}
{publicGame.playWithMobile && <SmartphoneIcon />}
</Line>
)}
<TextField
value={publicGame.gameName}
readOnly
fullWidth
floatingLabelText={<Trans>Game name</Trans>}
floatingLabelFixed={true}
/>
<TextField
value={publicGame.description || ''}
readOnly
fullWidth
floatingLabelText={<Trans>Game description</Trans>}
floatingLabelFixed={true}
hintText={t`No description set.`}
multiline
rows={5}
/>
<SelectField
disabled
fullWidth
floatingLabelText={
<Trans>Device orientation (for mobile)</Trans>
}
value={publicGame.orientation}
>
<SelectOption
value="default"
primaryText={t`Platform default`}
/>
<SelectOption value="landscape" primaryText={t`Landscape`} />
<SelectOption value="portrait" primaryText={t`Portrait`} />
</SelectField>
<Line noMargin justifyContent="flex-end">
<FlatButton
onClick={() => {
const answer = Window.showConfirmDialog(
"Are you sure you want to unregister this game? \n\nIt will disappear from your games dashboard and you won't get access to analytics, unless you register it again."
);
if (!answer) return;
unregisterGame();
}}
label={<Trans>Unregister this game</Trans>}
/>
<Spacer />
{publicGame.publicWebBuildId && (
<>
<RaisedButton
<Line alignItems="center" noMargin>
<Line
expand
justifyContent="flex-start"
alignItems="center"
noMargin
>
{authorUsernames && (
<>
<Text>
<Trans>Authors:</Trans>
</Text>
<Line noMargin>
{authorUsernames.map((username, index) => (
<React.Fragment key={username}>
<Spacer />
<Chip
size="small"
label={username}
color={index === 0 ? 'primary' : 'default'}
/>
</React.Fragment>
))}
</Line>
</>
)}
</Line>
<Line expand justifyContent="flex-end" noMargin>
<Text>
<Trans>
Created on {i18n.date(game.createdAt * 1000)}
</Trans>
</Text>
</Line>
</Line>
{(publicGame.playWithKeyboard ||
publicGame.playWithGamepad ||
publicGame.playWithMobile ||
publicGame.categories) && (
<Line alignItems="center" noMargin>
<Line
expand
justifyContent="flex-start"
alignItems="center"
noMargin
>
{publicGame.categories &&
!!publicGame.categories.length && (
<>
<Text>
<Trans>Genres:</Trans>
</Text>
<Line noMargin>
{publicGame.categories.map(
(category, index) => (
<React.Fragment key={category}>
<Spacer />
<Chip
size="small"
label={getCategoryName(category, i18n)}
color={
index === 0 ? 'primary' : 'default'
}
/>
</React.Fragment>
)
)}
</Line>
</>
)}
</Line>
<Line expand justifyContent="flex-end" noMargin>
{publicGame.playWithKeyboard && <KeyboardIcon />}
{publicGame.playWithGamepad && <SportsEsportsIcon />}
{publicGame.playWithMobile && <SmartphoneIcon />}
</Line>
</Line>
)}
<TextField
value={publicGame.gameName}
readOnly
fullWidth
floatingLabelText={<Trans>Game name</Trans>}
floatingLabelFixed={true}
/>
<TextField
value={publicGame.description || ''}
readOnly
fullWidth
floatingLabelText={<Trans>Game description</Trans>}
floatingLabelFixed={true}
hintText={t`No description set.`}
multiline
rows={5}
/>
<SelectField
disabled
fullWidth
floatingLabelText={
<Trans>Device orientation (for mobile)</Trans>
}
value={publicGame.orientation}
>
<SelectOption
value="default"
primaryText={t`Platform default`}
/>
<SelectOption
value="landscape"
primaryText={t`Landscape`}
/>
<SelectOption value="portrait" primaryText={t`Portrait`} />
</SelectField>
<Line noMargin justifyContent="flex-end">
<FlatButton
onClick={() => {
const answer = Window.showConfirmDialog(
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo unique game URL not accessible anymore. \n\nYou can decide anytime to publish it again.'
"Are you sure you want to unregister this game? \n\nIt will disappear from your games dashboard and you won't get access to analytics, unless you register it again."
);
if (!answer) return;
unpublishGame();
unregisterGame();
}}
label={<Trans>Unpublish from Liluo</Trans>}
label={<Trans>Unregister this game</Trans>}
/>
<Spacer />
</>
)}
<RaisedButton
primary
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
label={<Trans>Edit game details</Trans>}
disabled={!isGameOpenedAsProject}
/>
</Line>
</ColumnStackLayout>
)
) : null}
{currentTab === 'builds' ? (
<Builds
game={game}
authenticatedUser={authenticatedUser}
onGameUpdated={onGameUpdated}
/>
) : null}
{currentTab === 'analytics' ? (
gameRollingMetricsError ? (
<PlaceholderError
onRetry={() => {
loadGameMetrics();
}}
>
<Trans>There was an issue getting the game analytics.</Trans>{' '}
<Trans>Verify your internet connection or try again later.</Trans>
</PlaceholderError>
) : (
<ColumnStackLayout expand>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Consolidated metrics</Trans>
</Text>
<Spacer />
{!publicGame && <CircularProgress size={20} />}
</Line>
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Last week sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame &&
publicGame.metrics &&
publicGame.metrics.lastWeekSessionsCount
? publicGame.metrics.lastWeekSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Last year sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame &&
publicGame.metrics &&
publicGame.metrics.lastYearSessionsCount
? publicGame.metrics.lastYearSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Daily metrics</Trans>
</Text>
<Spacer />
{isGameMetricsLoading && <CircularProgress size={20} />}
</Line>
<Line noMargin>
<SelectField
fullWidth
floatingLabelText={<Trans>Day</Trans>}
value={analyticsDate}
onChange={(_, _index, newIsoDate) => {
setAnalyticsDate(newIsoDate);
{publicGame.publicWebBuildId && (
<>
<RaisedButton
onClick={() => {
const answer = Window.showConfirmDialog(
'Are you sure you want to unpublish this game? \n\nThis will make your Liluo unique game URL not accessible anymore. \n\nYou can decide anytime to publish it again.'
);
if (!answer) return;
unpublishGame();
}}
label={<Trans>Unpublish from Liluo</Trans>}
/>
<Spacer />
</>
)}
<RaisedButton
primary
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
label={<Trans>Edit game details</Trans>}
disabled={!isGameOpenedAsProject}
/>
</Line>
</ColumnStackLayout>
)
) : null}
{currentTab === 'builds' ? (
<Builds
game={game}
authenticatedUser={authenticatedUser}
onGameUpdated={onGameUpdated}
/>
) : null}
{currentTab === 'analytics' ? (
gameRollingMetricsError ? (
<PlaceholderError
onRetry={() => {
loadGameMetrics();
}}
>
{Array(5)
.fill('')
.map((_, index) => {
const isoDate = formatISO(
subDays(new Date(), index + 2),
{
representation: 'date',
}
);
return (
<SelectOption
key={isoDate}
value={isoDate}
primaryText={isoDate}
/>
);
})
.reverse()}
<SelectOption
value={yesterdayIsoDate}
primaryText={t`Yesterday`}
/>
<SelectOption
value={formatISO(new Date(), { representation: 'date' })}
primaryText={t`Today (so far, in real time)`}
/>
</SelectField>
</Line>
{!isGameMetricsLoading && !gameRollingMetrics ? (
<AlertMessage kind="warning">
<Trans>There was an issue getting the game analytics.</Trans>{' '}
<Trans>
There were no players or stored metrics for this day. Be
sure to publish your game and get players to try it to see
the collected anonymous analytics.
Verify your internet connection or try again later.
</Trans>
</AlertMessage>
) : null}
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0Players
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.sessions
? gameRollingMetrics.sessions.d0Sessions
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>New players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0NewPlayers
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
{gameRollingMetrics &&
(!gameRollingMetrics.retention || !gameRollingMetrics.players) ? (
<AlertMessage kind="info">
Upgrade your account with a subscription to unlock all the
metrics for your game.
</AlertMessage>
) : null}
<Table>
<TableBody>
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
<TableRow key={dayIndex}>
<TableRowColumn>
<Trans>Day {dayIndex} retained players</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics &&
gameRollingMetrics.retention &&
gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
] != null
? gameRollingMetrics.retention[
</PlaceholderError>
) : (
<ColumnStackLayout expand>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Consolidated metrics</Trans>
</Text>
<Spacer />
{!publicGame && <CircularProgress size={20} />}
</Line>
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Last week sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame &&
publicGame.metrics &&
publicGame.metrics.lastWeekSessionsCount
? publicGame.metrics.lastWeekSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Last year sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame &&
publicGame.metrics &&
publicGame.metrics.lastYearSessionsCount
? publicGame.metrics.lastYearSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Daily metrics</Trans>
</Text>
<Spacer />
{isGameMetricsLoading && <CircularProgress size={20} />}
</Line>
<Line noMargin>
<SelectField
fullWidth
floatingLabelText={<Trans>Day</Trans>}
value={analyticsDate}
onChange={(_, _index, newIsoDate) => {
setAnalyticsDate(newIsoDate);
}}
>
{Array(5)
.fill('')
.map((_, index) => {
const isoDate = formatISO(
subDays(new Date(), index + 2),
{
representation: 'date',
}
);
return (
<SelectOption
key={isoDate}
value={isoDate}
primaryText={isoDate}
/>
);
})
.reverse()}
<SelectOption
value={yesterdayIsoDate}
primaryText={t`Yesterday`}
/>
<SelectOption
value={formatISO(new Date(), {
representation: 'date',
})}
primaryText={t`Today (so far, in real time)`}
/>
</SelectField>
</Line>
{!isGameMetricsLoading && !gameRollingMetrics ? (
<AlertMessage kind="warning">
<Trans>
There were no players or stored metrics for this day. Be
sure to publish your game and get players to try it to
see the collected anonymous analytics.
</Trans>
</AlertMessage>
) : null}
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0Players
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.sessions
? gameRollingMetrics.sessions.d0Sessions
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>New players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0NewPlayers
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
{gameRollingMetrics &&
(!gameRollingMetrics.retention ||
!gameRollingMetrics.players) ? (
<AlertMessage kind="info">
Upgrade your account with a subscription to unlock all the
metrics for your game.
</AlertMessage>
) : null}
<Table>
<TableBody>
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
<TableRow key={dayIndex}>
<TableRowColumn>
<Trans>Day {dayIndex} retained players</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics &&
gameRollingMetrics.retention &&
gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
]
: '-'}
</TableRowColumn>
</TableRow>
))}
</TableBody>
</Table>
</ColumnStackLayout>
)
) : null}
</Line>
{publicGame && project && (
<PublicGamePropertiesDialog
open={isPublicGamePropertiesDialogOpen}
project={project}
game={publicGame}
onApply={() => {
setIsPublicGamePropertiesDialogOpen(false);
updateGameFromProject();
}}
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
/>
] != null
? gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
]
: '-'}
</TableRowColumn>
</TableRow>
))}
</TableBody>
</Table>
</ColumnStackLayout>
)
) : null}
</Line>
{publicGame && project && (
<PublicGamePropertiesDialog
open={isPublicGamePropertiesDialogOpen}
project={project}
publicGame={publicGame}
onApply={() => {
setIsPublicGamePropertiesDialogOpen(false);
updateGameFromProject();
}}
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
/>
)}
</Dialog>
)}
</Dialog>
</I18n>
);
};

View File

@@ -8,11 +8,19 @@ import Checkbox from '../UI/Checkbox';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import { t } from '@lingui/macro';
import SemiControlledMultiAutoComplete from '../UI/SemiControlledMultiAutoComplete';
import {
allGameCategories,
getCategoryName,
} from '../Utils/GDevelopServices/Game';
import { I18n } from '@lingui/react';
type Props = {|
project: gdProject,
setName: string => void,
name: string,
setCategories?: (string[]) => void,
categories?: string[],
setDescription: string => void,
description: ?string,
setAuthorIds: (string[]) => void,
@@ -31,6 +39,8 @@ function PublicGameProperties({
project,
setName,
name,
categories,
setCategories,
setDescription,
description,
setAuthorIds,
@@ -44,72 +54,113 @@ function PublicGameProperties({
setOrientation,
orientation,
}: Props) {
const [categoryInput, setCategoryInput] = React.useState('');
return (
<ColumnStackLayout noMargin>
<SemiControlledTextField
floatingLabelText={<Trans>Game name</Trans>}
fullWidth
type="text"
value={name}
onChange={setName}
autoFocus
/>
<SemiControlledTextField
floatingLabelText={<Trans>Game description</Trans>}
fullWidth
type="text"
value={description || ''}
onChange={setDescription}
autoFocus
multiline
rows={5}
/>
<UsersAutocomplete
userIds={authorIds}
onChange={setAuthorIds}
floatingLabelText={<Trans>Authors</Trans>}
helperText={
<Trans>
Select the usernames of the authors of this project. They will be
displayed in the selected order, if you publish this game as an
example or in the community.
</Trans>
}
/>
<SelectField
fullWidth
floatingLabelText={<Trans>Device orientation (for mobile)</Trans>}
value={orientation}
onChange={(e, i, value: string) => setOrientation(value)}
>
<SelectOption value="default" primaryText={t`Platform default`} />
<SelectOption value="landscape" primaryText={t`Landscape`} />
<SelectOption value="portrait" primaryText={t`Portrait`} />
</SelectField>
{// This view is used for public game properties as well as project properties.
// The following properties are not shown in project properties.
setPlayableWithKeyboard &&
setPlayableWithGamepad &&
setPlayableWithMobile && (
<React.Fragment>
<Checkbox
label={<Trans>Playable with a keyboard</Trans>}
checked={!!playWithKeyboard}
onCheck={(e, checked) => setPlayableWithKeyboard(checked)}
<I18n>
{({ i18n }) => (
<ColumnStackLayout noMargin>
<SemiControlledTextField
floatingLabelText={<Trans>Game name</Trans>}
fullWidth
type="text"
value={name}
onChange={setName}
autoFocus
/>
{setCategories && (
<SemiControlledMultiAutoComplete
hintText={t`Select a genre`}
floatingLabelText={<Trans>Genres</Trans>}
helperText={
<Trans>
Select up to 4 genres, the first one will define the game's
main genre
</Trans>
}
value={
categories
? categories.map(category => ({
value: category,
text: getCategoryName(category, i18n),
}))
: []
}
onChange={(event, values) => {
setCategories(
values ? values.map(category => category.value) : []
);
}}
inputValue={categoryInput}
onInputChange={(event, value) => {
setCategoryInput(value);
}}
dataSource={allGameCategories.map(category => ({
value: category,
text: getCategoryName(category, i18n),
}))}
fullWidth
optionsLimit={4}
/>
<Checkbox
label={<Trans>Playable with a gamepad</Trans>}
checked={!!playWithGamepad}
onCheck={(e, checked) => setPlayableWithGamepad(checked)}
/>
<Checkbox
label={<Trans>Playable on mobile</Trans>}
checked={!!playWithMobile}
onCheck={(e, checked) => setPlayableWithMobile(checked)}
/>
</React.Fragment>
)}
</ColumnStackLayout>
)}
<SemiControlledTextField
floatingLabelText={<Trans>Game description</Trans>}
fullWidth
type="text"
value={description || ''}
onChange={setDescription}
autoFocus
multiline
rows={5}
/>
<UsersAutocomplete
userIds={authorIds}
onChange={setAuthorIds}
floatingLabelText={<Trans>Authors</Trans>}
helperText={
<Trans>
Select the usernames of the authors of this project. They will
be displayed in the selected order, if you publish this game as
an example or in the community.
</Trans>
}
/>
<SelectField
fullWidth
floatingLabelText={<Trans>Device orientation (for mobile)</Trans>}
value={orientation}
onChange={(e, i, value: string) => setOrientation(value)}
>
<SelectOption value="default" primaryText={t`Platform default`} />
<SelectOption value="landscape" primaryText={t`Landscape`} />
<SelectOption value="portrait" primaryText={t`Portrait`} />
</SelectField>
{// This view is used for public game properties as well as project properties.
// The following properties are not shown in project properties.
setPlayableWithKeyboard &&
setPlayableWithGamepad &&
setPlayableWithMobile && (
<React.Fragment>
<Checkbox
label={<Trans>Playable with a keyboard</Trans>}
checked={!!playWithKeyboard}
onCheck={(e, checked) => setPlayableWithKeyboard(checked)}
/>
<Checkbox
label={<Trans>Playable with a gamepad</Trans>}
checked={!!playWithGamepad}
onCheck={(e, checked) => setPlayableWithGamepad(checked)}
/>
<Checkbox
label={<Trans>Playable on mobile</Trans>}
checked={!!playWithMobile}
onCheck={(e, checked) => setPlayableWithMobile(checked)}
/>
</React.Fragment>
)}
</ColumnStackLayout>
)}
</I18n>
);
}

View File

@@ -17,6 +17,7 @@ import { type PublicGame } from '../Utils/GDevelopServices/Game';
*/
type PublicProjectProperties = {|
name: string,
categories: string[],
description: string,
authorIds: string[],
playWithKeyboard: boolean,
@@ -30,8 +31,11 @@ function applyPublicPropertiesToProject(
newProperties: PublicProjectProperties
) {
const t = str => str; //TODO
const { name, authorIds, description } = newProperties;
const { name, authorIds, description, categories } = newProperties;
project.setName(name);
const projectCategories = project.getCategories();
projectCategories.clear();
categories.forEach(category => projectCategories.push_back(category));
project.setDescription(description);
const projectAuthorIds = project.getAuthorIds();
projectAuthorIds.clear();
@@ -46,7 +50,7 @@ function applyPublicPropertiesToProject(
type Props = {|
project: gdProject,
game: PublicGame,
publicGame: PublicGame,
open: boolean,
onClose: () => void,
onApply: () => void,
@@ -54,29 +58,30 @@ type Props = {|
const PublicGamePropertiesDialog = ({
project,
game,
publicGame,
open,
onClose,
onApply,
}: Props) => {
const publicGameAuthorIds = game.authors
const publicGameAuthorIds = publicGame.authors
.map(author => (author ? author.id : null))
.filter(Boolean);
const [name, setName] = React.useState(game.gameName);
const [description, setDescription] = React.useState(game.description);
const [name, setName] = React.useState(publicGame.gameName);
const [categories, setCategories] = React.useState(publicGame.categories);
const [description, setDescription] = React.useState(publicGame.description);
const [authorIds, setAuthorIds] = React.useState<string[]>(
publicGameAuthorIds
);
const [playWithKeyboard, setPlayableWithKeyboard] = React.useState(
game.playWithKeyboard
publicGame.playWithKeyboard
);
const [playWithGamepad, setPlayableWithGamepad] = React.useState(
game.playWithGamepad
publicGame.playWithGamepad
);
const [playWithMobile, setPlayableWithMobile] = React.useState(
game.playWithMobile
publicGame.playWithMobile
);
const [orientation, setOrientation] = React.useState(game.orientation);
const [orientation, setOrientation] = React.useState(publicGame.orientation);
if (!open) return null;
@@ -84,6 +89,7 @@ const PublicGamePropertiesDialog = ({
if (
applyPublicPropertiesToProject(project, {
name,
categories: categories || [],
description: description || '',
authorIds,
playWithKeyboard: !!playWithKeyboard,
@@ -121,6 +127,8 @@ const PublicGamePropertiesDialog = ({
<PublicGameProperties
name={name}
setName={setName}
categories={categories}
setCategories={setCategories}
description={description}
setDescription={setDescription}
project={project}

View File

@@ -5,12 +5,12 @@ import TextField from '@material-ui/core/TextField';
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import Autocomplete from '@material-ui/lab/Autocomplete';
type Option = {|
export type AutocompleteOption = {|
text: string, // The text displayed
value: string, // The internal value selected
|};
export type DataSource = Array<?Option>;
export type DataSource = Array<?AutocompleteOption>;
const styles = {
chip: {
@@ -20,8 +20,8 @@ const styles = {
};
type Props = {|
value: Array<Option>,
onChange: Option => void,
value: Array<AutocompleteOption>,
onChange: AutocompleteOption => void,
dataSource: DataSource,
inputValue: ?string,
onInputChange: string => void,
@@ -31,7 +31,8 @@ type Props = {|
helperText?: React.Node,
fullWidth?: boolean,
error?: ?string,
loading: boolean,
loading?: boolean,
optionsLimit?: number, // Allow limiting the number of options by disabling the autocomplete.
|};
export default function SemiControlledMultiAutoComplete(props: Props) {
@@ -45,11 +46,12 @@ export default function SemiControlledMultiAutoComplete(props: Props) {
inputValue={props.inputValue}
onInputChange={props.onInputChange}
options={props.dataSource}
getOptionLabel={(option: Option) => option.text}
getOptionDisabled={(option: Option) =>
getOptionLabel={(option: AutocompleteOption) => option.text}
getOptionDisabled={(option: AutocompleteOption) =>
!!props.value.find(
element => element && element.value === option.value
)
) ||
(props.optionsLimit && props.value.length >= props.optionsLimit)
}
loading={props.loading}
renderInput={params => (

View File

@@ -1,8 +1,10 @@
// @flow
import axios from 'axios';
import { type I18n as I18nType } from '@lingui/core';
import { GDevelopGameApi, GDevelopGamesPlatform } from './ApiConfigs';
import { type Filters } from './Filters';
import { type UserPublicProfile } from './User';
import { t } from '@lingui/macro';
export type PublicGame = {
id: string,
@@ -19,6 +21,7 @@ export type PublicGame = {
lastWeekSessionsCount: number,
lastYearSessionsCount: number,
},
categories?: string[],
};
export type Game = {
@@ -63,6 +66,64 @@ export type AllShowcasedGames = {
filters: Filters,
};
export const allGameCategories = [
'action',
'adventure',
'shooter',
'platformer',
'rpg',
'horror',
'strategy',
'puzzle',
'story-rich',
'survival',
'racing',
'building',
'simulation',
'sport',
'multiplayer',
'leaderboard',
];
export const getCategoryName = (category: string, i18n: I18nType) => {
switch (category) {
case 'action':
return i18n._(t`Action`);
case 'adventure':
return i18n._(t`Adventure`);
case 'shooter':
return i18n._(t`Shooter`);
case 'platformer':
return i18n._(t`Platformer`);
case 'rpg':
return i18n._(t`RPG`);
case 'horror':
return i18n._(t`Horror`);
case 'strategy':
return i18n._(t`Strategy`);
case 'puzzle':
return i18n._(t`Puzzle`);
case 'racing':
return i18n._(t`Racing`);
case 'simulation':
return i18n._(t`Simulation`);
case 'sport':
return i18n._(t`Sport`);
case 'story-rich':
return i18n._(t`Story-Rich`);
case 'survival':
return i18n._(t`Survival`);
case 'building':
return i18n._(t`Building`);
case 'multiplayer':
return i18n._(t`Multiplayer`);
case 'leaderboard':
return i18n._(t`Leaderboard`);
default:
return category;
}
};
export const getGameUrl = (game: ?Game) => {
if (!game) return null;
return GDevelopGamesPlatform.getGameUrl(game.id);
@@ -135,6 +196,7 @@ export const updateGame = (
gameId: string,
{
gameName,
categories,
authorName,
publicWebBuildId,
description,
@@ -144,6 +206,7 @@ export const updateGame = (
orientation,
}: {|
gameName?: string,
categories?: string[],
authorName?: string,
publicWebBuildId?: ?string,
description?: string,
@@ -159,6 +222,7 @@ export const updateGame = (
`${GDevelopGameApi.baseUrl}/game/${gameId}`,
{
gameName,
categories,
authorName,
publicWebBuildId,
description,

View File

@@ -10,6 +10,7 @@ import {
type UserPublicProfileSearch,
getUserPublicProfilesByIds,
} from './GDevelopServices/User';
import { type AutocompleteOption } from '../UI/SemiControlledMultiAutoComplete';
import useForceUpdate from './UseForceUpdate';
@@ -20,11 +21,6 @@ type Props = {|
helperText: React.Node,
|};
export type AutocompleteOption = {|
text: string,
value: string,
|};
const getErrorMessage = (error: ?Error) => {
if (error) return 'Error while loading users';
};