mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
4 Commits
a05b0cd0ef
...
fix/slug-v
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e93e65e7e1 | ||
![]() |
b67e7035bc | ||
![]() |
b0f824f889 | ||
![]() |
ee7065d52c |
@@ -38,7 +38,10 @@ import {
|
||||
getGame,
|
||||
getGameUrl,
|
||||
updateGame,
|
||||
setGameSlug,
|
||||
getGameSlugs,
|
||||
type Game,
|
||||
type GameSlug,
|
||||
} from '../../../Utils/GDevelopServices/Game';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
import AlertMessage from '../../../UI/AlertMessage';
|
||||
@@ -78,6 +81,7 @@ const OnlineGameLink = ({
|
||||
setIsOnlineGamePropertiesDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
const [game, setGame] = React.useState<?Game>(null);
|
||||
const [slug, setSlug] = React.useState<?GameSlug>(null);
|
||||
const [isGameLoading, setIsGameLoading] = React.useState<boolean>(false);
|
||||
const { getAuthorizationHeader, profile } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
@@ -86,7 +90,7 @@ const OnlineGameLink = ({
|
||||
const exportPending = !errored && exportStep !== '' && exportStep !== 'done';
|
||||
const isBuildComplete = build && build.status === 'complete';
|
||||
const isBuildPublished = build && game && build.id === game.publicWebBuildId;
|
||||
const gameUrl = getGameUrl(game);
|
||||
const gameUrl = getGameUrl(game, slug);
|
||||
const buildUrl =
|
||||
exportPending || !isBuildComplete
|
||||
? null
|
||||
@@ -102,8 +106,16 @@ const OnlineGameLink = ({
|
||||
const { id } = profile;
|
||||
try {
|
||||
setIsGameLoading(true);
|
||||
const game = await getGame(getAuthorizationHeader, id, gameId);
|
||||
const [game, slugs] = await Promise.all([
|
||||
getGame(getAuthorizationHeader, id, gameId),
|
||||
getGameSlugs(getAuthorizationHeader, id, gameId).catch(err => {
|
||||
console.error('Unable to get the game slug', err);
|
||||
}),
|
||||
]);
|
||||
setGame(game);
|
||||
if (slugs && slugs.length > 0) {
|
||||
setSlug(slugs[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Unable to load the game', err);
|
||||
} finally {
|
||||
@@ -189,6 +201,35 @@ const OnlineGameLink = ({
|
||||
}
|
||||
);
|
||||
setGame(updatedGame);
|
||||
const { userSlug, gameSlug } = partialGameChange;
|
||||
if (userSlug && gameSlug && userSlug === profile.username) {
|
||||
try {
|
||||
await setGameSlug(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
userSlug,
|
||||
gameSlug
|
||||
);
|
||||
setSlug({ username: userSlug, gameSlug: gameSlug, createdAt: 0 });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Unable to update the game slug:',
|
||||
error.response || error.message
|
||||
);
|
||||
showErrorBox({
|
||||
message:
|
||||
i18n._(
|
||||
t`Unable to update the game slug. A slug must be 6 to 30 characters long and only contains letters, digits or dashes.`
|
||||
) +
|
||||
' ' +
|
||||
i18n._(t`Verify your internet connection or try again later.`),
|
||||
rawError: error,
|
||||
errorId: 'game-slug-update-error',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorBox({
|
||||
message: i18n._(
|
||||
@@ -377,6 +418,7 @@ const OnlineGameLink = ({
|
||||
}
|
||||
}}
|
||||
game={game}
|
||||
slug={slug}
|
||||
isLoading={isGameLoading}
|
||||
/>
|
||||
)}
|
||||
|
@@ -2,22 +2,27 @@
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { type Game } from '../../../Utils/GDevelopServices/Game';
|
||||
import { type Game, type GameSlug } from '../../../Utils/GDevelopServices/Game';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import Dialog from '../../../UI/Dialog';
|
||||
import PublicGameProperties from '../../../GameDashboard/PublicGameProperties';
|
||||
import {
|
||||
cleanUpGameSlug,
|
||||
PublicGameProperties,
|
||||
} from '../../../GameDashboard/PublicGameProperties';
|
||||
import {
|
||||
applyPublicPropertiesToProject,
|
||||
type PartialGameChange,
|
||||
} from '../../../GameDashboard/PublicGamePropertiesDialog';
|
||||
import { getWebBuildThumbnailUrl } from '../../../Utils/GDevelopServices/Build';
|
||||
import RaisedButtonWithSplitMenu from '../../../UI/RaisedButtonWithSplitMenu';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
onSaveProject: () => Promise<void>,
|
||||
buildId: string,
|
||||
game: Game,
|
||||
slug: ?GameSlug,
|
||||
onClose: () => void,
|
||||
onApply: PartialGameChange => Promise<void>,
|
||||
isLoading: boolean,
|
||||
@@ -28,10 +33,13 @@ export const OnlineGamePropertiesDialog = ({
|
||||
onSaveProject,
|
||||
buildId,
|
||||
game,
|
||||
slug,
|
||||
onClose,
|
||||
onApply,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const { profile } = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const [name, setName] = React.useState<string>(project.getName());
|
||||
const [categories, setCategories] = React.useState<string[]>(
|
||||
project.getCategories().toJSArray()
|
||||
@@ -51,6 +59,12 @@ export const OnlineGamePropertiesDialog = ({
|
||||
const [playWithMobile, setPlayableWithMobile] = React.useState<boolean>(
|
||||
project.isPlayableWithMobile()
|
||||
);
|
||||
const [userSlug, setUserSlug] = React.useState<string>(
|
||||
(slug && slug.username) || (profile && profile.username) || ''
|
||||
);
|
||||
const [gameSlug, setGameSlug] = React.useState<string>(
|
||||
(slug && slug.gameSlug) || cleanUpGameSlug(project.getName())
|
||||
);
|
||||
const [orientation, setOrientation] = React.useState<string>(
|
||||
project.getOrientation()
|
||||
);
|
||||
@@ -78,7 +92,7 @@ export const OnlineGamePropertiesDialog = ({
|
||||
orientation: orientation || 'default',
|
||||
})
|
||||
) {
|
||||
await onApply({ discoverable });
|
||||
await onApply({ discoverable, userSlug, gameSlug });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,6 +145,10 @@ export const OnlineGamePropertiesDialog = ({
|
||||
playWithMobile={playWithMobile}
|
||||
setOrientation={setOrientation}
|
||||
orientation={orientation}
|
||||
userSlug={userSlug}
|
||||
setUserSlug={setUserSlug}
|
||||
gameSlug={gameSlug}
|
||||
setGameSlug={setGameSlug}
|
||||
discoverable={discoverable}
|
||||
setDiscoverable={setDiscoverable}
|
||||
displayThumbnail
|
||||
|
@@ -17,6 +17,7 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import BackgroundText from '../UI/BackgroundText';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import { GameThumbnail } from './GameThumbnail';
|
||||
|
||||
const isCyrillic = (text: string) =>
|
||||
@@ -195,42 +196,68 @@ export function PublicGameProperties({
|
||||
rows={5}
|
||||
/>
|
||||
{setUserSlug && setGameSlug && (
|
||||
<Line>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>User name in the game URL</Trans>}
|
||||
value={userSlug || ''}
|
||||
onChange={(e, i, value: string) => setUserSlug(value)}
|
||||
// It's disabled if one of the condition of SelectOption is false.
|
||||
disabled={
|
||||
!(
|
||||
profile &&
|
||||
profile.username &&
|
||||
userSlug &&
|
||||
(!profile || userSlug !== profile.username)
|
||||
)
|
||||
}
|
||||
>
|
||||
{profile && profile.username && (
|
||||
<SelectOption
|
||||
value={profile.username}
|
||||
primaryText={profile.username}
|
||||
/>
|
||||
)}
|
||||
{userSlug && (!profile || userSlug !== profile.username) && (
|
||||
<SelectOption value={userSlug} primaryText={userSlug} />
|
||||
)}
|
||||
</SelectField>
|
||||
<Spacer />
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Game name in the game URL</Trans>}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={gameSlug || ''}
|
||||
onChange={gameSlug => setGameSlug(cleanUpGameSlug(gameSlug))}
|
||||
autoFocus
|
||||
/>
|
||||
</Line>
|
||||
<>
|
||||
<Line>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>User name in the game URL</Trans>}
|
||||
value={userSlug || ''}
|
||||
onChange={(e, i, value: string) => setUserSlug(value)}
|
||||
// It's disabled if one of the condition of SelectOption is false.
|
||||
disabled={
|
||||
!(
|
||||
profile &&
|
||||
profile.username &&
|
||||
userSlug &&
|
||||
(!profile || userSlug !== profile.username)
|
||||
)
|
||||
}
|
||||
>
|
||||
{profile && profile.username && (
|
||||
<SelectOption
|
||||
value={profile.username}
|
||||
primaryText={profile.username}
|
||||
/>
|
||||
)}
|
||||
{userSlug && (!profile || userSlug !== profile.username) && (
|
||||
<SelectOption value={userSlug} primaryText={userSlug} />
|
||||
)}
|
||||
</SelectField>
|
||||
<Spacer />
|
||||
<SemiControlledTextField
|
||||
disabled={
|
||||
!(
|
||||
userSlug &&
|
||||
userSlug.length &&
|
||||
profile &&
|
||||
profile.username
|
||||
)
|
||||
}
|
||||
floatingLabelText={<Trans>Game name in the game URL</Trans>}
|
||||
fullWidth
|
||||
type="text"
|
||||
value={
|
||||
userSlug && userSlug.length && profile && profile.username
|
||||
? gameSlug || ''
|
||||
: ''
|
||||
}
|
||||
onChange={gameSlug => setGameSlug(cleanUpGameSlug(gameSlug))}
|
||||
autoFocus
|
||||
/>
|
||||
</Line>
|
||||
{!(
|
||||
userSlug &&
|
||||
userSlug.length &&
|
||||
profile &&
|
||||
profile.username
|
||||
) && (
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
Usernames are required to choose a custom game URL.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<UsersAutocomplete
|
||||
userIds={authorIds}
|
||||
|
@@ -16,6 +16,10 @@ export const GDevelopGamesPlatform = {
|
||||
isDev
|
||||
? `https://liluo.io/games/${gameId}?dev=true`
|
||||
: `https://liluo.io/games/${gameId}`,
|
||||
getGameUrlWithSlug: (userSlug: string, gameSlug: string) =>
|
||||
isDev
|
||||
? `https://liluo.io/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}?dev=true`
|
||||
: `https://liluo.io/${userSlug.toLowerCase()}/${gameSlug.toLowerCase()}`,
|
||||
getUserPublicProfileUrl: (userId: string, username: ?string) =>
|
||||
username
|
||||
? `https://liluo.io/${username}${isDev ? '?dev=true' : ''}`
|
||||
|
@@ -38,6 +38,12 @@ export type Game = {
|
||||
discoverable?: boolean,
|
||||
};
|
||||
|
||||
export type GameSlug = {
|
||||
username: string,
|
||||
gameSlug: string,
|
||||
createdAt: number,
|
||||
};
|
||||
|
||||
export type ShowcasedGameLink = {
|
||||
url: string,
|
||||
type:
|
||||
@@ -129,9 +135,11 @@ export const getCategoryName = (category: string, i18n: I18nType) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getGameUrl = (game: ?Game) => {
|
||||
export const getGameUrl = (game: ?Game, slug: ?GameSlug) => {
|
||||
if (!game) return null;
|
||||
return GDevelopGamesPlatform.getGameUrl(game.id);
|
||||
return slug
|
||||
? GDevelopGamesPlatform.getGameUrlWithSlug(slug.username, slug.gameSlug)
|
||||
: GDevelopGamesPlatform.getGameUrl(game.id);
|
||||
};
|
||||
|
||||
export const getAclsFromUserIds = (
|
||||
@@ -374,3 +382,23 @@ export const getPublicGame = (gameId: string): Promise<PublicGame> => {
|
||||
.get(`${GDevelopGameApi.baseUrl}/public-game/${gameId}`)
|
||||
.then(response => response.data);
|
||||
};
|
||||
|
||||
export const getGameSlugs = (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string,
|
||||
gameId: string
|
||||
): Promise<Array<GameSlug>> => {
|
||||
return getAuthorizationHeader()
|
||||
.then(authorizationHeader =>
|
||||
axios.get(`${GDevelopGameApi.baseUrl}/game-slug`, {
|
||||
params: {
|
||||
userId,
|
||||
gameId,
|
||||
},
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
})
|
||||
)
|
||||
.then(response => response.data);
|
||||
};
|
||||
|
Reference in New Issue
Block a user