Skip to content

Commit

Permalink
Allow to open projects from the game dashboard
Browse files Browse the repository at this point in the history
- [x] Display "Open project" button (with arrow if multiples)
- [x] Display cloud/local projects in game dashboard for a game
  - [/] Ensure we always save the game id
  - [x] Adapt the wording if nothing is found.
  • Loading branch information
4ian committed Nov 16, 2024
1 parent dda85cf commit 60924d2
Show file tree
Hide file tree
Showing 26 changed files with 713 additions and 367 deletions.
123 changes: 105 additions & 18 deletions newIDE/app/src/GameDashboard/GameCard.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// @flow
import { Trans } from '@lingui/macro';
import { t, Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';

import {
ColumnStackLayout,
LineStackLayout,
ResponsiveLineStackLayout,
} from '../UI/Layout';
import {
type FileMetadataAndStorageProviderName,
type StorageProvider,
} from '../ProjectsStorage';
import FlatButton from '../UI/FlatButton';
import Text from '../UI/Text';

import { GameThumbnail } from './GameThumbnail';

import {
getGameMainImageUrl,
getGameUrl,
Expand All @@ -28,26 +29,45 @@ import DollarCoin from '../UI/CustomSvgIcons/DollarCoin';
import Cross from '../UI/CustomSvgIcons/Cross';
import Messages from '../UI/CustomSvgIcons/Messages';
import GameLinkAndShareIcons from './GameLinkAndShareIcons';
import {
getStorageProviderByInternalName,
useProjectsListFor,
} from '../MainFrame/EditorContainers/HomePage/CreateSection/utils';
import FlatButtonWithSplitMenu from '../UI/FlatButtonWithSplitMenu';
import useOnResize from '../Utils/UseOnResize';
import useForceUpdate from '../Utils/UseForceUpdate';

const styles = {
buttonsContainer: { display: 'flex', flexShrink: 0 },
iconAndText: { display: 'flex', gap: 2, alignItems: 'flex-start' },
};

type Props = {|
game: Game,
isCurrentGame: boolean,
onOpenGameManager: () => void,
storageProviders: Array<StorageProvider>,
onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|};

export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => {
export const GameCard = ({
storageProviders,
game,
isCurrentGame,
onOpenGameManager,
onOpenProject,
}: Props) => {
useOnResize(useForceUpdate());
const projectsList = useProjectsListFor(game);
const isPublishedOnGdGames = !!game.publicWebBuildId;
const gameUrl = isPublishedOnGdGames ? getGameUrl(game) : null;

const gameThumbnailUrl = React.useMemo(() => getGameMainImageUrl(game), [
game,
]);

const { isMobile } = useResponsiveWindowSize();
const { isMobile, windowSize } = useResponsiveWindowSize();
const isWidthConstrained = windowSize === 'small' || windowSize === 'medium';
const gdevelopTheme = React.useContext(GDevelopThemeContext);

const renderPublicInfo = () => {
Expand Down Expand Up @@ -119,19 +139,86 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => {
gameId={game.id}
thumbnailUrl={gameThumbnailUrl}
background="light"
width={
isMobile
? undefined
: // On medium/large screens, adapt the size to the width of the window.
Math.min(272, Math.max(130, window.innerWidth / 5))
}
/>
);

const renderButtons = () => (
<LineStackLayout noMargin>
<FlatButton
primary
fullWidth
label={<Trans>Manage game</Trans>}
onClick={onOpenGameManager}
/>
</LineStackLayout>
);
const renderButtons = (fullWidth: boolean) => {
return (
<div styles={styles.buttonsContainer}>
<LineStackLayout noMargin>
<FlatButton
primary
fullWidth={fullWidth}
label={
isWidthConstrained ? (
<Trans>Manage</Trans>
) : (
<Trans>Manage game</Trans>
)
}
onClick={onOpenGameManager}
/>
{projectsList.length === 0 ? null : projectsList.length === 1 ? (
<FlatButton
primary
fullWidth={fullWidth}
disabled={isCurrentGame}
label={
isCurrentGame ? (
<Trans>Opened</Trans>
) : isWidthConstrained ? (
<Trans>Open</Trans>
) : (
<Trans>Open project</Trans>
)
}
onClick={() => onOpenProject(projectsList[0])}
/>
) : (
<FlatButtonWithSplitMenu
primary
fullWidth={fullWidth}
disabled={isCurrentGame}
label={
isCurrentGame ? <Trans>Opened</Trans> : <Trans>Open</Trans>
}
onClick={() => onOpenProject(projectsList[0])}
buildMenuTemplate={i18n => [
...projectsList.map(fileMetadataAndStorageProviderName => {
const name =
fileMetadataAndStorageProviderName.fileMetadata.name || '-';
const storageProvider = getStorageProviderByInternalName(
storageProviders,
fileMetadataAndStorageProviderName.storageProviderName
);
return {
label: i18n._(
t`${name} (${
storageProvider ? i18n._(storageProvider.name) : '-'
})`
),
click: () =>
onOpenProject(fileMetadataAndStorageProviderName),
};
}),
{ type: 'separator' },
{
label: i18n._(t`See all in the game dashboard`),
click: onOpenGameManager,
},
]}
/>
)}
</LineStackLayout>
</div>
);
};

const renderShareUrl = (i18n: I18nType) =>
gameUrl ? <GameLinkAndShareIcons url={gameUrl} display="line" /> : null;
Expand All @@ -153,7 +240,7 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => {
{renderPublicInfo()}
</LineStackLayout>
{renderShareUrl(i18n)}
{renderButtons()}
{renderButtons(/*fullWidth=*/ true)}
</ColumnStackLayout>
) : (
<LineStackLayout noMargin>
Expand All @@ -169,7 +256,7 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => {
alignItems="flex-start"
>
{renderTitle(i18n)}
{renderButtons()}
{renderButtons(/*fullWidth=*/ false)}
</LineStackLayout>
{renderPublicInfo()}
{renderShareUrl(i18n)}
Expand Down
4 changes: 4 additions & 0 deletions newIDE/app/src/GameDashboard/GameHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import Edit from '../UI/CustomSvgIcons/Edit';
import GameLinkAndShareIcons from './GameLinkAndShareIcons';
import { CompactToggleField } from '../UI/CompactToggleField';
import { FixedHeightFlexContainer } from '../UI/Grid';
import useOnResize from '../Utils/UseOnResize';
import useForceUpdate from '../Utils/UseForceUpdate';

const styles = {
iconAndText: { display: 'flex', gap: 2, alignItems: 'flex-start' },
Expand All @@ -42,6 +44,7 @@ const GameHeader = ({
gameUrl,
onPublishOnGdGames,
}: Props) => {
useOnResize(useForceUpdate());
const { isMobile } = useResponsiveWindowSize();
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const gameMainImageUrl = getGameMainImageUrl(game);
Expand Down Expand Up @@ -113,6 +116,7 @@ const GameHeader = ({
gameId={game.id}
thumbnailUrl={gameMainImageUrl}
background="medium"
width={Math.min(272, Math.max(150, window.innerWidth / 4.5))}
/>
);

Expand Down
27 changes: 11 additions & 16 deletions newIDE/app/src/GameDashboard/GameThumbnail.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,6 @@ const styles = {
aspectRatio: '16 / 9',
height: 'auto',
justifyContent: 'center',
overflow: 'hidden', // Keep the radius effect.
},
thumbnailContainer: {
height: 153,
width: 272,
overflow: 'hidden', // Keep the radius effect.
},
mobileThumbnailContainer: {
height: 84,
width: 150,
overflow: 'hidden', // Keep the radius effect.
},
thumbnail: {
aspectRatio: '16 / 9',
Expand All @@ -62,6 +51,7 @@ type Props = {|
onGameUpdated?: (updatedGame: Game) => void,
onUpdatingGame?: (isUpdatingGame: boolean) => void,
fullWidthOnMobile?: boolean,
width?: number,
|};

export const GameThumbnail = ({
Expand All @@ -74,6 +64,7 @@ export const GameThumbnail = ({
onUpdatingGame,
background = 'light',
fullWidthOnMobile,
width,
}: Props) => {
const { isMobile, isLandscape } = useResponsiveWindowSize();
const { profile, getAuthorizationHeader } = React.useContext(
Expand Down Expand Up @@ -168,15 +159,19 @@ export const GameThumbnail = ({
}
};

const thumbnailWidth = width ? width : isMobile && !isLandscape ? 150 : 272;
const thumbnailHeight = Math.floor(thumbnailWidth / (16 / 9));

return (
<ColumnStackLayout noMargin alignItems="center">
<Paper
style={{
...(isMobile && !isLandscape
? fullWidthOnMobile
? styles.fullWidthContainer
: styles.mobileThumbnailContainer
: styles.thumbnailContainer),
width: thumbnailWidth,
height: thumbnailHeight,
...(isMobile && !isLandscape && fullWidthOnMobile
? styles.fullWidthContainer
: undefined),
overflow: 'hidden', // Keep the radius effect.
whiteSpace: 'normal',
display: 'flex',
}}
Expand Down
17 changes: 16 additions & 1 deletion newIDE/app/src/GameDashboard/GamesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import BackgroundText from '../UI/BackgroundText';
import SelectOption from '../UI/SelectOption';
import SearchBarSelectField from '../UI/SearchBarSelectField';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import {
type FileMetadataAndStorageProviderName,
type StorageProvider,
} from '../ProjectsStorage';

const pageSize = 10;

Expand Down Expand Up @@ -75,13 +79,22 @@ const getGamesToDisplay = ({
};

type Props = {|
storageProviders: Array<StorageProvider>,
project: ?gdProject,
games: Array<Game>,
onRefreshGames: () => Promise<void>,
onOpenGameId: (gameId: ?string) => void,
onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|};

const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => {
const GamesList = ({
project,
games,
onRefreshGames,
onOpenGameId,
onOpenProject,
storageProviders,
}: Props) => {
const { values, setGamesListOrderBy } = React.useContext(PreferencesContext);
const [searchText, setSearchText] = React.useState<string>('');
const [currentPage, setCurrentPage] = React.useState<number>(0);
Expand Down Expand Up @@ -211,12 +224,14 @@ const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => {
{displayedGames.length > 0 ? (
displayedGames.map(game => (
<GameCard
storageProviders={storageProviders}
key={game.id}
isCurrentGame={!!projectUuid && game.id === projectUuid}
game={game}
onOpenGameManager={() => {
onOpenGameId(game.id);
}}
onOpenProject={onOpenProject}
/>
))
) : !!searchText ? (
Expand Down
36 changes: 36 additions & 0 deletions newIDE/app/src/GameDashboard/Widgets/ProjectsWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import { Trans } from '@lingui/macro';
import { type Game } from '../../Utils/GDevelopServices/Game';
import {
type FileMetadataAndStorageProviderName,
type FileMetadata,
type StorageProvider,
} from '../../ProjectsStorage';
import DashboardWidget from './DashboardWidget';
import ProjectFileList from '../../MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileList';

type Props = {|
game: Game,
onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise<void>,
storageProviders: Array<StorageProvider>,

project: ?gdProject,
currentFileMetadata: ?FileMetadata,
closeProject: () => Promise<void>,
|};

const ProjectsWidget = (props: Props) => {
return (
<I18n>
{({ i18n }) => (
<DashboardWidget gridSize={3} title={<Trans>Projects</Trans>}>
<ProjectFileList {...props} i18n={i18n} />
</DashboardWidget>
)}
</I18n>
);
};

export default ProjectsWidget;
Loading

0 comments on commit 60924d2

Please sign in to comment.