From 272766c70502c8c737f321d9b1a86af1da4dce97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:07:09 +0100 Subject: [PATCH] Automatically use a screenshot from the latest preview as the game thumbnail if none are set (#7156) --- .../Preferences/PreferencesContext.js | 4 + .../Preferences/PreferencesDialog.js | 9 ++ .../Preferences/PreferencesProvider.js | 13 +++ .../app/src/MainFrame/UseCapturesManager.js | 84 +++++++++++++++++-- newIDE/app/src/MainFrame/index.js | 20 ++++- 5 files changed, 121 insertions(+), 9 deletions(-) diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js b/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js index 4a1457f87fc0..52437f169aec 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js @@ -225,6 +225,7 @@ export type PreferencesValues = {| fetchPlayerTokenForPreviewAutomatically: boolean, previewCrashReportUploadLevel: string, gamesListOrderBy: 'createdAt' | 'totalSessions' | 'weeklySessions', + takeScreenshotOnPreview: boolean, |}; /** @@ -326,6 +327,7 @@ export type Preferences = {| setGamesListOrderBy: ( orderBy: 'createdAt' | 'totalSessions' | 'weeklySessions' ) => void, + setTakeScreenshotOnPreview: (enabled: boolean) => void, |}; export const initialPreferences = { @@ -382,6 +384,7 @@ export const initialPreferences = { fetchPlayerTokenForPreviewAutomatically: true, previewCrashReportUploadLevel: 'exclude-javascript-code-events', gamesListOrderBy: 'createdAt', + takeScreenshotOnPreview: true, }, setLanguage: () => {}, setThemeName: () => {}, @@ -455,6 +458,7 @@ export const initialPreferences = { setGamesListOrderBy: ( orderBy: 'createdAt' | 'totalSessions' | 'weeklySessions' ) => {}, + setTakeScreenshotOnPreview: (enabled: boolean) => {}, }; const PreferencesContext = React.createContext(initialPreferences); diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js b/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js index 4a6570d38857..b574c7b869c4 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js @@ -82,6 +82,7 @@ const PreferencesDialog = ({ setDisplaySaveReminder, setFetchPlayerTokenForPreviewAutomatically, setPreviewCrashReportUploadLevel, + setTakeScreenshotOnPreview, } = React.useContext(PreferencesContext); const initialUse3DEditor = React.useRef(values.use3DEditor); @@ -441,6 +442,14 @@ const PreferencesDialog = ({ Send crash reports during previews to GDevelop } /> + setTakeScreenshotOnPreview(check)} + toggled={values.takeScreenshotOnPreview} + labelPosition="right" + label={ + Automatically take a screenshot in game previews + } + /> setShowDeprecatedInstructionWarning(check)} toggled={values.showDeprecatedInstructionWarning} diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js index 9ba391c3bafd..b6583ab83248 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js @@ -197,6 +197,7 @@ export default class PreferencesProvider extends React.Component { this ), setGamesListOrderBy: this._setGamesListOrderBy.bind(this), + setTakeScreenshotOnPreview: this._setTakeScreenshotOnPreview.bind(this), }; componentDidMount() { @@ -1021,6 +1022,18 @@ export default class PreferencesProvider extends React.Component { ); } + _setTakeScreenshotOnPreview(newValue: boolean) { + this.setState( + state => ({ + values: { + ...state.values, + takeScreenshotOnPreview: newValue, + }, + }), + () => this._persistValuesToLocalStorage(this.state) + ); + } + render() { return ( diff --git a/newIDE/app/src/MainFrame/UseCapturesManager.js b/newIDE/app/src/MainFrame/UseCapturesManager.js index f7f4d7a9c80b..320097389803 100644 --- a/newIDE/app/src/MainFrame/UseCapturesManager.js +++ b/newIDE/app/src/MainFrame/UseCapturesManager.js @@ -5,9 +5,23 @@ import { type LaunchCaptureOptions, type CaptureOptions, } from '../ExportAndShare/PreviewLauncher.flow'; -import { createGameResourceSignedUrls } from '../Utils/GDevelopServices/Game'; - -const useCapturesManager = ({ project }: { project: ?gdProject }) => { +import { + createGameResourceSignedUrls, + updateGame, +} from '../Utils/GDevelopServices/Game'; +import { type GamesList } from '../GameDashboard/UseGamesList'; +import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext'; +import PreferencesContext from './Preferences/PreferencesContext'; + +export const TIME_BETWEEN_PREVIEW_SCREENSHOTS = 1000 * 60 * 3; // 3 minutes + +const useCapturesManager = ({ + project, + gamesList, +}: { + project: ?gdProject, + gamesList: GamesList, +}) => { const [ unverifiedGameScreenshots, setUnverifiedGameScreenshots, @@ -17,6 +31,14 @@ const useCapturesManager = ({ project }: { project: ?gdProject }) => { unverifiedPublicUrl: string, |}> >([]); + const [ + lastPreviewScreenshotsTakenAt, + setLastPreviewScreenshotsTakenAt, + ] = React.useState<{ [projectUuid: string]: number }>({}); + const { getAuthorizationHeader, profile } = React.useContext( + AuthenticatedUserContext + ); + const preferences = React.useContext(PreferencesContext); const createCaptureOptionsForPreview = React.useCallback( async ( @@ -70,6 +92,7 @@ const useCapturesManager = ({ project }: { project: ?gdProject }) => { const onCaptureFinished = React.useCallback( async (captureOptions: CaptureOptions) => { if (!project) return; + const projectId = project.getProjectUuid(); try { const screenshots = captureOptions.screenshots; @@ -101,10 +124,46 @@ const useCapturesManager = ({ project }: { project: ?gdProject }) => { if (!uploadedScreenshotPublicUrls.length) return; + const game = gamesList.games + ? gamesList.games.find(game => game.id === projectId) + : null; + + setLastPreviewScreenshotsTakenAt(lastPreviewScreenshotsTakenAt => ({ + ...lastPreviewScreenshotsTakenAt, + [projectId]: Date.now(), + })); + + // The game is registered, let's update it. + if (game && profile) { + try { + const currentGameScreenshotUrls = game.screenshotUrls || []; + const newGameScreenshotUrls = [ + ...currentGameScreenshotUrls, + ...uploadedScreenshotPublicUrls, + ]; + const updatedGame = await updateGame( + getAuthorizationHeader, + profile.id, + game.id, + { + screenshotUrls: newGameScreenshotUrls, + } + ); + gamesList.onGameUpdated(updatedGame); + } catch (error) { + console.error( + 'Error while updating game with new screenshots:', + error + ); + // Do not throw or save the screenshots. + } + return; + } + setUnverifiedGameScreenshots(unverifiedScreenshots => [ ...unverifiedScreenshots, ...uploadedScreenshotPublicUrls.map(unverifiedPublicUrl => ({ - projectUuid: project.getProjectUuid(), + projectUuid: projectId, unverifiedPublicUrl, })), ]); @@ -112,7 +171,7 @@ const useCapturesManager = ({ project }: { project: ?gdProject }) => { console.error('Error while handling finished capture options:', error); } }, - [project] + [project, gamesList, getAuthorizationHeader, profile] ); const getGameUnverifiedScreenshotUrls = React.useCallback( @@ -138,11 +197,26 @@ const useCapturesManager = ({ project }: { project: ?gdProject }) => { [project] ); + const getHotReloadPreviewLaunchCaptureOptions = React.useCallback( + (gameId: string): LaunchCaptureOptions | void => { + const shouldTakeScreenshotOnPreview = + preferences.values.takeScreenshotOnPreview && + Date.now() > + (lastPreviewScreenshotsTakenAt[gameId] || 0) + + TIME_BETWEEN_PREVIEW_SCREENSHOTS; + return shouldTakeScreenshotOnPreview + ? { screenshots: [{ delayTimeInSeconds: 3000 }] } + : undefined; + }, + [preferences.values.takeScreenshotOnPreview, lastPreviewScreenshotsTakenAt] + ); + return { createCaptureOptionsForPreview, onCaptureFinished, getGameUnverifiedScreenshotUrls, onGameScreenshotsClaimed, + getHotReloadPreviewLaunchCaptureOptions, }; }; diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 6efae7c307f4..b7f4c172d495 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -541,7 +541,8 @@ const MainFrame = (props: Props) => { onCaptureFinished, onGameScreenshotsClaimed, getGameUnverifiedScreenshotUrls, - } = useCapturesManager({ project: currentProject }); + getHotReloadPreviewLaunchCaptureOptions, + } = useCapturesManager({ project: currentProject, gamesList }); /** * This reference is useful to get the current opened project, @@ -1723,14 +1724,25 @@ const MainFrame = (props: Props) => { const launchNewPreview = React.useCallback( async options => { const numberOfWindows = options ? options.numberOfWindows : 1; - launchPreview({ networkPreview: false, numberOfWindows }); + await launchPreview({ networkPreview: false, numberOfWindows }); }, [launchPreview] ); const launchHotReloadPreview = React.useCallback( - () => launchPreview({ networkPreview: false, hotReload: true }), - [launchPreview] + async () => { + const launchCaptureOptions = currentProject + ? getHotReloadPreviewLaunchCaptureOptions( + currentProject.getProjectUuid() + ) + : undefined; + await launchPreview({ + networkPreview: false, + hotReload: true, + launchCaptureOptions, + }); + }, + [currentProject, launchPreview, getHotReloadPreviewLaunchCaptureOptions] ); const launchNetworkPreview = React.useCallback(