diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js index ecd76a389e..177b0c27dd 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js +++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js @@ -5,6 +5,7 @@ import { type ResourceSource, type ChooseResourceFunction, } from '../../ResourcesList/ResourceSource'; +import type { StorageProvider } from '../../ProjectsStorage'; import { type PreviewDebuggerServer } from '../../Export/PreviewLauncher.flow'; import { type HotReloadPreviewButtonProps } from '../../HotReload/HotReloadPreviewButton'; import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow'; @@ -16,8 +17,12 @@ import { import { type FileMetadataAndStorageProviderName } from '../../ProjectsStorage'; export type EditorContainerExtraProps = {| + // Events function extension editor initiallyFocusedFunctionName?: ?string, initiallyFocusedBehaviorName?: ?string, + + // Homepage + storageProviders?: Array, |}; export type RenderEditorContainerProps = {| diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection.js index 65d43d506f..8d6ee4f8b6 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection.js @@ -3,19 +3,25 @@ import * as React from 'react'; import { Trans, t } from '@lingui/macro'; import { I18n } from '@lingui/react'; import { type I18n as I18nType } from '@lingui/core'; +import { makeStyles } from '@material-ui/core/styles'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import AddIcon from '@material-ui/icons/Add'; import Text from '../../../UI/Text'; import TextButton from '../../../UI/TextButton'; import RaisedButton from '../../../UI/RaisedButton'; +import AlertMessage from '../../../UI/AlertMessage'; import { Line, Column, Spacer } from '../../../UI/Grid'; import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer'; import { LineStackLayout, ResponsiveLineStackLayout } from '../../../UI/Layout'; -import { type FileMetadataAndStorageProviderName } from '../../../ProjectsStorage'; +import { + type FileMetadataAndStorageProviderName, + type StorageProvider, +} from '../../../ProjectsStorage'; import PreferencesContext from '../../Preferences/PreferencesContext'; import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext'; import SectionContainer, { SectionRow } from './SectionContainer'; @@ -25,7 +31,10 @@ import ContextMenu, { import CircularProgress from '../../../UI/CircularProgress'; import { type MenuItemTemplate } from '../../../UI/Menu/Menu.flow'; import useConfirmDialog from '../../../UI/Confirm/useConfirmDialog'; -import { deleteCloudProject } from '../../../Utils/GDevelopServices/Project'; +import { + CLOUD_PROJECT_MAX_COUNT, + deleteCloudProject, +} from '../../../Utils/GDevelopServices/Project'; import optionalRequire from '../../../Utils/OptionalRequire'; import { showErrorBox } from '../../../UI/Messages/MessageBox'; import { getRelativeOrAbsoluteDisplayDate } from '../../../Utils/DateDisplay'; @@ -51,6 +60,7 @@ type Props = {| onOpen: () => void, onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void, onCreateProject: () => void, + storageProviders: Array, |}; export type BuildSectionInterface = {| @@ -71,8 +81,33 @@ const PrettyBreakablePath = ({ path }: {| path: string |}) => { }, []); }; +const getStorageProviderByInternalName = ( + storageProviders: Array, + internalName: string +): ?StorageProvider => { + return storageProviders.find( + storageProvider => storageProvider.internalName === internalName + ); +}; + +const useStylesForListItemIcon = makeStyles({ + root: { + minWidth: 0, + }, +}); + const BuildSection = React.forwardRef( - ({ project, canOpen, onOpen, onCreateProject, onOpenRecentFile }, ref) => { + ( + { + project, + canOpen, + onOpen, + onCreateProject, + onOpenRecentFile, + storageProviders, + }, + ref + ) => { const { getRecentProjectFiles, removeRecentProjectFile } = React.useContext( PreferencesContext ); @@ -83,6 +118,8 @@ const BuildSection = React.forwardRef( const windowWidth = useResponsiveWindowWidth(); const forceUpdate = useForceUpdate(); + const iconClasses = useStylesForListItemIcon(); + React.useImperativeHandle(ref, () => ({ forceUpdate, })); @@ -91,6 +128,8 @@ const BuildSection = React.forwardRef( file => file.fileMetadata ); + let hasTooManyCloudProjects = false; + // Show cloud projects on the web app only. if (isWebApp) { const { cloudProjects } = authenticatedUser; @@ -112,6 +151,9 @@ const BuildSection = React.forwardRef( }) .filter(Boolean) ); + hasTooManyCloudProjects = + cloudProjects.filter(cloudProject => !cloudProject.deletedAt) + .length >= CLOUD_PROJECT_MAX_COUNT; } } @@ -213,7 +255,33 @@ const BuildSection = React.forwardRef( {({ i18n }) => ( <> - My projects}> + My projects} + renderFooter={() => + isWebApp && + hasTooManyCloudProjects && ( + + + + + + You've reached your maximum storage of{' '} + {CLOUD_PROJECT_MAX_COUNT} + cloud-based projects + + + + + To keep using GDevelop cloud, consider deleting one + of your projects. + + + + + + ) + } + > ( File name + + + Location + + Last edited @@ -280,93 +353,136 @@ const BuildSection = React.forwardRef( )} - {projectFiles.map(file => ( - { - onOpenRecentFile(file); - }} - style={styles.listItem} - onContextMenu={event => - openContextMenu(event, file) - } - > - {isWindowWidthMediumOrLarger ? ( - - - - - {file.fileMetadata.name || ( - - )} - - - {pendingProject === - file.fileMetadata.fileIdentifier && ( - <> - - - - )} - - - - {file.fileMetadata.lastModifiedDate && ( - - {getRelativeOrAbsoluteDisplayDate( - i18n, - file.fileMetadata.lastModifiedDate - )} - + {projectFiles.map(file => { + const storageProvider = getStorageProviderByInternalName( + storageProviders, + file.storageProviderName + ); + return ( + { + onOpenRecentFile(file); + }} + style={styles.listItem} + onContextMenu={event => + openContextMenu(event, file) + } + > + <> + {storageProvider && + storageProvider.renderIcon && + !isWindowWidthMediumOrLarger && ( + + {storageProvider.renderIcon({ + size: 'small', + })} + )} - - - ) : ( - - - - ) - } - secondary={ - file.fileMetadata.lastModifiedDate - ? getRelativeOrAbsoluteDisplayDate( + {isWindowWidthMediumOrLarger ? ( + + + + {storageProvider && + storageProvider.renderIcon && ( + <> + {storageProvider.renderIcon({ + size: 'small', + })} + + + )} + + {file.fileMetadata.name || ( + + )} + + + {pendingProject === + file.fileMetadata + .fileIdentifier && ( + <> + + + + )} + + + + + {storageProvider + ? i18n._(storageProvider.name) + : ''} + + + + {file.fileMetadata.lastModifiedDate && ( + + {getRelativeOrAbsoluteDisplayDate( i18n, file.fileMetadata.lastModifiedDate + )} + + )} + + + ) : ( + + + ) - : undefined - } - onContextMenu={event => - openContextMenu(event, file) - } - /> - {pendingProject === - file.fileMetadata.fileIdentifier && ( - - )} - - - )} - - ))} + } + secondary={ + file.fileMetadata.lastModifiedDate + ? getRelativeOrAbsoluteDisplayDate( + i18n, + file.fileMetadata + .lastModifiedDate + ) + : undefined + } + onContextMenu={event => + openContextMenu(event, file) + } + /> + {pendingProject === + file.fileMetadata.fileIdentifier && ( + + )} + + + )} + + + ); + })} diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js index 555885483b..5f6b6b6cd8 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js @@ -20,6 +20,12 @@ const styles = { paddingLeft: SECTION_PADDING, paddingRight: SECTION_PADDING, }, + mobileFooter: { + padding: 5, + }, + desktopFooter: { + paddingLeft: SECTION_PADDING, + }, rowContainer: { display: 'flex', flexDirection: 'column', @@ -37,6 +43,7 @@ type Props = {| subtitle?: React.Node, backAction?: () => void, flexBody?: boolean, + renderFooter?: () => React.Node, |}; const SectionContainer = ({ @@ -45,12 +52,14 @@ const SectionContainer = ({ subtitle, backAction, flexBody, + renderFooter, }: Props) => { const windowWidth = useResponsiveWindowWidth(); const GDevelopTheme = React.useContext(GDevelopThemeContext); return ( + {renderFooter && ( + + {renderFooter()} + + )} ); }; diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index 8717fe1521..a9f99c80bf 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -8,7 +8,10 @@ import { type OnCreateBlankFunction, type OnOpenProjectAfterCreationFunction, } from '../../../ProjectCreation/CreateProjectDialog'; -import { type FileMetadataAndStorageProviderName } from '../../../ProjectsStorage'; +import { + type FileMetadataAndStorageProviderName, + type StorageProvider, +} from '../../../ProjectsStorage'; import GetStartedSection from './GetStartedSection'; import BuildSection, { type BuildSectionInterface } from './BuildSection'; import LearnSection from './LearnSection'; @@ -29,6 +32,7 @@ type Props = {| projectItemName: ?string, project: ?gdProject, setToolbar: (?React.Node) => void, + storageProviders: Array, // Project opening canOpen: boolean, @@ -74,6 +78,7 @@ export const HomePage = React.memo( setToolbar, onOpenOnboardingDialog, isActive, + storageProviders, }: Props, ref ) => { @@ -179,6 +184,7 @@ export const HomePage = React.memo( onOpen={onOpen} onCreateProject={onCreateProject} onOpenRecentFile={onOpenRecentFile} + storageProviders={storageProviders} /> )} {activeTab === 'learn' && ( @@ -225,5 +231,8 @@ export const renderHomePageContainer = ( onOpenLanguageDialog={props.onOpenLanguageDialog} onOpenProfile={props.onOpenProfile} onOpenOnboardingDialog={props.onOpenOnboardingDialog} + storageProviders={ + (props.extraEditorProps && props.extraEditorProps.storageProviders) || [] + } /> ); diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 627e38fd47..507ff0451c 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -1468,11 +1468,14 @@ const MainFrame = (props: Props) => { projectItemName: null, renderEditorContainer: renderHomePageContainer, key: 'start page', + extraEditorProps: { + storageProviders: props.storageProviders, + }, closable: false, }), })); }, - [setState, i18n] + [setState, i18n, props.storageProviders] ); const _openDebugger = React.useCallback( diff --git a/newIDE/app/src/ProjectsStorage/CloudStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/CloudStorageProvider/index.js index f68dc92e69..dbbcbbef52 100644 --- a/newIDE/app/src/ProjectsStorage/CloudStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/CloudStorageProvider/index.js @@ -18,8 +18,8 @@ import Cloud from '../../UI/CustomSvgIcons/Cloud'; export default ({ internalName: 'Cloud', - name: t`GDevelop cloud storage`, - renderIcon: () => , + name: t`GDevelop Cloud`, + renderIcon: props => , hiddenInOpenDialog: true, needUserAuthentication: true, getFileMetadataFromAppArguments: (appArguments: AppArguments) => { diff --git a/newIDE/app/src/ProjectsStorage/DownloadFileStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/DownloadFileStorageProvider/index.js index 909f332e2a..7dec695821 100644 --- a/newIDE/app/src/ProjectsStorage/DownloadFileStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/DownloadFileStorageProvider/index.js @@ -12,7 +12,7 @@ import SaveAlt from '@material-ui/icons/SaveAlt'; export default ({ internalName: 'DownloadFile', name: t`Download a copy`, - renderIcon: () => , + renderIcon: props => , hiddenInOpenDialog: true, createOperations: ({ setDialog, closeDialog }) => ({ onSaveProjectAs: (project: gdProject, fileMetadata: ?FileMetadata) => { diff --git a/newIDE/app/src/ProjectsStorage/DropboxStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/DropboxStorageProvider/index.js index ae8649bb7e..88fb1936d8 100644 --- a/newIDE/app/src/ProjectsStorage/DropboxStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/DropboxStorageProvider/index.js @@ -56,7 +56,7 @@ export default ({ internalName: 'Dropbox', name: t`Dropbox (coming soon)`, disabled: true, - renderIcon: () => , + renderIcon: props => , createOperations: ({ setDialog, closeDialog }) => { initializeApis().catch(() => {}); diff --git a/newIDE/app/src/ProjectsStorage/FakeCloudStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/FakeCloudStorageProvider/index.js index aa12be0354..25bafca209 100644 --- a/newIDE/app/src/ProjectsStorage/FakeCloudStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/FakeCloudStorageProvider/index.js @@ -11,7 +11,7 @@ const FakeCloudStorageProvider = ({ internalName: 'FakeCloud', name: t`GDevelop cloud storage (coming soon)`, disabled: true, - renderIcon: () => , + renderIcon: props => , createOperations: () => { return { doesInitialOpenRequireUserInteraction: true, diff --git a/newIDE/app/src/ProjectsStorage/GoogleDriveStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/GoogleDriveStorageProvider/index.js index 5170af1203..e72198cdef 100644 --- a/newIDE/app/src/ProjectsStorage/GoogleDriveStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/GoogleDriveStorageProvider/index.js @@ -276,7 +276,7 @@ const showFilePicker = ({ export default ({ internalName: 'GoogleDrive', name: t`Google Drive`, - renderIcon: () => , + renderIcon: props => , getFileMetadataFromAppArguments: (appArguments: AppArguments) => { if (appArguments.state) { try { diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/index.js index f16d55822a..78226b234e 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/index.js @@ -28,7 +28,7 @@ import Computer from '../../UI/CustomSvgIcons/Computer'; export default ({ internalName: 'LocalFile', name: t`Your computer`, - renderIcon: () => , + renderIcon: props => , getFileMetadataFromAppArguments: (appArguments: AppArguments) => { if (!appArguments[POSITIONAL_ARGUMENTS_KEY]) return null; if (!appArguments[POSITIONAL_ARGUMENTS_KEY].length) return null; diff --git a/newIDE/app/src/ProjectsStorage/OneDriveStorageProvider/index.js b/newIDE/app/src/ProjectsStorage/OneDriveStorageProvider/index.js index f1ad0c176b..a598cbc88b 100644 --- a/newIDE/app/src/ProjectsStorage/OneDriveStorageProvider/index.js +++ b/newIDE/app/src/ProjectsStorage/OneDriveStorageProvider/index.js @@ -56,7 +56,7 @@ export default ({ internalName: 'OneDrive', name: t`OneDrive (coming soon)`, disabled: true, - renderIcon: () => , + renderIcon: props => , createOperations: ({ setDialog, closeDialog }) => { initializeApis().catch(() => {}); diff --git a/newIDE/app/src/ProjectsStorage/OpenFromStorageProviderDialog.js b/newIDE/app/src/ProjectsStorage/OpenFromStorageProviderDialog.js index 4472714303..096efa8e69 100644 --- a/newIDE/app/src/ProjectsStorage/OpenFromStorageProviderDialog.js +++ b/newIDE/app/src/ProjectsStorage/OpenFromStorageProviderDialog.js @@ -50,7 +50,7 @@ const OpenFromStorageProviderDialog = ({ primaryText={i18n._(storageProvider.name)} leftIcon={ storageProvider.renderIcon - ? storageProvider.renderIcon() + ? storageProvider.renderIcon({}) : undefined } onClick={() => onChooseProvider(storageProvider)} diff --git a/newIDE/app/src/ProjectsStorage/StorageProviderListItem.js b/newIDE/app/src/ProjectsStorage/StorageProviderListItem.js index 7cedc5f9ce..6ec5ebeedd 100644 --- a/newIDE/app/src/ProjectsStorage/StorageProviderListItem.js +++ b/newIDE/app/src/ProjectsStorage/StorageProviderListItem.js @@ -60,12 +60,15 @@ const StorageProviderListItem = ({ > {storageProvider.renderIcon - ? storageProvider.renderIcon() + ? storageProvider.renderIcon({}) : undefined} - {i18n._(storageProvider.name)} + + {i18n._(storageProvider.name)}{' '} + {storageProvider.internalName === 'Cloud' && i18n._(t`(Beta)`)} + {shouldDisplayAuthenticationButtons && ( React.Node, + renderIcon?: ({| size?: 'small' | 'medium' |}) => React.Node, getFileMetadataFromAppArguments?: AppArguments => ?FileMetadata, createOperations: ({ /** Open a dialog (a render function) */ diff --git a/newIDE/app/src/Utils/GDevelopServices/Project.js b/newIDE/app/src/Utils/GDevelopServices/Project.js index 02d335d562..d15dcb7c05 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Project.js +++ b/newIDE/app/src/Utils/GDevelopServices/Project.js @@ -7,6 +7,7 @@ import { import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext'; export const CLOUD_PROJECT_NAME_MAX_LENGTH = 50; +export const CLOUD_PROJECT_MAX_COUNT = 10; const projectResourcesClient = axios.create({ baseURL: GDevelopProjectResourcesStorage.baseUrl, diff --git a/newIDE/app/src/stories/componentStories/HomePage.stories.js b/newIDE/app/src/stories/componentStories/HomePage.stories.js index e538bac4e5..e486a363b2 100644 --- a/newIDE/app/src/stories/componentStories/HomePage.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage.stories.js @@ -18,6 +18,7 @@ import AuthenticatedUserContext, { initialAuthenticatedUser, type AuthenticatedUser, } from '../../Profile/AuthenticatedUserContext'; +import CloudStorageProvider from '../../ProjectsStorage/CloudStorageProvider'; import { fakeIndieAuthenticatedUser, indieUserProfile, @@ -83,6 +84,7 @@ const WrappedHomePage = ({ projectItemName={null} setToolbar={() => {}} canOpen={true} + storageProviders={[CloudStorageProvider]} onOpen={() => action('onOpen')()} onOpenRecentFile={() => action('onOpenRecentFile')()} onCreateProject={() => action('onCreateProject')()}